@dreb/telegram 1.16.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (82) hide show
  1. package/README.md +91 -0
  2. package/dist/agent-bridge.d.ts +146 -0
  3. package/dist/agent-bridge.d.ts.map +1 -0
  4. package/dist/agent-bridge.js +466 -0
  5. package/dist/agent-bridge.js.map +1 -0
  6. package/dist/bot.d.ts +11 -0
  7. package/dist/bot.d.ts.map +1 -0
  8. package/dist/bot.js +112 -0
  9. package/dist/bot.js.map +1 -0
  10. package/dist/bridge-lifecycle.d.ts +17 -0
  11. package/dist/bridge-lifecycle.d.ts.map +1 -0
  12. package/dist/bridge-lifecycle.js +71 -0
  13. package/dist/bridge-lifecycle.js.map +1 -0
  14. package/dist/commands/agent.d.ts +11 -0
  15. package/dist/commands/agent.d.ts.map +1 -0
  16. package/dist/commands/agent.js +171 -0
  17. package/dist/commands/agent.js.map +1 -0
  18. package/dist/commands/buddy.d.ts +20 -0
  19. package/dist/commands/buddy.d.ts.map +1 -0
  20. package/dist/commands/buddy.js +84 -0
  21. package/dist/commands/buddy.js.map +1 -0
  22. package/dist/commands/core.d.ts +13 -0
  23. package/dist/commands/core.d.ts.map +1 -0
  24. package/dist/commands/core.js +107 -0
  25. package/dist/commands/core.js.map +1 -0
  26. package/dist/commands/index.d.ts +16 -0
  27. package/dist/commands/index.d.ts.map +1 -0
  28. package/dist/commands/index.js +132 -0
  29. package/dist/commands/index.js.map +1 -0
  30. package/dist/commands/refresh.d.ts +18 -0
  31. package/dist/commands/refresh.d.ts.map +1 -0
  32. package/dist/commands/refresh.js +55 -0
  33. package/dist/commands/refresh.js.map +1 -0
  34. package/dist/commands/sessions.d.ts +10 -0
  35. package/dist/commands/sessions.d.ts.map +1 -0
  36. package/dist/commands/sessions.js +125 -0
  37. package/dist/commands/sessions.js.map +1 -0
  38. package/dist/commands/skills.d.ts +10 -0
  39. package/dist/commands/skills.d.ts.map +1 -0
  40. package/dist/commands/skills.js +48 -0
  41. package/dist/commands/skills.js.map +1 -0
  42. package/dist/config.d.ts +30 -0
  43. package/dist/config.d.ts.map +1 -0
  44. package/dist/config.js +77 -0
  45. package/dist/config.js.map +1 -0
  46. package/dist/handlers/buddy.d.ts +31 -0
  47. package/dist/handlers/buddy.d.ts.map +1 -0
  48. package/dist/handlers/buddy.js +126 -0
  49. package/dist/handlers/buddy.js.map +1 -0
  50. package/dist/handlers/events.d.ts +65 -0
  51. package/dist/handlers/events.d.ts.map +1 -0
  52. package/dist/handlers/events.js +381 -0
  53. package/dist/handlers/events.js.map +1 -0
  54. package/dist/handlers/file.d.ts +11 -0
  55. package/dist/handlers/file.d.ts.map +1 -0
  56. package/dist/handlers/file.js +138 -0
  57. package/dist/handlers/file.js.map +1 -0
  58. package/dist/handlers/message.d.ts +34 -0
  59. package/dist/handlers/message.d.ts.map +1 -0
  60. package/dist/handlers/message.js +262 -0
  61. package/dist/handlers/message.js.map +1 -0
  62. package/dist/index.d.ts +8 -0
  63. package/dist/index.d.ts.map +1 -0
  64. package/dist/index.js +82 -0
  65. package/dist/index.js.map +1 -0
  66. package/dist/state.d.ts +11 -0
  67. package/dist/state.d.ts.map +1 -0
  68. package/dist/state.js +47 -0
  69. package/dist/state.js.map +1 -0
  70. package/dist/types.d.ts +50 -0
  71. package/dist/types.d.ts.map +1 -0
  72. package/dist/types.js +5 -0
  73. package/dist/types.js.map +1 -0
  74. package/dist/util/files.d.ts +27 -0
  75. package/dist/util/files.d.ts.map +1 -0
  76. package/dist/util/files.js +75 -0
  77. package/dist/util/files.js.map +1 -0
  78. package/dist/util/telegram.d.ts +60 -0
  79. package/dist/util/telegram.d.ts.map +1 -0
  80. package/dist/util/telegram.js +192 -0
  81. package/dist/util/telegram.js.map +1 -0
  82. package/package.json +49 -0
