@openacp/cli 0.2.4 → 0.2.11

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.
@@ -0,0 +1,1481 @@
1
+ import {
2
+ log
3
+ } from "./chunk-KADEDKIM.js";
4
+
5
+ // src/core/streams.ts
6
+ function nodeToWebWritable(nodeStream) {
7
+ return new WritableStream({
8
+ write(chunk) {
9
+ return new Promise((resolve, reject) => {
10
+ nodeStream.write(Buffer.from(chunk), (err) => {
11
+ if (err) reject(err);
12
+ else resolve();
13
+ });
14
+ });
15
+ }
16
+ });
17
+ }
18
+ function nodeToWebReadable(nodeStream) {
19
+ return new ReadableStream({
20
+ start(controller) {
21
+ nodeStream.on("data", (chunk) => {
22
+ controller.enqueue(new Uint8Array(chunk));
23
+ });
24
+ nodeStream.on("end", () => controller.close());
25
+ nodeStream.on("error", (err) => controller.error(err));
26
+ }
27
+ });
28
+ }
29
+
30
+ // src/core/stderr-capture.ts
31
+ var StderrCapture = class {
32
+ constructor(maxLines = 50) {
33
+ this.maxLines = maxLines;
34
+ }
35
+ lines = [];
36
+ append(chunk) {
37
+ this.lines.push(...chunk.split("\n").filter(Boolean));
38
+ if (this.lines.length > this.maxLines) {
39
+ this.lines = this.lines.slice(-this.maxLines);
40
+ }
41
+ }
42
+ getLastLines() {
43
+ return this.lines.join("\n");
44
+ }
45
+ };
46
+
47
+ // src/core/agent-instance.ts
48
+ import { spawn, execSync } from "child_process";
49
+ import fs from "fs";
50
+ import path from "path";
51
+ import { randomUUID } from "crypto";
52
+ import { ClientSideConnection, ndJsonStream } from "@agentclientprotocol/sdk";
53
+ function resolveAgentCommand(cmd) {
54
+ const packageDirs = [
55
+ path.resolve(process.cwd(), "node_modules", "@zed-industries", cmd, "dist", "index.js"),
56
+ path.resolve(process.cwd(), "node_modules", cmd, "dist", "index.js")
57
+ ];
58
+ for (const jsPath of packageDirs) {
59
+ if (fs.existsSync(jsPath)) {
60
+ return { command: process.execPath, args: [jsPath] };
61
+ }
62
+ }
63
+ const localBin = path.resolve(process.cwd(), "node_modules", ".bin", cmd);
64
+ if (fs.existsSync(localBin)) {
65
+ const content = fs.readFileSync(localBin, "utf-8");
66
+ if (content.startsWith("#!/usr/bin/env node")) {
67
+ return { command: process.execPath, args: [localBin] };
68
+ }
69
+ const match = content.match(/"([^"]+\.js)"/);
70
+ if (match) {
71
+ const target = path.resolve(path.dirname(localBin), match[1]);
72
+ if (fs.existsSync(target)) {
73
+ return { command: process.execPath, args: [target] };
74
+ }
75
+ }
76
+ }
77
+ try {
78
+ const fullPath = execSync(`which ${cmd}`, { encoding: "utf-8" }).trim();
79
+ if (fullPath) {
80
+ const content = fs.readFileSync(fullPath, "utf-8");
81
+ if (content.startsWith("#!/usr/bin/env node")) {
82
+ return { command: process.execPath, args: [fullPath] };
83
+ }
84
+ }
85
+ } catch {
86
+ }
87
+ return { command: cmd, args: [] };
88
+ }
89
+ var AgentInstance = class _AgentInstance {
90
+ connection;
91
+ child;
92
+ stderrCapture;
93
+ terminals = /* @__PURE__ */ new Map();
94
+ sessionId;
95
+ agentName;
96
+ // Callbacks — set by core when wiring events
97
+ onSessionUpdate = () => {
98
+ };
99
+ onPermissionRequest = async () => "";
100
+ constructor(agentName) {
101
+ this.agentName = agentName;
102
+ }
103
+ static async spawn(agentDef, workingDirectory) {
104
+ const instance = new _AgentInstance(agentDef.name);
105
+ const resolved = resolveAgentCommand(agentDef.command);
106
+ log.debug(`Spawning agent "${agentDef.name}" \u2192 ${resolved.command} ${resolved.args.join(" ")}`);
107
+ instance.child = spawn(resolved.command, [...resolved.args, ...agentDef.args], {
108
+ stdio: ["pipe", "pipe", "pipe"],
109
+ cwd: workingDirectory,
110
+ env: { ...process.env, ...agentDef.env }
111
+ });
112
+ await new Promise((resolve, reject) => {
113
+ instance.child.on("error", (err) => {
114
+ reject(new Error(`Failed to spawn agent "${agentDef.name}": ${err.message}. Is "${agentDef.command}" installed?`));
115
+ });
116
+ instance.child.on("spawn", () => resolve());
117
+ });
118
+ instance.stderrCapture = new StderrCapture(50);
119
+ instance.child.stderr.on("data", (chunk) => {
120
+ instance.stderrCapture.append(chunk.toString());
121
+ });
122
+ const toAgent = nodeToWebWritable(instance.child.stdin);
123
+ const fromAgent = nodeToWebReadable(instance.child.stdout);
124
+ const stream = ndJsonStream(toAgent, fromAgent);
125
+ instance.connection = new ClientSideConnection(
126
+ (_agent) => instance.createClient(_agent),
127
+ stream
128
+ );
129
+ await instance.connection.initialize({
130
+ protocolVersion: 1,
131
+ clientCapabilities: {
132
+ fs: { readTextFile: true, writeTextFile: true },
133
+ terminal: true
134
+ }
135
+ });
136
+ const response = await instance.connection.newSession({
137
+ cwd: workingDirectory,
138
+ mcpServers: []
139
+ });
140
+ instance.sessionId = response.sessionId;
141
+ instance.child.on("exit", (code, signal) => {
142
+ if (code !== 0 && code !== null) {
143
+ const stderr = instance.stderrCapture.getLastLines();
144
+ instance.onSessionUpdate({
145
+ type: "error",
146
+ message: `Agent crashed (exit code ${code})
147
+ ${stderr}`
148
+ });
149
+ }
150
+ });
151
+ instance.connection.closed.then(() => {
152
+ log.debug("ACP connection closed for", instance.agentName);
153
+ });
154
+ log.info(`Agent "${agentDef.name}" spawned with session ${response.sessionId}`);
155
+ return instance;
156
+ }
157
+ // createClient — implemented in Task 6b
158
+ createClient(_agent) {
159
+ const self = this;
160
+ const MAX_OUTPUT_BYTES = 1024 * 1024;
161
+ return {
162
+ // ── Session updates ──────────────────────────────────────────────────
163
+ async sessionUpdate(params) {
164
+ const update = params.update;
165
+ let event = null;
166
+ switch (update.sessionUpdate) {
167
+ case "agent_message_chunk":
168
+ if (update.content.type === "text") {
169
+ event = { type: "text", content: update.content.text };
170
+ }
171
+ break;
172
+ case "agent_thought_chunk":
173
+ if (update.content.type === "text") {
174
+ event = { type: "thought", content: update.content.text };
175
+ }
176
+ break;
177
+ case "tool_call":
178
+ event = {
179
+ type: "tool_call",
180
+ id: update.toolCallId,
181
+ name: update.title,
182
+ kind: update.kind ?? void 0,
183
+ status: update.status ?? "pending",
184
+ content: update.content ?? void 0
185
+ };
186
+ break;
187
+ case "tool_call_update":
188
+ event = {
189
+ type: "tool_update",
190
+ id: update.toolCallId,
191
+ status: update.status ?? "pending",
192
+ content: update.content ?? void 0
193
+ };
194
+ break;
195
+ case "plan":
196
+ event = { type: "plan", entries: update.entries };
197
+ break;
198
+ case "usage_update":
199
+ event = {
200
+ type: "usage",
201
+ tokensUsed: update.used,
202
+ contextSize: update.size,
203
+ cost: update.cost ?? void 0
204
+ };
205
+ break;
206
+ case "available_commands_update":
207
+ event = { type: "commands_update", commands: update.availableCommands };
208
+ break;
209
+ default:
210
+ return;
211
+ }
212
+ if (event !== null) {
213
+ self.onSessionUpdate(event);
214
+ }
215
+ },
216
+ // ── Permission requests ──────────────────────────────────────────────
217
+ async requestPermission(params) {
218
+ const permissionRequest = {
219
+ id: params.toolCall.toolCallId,
220
+ description: params.toolCall.title ?? params.toolCall.toolCallId,
221
+ options: params.options.map((opt) => ({
222
+ id: opt.optionId,
223
+ label: opt.name,
224
+ isAllow: opt.kind === "allow_once" || opt.kind === "allow_always"
225
+ }))
226
+ };
227
+ const selectedOptionId = await self.onPermissionRequest(permissionRequest);
228
+ return {
229
+ outcome: { outcome: "selected", optionId: selectedOptionId }
230
+ };
231
+ },
232
+ // ── File operations ──────────────────────────────────────────────────
233
+ async readTextFile(params) {
234
+ const content = await fs.promises.readFile(params.path, "utf-8");
235
+ return { content };
236
+ },
237
+ async writeTextFile(params) {
238
+ await fs.promises.mkdir(path.dirname(params.path), { recursive: true });
239
+ await fs.promises.writeFile(params.path, params.content, "utf-8");
240
+ return {};
241
+ },
242
+ // ── Terminal operations ──────────────────────────────────────────────
243
+ async createTerminal(params) {
244
+ const terminalId = randomUUID();
245
+ const args = params.args ?? [];
246
+ const env = {};
247
+ for (const ev of params.env ?? []) {
248
+ env[ev.name] = ev.value;
249
+ }
250
+ const childProcess = spawn(params.command, args, {
251
+ cwd: params.cwd ?? void 0,
252
+ env: { ...process.env, ...env },
253
+ shell: false
254
+ });
255
+ const state = {
256
+ process: childProcess,
257
+ output: "",
258
+ exitStatus: null
259
+ };
260
+ self.terminals.set(terminalId, state);
261
+ const outputByteLimit = params.outputByteLimit ?? MAX_OUTPUT_BYTES;
262
+ const appendOutput = (chunk) => {
263
+ state.output += chunk;
264
+ const bytes = Buffer.byteLength(state.output, "utf-8");
265
+ if (bytes > outputByteLimit) {
266
+ const excess = bytes - outputByteLimit;
267
+ state.output = state.output.slice(excess);
268
+ }
269
+ };
270
+ childProcess.stdout?.on("data", (chunk) => appendOutput(chunk.toString()));
271
+ childProcess.stderr?.on("data", (chunk) => appendOutput(chunk.toString()));
272
+ childProcess.on("exit", (code, signal) => {
273
+ state.exitStatus = { exitCode: code, signal };
274
+ });
275
+ return { terminalId };
276
+ },
277
+ async terminalOutput(params) {
278
+ const state = self.terminals.get(params.terminalId);
279
+ if (!state) {
280
+ throw new Error(`Terminal not found: ${params.terminalId}`);
281
+ }
282
+ return {
283
+ output: state.output,
284
+ truncated: false,
285
+ exitStatus: state.exitStatus ? { exitCode: state.exitStatus.exitCode, signal: state.exitStatus.signal } : void 0
286
+ };
287
+ },
288
+ async waitForTerminalExit(params) {
289
+ const state = self.terminals.get(params.terminalId);
290
+ if (!state) {
291
+ throw new Error(`Terminal not found: ${params.terminalId}`);
292
+ }
293
+ if (state.exitStatus !== null) {
294
+ return { exitCode: state.exitStatus.exitCode, signal: state.exitStatus.signal };
295
+ }
296
+ return new Promise((resolve) => {
297
+ state.process.on("exit", (code, signal) => {
298
+ resolve({ exitCode: code, signal });
299
+ });
300
+ });
301
+ },
302
+ async killTerminal(params) {
303
+ const state = self.terminals.get(params.terminalId);
304
+ if (!state) {
305
+ throw new Error(`Terminal not found: ${params.terminalId}`);
306
+ }
307
+ state.process.kill("SIGTERM");
308
+ return {};
309
+ },
310
+ async releaseTerminal(params) {
311
+ const state = self.terminals.get(params.terminalId);
312
+ if (!state) {
313
+ return;
314
+ }
315
+ state.process.kill("SIGKILL");
316
+ self.terminals.delete(params.terminalId);
317
+ }
318
+ };
319
+ }
320
+ async prompt(text) {
321
+ return this.connection.prompt({
322
+ sessionId: this.sessionId,
323
+ prompt: [{ type: "text", text }]
324
+ });
325
+ }
326
+ async cancel() {
327
+ await this.connection.cancel({ sessionId: this.sessionId });
328
+ }
329
+ async destroy() {
330
+ for (const [, t] of this.terminals) {
331
+ t.process.kill("SIGKILL");
332
+ }
333
+ this.terminals.clear();
334
+ this.child.kill("SIGTERM");
335
+ setTimeout(() => {
336
+ if (!this.child.killed) this.child.kill("SIGKILL");
337
+ }, 1e4);
338
+ }
339
+ };
340
+
341
+ // src/core/agent-manager.ts
342
+ var AgentManager = class {
343
+ constructor(config) {
344
+ this.config = config;
345
+ }
346
+ getAvailableAgents() {
347
+ return Object.entries(this.config.agents).map(([name, cfg]) => ({
348
+ name,
349
+ command: cfg.command,
350
+ args: cfg.args,
351
+ workingDirectory: cfg.workingDirectory,
352
+ env: cfg.env
353
+ }));
354
+ }
355
+ getAgent(name) {
356
+ const cfg = this.config.agents[name];
357
+ if (!cfg) return void 0;
358
+ return { name, ...cfg };
359
+ }
360
+ async spawn(agentName, workingDirectory) {
361
+ const agentDef = this.getAgent(agentName);
362
+ if (!agentDef) throw new Error(`Agent "${agentName}" not found in config`);
363
+ return AgentInstance.spawn(agentDef, workingDirectory);
364
+ }
365
+ };
366
+
367
+ // src/core/session.ts
368
+ import { nanoid } from "nanoid";
369
+ var Session = class {
370
+ id;
371
+ channelId;
372
+ threadId = "";
373
+ agentName;
374
+ workingDirectory;
375
+ agentInstance;
376
+ status = "initializing";
377
+ name;
378
+ promptQueue = [];
379
+ promptRunning = false;
380
+ createdAt = /* @__PURE__ */ new Date();
381
+ adapter;
382
+ // Set by wireSessionEvents for renaming
383
+ pendingPermission;
384
+ constructor(opts) {
385
+ this.id = opts.id || nanoid(12);
386
+ this.channelId = opts.channelId;
387
+ this.agentName = opts.agentName;
388
+ this.workingDirectory = opts.workingDirectory;
389
+ this.agentInstance = opts.agentInstance;
390
+ }
391
+ async enqueuePrompt(text) {
392
+ if (this.promptRunning) {
393
+ this.promptQueue.push(text);
394
+ log.debug(`Prompt queued for session ${this.id} (${this.promptQueue.length} in queue)`);
395
+ return;
396
+ }
397
+ await this.runPrompt(text);
398
+ }
399
+ async runPrompt(text) {
400
+ this.promptRunning = true;
401
+ this.status = "active";
402
+ try {
403
+ await this.agentInstance.prompt(text);
404
+ if (!this.name) {
405
+ await this.autoName();
406
+ }
407
+ } catch (err) {
408
+ this.status = "error";
409
+ log.error(`Prompt failed for session ${this.id}:`, err);
410
+ } finally {
411
+ this.promptRunning = false;
412
+ if (this.promptQueue.length > 0) {
413
+ const next = this.promptQueue.shift();
414
+ await this.runPrompt(next);
415
+ }
416
+ }
417
+ }
418
+ // NOTE: This injects a summary prompt into the agent's conversation history.
419
+ // Known Phase 1 limitation — the agent sees this prompt in its context.
420
+ async autoName() {
421
+ let title = "";
422
+ const prevHandler = this.agentInstance.onSessionUpdate;
423
+ this.agentInstance.onSessionUpdate = (event) => {
424
+ if (event.type === "text") title += event.content;
425
+ };
426
+ try {
427
+ await this.agentInstance.prompt(
428
+ "Summarize this conversation in max 5 words for a topic title. Reply ONLY with the title, nothing else."
429
+ );
430
+ this.name = title.trim().slice(0, 50);
431
+ if (this.adapter && this.name) {
432
+ await this.adapter.renameSessionThread(this.id, this.name);
433
+ }
434
+ } catch {
435
+ this.name = `Session ${this.id.slice(0, 6)}`;
436
+ } finally {
437
+ this.agentInstance.onSessionUpdate = prevHandler;
438
+ }
439
+ }
440
+ async cancel() {
441
+ this.status = "cancelled";
442
+ await this.agentInstance.cancel();
443
+ }
444
+ async destroy() {
445
+ await this.agentInstance.destroy();
446
+ }
447
+ };
448
+
449
+ // src/core/session-manager.ts
450
+ var SessionManager = class {
451
+ sessions = /* @__PURE__ */ new Map();
452
+ async createSession(channelId, agentName, workingDirectory, agentManager) {
453
+ const agentInstance = await agentManager.spawn(agentName, workingDirectory);
454
+ const session = new Session({ channelId, agentName, workingDirectory, agentInstance });
455
+ this.sessions.set(session.id, session);
456
+ return session;
457
+ }
458
+ getSession(sessionId) {
459
+ return this.sessions.get(sessionId);
460
+ }
461
+ getSessionByThread(channelId, threadId) {
462
+ for (const session of this.sessions.values()) {
463
+ if (session.channelId === channelId && session.threadId === threadId) {
464
+ return session;
465
+ }
466
+ }
467
+ return void 0;
468
+ }
469
+ async cancelSession(sessionId) {
470
+ const session = this.sessions.get(sessionId);
471
+ if (session) await session.cancel();
472
+ }
473
+ listSessions(channelId) {
474
+ const all = Array.from(this.sessions.values());
475
+ if (channelId) return all.filter((s) => s.channelId === channelId);
476
+ return all;
477
+ }
478
+ async destroyAll() {
479
+ for (const session of this.sessions.values()) {
480
+ await session.destroy();
481
+ }
482
+ this.sessions.clear();
483
+ }
484
+ };
485
+
486
+ // src/core/notification.ts
487
+ var NotificationManager = class {
488
+ constructor(adapters) {
489
+ this.adapters = adapters;
490
+ }
491
+ async notify(channelId, notification) {
492
+ const adapter = this.adapters.get(channelId);
493
+ if (adapter) {
494
+ await adapter.sendNotification(notification);
495
+ }
496
+ }
497
+ async notifyAll(notification) {
498
+ for (const adapter of this.adapters.values()) {
499
+ await adapter.sendNotification(notification);
500
+ }
501
+ }
502
+ };
503
+
504
+ // src/core/core.ts
505
+ var OpenACPCore = class {
506
+ configManager;
507
+ agentManager;
508
+ sessionManager;
509
+ notificationManager;
510
+ adapters = /* @__PURE__ */ new Map();
511
+ constructor(configManager) {
512
+ this.configManager = configManager;
513
+ const config = configManager.get();
514
+ this.agentManager = new AgentManager(config);
515
+ this.sessionManager = new SessionManager();
516
+ this.notificationManager = new NotificationManager(this.adapters);
517
+ }
518
+ registerAdapter(name, adapter) {
519
+ this.adapters.set(name, adapter);
520
+ }
521
+ async start() {
522
+ for (const adapter of this.adapters.values()) {
523
+ await adapter.start();
524
+ }
525
+ }
526
+ async stop() {
527
+ try {
528
+ await this.notificationManager.notifyAll({
529
+ sessionId: "system",
530
+ type: "error",
531
+ summary: "OpenACP is shutting down"
532
+ });
533
+ } catch {
534
+ }
535
+ await this.sessionManager.destroyAll();
536
+ for (const adapter of this.adapters.values()) {
537
+ await adapter.stop();
538
+ }
539
+ }
540
+ // --- Message Routing ---
541
+ async handleMessage(message) {
542
+ const config = this.configManager.get();
543
+ if (config.security.allowedUserIds.length > 0) {
544
+ if (!config.security.allowedUserIds.includes(message.userId)) return;
545
+ }
546
+ const activeSessions = this.sessionManager.listSessions().filter((s) => s.status === "active" || s.status === "initializing");
547
+ if (activeSessions.length >= config.security.maxConcurrentSessions) {
548
+ const adapter = this.adapters.get(message.channelId);
549
+ if (adapter) {
550
+ await adapter.sendMessage("system", {
551
+ type: "error",
552
+ text: `Max concurrent sessions (${config.security.maxConcurrentSessions}) reached. Cancel a session first.`
553
+ });
554
+ }
555
+ return;
556
+ }
557
+ const session = this.sessionManager.getSessionByThread(message.channelId, message.threadId);
558
+ if (!session) return;
559
+ await session.enqueuePrompt(message.text);
560
+ }
561
+ async handleNewSession(channelId, agentName, workspacePath) {
562
+ const config = this.configManager.get();
563
+ const resolvedAgent = agentName || config.defaultAgent;
564
+ const resolvedWorkspace = this.configManager.resolveWorkspace(
565
+ workspacePath || config.agents[resolvedAgent]?.workingDirectory
566
+ );
567
+ const session = await this.sessionManager.createSession(
568
+ channelId,
569
+ resolvedAgent,
570
+ resolvedWorkspace,
571
+ this.agentManager
572
+ );
573
+ const adapter = this.adapters.get(channelId);
574
+ if (adapter) {
575
+ this.wireSessionEvents(session, adapter);
576
+ }
577
+ return session;
578
+ }
579
+ async handleNewChat(channelId, currentThreadId) {
580
+ const currentSession = this.sessionManager.getSessionByThread(channelId, currentThreadId);
581
+ if (!currentSession) return null;
582
+ return this.handleNewSession(
583
+ channelId,
584
+ currentSession.agentName,
585
+ currentSession.workingDirectory
586
+ );
587
+ }
588
+ // --- Event Wiring ---
589
+ toOutgoingMessage(event) {
590
+ switch (event.type) {
591
+ case "text":
592
+ return { type: "text", text: event.content };
593
+ case "thought":
594
+ return { type: "thought", text: event.content };
595
+ case "tool_call":
596
+ return { type: "tool_call", text: event.name, metadata: { id: event.id, kind: event.kind, status: event.status, content: event.content, locations: event.locations } };
597
+ case "tool_update":
598
+ return { type: "tool_update", text: "", metadata: { id: event.id, status: event.status, content: event.content } };
599
+ case "plan":
600
+ return { type: "plan", text: "", metadata: { entries: event.entries } };
601
+ case "usage":
602
+ return { type: "usage", text: "", metadata: { tokensUsed: event.tokensUsed, contextSize: event.contextSize, cost: event.cost } };
603
+ case "commands_update":
604
+ log.debug("Commands update:", event.commands);
605
+ return { type: "text", text: "" };
606
+ // no-op for now
607
+ default:
608
+ return { type: "text", text: "" };
609
+ }
610
+ }
611
+ // Public — adapters call this for assistant session wiring
612
+ wireSessionEvents(session, adapter) {
613
+ session.adapter = adapter;
614
+ session.agentInstance.onSessionUpdate = (event) => {
615
+ switch (event.type) {
616
+ case "text":
617
+ case "thought":
618
+ case "tool_call":
619
+ case "tool_update":
620
+ case "plan":
621
+ case "usage":
622
+ adapter.sendMessage(session.id, this.toOutgoingMessage(event));
623
+ break;
624
+ case "session_end":
625
+ session.status = "finished";
626
+ adapter.sendMessage(session.id, { type: "session_end", text: `Done (${event.reason})` });
627
+ this.notificationManager.notify(session.channelId, {
628
+ sessionId: session.id,
629
+ sessionName: session.name,
630
+ type: "completed",
631
+ summary: `Session "${session.name || session.id}" completed`
632
+ });
633
+ break;
634
+ case "error":
635
+ adapter.sendMessage(session.id, { type: "error", text: event.message });
636
+ this.notificationManager.notify(session.channelId, {
637
+ sessionId: session.id,
638
+ sessionName: session.name,
639
+ type: "error",
640
+ summary: event.message
641
+ });
642
+ break;
643
+ case "commands_update":
644
+ log.debug("Commands available:", event.commands);
645
+ break;
646
+ }
647
+ };
648
+ session.agentInstance.onPermissionRequest = async (request) => {
649
+ const promise = new Promise((resolve) => {
650
+ session.pendingPermission = { requestId: request.id, resolve };
651
+ });
652
+ await adapter.sendPermissionRequest(session.id, request);
653
+ return promise;
654
+ };
655
+ }
656
+ };
657
+
658
+ // src/core/channel.ts
659
+ var ChannelAdapter = class {
660
+ constructor(core, config) {
661
+ this.core = core;
662
+ this.config = config;
663
+ }
664
+ };
665
+
666
+ // src/adapters/telegram/adapter.ts
667
+ import { Bot } from "grammy";
668
+
669
+ // src/adapters/telegram/formatting.ts
670
+ function escapeHtml(text) {
671
+ if (!text) return "";
672
+ return text.replace(/&/g, "&amp;").replace(/</g, "&lt;").replace(/>/g, "&gt;");
673
+ }
674
+ function markdownToTelegramHtml(md) {
675
+ const codeBlocks = [];
676
+ const inlineCodes = [];
677
+ let text = md.replace(/```(\w*)\n?([\s\S]*?)```/g, (_match, lang, code) => {
678
+ const index = codeBlocks.length;
679
+ const escapedCode = escapeHtml(code);
680
+ const langAttr = lang ? ` class="language-${escapeHtml(lang)}"` : "";
681
+ codeBlocks.push(`<pre><code${langAttr}>${escapedCode}</code></pre>`);
682
+ return `\0CODE_BLOCK_${index}\0`;
683
+ });
684
+ text = text.replace(/`([^`]+)`/g, (_match, code) => {
685
+ const index = inlineCodes.length;
686
+ inlineCodes.push(`<code>${escapeHtml(code)}</code>`);
687
+ return `\0INLINE_CODE_${index}\0`;
688
+ });
689
+ text = escapeHtml(text);
690
+ text = text.replace(/\*\*(.+?)\*\*/g, "<b>$1</b>");
691
+ text = text.replace(/(?<!\*)\*(?!\*)(.+?)(?<!\*)\*(?!\*)/g, "<i>$1</i>");
692
+ text = text.replace(/\[([^\]]+)\]\(([^)]+)\)/g, '<a href="$2">$1</a>');
693
+ text = text.replace(/\x00CODE_BLOCK_(\d+)\x00/g, (_match, idx) => {
694
+ return codeBlocks[parseInt(idx, 10)];
695
+ });
696
+ text = text.replace(/\x00INLINE_CODE_(\d+)\x00/g, (_match, idx) => {
697
+ return inlineCodes[parseInt(idx, 10)];
698
+ });
699
+ return text;
700
+ }
701
+ var STATUS_ICON = {
702
+ pending: "\u23F3",
703
+ in_progress: "\u{1F504}",
704
+ completed: "\u2705",
705
+ failed: "\u274C"
706
+ };
707
+ var KIND_ICON = {
708
+ read: "\u{1F4D6}",
709
+ edit: "\u270F\uFE0F",
710
+ delete: "\u{1F5D1}\uFE0F",
711
+ execute: "\u25B6\uFE0F",
712
+ search: "\u{1F50D}",
713
+ fetch: "\u{1F310}",
714
+ think: "\u{1F9E0}",
715
+ move: "\u{1F4E6}",
716
+ other: "\u{1F6E0}\uFE0F"
717
+ };
718
+ function extractContentText(content) {
719
+ if (!content) return "";
720
+ if (typeof content === "string") return content;
721
+ if (Array.isArray(content)) {
722
+ return content.map((c) => extractContentText(c)).filter(Boolean).join("\n");
723
+ }
724
+ if (typeof content === "object" && content !== null) {
725
+ const c = content;
726
+ if (c.type === "text" && typeof c.text === "string") return c.text;
727
+ if (typeof c.text === "string") return c.text;
728
+ if (typeof c.content === "string") return c.content;
729
+ if (c.input) return extractContentText(c.input);
730
+ if (c.output) return extractContentText(c.output);
731
+ const keys = Object.keys(c).filter((k) => k !== "type");
732
+ if (keys.length === 0) return "";
733
+ return JSON.stringify(c, null, 2);
734
+ }
735
+ return String(content);
736
+ }
737
+ function truncateContent(text, maxLen = 3800) {
738
+ if (text.length <= maxLen) return text;
739
+ return text.slice(0, maxLen) + "\n\u2026 (truncated)";
740
+ }
741
+ function formatToolCall(tool) {
742
+ const si = STATUS_ICON[tool.status || ""] || "\u{1F527}";
743
+ const ki = KIND_ICON[tool.kind || ""] || "\u{1F6E0}\uFE0F";
744
+ let text = `${si} ${ki} <b>${escapeHtml(tool.name || "Tool")}</b>`;
745
+ const details = extractContentText(tool.content);
746
+ if (details) {
747
+ text += `
748
+ <pre>${escapeHtml(truncateContent(details))}</pre>`;
749
+ }
750
+ return text;
751
+ }
752
+ function formatToolUpdate(update) {
753
+ const si = STATUS_ICON[update.status] || "\u{1F527}";
754
+ const ki = KIND_ICON[update.kind || ""] || "\u{1F6E0}\uFE0F";
755
+ const name = update.name || "Tool";
756
+ let text = `${si} ${ki} <b>${escapeHtml(name)}</b>`;
757
+ const details = extractContentText(update.content);
758
+ if (details) {
759
+ text += `
760
+ <pre>${escapeHtml(truncateContent(details))}</pre>`;
761
+ }
762
+ return text;
763
+ }
764
+ function formatPlan(plan) {
765
+ const statusIcon = { pending: "\u2B1C", in_progress: "\u{1F504}", completed: "\u2705" };
766
+ const lines = plan.entries.map(
767
+ (e, i) => `${statusIcon[e.status] || "\u2B1C"} ${i + 1}. ${escapeHtml(e.content)}`
768
+ );
769
+ return `<b>Plan:</b>
770
+ ${lines.join("\n")}`;
771
+ }
772
+ function formatUsage(usage) {
773
+ const parts = [];
774
+ if (usage.tokensUsed != null) parts.push(`Tokens: ${usage.tokensUsed.toLocaleString()}`);
775
+ if (usage.contextSize != null) parts.push(`Context: ${usage.contextSize.toLocaleString()}`);
776
+ if (usage.cost) parts.push(`Cost: $${usage.cost.amount.toFixed(4)}`);
777
+ return `\u{1F4CA} ${parts.join(" | ")}`;
778
+ }
779
+ function splitMessage(text, maxLength = 4096) {
780
+ if (text.length <= maxLength) return [text];
781
+ const chunks = [];
782
+ let remaining = text;
783
+ while (remaining.length > 0) {
784
+ if (remaining.length <= maxLength) {
785
+ chunks.push(remaining);
786
+ break;
787
+ }
788
+ let splitAt = remaining.lastIndexOf("\n\n", maxLength);
789
+ if (splitAt === -1 || splitAt < maxLength * 0.5) {
790
+ splitAt = remaining.lastIndexOf("\n", maxLength);
791
+ }
792
+ if (splitAt === -1 || splitAt < maxLength * 0.5) {
793
+ splitAt = maxLength;
794
+ }
795
+ chunks.push(remaining.slice(0, splitAt));
796
+ remaining = remaining.slice(splitAt).trimStart();
797
+ }
798
+ return chunks;
799
+ }
800
+
801
+ // src/adapters/telegram/streaming.ts
802
+ var MessageDraft = class {
803
+ // 1 second throttle
804
+ constructor(bot, chatId, threadId) {
805
+ this.bot = bot;
806
+ this.chatId = chatId;
807
+ this.threadId = threadId;
808
+ }
809
+ messageId;
810
+ buffer = "";
811
+ lastFlush = 0;
812
+ flushTimer;
813
+ flushPromise = Promise.resolve();
814
+ // serialize flushes
815
+ minInterval = 1e3;
816
+ append(text) {
817
+ this.buffer += text;
818
+ this.scheduleFlush();
819
+ }
820
+ scheduleFlush() {
821
+ const now = Date.now();
822
+ const elapsed = now - this.lastFlush;
823
+ if (elapsed >= this.minInterval) {
824
+ this.flushPromise = this.flushPromise.then(() => this.flush()).catch(() => {
825
+ });
826
+ } else if (!this.flushTimer) {
827
+ this.flushTimer = setTimeout(() => {
828
+ this.flushTimer = void 0;
829
+ this.flushPromise = this.flushPromise.then(() => this.flush()).catch(() => {
830
+ });
831
+ }, this.minInterval - elapsed);
832
+ }
833
+ }
834
+ async flush() {
835
+ if (!this.buffer) return;
836
+ this.lastFlush = Date.now();
837
+ const html = markdownToTelegramHtml(this.buffer);
838
+ const truncated = html.length > 4096 ? html.slice(0, 4090) + "\n..." : html;
839
+ if (!truncated) return;
840
+ try {
841
+ if (!this.messageId) {
842
+ const msg = await this.bot.api.sendMessage(this.chatId, truncated, {
843
+ message_thread_id: this.threadId,
844
+ parse_mode: "HTML",
845
+ disable_notification: true
846
+ });
847
+ this.messageId = msg.message_id;
848
+ } else {
849
+ await this.bot.api.editMessageText(this.chatId, this.messageId, truncated, {
850
+ parse_mode: "HTML"
851
+ });
852
+ }
853
+ } catch {
854
+ try {
855
+ if (!this.messageId) {
856
+ const msg = await this.bot.api.sendMessage(this.chatId, this.buffer.slice(0, 4096), {
857
+ message_thread_id: this.threadId,
858
+ disable_notification: true
859
+ });
860
+ this.messageId = msg.message_id;
861
+ }
862
+ } catch {
863
+ }
864
+ }
865
+ }
866
+ async finalize() {
867
+ if (this.flushTimer) {
868
+ clearTimeout(this.flushTimer);
869
+ this.flushTimer = void 0;
870
+ }
871
+ await this.flushPromise;
872
+ if (!this.buffer) return this.messageId;
873
+ const html = markdownToTelegramHtml(this.buffer);
874
+ const chunks = splitMessage(html);
875
+ try {
876
+ for (let i = 0; i < chunks.length; i++) {
877
+ const chunk = chunks[i];
878
+ if (i === 0 && this.messageId) {
879
+ await this.bot.api.editMessageText(this.chatId, this.messageId, chunk, {
880
+ parse_mode: "HTML"
881
+ });
882
+ } else {
883
+ const msg = await this.bot.api.sendMessage(this.chatId, chunk, {
884
+ message_thread_id: this.threadId,
885
+ parse_mode: "HTML",
886
+ disable_notification: true
887
+ });
888
+ this.messageId = msg.message_id;
889
+ }
890
+ }
891
+ } catch {
892
+ try {
893
+ await this.bot.api.sendMessage(this.chatId, this.buffer.slice(0, 4096), {
894
+ message_thread_id: this.threadId,
895
+ disable_notification: true
896
+ });
897
+ } catch {
898
+ }
899
+ }
900
+ return this.messageId;
901
+ }
902
+ getMessageId() {
903
+ return this.messageId;
904
+ }
905
+ };
906
+
907
+ // src/adapters/telegram/topics.ts
908
+ async function ensureTopics(bot, chatId, config, saveConfig) {
909
+ let notificationTopicId = config.notificationTopicId;
910
+ let assistantTopicId = config.assistantTopicId;
911
+ if (notificationTopicId === null) {
912
+ const topic = await bot.api.createForumTopic(chatId, "\u{1F4CB} Notifications");
913
+ notificationTopicId = topic.message_thread_id;
914
+ await saveConfig({ notificationTopicId });
915
+ }
916
+ if (assistantTopicId === null) {
917
+ const topic = await bot.api.createForumTopic(chatId, "\u{1F916} Assistant");
918
+ assistantTopicId = topic.message_thread_id;
919
+ await saveConfig({ assistantTopicId });
920
+ }
921
+ return { notificationTopicId, assistantTopicId };
922
+ }
923
+ async function createSessionTopic(bot, chatId, name) {
924
+ const topic = await bot.api.createForumTopic(chatId, name);
925
+ return topic.message_thread_id;
926
+ }
927
+ async function renameSessionTopic(bot, chatId, threadId, name) {
928
+ try {
929
+ await bot.api.editForumTopic(chatId, threadId, { name });
930
+ } catch {
931
+ }
932
+ }
933
+ function buildDeepLink(chatId, messageId) {
934
+ const cleanId = String(chatId).replace("-100", "");
935
+ return `https://t.me/c/${cleanId}/${messageId}`;
936
+ }
937
+
938
+ // src/adapters/telegram/commands.ts
939
+ import { InlineKeyboard } from "grammy";
940
+ function setupCommands(bot, core, chatId) {
941
+ bot.command("new", (ctx) => handleNew(ctx, core, chatId));
942
+ bot.command("new_chat", (ctx) => handleNewChat(ctx, core, chatId));
943
+ bot.command("cancel", (ctx) => handleCancel(ctx, core));
944
+ bot.command("status", (ctx) => handleStatus(ctx, core));
945
+ bot.command("agents", (ctx) => handleAgents(ctx, core));
946
+ bot.command("help", (ctx) => handleHelp(ctx));
947
+ bot.command("menu", (ctx) => handleMenu(ctx));
948
+ }
949
+ function buildMenuKeyboard() {
950
+ return new InlineKeyboard().text("\u{1F195} New Session", "m:new").text("\u{1F4AC} New Chat", "m:new_chat").row().text("\u26D4 Cancel", "m:cancel").text("\u{1F4CA} Status", "m:status").row().text("\u{1F916} Agents", "m:agents").text("\u2753 Help", "m:help");
951
+ }
952
+ async function handleMenu(ctx) {
953
+ await ctx.reply(`<b>OpenACP Menu</b>
954
+ Choose an action:`, {
955
+ parse_mode: "HTML",
956
+ reply_markup: buildMenuKeyboard()
957
+ });
958
+ }
959
+ async function handleNew(ctx, core, chatId) {
960
+ const rawMatch = ctx.match;
961
+ const matchStr = typeof rawMatch === "string" ? rawMatch : "";
962
+ const args = matchStr.split(" ").filter(Boolean);
963
+ const agentName = args[0];
964
+ const workspace = args[1];
965
+ let threadId;
966
+ try {
967
+ const topicName = `\u{1F504} New Session`;
968
+ threadId = await createSessionTopic(botFromCtx(ctx), chatId, topicName);
969
+ const session = await core.handleNewSession(
970
+ "telegram",
971
+ agentName,
972
+ workspace
973
+ );
974
+ session.threadId = String(threadId);
975
+ const finalName = `\u{1F504} ${session.agentName} \u2014 New Session`;
976
+ try {
977
+ await ctx.api.editForumTopic(chatId, threadId, { name: finalName });
978
+ } catch {
979
+ }
980
+ await ctx.api.sendMessage(
981
+ chatId,
982
+ `\u2705 Session started
983
+ <b>Agent:</b> ${escapeHtml(session.agentName)}
984
+ <b>Workspace:</b> <code>${escapeHtml(session.workingDirectory)}</code>`,
985
+ {
986
+ message_thread_id: threadId,
987
+ parse_mode: "HTML"
988
+ }
989
+ );
990
+ } catch (err) {
991
+ if (threadId) {
992
+ try {
993
+ await ctx.api.deleteForumTopic(chatId, threadId);
994
+ } catch {
995
+ }
996
+ }
997
+ const message = err instanceof Error ? err.message : String(err);
998
+ await ctx.reply(`\u274C ${escapeHtml(message)}`, { parse_mode: "HTML" });
999
+ }
1000
+ }
1001
+ async function handleNewChat(ctx, core, chatId) {
1002
+ const threadId = ctx.message?.message_thread_id;
1003
+ if (!threadId) {
1004
+ await ctx.reply(
1005
+ "Use /new_chat inside a session topic to inherit its config.",
1006
+ { parse_mode: "HTML" }
1007
+ );
1008
+ return;
1009
+ }
1010
+ try {
1011
+ const session = await core.handleNewChat("telegram", String(threadId));
1012
+ if (!session) {
1013
+ await ctx.reply("No active session in this topic.", {
1014
+ parse_mode: "HTML"
1015
+ });
1016
+ return;
1017
+ }
1018
+ const topicName = `\u{1F504} ${session.agentName} \u2014 New Chat`;
1019
+ const newThreadId = await createSessionTopic(
1020
+ botFromCtx(ctx),
1021
+ chatId,
1022
+ topicName
1023
+ );
1024
+ session.threadId = String(newThreadId);
1025
+ await ctx.api.sendMessage(
1026
+ chatId,
1027
+ `\u2705 New chat (same agent &amp; workspace)
1028
+ <b>Agent:</b> ${escapeHtml(session.agentName)}
1029
+ <b>Workspace:</b> <code>${escapeHtml(session.workingDirectory)}</code>`,
1030
+ {
1031
+ message_thread_id: newThreadId,
1032
+ parse_mode: "HTML"
1033
+ }
1034
+ );
1035
+ } catch (err) {
1036
+ const message = err instanceof Error ? err.message : String(err);
1037
+ await ctx.reply(`\u274C ${escapeHtml(message)}`, { parse_mode: "HTML" });
1038
+ }
1039
+ }
1040
+ async function handleCancel(ctx, core) {
1041
+ const threadId = ctx.message?.message_thread_id;
1042
+ if (!threadId) return;
1043
+ const session = core.sessionManager.getSessionByThread(
1044
+ "telegram",
1045
+ String(threadId)
1046
+ );
1047
+ if (session) {
1048
+ await session.cancel();
1049
+ await ctx.reply("\u26D4 Session cancelled.", { parse_mode: "HTML" });
1050
+ }
1051
+ }
1052
+ async function handleStatus(ctx, core) {
1053
+ const threadId = ctx.message?.message_thread_id;
1054
+ if (threadId) {
1055
+ const session = core.sessionManager.getSessionByThread(
1056
+ "telegram",
1057
+ String(threadId)
1058
+ );
1059
+ if (session) {
1060
+ await ctx.reply(
1061
+ `<b>Session:</b> ${escapeHtml(session.name || session.id)}
1062
+ <b>Agent:</b> ${escapeHtml(session.agentName)}
1063
+ <b>Status:</b> ${escapeHtml(session.status)}
1064
+ <b>Workspace:</b> <code>${escapeHtml(session.workingDirectory)}</code>
1065
+ <b>Queue:</b> ${session.promptQueue.length} pending`,
1066
+ { parse_mode: "HTML" }
1067
+ );
1068
+ } else {
1069
+ await ctx.reply("No active session in this topic.", {
1070
+ parse_mode: "HTML"
1071
+ });
1072
+ }
1073
+ } else {
1074
+ const sessions = core.sessionManager.listSessions("telegram");
1075
+ const active = sessions.filter(
1076
+ (s) => s.status === "active" || s.status === "initializing"
1077
+ );
1078
+ await ctx.reply(
1079
+ `<b>OpenACP Status</b>
1080
+ Active sessions: ${active.length}
1081
+ Total sessions: ${sessions.length}`,
1082
+ { parse_mode: "HTML" }
1083
+ );
1084
+ }
1085
+ }
1086
+ async function handleAgents(ctx, core) {
1087
+ const agents = core.agentManager.getAvailableAgents();
1088
+ const defaultAgent = core.configManager.get().defaultAgent;
1089
+ const lines = agents.map(
1090
+ (a) => `\u2022 <b>${escapeHtml(a.name)}</b>${a.name === defaultAgent ? " (default)" : ""}
1091
+ <code>${escapeHtml(a.command)} ${a.args.map((arg) => escapeHtml(arg)).join(" ")}</code>`
1092
+ );
1093
+ const text = lines.length > 0 ? `<b>Available Agents:</b>
1094
+
1095
+ ${lines.join("\n")}` : `<b>Available Agents:</b>
1096
+
1097
+ No agents configured.`;
1098
+ await ctx.reply(text, { parse_mode: "HTML" });
1099
+ }
1100
+ async function handleHelp(ctx) {
1101
+ await ctx.reply(
1102
+ `<b>OpenACP Commands:</b>
1103
+
1104
+ /new [agent] [workspace] \u2014 Create new session
1105
+ /new_chat \u2014 New chat, same agent &amp; workspace
1106
+ /cancel \u2014 Cancel current session
1107
+ /status \u2014 Show session/system status
1108
+ /agents \u2014 List available agents
1109
+ /menu \u2014 Show interactive menu
1110
+ /help \u2014 Show this help
1111
+
1112
+ Or just chat in the \u{1F916} Assistant topic for help!`,
1113
+ { parse_mode: "HTML" }
1114
+ );
1115
+ }
1116
+ function botFromCtx(ctx) {
1117
+ return { api: ctx.api };
1118
+ }
1119
+
1120
+ // src/adapters/telegram/permissions.ts
1121
+ import { InlineKeyboard as InlineKeyboard2 } from "grammy";
1122
+ import { nanoid as nanoid2 } from "nanoid";
1123
+ var PermissionHandler = class {
1124
+ constructor(bot, chatId, getSession, sendNotification) {
1125
+ this.bot = bot;
1126
+ this.chatId = chatId;
1127
+ this.getSession = getSession;
1128
+ this.sendNotification = sendNotification;
1129
+ }
1130
+ pending = /* @__PURE__ */ new Map();
1131
+ async sendPermissionRequest(session, request) {
1132
+ const threadId = Number(session.threadId);
1133
+ const callbackKey = nanoid2(8);
1134
+ this.pending.set(callbackKey, { sessionId: session.id, requestId: request.id });
1135
+ const keyboard = new InlineKeyboard2();
1136
+ for (const option of request.options) {
1137
+ const emoji = option.isAllow ? "\u2705" : "\u274C";
1138
+ keyboard.text(`${emoji} ${option.label}`, `p:${callbackKey}:${option.id}`);
1139
+ }
1140
+ const msg = await this.bot.api.sendMessage(
1141
+ this.chatId,
1142
+ `\u{1F510} <b>Permission request:</b>
1143
+
1144
+ ${escapeHtml(request.description)}`,
1145
+ {
1146
+ message_thread_id: threadId,
1147
+ parse_mode: "HTML",
1148
+ reply_markup: keyboard,
1149
+ disable_notification: false
1150
+ }
1151
+ );
1152
+ const deepLink = buildDeepLink(this.chatId, msg.message_id);
1153
+ await this.sendNotification({
1154
+ sessionId: session.id,
1155
+ sessionName: session.name,
1156
+ type: "permission",
1157
+ summary: request.description,
1158
+ deepLink
1159
+ });
1160
+ }
1161
+ setupCallbackHandler() {
1162
+ this.bot.on("callback_query:data", async (ctx) => {
1163
+ const data = ctx.callbackQuery.data;
1164
+ if (!data.startsWith("p:")) return;
1165
+ const parts = data.split(":");
1166
+ if (parts.length < 3) return;
1167
+ const [, callbackKey, optionId] = parts;
1168
+ const pending = this.pending.get(callbackKey);
1169
+ if (!pending) {
1170
+ try {
1171
+ await ctx.answerCallbackQuery({ text: "\u274C Expired" });
1172
+ } catch {
1173
+ }
1174
+ return;
1175
+ }
1176
+ const session = this.getSession(pending.sessionId);
1177
+ if (session?.pendingPermission?.requestId === pending.requestId) {
1178
+ session.pendingPermission.resolve(optionId);
1179
+ session.pendingPermission = void 0;
1180
+ }
1181
+ this.pending.delete(callbackKey);
1182
+ try {
1183
+ await ctx.answerCallbackQuery({ text: "\u2705 Responded" });
1184
+ } catch {
1185
+ }
1186
+ try {
1187
+ await ctx.editMessageReplyMarkup({ reply_markup: void 0 });
1188
+ } catch {
1189
+ }
1190
+ });
1191
+ }
1192
+ };
1193
+
1194
+ // src/adapters/telegram/assistant.ts
1195
+ async function spawnAssistant(core, adapter, assistantTopicId) {
1196
+ const config = core.configManager.get();
1197
+ const session = await core.sessionManager.createSession(
1198
+ "telegram",
1199
+ config.defaultAgent,
1200
+ core.configManager.resolveWorkspace(),
1201
+ core.agentManager
1202
+ );
1203
+ session.threadId = String(assistantTopicId);
1204
+ const systemPrompt = buildAssistantSystemPrompt(config);
1205
+ await session.enqueuePrompt(systemPrompt);
1206
+ core.wireSessionEvents(session, adapter);
1207
+ return session;
1208
+ }
1209
+ function buildAssistantSystemPrompt(config) {
1210
+ const agentNames = Object.keys(config.agents).join(", ");
1211
+ return `You are the OpenACP Assistant. Help users manage their AI coding sessions.
1212
+
1213
+ Available agents: ${agentNames}
1214
+ Default agent: ${config.defaultAgent}
1215
+ Workspace base: ${config.workspace.baseDir}
1216
+
1217
+ When a user wants to create a session, guide them through:
1218
+ 1. Which agent to use
1219
+ 2. Which workspace/project
1220
+ 3. Confirm and create
1221
+
1222
+ Commands reference:
1223
+ - /new [agent] [workspace] \u2014 Create new session
1224
+ - /new_chat \u2014 New chat with same agent & workspace
1225
+ - /cancel \u2014 Cancel current session
1226
+ - /status \u2014 Show status
1227
+ - /agents \u2014 List agents
1228
+ - /help \u2014 Show help
1229
+
1230
+ Be concise and helpful. When the user confirms session creation, tell them you'll create it now.`;
1231
+ }
1232
+ async function handleAssistantMessage(session, text) {
1233
+ if (!session) return;
1234
+ await session.enqueuePrompt(text);
1235
+ }
1236
+ function redirectToAssistant(chatId, assistantTopicId) {
1237
+ const cleanId = String(chatId).replace("-100", "");
1238
+ const link = `https://t.me/c/${cleanId}/${assistantTopicId}`;
1239
+ return `\u{1F4AC} Please use the <a href="${link}">\u{1F916} Assistant</a> topic to chat with OpenACP.`;
1240
+ }
1241
+
1242
+ // src/adapters/telegram/adapter.ts
1243
+ var TelegramAdapter = class extends ChannelAdapter {
1244
+ bot;
1245
+ telegramConfig;
1246
+ sessionDrafts = /* @__PURE__ */ new Map();
1247
+ toolCallMessages = /* @__PURE__ */ new Map();
1248
+ // sessionId → (toolCallId → state)
1249
+ permissionHandler;
1250
+ assistantSession = null;
1251
+ notificationTopicId;
1252
+ assistantTopicId;
1253
+ constructor(core, config) {
1254
+ super(core, config);
1255
+ this.telegramConfig = config;
1256
+ }
1257
+ async start() {
1258
+ this.bot = new Bot(this.telegramConfig.botToken);
1259
+ this.bot.catch((err) => {
1260
+ log.error("Bot error:", err.message || err);
1261
+ });
1262
+ this.bot.api.config.use((prev, method, payload, signal) => {
1263
+ if (method === "getUpdates") {
1264
+ payload.allowed_updates = payload.allowed_updates ?? ["message", "callback_query"];
1265
+ }
1266
+ return prev(method, payload, signal);
1267
+ });
1268
+ this.bot.use((ctx, next) => {
1269
+ const chatId = ctx.chat?.id ?? ctx.callbackQuery?.message?.chat?.id;
1270
+ if (chatId !== this.telegramConfig.chatId) return;
1271
+ return next();
1272
+ });
1273
+ const topics = await ensureTopics(
1274
+ this.bot,
1275
+ this.telegramConfig.chatId,
1276
+ this.telegramConfig,
1277
+ async (updates) => {
1278
+ await this.core.configManager.save({
1279
+ channels: { telegram: updates }
1280
+ });
1281
+ }
1282
+ );
1283
+ this.notificationTopicId = topics.notificationTopicId;
1284
+ this.assistantTopicId = topics.assistantTopicId;
1285
+ this.permissionHandler = new PermissionHandler(
1286
+ this.bot,
1287
+ this.telegramConfig.chatId,
1288
+ (sessionId) => this.core.sessionManager.getSession(sessionId),
1289
+ (notification) => this.sendNotification(notification)
1290
+ );
1291
+ this.permissionHandler.setupCallbackHandler();
1292
+ setupCommands(this.bot, this.core, this.telegramConfig.chatId);
1293
+ this.setupRoutes();
1294
+ this.bot.start({
1295
+ allowed_updates: ["message", "callback_query"],
1296
+ onStart: () => log.info("Telegram bot started")
1297
+ });
1298
+ try {
1299
+ this.assistantSession = await spawnAssistant(
1300
+ this.core,
1301
+ this,
1302
+ this.assistantTopicId
1303
+ );
1304
+ } catch (err) {
1305
+ log.error("Failed to spawn assistant:", err);
1306
+ }
1307
+ }
1308
+ async stop() {
1309
+ if (this.assistantSession) {
1310
+ await this.assistantSession.destroy();
1311
+ }
1312
+ await this.bot.stop();
1313
+ }
1314
+ setupRoutes() {
1315
+ this.bot.on("message:text", async (ctx) => {
1316
+ const threadId = ctx.message.message_thread_id;
1317
+ if (!threadId) {
1318
+ const html = redirectToAssistant(this.telegramConfig.chatId, this.assistantTopicId);
1319
+ await ctx.reply(html, { parse_mode: "HTML" });
1320
+ return;
1321
+ }
1322
+ if (threadId === this.notificationTopicId) return;
1323
+ if (threadId === this.assistantTopicId) {
1324
+ handleAssistantMessage(this.assistantSession, ctx.message.text).catch((err) => log.error("Assistant error:", err));
1325
+ return;
1326
+ }
1327
+ ;
1328
+ this.core.handleMessage({
1329
+ channelId: "telegram",
1330
+ threadId: String(threadId),
1331
+ userId: String(ctx.from.id),
1332
+ text: ctx.message.text
1333
+ }).catch((err) => log.error("handleMessage error:", err));
1334
+ });
1335
+ }
1336
+ // --- ChannelAdapter implementations ---
1337
+ async sendMessage(sessionId, content) {
1338
+ const session = this.core.sessionManager.getSession(sessionId);
1339
+ if (!session) return;
1340
+ const threadId = Number(session.threadId);
1341
+ switch (content.type) {
1342
+ case "thought": {
1343
+ break;
1344
+ }
1345
+ case "text": {
1346
+ let draft = this.sessionDrafts.get(sessionId);
1347
+ if (!draft) {
1348
+ draft = new MessageDraft(this.bot, this.telegramConfig.chatId, threadId);
1349
+ this.sessionDrafts.set(sessionId, draft);
1350
+ }
1351
+ draft.append(content.text);
1352
+ break;
1353
+ }
1354
+ case "tool_call": {
1355
+ await this.finalizeDraft(sessionId);
1356
+ const meta = content.metadata;
1357
+ const msg = await this.bot.api.sendMessage(
1358
+ this.telegramConfig.chatId,
1359
+ formatToolCall(meta),
1360
+ { message_thread_id: threadId, parse_mode: "HTML", disable_notification: true }
1361
+ );
1362
+ if (!this.toolCallMessages.has(sessionId)) {
1363
+ this.toolCallMessages.set(sessionId, /* @__PURE__ */ new Map());
1364
+ }
1365
+ this.toolCallMessages.get(sessionId).set(meta.id, { msgId: msg.message_id, name: meta.name, kind: meta.kind });
1366
+ break;
1367
+ }
1368
+ case "tool_update": {
1369
+ const meta = content.metadata;
1370
+ const toolState = this.toolCallMessages.get(sessionId)?.get(meta.id);
1371
+ if (toolState) {
1372
+ const merged = { ...meta, name: meta.name || toolState.name, kind: meta.kind || toolState.kind };
1373
+ try {
1374
+ await this.bot.api.editMessageText(
1375
+ this.telegramConfig.chatId,
1376
+ toolState.msgId,
1377
+ formatToolUpdate(merged),
1378
+ { parse_mode: "HTML" }
1379
+ );
1380
+ } catch {
1381
+ }
1382
+ }
1383
+ break;
1384
+ }
1385
+ case "plan": {
1386
+ await this.finalizeDraft(sessionId);
1387
+ await this.bot.api.sendMessage(
1388
+ this.telegramConfig.chatId,
1389
+ formatPlan(content.metadata),
1390
+ { message_thread_id: threadId, parse_mode: "HTML", disable_notification: true }
1391
+ );
1392
+ break;
1393
+ }
1394
+ case "usage": {
1395
+ await this.bot.api.sendMessage(
1396
+ this.telegramConfig.chatId,
1397
+ formatUsage(content.metadata),
1398
+ { message_thread_id: threadId, parse_mode: "HTML", disable_notification: true }
1399
+ );
1400
+ break;
1401
+ }
1402
+ case "session_end": {
1403
+ await this.finalizeDraft(sessionId);
1404
+ this.sessionDrafts.delete(sessionId);
1405
+ this.toolCallMessages.delete(sessionId);
1406
+ await this.bot.api.sendMessage(
1407
+ this.telegramConfig.chatId,
1408
+ `\u2705 <b>Done</b>`,
1409
+ { message_thread_id: threadId, parse_mode: "HTML", disable_notification: true }
1410
+ );
1411
+ break;
1412
+ }
1413
+ case "error": {
1414
+ await this.finalizeDraft(sessionId);
1415
+ await this.bot.api.sendMessage(
1416
+ this.telegramConfig.chatId,
1417
+ `\u274C <b>Error:</b> ${escapeHtml(content.text)}`,
1418
+ { message_thread_id: threadId, parse_mode: "HTML", disable_notification: true }
1419
+ );
1420
+ break;
1421
+ }
1422
+ }
1423
+ }
1424
+ async sendPermissionRequest(sessionId, request) {
1425
+ const session = this.core.sessionManager.getSession(sessionId);
1426
+ if (!session) return;
1427
+ await this.permissionHandler.sendPermissionRequest(session, request);
1428
+ }
1429
+ async sendNotification(notification) {
1430
+ if (!this.notificationTopicId) return;
1431
+ const emoji = {
1432
+ completed: "\u2705",
1433
+ error: "\u274C",
1434
+ permission: "\u{1F510}",
1435
+ input_required: "\u{1F4AC}"
1436
+ };
1437
+ let text = `${emoji[notification.type] || "\u2139\uFE0F"} <b>${escapeHtml(notification.sessionName || notification.sessionId)}</b>
1438
+ `;
1439
+ text += escapeHtml(notification.summary);
1440
+ if (notification.deepLink) {
1441
+ text += `
1442
+
1443
+ <a href="${notification.deepLink}">\u2192 Go to message</a>`;
1444
+ }
1445
+ await this.bot.api.sendMessage(this.telegramConfig.chatId, text, {
1446
+ message_thread_id: this.notificationTopicId,
1447
+ parse_mode: "HTML",
1448
+ disable_notification: false
1449
+ });
1450
+ }
1451
+ async createSessionThread(sessionId, name) {
1452
+ return String(await createSessionTopic(this.bot, this.telegramConfig.chatId, name));
1453
+ }
1454
+ async renameSessionThread(sessionId, newName) {
1455
+ const session = this.core.sessionManager.getSession(sessionId);
1456
+ if (!session) return;
1457
+ await renameSessionTopic(this.bot, this.telegramConfig.chatId, Number(session.threadId), newName);
1458
+ }
1459
+ async finalizeDraft(sessionId) {
1460
+ const draft = this.sessionDrafts.get(sessionId);
1461
+ if (draft) {
1462
+ await draft.finalize();
1463
+ this.sessionDrafts.delete(sessionId);
1464
+ }
1465
+ }
1466
+ };
1467
+
1468
+ export {
1469
+ nodeToWebWritable,
1470
+ nodeToWebReadable,
1471
+ StderrCapture,
1472
+ AgentInstance,
1473
+ AgentManager,
1474
+ Session,
1475
+ SessionManager,
1476
+ NotificationManager,
1477
+ OpenACPCore,
1478
+ ChannelAdapter,
1479
+ TelegramAdapter
1480
+ };
1481
+ //# sourceMappingURL=chunk-I6KXISAR.js.map