@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
package/README.md ADDED
@@ -0,0 +1,91 @@
1
+ # @dreb/telegram
2
+
3
+ Telegram bot frontend for the dreb coding agent. Communicates with dreb via its native RPC protocol (stdin/stdout JSONL).
4
+
5
+ ## Setup
6
+
7
+ ### 1. Create a Telegram bot
8
+
9
+ Talk to [@BotFather](https://t.me/BotFather) on Telegram and create a new bot with `/newbot`. Copy the token.
10
+
11
+ ### 2. Get your Telegram user ID
12
+
13
+ Message [@userinfobot](https://t.me/userinfobot) to get your numeric user ID.
14
+
15
+ ### 3. Configure environment variables
16
+
17
+ | Variable | Required | Description |
18
+ |----------|----------|-------------|
19
+ | `TELEGRAM_BOT_TOKEN` | ✅ | Bot API token from BotFather |
20
+ | `ALLOWED_USER_IDS` | ✅ | Comma-separated authorized user IDs |
21
+ | `DREB_WORKING_DIR` | | Working directory for sessions (default: `$HOME`) |
22
+ | `DREB_PATH` | | Path to dreb binary (default: `dreb`) |
23
+ | `DREB_TELEGRAM_SERVICE` | | Systemd service name (default: `dreb-telegram`) |
24
+ | `DREB_PROVIDER` | | LLM provider (e.g., `anthropic`) |
25
+ | `DREB_MODEL` | | Model ID (e.g., `claude-sonnet-4`) |
26
+
27
+ ### 4. Create secrets file
28
+
29
+ The bot loads secrets from `~/.dreb/secrets/telegram.env` automatically — both when run directly and via systemd.
30
+
31
+ ```bash
32
+ mkdir -p ~/.dreb/secrets
33
+ cat > ~/.dreb/secrets/telegram.env << 'EOF'
34
+ TELEGRAM_BOT_TOKEN=your-token-here
35
+ ALLOWED_USER_IDS=your-user-id-here
36
+ EOF
37
+ chmod 600 ~/.dreb/secrets/telegram.env
38
+ ```
39
+
40
+ This file is gitignored. Explicit environment variables take priority over the file.
41
+
42
+ ### 5. Build and run
43
+
44
+ ```bash
45
+ # From the monorepo root
46
+ npm run build
47
+
48
+ # Run directly (auto-loads secrets from ~/.dreb/secrets/telegram.env)
49
+ node packages/telegram/dist/index.js
50
+ ```
51
+
52
+ ### 6. Systemd service (recommended)
53
+
54
+ ```bash
55
+ cp packages/telegram/dreb-telegram.service.template ~/.config/systemd/user/dreb-telegram.service
56
+ systemctl --user daemon-reload
57
+ systemctl --user enable --now dreb-telegram
58
+ ```
59
+
60
+ ## Commands
61
+
62
+ ### Session
63
+ - `/start` — Help & command list
64
+ - `/new` — Start fresh session
65
+ - `/sessions` — List recent sessions
66
+ - `/resume <id>` — Resume by session ID prefix
67
+ - `/recent [N]` — Resend last N assistant messages
68
+
69
+ ### Agent
70
+ - `/status` — Connection & version info
71
+ - `/stats` — Token usage & cost
72
+ - `/compact` — Compact context
73
+ - `/model [pattern]` — View/switch model
74
+ - `/thinking [level]` — View/set thinking level
75
+ - `/agents` — Background subagent status
76
+
77
+ ### Control
78
+ - `/cwd` — Working directory
79
+ - `/stop` — Interrupt & clear queue
80
+ - `/restart` — Restart the bot service
81
+
82
+ ## Features
83
+
84
+ - **Per-user message queue** — one prompt at a time, incoming messages queued
85
+ - **Live tool display** — ephemeral status message shows tools, task lists, subagents
86
+ - **Rate-limited status updates** — debounced to avoid Telegram 429 errors
87
+ - **File upload** — documents, photos, voice, audio, video with 3s batching
88
+ - **File download** — `[[telegram:send:/path]]` markers in assistant text
89
+ - **Session management** — auto-resume latest, prefix matching, persistence
90
+ - **Markdown with fallback** — tries Markdown first, falls back to plain text
91
+ - **Process isolation** — one RPC subprocess per user, auto-restart on crash
@@ -0,0 +1,146 @@
1
+ /**
2
+ * Agent bridge — manages the RPC connection to a dreb agent process.
3
+ * One bridge per user, handles lifecycle, event subscription, and session management.
4
+ */
5
+ import { type RpcSessionInfo } from "@dreb/coding-agent/rpc";
6
+ import type { Config } from "./config.js";
7
+ /** RPC events include both AgentEvent and session-specific events */
8
+ type RpcEvent = {
9
+ type: string;
10
+ [key: string]: any;
11
+ };
12
+ export type AgentEventListener = (event: RpcEvent) => void;
13
+ export declare class AgentBridge {
14
+ private config;
15
+ private client;
16
+ private eventListeners;
17
+ private _isStreaming;
18
+ private _sessionFile;
19
+ private _sessionId;
20
+ private exited;
21
+ constructor(config: Config);
22
+ /** Whether the RPC process is alive */
23
+ get isAlive(): boolean;
24
+ /** Whether the agent is currently streaming a response */
25
+ get isStreaming(): boolean;
26
+ /** Current session file path */
27
+ get sessionFile(): string | undefined;
28
+ /** Current session ID */
29
+ get sessionId(): string | undefined;
30
+ /**
31
+ * Start the RPC process. Does NOT resume a session — call resumeLatest() or newSession() after.
32
+ */
33
+ start(): Promise<void>;
34
+ /**
35
+ * Resume the most recent session, or do nothing if no sessions exist.
36
+ */
37
+ resumeLatest(): Promise<boolean>;
38
+ /**
39
+ * List available sessions.
40
+ */
41
+ listSessions(): Promise<RpcSessionInfo[]>;
42
+ /**
43
+ * Switch to a specific session by path.
44
+ */
45
+ switchSession(sessionPath: string): Promise<boolean>;
46
+ /**
47
+ * Create a new session.
48
+ */
49
+ newSession(): Promise<boolean>;
50
+ /**
51
+ * Send a prompt to the agent.
52
+ */
53
+ prompt(message: string, images?: Array<{
54
+ type: "image";
55
+ data: string;
56
+ mimeType: string;
57
+ }>): Promise<void>;
58
+ /**
59
+ * Queue a steering message to interrupt the agent mid-run.
60
+ * The agent injects it after the current tool-call batch finishes.
61
+ */
62
+ steer(message: string, images?: Array<{
63
+ type: "image";
64
+ data: string;
65
+ mimeType: string;
66
+ }>): Promise<void>;
67
+ /**
68
+ * Queue a follow-up message for after the agent finishes its current run.
69
+ */
70
+ followUp(message: string, images?: Array<{
71
+ type: "image";
72
+ data: string;
73
+ mimeType: string;
74
+ }>): Promise<void>;
75
+ /**
76
+ * Abort the current operation.
77
+ */
78
+ abort(): Promise<void>;
79
+ /**
80
+ * Get the dreb version.
81
+ */
82
+ getVersion(): Promise<string>;
83
+ /**
84
+ * Get session statistics.
85
+ */
86
+ getSessionStats(): Promise<any>;
87
+ /**
88
+ * Get current state.
89
+ */
90
+ getState(): Promise<any>;
91
+ /**
92
+ * Get available commands (skills, extensions, prompt templates).
93
+ */
94
+ getCommands(): Promise<any[]>;
95
+ /**
96
+ * Compact context.
97
+ */
98
+ compact(): Promise<any>;
99
+ /**
100
+ * Get available models.
101
+ */
102
+ getAvailableModels(): Promise<any[]>;
103
+ /**
104
+ * Set model.
105
+ */
106
+ setModel(provider: string, modelId: string): Promise<any>;
107
+ /**
108
+ * Set thinking level.
109
+ */
110
+ setThinkingLevel(level: string): Promise<void>;
111
+ /**
112
+ * Get all messages.
113
+ */
114
+ getMessages(): Promise<any[]>;
115
+ /**
116
+ * Hatch a new buddy companion. Runs inside the agent process
117
+ * so API keys never cross the process boundary.
118
+ */
119
+ buddyHatch(): Promise<any>;
120
+ /**
121
+ * Reroll the buddy companion. Runs inside the agent process
122
+ * so API keys never cross the process boundary.
123
+ */
124
+ buddyReroll(): Promise<any>;
125
+ /**
126
+ * Get last assistant text.
127
+ */
128
+ getLastAssistantText(): Promise<string | null>;
129
+ /**
130
+ * Refresh session info from the RPC process state.
131
+ */
132
+ refreshSessionInfo(): Promise<void>;
133
+ /**
134
+ * Subscribe to agent events.
135
+ */
136
+ onEvent(listener: AgentEventListener): () => void;
137
+ /**
138
+ * Stop the RPC process.
139
+ */
140
+ stop(): Promise<void>;
141
+ private handleEvent;
142
+ private handleProcessError;
143
+ private ensureAlive;
144
+ }
145
+ export {};
146
+ //# sourceMappingURL=agent-bridge.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"agent-bridge.d.ts","sourceRoot":"","sources":["../src/agent-bridge.ts"],"names":[],"mappings":"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","sourcesContent":["/**\n * Agent bridge — manages the RPC connection to a dreb agent process.\n * One bridge per user, handles lifecycle, event subscription, and session management.\n */\n\nimport { dirname, join } from \"node:path\";\nimport { fileURLToPath } from \"node:url\";\nimport { RpcClient, type RpcSessionInfo } from \"@dreb/coding-agent/rpc\";\nimport type { Config } from \"./config.js\";\nimport { log } from \"./util/telegram.js\";\n\n/**\n * Resolve the absolute path to the dreb CLI entry point.\n * RpcClient defaults to \"dist/cli.js\" (relative to cwd), but we need\n * the absolute path since the bot's working dir differs from the dreb repo.\n */\nfunction resolveDrebCliPath(): string {\n\t// import.meta.resolve finds @dreb/coding-agent/dist/index.js\n\tconst resolved = import.meta.resolve(\"@dreb/coding-agent\");\n\tconst distDir = dirname(fileURLToPath(resolved));\n\treturn join(distDir, \"cli.js\");\n}\n\n/** RPC events include both AgentEvent and session-specific events */\ntype RpcEvent = { type: string; [key: string]: any };\n\nexport type AgentEventListener = (event: RpcEvent) => void;\n\nexport class AgentBridge {\n\tprivate client: RpcClient | null = null;\n\tprivate eventListeners: AgentEventListener[] = [];\n\tprivate _isStreaming = false;\n\tprivate _sessionFile: string | undefined;\n\tprivate _sessionId: string | undefined;\n\tprivate exited = false;\n\n\tconstructor(private config: Config) {}\n\n\t/** Whether the RPC process is alive */\n\tget isAlive(): boolean {\n\t\treturn this.client !== null && !this.exited;\n\t}\n\n\t/** Whether the agent is currently streaming a response */\n\tget isStreaming(): boolean {\n\t\treturn this._isStreaming;\n\t}\n\n\t/** Current session file path */\n\tget sessionFile(): string | undefined {\n\t\treturn this._sessionFile;\n\t}\n\n\t/** Current session ID */\n\tget sessionId(): string | undefined {\n\t\treturn this._sessionId;\n\t}\n\n\t/**\n\t * Start the RPC process. Does NOT resume a session — call resumeLatest() or newSession() after.\n\t */\n\tasync start(): Promise<void> {\n\t\tif (this.client) return;\n\n\t\tthis.client = new RpcClient({\n\t\t\tcliPath: resolveDrebCliPath(),\n\t\t\tcwd: this.config.workingDir,\n\t\t\tprovider: this.config.provider,\n\t\t\tmodel: this.config.model,\n\t\t\targs: [\"--ui\", \"telegram\"],\n\t\t});\n\n\t\tthis.exited = false;\n\t\tawait this.client.start();\n\n\t\t// Subscribe to events and forward to listeners\n\t\t// Cast: RpcClient types events as AgentEvent but actually forwards all AgentSessionEvent types\n\t\tthis.client.onEvent((event) => {\n\t\t\tthis.handleEvent(event as RpcEvent);\n\t\t});\n\n\t\t// Detect process exit\n\t\t// RpcClient doesn't expose a direct \"on exit\" — we detect it when send() fails\n\t\tlog(\"[BRIDGE] RPC process started\");\n\t}\n\n\t/**\n\t * Resume the most recent session, or do nothing if no sessions exist.\n\t */\n\tasync resumeLatest(): Promise<boolean> {\n\t\tif (!this.client) return false;\n\t\ttry {\n\t\t\tconst sessions = await this.client.listSessions();\n\t\t\tif (sessions.length === 0) return false;\n\n\t\t\tconst latest = sessions[0]; // Already sorted by modified desc\n\t\t\tconst result = await this.client.switchSession(latest.path);\n\t\t\tif (!result.cancelled) {\n\t\t\t\tthis._sessionFile = latest.path;\n\t\t\t\tthis._sessionId = latest.id;\n\t\t\t\tlog(`[BRIDGE] Resumed session ${latest.id.slice(0, 8)}`);\n\t\t\t\treturn true;\n\t\t\t}\n\t\t} catch (e) {\n\t\t\tlog(`[BRIDGE] Failed to resume latest session: ${e}`);\n\t\t}\n\t\treturn false;\n\t}\n\n\t/**\n\t * List available sessions.\n\t */\n\tasync listSessions(): Promise<RpcSessionInfo[]> {\n\t\tif (!this.client) return [];\n\t\ttry {\n\t\t\treturn await this.client.listSessions();\n\t\t} catch (e) {\n\t\t\tlog(`[BRIDGE] Failed to list sessions: ${e}`);\n\t\t\treturn [];\n\t\t}\n\t}\n\n\t/**\n\t * Switch to a specific session by path.\n\t */\n\tasync switchSession(sessionPath: string): Promise<boolean> {\n\t\tif (!this.client) return false;\n\t\ttry {\n\t\t\tconst result = await this.client.switchSession(sessionPath);\n\t\t\tif (!result.cancelled) {\n\t\t\t\tthis._sessionFile = sessionPath;\n\t\t\t\tconst state = await this.client.getState();\n\t\t\t\tthis._sessionId = state.sessionId;\n\t\t\t\treturn true;\n\t\t\t}\n\t\t} catch (e) {\n\t\t\tlog(`[BRIDGE] Failed to switch session: ${e}`);\n\t\t}\n\t\treturn false;\n\t}\n\n\t/**\n\t * Create a new session.\n\t */\n\tasync newSession(): Promise<boolean> {\n\t\tif (!this.client) return false;\n\t\ttry {\n\t\t\tconst result = await this.client.newSession();\n\t\t\tif (!result.cancelled) {\n\t\t\t\tconst state = await this.client.getState();\n\t\t\t\tthis._sessionFile = state.sessionFile;\n\t\t\t\tthis._sessionId = state.sessionId;\n\t\t\t\tlog(`[BRIDGE] New session ${state.sessionId.slice(0, 8)}`);\n\t\t\t\treturn true;\n\t\t\t}\n\t\t} catch (e) {\n\t\t\tlog(`[BRIDGE] Failed to create new session: ${e}`);\n\t\t}\n\t\treturn false;\n\t}\n\n\t/**\n\t * Send a prompt to the agent.\n\t */\n\tasync prompt(message: string, images?: Array<{ type: \"image\"; data: string; mimeType: string }>): Promise<void> {\n\t\tawait this.ensureAlive();\n\t\ttry {\n\t\t\tawait this.client!.prompt(message, images);\n\t\t} catch (e) {\n\t\t\tthis.handleProcessError(e);\n\t\t\tthrow e;\n\t\t}\n\t}\n\n\t/**\n\t * Queue a steering message to interrupt the agent mid-run.\n\t * The agent injects it after the current tool-call batch finishes.\n\t */\n\tasync steer(message: string, images?: Array<{ type: \"image\"; data: string; mimeType: string }>): Promise<void> {\n\t\tawait this.ensureAlive();\n\t\ttry {\n\t\t\tawait this.client!.steer(message, images);\n\t\t} catch (e) {\n\t\t\tthis.handleProcessError(e);\n\t\t\tthrow e;\n\t\t}\n\t}\n\n\t/**\n\t * Queue a follow-up message for after the agent finishes its current run.\n\t */\n\tasync followUp(message: string, images?: Array<{ type: \"image\"; data: string; mimeType: string }>): Promise<void> {\n\t\tawait this.ensureAlive();\n\t\ttry {\n\t\t\tawait this.client!.followUp(message, images);\n\t\t} catch (e) {\n\t\t\tthis.handleProcessError(e);\n\t\t\tthrow e;\n\t\t}\n\t}\n\n\t/**\n\t * Abort the current operation.\n\t */\n\tasync abort(): Promise<void> {\n\t\tif (!this.client) return;\n\t\ttry {\n\t\t\tawait this.client.abort();\n\t\t} catch {\n\t\t\t// Process may have already exited\n\t\t}\n\t}\n\n\t/**\n\t * Get the dreb version.\n\t */\n\tasync getVersion(): Promise<string> {\n\t\tawait this.ensureAlive();\n\t\ttry {\n\t\t\treturn await this.client!.getVersion();\n\t\t} catch (e) {\n\t\t\tthis.handleProcessError(e);\n\t\t\tthrow e;\n\t\t}\n\t}\n\n\t/**\n\t * Get session statistics.\n\t */\n\tasync getSessionStats(): Promise<any> {\n\t\tif (!this.client) return null;\n\t\ttry {\n\t\t\treturn await this.client.getSessionStats();\n\t\t} catch (e) {\n\t\t\tthis.handleProcessError(e);\n\t\t\tthrow e;\n\t\t}\n\t}\n\n\t/**\n\t * Get current state.\n\t */\n\tasync getState(): Promise<any> {\n\t\tif (!this.client) return null;\n\t\ttry {\n\t\t\treturn await this.client.getState();\n\t\t} catch (e) {\n\t\t\tthis.handleProcessError(e);\n\t\t\tthrow e;\n\t\t}\n\t}\n\n\t/**\n\t * Get available commands (skills, extensions, prompt templates).\n\t */\n\tasync getCommands(): Promise<any[]> {\n\t\tif (!this.client) return [];\n\t\ttry {\n\t\t\treturn await this.client.getCommands();\n\t\t} catch (e) {\n\t\t\tthis.handleProcessError(e);\n\t\t\tthrow e;\n\t\t}\n\t}\n\n\t/**\n\t * Compact context.\n\t */\n\tasync compact(): Promise<any> {\n\t\tif (!this.client) return null;\n\t\ttry {\n\t\t\treturn await this.client.compact();\n\t\t} catch (e) {\n\t\t\tthis.handleProcessError(e);\n\t\t\tthrow e;\n\t\t}\n\t}\n\n\t/**\n\t * Get available models.\n\t */\n\tasync getAvailableModels(): Promise<any[]> {\n\t\tif (!this.client) return [];\n\t\ttry {\n\t\t\treturn await this.client.getAvailableModels();\n\t\t} catch (e) {\n\t\t\tthis.handleProcessError(e);\n\t\t\tthrow e;\n\t\t}\n\t}\n\n\t/**\n\t * Set model.\n\t */\n\tasync setModel(provider: string, modelId: string): Promise<any> {\n\t\tif (!this.client) return null;\n\t\ttry {\n\t\t\treturn await this.client.setModel(provider, modelId);\n\t\t} catch (e) {\n\t\t\tthis.handleProcessError(e);\n\t\t\tthrow e;\n\t\t}\n\t}\n\n\t/**\n\t * Set thinking level.\n\t */\n\tasync setThinkingLevel(level: string): Promise<void> {\n\t\tif (!this.client) return;\n\t\ttry {\n\t\t\tawait this.client.setThinkingLevel(level as any);\n\t\t} catch (e) {\n\t\t\tthis.handleProcessError(e);\n\t\t\tthrow e;\n\t\t}\n\t}\n\n\t/**\n\t * Get all messages.\n\t */\n\tasync getMessages(): Promise<any[]> {\n\t\tif (!this.client) return [];\n\t\ttry {\n\t\t\treturn await this.client.getMessages();\n\t\t} catch (e) {\n\t\t\tthis.handleProcessError(e);\n\t\t\tthrow e;\n\t\t}\n\t}\n\n\t/**\n\t * Hatch a new buddy companion. Runs inside the agent process\n\t * so API keys never cross the process boundary.\n\t */\n\tasync buddyHatch(): Promise<any> {\n\t\tif (!this.client) throw new Error(\"Agent not connected.\");\n\t\ttry {\n\t\t\treturn await this.client.buddyHatch();\n\t\t} catch (e) {\n\t\t\tthis.handleProcessError(e);\n\t\t\tthrow e;\n\t\t}\n\t}\n\n\t/**\n\t * Reroll the buddy companion. Runs inside the agent process\n\t * so API keys never cross the process boundary.\n\t */\n\tasync buddyReroll(): Promise<any> {\n\t\tif (!this.client) throw new Error(\"Agent not connected.\");\n\t\ttry {\n\t\t\treturn await this.client.buddyReroll();\n\t\t} catch (e) {\n\t\t\tthis.handleProcessError(e);\n\t\t\tthrow e;\n\t\t}\n\t}\n\n\t/**\n\t * Get last assistant text.\n\t */\n\tasync getLastAssistantText(): Promise<string | null> {\n\t\tif (!this.client) return null;\n\t\ttry {\n\t\t\treturn await this.client.getLastAssistantText();\n\t\t} catch (e) {\n\t\t\tthis.handleProcessError(e);\n\t\t\tthrow e;\n\t\t}\n\t}\n\n\t/**\n\t * Refresh session info from the RPC process state.\n\t */\n\tasync refreshSessionInfo(): Promise<void> {\n\t\tif (!this.client) return;\n\t\ttry {\n\t\t\tconst state = await this.client.getState();\n\t\t\tthis._sessionFile = state.sessionFile;\n\t\t\tthis._sessionId = state.sessionId;\n\t\t} catch (e) {\n\t\t\tthis.handleProcessError(e);\n\t\t\t// Non-critical — don't re-throw\n\t\t}\n\t}\n\n\t/**\n\t * Subscribe to agent events.\n\t */\n\tonEvent(listener: AgentEventListener): () => void {\n\t\tthis.eventListeners.push(listener);\n\t\treturn () => {\n\t\t\tconst idx = this.eventListeners.indexOf(listener);\n\t\t\tif (idx !== -1) this.eventListeners.splice(idx, 1);\n\t\t};\n\t}\n\n\t/**\n\t * Stop the RPC process.\n\t */\n\tasync stop(): Promise<void> {\n\t\tif (this.client) {\n\t\t\ttry {\n\t\t\t\tawait this.client.stop();\n\t\t\t} catch {\n\t\t\t\t// Ignore\n\t\t\t}\n\t\t\tthis.client = null;\n\t\t\tthis.exited = true;\n\t\t\tthis.eventListeners = [];\n\t\t\tlog(\"[BRIDGE] RPC process stopped\");\n\t\t}\n\t}\n\n\t// =========================================================================\n\t// Internal\n\t// =========================================================================\n\n\tprivate handleEvent(event: RpcEvent): void {\n\t\t// Track streaming state\n\t\tif (event.type === \"agent_start\") this._isStreaming = true;\n\t\tif (event.type === \"agent_end\") {\n\t\t\tthis._isStreaming = false;\n\t\t\t// Capture session info from agent_end messages\n\t\t\t// Session file/id updates happen via getState after prompt\n\t\t}\n\n\t\tfor (const listener of this.eventListeners) {\n\t\t\ttry {\n\t\t\t\tlistener(event);\n\t\t\t} catch (e) {\n\t\t\t\tlog(`[BRIDGE] Event listener error: ${e}`);\n\t\t\t}\n\t\t}\n\t}\n\n\tprivate handleProcessError(e: unknown): void {\n\t\tconst msg = e instanceof Error ? e.message : String(e);\n\t\tif (\n\t\t\tmsg.includes(\"not started\") ||\n\t\t\tmsg.includes(\"not running\") ||\n\t\t\tmsg.includes(\"EPIPE\") ||\n\t\t\tmsg.includes(\"write after end\") ||\n\t\t\tmsg.includes(\"Timeout waiting for response\") ||\n\t\t\tmsg.includes(\"RPC process exited\")\n\t\t) {\n\t\t\tlog(`[BRIDGE] RPC process exited or hung: ${msg.slice(0, 100)}`);\n\t\t\t// Kill the child process before dropping the reference to prevent orphans\n\t\t\tif (this.client) {\n\t\t\t\tthis.client.stop().catch(() => {});\n\t\t\t}\n\t\t\tthis.exited = true;\n\t\t\tthis.client = null;\n\t\t}\n\t}\n\n\tprivate async ensureAlive(): Promise<void> {\n\t\tif (!this.client || this.exited) {\n\t\t\tlog(\"[BRIDGE] Restarting dead RPC process\");\n\t\t\tthis.client = null;\n\t\t\tthis.exited = false;\n\t\t\tawait this.start();\n\t\t\t// Session selection is handled by ensureBridgeWithSession — not here.\n\t\t\t// The previous session is still the latest and will be picked up by\n\t\t\t// resumeLatest() on the next message.\n\t\t}\n\t}\n}\n"]}