@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.
- package/README.md +91 -0
- package/dist/agent-bridge.d.ts +146 -0
- package/dist/agent-bridge.d.ts.map +1 -0
- package/dist/agent-bridge.js +466 -0
- package/dist/agent-bridge.js.map +1 -0
- package/dist/bot.d.ts +11 -0
- package/dist/bot.d.ts.map +1 -0
- package/dist/bot.js +112 -0
- package/dist/bot.js.map +1 -0
- package/dist/bridge-lifecycle.d.ts +17 -0
- package/dist/bridge-lifecycle.d.ts.map +1 -0
- package/dist/bridge-lifecycle.js +71 -0
- package/dist/bridge-lifecycle.js.map +1 -0
- package/dist/commands/agent.d.ts +11 -0
- package/dist/commands/agent.d.ts.map +1 -0
- package/dist/commands/agent.js +171 -0
- package/dist/commands/agent.js.map +1 -0
- package/dist/commands/buddy.d.ts +20 -0
- package/dist/commands/buddy.d.ts.map +1 -0
- package/dist/commands/buddy.js +84 -0
- package/dist/commands/buddy.js.map +1 -0
- package/dist/commands/core.d.ts +13 -0
- package/dist/commands/core.d.ts.map +1 -0
- package/dist/commands/core.js +107 -0
- package/dist/commands/core.js.map +1 -0
- package/dist/commands/index.d.ts +16 -0
- package/dist/commands/index.d.ts.map +1 -0
- package/dist/commands/index.js +132 -0
- package/dist/commands/index.js.map +1 -0
- package/dist/commands/refresh.d.ts +18 -0
- package/dist/commands/refresh.d.ts.map +1 -0
- package/dist/commands/refresh.js +55 -0
- package/dist/commands/refresh.js.map +1 -0
- package/dist/commands/sessions.d.ts +10 -0
- package/dist/commands/sessions.d.ts.map +1 -0
- package/dist/commands/sessions.js +125 -0
- package/dist/commands/sessions.js.map +1 -0
- package/dist/commands/skills.d.ts +10 -0
- package/dist/commands/skills.d.ts.map +1 -0
- package/dist/commands/skills.js +48 -0
- package/dist/commands/skills.js.map +1 -0
- package/dist/config.d.ts +30 -0
- package/dist/config.d.ts.map +1 -0
- package/dist/config.js +77 -0
- package/dist/config.js.map +1 -0
- package/dist/handlers/buddy.d.ts +31 -0
- package/dist/handlers/buddy.d.ts.map +1 -0
- package/dist/handlers/buddy.js +126 -0
- package/dist/handlers/buddy.js.map +1 -0
- package/dist/handlers/events.d.ts +65 -0
- package/dist/handlers/events.d.ts.map +1 -0
- package/dist/handlers/events.js +381 -0
- package/dist/handlers/events.js.map +1 -0
- package/dist/handlers/file.d.ts +11 -0
- package/dist/handlers/file.d.ts.map +1 -0
- package/dist/handlers/file.js +138 -0
- package/dist/handlers/file.js.map +1 -0
- package/dist/handlers/message.d.ts +34 -0
- package/dist/handlers/message.d.ts.map +1 -0
- package/dist/handlers/message.js +262 -0
- package/dist/handlers/message.js.map +1 -0
- package/dist/index.d.ts +8 -0
- package/dist/index.d.ts.map +1 -0
- package/dist/index.js +82 -0
- package/dist/index.js.map +1 -0
- package/dist/state.d.ts +11 -0
- package/dist/state.d.ts.map +1 -0
- package/dist/state.js +47 -0
- package/dist/state.js.map +1 -0
- package/dist/types.d.ts +50 -0
- package/dist/types.d.ts.map +1 -0
- package/dist/types.js +5 -0
- package/dist/types.js.map +1 -0
- package/dist/util/files.d.ts +27 -0
- package/dist/util/files.d.ts.map +1 -0
- package/dist/util/files.js +75 -0
- package/dist/util/files.js.map +1 -0
- package/dist/util/telegram.d.ts +60 -0
- package/dist/util/telegram.d.ts.map +1 -0
- package/dist/util/telegram.js +192 -0
- package/dist/util/telegram.js.map +1 -0
- 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"]}
|