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