@@ -0,0 +1,34 @@
1
+ /**
2
+ * Message handler — sends user messages directly to the agent.
3
+ *
4
+ * Two paths (matching TUI parity):
5
+ * - **Steering**: agent is streaming → inject mid-run via steer()
6
+ * - **Normal**: agent is idle → create display, fire prompt, return
7
+ *
8
+ * Architecture:
9
+ * - Event chain processes events and pushes text to userState.outbox (never blocks on Telegram I/O)
10
+ * - Delivery loop drains outbox to Telegram independently, with retries on failure
11
+ * - Persistent event subscription per bridge handles DONE markers and session persistence
12
+ */
13
+ import type { Api } from "grammy";
14
+ import type { UserState } from "../types.js";
15
+ /** Push a message to the outbox and kick the delivery loop */
16
+ export declare function enqueueSend(api: Api, userState: UserState, chatId: number, text: string, long?: boolean): void;
17
+ /**
18
+ * Send a prompt to the agent. If the agent is streaming, steers instead.
19
+ *
20
+ * Returns immediately — the persistent subscription handles event delivery.
21
+ */
22
+ export declare function sendPrompt(api: Api, userState: UserState, opts: {
23
+ chatId: number;
24
+ replyToId: number;
25
+ userId?: number;
26
+ prompt: string;
27
+ images?: Array<{
28
+ type: "image";
29
+ data: string;
30
+ mimeType: string;
31
+ }>;
32
+ statusMessageId: number | null;
33
+ }): void;
34
+ //# sourceMappingURL=message.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"message.d.ts","sourceRoot":"","sources":["../../src/handlers/message.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;GAWG;AAEH,OAAO,KAAK,EAAE,GAAG,EAAE,MAAM,QAAQ,CAAC;AAIlC,OAAO,KAAK,EAAE,SAAS,EAAE,MAAM,aAAa,CAAC;AA0D7C,8DAA8D;AAC9D,wBAAgB,WAAW,CAAC,GAAG,EAAE,GAAG,EAAE,SAAS,EAAE,SAAS,EAAE,MAAM,EAAE,MAAM,EAAE,IAAI,EAAE,MAAM,EAAE,IAAI,CAAC,EAAE,OAAO,GAAG,IAAI,CAG9G;AAgID;;;;GAIG;AACH,wBAAgB,UAAU,CACzB,GAAG,EAAE,GAAG,EACR,SAAS,EAAE,SAAS,EACpB,IAAI,EAAE;IACL,MAAM,EAAE,MAAM,CAAC;IACf,SAAS,EAAE,MAAM,CAAC;IAClB,MAAM,CAAC,EAAE,MAAM,CAAC;IAChB,MAAM,EAAE,MAAM,CAAC;IACf,MAAM,CAAC,EAAE,KAAK,CAAC;QAAE,IAAI,EAAE,OAAO,CAAC;QAAC,IAAI,EAAE,MAAM,CAAC;QAAC,QAAQ,EAAE,MAAM,CAAA;KAAE,CAAC,CAAC;IAClE,eAAe,EAAE,MAAM,GAAG,IAAI,CAAC;CAC/B,GACC,IAAI,CA6EN","sourcesContent":["/**\n * Message handler — sends user messages directly to the agent.\n *\n * Two paths (matching TUI parity):\n * - **Steering**: agent is streaming → inject mid-run via steer()\n * - **Normal**: agent is idle → create display, fire prompt, return\n *\n * Architecture:\n * - Event chain processes events and pushes text to userState.outbox (never blocks on Telegram I/O)\n * - Delivery loop drains outbox to Telegram independently, with retries on failure\n * - Persistent event subscription per bridge handles DONE markers and session persistence\n */\n\nimport type { Api } from \"grammy\";\nimport type { AgentBridge } from \"../agent-bridge.js\";\nimport { ensureBuddyController } from \"../commands/buddy.js\";\nimport { setUserSession } from \"../state.js\";\nimport type { UserState } from \"../types.js\";\nimport { cleanupUploads } from \"../util/files.js\";\nimport { log, safeDelete, safeSend, sendLong } from \"../util/telegram.js\";\nimport { createEventDisplay, type EventDisplayState, handleAgentEvent } from \"./events.js\";\n\n// ---------------------------------------------------------------------------\n// Delivery loop — drains userState.outbox to Telegram, retries on failure\n// ---------------------------------------------------------------------------\n\nconst MAX_RETRIES = 5;\nconst RETRY_DELAY = 2_000;\n\n/** Track which users have an active delivery loop */\nconst activeDeliveryLoops = new WeakSet<UserState>();\n\n/**\n * Start draining the outbox. Runs independently of the event chain.\n * Self-terminates when the outbox is empty; restarts on next push.\n */\nasync function drainOutbox(api: Api, userState: UserState): Promise<void> {\n\tif (activeDeliveryLoops.has(userState)) return; // already draining\n\tactiveDeliveryLoops.add(userState);\n\n\ttry {\n\t\twhile (userState.outbox.length > 0) {\n\t\t\tconst item = userState.outbox[0];\n\t\t\titem.retries = (item.retries ?? 0) + 1;\n\n\t\t\tlet success: boolean;\n\t\t\tif (item.long) {\n\t\t\t\t// sendLong returns remaining undelivered text (empty = all delivered).\n\t\t\t\t// On partial failure, update item.text to only the undelivered tail\n\t\t\t\t// so retries don't resend already-delivered chunks.\n\t\t\t\tconst remaining = await sendLong(api, item.chatId, item.text);\n\t\t\t\tsuccess = remaining === \"\";\n\t\t\t\tif (!success) item.text = remaining;\n\t\t\t} else {\n\t\t\t\tconst msgId = await safeSend(api, item.chatId, item.text);\n\t\t\t\tsuccess = msgId !== 0;\n\t\t\t}\n\n\t\t\tif (success) {\n\t\t\t\tuserState.outbox.shift();\n\t\t\t} else if (item.retries >= MAX_RETRIES) {\n\t\t\t\tlog(`[DELIVER] Giving up on message after ${MAX_RETRIES} retries: ${item.text.slice(0, 100)}`);\n\t\t\t\tuserState.outbox.shift();\n\t\t\t} else {\n\t\t\t\t// Retry after delay\n\t\t\t\tawait new Promise((r) => setTimeout(r, RETRY_DELAY));\n\t\t\t}\n\t\t}\n\t} catch (e) {\n\t\tlog(`[DELIVER] Drain error: ${e}`);\n\t} finally {\n\t\tactiveDeliveryLoops.delete(userState);\n\t}\n}\n\n/** Push a message to the outbox and kick the delivery loop */\nexport function enqueueSend(api: Api, userState: UserState, chatId: number, text: string, long?: boolean): void {\n\tuserState.outbox.push({ chatId, text, long });\n\tvoid drainOutbox(api, userState);\n}\n\n// ---------------------------------------------------------------------------\n// Persistent event subscription\n// ---------------------------------------------------------------------------\n\n/** Track which bridges have a persistent event subscription */\nconst subscribedBridges = new WeakSet<AgentBridge>();\n\n/** Current display state per user — swapped on each new prompt */\nconst displays = new Map<UserState, EventDisplayState>();\n\n/** Track userId for session persistence (set by sendPrompt, used by completion handler) */\nconst userIds = new Map<UserState, number>();\n\n/**\n * Ensure a persistent event subscription exists for this bridge.\n * Called once per bridge lifetime — handles all event delivery,\n * DONE markers, session persistence, and cleanup.\n */\nfunction ensureSubscribed(api: Api, userState: UserState, bridge: AgentBridge): void {\n\tif (subscribedBridges.has(bridge)) return;\n\tsubscribedBridges.add(bridge);\n\n\t// New bridge — clear stale state from any previous bridge\n\tuserState.promptInFlight = false;\n\tdisplays.delete(userState);\n\tuserState.buddyController?.reset();\n\n\tlet eventChain = Promise.resolve();\n\tlet parentAgentDone = false; // true after agent_end fires (even if BG agents still running)\n\tlet completionFired = false; // prevents double DONE when agent_end and background_agent_end race\n\n\t/** Chain the completion sequence — session persist, cleanup, DONE marker */\n\tfunction chainCompletion(display: EventDisplayState) {\n\t\tif (completionFired) return;\n\t\tcompletionFired = true;\n\t\teventChain = eventChain\n\t\t\t.then(async () => {\n\t\t\t\t// Persist session\n\t\t\t\tif (bridge.isAlive) {\n\t\t\t\t\ttry {\n\t\t\t\t\t\tawait bridge.refreshSessionInfo();\n\t\t\t\t\t\tconst userId = userIds.get(userState);\n\t\t\t\t\t\tif (bridge.sessionFile && userId) {\n\t\t\t\t\t\t\tsetUserSession(userId, bridge.sessionFile);\n\t\t\t\t\t\t}\n\t\t\t\t\t} catch (e) {\n\t\t\t\t\t\tlog(`[EVENT] Session refresh error: ${e}`);\n\t\t\t\t\t}\n\t\t\t\t}\n\n\t\t\t\tcleanupUploads();\n\n\t\t\t\t// DONE marker — only when truly idle and not stopped.\n\t\t\t\t// Brief delay lets in-flight delivery (outbox drain) settle\n\t\t\t\t// before DONE appears in the chat.\n\t\t\t\tif (!bridge.isStreaming && !userState.stopRequested) {\n\t\t\t\t\tawait new Promise((r) => setTimeout(r, 150));\n\t\t\t\t\tenqueueSend(api, userState, display.chatId, \"🦀 _dreb DONE_\");\n\t\t\t\t}\n\t\t\t})\n\t\t\t.catch((e) => log(`[EVENT] Completion error: ${e}`));\n\t}\n\n\tbridge.onEvent((event) => {\n\t\t// Capture display at event arrival time — even if a new prompt\n\t\t// replaces the display later, this event uses the correct one.\n\t\tconst display = displays.get(userState);\n\t\tif (!display) {\n\t\t\tlog(`[EVENT] No display for event: ${event.type} — dropped`);\n\t\t\treturn;\n\t\t}\n\n\t\t// Clear promptInFlight on first event after a prompt\n\t\tif (userState.promptInFlight) {\n\t\t\tuserState.promptInFlight = false;\n\t\t}\n\n\t\t// Build a send callback that pushes to the outbox — captures chatId\n\t\t// at event arrival time (same snapshot as display).\n\t\tconst send = (text: string, long?: boolean) => {\n\t\t\tenqueueSend(api, userState, display.chatId, text, long);\n\t\t};\n\n\t\t// Queue event processing — the chain never blocks on Telegram I/O.\n\t\t// handleAgentEvent pushes permanent messages to outbox via send(),\n\t\t// only ephemeral operations (status edits, file sends) go inline.\n\t\teventChain = eventChain\n\t\t\t.then(() => handleAgentEvent(send, api, display, event))\n\t\t\t.catch((e) => log(`[EVENT] Error: ${e}`));\n\n\t\t// Handle turn completion — checks run INSIDE the eventChain so they\n\t\t// execute after handleAgentEvent has updated display state (e.g.\n\t\t// retryInProgress set by auto_retry_start that arrives right after agent_end).\n\t\tif (event.type === \"agent_end\") {\n\t\t\tparentAgentDone = true;\n\t\t\teventChain = eventChain.then(() => {\n\t\t\t\tif (userState.backgroundAgents.size > 0 || display.retryInProgress || display.pendingRetry) {\n\t\t\t\t\treturn;\n\t\t\t\t}\n\t\t\t\tchainCompletion(display);\n\t\t\t});\n\t\t}\n\n\t\t// Re-trigger completion when the last BG agent finishes.\n\t\t// agent_end already fired but skipped completion because BG agents were running.\n\t\t// Now that the last one is done, finalize.\n\t\tif (event.type === \"background_agent_end\") {\n\t\t\teventChain = eventChain.then(() => {\n\t\t\t\tif (parentAgentDone && userState.backgroundAgents.size === 0 && !bridge.isStreaming) {\n\t\t\t\t\tchainCompletion(display);\n\t\t\t\t}\n\t\t\t});\n\t\t}\n\n\t\t// Reset state when a new run starts (e.g. user sends another message)\n\t\tif (event.type === \"agent_start\") {\n\t\t\tparentAgentDone = false;\n\t\t\tcompletionFired = false;\n\t\t}\n\t});\n}\n\n// ---------------------------------------------------------------------------\n// sendPrompt — steering vs normal path\n// ---------------------------------------------------------------------------\n\n/**\n * Send a prompt to the agent. If the agent is streaming, steers instead.\n *\n * Returns immediately — the persistent subscription handles event delivery.\n */\nexport function sendPrompt(\n\tapi: Api,\n\tuserState: UserState,\n\topts: {\n\t\tchatId: number;\n\t\treplyToId: number;\n\t\tuserId?: number;\n\t\tprompt: string;\n\t\timages?: Array<{ type: \"image\"; data: string; mimeType: string }>;\n\t\tstatusMessageId: number | null;\n\t},\n): void {\n\tconst bridge = userState.bridge;\n\tif (!bridge) {\n\t\tlog(\"[PROMPT] No bridge available\");\n\t\tif (opts.statusMessageId) {\n\t\t\tvoid safeDelete(api, opts.chatId, opts.statusMessageId);\n\t\t}\n\t\tvoid safeSend(api, opts.chatId, \"❌ No agent connection. Try sending your message again.\");\n\t\treturn;\n\t}\n\n\t// Ensure persistent subscription exists for this bridge\n\tensureSubscribed(api, userState, bridge);\n\n\t// Buddy: auto-init controller (loads from shared buddy.json)\n\tif (!userState.buddyController) {\n\t\tensureBuddyController(api, userState, opts.chatId, userState.config);\n\t}\n\n\t// Buddy: capture context, reset idle, and check for name-call interception.\n\t// processUserMessage returns true if name-call detected (buddy handles it).\n\tconst isNameCall = userState.buddyController?.processUserMessage(opts.prompt);\n\tif (isNameCall) {\n\t\t// Clean up the status message (won't be needed — buddy responds instead)\n\t\tif (opts.statusMessageId) {\n\t\t\tvoid safeDelete(api, opts.chatId, opts.statusMessageId);\n\t\t}\n\t\treturn;\n\t}\n\n\t// Steering path — agent is actively streaming (same check as TUI).\n\t// promptInFlight covers the race window between prompt() and agent_start.\n\tif (bridge.isStreaming || userState.promptInFlight) {\n\t\tuserState.stopRequested = false;\n\t\tif (opts.statusMessageId) {\n\t\t\tvoid safeDelete(api, opts.chatId, opts.statusMessageId);\n\t\t}\n\t\tbridge\n\t\t\t.steer(opts.prompt, opts.images)\n\t\t\t.then(() => safeSend(api, opts.chatId, `↩️ _Steering:_ ${opts.prompt.slice(0, 200)}`))\n\t\t\t.catch((e) => {\n\t\t\t\tconst msg = e instanceof Error ? e.message : String(e);\n\t\t\t\tif (!userState.stopRequested) {\n\t\t\t\t\tvoid safeSend(api, opts.chatId, `❌ Steering error: ${msg.slice(0, 200)}`);\n\t\t\t\t}\n\t\t\t});\n\t\treturn;\n\t}\n\n\t// Normal path — create new display, fire prompt, return immediately\n\tuserState.promptInFlight = true;\n\tuserState.stopRequested = false;\n\tif (opts.userId) userIds.set(userState, opts.userId);\n\n\tconst display = createEventDisplay(api, opts.chatId, opts.replyToId, opts.statusMessageId);\n\tdisplay.buddyController = userState.buddyController;\n\n\t// Sync BG agent state — the display needs to know about running agents\n\t// so events.ts doesn't prematurely finalize on agent_end (flush editor,\n\t// delete status, set done). Without this, a new display created while\n\t// BG agents are running would have an empty backgroundAgents map,\n\t// causing events.ts to think the turn is over.\n\tfor (const [id, agent] of userState.backgroundAgents) {\n\t\tdisplay.backgroundAgents.set(id, agent);\n\t}\n\n\tdisplays.set(userState, display);\n\n\tbridge.prompt(opts.prompt, opts.images).catch((e) => {\n\t\tconst msg = e instanceof Error ? e.message : String(e);\n\t\tif (!userState.stopRequested) {\n\t\t\tvoid safeSend(api, opts.chatId, `❌ Error: ${msg.slice(0, 200)}`);\n\t\t}\n\t\tuserState.promptInFlight = false;\n\t\tdisplays.delete(userState);\n\t\tcleanupUploads();\n\t});\n}\n"]}
@@ -0,0 +1,262 @@
1
+ /**
2
+ * Message handler — sends user messages directly to the agent.
3
+ *
4
+ * Two paths (matching TUI parity):
5
+ * - **Steering**: agent is streaming → inject mid-run via steer()
6
+ * - **Normal**: agent is idle → create display, fire prompt, return
7
+ *
8
+ * Architecture:
9
+ * - Event chain processes events and pushes text to userState.outbox (never blocks on Telegram I/O)
10
+ * - Delivery loop drains outbox to Telegram independently, with retries on failure
11
+ * - Persistent event subscription per bridge handles DONE markers and session persistence
12
+ */
13
+ import { ensureBuddyController } from "../commands/buddy.js";
14
+ import { setUserSession } from "../state.js";
15
+ import { cleanupUploads } from "../util/files.js";
16
+ import { log, safeDelete, safeSend, sendLong } from "../util/telegram.js";
17
+ import { createEventDisplay, handleAgentEvent } from "./events.js";
18
+ // ---------------------------------------------------------------------------
19
+ // Delivery loop — drains userState.outbox to Telegram, retries on failure
20
+ // ---------------------------------------------------------------------------
21
+ const MAX_RETRIES = 5;
22
+ const RETRY_DELAY = 2_000;
23
+ /** Track which users have an active delivery loop */
24
+ const activeDeliveryLoops = new WeakSet();
25
+ /**
26
+ * Start draining the outbox. Runs independently of the event chain.
27
+ * Self-terminates when the outbox is empty; restarts on next push.
28
+ */
29
+ async function drainOutbox(api, userState) {
30
+ if (activeDeliveryLoops.has(userState))
31
+ return; // already draining
32
+ activeDeliveryLoops.add(userState);
33
+ try {
34
+ while (userState.outbox.length > 0) {
35
+ const item = userState.outbox[0];
36
+ item.retries = (item.retries ?? 0) + 1;
37
+ let success;
38
+ if (item.long) {
39
+ // sendLong returns remaining undelivered text (empty = all delivered).
40
+ // On partial failure, update item.text to only the undelivered tail
41
+ // so retries don't resend already-delivered chunks.
42
+ const remaining = await sendLong(api, item.chatId, item.text);
43
+ success = remaining === "";
44
+ if (!success)
45
+ item.text = remaining;
46
+ }
47
+ else {
48
+ const msgId = await safeSend(api, item.chatId, item.text);
49
+ success = msgId !== 0;
50
+ }
51
+ if (success) {
52
+ userState.outbox.shift();
53
+ }
54
+ else if (item.retries >= MAX_RETRIES) {
55
+ log(`[DELIVER] Giving up on message after ${MAX_RETRIES} retries: ${item.text.slice(0, 100)}`);
56
+ userState.outbox.shift();
57
+ }
58
+ else {
59
+ // Retry after delay
60
+ await new Promise((r) => setTimeout(r, RETRY_DELAY));
61
+ }
62
+ }
63
+ }
64
+ catch (e) {
65
+ log(`[DELIVER] Drain error: ${e}`);
66
+ }
67
+ finally {
68
+ activeDeliveryLoops.delete(userState);
69
+ }
70
+ }
71
+ /** Push a message to the outbox and kick the delivery loop */
72
+ export function enqueueSend(api, userState, chatId, text, long) {
73
+ userState.outbox.push({ chatId, text, long });
74
+ void drainOutbox(api, userState);
75
+ }
76
+ // ---------------------------------------------------------------------------
77
+ // Persistent event subscription
78
+ // ---------------------------------------------------------------------------
79
+ /** Track which bridges have a persistent event subscription */
80
+ const subscribedBridges = new WeakSet();
81
+ /** Current display state per user — swapped on each new prompt */
82
+ const displays = new Map();
83
+ /** Track userId for session persistence (set by sendPrompt, used by completion handler) */
84
+ const userIds = new Map();
85
+ /**
86
+ * Ensure a persistent event subscription exists for this bridge.
87
+ * Called once per bridge lifetime — handles all event delivery,
88
+ * DONE markers, session persistence, and cleanup.
89
+ */
90
+ function ensureSubscribed(api, userState, bridge) {
91
+ if (subscribedBridges.has(bridge))
92
+ return;
93
+ subscribedBridges.add(bridge);
94
+ // New bridge — clear stale state from any previous bridge
95
+ userState.promptInFlight = false;
96
+ displays.delete(userState);
97
+ userState.buddyController?.reset();
98
+ let eventChain = Promise.resolve();
99
+ let parentAgentDone = false; // true after agent_end fires (even if BG agents still running)
100
+ let completionFired = false; // prevents double DONE when agent_end and background_agent_end race
101
+ /** Chain the completion sequence — session persist, cleanup, DONE marker */
102
+ function chainCompletion(display) {
103
+ if (completionFired)
104
+ return;
105
+ completionFired = true;
106
+ eventChain = eventChain
107
+ .then(async () => {
108
+ // Persist session
109
+ if (bridge.isAlive) {
110
+ try {
111
+ await bridge.refreshSessionInfo();
112
+ const userId = userIds.get(userState);
113
+ if (bridge.sessionFile && userId) {
114
+ setUserSession(userId, bridge.sessionFile);
115
+ }
116
+ }
117
+ catch (e) {
118
+ log(`[EVENT] Session refresh error: ${e}`);
119
+ }
120
+ }
121
+ cleanupUploads();
122
+ // DONE marker — only when truly idle and not stopped.
123
+ // Brief delay lets in-flight delivery (outbox drain) settle
124
+ // before DONE appears in the chat.
125
+ if (!bridge.isStreaming && !userState.stopRequested) {
126
+ await new Promise((r) => setTimeout(r, 150));
127
+ enqueueSend(api, userState, display.chatId, "🦀 _dreb DONE_");
128
+ }
129
+ })
130
+ .catch((e) => log(`[EVENT] Completion error: ${e}`));
131
+ }
132
+ bridge.onEvent((event) => {
133
+ // Capture display at event arrival time — even if a new prompt
134
+ // replaces the display later, this event uses the correct one.
135
+ const display = displays.get(userState);
136
+ if (!display) {
137
+ log(`[EVENT] No display for event: ${event.type} — dropped`);
138
+ return;
139
+ }
140
+ // Clear promptInFlight on first event after a prompt
141
+ if (userState.promptInFlight) {
142
+ userState.promptInFlight = false;
143
+ }
144
+ // Build a send callback that pushes to the outbox — captures chatId
145
+ // at event arrival time (same snapshot as display).
146
+ const send = (text, long) => {
147
+ enqueueSend(api, userState, display.chatId, text, long);
148
+ };
149
+ // Queue event processing — the chain never blocks on Telegram I/O.
150
+ // handleAgentEvent pushes permanent messages to outbox via send(),
151
+ // only ephemeral operations (status edits, file sends) go inline.
152
+ eventChain = eventChain
153
+ .then(() => handleAgentEvent(send, api, display, event))
154
+ .catch((e) => log(`[EVENT] Error: ${e}`));
155
+ // Handle turn completion — checks run INSIDE the eventChain so they
156
+ // execute after handleAgentEvent has updated display state (e.g.
157
+ // retryInProgress set by auto_retry_start that arrives right after agent_end).
158
+ if (event.type === "agent_end") {
159
+ parentAgentDone = true;
160
+ eventChain = eventChain.then(() => {
161
+ if (userState.backgroundAgents.size > 0 || display.retryInProgress || display.pendingRetry) {
162
+ return;
163
+ }
164
+ chainCompletion(display);
165
+ });
166
+ }
167
+ // Re-trigger completion when the last BG agent finishes.
168
+ // agent_end already fired but skipped completion because BG agents were running.
169
+ // Now that the last one is done, finalize.
170
+ if (event.type === "background_agent_end") {
171
+ eventChain = eventChain.then(() => {
172
+ if (parentAgentDone && userState.backgroundAgents.size === 0 && !bridge.isStreaming) {
173
+ chainCompletion(display);
174
+ }
175
+ });
176
+ }
177
+ // Reset state when a new run starts (e.g. user sends another message)
178
+ if (event.type === "agent_start") {
179
+ parentAgentDone = false;
180
+ completionFired = false;
181
+ }
182
+ });
183
+ }
184
+ // ---------------------------------------------------------------------------
185
+ // sendPrompt — steering vs normal path
186
+ // ---------------------------------------------------------------------------
187
+ /**
188
+ * Send a prompt to the agent. If the agent is streaming, steers instead.
189
+ *
190
+ * Returns immediately — the persistent subscription handles event delivery.
191
+ */
192
+ export function sendPrompt(api, userState, opts) {
193
+ const bridge = userState.bridge;
194
+ if (!bridge) {
195
+ log("[PROMPT] No bridge available");
196
+ if (opts.statusMessageId) {
197
+ void safeDelete(api, opts.chatId, opts.statusMessageId);
198
+ }
199
+ void safeSend(api, opts.chatId, "❌ No agent connection. Try sending your message again.");
200
+ return;
201
+ }
202
+ // Ensure persistent subscription exists for this bridge
203
+ ensureSubscribed(api, userState, bridge);
204
+ // Buddy: auto-init controller (loads from shared buddy.json)
205
+ if (!userState.buddyController) {
206
+ ensureBuddyController(api, userState, opts.chatId, userState.config);
207
+ }
208
+ // Buddy: capture context, reset idle, and check for name-call interception.
209
+ // processUserMessage returns true if name-call detected (buddy handles it).
210
+ const isNameCall = userState.buddyController?.processUserMessage(opts.prompt);
211
+ if (isNameCall) {
212
+ // Clean up the status message (won't be needed — buddy responds instead)
213
+ if (opts.statusMessageId) {
214
+ void safeDelete(api, opts.chatId, opts.statusMessageId);
215
+ }
216
+ return;
217
+ }
218
+ // Steering path — agent is actively streaming (same check as TUI).
219
+ // promptInFlight covers the race window between prompt() and agent_start.
220
+ if (bridge.isStreaming || userState.promptInFlight) {
221
+ userState.stopRequested = false;
222
+ if (opts.statusMessageId) {
223
+ void safeDelete(api, opts.chatId, opts.statusMessageId);
224
+ }
225
+ bridge
226
+ .steer(opts.prompt, opts.images)
227
+ .then(() => safeSend(api, opts.chatId, `↩️ _Steering:_ ${opts.prompt.slice(0, 200)}`))
228
+ .catch((e) => {
229
+ const msg = e instanceof Error ? e.message : String(e);
230
+ if (!userState.stopRequested) {
231
+ void safeSend(api, opts.chatId, `❌ Steering error: ${msg.slice(0, 200)}`);
232
+ }
233
+ });
234
+ return;
235
+ }
236
+ // Normal path — create new display, fire prompt, return immediately
237
+ userState.promptInFlight = true;
238
+ userState.stopRequested = false;
239
+ if (opts.userId)
240
+ userIds.set(userState, opts.userId);
241
+ const display = createEventDisplay(api, opts.chatId, opts.replyToId, opts.statusMessageId);
242
+ display.buddyController = userState.buddyController;
243
+ // Sync BG agent state — the display needs to know about running agents
244
+ // so events.ts doesn't prematurely finalize on agent_end (flush editor,
245
+ // delete status, set done). Without this, a new display created while
246
+ // BG agents are running would have an empty backgroundAgents map,
247
+ // causing events.ts to think the turn is over.
248
+ for (const [id, agent] of userState.backgroundAgents) {
249
+ display.backgroundAgents.set(id, agent);
250
+ }
251
+ displays.set(userState, display);
252
+ bridge.prompt(opts.prompt, opts.images).catch((e) => {
253
+ const msg = e instanceof Error ? e.message : String(e);
254
+ if (!userState.stopRequested) {
255
+ void safeSend(api, opts.chatId, `❌ Error: ${msg.slice(0, 200)}`);
256
+ }
257
+ userState.promptInFlight = false;
258
+ displays.delete(userState);
259
+ cleanupUploads();
260
+ });
261
+ }
262
+ //# sourceMappingURL=message.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"message.js","sourceRoot":"","sources":["../../src/handlers/message.ts"],"names":[],"mappings":"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","sourcesContent":["/**\n * Message handler — sends user messages directly to the agent.\n *\n * Two paths (matching TUI parity):\n * - **Steering**: agent is streaming → inject mid-run via steer()\n * - **Normal**: agent is idle → create display, fire prompt, return\n *\n * Architecture:\n * - Event chain processes events and pushes text to userState.outbox (never blocks on Telegram I/O)\n * - Delivery loop drains outbox to Telegram independently, with retries on failure\n * - Persistent event subscription per bridge handles DONE markers and session persistence\n */\n\nimport type { Api } from \"grammy\";\nimport type { AgentBridge } from \"../agent-bridge.js\";\nimport { ensureBuddyController } from \"../commands/buddy.js\";\nimport { setUserSession } from \"../state.js\";\nimport type { UserState } from \"../types.js\";\nimport { cleanupUploads } from \"../util/files.js\";\nimport { log, safeDelete, safeSend, sendLong } from \"../util/telegram.js\";\nimport { createEventDisplay, type EventDisplayState, handleAgentEvent } from \"./events.js\";\n\n// ---------------------------------------------------------------------------\n// Delivery loop — drains userState.outbox to Telegram, retries on failure\n// ---------------------------------------------------------------------------\n\nconst MAX_RETRIES = 5;\nconst RETRY_DELAY = 2_000;\n\n/** Track which users have an active delivery loop */\nconst activeDeliveryLoops = new WeakSet<UserState>();\n\n/**\n * Start draining the outbox. Runs independently of the event chain.\n * Self-terminates when the outbox is empty; restarts on next push.\n */\nasync function drainOutbox(api: Api, userState: UserState): Promise<void> {\n\tif (activeDeliveryLoops.has(userState)) return; // already draining\n\tactiveDeliveryLoops.add(userState);\n\n\ttry {\n\t\twhile (userState.outbox.length > 0) {\n\t\t\tconst item = userState.outbox[0];\n\t\t\titem.retries = (item.retries ?? 0) + 1;\n\n\t\t\tlet success: boolean;\n\t\t\tif (item.long) {\n\t\t\t\t// sendLong returns remaining undelivered text (empty = all delivered).\n\t\t\t\t// On partial failure, update item.text to only the undelivered tail\n\t\t\t\t// so retries don't resend already-delivered chunks.\n\t\t\t\tconst remaining = await sendLong(api, item.chatId, item.text);\n\t\t\t\tsuccess = remaining === \"\";\n\t\t\t\tif (!success) item.text = remaining;\n\t\t\t} else {\n\t\t\t\tconst msgId = await safeSend(api, item.chatId, item.text);\n\t\t\t\tsuccess = msgId !== 0;\n\t\t\t}\n\n\t\t\tif (success) {\n\t\t\t\tuserState.outbox.shift();\n\t\t\t} else if (item.retries >= MAX_RETRIES) {\n\t\t\t\tlog(`[DELIVER] Giving up on message after ${MAX_RETRIES} retries: ${item.text.slice(0, 100)}`);\n\t\t\t\tuserState.outbox.shift();\n\t\t\t} else {\n\t\t\t\t// Retry after delay\n\t\t\t\tawait new Promise((r) => setTimeout(r, RETRY_DELAY));\n\t\t\t}\n\t\t}\n\t} catch (e) {\n\t\tlog(`[DELIVER] Drain error: ${e}`);\n\t} finally {\n\t\tactiveDeliveryLoops.delete(userState);\n\t}\n}\n\n/** Push a message to the outbox and kick the delivery loop */\nexport function enqueueSend(api: Api, userState: UserState, chatId: number, text: string, long?: boolean): void {\n\tuserState.outbox.push({ chatId, text, long });\n\tvoid drainOutbox(api, userState);\n}\n\n// ---------------------------------------------------------------------------\n// Persistent event subscription\n// ---------------------------------------------------------------------------\n\n/** Track which bridges have a persistent event subscription */\nconst subscribedBridges = new WeakSet<AgentBridge>();\n\n/** Current display state per user — swapped on each new prompt */\nconst displays = new Map<UserState, EventDisplayState>();\n\n/** Track userId for session persistence (set by sendPrompt, used by completion handler) */\nconst userIds = new Map<UserState, number>();\n\n/**\n * Ensure a persistent event subscription exists for this bridge.\n * Called once per bridge lifetime — handles all event delivery,\n * DONE markers, session persistence, and cleanup.\n */\nfunction ensureSubscribed(api: Api, userState: UserState, bridge: AgentBridge): void {\n\tif (subscribedBridges.has(bridge)) return;\n\tsubscribedBridges.add(bridge);\n\n\t// New bridge — clear stale state from any previous bridge\n\tuserState.promptInFlight = false;\n\tdisplays.delete(userState);\n\tuserState.buddyController?.reset();\n\n\tlet eventChain = Promise.resolve();\n\tlet parentAgentDone = false; // true after agent_end fires (even if BG agents still running)\n\tlet completionFired = false; // prevents double DONE when agent_end and background_agent_end race\n\n\t/** Chain the completion sequence — session persist, cleanup, DONE marker */\n\tfunction chainCompletion(display: EventDisplayState) {\n\t\tif (completionFired) return;\n\t\tcompletionFired = true;\n\t\teventChain = eventChain\n\t\t\t.then(async () => {\n\t\t\t\t// Persist session\n\t\t\t\tif (bridge.isAlive) {\n\t\t\t\t\ttry {\n\t\t\t\t\t\tawait bridge.refreshSessionInfo();\n\t\t\t\t\t\tconst userId = userIds.get(userState);\n\t\t\t\t\t\tif (bridge.sessionFile && userId) {\n\t\t\t\t\t\t\tsetUserSession(userId, bridge.sessionFile);\n\t\t\t\t\t\t}\n\t\t\t\t\t} catch (e) {\n\t\t\t\t\t\tlog(`[EVENT] Session refresh error: ${e}`);\n\t\t\t\t\t}\n\t\t\t\t}\n\n\t\t\t\tcleanupUploads();\n\n\t\t\t\t// DONE marker — only when truly idle and not stopped.\n\t\t\t\t// Brief delay lets in-flight delivery (outbox drain) settle\n\t\t\t\t// before DONE appears in the chat.\n\t\t\t\tif (!bridge.isStreaming && !userState.stopRequested) {\n\t\t\t\t\tawait new Promise((r) => setTimeout(r, 150));\n\t\t\t\t\tenqueueSend(api, userState, display.chatId, \"🦀 _dreb DONE_\");\n\t\t\t\t}\n\t\t\t})\n\t\t\t.catch((e) => log(`[EVENT] Completion error: ${e}`));\n\t}\n\n\tbridge.onEvent((event) => {\n\t\t// Capture display at event arrival time — even if a new prompt\n\t\t// replaces the display later, this event uses the correct one.\n\t\tconst display = displays.get(userState);\n\t\tif (!display) {\n\t\t\tlog(`[EVENT] No display for event: ${event.type} — dropped`);\n\t\t\treturn;\n\t\t}\n\n\t\t// Clear promptInFlight on first event after a prompt\n\t\tif (userState.promptInFlight) {\n\t\t\tuserState.promptInFlight = false;\n\t\t}\n\n\t\t// Build a send callback that pushes to the outbox — captures chatId\n\t\t// at event arrival time (same snapshot as display).\n\t\tconst send = (text: string, long?: boolean) => {\n\t\t\tenqueueSend(api, userState, display.chatId, text, long);\n\t\t};\n\n\t\t// Queue event processing — the chain never blocks on Telegram I/O.\n\t\t// handleAgentEvent pushes permanent messages to outbox via send(),\n\t\t// only ephemeral operations (status edits, file sends) go inline.\n\t\teventChain = eventChain\n\t\t\t.then(() => handleAgentEvent(send, api, display, event))\n\t\t\t.catch((e) => log(`[EVENT] Error: ${e}`));\n\n\t\t// Handle turn completion — checks run INSIDE the eventChain so they\n\t\t// execute after handleAgentEvent has updated display state (e.g.\n\t\t// retryInProgress set by auto_retry_start that arrives right after agent_end).\n\t\tif (event.type === \"agent_end\") {\n\t\t\tparentAgentDone = true;\n\t\t\teventChain = eventChain.then(() => {\n\t\t\t\tif (userState.backgroundAgents.size > 0 || display.retryInProgress || display.pendingRetry) {\n\t\t\t\t\treturn;\n\t\t\t\t}\n\t\t\t\tchainCompletion(display);\n\t\t\t});\n\t\t}\n\n\t\t// Re-trigger completion when the last BG agent finishes.\n\t\t// agent_end already fired but skipped completion because BG agents were running.\n\t\t// Now that the last one is done, finalize.\n\t\tif (event.type === \"background_agent_end\") {\n\t\t\teventChain = eventChain.then(() => {\n\t\t\t\tif (parentAgentDone && userState.backgroundAgents.size === 0 && !bridge.isStreaming) {\n\t\t\t\t\tchainCompletion(display);\n\t\t\t\t}\n\t\t\t});\n\t\t}\n\n\t\t// Reset state when a new run starts (e.g. user sends another message)\n\t\tif (event.type === \"agent_start\") {\n\t\t\tparentAgentDone = false;\n\t\t\tcompletionFired = false;\n\t\t}\n\t});\n}\n\n// ---------------------------------------------------------------------------\n// sendPrompt — steering vs normal path\n// ---------------------------------------------------------------------------\n\n/**\n * Send a prompt to the agent. If the agent is streaming, steers instead.\n *\n * Returns immediately — the persistent subscription handles event delivery.\n */\nexport function sendPrompt(\n\tapi: Api,\n\tuserState: UserState,\n\topts: {\n\t\tchatId: number;\n\t\treplyToId: number;\n\t\tuserId?: number;\n\t\tprompt: string;\n\t\timages?: Array<{ type: \"image\"; data: string; mimeType: string }>;\n\t\tstatusMessageId: number | null;\n\t},\n): void {\n\tconst bridge = userState.bridge;\n\tif (!bridge) {\n\t\tlog(\"[PROMPT] No bridge available\");\n\t\tif (opts.statusMessageId) {\n\t\t\tvoid safeDelete(api, opts.chatId, opts.statusMessageId);\n\t\t}\n\t\tvoid safeSend(api, opts.chatId, \"❌ No agent connection. Try sending your message again.\");\n\t\treturn;\n\t}\n\n\t// Ensure persistent subscription exists for this bridge\n\tensureSubscribed(api, userState, bridge);\n\n\t// Buddy: auto-init controller (loads from shared buddy.json)\n\tif (!userState.buddyController) {\n\t\tensureBuddyController(api, userState, opts.chatId, userState.config);\n\t}\n\n\t// Buddy: capture context, reset idle, and check for name-call interception.\n\t// processUserMessage returns true if name-call detected (buddy handles it).\n\tconst isNameCall = userState.buddyController?.processUserMessage(opts.prompt);\n\tif (isNameCall) {\n\t\t// Clean up the status message (won't be needed — buddy responds instead)\n\t\tif (opts.statusMessageId) {\n\t\t\tvoid safeDelete(api, opts.chatId, opts.statusMessageId);\n\t\t}\n\t\treturn;\n\t}\n\n\t// Steering path — agent is actively streaming (same check as TUI).\n\t// promptInFlight covers the race window between prompt() and agent_start.\n\tif (bridge.isStreaming || userState.promptInFlight) {\n\t\tuserState.stopRequested = false;\n\t\tif (opts.statusMessageId) {\n\t\t\tvoid safeDelete(api, opts.chatId, opts.statusMessageId);\n\t\t}\n\t\tbridge\n\t\t\t.steer(opts.prompt, opts.images)\n\t\t\t.then(() => safeSend(api, opts.chatId, `↩️ _Steering:_ ${opts.prompt.slice(0, 200)}`))\n\t\t\t.catch((e) => {\n\t\t\t\tconst msg = e instanceof Error ? e.message : String(e);\n\t\t\t\tif (!userState.stopRequested) {\n\t\t\t\t\tvoid safeSend(api, opts.chatId, `❌ Steering error: ${msg.slice(0, 200)}`);\n\t\t\t\t}\n\t\t\t});\n\t\treturn;\n\t}\n\n\t// Normal path — create new display, fire prompt, return immediately\n\tuserState.promptInFlight = true;\n\tuserState.stopRequested = false;\n\tif (opts.userId) userIds.set(userState, opts.userId);\n\n\tconst display = createEventDisplay(api, opts.chatId, opts.replyToId, opts.statusMessageId);\n\tdisplay.buddyController = userState.buddyController;\n\n\t// Sync BG agent state — the display needs to know about running agents\n\t// so events.ts doesn't prematurely finalize on agent_end (flush editor,\n\t// delete status, set done). Without this, a new display created while\n\t// BG agents are running would have an empty backgroundAgents map,\n\t// causing events.ts to think the turn is over.\n\tfor (const [id, agent] of userState.backgroundAgents) {\n\t\tdisplay.backgroundAgents.set(id, agent);\n\t}\n\n\tdisplays.set(userState, display);\n\n\tbridge.prompt(opts.prompt, opts.images).catch((e) => {\n\t\tconst msg = e instanceof Error ? e.message : String(e);\n\t\tif (!userState.stopRequested) {\n\t\t\tvoid safeSend(api, opts.chatId, `❌ Error: ${msg.slice(0, 200)}`);\n\t\t}\n\t\tuserState.promptInFlight = false;\n\t\tdisplays.delete(userState);\n\t\tcleanupUploads();\n\t});\n}\n"]}
@@ -0,0 +1,8 @@
1
+ #!/usr/bin/env node
2
+ /**
3
+ * dreb Telegram bot — entry point.
4
+ *
5
+ * Starts the bot in long-polling mode with the configuration from env vars.
6
+ */
7
+ export {};
8
+ //# sourceMappingURL=index.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../src/index.ts"],"names":[],"mappings":";AAEA;;;;GAIG","sourcesContent":["#!/usr/bin/env node\n\n/**\n * dreb Telegram bot — entry point.\n *\n * Starts the bot in long-polling mode with the configuration from env vars.\n */\n\nimport { createBot, getUserState, setMyCommands } from \"./bot.js\";\nimport { ensureBridge } from \"./bridge-lifecycle.js\";\nimport { refreshCommandsWithSkills } from \"./commands/refresh.js\";\nimport { setSkillsBot } from \"./commands/skills.js\";\nimport { loadConfig } from \"./config.js\";\nimport { getUserSession, loadState, setUserSession } from \"./state.js\";\nimport { log } from \"./util/telegram.js\";\n\n/**\n * Eagerly reconnect known users to their last session.\n * Spins up an RPC bridge and switches to the persisted session path.\n */\nasync function reconnectUsers(config: import(\"./config.js\").Config): Promise<void> {\n\tfor (const userId of config.allowedUserIds) {\n\t\tconst sessionPath = getUserSession(userId);\n\t\tif (!sessionPath) {\n\t\t\tlog(`[RECONNECT] No persisted session for user ${userId}, skipping`);\n\t\t\tcontinue;\n\t\t}\n\n\t\ttry {\n\t\t\tconst userState = getUserState(userId, config);\n\t\t\tconst bridge = await ensureBridge(config, userState);\n\t\t\tconst switched = await bridge.switchSession(sessionPath);\n\t\t\tif (switched) {\n\t\t\t\tlog(`[RECONNECT] User ${userId} reconnected to session ${bridge.sessionId?.slice(0, 8)}`);\n\t\t\t} else {\n\t\t\t\tlog(`[RECONNECT] User ${userId} failed to switch to persisted session, will resume latest`);\n\t\t\t\tawait bridge.resumeLatest();\n\t\t\t}\n\t\t} catch (e) {\n\t\t\tlog(`[RECONNECT] User ${userId} reconnect failed: ${e}`);\n\t\t\t// Clear stale session so we don't retry every restart\n\t\t\tsetUserSession(userId, \"\");\n\t\t}\n\t}\n}\n\nasync function main(): Promise<void> {\n\tconst config = loadConfig();\n\n\tlog(`Starting dreb Telegram bot...`);\n\tlog(`Working directory: ${config.workingDir}`);\n\tif (config.allowedUserIds.length > 0) {\n\t\tlog(`Allowed users: ${config.allowedUserIds.join(\", \")}`);\n\t} else {\n\t\tlog(\"WARNING: ALLOWED_USER_IDS not set — bot will accept messages from anyone!\");\n\t}\n\n\t// Load persisted state and reconnect users before accepting messages\n\tloadState();\n\tawait reconnectUsers(config);\n\n\tconst bot = createBot(config);\n\n\t// Store bot ref for dynamic command refresh from /skills\n\tsetSkillsBot(bot);\n\n\t// Register static commands for autocomplete\n\tawait setMyCommands(bot);\n\n\t// Refresh command menu with dynamic skill commands from the first available bridge\n\tfor (const userId of config.allowedUserIds) {\n\t\tconst userState = getUserState(userId, config);\n\t\tif (userState.bridge?.isAlive) {\n\t\t\tawait refreshCommandsWithSkills(bot, userState.bridge);\n\t\t\tbreak; // Only need one bridge to query skills\n\t\t}\n\t}\n\n\t// Start polling\n\tlog(\"Bot running. Press Ctrl+C to stop.\");\n\tawait bot.start({\n\t\tdrop_pending_updates: true,\n\t\tallowed_updates: [\"message\", \"callback_query\"],\n\t\tonStart: () => log(\"Bot polling started\"),\n\t});\n}\n\nmain().catch((e) => {\n\tconsole.error(\"Fatal:\", e);\n\tprocess.exit(1);\n});\n"]}
package/dist/index.js ADDED
@@ -0,0 +1,82 @@
1
+ #!/usr/bin/env node
2
+ /**
3
+ * dreb Telegram bot — entry point.
4
+ *
5
+ * Starts the bot in long-polling mode with the configuration from env vars.
6
+ */
7
+ import { createBot, getUserState, setMyCommands } from "./bot.js";
8
+ import { ensureBridge } from "./bridge-lifecycle.js";
9
+ import { refreshCommandsWithSkills } from "./commands/refresh.js";
10
+ import { setSkillsBot } from "./commands/skills.js";
11
+ import { loadConfig } from "./config.js";
12
+ import { getUserSession, loadState, setUserSession } from "./state.js";
13
+ import { log } from "./util/telegram.js";
14
+ /**
15
+ * Eagerly reconnect known users to their last session.
16
+ * Spins up an RPC bridge and switches to the persisted session path.
17
+ */
18
+ async function reconnectUsers(config) {
19
+ for (const userId of config.allowedUserIds) {
20
+ const sessionPath = getUserSession(userId);
21
+ if (!sessionPath) {
22
+ log(`[RECONNECT] No persisted session for user ${userId}, skipping`);
23
+ continue;
24
+ }
25
+ try {
26
+ const userState = getUserState(userId, config);
27
+ const bridge = await ensureBridge(config, userState);
28
+ const switched = await bridge.switchSession(sessionPath);
29
+ if (switched) {
30
+ log(`[RECONNECT] User ${userId} reconnected to session ${bridge.sessionId?.slice(0, 8)}`);
31
+ }
32
+ else {
33
+ log(`[RECONNECT] User ${userId} failed to switch to persisted session, will resume latest`);
34
+ await bridge.resumeLatest();
35
+ }
36
+ }
37
+ catch (e) {
38
+ log(`[RECONNECT] User ${userId} reconnect failed: ${e}`);
39
+ // Clear stale session so we don't retry every restart
40
+ setUserSession(userId, "");
41
+ }
42
+ }
43
+ }
44
+ async function main() {
45
+ const config = loadConfig();
46
+ log(`Starting dreb Telegram bot...`);
47
+ log(`Working directory: ${config.workingDir}`);
48
+ if (config.allowedUserIds.length > 0) {
49
+ log(`Allowed users: ${config.allowedUserIds.join(", ")}`);
50
+ }
51
+ else {
52
+ log("WARNING: ALLOWED_USER_IDS not set — bot will accept messages from anyone!");
53
+ }
54
+ // Load persisted state and reconnect users before accepting messages
55
+ loadState();
56
+ await reconnectUsers(config);
57
+ const bot = createBot(config);
58
+ // Store bot ref for dynamic command refresh from /skills
59
+ setSkillsBot(bot);
60
+ // Register static commands for autocomplete
61
+ await setMyCommands(bot);
62
+ // Refresh command menu with dynamic skill commands from the first available bridge
63
+ for (const userId of config.allowedUserIds) {
64
+ const userState = getUserState(userId, config);
65
+ if (userState.bridge?.isAlive) {
66
+ await refreshCommandsWithSkills(bot, userState.bridge);
67
+ break; // Only need one bridge to query skills
68
+ }
69
+ }
70
+ // Start polling
71
+ log("Bot running. Press Ctrl+C to stop.");
72
+ await bot.start({
73
+ drop_pending_updates: true,
74
+ allowed_updates: ["message", "callback_query"],
75
+ onStart: () => log("Bot polling started"),
76
+ });
77
+ }
78
+ main().catch((e) => {
79
+ console.error("Fatal:", e);
80
+ process.exit(1);
81
+ });
82
+ //# sourceMappingURL=index.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"index.js","sourceRoot":"","sources":["../src/index.ts"],"names":[],"mappings":"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","sourcesContent":["#!/usr/bin/env node\n\n/**\n * dreb Telegram bot — entry point.\n *\n * Starts the bot in long-polling mode with the configuration from env vars.\n */\n\nimport { createBot, getUserState, setMyCommands } from \"./bot.js\";\nimport { ensureBridge } from \"./bridge-lifecycle.js\";\nimport { refreshCommandsWithSkills } from \"./commands/refresh.js\";\nimport { setSkillsBot } from \"./commands/skills.js\";\nimport { loadConfig } from \"./config.js\";\nimport { getUserSession, loadState, setUserSession } from \"./state.js\";\nimport { log } from \"./util/telegram.js\";\n\n/**\n * Eagerly reconnect known users to their last session.\n * Spins up an RPC bridge and switches to the persisted session path.\n */\nasync function reconnectUsers(config: import(\"./config.js\").Config): Promise<void> {\n\tfor (const userId of config.allowedUserIds) {\n\t\tconst sessionPath = getUserSession(userId);\n\t\tif (!sessionPath) {\n\t\t\tlog(`[RECONNECT] No persisted session for user ${userId}, skipping`);\n\t\t\tcontinue;\n\t\t}\n\n\t\ttry {\n\t\t\tconst userState = getUserState(userId, config);\n\t\t\tconst bridge = await ensureBridge(config, userState);\n\t\t\tconst switched = await bridge.switchSession(sessionPath);\n\t\t\tif (switched) {\n\t\t\t\tlog(`[RECONNECT] User ${userId} reconnected to session ${bridge.sessionId?.slice(0, 8)}`);\n\t\t\t} else {\n\t\t\t\tlog(`[RECONNECT] User ${userId} failed to switch to persisted session, will resume latest`);\n\t\t\t\tawait bridge.resumeLatest();\n\t\t\t}\n\t\t} catch (e) {\n\t\t\tlog(`[RECONNECT] User ${userId} reconnect failed: ${e}`);\n\t\t\t// Clear stale session so we don't retry every restart\n\t\t\tsetUserSession(userId, \"\");\n\t\t}\n\t}\n}\n\nasync function main(): Promise<void> {\n\tconst config = loadConfig();\n\n\tlog(`Starting dreb Telegram bot...`);\n\tlog(`Working directory: ${config.workingDir}`);\n\tif (config.allowedUserIds.length > 0) {\n\t\tlog(`Allowed users: ${config.allowedUserIds.join(\", \")}`);\n\t} else {\n\t\tlog(\"WARNING: ALLOWED_USER_IDS not set — bot will accept messages from anyone!\");\n\t}\n\n\t// Load persisted state and reconnect users before accepting messages\n\tloadState();\n\tawait reconnectUsers(config);\n\n\tconst bot = createBot(config);\n\n\t// Store bot ref for dynamic command refresh from /skills\n\tsetSkillsBot(bot);\n\n\t// Register static commands for autocomplete\n\tawait setMyCommands(bot);\n\n\t// Refresh command menu with dynamic skill commands from the first available bridge\n\tfor (const userId of config.allowedUserIds) {\n\t\tconst userState = getUserState(userId, config);\n\t\tif (userState.bridge?.isAlive) {\n\t\t\tawait refreshCommandsWithSkills(bot, userState.bridge);\n\t\t\tbreak; // Only need one bridge to query skills\n\t\t}\n\t}\n\n\t// Start polling\n\tlog(\"Bot running. Press Ctrl+C to stop.\");\n\tawait bot.start({\n\t\tdrop_pending_updates: true,\n\t\tallowed_updates: [\"message\", \"callback_query\"],\n\t\tonStart: () => log(\"Bot polling started\"),\n\t});\n}\n\nmain().catch((e) => {\n\tconsole.error(\"Fatal:\", e);\n\tprocess.exit(1);\n});\n"]}
@@ -0,0 +1,11 @@
1
+ /**
2
+ * Persistent state — survives bot restarts.
3
+ * Stores per-user session paths so the bot can eagerly reconnect on startup.
4
+ */
5
+ /** Load persisted state from disk. Safe to call multiple times. */
6
+ export declare function loadState(): void;
7
+ /** Record the active session for a user. Persists immediately. */
8
+ export declare function setUserSession(userId: number, sessionPath: string): void;
9
+ /** Get the last known session path for a user, or undefined. */
10
+ export declare function getUserSession(userId: number): string | undefined;
11
+ //# sourceMappingURL=state.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"state.d.ts","sourceRoot":"","sources":["../src/state.ts"],"names":[],"mappings":"AAAA;;;GAGG;AAgBH,mEAAmE;AACnE,wBAAgB,SAAS,IAAI,IAAI,CAWhC;AAeD,kEAAkE;AAClE,wBAAgB,cAAc,CAAC,MAAM,EAAE,MAAM,EAAE,WAAW,EAAE,MAAM,GAAG,IAAI,CAGxE;AAED,gEAAgE;AAChE,wBAAgB,cAAc,CAAC,MAAM,EAAE,MAAM,GAAG,MAAM,GAAG,SAAS,CAEjE","sourcesContent":["/**\n * Persistent state — survives bot restarts.\n * Stores per-user session paths so the bot can eagerly reconnect on startup.\n */\n\nimport { existsSync, mkdirSync, readFileSync, writeFileSync } from \"node:fs\";\nimport { homedir } from \"node:os\";\nimport { dirname, join } from \"node:path\";\nimport { log } from \"./util/telegram.js\";\n\nconst STATE_FILE = join(homedir(), \".dreb\", \"telegram\", \"state.json\");\n\ninterface PersistedState {\n\t/** userId → last active session file path */\n\tsessions: Record<string, string>;\n}\n\nlet state: PersistedState = { sessions: {} };\n\n/** Load persisted state from disk. Safe to call multiple times. */\nexport function loadState(): void {\n\ttry {\n\t\tif (existsSync(STATE_FILE)) {\n\t\t\tconst raw = readFileSync(STATE_FILE, \"utf-8\");\n\t\t\tstate = JSON.parse(raw);\n\t\t\tlog(`[STATE] Loaded state: ${Object.keys(state.sessions).length} user(s)`);\n\t\t}\n\t} catch (e) {\n\t\tlog(`[STATE] Failed to load state: ${e}`);\n\t\tstate = { sessions: {} };\n\t}\n}\n\n/** Save current state to disk. */\nfunction saveState(): void {\n\ttry {\n\t\tconst dir = dirname(STATE_FILE);\n\t\tif (!existsSync(dir)) {\n\t\t\tmkdirSync(dir, { recursive: true });\n\t\t}\n\t\twriteFileSync(STATE_FILE, JSON.stringify(state, null, 2));\n\t} catch (e) {\n\t\tlog(`[STATE] Failed to save state: ${e}`);\n\t}\n}\n\n/** Record the active session for a user. Persists immediately. */\nexport function setUserSession(userId: number, sessionPath: string): void {\n\tstate.sessions[String(userId)] = sessionPath;\n\tsaveState();\n}\n\n/** Get the last known session path for a user, or undefined. */\nexport function getUserSession(userId: number): string | undefined {\n\treturn state.sessions[String(userId)];\n}\n"]}
package/dist/state.js ADDED
@@ -0,0 +1,47 @@
1
+ /**
2
+ * Persistent state — survives bot restarts.
3
+ * Stores per-user session paths so the bot can eagerly reconnect on startup.
4
+ */
5
+ import { existsSync, mkdirSync, readFileSync, writeFileSync } from "node:fs";
6
+ import { homedir } from "node:os";
7
+ import { dirname, join } from "node:path";
8
+ import { log } from "./util/telegram.js";
9
+ const STATE_FILE = join(homedir(), ".dreb", "telegram", "state.json");
10
+ let state = { sessions: {} };
11
+ /** Load persisted state from disk. Safe to call multiple times. */
12
+ export function loadState() {
13
+ try {
14
+ if (existsSync(STATE_FILE)) {
15
+ const raw = readFileSync(STATE_FILE, "utf-8");
16
+ state = JSON.parse(raw);
17
+ log(`[STATE] Loaded state: ${Object.keys(state.sessions).length} user(s)`);
18
+ }
19
+ }
20
+ catch (e) {
21
+ log(`[STATE] Failed to load state: ${e}`);
22
+ state = { sessions: {} };
23
+ }
24
+ }
25
+ /** Save current state to disk. */
26
+ function saveState() {
27
+ try {
28
+ const dir = dirname(STATE_FILE);
29
+ if (!existsSync(dir)) {
30
+ mkdirSync(dir, { recursive: true });
31
+ }
32
+ writeFileSync(STATE_FILE, JSON.stringify(state, null, 2));
33
+ }
34
+ catch (e) {
35
+ log(`[STATE] Failed to save state: ${e}`);
36
+ }
37
+ }
38
+ /** Record the active session for a user. Persists immediately. */
39
+ export function setUserSession(userId, sessionPath) {
40
+ state.sessions[String(userId)] = sessionPath;
41
+ saveState();
42
+ }
43
+ /** Get the last known session path for a user, or undefined. */
44
+ export function getUserSession(userId) {
45
+ return state.sessions[String(userId)];
46
+ }
47
+ //# sourceMappingURL=state.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"state.js","sourceRoot":"","sources":["../src/state.ts"],"names":[],"mappings":"AAAA;;;GAGG;AAEH,OAAO,EAAE,UAAU,EAAE,SAAS,EAAE,YAAY,EAAE,aAAa,EAAE,MAAM,SAAS,CAAC;AAC7E,OAAO,EAAE,OAAO,EAAE,MAAM,SAAS,CAAC;AAClC,OAAO,EAAE,OAAO,EAAE,IAAI,EAAE,MAAM,WAAW,CAAC;AAC1C,OAAO,EAAE,GAAG,EAAE,MAAM,oBAAoB,CAAC;AAEzC,MAAM,UAAU,GAAG,IAAI,CAAC,OAAO,EAAE,EAAE,OAAO,EAAE,UAAU,EAAE,YAAY,CAAC,CAAC;AAOtE,IAAI,KAAK,GAAmB,EAAE,QAAQ,EAAE,EAAE,EAAE,CAAC;AAE7C,mEAAmE;AACnE,MAAM,UAAU,SAAS,GAAS;IACjC,IAAI,CAAC;QACJ,IAAI,UAAU,CAAC,UAAU,CAAC,EAAE,CAAC;YAC5B,MAAM,GAAG,GAAG,YAAY,CAAC,UAAU,EAAE,OAAO,CAAC,CAAC;YAC9C,KAAK,GAAG,IAAI,CAAC,KAAK,CAAC,GAAG,CAAC,CAAC;YACxB,GAAG,CAAC,yBAAyB,MAAM,CAAC,IAAI,CAAC,KAAK,CAAC,QAAQ,CAAC,CAAC,MAAM,UAAU,CAAC,CAAC;QAC5E,CAAC;IACF,CAAC;IAAC,OAAO,CAAC,EAAE,CAAC;QACZ,GAAG,CAAC,iCAAiC,CAAC,EAAE,CAAC,CAAC;QAC1C,KAAK,GAAG,EAAE,QAAQ,EAAE,EAAE,EAAE,CAAC;IAC1B,CAAC;AAAA,CACD;AAED,kCAAkC;AAClC,SAAS,SAAS,GAAS;IAC1B,IAAI,CAAC;QACJ,MAAM,GAAG,GAAG,OAAO,CAAC,UAAU,CAAC,CAAC;QAChC,IAAI,CAAC,UAAU,CAAC,GAAG,CAAC,EAAE,CAAC;YACtB,SAAS,CAAC,GAAG,EAAE,EAAE,SAAS,EAAE,IAAI,EAAE,CAAC,CAAC;QACrC,CAAC;QACD,aAAa,CAAC,UAAU,EAAE,IAAI,CAAC,SAAS,CAAC,KAAK,EAAE,IAAI,EAAE,CAAC,CAAC,CAAC,CAAC;IAC3D,CAAC;IAAC,OAAO,CAAC,EAAE,CAAC;QACZ,GAAG,CAAC,iCAAiC,CAAC,EAAE,CAAC,CAAC;IAC3C,CAAC;AAAA,CACD;AAED,kEAAkE;AAClE,MAAM,UAAU,cAAc,CAAC,MAAc,EAAE,WAAmB,EAAQ;IACzE,KAAK,CAAC,QAAQ,CAAC,MAAM,CAAC,MAAM,CAAC,CAAC,GAAG,WAAW,CAAC;IAC7C,SAAS,EAAE,CAAC;AAAA,CACZ;AAED,gEAAgE;AAChE,MAAM,UAAU,cAAc,CAAC,MAAc,EAAsB;IAClE,OAAO,KAAK,CAAC,QAAQ,CAAC,MAAM,CAAC,MAAM,CAAC,CAAC,CAAC;AAAA,CACtC","sourcesContent":["/**\n * Persistent state — survives bot restarts.\n * Stores per-user session paths so the bot can eagerly reconnect on startup.\n */\n\nimport { existsSync, mkdirSync, readFileSync, writeFileSync } from \"node:fs\";\nimport { homedir } from \"node:os\";\nimport { dirname, join } from \"node:path\";\nimport { log } from \"./util/telegram.js\";\n\nconst STATE_FILE = join(homedir(), \".dreb\", \"telegram\", \"state.json\");\n\ninterface PersistedState {\n\t/** userId → last active session file path */\n\tsessions: Record<string, string>;\n}\n\nlet state: PersistedState = { sessions: {} };\n\n/** Load persisted state from disk. Safe to call multiple times. */\nexport function loadState(): void {\n\ttry {\n\t\tif (existsSync(STATE_FILE)) {\n\t\t\tconst raw = readFileSync(STATE_FILE, \"utf-8\");\n\t\t\tstate = JSON.parse(raw);\n\t\t\tlog(`[STATE] Loaded state: ${Object.keys(state.sessions).length} user(s)`);\n\t\t}\n\t} catch (e) {\n\t\tlog(`[STATE] Failed to load state: ${e}`);\n\t\tstate = { sessions: {} };\n\t}\n}\n\n/** Save current state to disk. */\nfunction saveState(): void {\n\ttry {\n\t\tconst dir = dirname(STATE_FILE);\n\t\tif (!existsSync(dir)) {\n\t\t\tmkdirSync(dir, { recursive: true });\n\t\t}\n\t\twriteFileSync(STATE_FILE, JSON.stringify(state, null, 2));\n\t} catch (e) {\n\t\tlog(`[STATE] Failed to save state: ${e}`);\n\t}\n}\n\n/** Record the active session for a user. Persists immediately. */\nexport function setUserSession(userId: number, sessionPath: string): void {\n\tstate.sessions[String(userId)] = sessionPath;\n\tsaveState();\n}\n\n/** Get the last known session path for a user, or undefined. */\nexport function getUserSession(userId: number): string | undefined {\n\treturn state.sessions[String(userId)];\n}\n"]}
@@ -0,0 +1,50 @@
1
+ /**
2
+ * Bot-specific types for the Telegram frontend.
3
+ */
4
+ import type { AgentBridge } from "./agent-bridge.js";
5
+ import type { Config } from "./config.js";
6
+ /** Tracked background agent */
7
+ export interface TrackedAgent {
8
+ agentId: string;
9
+ agentType: string;
10
+ taskSummary: string;
11
+ startTime: number;
12
+ }
13
+ /** Per-user state managed by the bot */
14
+ export interface UserState {
15
+ /** Active agent bridge (RPC process) */
16
+ bridge: AgentBridge | null;
17
+ /** Bot config — needed by buddy controller for RPC hatch/reroll */
18
+ config: Config;
19
+ /** Covers the race window between prompt() call and agent_start event */
20
+ promptInFlight: boolean;
21
+ /** Flag to start a fresh session on next message */
22
+ newSessionFlag: boolean;
23
+ /** Optional working directory override for the next new session */
24
+ newSessionCwd: string | null;
25
+ /** The actual working directory of the current bridge (may differ from config default) */
26
+ effectiveCwd: string | null;
27
+ /** Currently running background agents */
28
+ backgroundAgents: Map<string, TrackedAgent>;
29
+ /** Whether /stop was used (suppress DONE marker) */
30
+ stopRequested: boolean;
31
+ /** Messages waiting to be delivered to Telegram — drained by a delivery loop */
32
+ outbox: Array<{
33
+ chatId: number;
34
+ text: string;
35
+ long?: boolean;
36
+ retries?: number;
37
+ }>;
38
+ /** Buddy controller — any to avoid import of @dreb/coding-agent/buddy */
39
+ buddyController: any;
40
+ }
41
+ /** Session info for persistence across bot restarts */
42
+ export interface SavedSessions {
43
+ [userId: string]: Array<{
44
+ sessionPath: string;
45
+ sessionId: string;
46
+ timestamp: number;
47
+ preview: string;
48
+ }>;
49
+ }
50
+ //# sourceMappingURL=types.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"types.d.ts","sourceRoot":"","sources":["../src/types.ts"],"names":[],"mappings":"AAAA;;GAEG;AAEH,OAAO,KAAK,EAAE,WAAW,EAAE,MAAM,mBAAmB,CAAC;AACrD,OAAO,KAAK,EAAE,MAAM,EAAE,MAAM,aAAa,CAAC;AAE1C,+BAA+B;AAC/B,MAAM,WAAW,YAAY;IAC5B,OAAO,EAAE,MAAM,CAAC;IAChB,SAAS,EAAE,MAAM,CAAC;IAClB,WAAW,EAAE,MAAM,CAAC;IACpB,SAAS,EAAE,MAAM,CAAC;CAClB;AAED,wCAAwC;AACxC,MAAM,WAAW,SAAS;IACzB,wCAAwC;IACxC,MAAM,EAAE,WAAW,GAAG,IAAI,CAAC;IAC3B,qEAAmE;IACnE,MAAM,EAAE,MAAM,CAAC;IACf,yEAAyE;IACzE,cAAc,EAAE,OAAO,CAAC;IACxB,oDAAoD;IACpD,cAAc,EAAE,OAAO,CAAC;IACxB,mEAAmE;IACnE,aAAa,EAAE,MAAM,GAAG,IAAI,CAAC;IAC7B,0FAA0F;IAC1F,YAAY,EAAE,MAAM,GAAG,IAAI,CAAC;IAC5B,0CAA0C;IAC1C,gBAAgB,EAAE,GAAG,CAAC,MAAM,EAAE,YAAY,CAAC,CAAC;IAC5C,oDAAoD;IACpD,aAAa,EAAE,OAAO,CAAC;IACvB,kFAAgF;IAChF,MAAM,EAAE,KAAK,CAAC;QAAE,MAAM,EAAE,MAAM,CAAC;QAAC,IAAI,EAAE,MAAM,CAAC;QAAC,IAAI,CAAC,EAAE,OAAO,CAAC;QAAC,OAAO,CAAC,EAAE,MAAM,CAAA;KAAE,CAAC,CAAC;IAClF,2EAAyE;IACzE,eAAe,EAAE,GAAG,CAAC;CACrB;AAED,uDAAuD;AACvD,MAAM,WAAW,aAAa;IAC7B,CAAC,MAAM,EAAE,MAAM,GAAG,KAAK,CAAC;QACvB,WAAW,EAAE,MAAM,CAAC;QACpB,SAAS,EAAE,MAAM,CAAC;QAClB,SAAS,EAAE,MAAM,CAAC;QAClB,OAAO,EAAE,MAAM,CAAC;KAChB,CAAC,CAAC;CACH","sourcesContent":["/**\n * Bot-specific types for the Telegram frontend.\n */\n\nimport type { AgentBridge } from \"./agent-bridge.js\";\nimport type { Config } from \"./config.js\";\n\n/** Tracked background agent */\nexport interface TrackedAgent {\n\tagentId: string;\n\tagentType: string;\n\ttaskSummary: string;\n\tstartTime: number;\n}\n\n/** Per-user state managed by the bot */\nexport interface UserState {\n\t/** Active agent bridge (RPC process) */\n\tbridge: AgentBridge | null;\n\t/** Bot config — needed by buddy controller for RPC hatch/reroll */\n\tconfig: Config;\n\t/** Covers the race window between prompt() call and agent_start event */\n\tpromptInFlight: boolean;\n\t/** Flag to start a fresh session on next message */\n\tnewSessionFlag: boolean;\n\t/** Optional working directory override for the next new session */\n\tnewSessionCwd: string | null;\n\t/** The actual working directory of the current bridge (may differ from config default) */\n\teffectiveCwd: string | null;\n\t/** Currently running background agents */\n\tbackgroundAgents: Map<string, TrackedAgent>;\n\t/** Whether /stop was used (suppress DONE marker) */\n\tstopRequested: boolean;\n\t/** Messages waiting to be delivered to Telegram — drained by a delivery loop */\n\toutbox: Array<{ chatId: number; text: string; long?: boolean; retries?: number }>;\n\t/** Buddy controller — any to avoid import of @dreb/coding-agent/buddy */\n\tbuddyController: any;\n}\n\n/** Session info for persistence across bot restarts */\nexport interface SavedSessions {\n\t[userId: string]: Array<{\n\t\tsessionPath: string;\n\t\tsessionId: string;\n\t\ttimestamp: number;\n\t\tpreview: string;\n\t}>;\n}\n"]}
package/dist/types.js ADDED
@@ -0,0 +1,5 @@
1
+ /**
2
+ * Bot-specific types for the Telegram frontend.
3
+ */
4
+ export {};
5
+ //# sourceMappingURL=types.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"types.js","sourceRoot":"","sources":["../src/types.ts"],"names":[],"mappings":"AAAA;;GAEG","sourcesContent":["/**\n * Bot-specific types for the Telegram frontend.\n */\n\nimport type { AgentBridge } from \"./agent-bridge.js\";\nimport type { Config } from \"./config.js\";\n\n/** Tracked background agent */\nexport interface TrackedAgent {\n\tagentId: string;\n\tagentType: string;\n\ttaskSummary: string;\n\tstartTime: number;\n}\n\n/** Per-user state managed by the bot */\nexport interface UserState {\n\t/** Active agent bridge (RPC process) */\n\tbridge: AgentBridge | null;\n\t/** Bot config — needed by buddy controller for RPC hatch/reroll */\n\tconfig: Config;\n\t/** Covers the race window between prompt() call and agent_start event */\n\tpromptInFlight: boolean;\n\t/** Flag to start a fresh session on next message */\n\tnewSessionFlag: boolean;\n\t/** Optional working directory override for the next new session */\n\tnewSessionCwd: string | null;\n\t/** The actual working directory of the current bridge (may differ from config default) */\n\teffectiveCwd: string | null;\n\t/** Currently running background agents */\n\tbackgroundAgents: Map<string, TrackedAgent>;\n\t/** Whether /stop was used (suppress DONE marker) */\n\tstopRequested: boolean;\n\t/** Messages waiting to be delivered to Telegram — drained by a delivery loop */\n\toutbox: Array<{ chatId: number; text: string; long?: boolean; retries?: number }>;\n\t/** Buddy controller — any to avoid import of @dreb/coding-agent/buddy */\n\tbuddyController: any;\n}\n\n/** Session info for persistence across bot restarts */\nexport interface SavedSessions {\n\t[userId: string]: Array<{\n\t\tsessionPath: string;\n\t\tsessionId: string;\n\t\ttimestamp: number;\n\t\tpreview: string;\n\t}>;\n}\n"]}