dréclaw is a personal Cloudflare-first Telegram AI assistant.
- Telegram private chat-only, single-user (me)
- Chat SDK + Telegram adapter runtime
- Commands:
/help,/status,/model,/new,/reset,/factory-reset,/stop,/verbose,/thinking,/reasoning,/google ... - Core tool:
codemode - Persistent memory: D1 episodic/fact memory + Vectorize semantic recall
- Hybrid memory pipeline: D1 episodic/fact memory + Vectorize semantic recall (Workers AI embeddings)
- AI SDK provider switch:
opencode,opencode-go, orworkers(Workers AI)
flowchart TD
U[Telegram Chat] --> W[Worker Gateway]
W --> C[Chat SDK]
C --> M[Model Loop]
M --> MM[Memory: episodes/facts]
M --> T[Tool: codemode]
C --> W --> U
- Worker routes Telegram webhooks into Chat SDK.
- Chat SDK handles Telegram transport, subscriptions, streaming, and locking via a D1-backed state adapter.
- Runtime retrieves memory context (hybrid semantic + lexical + recency) and injects it into the system prompt.
- Agent loop runs on AI SDK
ToolLoopAgentwith runtime-managed memory persistence. - OpenCode uses
AI_PROVIDER=opencode(Zen default URL) orAI_PROVIDER=opencode-go(Go default URL). - Workers AI runs via
workers-ai-providerbinding (env.AI) whenAI_PROVIDER=workers. - Agent can run sandboxed JS with
codemode.
- Cloudflare account
- Telegram bot
vpand a Node.js runtime managed by Vite+
Copy .env.example to .env and fill values.
Create local Wrangler config from template (not committed):
cp wrangler.toml.example wrangler.tomlThen set your own Cloudflare resource IDs in wrangler.toml.
Also set route in wrangler.toml.
Set Worker secret:
vp run cf:secrets:syncThis loads .env automatically and syncs the selected vars as Worker secrets (TELEGRAM_*, AI_PROVIDER, OPENCODE_API_KEY, GOOGLE_OAUTH_*, MODEL, BASE_URL).
Route is read from wrangler.toml:
vp run cf:deploy- Message the bot in a private Telegram chat.
/helplists commands./statusshows model/provider/memory/google/verbose status./modellists aliases includingglm,workers-kimi,kimi,fireworks-kimi, andfireworks-minimax./newstarts a fresh session and keeps thread settings./resetclears conversation context and restores chat defaults./factory-resetclears conversation, memory, runtime state, and workspace files./stopcooperatively stops the current run./verbose on|offtoggles tool traces, including codemode code, writes, and result summaries./thinking on|offcontrols model thinking effort for the chat./reasoning on|offtoggles visible reasoning text when available./google connectstarts Google OAuth linking flow./google statusshows current Google link status and scopes./google disconnectremoves stored Google OAuth token.
Normal text messages stream a single assistant reply per turn.
- Testing policy lives in
docs/testing.md. - Requirement tests are promoted intentionally under
test/requirements/.... - Automated tests are split between
test/requirements/...andtest/supporting/.... - Run full tests:
vp test - Run supporting tests only:
vp run test:supporting - Run requirement tests only:
vp run test:requirements - Check format, lint, and types:
vp check - Run live model smoke test (real OpenCode Go + tool loop):
vp run smoke:live -- --prompt "hey" - Run Telegram live test via GramJS:
vp run live:telegram -- --prompt "hey" - Run staging long-run Cloudflare verification:
vp run live:telegram:long-run -- --bot <staging-bot> --timeout-ms 120000 --scenario-secret <secret> - Run pre-deploy gate:
vp run cf:verify:predeploy
- Uses a real Telegram user account via GramJS, not Telegram Web.
- Add local-only env vars:
TELEGRAM_TEST_API_ID,TELEGRAM_TEST_API_HASH,TELEGRAM_TEST_BOT_USERNAME,TELEGRAM_TEST_SESSION. - Get
TELEGRAM_TEST_API_IDandTELEGRAM_TEST_API_HASHfromhttps://my.telegram.org/apps. - First-time login:
vp run live:telegram -- --loginand save the printed session string into.envasTELEGRAM_TEST_SESSION. - Keep these values local only; do not sync them as Worker secrets.
- This check is manual and intended for a dedicated staging bot/worker, not production.
- Enable the scenario only in staging by setting
LIVE_TEST_SCENARIOS_ENABLED=trueand a Worker secretLIVE_TEST_SCENARIO_SECRET. - The scenario does not use a real LLM. It keeps the workflow-backed run alive for a controlled duration and verifies that a second queued turn still completes afterward.
- Example:
vp run live:telegram:long-run -- \
--bot dreclaw-staging-bot \
--timeout-ms 120000 \
--scenario-secret "$LIVE_TEST_SCENARIO_SECRET"- Expected pass conditions:
/statusreportsbusy: yeswhile the first run is active- the second normal message is queued, not rejected with the busy message
- both deterministic completion markers arrive in order
- Conversation history and bot state live in Chat SDK thread state backed by D1.
- Long-term memory facts/episodes live in D1 + Vectorize (
VECTORIZE_MEMORY) with Workers AI embeddings (env.AI). - Memory writes are salience-gated and consolidated through reflection.
codemoderuns JavaScript in a sandboxed dynamic Worker powered by@cloudflare/codemode.- Filesystem and workspace state are provided by
@cloudflare/shellviastate.*. codemodeexposesstate.*,web.fetch,memory.find/save/remove,google.execute,reminders.query/update, andskills.list/load.- Built-in skills live under
/skills/system/*; user skills live under/skills/user/*. - Workspace writes are durable and traced through the state backend.
- Configure Google OAuth app as Web application in Google Cloud Console.
- Add redirect URI:
https://<worker-host>/google/oauth/callback. - Set
GOOGLE_OAUTH_*values in.envand sync secrets. - Recommended full-access scope set for this project: Gmail (
https://mail.google.com/), Sheets (https://www.googleapis.com/auth/spreadsheets), Docs (https://www.googleapis.com/auth/documents), Calendar (https://www.googleapis.com/auth/calendar), Drive (https://www.googleapis.com/auth/drive). - In Telegram, run
/google connect, open link, approve scopes.
const messages = await google.execute({
service: "gmail",
version: "v1",
method: "users.messages.list",
params: { userId: "me", maxResults: 5, q: "is:unread" },
});
messages;await google.execute({
service: "sheets",
version: "v4",
method: "spreadsheets.values.update",
params: {
spreadsheetId: input.sheetId,
range: "Sheet1!A1:B2",
valueInputOption: "RAW",
},
body: {
values: [
["name", "value"],
["demo", "1"],
],
},
});OPENCODE_API_KEYis stored as Worker secret./statusreports readiness only (no secrets).
See docs/security.md.