@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.
- package/dist/chunk-I6KXISAR.js +1481 -0
- package/dist/chunk-I6KXISAR.js.map +1 -0
- package/dist/{chunk-M5ZYTPZY.js → chunk-KADEDKIM.js} +4 -4
- package/dist/chunk-KADEDKIM.js.map +1 -0
- package/dist/cli.js +3 -4
- package/dist/cli.js.map +1 -1
- package/dist/index.d.ts +30 -1
- package/dist/index.js +5 -11
- package/dist/index.js.map +1 -1
- package/dist/{main-GIN2OHQV.js → main-L5JD5STD.js} +8 -17
- package/dist/main-L5JD5STD.js.map +1 -0
- package/dist/setup-2UVU4YYA.js +284 -0
- package/dist/setup-2UVU4YYA.js.map +1 -0
- package/package.json +3 -3
- package/dist/chunk-6YLIH7L5.js +0 -669
- package/dist/chunk-6YLIH7L5.js.map +0 -1
- package/dist/chunk-LZOMFHX3.js +0 -38
- package/dist/chunk-LZOMFHX3.js.map +0 -1
- package/dist/chunk-M5ZYTPZY.js.map +0 -1
- package/dist/main-GIN2OHQV.js.map +0 -1
- package/dist/setup-KOMX6WTE.js +0 -2200
- package/dist/setup-KOMX6WTE.js.map +0 -1
|
@@ -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, "&").replace(/</g, "<").replace(/>/g, ">");
|
|
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 & 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 & 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
|