@openacp/cli 0.6.4 → 0.6.6
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/adapter-RKK7A5GI.js +798 -0
- package/dist/adapter-RKK7A5GI.js.map +1 -0
- package/dist/{chunk-DJIXG62C.js → chunk-3IRAWHMC.js} +3 -3
- package/dist/{chunk-L5KZXYJD.js → chunk-3ZO3MHZN.js} +21 -2
- package/dist/chunk-3ZO3MHZN.js.map +1 -0
- package/dist/chunk-4GQ3I65A.js +23 -0
- package/dist/chunk-4GQ3I65A.js.map +1 -0
- package/dist/{chunk-TNFXJQZP.js → chunk-7KZI2236.js} +2 -2
- package/dist/{chunk-E56PPPAE.js → chunk-AHPRT3RY.js} +382 -4034
- package/dist/chunk-AHPRT3RY.js.map +1 -0
- package/dist/{chunk-FMWSVLRM.js → chunk-FCLGYYTY.js} +1 -21
- package/dist/chunk-FCLGYYTY.js.map +1 -0
- package/dist/{chunk-TOQPQB5Q.js → chunk-PJVKOZTR.js} +2 -2
- package/dist/{chunk-DOCFD5JR.js → chunk-WVMSP4AF.js} +2 -2
- package/dist/{chunk-3CHBVO4T.js → chunk-XVL6AGMG.js} +2 -2
- package/dist/{chunk-N6E3HE42.js → chunk-ZKTIZME6.js} +2 -2
- package/dist/chunk-ZMVVW3BK.js +4771 -0
- package/dist/chunk-ZMVVW3BK.js.map +1 -0
- package/dist/cli.js +21 -21
- package/dist/{config-XDUOULXX.js → config-B26J3XXN.js} +2 -2
- package/dist/{config-editor-3GGBY7NL.js → config-editor-QTGUK3CD.js} +4 -4
- package/dist/{daemon-QY7WXHQ3.js → daemon-5DS5BQXJ.js} +3 -3
- package/dist/{discord-4DE22BQC.js → discord-QKT3JMRW.js} +14 -12
- package/dist/{discord-4DE22BQC.js.map → discord-QKT3JMRW.js.map} +1 -1
- package/dist/doctor-6SUCVUZB.js +9 -0
- package/dist/{doctor-D3YZ6VHJ.js → doctor-QQ3YZEYV.js} +4 -4
- package/dist/index.d.ts +246 -12
- package/dist/index.js +17 -10
- package/dist/{main-GVTLD7VI.js → main-B5L3DD3Y.js} +25 -17
- package/dist/main-B5L3DD3Y.js.map +1 -0
- package/dist/{setup-D6BU36ZL.js → setup-5ZKSUR26.js} +3 -3
- package/package.json +5 -1
- package/dist/chunk-E56PPPAE.js.map +0 -1
- package/dist/chunk-FMWSVLRM.js.map +0 -1
- package/dist/chunk-L5KZXYJD.js.map +0 -1
- package/dist/doctor-SNSQ5SS2.js +0 -9
- package/dist/main-GVTLD7VI.js.map +0 -1
- /package/dist/{chunk-DJIXG62C.js.map → chunk-3IRAWHMC.js.map} +0 -0
- /package/dist/{chunk-TNFXJQZP.js.map → chunk-7KZI2236.js.map} +0 -0
- /package/dist/{chunk-TOQPQB5Q.js.map → chunk-PJVKOZTR.js.map} +0 -0
- /package/dist/{chunk-DOCFD5JR.js.map → chunk-WVMSP4AF.js.map} +0 -0
- /package/dist/{chunk-3CHBVO4T.js.map → chunk-XVL6AGMG.js.map} +0 -0
- /package/dist/{chunk-N6E3HE42.js.map → chunk-ZKTIZME6.js.map} +0 -0
- /package/dist/{config-XDUOULXX.js.map → config-B26J3XXN.js.map} +0 -0
- /package/dist/{config-editor-3GGBY7NL.js.map → config-editor-QTGUK3CD.js.map} +0 -0
- /package/dist/{daemon-QY7WXHQ3.js.map → daemon-5DS5BQXJ.js.map} +0 -0
- /package/dist/{doctor-D3YZ6VHJ.js.map → doctor-6SUCVUZB.js.map} +0 -0
- /package/dist/{doctor-SNSQ5SS2.js.map → doctor-QQ3YZEYV.js.map} +0 -0
- /package/dist/{setup-D6BU36ZL.js.map → setup-5ZKSUR26.js.map} +0 -0
|
@@ -1,11 +1,10 @@
|
|
|
1
1
|
import {
|
|
2
|
-
ChannelAdapter,
|
|
3
2
|
PRODUCT_GUIDE,
|
|
4
3
|
dispatchMessage
|
|
5
|
-
} from "./chunk-
|
|
4
|
+
} from "./chunk-FCLGYYTY.js";
|
|
6
5
|
import {
|
|
7
6
|
DoctorEngine
|
|
8
|
-
} from "./chunk-
|
|
7
|
+
} from "./chunk-PJVKOZTR.js";
|
|
9
8
|
import {
|
|
10
9
|
buildMenuKeyboard,
|
|
11
10
|
buildSkillMessages,
|
|
@@ -14,11 +13,12 @@ import {
|
|
|
14
13
|
handleMenu
|
|
15
14
|
} from "./chunk-7QJS2XBD.js";
|
|
16
15
|
import {
|
|
17
|
-
|
|
18
|
-
|
|
16
|
+
CheckpointReader,
|
|
17
|
+
DEFAULT_MAX_TOKENS
|
|
18
|
+
} from "./chunk-ZMVVW3BK.js";
|
|
19
19
|
import {
|
|
20
|
-
|
|
21
|
-
} from "./chunk-
|
|
20
|
+
ChannelAdapter
|
|
21
|
+
} from "./chunk-4GQ3I65A.js";
|
|
22
22
|
import {
|
|
23
23
|
getConfigValue,
|
|
24
24
|
getSafeFields,
|
|
@@ -26,3918 +26,9 @@ import {
|
|
|
26
26
|
resolveOptions
|
|
27
27
|
} from "./chunk-F3AICYO4.js";
|
|
28
28
|
import {
|
|
29
|
-
createChildLogger
|
|
30
|
-
createSessionLogger
|
|
29
|
+
createChildLogger
|
|
31
30
|
} from "./chunk-GAK6PIBW.js";
|
|
32
31
|
|
|
33
|
-
// src/core/streams.ts
|
|
34
|
-
function nodeToWebWritable(nodeStream) {
|
|
35
|
-
return new WritableStream({
|
|
36
|
-
write(chunk) {
|
|
37
|
-
return new Promise((resolve3, reject) => {
|
|
38
|
-
nodeStream.write(Buffer.from(chunk), (err) => {
|
|
39
|
-
if (err) reject(err);
|
|
40
|
-
else resolve3();
|
|
41
|
-
});
|
|
42
|
-
});
|
|
43
|
-
}
|
|
44
|
-
});
|
|
45
|
-
}
|
|
46
|
-
function nodeToWebReadable(nodeStream) {
|
|
47
|
-
return new ReadableStream({
|
|
48
|
-
start(controller) {
|
|
49
|
-
nodeStream.on("data", (chunk) => {
|
|
50
|
-
controller.enqueue(new Uint8Array(chunk));
|
|
51
|
-
});
|
|
52
|
-
nodeStream.on("end", () => controller.close());
|
|
53
|
-
nodeStream.on("error", (err) => controller.error(err));
|
|
54
|
-
}
|
|
55
|
-
});
|
|
56
|
-
}
|
|
57
|
-
|
|
58
|
-
// src/core/stderr-capture.ts
|
|
59
|
-
var StderrCapture = class {
|
|
60
|
-
constructor(maxLines = 50) {
|
|
61
|
-
this.maxLines = maxLines;
|
|
62
|
-
}
|
|
63
|
-
lines = [];
|
|
64
|
-
append(chunk) {
|
|
65
|
-
this.lines.push(...chunk.split("\n").filter(Boolean));
|
|
66
|
-
if (this.lines.length > this.maxLines) {
|
|
67
|
-
this.lines = this.lines.slice(-this.maxLines);
|
|
68
|
-
}
|
|
69
|
-
}
|
|
70
|
-
getLastLines() {
|
|
71
|
-
return this.lines.join("\n");
|
|
72
|
-
}
|
|
73
|
-
};
|
|
74
|
-
|
|
75
|
-
// src/core/typed-emitter.ts
|
|
76
|
-
var TypedEmitter = class {
|
|
77
|
-
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
|
78
|
-
listeners = /* @__PURE__ */ new Map();
|
|
79
|
-
paused = false;
|
|
80
|
-
buffer = [];
|
|
81
|
-
on(event, listener) {
|
|
82
|
-
let set = this.listeners.get(event);
|
|
83
|
-
if (!set) {
|
|
84
|
-
set = /* @__PURE__ */ new Set();
|
|
85
|
-
this.listeners.set(event, set);
|
|
86
|
-
}
|
|
87
|
-
set.add(listener);
|
|
88
|
-
return this;
|
|
89
|
-
}
|
|
90
|
-
off(event, listener) {
|
|
91
|
-
this.listeners.get(event)?.delete(listener);
|
|
92
|
-
return this;
|
|
93
|
-
}
|
|
94
|
-
emit(event, ...args) {
|
|
95
|
-
if (this.paused) {
|
|
96
|
-
if (this.passthroughFn?.(event, args)) {
|
|
97
|
-
this.deliver(event, args);
|
|
98
|
-
} else {
|
|
99
|
-
this.buffer.push({ event, args });
|
|
100
|
-
}
|
|
101
|
-
return;
|
|
102
|
-
}
|
|
103
|
-
this.deliver(event, args);
|
|
104
|
-
}
|
|
105
|
-
/**
|
|
106
|
-
* Pause event delivery. Events emitted while paused are buffered.
|
|
107
|
-
* Optionally pass a filter to allow specific events through even while paused.
|
|
108
|
-
*/
|
|
109
|
-
pause(passthrough) {
|
|
110
|
-
this.paused = true;
|
|
111
|
-
this.passthroughFn = passthrough;
|
|
112
|
-
}
|
|
113
|
-
passthroughFn;
|
|
114
|
-
/** Resume event delivery and replay buffered events in order. */
|
|
115
|
-
resume() {
|
|
116
|
-
this.paused = false;
|
|
117
|
-
this.passthroughFn = void 0;
|
|
118
|
-
const buffered = this.buffer.splice(0);
|
|
119
|
-
for (const { event, args } of buffered) {
|
|
120
|
-
this.deliver(event, args);
|
|
121
|
-
}
|
|
122
|
-
}
|
|
123
|
-
/** Discard all buffered events without delivering them. */
|
|
124
|
-
clearBuffer() {
|
|
125
|
-
this.buffer.length = 0;
|
|
126
|
-
}
|
|
127
|
-
get isPaused() {
|
|
128
|
-
return this.paused;
|
|
129
|
-
}
|
|
130
|
-
get bufferSize() {
|
|
131
|
-
return this.buffer.length;
|
|
132
|
-
}
|
|
133
|
-
removeAllListeners(event) {
|
|
134
|
-
if (event) {
|
|
135
|
-
this.listeners.delete(event);
|
|
136
|
-
} else {
|
|
137
|
-
this.listeners.clear();
|
|
138
|
-
}
|
|
139
|
-
}
|
|
140
|
-
deliver(event, args) {
|
|
141
|
-
const set = this.listeners.get(event);
|
|
142
|
-
if (!set) return;
|
|
143
|
-
for (const listener of set) {
|
|
144
|
-
listener(...args);
|
|
145
|
-
}
|
|
146
|
-
}
|
|
147
|
-
};
|
|
148
|
-
|
|
149
|
-
// src/core/agent-instance.ts
|
|
150
|
-
import { spawn, execFileSync } from "child_process";
|
|
151
|
-
import { Transform } from "stream";
|
|
152
|
-
import fs from "fs";
|
|
153
|
-
import path from "path";
|
|
154
|
-
import { randomUUID } from "crypto";
|
|
155
|
-
import { ClientSideConnection, ndJsonStream } from "@agentclientprotocol/sdk";
|
|
156
|
-
var log = createChildLogger({ module: "agent-instance" });
|
|
157
|
-
function findPackageRoot(startDir) {
|
|
158
|
-
let dir = startDir;
|
|
159
|
-
while (dir !== path.dirname(dir)) {
|
|
160
|
-
if (fs.existsSync(path.join(dir, "package.json"))) {
|
|
161
|
-
return dir;
|
|
162
|
-
}
|
|
163
|
-
dir = path.dirname(dir);
|
|
164
|
-
}
|
|
165
|
-
return startDir;
|
|
166
|
-
}
|
|
167
|
-
function resolveAgentCommand(cmd) {
|
|
168
|
-
const searchRoots = [process.cwd()];
|
|
169
|
-
const ownDir = findPackageRoot(import.meta.dirname);
|
|
170
|
-
if (ownDir !== process.cwd()) {
|
|
171
|
-
searchRoots.push(ownDir);
|
|
172
|
-
}
|
|
173
|
-
for (const root of searchRoots) {
|
|
174
|
-
const packageDirs = [
|
|
175
|
-
path.resolve(root, "node_modules", "@zed-industries", cmd, "dist", "index.js"),
|
|
176
|
-
path.resolve(root, "node_modules", cmd, "dist", "index.js")
|
|
177
|
-
];
|
|
178
|
-
for (const jsPath of packageDirs) {
|
|
179
|
-
if (fs.existsSync(jsPath)) {
|
|
180
|
-
return { command: process.execPath, args: [jsPath] };
|
|
181
|
-
}
|
|
182
|
-
}
|
|
183
|
-
}
|
|
184
|
-
for (const root of searchRoots) {
|
|
185
|
-
const localBin = path.resolve(root, "node_modules", ".bin", cmd);
|
|
186
|
-
if (fs.existsSync(localBin)) {
|
|
187
|
-
const content = fs.readFileSync(localBin, "utf-8");
|
|
188
|
-
if (content.startsWith("#!/usr/bin/env node")) {
|
|
189
|
-
return { command: process.execPath, args: [localBin] };
|
|
190
|
-
}
|
|
191
|
-
const match = content.match(/"([^"]+\.js)"/);
|
|
192
|
-
if (match) {
|
|
193
|
-
const target = path.resolve(path.dirname(localBin), match[1]);
|
|
194
|
-
if (fs.existsSync(target)) {
|
|
195
|
-
return { command: process.execPath, args: [target] };
|
|
196
|
-
}
|
|
197
|
-
}
|
|
198
|
-
}
|
|
199
|
-
}
|
|
200
|
-
try {
|
|
201
|
-
const fullPath = execFileSync("which", [cmd], { encoding: "utf-8" }).trim();
|
|
202
|
-
if (fullPath) {
|
|
203
|
-
const content = fs.readFileSync(fullPath, "utf-8");
|
|
204
|
-
if (content.startsWith("#!/usr/bin/env node")) {
|
|
205
|
-
return { command: process.execPath, args: [fullPath] };
|
|
206
|
-
}
|
|
207
|
-
}
|
|
208
|
-
} catch {
|
|
209
|
-
}
|
|
210
|
-
return { command: cmd, args: [] };
|
|
211
|
-
}
|
|
212
|
-
var AgentInstance = class _AgentInstance extends TypedEmitter {
|
|
213
|
-
connection;
|
|
214
|
-
child;
|
|
215
|
-
stderrCapture;
|
|
216
|
-
terminals = /* @__PURE__ */ new Map();
|
|
217
|
-
sessionId;
|
|
218
|
-
agentName;
|
|
219
|
-
promptCapabilities;
|
|
220
|
-
// Callback — set by core when wiring events
|
|
221
|
-
onPermissionRequest = async () => "";
|
|
222
|
-
constructor(agentName) {
|
|
223
|
-
super();
|
|
224
|
-
this.agentName = agentName;
|
|
225
|
-
}
|
|
226
|
-
static async spawnSubprocess(agentDef, workingDirectory) {
|
|
227
|
-
const instance = new _AgentInstance(agentDef.name);
|
|
228
|
-
const resolved = resolveAgentCommand(agentDef.command);
|
|
229
|
-
log.debug(
|
|
230
|
-
{
|
|
231
|
-
agentName: agentDef.name,
|
|
232
|
-
command: resolved.command,
|
|
233
|
-
args: resolved.args
|
|
234
|
-
},
|
|
235
|
-
"Resolved agent command"
|
|
236
|
-
);
|
|
237
|
-
instance.child = spawn(
|
|
238
|
-
resolved.command,
|
|
239
|
-
[...resolved.args, ...agentDef.args],
|
|
240
|
-
{
|
|
241
|
-
stdio: ["pipe", "pipe", "pipe"],
|
|
242
|
-
cwd: workingDirectory,
|
|
243
|
-
env: { ...process.env, ...agentDef.env }
|
|
244
|
-
}
|
|
245
|
-
);
|
|
246
|
-
await new Promise((resolve3, reject) => {
|
|
247
|
-
instance.child.on("error", (err) => {
|
|
248
|
-
reject(
|
|
249
|
-
new Error(
|
|
250
|
-
`Failed to spawn agent "${agentDef.name}": ${err.message}. Is "${agentDef.command}" installed?`
|
|
251
|
-
)
|
|
252
|
-
);
|
|
253
|
-
});
|
|
254
|
-
instance.child.on("spawn", () => resolve3());
|
|
255
|
-
});
|
|
256
|
-
instance.stderrCapture = new StderrCapture(50);
|
|
257
|
-
instance.child.stderr.on("data", (chunk) => {
|
|
258
|
-
instance.stderrCapture.append(chunk.toString());
|
|
259
|
-
});
|
|
260
|
-
const stdinLogger = new Transform({
|
|
261
|
-
transform(chunk, _enc, cb) {
|
|
262
|
-
log.debug(
|
|
263
|
-
{ direction: "send", raw: chunk.toString().trimEnd() },
|
|
264
|
-
"ACP raw"
|
|
265
|
-
);
|
|
266
|
-
cb(null, chunk);
|
|
267
|
-
}
|
|
268
|
-
});
|
|
269
|
-
stdinLogger.pipe(instance.child.stdin);
|
|
270
|
-
const stdoutLogger = new Transform({
|
|
271
|
-
transform(chunk, _enc, cb) {
|
|
272
|
-
log.debug(
|
|
273
|
-
{ direction: "recv", raw: chunk.toString().trimEnd() },
|
|
274
|
-
"ACP raw"
|
|
275
|
-
);
|
|
276
|
-
cb(null, chunk);
|
|
277
|
-
}
|
|
278
|
-
});
|
|
279
|
-
instance.child.stdout.pipe(stdoutLogger);
|
|
280
|
-
const toAgent = nodeToWebWritable(stdinLogger);
|
|
281
|
-
const fromAgent = nodeToWebReadable(stdoutLogger);
|
|
282
|
-
const stream = ndJsonStream(toAgent, fromAgent);
|
|
283
|
-
instance.connection = new ClientSideConnection(
|
|
284
|
-
(_agent) => instance.createClient(_agent),
|
|
285
|
-
stream
|
|
286
|
-
);
|
|
287
|
-
const initResponse = await instance.connection.initialize({
|
|
288
|
-
protocolVersion: 1,
|
|
289
|
-
clientCapabilities: {
|
|
290
|
-
fs: { readTextFile: true, writeTextFile: true },
|
|
291
|
-
terminal: true
|
|
292
|
-
}
|
|
293
|
-
});
|
|
294
|
-
instance.promptCapabilities = initResponse.agentCapabilities?.promptCapabilities;
|
|
295
|
-
log.info(
|
|
296
|
-
{ promptCapabilities: instance.promptCapabilities ?? {} },
|
|
297
|
-
"Agent prompt capabilities"
|
|
298
|
-
);
|
|
299
|
-
return instance;
|
|
300
|
-
}
|
|
301
|
-
setupCrashDetection() {
|
|
302
|
-
this.child.on("exit", (code, signal) => {
|
|
303
|
-
log.info(
|
|
304
|
-
{ sessionId: this.sessionId, exitCode: code, signal },
|
|
305
|
-
"Agent process exited"
|
|
306
|
-
);
|
|
307
|
-
if (code !== 0 && code !== null) {
|
|
308
|
-
const stderr = this.stderrCapture.getLastLines();
|
|
309
|
-
this.emit("agent_event", {
|
|
310
|
-
type: "error",
|
|
311
|
-
message: `Agent crashed (exit code ${code})
|
|
312
|
-
${stderr}`
|
|
313
|
-
});
|
|
314
|
-
}
|
|
315
|
-
});
|
|
316
|
-
this.connection.closed.then(() => {
|
|
317
|
-
log.debug({ sessionId: this.sessionId }, "ACP connection closed");
|
|
318
|
-
});
|
|
319
|
-
}
|
|
320
|
-
static async spawn(agentDef, workingDirectory) {
|
|
321
|
-
log.debug(
|
|
322
|
-
{ agentName: agentDef.name, command: agentDef.command },
|
|
323
|
-
"Spawning agent"
|
|
324
|
-
);
|
|
325
|
-
const spawnStart = Date.now();
|
|
326
|
-
const instance = await _AgentInstance.spawnSubprocess(
|
|
327
|
-
agentDef,
|
|
328
|
-
workingDirectory
|
|
329
|
-
);
|
|
330
|
-
const response = await instance.connection.newSession({
|
|
331
|
-
cwd: workingDirectory,
|
|
332
|
-
mcpServers: []
|
|
333
|
-
});
|
|
334
|
-
instance.sessionId = response.sessionId;
|
|
335
|
-
instance.setupCrashDetection();
|
|
336
|
-
log.info(
|
|
337
|
-
{ sessionId: response.sessionId, durationMs: Date.now() - spawnStart },
|
|
338
|
-
"Agent spawn complete"
|
|
339
|
-
);
|
|
340
|
-
return instance;
|
|
341
|
-
}
|
|
342
|
-
static async resume(agentDef, workingDirectory, agentSessionId) {
|
|
343
|
-
log.debug({ agentName: agentDef.name, agentSessionId }, "Resuming agent");
|
|
344
|
-
const spawnStart = Date.now();
|
|
345
|
-
const instance = await _AgentInstance.spawnSubprocess(
|
|
346
|
-
agentDef,
|
|
347
|
-
workingDirectory
|
|
348
|
-
);
|
|
349
|
-
try {
|
|
350
|
-
const response = await instance.connection.unstable_resumeSession({
|
|
351
|
-
sessionId: agentSessionId,
|
|
352
|
-
cwd: workingDirectory
|
|
353
|
-
});
|
|
354
|
-
instance.sessionId = response.sessionId;
|
|
355
|
-
log.info(
|
|
356
|
-
{ sessionId: response.sessionId, durationMs: Date.now() - spawnStart },
|
|
357
|
-
"Agent resume complete"
|
|
358
|
-
);
|
|
359
|
-
} catch (err) {
|
|
360
|
-
log.warn(
|
|
361
|
-
{ err, agentSessionId },
|
|
362
|
-
"Resume failed, falling back to new session"
|
|
363
|
-
);
|
|
364
|
-
const response = await instance.connection.newSession({
|
|
365
|
-
cwd: workingDirectory,
|
|
366
|
-
mcpServers: []
|
|
367
|
-
});
|
|
368
|
-
instance.sessionId = response.sessionId;
|
|
369
|
-
log.info(
|
|
370
|
-
{ sessionId: response.sessionId, durationMs: Date.now() - spawnStart },
|
|
371
|
-
"Agent fallback spawn complete"
|
|
372
|
-
);
|
|
373
|
-
}
|
|
374
|
-
instance.setupCrashDetection();
|
|
375
|
-
return instance;
|
|
376
|
-
}
|
|
377
|
-
// createClient — implemented in Task 6b
|
|
378
|
-
createClient(_agent) {
|
|
379
|
-
const self = this;
|
|
380
|
-
const MAX_OUTPUT_BYTES = 1024 * 1024;
|
|
381
|
-
return {
|
|
382
|
-
// ── Session updates ──────────────────────────────────────────────────
|
|
383
|
-
async sessionUpdate(params) {
|
|
384
|
-
const update = params.update;
|
|
385
|
-
let event = null;
|
|
386
|
-
switch (update.sessionUpdate) {
|
|
387
|
-
case "agent_message_chunk":
|
|
388
|
-
if (update.content.type === "text") {
|
|
389
|
-
event = { type: "text", content: update.content.text };
|
|
390
|
-
} else if (update.content.type === "image") {
|
|
391
|
-
const c = update.content;
|
|
392
|
-
event = { type: "image_content", data: c.data, mimeType: c.mimeType };
|
|
393
|
-
} else if (update.content.type === "audio") {
|
|
394
|
-
const c = update.content;
|
|
395
|
-
event = { type: "audio_content", data: c.data, mimeType: c.mimeType };
|
|
396
|
-
}
|
|
397
|
-
break;
|
|
398
|
-
case "agent_thought_chunk":
|
|
399
|
-
if (update.content.type === "text") {
|
|
400
|
-
event = { type: "thought", content: update.content.text };
|
|
401
|
-
}
|
|
402
|
-
break;
|
|
403
|
-
case "tool_call":
|
|
404
|
-
event = {
|
|
405
|
-
type: "tool_call",
|
|
406
|
-
id: update.toolCallId,
|
|
407
|
-
name: update.title,
|
|
408
|
-
kind: update.kind ?? void 0,
|
|
409
|
-
status: update.status ?? "pending",
|
|
410
|
-
content: update.content ?? void 0,
|
|
411
|
-
rawInput: update.rawInput ?? void 0,
|
|
412
|
-
meta: update._meta ?? void 0
|
|
413
|
-
};
|
|
414
|
-
break;
|
|
415
|
-
case "tool_call_update":
|
|
416
|
-
event = {
|
|
417
|
-
type: "tool_update",
|
|
418
|
-
id: update.toolCallId,
|
|
419
|
-
name: update.title ?? void 0,
|
|
420
|
-
kind: update.kind ?? void 0,
|
|
421
|
-
status: update.status ?? "pending",
|
|
422
|
-
content: update.content ?? void 0,
|
|
423
|
-
rawInput: update.rawInput ?? void 0,
|
|
424
|
-
meta: update._meta ?? void 0
|
|
425
|
-
};
|
|
426
|
-
break;
|
|
427
|
-
case "plan":
|
|
428
|
-
event = { type: "plan", entries: update.entries };
|
|
429
|
-
break;
|
|
430
|
-
case "usage_update":
|
|
431
|
-
event = {
|
|
432
|
-
type: "usage",
|
|
433
|
-
tokensUsed: update.used,
|
|
434
|
-
contextSize: update.size,
|
|
435
|
-
cost: update.cost ?? void 0
|
|
436
|
-
};
|
|
437
|
-
break;
|
|
438
|
-
case "available_commands_update":
|
|
439
|
-
event = {
|
|
440
|
-
type: "commands_update",
|
|
441
|
-
commands: update.availableCommands
|
|
442
|
-
};
|
|
443
|
-
break;
|
|
444
|
-
default:
|
|
445
|
-
return;
|
|
446
|
-
}
|
|
447
|
-
if (event !== null) {
|
|
448
|
-
self.emit("agent_event", event);
|
|
449
|
-
}
|
|
450
|
-
},
|
|
451
|
-
// ── Permission requests ──────────────────────────────────────────────
|
|
452
|
-
async requestPermission(params) {
|
|
453
|
-
const permissionRequest = {
|
|
454
|
-
id: params.toolCall.toolCallId,
|
|
455
|
-
description: params.toolCall.title ?? params.toolCall.toolCallId,
|
|
456
|
-
options: params.options.map((opt) => ({
|
|
457
|
-
id: opt.optionId,
|
|
458
|
-
label: opt.name,
|
|
459
|
-
isAllow: opt.kind === "allow_once" || opt.kind === "allow_always"
|
|
460
|
-
}))
|
|
461
|
-
};
|
|
462
|
-
const selectedOptionId = await self.onPermissionRequest(permissionRequest);
|
|
463
|
-
return {
|
|
464
|
-
outcome: { outcome: "selected", optionId: selectedOptionId }
|
|
465
|
-
};
|
|
466
|
-
},
|
|
467
|
-
// ── File operations ──────────────────────────────────────────────────
|
|
468
|
-
async readTextFile(params) {
|
|
469
|
-
const content = await fs.promises.readFile(params.path, "utf-8");
|
|
470
|
-
return { content };
|
|
471
|
-
},
|
|
472
|
-
async writeTextFile(params) {
|
|
473
|
-
await fs.promises.mkdir(path.dirname(params.path), { recursive: true });
|
|
474
|
-
await fs.promises.writeFile(params.path, params.content, "utf-8");
|
|
475
|
-
return {};
|
|
476
|
-
},
|
|
477
|
-
// ── Terminal operations ──────────────────────────────────────────────
|
|
478
|
-
async createTerminal(params) {
|
|
479
|
-
const terminalId = randomUUID();
|
|
480
|
-
const args = params.args ?? [];
|
|
481
|
-
const env = {};
|
|
482
|
-
for (const ev of params.env ?? []) {
|
|
483
|
-
env[ev.name] = ev.value;
|
|
484
|
-
}
|
|
485
|
-
const childProcess = spawn(params.command, args, {
|
|
486
|
-
cwd: params.cwd ?? void 0,
|
|
487
|
-
env: { ...process.env, ...env },
|
|
488
|
-
shell: false
|
|
489
|
-
});
|
|
490
|
-
const state = {
|
|
491
|
-
process: childProcess,
|
|
492
|
-
output: "",
|
|
493
|
-
exitStatus: null
|
|
494
|
-
};
|
|
495
|
-
self.terminals.set(terminalId, state);
|
|
496
|
-
const outputByteLimit = params.outputByteLimit ?? MAX_OUTPUT_BYTES;
|
|
497
|
-
const appendOutput = (chunk) => {
|
|
498
|
-
state.output += chunk;
|
|
499
|
-
const bytes = Buffer.byteLength(state.output, "utf-8");
|
|
500
|
-
if (bytes > outputByteLimit) {
|
|
501
|
-
const excess = bytes - outputByteLimit;
|
|
502
|
-
state.output = state.output.slice(excess);
|
|
503
|
-
}
|
|
504
|
-
};
|
|
505
|
-
childProcess.stdout?.on(
|
|
506
|
-
"data",
|
|
507
|
-
(chunk) => appendOutput(chunk.toString())
|
|
508
|
-
);
|
|
509
|
-
childProcess.stderr?.on(
|
|
510
|
-
"data",
|
|
511
|
-
(chunk) => appendOutput(chunk.toString())
|
|
512
|
-
);
|
|
513
|
-
childProcess.on("exit", (code, signal) => {
|
|
514
|
-
state.exitStatus = { exitCode: code, signal };
|
|
515
|
-
});
|
|
516
|
-
return { terminalId };
|
|
517
|
-
},
|
|
518
|
-
async terminalOutput(params) {
|
|
519
|
-
const state = self.terminals.get(params.terminalId);
|
|
520
|
-
if (!state) {
|
|
521
|
-
throw new Error(`Terminal not found: ${params.terminalId}`);
|
|
522
|
-
}
|
|
523
|
-
return {
|
|
524
|
-
output: state.output,
|
|
525
|
-
truncated: false,
|
|
526
|
-
exitStatus: state.exitStatus ? {
|
|
527
|
-
exitCode: state.exitStatus.exitCode,
|
|
528
|
-
signal: state.exitStatus.signal
|
|
529
|
-
} : void 0
|
|
530
|
-
};
|
|
531
|
-
},
|
|
532
|
-
async waitForTerminalExit(params) {
|
|
533
|
-
const state = self.terminals.get(params.terminalId);
|
|
534
|
-
if (!state) {
|
|
535
|
-
throw new Error(`Terminal not found: ${params.terminalId}`);
|
|
536
|
-
}
|
|
537
|
-
if (state.exitStatus !== null) {
|
|
538
|
-
return {
|
|
539
|
-
exitCode: state.exitStatus.exitCode,
|
|
540
|
-
signal: state.exitStatus.signal
|
|
541
|
-
};
|
|
542
|
-
}
|
|
543
|
-
return new Promise((resolve3) => {
|
|
544
|
-
state.process.on("exit", (code, signal) => {
|
|
545
|
-
resolve3({ exitCode: code, signal });
|
|
546
|
-
});
|
|
547
|
-
});
|
|
548
|
-
},
|
|
549
|
-
async killTerminal(params) {
|
|
550
|
-
const state = self.terminals.get(params.terminalId);
|
|
551
|
-
if (!state) {
|
|
552
|
-
throw new Error(`Terminal not found: ${params.terminalId}`);
|
|
553
|
-
}
|
|
554
|
-
state.process.kill("SIGTERM");
|
|
555
|
-
return {};
|
|
556
|
-
},
|
|
557
|
-
async releaseTerminal(params) {
|
|
558
|
-
const state = self.terminals.get(params.terminalId);
|
|
559
|
-
if (!state) {
|
|
560
|
-
return;
|
|
561
|
-
}
|
|
562
|
-
state.process.kill("SIGKILL");
|
|
563
|
-
self.terminals.delete(params.terminalId);
|
|
564
|
-
}
|
|
565
|
-
};
|
|
566
|
-
}
|
|
567
|
-
async prompt(text, attachments) {
|
|
568
|
-
const contentBlocks = [{ type: "text", text }];
|
|
569
|
-
const SUPPORTED_IMAGE_MIMES = /* @__PURE__ */ new Set(["image/jpeg", "image/png", "image/gif", "image/webp"]);
|
|
570
|
-
for (const att of attachments ?? []) {
|
|
571
|
-
const tooLarge = att.size > 10 * 1024 * 1024;
|
|
572
|
-
if (att.type === "image" && this.promptCapabilities?.image && !tooLarge && SUPPORTED_IMAGE_MIMES.has(att.mimeType)) {
|
|
573
|
-
const data = await fs.promises.readFile(att.filePath);
|
|
574
|
-
contentBlocks.push({ type: "image", data: data.toString("base64"), mimeType: att.mimeType });
|
|
575
|
-
} else if (att.type === "audio" && this.promptCapabilities?.audio && !tooLarge) {
|
|
576
|
-
const data = await fs.promises.readFile(att.filePath);
|
|
577
|
-
contentBlocks.push({ type: "audio", data: data.toString("base64"), mimeType: att.mimeType });
|
|
578
|
-
} else {
|
|
579
|
-
if ((att.type === "image" || att.type === "audio") && !tooLarge) {
|
|
580
|
-
log.debug(
|
|
581
|
-
{ type: att.type, capabilities: this.promptCapabilities ?? {} },
|
|
582
|
-
"Agent does not support %s content, falling back to file path",
|
|
583
|
-
att.type
|
|
584
|
-
);
|
|
585
|
-
}
|
|
586
|
-
contentBlocks[0].text += `
|
|
587
|
-
|
|
588
|
-
[Attached file: ${att.filePath}]`;
|
|
589
|
-
}
|
|
590
|
-
}
|
|
591
|
-
return this.connection.prompt({
|
|
592
|
-
sessionId: this.sessionId,
|
|
593
|
-
prompt: contentBlocks
|
|
594
|
-
});
|
|
595
|
-
}
|
|
596
|
-
async cancel() {
|
|
597
|
-
await this.connection.cancel({ sessionId: this.sessionId });
|
|
598
|
-
}
|
|
599
|
-
async destroy() {
|
|
600
|
-
for (const [, t] of this.terminals) {
|
|
601
|
-
t.process.kill("SIGKILL");
|
|
602
|
-
}
|
|
603
|
-
this.terminals.clear();
|
|
604
|
-
this.child.kill("SIGTERM");
|
|
605
|
-
setTimeout(() => {
|
|
606
|
-
if (!this.child.killed) this.child.kill("SIGKILL");
|
|
607
|
-
}, 1e4);
|
|
608
|
-
}
|
|
609
|
-
};
|
|
610
|
-
|
|
611
|
-
// src/core/agent-manager.ts
|
|
612
|
-
var AgentManager = class {
|
|
613
|
-
constructor(catalog) {
|
|
614
|
-
this.catalog = catalog;
|
|
615
|
-
}
|
|
616
|
-
getAvailableAgents() {
|
|
617
|
-
const installed = this.catalog.getInstalledEntries();
|
|
618
|
-
return Object.entries(installed).map(([key, agent]) => ({
|
|
619
|
-
name: key,
|
|
620
|
-
command: agent.command,
|
|
621
|
-
args: agent.args,
|
|
622
|
-
env: agent.env
|
|
623
|
-
}));
|
|
624
|
-
}
|
|
625
|
-
getAgent(name) {
|
|
626
|
-
return this.catalog.resolve(name);
|
|
627
|
-
}
|
|
628
|
-
async spawn(agentName, workingDirectory) {
|
|
629
|
-
const agentDef = this.getAgent(agentName);
|
|
630
|
-
if (!agentDef) throw new Error(`Agent "${agentName}" is not installed. Run "openacp agents install ${agentName}" to add it.`);
|
|
631
|
-
return AgentInstance.spawn(agentDef, workingDirectory);
|
|
632
|
-
}
|
|
633
|
-
async resume(agentName, workingDirectory, agentSessionId) {
|
|
634
|
-
const agentDef = this.getAgent(agentName);
|
|
635
|
-
if (!agentDef) throw new Error(`Agent "${agentName}" is not installed. Run "openacp agents install ${agentName}" to add it.`);
|
|
636
|
-
return AgentInstance.resume(agentDef, workingDirectory, agentSessionId);
|
|
637
|
-
}
|
|
638
|
-
};
|
|
639
|
-
|
|
640
|
-
// src/core/prompt-queue.ts
|
|
641
|
-
var PromptQueue = class {
|
|
642
|
-
constructor(processor, onError) {
|
|
643
|
-
this.processor = processor;
|
|
644
|
-
this.onError = onError;
|
|
645
|
-
}
|
|
646
|
-
queue = [];
|
|
647
|
-
processing = false;
|
|
648
|
-
abortController = null;
|
|
649
|
-
async enqueue(text, attachments) {
|
|
650
|
-
if (this.processing) {
|
|
651
|
-
return new Promise((resolve3) => {
|
|
652
|
-
this.queue.push({ text, attachments, resolve: resolve3 });
|
|
653
|
-
});
|
|
654
|
-
}
|
|
655
|
-
await this.process(text, attachments);
|
|
656
|
-
}
|
|
657
|
-
async process(text, attachments) {
|
|
658
|
-
this.processing = true;
|
|
659
|
-
this.abortController = new AbortController();
|
|
660
|
-
const { signal } = this.abortController;
|
|
661
|
-
try {
|
|
662
|
-
await Promise.race([
|
|
663
|
-
this.processor(text, attachments),
|
|
664
|
-
new Promise((_, reject) => {
|
|
665
|
-
signal.addEventListener("abort", () => reject(new Error("Prompt aborted")), { once: true });
|
|
666
|
-
})
|
|
667
|
-
]);
|
|
668
|
-
} catch (err) {
|
|
669
|
-
if (!(err instanceof Error && err.message === "Prompt aborted")) {
|
|
670
|
-
this.onError?.(err);
|
|
671
|
-
}
|
|
672
|
-
} finally {
|
|
673
|
-
this.abortController = null;
|
|
674
|
-
this.processing = false;
|
|
675
|
-
this.drainNext();
|
|
676
|
-
}
|
|
677
|
-
}
|
|
678
|
-
drainNext() {
|
|
679
|
-
const next = this.queue.shift();
|
|
680
|
-
if (next) {
|
|
681
|
-
this.process(next.text, next.attachments).then(next.resolve);
|
|
682
|
-
}
|
|
683
|
-
}
|
|
684
|
-
clear() {
|
|
685
|
-
if (this.abortController) {
|
|
686
|
-
this.abortController.abort();
|
|
687
|
-
}
|
|
688
|
-
for (const item of this.queue) {
|
|
689
|
-
item.resolve();
|
|
690
|
-
}
|
|
691
|
-
this.queue = [];
|
|
692
|
-
}
|
|
693
|
-
get pending() {
|
|
694
|
-
return this.queue.length;
|
|
695
|
-
}
|
|
696
|
-
get isProcessing() {
|
|
697
|
-
return this.processing;
|
|
698
|
-
}
|
|
699
|
-
};
|
|
700
|
-
|
|
701
|
-
// src/core/permission-gate.ts
|
|
702
|
-
var DEFAULT_TIMEOUT_MS = 10 * 60 * 1e3;
|
|
703
|
-
var PermissionGate = class {
|
|
704
|
-
request;
|
|
705
|
-
resolveFn;
|
|
706
|
-
rejectFn;
|
|
707
|
-
settled = false;
|
|
708
|
-
timeoutTimer;
|
|
709
|
-
timeoutMs;
|
|
710
|
-
constructor(timeoutMs) {
|
|
711
|
-
this.timeoutMs = timeoutMs ?? DEFAULT_TIMEOUT_MS;
|
|
712
|
-
}
|
|
713
|
-
setPending(request) {
|
|
714
|
-
this.request = request;
|
|
715
|
-
this.settled = false;
|
|
716
|
-
this.clearTimeout();
|
|
717
|
-
return new Promise((resolve3, reject) => {
|
|
718
|
-
this.resolveFn = resolve3;
|
|
719
|
-
this.rejectFn = reject;
|
|
720
|
-
this.timeoutTimer = setTimeout(() => {
|
|
721
|
-
this.reject("Permission request timed out (no response received)");
|
|
722
|
-
}, this.timeoutMs);
|
|
723
|
-
});
|
|
724
|
-
}
|
|
725
|
-
resolve(optionId) {
|
|
726
|
-
if (this.settled || !this.resolveFn) return;
|
|
727
|
-
this.settled = true;
|
|
728
|
-
this.clearTimeout();
|
|
729
|
-
this.resolveFn(optionId);
|
|
730
|
-
this.cleanup();
|
|
731
|
-
}
|
|
732
|
-
reject(reason) {
|
|
733
|
-
if (this.settled || !this.rejectFn) return;
|
|
734
|
-
this.settled = true;
|
|
735
|
-
this.clearTimeout();
|
|
736
|
-
this.rejectFn(new Error(reason ?? "Permission rejected"));
|
|
737
|
-
this.cleanup();
|
|
738
|
-
}
|
|
739
|
-
get isPending() {
|
|
740
|
-
return !!this.request && !this.settled;
|
|
741
|
-
}
|
|
742
|
-
get currentRequest() {
|
|
743
|
-
return this.isPending ? this.request : void 0;
|
|
744
|
-
}
|
|
745
|
-
/** The request ID of the current pending request, undefined after settlement */
|
|
746
|
-
get requestId() {
|
|
747
|
-
return this.request?.id;
|
|
748
|
-
}
|
|
749
|
-
clearTimeout() {
|
|
750
|
-
if (this.timeoutTimer) {
|
|
751
|
-
clearTimeout(this.timeoutTimer);
|
|
752
|
-
this.timeoutTimer = void 0;
|
|
753
|
-
}
|
|
754
|
-
}
|
|
755
|
-
cleanup() {
|
|
756
|
-
this.request = void 0;
|
|
757
|
-
this.resolveFn = void 0;
|
|
758
|
-
this.rejectFn = void 0;
|
|
759
|
-
}
|
|
760
|
-
};
|
|
761
|
-
|
|
762
|
-
// src/core/session.ts
|
|
763
|
-
import { nanoid } from "nanoid";
|
|
764
|
-
import * as fs2 from "fs";
|
|
765
|
-
var moduleLog = createChildLogger({ module: "session" });
|
|
766
|
-
var TTS_PROMPT_INSTRUCTION = `
|
|
767
|
-
|
|
768
|
-
Additionally, include a [TTS]...[/TTS] block with a spoken-friendly summary of your response. Focus on key information, decisions the user needs to make, or actions required. The agent decides what to say and how long. Respond in the same language the user is using. This instruction applies to this message only.`;
|
|
769
|
-
var TTS_BLOCK_REGEX = /\[TTS\]([\s\S]*?)\[\/TTS\]/;
|
|
770
|
-
var TTS_MAX_LENGTH = 5e3;
|
|
771
|
-
var TTS_TIMEOUT_MS = 3e4;
|
|
772
|
-
var VALID_TRANSITIONS = {
|
|
773
|
-
initializing: /* @__PURE__ */ new Set(["active", "error"]),
|
|
774
|
-
active: /* @__PURE__ */ new Set(["error", "finished", "cancelled"]),
|
|
775
|
-
error: /* @__PURE__ */ new Set(["active"]),
|
|
776
|
-
cancelled: /* @__PURE__ */ new Set(["active"]),
|
|
777
|
-
finished: /* @__PURE__ */ new Set()
|
|
778
|
-
};
|
|
779
|
-
var Session = class extends TypedEmitter {
|
|
780
|
-
id;
|
|
781
|
-
channelId;
|
|
782
|
-
threadId = "";
|
|
783
|
-
agentName;
|
|
784
|
-
workingDirectory;
|
|
785
|
-
agentInstance;
|
|
786
|
-
agentSessionId = "";
|
|
787
|
-
_status = "initializing";
|
|
788
|
-
name;
|
|
789
|
-
createdAt = /* @__PURE__ */ new Date();
|
|
790
|
-
voiceMode = "off";
|
|
791
|
-
dangerousMode = false;
|
|
792
|
-
archiving = false;
|
|
793
|
-
log;
|
|
794
|
-
permissionGate = new PermissionGate();
|
|
795
|
-
queue;
|
|
796
|
-
speechService;
|
|
797
|
-
constructor(opts) {
|
|
798
|
-
super();
|
|
799
|
-
this.id = opts.id || nanoid(12);
|
|
800
|
-
this.channelId = opts.channelId;
|
|
801
|
-
this.agentName = opts.agentName;
|
|
802
|
-
this.workingDirectory = opts.workingDirectory;
|
|
803
|
-
this.agentInstance = opts.agentInstance;
|
|
804
|
-
this.speechService = opts.speechService;
|
|
805
|
-
this.log = createSessionLogger(this.id, moduleLog);
|
|
806
|
-
this.log.info({ agentName: this.agentName }, "Session created");
|
|
807
|
-
this.queue = new PromptQueue(
|
|
808
|
-
(text, attachments) => this.processPrompt(text, attachments),
|
|
809
|
-
(err) => {
|
|
810
|
-
this.fail("Prompt execution failed");
|
|
811
|
-
this.log.error({ err }, "Prompt execution failed");
|
|
812
|
-
}
|
|
813
|
-
);
|
|
814
|
-
}
|
|
815
|
-
// --- State Machine ---
|
|
816
|
-
get status() {
|
|
817
|
-
return this._status;
|
|
818
|
-
}
|
|
819
|
-
/** Transition to active — from initializing, error, or cancelled */
|
|
820
|
-
activate() {
|
|
821
|
-
this.transition("active");
|
|
822
|
-
}
|
|
823
|
-
/** Transition to error — from initializing or active */
|
|
824
|
-
fail(reason) {
|
|
825
|
-
this.transition("error");
|
|
826
|
-
this.emit("error", new Error(reason));
|
|
827
|
-
}
|
|
828
|
-
/** Transition to finished — from active only. Emits session_end for backward compat. */
|
|
829
|
-
finish(reason) {
|
|
830
|
-
this.transition("finished");
|
|
831
|
-
this.emit("session_end", reason ?? "completed");
|
|
832
|
-
}
|
|
833
|
-
/** Transition to cancelled — from active only (terminal session cancel) */
|
|
834
|
-
markCancelled() {
|
|
835
|
-
this.transition("cancelled");
|
|
836
|
-
}
|
|
837
|
-
transition(to) {
|
|
838
|
-
const from = this._status;
|
|
839
|
-
const allowed = VALID_TRANSITIONS[from];
|
|
840
|
-
if (!allowed?.has(to)) {
|
|
841
|
-
throw new Error(
|
|
842
|
-
`Invalid session transition: ${from} \u2192 ${to}`
|
|
843
|
-
);
|
|
844
|
-
}
|
|
845
|
-
this._status = to;
|
|
846
|
-
this.log.debug({ from, to }, "Session status transition");
|
|
847
|
-
this.emit("status_change", from, to);
|
|
848
|
-
}
|
|
849
|
-
/** Number of prompts waiting in queue */
|
|
850
|
-
get queueDepth() {
|
|
851
|
-
return this.queue.pending;
|
|
852
|
-
}
|
|
853
|
-
get promptRunning() {
|
|
854
|
-
return this.queue.isProcessing;
|
|
855
|
-
}
|
|
856
|
-
// --- Voice Mode ---
|
|
857
|
-
setVoiceMode(mode) {
|
|
858
|
-
this.voiceMode = mode;
|
|
859
|
-
this.log.info({ voiceMode: mode }, "TTS mode changed");
|
|
860
|
-
}
|
|
861
|
-
// --- Public API ---
|
|
862
|
-
async enqueuePrompt(text, attachments) {
|
|
863
|
-
await this.queue.enqueue(text, attachments);
|
|
864
|
-
}
|
|
865
|
-
async processPrompt(text, attachments) {
|
|
866
|
-
if (text === "\0__warmup__") {
|
|
867
|
-
await this.runWarmup();
|
|
868
|
-
return;
|
|
869
|
-
}
|
|
870
|
-
if (this._status === "initializing") {
|
|
871
|
-
this.activate();
|
|
872
|
-
}
|
|
873
|
-
const promptStart = Date.now();
|
|
874
|
-
this.log.debug("Prompt execution started");
|
|
875
|
-
const processed = await this.maybeTranscribeAudio(text, attachments);
|
|
876
|
-
const ttsActive = this.voiceMode !== "off" && !!this.speechService?.isTTSAvailable();
|
|
877
|
-
if (ttsActive) {
|
|
878
|
-
processed.text += TTS_PROMPT_INSTRUCTION;
|
|
879
|
-
if (this.voiceMode === "next") {
|
|
880
|
-
this.voiceMode = "off";
|
|
881
|
-
}
|
|
882
|
-
}
|
|
883
|
-
let accumulatedText = "";
|
|
884
|
-
const accumulatorListener = ttsActive ? (event) => {
|
|
885
|
-
if (event.type === "text") {
|
|
886
|
-
accumulatedText += event.content;
|
|
887
|
-
}
|
|
888
|
-
} : null;
|
|
889
|
-
if (accumulatorListener) {
|
|
890
|
-
this.on("agent_event", accumulatorListener);
|
|
891
|
-
}
|
|
892
|
-
try {
|
|
893
|
-
await this.agentInstance.prompt(processed.text, processed.attachments);
|
|
894
|
-
} finally {
|
|
895
|
-
if (accumulatorListener) {
|
|
896
|
-
this.off("agent_event", accumulatorListener);
|
|
897
|
-
}
|
|
898
|
-
}
|
|
899
|
-
this.log.info(
|
|
900
|
-
{ durationMs: Date.now() - promptStart },
|
|
901
|
-
"Prompt execution completed"
|
|
902
|
-
);
|
|
903
|
-
if (ttsActive && accumulatedText) {
|
|
904
|
-
this.processTTSResponse(accumulatedText).catch((err) => {
|
|
905
|
-
this.log.warn({ err }, "TTS post-processing failed");
|
|
906
|
-
});
|
|
907
|
-
}
|
|
908
|
-
if (!this.name) {
|
|
909
|
-
await this.autoName();
|
|
910
|
-
}
|
|
911
|
-
}
|
|
912
|
-
async maybeTranscribeAudio(text, attachments) {
|
|
913
|
-
if (!attachments?.length || !this.speechService) {
|
|
914
|
-
return { text, attachments };
|
|
915
|
-
}
|
|
916
|
-
const hasAudioCapability = this.agentInstance.promptCapabilities?.audio === true;
|
|
917
|
-
if (hasAudioCapability) {
|
|
918
|
-
return { text, attachments };
|
|
919
|
-
}
|
|
920
|
-
if (!this.speechService.isSTTAvailable()) {
|
|
921
|
-
return { text, attachments };
|
|
922
|
-
}
|
|
923
|
-
let transcribedText = text;
|
|
924
|
-
const remainingAttachments = [];
|
|
925
|
-
for (const att of attachments) {
|
|
926
|
-
if (att.type !== "audio") {
|
|
927
|
-
remainingAttachments.push(att);
|
|
928
|
-
continue;
|
|
929
|
-
}
|
|
930
|
-
try {
|
|
931
|
-
const audioPath = att.originalFilePath || att.filePath;
|
|
932
|
-
const audioMime = att.originalFilePath ? "audio/ogg" : att.mimeType;
|
|
933
|
-
const audioBuffer = await fs2.promises.readFile(audioPath);
|
|
934
|
-
const result = await this.speechService.transcribe(audioBuffer, audioMime);
|
|
935
|
-
this.log.info({ provider: "stt", duration: result.duration }, "Voice transcribed");
|
|
936
|
-
this.emit("agent_event", {
|
|
937
|
-
type: "system_message",
|
|
938
|
-
message: `\u{1F3A4} You said: ${result.text}`
|
|
939
|
-
});
|
|
940
|
-
transcribedText = transcribedText.replace(/\[Audio:\s*[^\]]*\]\s*/g, "").trim();
|
|
941
|
-
transcribedText = transcribedText ? `${transcribedText}
|
|
942
|
-
${result.text}` : result.text;
|
|
943
|
-
} catch (err) {
|
|
944
|
-
this.log.warn({ err }, "STT transcription failed, keeping audio attachment");
|
|
945
|
-
this.emit("agent_event", {
|
|
946
|
-
type: "error",
|
|
947
|
-
message: `Voice transcription failed: ${err.message}`
|
|
948
|
-
});
|
|
949
|
-
remainingAttachments.push(att);
|
|
950
|
-
}
|
|
951
|
-
}
|
|
952
|
-
return {
|
|
953
|
-
text: transcribedText,
|
|
954
|
-
attachments: remainingAttachments.length > 0 ? remainingAttachments : void 0
|
|
955
|
-
};
|
|
956
|
-
}
|
|
957
|
-
async processTTSResponse(responseText) {
|
|
958
|
-
const match = TTS_BLOCK_REGEX.exec(responseText);
|
|
959
|
-
if (!match?.[1]) {
|
|
960
|
-
this.log.debug("No [TTS] block found in response, skipping synthesis");
|
|
961
|
-
return;
|
|
962
|
-
}
|
|
963
|
-
let ttsText = match[1].trim();
|
|
964
|
-
if (!ttsText) return;
|
|
965
|
-
if (ttsText.length > TTS_MAX_LENGTH) {
|
|
966
|
-
ttsText = ttsText.slice(0, TTS_MAX_LENGTH);
|
|
967
|
-
}
|
|
968
|
-
try {
|
|
969
|
-
const timeoutPromise = new Promise(
|
|
970
|
-
(_, reject) => setTimeout(() => reject(new Error("TTS synthesis timed out")), TTS_TIMEOUT_MS)
|
|
971
|
-
);
|
|
972
|
-
const result = await Promise.race([
|
|
973
|
-
this.speechService.synthesize(ttsText),
|
|
974
|
-
timeoutPromise
|
|
975
|
-
]);
|
|
976
|
-
const base64 = result.audioBuffer.toString("base64");
|
|
977
|
-
this.emit("agent_event", {
|
|
978
|
-
type: "audio_content",
|
|
979
|
-
data: base64,
|
|
980
|
-
mimeType: result.mimeType
|
|
981
|
-
});
|
|
982
|
-
this.log.info("TTS synthesis completed");
|
|
983
|
-
} catch (err) {
|
|
984
|
-
this.log.warn({ err }, "TTS synthesis failed, skipping");
|
|
985
|
-
}
|
|
986
|
-
}
|
|
987
|
-
// NOTE: This injects a summary prompt into the agent's conversation history.
|
|
988
|
-
async autoName() {
|
|
989
|
-
let title = "";
|
|
990
|
-
const captureHandler = (event) => {
|
|
991
|
-
if (event.type === "text") title += event.content;
|
|
992
|
-
};
|
|
993
|
-
this.pause((event) => event !== "agent_event");
|
|
994
|
-
this.agentInstance.on("agent_event", captureHandler);
|
|
995
|
-
try {
|
|
996
|
-
await this.agentInstance.prompt(
|
|
997
|
-
"Summarize this conversation in max 5 words for a topic title. Reply ONLY with the title, nothing else."
|
|
998
|
-
);
|
|
999
|
-
this.name = title.trim().slice(0, 50) || `Session ${this.id.slice(0, 6)}`;
|
|
1000
|
-
this.log.info({ name: this.name }, "Session auto-named");
|
|
1001
|
-
this.emit("named", this.name);
|
|
1002
|
-
} catch {
|
|
1003
|
-
this.name = `Session ${this.id.slice(0, 6)}`;
|
|
1004
|
-
} finally {
|
|
1005
|
-
this.agentInstance.off("agent_event", captureHandler);
|
|
1006
|
-
this.clearBuffer();
|
|
1007
|
-
this.resume();
|
|
1008
|
-
}
|
|
1009
|
-
}
|
|
1010
|
-
/** Fire-and-forget warm-up: primes model cache while user types their first message */
|
|
1011
|
-
async warmup() {
|
|
1012
|
-
await this.queue.enqueue("\0__warmup__");
|
|
1013
|
-
}
|
|
1014
|
-
async runWarmup() {
|
|
1015
|
-
this.pause((_event, args) => {
|
|
1016
|
-
const agentEvent = args[0];
|
|
1017
|
-
return agentEvent?.type === "commands_update";
|
|
1018
|
-
});
|
|
1019
|
-
try {
|
|
1020
|
-
const start = Date.now();
|
|
1021
|
-
await this.agentInstance.prompt('Reply with only "ready".');
|
|
1022
|
-
this.activate();
|
|
1023
|
-
this.log.info({ durationMs: Date.now() - start }, "Warm-up complete");
|
|
1024
|
-
} catch (err) {
|
|
1025
|
-
this.log.error({ err }, "Warm-up failed");
|
|
1026
|
-
} finally {
|
|
1027
|
-
this.clearBuffer();
|
|
1028
|
-
this.resume();
|
|
1029
|
-
}
|
|
1030
|
-
}
|
|
1031
|
-
/** Cancel the current prompt and clear the queue. Stays in active state. */
|
|
1032
|
-
async abortPrompt() {
|
|
1033
|
-
this.queue.clear();
|
|
1034
|
-
this.log.info("Prompt aborted");
|
|
1035
|
-
await this.agentInstance.cancel();
|
|
1036
|
-
}
|
|
1037
|
-
async destroy() {
|
|
1038
|
-
this.log.info("Session destroyed");
|
|
1039
|
-
await this.agentInstance.destroy();
|
|
1040
|
-
}
|
|
1041
|
-
};
|
|
1042
|
-
|
|
1043
|
-
// src/core/session-manager.ts
|
|
1044
|
-
var SessionManager = class {
|
|
1045
|
-
sessions = /* @__PURE__ */ new Map();
|
|
1046
|
-
store;
|
|
1047
|
-
eventBus;
|
|
1048
|
-
setEventBus(eventBus) {
|
|
1049
|
-
this.eventBus = eventBus;
|
|
1050
|
-
}
|
|
1051
|
-
constructor(store = null) {
|
|
1052
|
-
this.store = store;
|
|
1053
|
-
}
|
|
1054
|
-
async createSession(channelId, agentName, workingDirectory, agentManager) {
|
|
1055
|
-
const agentInstance = await agentManager.spawn(agentName, workingDirectory);
|
|
1056
|
-
const session = new Session({
|
|
1057
|
-
channelId,
|
|
1058
|
-
agentName,
|
|
1059
|
-
workingDirectory,
|
|
1060
|
-
agentInstance
|
|
1061
|
-
});
|
|
1062
|
-
this.sessions.set(session.id, session);
|
|
1063
|
-
session.agentSessionId = session.agentInstance.sessionId;
|
|
1064
|
-
if (this.store) {
|
|
1065
|
-
await this.store.save({
|
|
1066
|
-
sessionId: session.id,
|
|
1067
|
-
agentSessionId: session.agentInstance.sessionId,
|
|
1068
|
-
agentName: session.agentName,
|
|
1069
|
-
workingDir: session.workingDirectory,
|
|
1070
|
-
channelId,
|
|
1071
|
-
status: session.status,
|
|
1072
|
-
createdAt: session.createdAt.toISOString(),
|
|
1073
|
-
lastActiveAt: (/* @__PURE__ */ new Date()).toISOString(),
|
|
1074
|
-
name: session.name,
|
|
1075
|
-
dangerousMode: false,
|
|
1076
|
-
platform: {}
|
|
1077
|
-
});
|
|
1078
|
-
}
|
|
1079
|
-
return session;
|
|
1080
|
-
}
|
|
1081
|
-
getSession(sessionId) {
|
|
1082
|
-
return this.sessions.get(sessionId);
|
|
1083
|
-
}
|
|
1084
|
-
getSessionByThread(channelId, threadId) {
|
|
1085
|
-
for (const session of this.sessions.values()) {
|
|
1086
|
-
if (session.channelId === channelId && session.threadId === threadId) {
|
|
1087
|
-
return session;
|
|
1088
|
-
}
|
|
1089
|
-
}
|
|
1090
|
-
return void 0;
|
|
1091
|
-
}
|
|
1092
|
-
getSessionByAgentSessionId(agentSessionId) {
|
|
1093
|
-
for (const session of this.sessions.values()) {
|
|
1094
|
-
if (session.agentSessionId === agentSessionId) {
|
|
1095
|
-
return session;
|
|
1096
|
-
}
|
|
1097
|
-
}
|
|
1098
|
-
return void 0;
|
|
1099
|
-
}
|
|
1100
|
-
getRecordByAgentSessionId(agentSessionId) {
|
|
1101
|
-
return this.store?.findByAgentSessionId(agentSessionId);
|
|
1102
|
-
}
|
|
1103
|
-
getRecordByThread(channelId, threadId) {
|
|
1104
|
-
return this.store?.findByPlatform(
|
|
1105
|
-
channelId,
|
|
1106
|
-
(p) => String(p.topicId) === threadId || p.threadId === threadId
|
|
1107
|
-
);
|
|
1108
|
-
}
|
|
1109
|
-
registerSession(session) {
|
|
1110
|
-
this.sessions.set(session.id, session);
|
|
1111
|
-
}
|
|
1112
|
-
async patchRecord(sessionId, patch) {
|
|
1113
|
-
if (!this.store) return;
|
|
1114
|
-
const record = this.store.get(sessionId);
|
|
1115
|
-
if (record) {
|
|
1116
|
-
await this.store.save({ ...record, ...patch });
|
|
1117
|
-
} else if (patch.sessionId) {
|
|
1118
|
-
await this.store.save(patch);
|
|
1119
|
-
}
|
|
1120
|
-
}
|
|
1121
|
-
getSessionRecord(sessionId) {
|
|
1122
|
-
return this.store?.get(sessionId);
|
|
1123
|
-
}
|
|
1124
|
-
async cancelSession(sessionId) {
|
|
1125
|
-
const session = this.sessions.get(sessionId);
|
|
1126
|
-
if (session) {
|
|
1127
|
-
await session.abortPrompt();
|
|
1128
|
-
session.markCancelled();
|
|
1129
|
-
}
|
|
1130
|
-
if (this.store) {
|
|
1131
|
-
const record = this.store.get(sessionId);
|
|
1132
|
-
if (record && record.status !== "cancelled") {
|
|
1133
|
-
await this.store.save({ ...record, status: "cancelled" });
|
|
1134
|
-
}
|
|
1135
|
-
}
|
|
1136
|
-
}
|
|
1137
|
-
listSessions(channelId) {
|
|
1138
|
-
const all = Array.from(this.sessions.values());
|
|
1139
|
-
if (channelId) return all.filter((s) => s.channelId === channelId);
|
|
1140
|
-
return all;
|
|
1141
|
-
}
|
|
1142
|
-
listRecords(filter) {
|
|
1143
|
-
if (!this.store) return [];
|
|
1144
|
-
let records = this.store.list();
|
|
1145
|
-
if (filter?.statuses?.length) {
|
|
1146
|
-
records = records.filter((r) => filter.statuses.includes(r.status));
|
|
1147
|
-
}
|
|
1148
|
-
return records;
|
|
1149
|
-
}
|
|
1150
|
-
async removeRecord(sessionId) {
|
|
1151
|
-
if (!this.store) return;
|
|
1152
|
-
await this.store.remove(sessionId);
|
|
1153
|
-
this.eventBus?.emit("session:deleted", { sessionId });
|
|
1154
|
-
}
|
|
1155
|
-
async destroyAll() {
|
|
1156
|
-
if (this.store) {
|
|
1157
|
-
for (const session of this.sessions.values()) {
|
|
1158
|
-
const record = this.store.get(session.id);
|
|
1159
|
-
if (record) {
|
|
1160
|
-
await this.store.save({ ...record, status: "finished" });
|
|
1161
|
-
}
|
|
1162
|
-
}
|
|
1163
|
-
}
|
|
1164
|
-
for (const session of this.sessions.values()) {
|
|
1165
|
-
await session.destroy();
|
|
1166
|
-
}
|
|
1167
|
-
this.sessions.clear();
|
|
1168
|
-
}
|
|
1169
|
-
};
|
|
1170
|
-
|
|
1171
|
-
// src/core/file-service.ts
|
|
1172
|
-
import fs3 from "fs";
|
|
1173
|
-
import path2 from "path";
|
|
1174
|
-
import { OggOpusDecoder } from "ogg-opus-decoder";
|
|
1175
|
-
import wav from "node-wav";
|
|
1176
|
-
var MIME_TO_EXT = {
|
|
1177
|
-
"image/jpeg": ".jpg",
|
|
1178
|
-
"image/png": ".png",
|
|
1179
|
-
"image/gif": ".gif",
|
|
1180
|
-
"image/webp": ".webp",
|
|
1181
|
-
"image/svg+xml": ".svg",
|
|
1182
|
-
"audio/ogg": ".ogg",
|
|
1183
|
-
"audio/mpeg": ".mp3",
|
|
1184
|
-
"audio/wav": ".wav",
|
|
1185
|
-
"audio/webm": ".webm",
|
|
1186
|
-
"audio/mp4": ".m4a",
|
|
1187
|
-
"video/mp4": ".mp4",
|
|
1188
|
-
"video/webm": ".webm",
|
|
1189
|
-
"application/pdf": ".pdf",
|
|
1190
|
-
"text/plain": ".txt"
|
|
1191
|
-
};
|
|
1192
|
-
var EXT_TO_MIME = {
|
|
1193
|
-
".jpg": "image/jpeg",
|
|
1194
|
-
".jpeg": "image/jpeg",
|
|
1195
|
-
".png": "image/png",
|
|
1196
|
-
".gif": "image/gif",
|
|
1197
|
-
".webp": "image/webp",
|
|
1198
|
-
".svg": "image/svg+xml",
|
|
1199
|
-
".ogg": "audio/ogg",
|
|
1200
|
-
".oga": "audio/ogg",
|
|
1201
|
-
".mp3": "audio/mpeg",
|
|
1202
|
-
".wav": "audio/wav",
|
|
1203
|
-
".m4a": "audio/mp4",
|
|
1204
|
-
".mp4": "video/mp4",
|
|
1205
|
-
".pdf": "application/pdf",
|
|
1206
|
-
".txt": "text/plain"
|
|
1207
|
-
};
|
|
1208
|
-
function classifyMime(mimeType) {
|
|
1209
|
-
if (mimeType.startsWith("image/")) return "image";
|
|
1210
|
-
if (mimeType.startsWith("audio/")) return "audio";
|
|
1211
|
-
return "file";
|
|
1212
|
-
}
|
|
1213
|
-
var FileService = class {
|
|
1214
|
-
constructor(baseDir) {
|
|
1215
|
-
this.baseDir = baseDir;
|
|
1216
|
-
}
|
|
1217
|
-
async saveFile(sessionId, fileName, data, mimeType) {
|
|
1218
|
-
const sessionDir = path2.join(this.baseDir, sessionId);
|
|
1219
|
-
await fs3.promises.mkdir(sessionDir, { recursive: true });
|
|
1220
|
-
const safeName = `${Date.now()}-${fileName.replace(/[^a-zA-Z0-9._-]/g, "_")}`;
|
|
1221
|
-
const filePath = path2.join(sessionDir, safeName);
|
|
1222
|
-
await fs3.promises.writeFile(filePath, data);
|
|
1223
|
-
return {
|
|
1224
|
-
type: classifyMime(mimeType),
|
|
1225
|
-
filePath,
|
|
1226
|
-
fileName,
|
|
1227
|
-
mimeType,
|
|
1228
|
-
size: data.length
|
|
1229
|
-
};
|
|
1230
|
-
}
|
|
1231
|
-
async resolveFile(filePath) {
|
|
1232
|
-
try {
|
|
1233
|
-
const stat = await fs3.promises.stat(filePath);
|
|
1234
|
-
if (!stat.isFile()) return null;
|
|
1235
|
-
const ext = path2.extname(filePath).toLowerCase();
|
|
1236
|
-
const mimeType = EXT_TO_MIME[ext] || "application/octet-stream";
|
|
1237
|
-
return {
|
|
1238
|
-
type: classifyMime(mimeType),
|
|
1239
|
-
filePath,
|
|
1240
|
-
fileName: path2.basename(filePath),
|
|
1241
|
-
mimeType,
|
|
1242
|
-
size: stat.size
|
|
1243
|
-
};
|
|
1244
|
-
} catch {
|
|
1245
|
-
return null;
|
|
1246
|
-
}
|
|
1247
|
-
}
|
|
1248
|
-
/**
|
|
1249
|
-
* Convert OGG Opus audio to WAV format.
|
|
1250
|
-
* Telegram voice messages use OGG Opus which many AI agents can't read.
|
|
1251
|
-
*/
|
|
1252
|
-
async convertOggToWav(oggData) {
|
|
1253
|
-
const decoder = new OggOpusDecoder();
|
|
1254
|
-
await decoder.ready;
|
|
1255
|
-
try {
|
|
1256
|
-
const { channelData, sampleRate } = await decoder.decode(new Uint8Array(oggData));
|
|
1257
|
-
const wavData = wav.encode(channelData, { sampleRate, float: true, bitDepth: 32 });
|
|
1258
|
-
return Buffer.from(wavData);
|
|
1259
|
-
} finally {
|
|
1260
|
-
decoder.free();
|
|
1261
|
-
}
|
|
1262
|
-
}
|
|
1263
|
-
static extensionFromMime(mimeType) {
|
|
1264
|
-
return MIME_TO_EXT[mimeType] || ".bin";
|
|
1265
|
-
}
|
|
1266
|
-
};
|
|
1267
|
-
|
|
1268
|
-
// src/core/session-bridge.ts
|
|
1269
|
-
var log2 = createChildLogger({ module: "session-bridge" });
|
|
1270
|
-
var SessionBridge = class {
|
|
1271
|
-
constructor(session, adapter, deps) {
|
|
1272
|
-
this.session = session;
|
|
1273
|
-
this.adapter = adapter;
|
|
1274
|
-
this.deps = deps;
|
|
1275
|
-
}
|
|
1276
|
-
connected = false;
|
|
1277
|
-
agentEventHandler;
|
|
1278
|
-
sessionEventHandler;
|
|
1279
|
-
statusChangeHandler;
|
|
1280
|
-
namedHandler;
|
|
1281
|
-
connect() {
|
|
1282
|
-
if (this.connected) return;
|
|
1283
|
-
this.connected = true;
|
|
1284
|
-
this.wireAgentToSession();
|
|
1285
|
-
this.wireSessionToAdapter();
|
|
1286
|
-
this.wirePermissions();
|
|
1287
|
-
this.wireLifecycle();
|
|
1288
|
-
}
|
|
1289
|
-
disconnect() {
|
|
1290
|
-
if (!this.connected) return;
|
|
1291
|
-
this.connected = false;
|
|
1292
|
-
if (this.agentEventHandler) {
|
|
1293
|
-
this.session.agentInstance.off("agent_event", this.agentEventHandler);
|
|
1294
|
-
}
|
|
1295
|
-
if (this.sessionEventHandler) {
|
|
1296
|
-
this.session.off("agent_event", this.sessionEventHandler);
|
|
1297
|
-
}
|
|
1298
|
-
if (this.statusChangeHandler) {
|
|
1299
|
-
this.session.off("status_change", this.statusChangeHandler);
|
|
1300
|
-
}
|
|
1301
|
-
if (this.namedHandler) {
|
|
1302
|
-
this.session.off("named", this.namedHandler);
|
|
1303
|
-
}
|
|
1304
|
-
this.session.agentInstance.onPermissionRequest = async () => "";
|
|
1305
|
-
}
|
|
1306
|
-
wireAgentToSession() {
|
|
1307
|
-
this.agentEventHandler = (event) => {
|
|
1308
|
-
this.session.emit("agent_event", event);
|
|
1309
|
-
};
|
|
1310
|
-
this.session.agentInstance.on("agent_event", this.agentEventHandler);
|
|
1311
|
-
}
|
|
1312
|
-
wireSessionToAdapter() {
|
|
1313
|
-
const session = this.session;
|
|
1314
|
-
const ctx = {
|
|
1315
|
-
get id() {
|
|
1316
|
-
return session.id;
|
|
1317
|
-
},
|
|
1318
|
-
get workingDirectory() {
|
|
1319
|
-
return session.workingDirectory;
|
|
1320
|
-
}
|
|
1321
|
-
};
|
|
1322
|
-
this.sessionEventHandler = (event) => {
|
|
1323
|
-
switch (event.type) {
|
|
1324
|
-
case "text":
|
|
1325
|
-
case "thought":
|
|
1326
|
-
case "tool_call":
|
|
1327
|
-
case "tool_update":
|
|
1328
|
-
case "plan":
|
|
1329
|
-
case "usage":
|
|
1330
|
-
this.adapter.sendMessage(
|
|
1331
|
-
this.session.id,
|
|
1332
|
-
this.deps.messageTransformer.transform(event, ctx)
|
|
1333
|
-
);
|
|
1334
|
-
break;
|
|
1335
|
-
case "session_end":
|
|
1336
|
-
this.session.finish(event.reason);
|
|
1337
|
-
this.adapter.cleanupSkillCommands(this.session.id);
|
|
1338
|
-
this.adapter.sendMessage(
|
|
1339
|
-
this.session.id,
|
|
1340
|
-
this.deps.messageTransformer.transform(event)
|
|
1341
|
-
);
|
|
1342
|
-
this.deps.notificationManager.notify(this.session.channelId, {
|
|
1343
|
-
sessionId: this.session.id,
|
|
1344
|
-
sessionName: this.session.name,
|
|
1345
|
-
type: "completed",
|
|
1346
|
-
summary: `Session "${this.session.name || this.session.id}" completed`
|
|
1347
|
-
});
|
|
1348
|
-
break;
|
|
1349
|
-
case "error":
|
|
1350
|
-
this.session.fail(event.message);
|
|
1351
|
-
this.adapter.cleanupSkillCommands(this.session.id);
|
|
1352
|
-
this.adapter.sendMessage(
|
|
1353
|
-
this.session.id,
|
|
1354
|
-
this.deps.messageTransformer.transform(event)
|
|
1355
|
-
);
|
|
1356
|
-
this.deps.notificationManager.notify(this.session.channelId, {
|
|
1357
|
-
sessionId: this.session.id,
|
|
1358
|
-
sessionName: this.session.name,
|
|
1359
|
-
type: "error",
|
|
1360
|
-
summary: event.message
|
|
1361
|
-
});
|
|
1362
|
-
break;
|
|
1363
|
-
case "image_content": {
|
|
1364
|
-
if (this.deps.fileService) {
|
|
1365
|
-
const fs8 = this.deps.fileService;
|
|
1366
|
-
const sid = this.session.id;
|
|
1367
|
-
const { data, mimeType } = event;
|
|
1368
|
-
const buffer = Buffer.from(data, "base64");
|
|
1369
|
-
const ext = FileService.extensionFromMime(mimeType);
|
|
1370
|
-
fs8.saveFile(sid, `agent-image${ext}`, buffer, mimeType).then((att) => {
|
|
1371
|
-
this.adapter.sendMessage(sid, {
|
|
1372
|
-
type: "attachment",
|
|
1373
|
-
text: "",
|
|
1374
|
-
attachment: att
|
|
1375
|
-
});
|
|
1376
|
-
}).catch((err) => log2.error({ err }, "Failed to save agent image"));
|
|
1377
|
-
}
|
|
1378
|
-
break;
|
|
1379
|
-
}
|
|
1380
|
-
case "audio_content": {
|
|
1381
|
-
if (this.deps.fileService) {
|
|
1382
|
-
const fs8 = this.deps.fileService;
|
|
1383
|
-
const sid = this.session.id;
|
|
1384
|
-
const { data, mimeType } = event;
|
|
1385
|
-
const buffer = Buffer.from(data, "base64");
|
|
1386
|
-
const ext = FileService.extensionFromMime(mimeType);
|
|
1387
|
-
fs8.saveFile(sid, `agent-audio${ext}`, buffer, mimeType).then((att) => {
|
|
1388
|
-
this.adapter.sendMessage(sid, {
|
|
1389
|
-
type: "attachment",
|
|
1390
|
-
text: "",
|
|
1391
|
-
attachment: att
|
|
1392
|
-
});
|
|
1393
|
-
}).catch((err) => log2.error({ err }, "Failed to save agent audio"));
|
|
1394
|
-
}
|
|
1395
|
-
break;
|
|
1396
|
-
}
|
|
1397
|
-
case "commands_update":
|
|
1398
|
-
log2.debug({ commands: event.commands }, "Commands available");
|
|
1399
|
-
this.adapter.sendSkillCommands(this.session.id, event.commands);
|
|
1400
|
-
break;
|
|
1401
|
-
case "system_message":
|
|
1402
|
-
this.adapter.sendMessage(
|
|
1403
|
-
this.session.id,
|
|
1404
|
-
this.deps.messageTransformer.transform(event)
|
|
1405
|
-
);
|
|
1406
|
-
break;
|
|
1407
|
-
}
|
|
1408
|
-
this.deps.eventBus?.emit("agent:event", {
|
|
1409
|
-
sessionId: this.session.id,
|
|
1410
|
-
event
|
|
1411
|
-
});
|
|
1412
|
-
};
|
|
1413
|
-
this.session.on("agent_event", this.sessionEventHandler);
|
|
1414
|
-
}
|
|
1415
|
-
wirePermissions() {
|
|
1416
|
-
this.session.agentInstance.onPermissionRequest = async (request) => {
|
|
1417
|
-
this.session.emit("permission_request", request);
|
|
1418
|
-
this.deps.eventBus?.emit("permission:request", {
|
|
1419
|
-
sessionId: this.session.id,
|
|
1420
|
-
permission: request
|
|
1421
|
-
});
|
|
1422
|
-
if (request.description.toLowerCase().includes("openacp")) {
|
|
1423
|
-
const allowOption = request.options.find((o) => o.isAllow);
|
|
1424
|
-
if (allowOption) {
|
|
1425
|
-
log2.info(
|
|
1426
|
-
{ sessionId: this.session.id, requestId: request.id },
|
|
1427
|
-
"Auto-approving openacp command"
|
|
1428
|
-
);
|
|
1429
|
-
return allowOption.id;
|
|
1430
|
-
}
|
|
1431
|
-
}
|
|
1432
|
-
if (this.session.dangerousMode) {
|
|
1433
|
-
const allowOption = request.options.find((o) => o.isAllow);
|
|
1434
|
-
if (allowOption) {
|
|
1435
|
-
log2.info(
|
|
1436
|
-
{ sessionId: this.session.id, requestId: request.id, optionId: allowOption.id },
|
|
1437
|
-
"Dangerous mode: auto-approving permission"
|
|
1438
|
-
);
|
|
1439
|
-
return allowOption.id;
|
|
1440
|
-
}
|
|
1441
|
-
}
|
|
1442
|
-
const promise = this.session.permissionGate.setPending(request);
|
|
1443
|
-
await this.adapter.sendPermissionRequest(this.session.id, request);
|
|
1444
|
-
return promise;
|
|
1445
|
-
};
|
|
1446
|
-
}
|
|
1447
|
-
wireLifecycle() {
|
|
1448
|
-
this.statusChangeHandler = (from, to) => {
|
|
1449
|
-
this.deps.sessionManager.patchRecord(this.session.id, {
|
|
1450
|
-
status: to,
|
|
1451
|
-
lastActiveAt: (/* @__PURE__ */ new Date()).toISOString()
|
|
1452
|
-
});
|
|
1453
|
-
this.deps.eventBus?.emit("session:updated", {
|
|
1454
|
-
sessionId: this.session.id,
|
|
1455
|
-
status: to
|
|
1456
|
-
});
|
|
1457
|
-
if (to === "finished" || to === "cancelled") {
|
|
1458
|
-
queueMicrotask(() => this.disconnect());
|
|
1459
|
-
}
|
|
1460
|
-
};
|
|
1461
|
-
this.session.on("status_change", this.statusChangeHandler);
|
|
1462
|
-
this.namedHandler = (name) => {
|
|
1463
|
-
this.deps.sessionManager.patchRecord(this.session.id, { name });
|
|
1464
|
-
this.deps.eventBus?.emit("session:updated", {
|
|
1465
|
-
sessionId: this.session.id,
|
|
1466
|
-
name
|
|
1467
|
-
});
|
|
1468
|
-
this.adapter.renameSessionThread(this.session.id, name);
|
|
1469
|
-
};
|
|
1470
|
-
this.session.on("named", this.namedHandler);
|
|
1471
|
-
}
|
|
1472
|
-
};
|
|
1473
|
-
|
|
1474
|
-
// src/core/notification.ts
|
|
1475
|
-
var NotificationManager = class {
|
|
1476
|
-
constructor(adapters) {
|
|
1477
|
-
this.adapters = adapters;
|
|
1478
|
-
}
|
|
1479
|
-
async notify(channelId, notification) {
|
|
1480
|
-
const adapter = this.adapters.get(channelId);
|
|
1481
|
-
if (adapter) {
|
|
1482
|
-
await adapter.sendNotification(notification);
|
|
1483
|
-
}
|
|
1484
|
-
}
|
|
1485
|
-
async notifyAll(notification) {
|
|
1486
|
-
for (const adapter of this.adapters.values()) {
|
|
1487
|
-
await adapter.sendNotification(notification);
|
|
1488
|
-
}
|
|
1489
|
-
}
|
|
1490
|
-
};
|
|
1491
|
-
|
|
1492
|
-
// src/tunnel/extract-file-info.ts
|
|
1493
|
-
function extractFileInfo(name, kind, content, rawInput, meta) {
|
|
1494
|
-
if (kind && !["read", "edit", "write"].includes(kind)) return null;
|
|
1495
|
-
let info = null;
|
|
1496
|
-
if (meta) {
|
|
1497
|
-
const m = meta;
|
|
1498
|
-
const claudeCode = m?.claudeCode;
|
|
1499
|
-
const tr = claudeCode?.toolResponse;
|
|
1500
|
-
const file = tr?.file;
|
|
1501
|
-
if (typeof file?.filePath === "string" && typeof file?.content === "string") {
|
|
1502
|
-
info = { filePath: file.filePath, content: file.content };
|
|
1503
|
-
}
|
|
1504
|
-
if (!info && typeof tr?.filePath === "string" && typeof tr?.content === "string") {
|
|
1505
|
-
info = { filePath: tr.filePath, content: tr.content };
|
|
1506
|
-
}
|
|
1507
|
-
}
|
|
1508
|
-
if (!info && rawInput && typeof rawInput === "object") {
|
|
1509
|
-
const ri = rawInput;
|
|
1510
|
-
const filePath = ri?.file_path || ri?.filePath || ri?.path;
|
|
1511
|
-
if (typeof filePath === "string") {
|
|
1512
|
-
const parsed = content ? parseContent(content) : null;
|
|
1513
|
-
const riContent = typeof ri?.content === "string" ? ri.content : void 0;
|
|
1514
|
-
info = { filePath, content: parsed?.content || riContent, oldContent: parsed?.oldContent };
|
|
1515
|
-
}
|
|
1516
|
-
}
|
|
1517
|
-
if (!info && content) {
|
|
1518
|
-
info = parseContent(content);
|
|
1519
|
-
}
|
|
1520
|
-
if (!info) return null;
|
|
1521
|
-
if (!info.filePath) {
|
|
1522
|
-
const pathMatch = name.match(/(?:Read|Edit|Write|View)\s+(.+)/i);
|
|
1523
|
-
if (pathMatch) info.filePath = pathMatch[1].trim();
|
|
1524
|
-
}
|
|
1525
|
-
if (!info.filePath || !info.content) return null;
|
|
1526
|
-
return info;
|
|
1527
|
-
}
|
|
1528
|
-
function parseContent(content) {
|
|
1529
|
-
if (typeof content === "string") {
|
|
1530
|
-
return { content };
|
|
1531
|
-
}
|
|
1532
|
-
if (Array.isArray(content)) {
|
|
1533
|
-
for (const block of content) {
|
|
1534
|
-
const result = parseContent(block);
|
|
1535
|
-
if (result?.content || result?.filePath) return result;
|
|
1536
|
-
}
|
|
1537
|
-
return null;
|
|
1538
|
-
}
|
|
1539
|
-
if (typeof content === "object" && content !== null) {
|
|
1540
|
-
const c = content;
|
|
1541
|
-
if (c.type === "diff" && typeof c.path === "string") {
|
|
1542
|
-
const newText = c.newText;
|
|
1543
|
-
const oldText = c.oldText;
|
|
1544
|
-
if (newText) {
|
|
1545
|
-
return {
|
|
1546
|
-
filePath: c.path,
|
|
1547
|
-
content: newText,
|
|
1548
|
-
oldContent: oldText ?? void 0
|
|
1549
|
-
};
|
|
1550
|
-
}
|
|
1551
|
-
}
|
|
1552
|
-
if (c.type === "content" && c.content) {
|
|
1553
|
-
return parseContent(c.content);
|
|
1554
|
-
}
|
|
1555
|
-
if (c.type === "text" && typeof c.text === "string") {
|
|
1556
|
-
return { content: c.text, filePath: c.filePath };
|
|
1557
|
-
}
|
|
1558
|
-
if (typeof c.text === "string") {
|
|
1559
|
-
return { content: c.text, filePath: c.filePath };
|
|
1560
|
-
}
|
|
1561
|
-
if (typeof c.file_path === "string" || typeof c.filePath === "string" || typeof c.path === "string") {
|
|
1562
|
-
const filePath = c.file_path || c.filePath || c.path;
|
|
1563
|
-
const fileContent = c.content || c.text || c.output || c.newText;
|
|
1564
|
-
if (typeof fileContent === "string") {
|
|
1565
|
-
return {
|
|
1566
|
-
filePath,
|
|
1567
|
-
content: fileContent,
|
|
1568
|
-
oldContent: c.old_content || c.oldText
|
|
1569
|
-
};
|
|
1570
|
-
}
|
|
1571
|
-
}
|
|
1572
|
-
if (c.input) {
|
|
1573
|
-
const result = parseContent(c.input);
|
|
1574
|
-
if (result) return result;
|
|
1575
|
-
}
|
|
1576
|
-
if (c.output) {
|
|
1577
|
-
const result = parseContent(c.output);
|
|
1578
|
-
if (result) return result;
|
|
1579
|
-
}
|
|
1580
|
-
}
|
|
1581
|
-
return null;
|
|
1582
|
-
}
|
|
1583
|
-
|
|
1584
|
-
// src/core/message-transformer.ts
|
|
1585
|
-
var log3 = createChildLogger({ module: "message-transformer" });
|
|
1586
|
-
var MessageTransformer = class {
|
|
1587
|
-
constructor(tunnelService) {
|
|
1588
|
-
this.tunnelService = tunnelService;
|
|
1589
|
-
}
|
|
1590
|
-
transform(event, sessionContext) {
|
|
1591
|
-
switch (event.type) {
|
|
1592
|
-
case "text":
|
|
1593
|
-
return { type: "text", text: event.content };
|
|
1594
|
-
case "thought":
|
|
1595
|
-
return { type: "thought", text: event.content };
|
|
1596
|
-
case "tool_call": {
|
|
1597
|
-
const metadata = {
|
|
1598
|
-
id: event.id,
|
|
1599
|
-
name: event.name,
|
|
1600
|
-
kind: event.kind,
|
|
1601
|
-
status: event.status,
|
|
1602
|
-
content: event.content,
|
|
1603
|
-
locations: event.locations
|
|
1604
|
-
};
|
|
1605
|
-
this.enrichWithViewerLinks(event, metadata, sessionContext);
|
|
1606
|
-
return { type: "tool_call", text: event.name, metadata };
|
|
1607
|
-
}
|
|
1608
|
-
case "tool_update": {
|
|
1609
|
-
const metadata = {
|
|
1610
|
-
id: event.id,
|
|
1611
|
-
name: event.name,
|
|
1612
|
-
kind: event.kind,
|
|
1613
|
-
status: event.status,
|
|
1614
|
-
content: event.content
|
|
1615
|
-
};
|
|
1616
|
-
this.enrichWithViewerLinks(event, metadata, sessionContext);
|
|
1617
|
-
return { type: "tool_update", text: "", metadata };
|
|
1618
|
-
}
|
|
1619
|
-
case "plan":
|
|
1620
|
-
return {
|
|
1621
|
-
type: "plan",
|
|
1622
|
-
text: "",
|
|
1623
|
-
metadata: { entries: event.entries }
|
|
1624
|
-
};
|
|
1625
|
-
case "usage":
|
|
1626
|
-
return {
|
|
1627
|
-
type: "usage",
|
|
1628
|
-
text: "",
|
|
1629
|
-
metadata: {
|
|
1630
|
-
tokensUsed: event.tokensUsed,
|
|
1631
|
-
contextSize: event.contextSize,
|
|
1632
|
-
cost: event.cost
|
|
1633
|
-
}
|
|
1634
|
-
};
|
|
1635
|
-
case "session_end":
|
|
1636
|
-
return { type: "session_end", text: `Done (${event.reason})` };
|
|
1637
|
-
case "error":
|
|
1638
|
-
return { type: "error", text: event.message };
|
|
1639
|
-
case "system_message":
|
|
1640
|
-
return { type: "system_message", text: event.message };
|
|
1641
|
-
default:
|
|
1642
|
-
return { type: "text", text: "" };
|
|
1643
|
-
}
|
|
1644
|
-
}
|
|
1645
|
-
enrichWithViewerLinks(event, metadata, sessionContext) {
|
|
1646
|
-
if (!this.tunnelService || !sessionContext) return;
|
|
1647
|
-
const name = "name" in event ? event.name || "" : "";
|
|
1648
|
-
const kind = "kind" in event ? event.kind : void 0;
|
|
1649
|
-
log3.debug(
|
|
1650
|
-
{ name, kind, status: event.status, hasContent: !!event.content },
|
|
1651
|
-
"enrichWithViewerLinks: inspecting event"
|
|
1652
|
-
);
|
|
1653
|
-
const fileInfo = extractFileInfo(
|
|
1654
|
-
name,
|
|
1655
|
-
kind,
|
|
1656
|
-
event.content,
|
|
1657
|
-
event.rawInput,
|
|
1658
|
-
event.meta
|
|
1659
|
-
);
|
|
1660
|
-
if (!fileInfo) return;
|
|
1661
|
-
log3.info(
|
|
1662
|
-
{
|
|
1663
|
-
name,
|
|
1664
|
-
kind,
|
|
1665
|
-
filePath: fileInfo.filePath,
|
|
1666
|
-
hasOldContent: !!fileInfo.oldContent
|
|
1667
|
-
},
|
|
1668
|
-
"enrichWithViewerLinks: extracted file info"
|
|
1669
|
-
);
|
|
1670
|
-
const store = this.tunnelService.getStore();
|
|
1671
|
-
const viewerLinks = {};
|
|
1672
|
-
if (fileInfo.oldContent) {
|
|
1673
|
-
const id2 = store.storeDiff(
|
|
1674
|
-
sessionContext.id,
|
|
1675
|
-
fileInfo.filePath,
|
|
1676
|
-
fileInfo.oldContent,
|
|
1677
|
-
fileInfo.content,
|
|
1678
|
-
sessionContext.workingDirectory
|
|
1679
|
-
);
|
|
1680
|
-
if (id2) viewerLinks.diff = this.tunnelService.diffUrl(id2);
|
|
1681
|
-
}
|
|
1682
|
-
const id = store.storeFile(
|
|
1683
|
-
sessionContext.id,
|
|
1684
|
-
fileInfo.filePath,
|
|
1685
|
-
fileInfo.content,
|
|
1686
|
-
sessionContext.workingDirectory
|
|
1687
|
-
);
|
|
1688
|
-
if (id) viewerLinks.file = this.tunnelService.fileUrl(id);
|
|
1689
|
-
if (Object.keys(viewerLinks).length > 0) {
|
|
1690
|
-
metadata.viewerLinks = viewerLinks;
|
|
1691
|
-
metadata.viewerFilePath = fileInfo.filePath;
|
|
1692
|
-
}
|
|
1693
|
-
}
|
|
1694
|
-
};
|
|
1695
|
-
|
|
1696
|
-
// src/core/usage-store.ts
|
|
1697
|
-
import fs4 from "fs";
|
|
1698
|
-
import path3 from "path";
|
|
1699
|
-
var log4 = createChildLogger({ module: "usage-store" });
|
|
1700
|
-
var DEBOUNCE_MS = 2e3;
|
|
1701
|
-
var UsageStore = class {
|
|
1702
|
-
constructor(filePath, retentionDays) {
|
|
1703
|
-
this.filePath = filePath;
|
|
1704
|
-
this.retentionDays = retentionDays;
|
|
1705
|
-
this.load();
|
|
1706
|
-
this.cleanup();
|
|
1707
|
-
this.cleanupInterval = setInterval(
|
|
1708
|
-
() => this.cleanup(),
|
|
1709
|
-
24 * 60 * 60 * 1e3
|
|
1710
|
-
);
|
|
1711
|
-
this.flushHandler = () => {
|
|
1712
|
-
try {
|
|
1713
|
-
this.flushSync();
|
|
1714
|
-
} catch {
|
|
1715
|
-
}
|
|
1716
|
-
};
|
|
1717
|
-
process.on("SIGTERM", this.flushHandler);
|
|
1718
|
-
process.on("SIGINT", this.flushHandler);
|
|
1719
|
-
process.on("exit", this.flushHandler);
|
|
1720
|
-
}
|
|
1721
|
-
records = [];
|
|
1722
|
-
debounceTimer = null;
|
|
1723
|
-
cleanupInterval = null;
|
|
1724
|
-
flushHandler = null;
|
|
1725
|
-
append(record) {
|
|
1726
|
-
this.records.push(record);
|
|
1727
|
-
this.scheduleDiskWrite();
|
|
1728
|
-
}
|
|
1729
|
-
query(period) {
|
|
1730
|
-
const cutoff = this.getCutoff(period);
|
|
1731
|
-
const filtered = cutoff ? this.records.filter((r) => new Date(r.timestamp).getTime() >= cutoff) : this.records;
|
|
1732
|
-
const totalTokens = filtered.reduce((sum, r) => sum + r.tokensUsed, 0);
|
|
1733
|
-
const totalCost = filtered.reduce(
|
|
1734
|
-
(sum, r) => sum + (r.cost?.amount ?? 0),
|
|
1735
|
-
0
|
|
1736
|
-
);
|
|
1737
|
-
const sessionIds = new Set(filtered.map((r) => r.sessionId));
|
|
1738
|
-
const currency = filtered.find((r) => r.cost?.currency)?.cost?.currency ?? "USD";
|
|
1739
|
-
return {
|
|
1740
|
-
period,
|
|
1741
|
-
totalTokens,
|
|
1742
|
-
totalCost,
|
|
1743
|
-
currency,
|
|
1744
|
-
sessionCount: sessionIds.size,
|
|
1745
|
-
recordCount: filtered.length
|
|
1746
|
-
};
|
|
1747
|
-
}
|
|
1748
|
-
getMonthlyTotal() {
|
|
1749
|
-
const now = /* @__PURE__ */ new Date();
|
|
1750
|
-
const startOfMonth = new Date(now.getFullYear(), now.getMonth(), 1);
|
|
1751
|
-
const cutoff = startOfMonth.getTime();
|
|
1752
|
-
const filtered = this.records.filter(
|
|
1753
|
-
(r) => new Date(r.timestamp).getTime() >= cutoff
|
|
1754
|
-
);
|
|
1755
|
-
const totalCost = filtered.reduce(
|
|
1756
|
-
(sum, r) => sum + (r.cost?.amount ?? 0),
|
|
1757
|
-
0
|
|
1758
|
-
);
|
|
1759
|
-
const currency = filtered.find((r) => r.cost?.currency)?.cost?.currency ?? "USD";
|
|
1760
|
-
return { totalCost, currency };
|
|
1761
|
-
}
|
|
1762
|
-
cleanup() {
|
|
1763
|
-
const cutoff = Date.now() - this.retentionDays * 24 * 60 * 60 * 1e3;
|
|
1764
|
-
const before = this.records.length;
|
|
1765
|
-
this.records = this.records.filter(
|
|
1766
|
-
(r) => new Date(r.timestamp).getTime() >= cutoff
|
|
1767
|
-
);
|
|
1768
|
-
const removed = before - this.records.length;
|
|
1769
|
-
if (removed > 0) {
|
|
1770
|
-
log4.info({ removed }, "Cleaned up expired usage records");
|
|
1771
|
-
this.scheduleDiskWrite();
|
|
1772
|
-
}
|
|
1773
|
-
}
|
|
1774
|
-
flushSync() {
|
|
1775
|
-
if (this.debounceTimer) {
|
|
1776
|
-
clearTimeout(this.debounceTimer);
|
|
1777
|
-
this.debounceTimer = null;
|
|
1778
|
-
}
|
|
1779
|
-
const data = { version: 1, records: this.records };
|
|
1780
|
-
const dir = path3.dirname(this.filePath);
|
|
1781
|
-
if (!fs4.existsSync(dir)) fs4.mkdirSync(dir, { recursive: true });
|
|
1782
|
-
fs4.writeFileSync(this.filePath, JSON.stringify(data, null, 2));
|
|
1783
|
-
}
|
|
1784
|
-
destroy() {
|
|
1785
|
-
if (this.debounceTimer) this.flushSync();
|
|
1786
|
-
if (this.cleanupInterval) clearInterval(this.cleanupInterval);
|
|
1787
|
-
if (this.flushHandler) {
|
|
1788
|
-
process.removeListener("SIGTERM", this.flushHandler);
|
|
1789
|
-
process.removeListener("SIGINT", this.flushHandler);
|
|
1790
|
-
process.removeListener("exit", this.flushHandler);
|
|
1791
|
-
this.flushHandler = null;
|
|
1792
|
-
}
|
|
1793
|
-
}
|
|
1794
|
-
load() {
|
|
1795
|
-
if (!fs4.existsSync(this.filePath)) return;
|
|
1796
|
-
try {
|
|
1797
|
-
const raw = JSON.parse(
|
|
1798
|
-
fs4.readFileSync(this.filePath, "utf-8")
|
|
1799
|
-
);
|
|
1800
|
-
if (raw.version !== 1) {
|
|
1801
|
-
log4.warn(
|
|
1802
|
-
{ version: raw.version },
|
|
1803
|
-
"Unknown usage store version, skipping load"
|
|
1804
|
-
);
|
|
1805
|
-
return;
|
|
1806
|
-
}
|
|
1807
|
-
this.records = raw.records || [];
|
|
1808
|
-
log4.debug({ count: this.records.length }, "Loaded usage records");
|
|
1809
|
-
} catch (err) {
|
|
1810
|
-
log4.error({ err }, "Failed to load usage store, backing up corrupt file");
|
|
1811
|
-
try {
|
|
1812
|
-
fs4.copyFileSync(this.filePath, this.filePath + ".bak");
|
|
1813
|
-
} catch {
|
|
1814
|
-
}
|
|
1815
|
-
this.records = [];
|
|
1816
|
-
}
|
|
1817
|
-
}
|
|
1818
|
-
getCutoff(period) {
|
|
1819
|
-
const now = /* @__PURE__ */ new Date();
|
|
1820
|
-
switch (period) {
|
|
1821
|
-
case "today": {
|
|
1822
|
-
const start = new Date(now);
|
|
1823
|
-
start.setHours(0, 0, 0, 0);
|
|
1824
|
-
return start.getTime();
|
|
1825
|
-
}
|
|
1826
|
-
case "week":
|
|
1827
|
-
return Date.now() - 7 * 24 * 60 * 60 * 1e3;
|
|
1828
|
-
case "month": {
|
|
1829
|
-
const startOfMonth = new Date(now.getFullYear(), now.getMonth(), 1);
|
|
1830
|
-
return startOfMonth.getTime();
|
|
1831
|
-
}
|
|
1832
|
-
case "all":
|
|
1833
|
-
return null;
|
|
1834
|
-
}
|
|
1835
|
-
}
|
|
1836
|
-
scheduleDiskWrite() {
|
|
1837
|
-
if (this.debounceTimer) clearTimeout(this.debounceTimer);
|
|
1838
|
-
this.debounceTimer = setTimeout(() => {
|
|
1839
|
-
this.flushSync();
|
|
1840
|
-
}, DEBOUNCE_MS);
|
|
1841
|
-
}
|
|
1842
|
-
};
|
|
1843
|
-
|
|
1844
|
-
// src/core/usage-budget.ts
|
|
1845
|
-
var UsageBudget = class {
|
|
1846
|
-
constructor(store, config, now = () => /* @__PURE__ */ new Date()) {
|
|
1847
|
-
this.store = store;
|
|
1848
|
-
this.config = config;
|
|
1849
|
-
this.now = now;
|
|
1850
|
-
this.lastNotifiedMonth = this.now().getMonth();
|
|
1851
|
-
}
|
|
1852
|
-
lastNotifiedStatus = "ok";
|
|
1853
|
-
lastNotifiedMonth;
|
|
1854
|
-
check() {
|
|
1855
|
-
if (!this.config.monthlyBudget) {
|
|
1856
|
-
return { status: "ok" };
|
|
1857
|
-
}
|
|
1858
|
-
const currentMonth = this.now().getMonth();
|
|
1859
|
-
if (currentMonth !== this.lastNotifiedMonth) {
|
|
1860
|
-
this.lastNotifiedStatus = "ok";
|
|
1861
|
-
this.lastNotifiedMonth = currentMonth;
|
|
1862
|
-
}
|
|
1863
|
-
const { totalCost } = this.store.getMonthlyTotal();
|
|
1864
|
-
const budget = this.config.monthlyBudget;
|
|
1865
|
-
const threshold = this.config.warningThreshold;
|
|
1866
|
-
let status;
|
|
1867
|
-
if (totalCost >= budget) {
|
|
1868
|
-
status = "exceeded";
|
|
1869
|
-
} else if (totalCost >= threshold * budget) {
|
|
1870
|
-
status = "warning";
|
|
1871
|
-
} else {
|
|
1872
|
-
status = "ok";
|
|
1873
|
-
}
|
|
1874
|
-
let message;
|
|
1875
|
-
if (status !== "ok" && status !== this.lastNotifiedStatus) {
|
|
1876
|
-
const pct = Math.round(totalCost / budget * 100);
|
|
1877
|
-
const filled = Math.round(Math.min(totalCost / budget, 1) * 10);
|
|
1878
|
-
const bar = "\u2593".repeat(filled) + "\u2591".repeat(10 - filled);
|
|
1879
|
-
if (status === "warning") {
|
|
1880
|
-
message = `\u26A0\uFE0F <b>Budget Warning</b>
|
|
1881
|
-
Monthly usage: $${totalCost.toFixed(2)} / $${budget.toFixed(2)} (${pct}%)
|
|
1882
|
-
${bar} ${pct}%`;
|
|
1883
|
-
} else {
|
|
1884
|
-
message = `\u{1F6A8} <b>Budget Exceeded</b>
|
|
1885
|
-
Monthly usage: $${totalCost.toFixed(2)} / $${budget.toFixed(2)} (${pct}%)
|
|
1886
|
-
${bar} ${pct}%
|
|
1887
|
-
Sessions are NOT blocked \u2014 this is a warning only.`;
|
|
1888
|
-
}
|
|
1889
|
-
}
|
|
1890
|
-
this.lastNotifiedStatus = status;
|
|
1891
|
-
return { status, message };
|
|
1892
|
-
}
|
|
1893
|
-
getStatus() {
|
|
1894
|
-
const { totalCost } = this.store.getMonthlyTotal();
|
|
1895
|
-
const budget = this.config.monthlyBudget ?? 0;
|
|
1896
|
-
let status = "ok";
|
|
1897
|
-
if (budget > 0) {
|
|
1898
|
-
if (totalCost >= budget) {
|
|
1899
|
-
status = "exceeded";
|
|
1900
|
-
} else if (totalCost >= this.config.warningThreshold * budget) {
|
|
1901
|
-
status = "warning";
|
|
1902
|
-
}
|
|
1903
|
-
}
|
|
1904
|
-
const percent = budget > 0 ? Math.round(totalCost / budget * 100) : 0;
|
|
1905
|
-
return { status, used: totalCost, budget, percent };
|
|
1906
|
-
}
|
|
1907
|
-
};
|
|
1908
|
-
|
|
1909
|
-
// src/core/security-guard.ts
|
|
1910
|
-
var SecurityGuard = class {
|
|
1911
|
-
constructor(configManager, sessionManager) {
|
|
1912
|
-
this.configManager = configManager;
|
|
1913
|
-
this.sessionManager = sessionManager;
|
|
1914
|
-
}
|
|
1915
|
-
checkAccess(message) {
|
|
1916
|
-
const config = this.configManager.get();
|
|
1917
|
-
if (config.security.allowedUserIds.length > 0) {
|
|
1918
|
-
const userId = String(message.userId);
|
|
1919
|
-
if (!config.security.allowedUserIds.includes(userId)) {
|
|
1920
|
-
return { allowed: false, reason: "Unauthorized user" };
|
|
1921
|
-
}
|
|
1922
|
-
}
|
|
1923
|
-
const active = this.sessionManager.listSessions().filter((s) => s.status === "active" || s.status === "initializing");
|
|
1924
|
-
if (active.length >= config.security.maxConcurrentSessions) {
|
|
1925
|
-
return { allowed: false, reason: `Session limit reached (${config.security.maxConcurrentSessions})` };
|
|
1926
|
-
}
|
|
1927
|
-
return { allowed: true };
|
|
1928
|
-
}
|
|
1929
|
-
};
|
|
1930
|
-
|
|
1931
|
-
// src/core/session-factory.ts
|
|
1932
|
-
import { nanoid as nanoid2 } from "nanoid";
|
|
1933
|
-
var log5 = createChildLogger({ module: "session-factory" });
|
|
1934
|
-
var SessionFactory = class {
|
|
1935
|
-
constructor(agentManager, sessionManager, speechService, eventBus) {
|
|
1936
|
-
this.agentManager = agentManager;
|
|
1937
|
-
this.sessionManager = sessionManager;
|
|
1938
|
-
this.speechService = speechService;
|
|
1939
|
-
this.eventBus = eventBus;
|
|
1940
|
-
}
|
|
1941
|
-
async create(params) {
|
|
1942
|
-
const agentInstance = params.resumeAgentSessionId ? await this.agentManager.resume(
|
|
1943
|
-
params.agentName,
|
|
1944
|
-
params.workingDirectory,
|
|
1945
|
-
params.resumeAgentSessionId
|
|
1946
|
-
) : await this.agentManager.spawn(
|
|
1947
|
-
params.agentName,
|
|
1948
|
-
params.workingDirectory
|
|
1949
|
-
);
|
|
1950
|
-
const session = new Session({
|
|
1951
|
-
id: params.existingSessionId,
|
|
1952
|
-
channelId: params.channelId,
|
|
1953
|
-
agentName: params.agentName,
|
|
1954
|
-
workingDirectory: params.workingDirectory,
|
|
1955
|
-
agentInstance,
|
|
1956
|
-
speechService: this.speechService
|
|
1957
|
-
});
|
|
1958
|
-
session.agentSessionId = agentInstance.sessionId;
|
|
1959
|
-
if (params.initialName) {
|
|
1960
|
-
session.name = params.initialName;
|
|
1961
|
-
}
|
|
1962
|
-
this.sessionManager.registerSession(session);
|
|
1963
|
-
this.eventBus.emit("session:created", {
|
|
1964
|
-
sessionId: session.id,
|
|
1965
|
-
agent: session.agentName,
|
|
1966
|
-
status: session.status
|
|
1967
|
-
});
|
|
1968
|
-
return session;
|
|
1969
|
-
}
|
|
1970
|
-
wireSideEffects(session, deps) {
|
|
1971
|
-
if (deps.usageStore) {
|
|
1972
|
-
const usageStore = deps.usageStore;
|
|
1973
|
-
const usageBudget = deps.usageBudget;
|
|
1974
|
-
const notificationManager = deps.notificationManager;
|
|
1975
|
-
session.on("agent_event", (event) => {
|
|
1976
|
-
if (event.type !== "usage") return;
|
|
1977
|
-
const record = {
|
|
1978
|
-
id: nanoid2(),
|
|
1979
|
-
sessionId: session.id,
|
|
1980
|
-
agentName: session.agentName,
|
|
1981
|
-
tokensUsed: event.tokensUsed ?? 0,
|
|
1982
|
-
contextSize: event.contextSize ?? 0,
|
|
1983
|
-
cost: event.cost,
|
|
1984
|
-
timestamp: (/* @__PURE__ */ new Date()).toISOString()
|
|
1985
|
-
};
|
|
1986
|
-
usageStore.append(record);
|
|
1987
|
-
if (usageBudget) {
|
|
1988
|
-
const result = usageBudget.check();
|
|
1989
|
-
if (result.message) {
|
|
1990
|
-
notificationManager.notifyAll({
|
|
1991
|
-
sessionId: session.id,
|
|
1992
|
-
sessionName: session.name,
|
|
1993
|
-
type: "budget_warning",
|
|
1994
|
-
summary: result.message
|
|
1995
|
-
});
|
|
1996
|
-
}
|
|
1997
|
-
}
|
|
1998
|
-
});
|
|
1999
|
-
}
|
|
2000
|
-
session.on("status_change", (_from, to) => {
|
|
2001
|
-
if ((to === "finished" || to === "cancelled") && deps.tunnelService) {
|
|
2002
|
-
deps.tunnelService.stopBySession(session.id).then((stopped) => {
|
|
2003
|
-
for (const entry of stopped) {
|
|
2004
|
-
deps.notificationManager.notifyAll({
|
|
2005
|
-
sessionId: session.id,
|
|
2006
|
-
sessionName: session.name,
|
|
2007
|
-
type: "completed",
|
|
2008
|
-
summary: `Tunnel stopped: port ${entry.port}${entry.label ? ` (${entry.label})` : ""} \u2014 session ended`
|
|
2009
|
-
}).catch(() => {
|
|
2010
|
-
});
|
|
2011
|
-
}
|
|
2012
|
-
}).catch(() => {
|
|
2013
|
-
});
|
|
2014
|
-
}
|
|
2015
|
-
});
|
|
2016
|
-
}
|
|
2017
|
-
};
|
|
2018
|
-
|
|
2019
|
-
// src/core/event-bus.ts
|
|
2020
|
-
var EventBus = class extends TypedEmitter {
|
|
2021
|
-
};
|
|
2022
|
-
|
|
2023
|
-
// src/core/speech/speech-service.ts
|
|
2024
|
-
var SpeechService = class {
|
|
2025
|
-
constructor(config) {
|
|
2026
|
-
this.config = config;
|
|
2027
|
-
}
|
|
2028
|
-
sttProviders = /* @__PURE__ */ new Map();
|
|
2029
|
-
ttsProviders = /* @__PURE__ */ new Map();
|
|
2030
|
-
registerSTTProvider(name, provider) {
|
|
2031
|
-
this.sttProviders.set(name, provider);
|
|
2032
|
-
}
|
|
2033
|
-
registerTTSProvider(name, provider) {
|
|
2034
|
-
this.ttsProviders.set(name, provider);
|
|
2035
|
-
}
|
|
2036
|
-
isSTTAvailable() {
|
|
2037
|
-
const { provider, providers } = this.config.stt;
|
|
2038
|
-
return provider !== null && providers[provider]?.apiKey !== void 0;
|
|
2039
|
-
}
|
|
2040
|
-
isTTSAvailable() {
|
|
2041
|
-
const provider = this.config.tts.provider;
|
|
2042
|
-
return provider !== null && this.ttsProviders.has(provider);
|
|
2043
|
-
}
|
|
2044
|
-
async transcribe(audioBuffer, mimeType, options) {
|
|
2045
|
-
const providerName = this.config.stt.provider;
|
|
2046
|
-
if (!providerName || !this.config.stt.providers[providerName]?.apiKey) {
|
|
2047
|
-
throw new Error("STT not configured. Set speech.stt.provider and API key in config.");
|
|
2048
|
-
}
|
|
2049
|
-
const provider = this.sttProviders.get(providerName);
|
|
2050
|
-
if (!provider) {
|
|
2051
|
-
throw new Error(`STT provider "${providerName}" not registered. Available: ${[...this.sttProviders.keys()].join(", ") || "none"}`);
|
|
2052
|
-
}
|
|
2053
|
-
return provider.transcribe(audioBuffer, mimeType, options);
|
|
2054
|
-
}
|
|
2055
|
-
async synthesize(text, options) {
|
|
2056
|
-
const providerName = this.config.tts.provider;
|
|
2057
|
-
if (!providerName) {
|
|
2058
|
-
throw new Error("TTS not configured. Set speech.tts.provider in config.");
|
|
2059
|
-
}
|
|
2060
|
-
const provider = this.ttsProviders.get(providerName);
|
|
2061
|
-
if (!provider) {
|
|
2062
|
-
throw new Error(`TTS provider "${providerName}" not registered. Available: ${[...this.ttsProviders.keys()].join(", ") || "none"}`);
|
|
2063
|
-
}
|
|
2064
|
-
return provider.synthesize(text, options);
|
|
2065
|
-
}
|
|
2066
|
-
updateConfig(config) {
|
|
2067
|
-
this.config = config;
|
|
2068
|
-
}
|
|
2069
|
-
};
|
|
2070
|
-
|
|
2071
|
-
// src/core/speech/providers/groq.ts
|
|
2072
|
-
var GROQ_API_URL = "https://api.groq.com/openai/v1/audio/transcriptions";
|
|
2073
|
-
var GroqSTT = class {
|
|
2074
|
-
constructor(apiKey, defaultModel = "whisper-large-v3-turbo") {
|
|
2075
|
-
this.apiKey = apiKey;
|
|
2076
|
-
this.defaultModel = defaultModel;
|
|
2077
|
-
}
|
|
2078
|
-
name = "groq";
|
|
2079
|
-
async transcribe(audioBuffer, mimeType, options) {
|
|
2080
|
-
const ext = mimeToExt(mimeType);
|
|
2081
|
-
const form = new FormData();
|
|
2082
|
-
form.append("file", new Blob([new Uint8Array(audioBuffer)], { type: mimeType }), `audio${ext}`);
|
|
2083
|
-
form.append("model", options?.model || this.defaultModel);
|
|
2084
|
-
form.append("response_format", "verbose_json");
|
|
2085
|
-
if (options?.language) {
|
|
2086
|
-
form.append("language", options.language);
|
|
2087
|
-
}
|
|
2088
|
-
const resp = await fetch(GROQ_API_URL, {
|
|
2089
|
-
method: "POST",
|
|
2090
|
-
headers: { Authorization: `Bearer ${this.apiKey}` },
|
|
2091
|
-
body: form
|
|
2092
|
-
});
|
|
2093
|
-
if (!resp.ok) {
|
|
2094
|
-
const body = await resp.text();
|
|
2095
|
-
if (resp.status === 401) {
|
|
2096
|
-
throw new Error("Invalid Groq API key. Check your key at console.groq.com.");
|
|
2097
|
-
}
|
|
2098
|
-
if (resp.status === 413) {
|
|
2099
|
-
throw new Error("Audio file too large for Groq API (max 25MB).");
|
|
2100
|
-
}
|
|
2101
|
-
if (resp.status === 429) {
|
|
2102
|
-
throw new Error("Groq rate limit exceeded. Free tier: 28,800 seconds/day. Try again later.");
|
|
2103
|
-
}
|
|
2104
|
-
throw new Error(`Groq STT error (${resp.status}): ${body}`);
|
|
2105
|
-
}
|
|
2106
|
-
const data = await resp.json();
|
|
2107
|
-
return {
|
|
2108
|
-
text: data.text,
|
|
2109
|
-
language: data.language,
|
|
2110
|
-
duration: data.duration
|
|
2111
|
-
};
|
|
2112
|
-
}
|
|
2113
|
-
};
|
|
2114
|
-
function mimeToExt(mimeType) {
|
|
2115
|
-
const map = {
|
|
2116
|
-
"audio/ogg": ".ogg",
|
|
2117
|
-
"audio/wav": ".wav",
|
|
2118
|
-
"audio/mpeg": ".mp3",
|
|
2119
|
-
"audio/mp4": ".m4a",
|
|
2120
|
-
"audio/webm": ".webm",
|
|
2121
|
-
"audio/flac": ".flac"
|
|
2122
|
-
};
|
|
2123
|
-
return map[mimeType] || ".bin";
|
|
2124
|
-
}
|
|
2125
|
-
|
|
2126
|
-
// src/core/speech/providers/edge-tts.ts
|
|
2127
|
-
var DEFAULT_VOICE = "en-US-AriaNeural";
|
|
2128
|
-
var EdgeTTS = class {
|
|
2129
|
-
name = "edge-tts";
|
|
2130
|
-
voice;
|
|
2131
|
-
constructor(voice) {
|
|
2132
|
-
this.voice = voice || DEFAULT_VOICE;
|
|
2133
|
-
}
|
|
2134
|
-
async synthesize(text, options) {
|
|
2135
|
-
const { MsEdgeTTS, OUTPUT_FORMAT } = await import("msedge-tts");
|
|
2136
|
-
const tts = new MsEdgeTTS();
|
|
2137
|
-
const voice = options?.voice || this.voice;
|
|
2138
|
-
const format = OUTPUT_FORMAT.AUDIO_24KHZ_48KBITRATE_MONO_MP3;
|
|
2139
|
-
await tts.setMetadata(voice, format);
|
|
2140
|
-
const { audioStream } = tts.toStream(text);
|
|
2141
|
-
const chunks = [];
|
|
2142
|
-
for await (const chunk of audioStream) {
|
|
2143
|
-
chunks.push(Buffer.isBuffer(chunk) ? chunk : Buffer.from(chunk));
|
|
2144
|
-
}
|
|
2145
|
-
tts.close();
|
|
2146
|
-
return {
|
|
2147
|
-
audioBuffer: Buffer.concat(chunks),
|
|
2148
|
-
mimeType: "audio/mpeg"
|
|
2149
|
-
};
|
|
2150
|
-
}
|
|
2151
|
-
};
|
|
2152
|
-
|
|
2153
|
-
// src/core/core.ts
|
|
2154
|
-
import path5 from "path";
|
|
2155
|
-
import os from "os";
|
|
2156
|
-
|
|
2157
|
-
// src/core/session-store.ts
|
|
2158
|
-
import fs5 from "fs";
|
|
2159
|
-
import path4 from "path";
|
|
2160
|
-
var log6 = createChildLogger({ module: "session-store" });
|
|
2161
|
-
var DEBOUNCE_MS2 = 2e3;
|
|
2162
|
-
var JsonFileSessionStore = class {
|
|
2163
|
-
records = /* @__PURE__ */ new Map();
|
|
2164
|
-
filePath;
|
|
2165
|
-
ttlDays;
|
|
2166
|
-
debounceTimer = null;
|
|
2167
|
-
cleanupInterval = null;
|
|
2168
|
-
flushHandler = null;
|
|
2169
|
-
constructor(filePath, ttlDays) {
|
|
2170
|
-
this.filePath = filePath;
|
|
2171
|
-
this.ttlDays = ttlDays;
|
|
2172
|
-
this.load();
|
|
2173
|
-
this.cleanup();
|
|
2174
|
-
this.cleanupInterval = setInterval(
|
|
2175
|
-
() => this.cleanup(),
|
|
2176
|
-
24 * 60 * 60 * 1e3
|
|
2177
|
-
);
|
|
2178
|
-
this.flushHandler = () => this.flushSync();
|
|
2179
|
-
process.on("SIGTERM", this.flushHandler);
|
|
2180
|
-
process.on("SIGINT", this.flushHandler);
|
|
2181
|
-
process.on("exit", this.flushHandler);
|
|
2182
|
-
}
|
|
2183
|
-
async save(record) {
|
|
2184
|
-
this.records.set(record.sessionId, { ...record });
|
|
2185
|
-
this.scheduleDiskWrite();
|
|
2186
|
-
}
|
|
2187
|
-
get(sessionId) {
|
|
2188
|
-
return this.records.get(sessionId);
|
|
2189
|
-
}
|
|
2190
|
-
findByPlatform(channelId, predicate) {
|
|
2191
|
-
for (const record of this.records.values()) {
|
|
2192
|
-
if (record.channelId === channelId && predicate(record.platform)) {
|
|
2193
|
-
return record;
|
|
2194
|
-
}
|
|
2195
|
-
}
|
|
2196
|
-
return void 0;
|
|
2197
|
-
}
|
|
2198
|
-
findByAgentSessionId(agentSessionId) {
|
|
2199
|
-
return [...this.records.values()].find(
|
|
2200
|
-
(r) => r.agentSessionId === agentSessionId || r.originalAgentSessionId === agentSessionId
|
|
2201
|
-
);
|
|
2202
|
-
}
|
|
2203
|
-
list(channelId) {
|
|
2204
|
-
const all = [...this.records.values()];
|
|
2205
|
-
if (channelId) return all.filter((r) => r.channelId === channelId);
|
|
2206
|
-
return all;
|
|
2207
|
-
}
|
|
2208
|
-
async remove(sessionId) {
|
|
2209
|
-
this.records.delete(sessionId);
|
|
2210
|
-
this.scheduleDiskWrite();
|
|
2211
|
-
}
|
|
2212
|
-
flushSync() {
|
|
2213
|
-
if (this.debounceTimer) {
|
|
2214
|
-
clearTimeout(this.debounceTimer);
|
|
2215
|
-
this.debounceTimer = null;
|
|
2216
|
-
}
|
|
2217
|
-
const data = {
|
|
2218
|
-
version: 1,
|
|
2219
|
-
sessions: Object.fromEntries(this.records)
|
|
2220
|
-
};
|
|
2221
|
-
const dir = path4.dirname(this.filePath);
|
|
2222
|
-
if (!fs5.existsSync(dir)) fs5.mkdirSync(dir, { recursive: true });
|
|
2223
|
-
fs5.writeFileSync(this.filePath, JSON.stringify(data, null, 2));
|
|
2224
|
-
}
|
|
2225
|
-
destroy() {
|
|
2226
|
-
if (this.debounceTimer) clearTimeout(this.debounceTimer);
|
|
2227
|
-
if (this.cleanupInterval) clearInterval(this.cleanupInterval);
|
|
2228
|
-
if (this.flushHandler) {
|
|
2229
|
-
process.removeListener("SIGTERM", this.flushHandler);
|
|
2230
|
-
process.removeListener("SIGINT", this.flushHandler);
|
|
2231
|
-
process.removeListener("exit", this.flushHandler);
|
|
2232
|
-
this.flushHandler = null;
|
|
2233
|
-
}
|
|
2234
|
-
}
|
|
2235
|
-
load() {
|
|
2236
|
-
if (!fs5.existsSync(this.filePath)) return;
|
|
2237
|
-
try {
|
|
2238
|
-
const raw = JSON.parse(
|
|
2239
|
-
fs5.readFileSync(this.filePath, "utf-8")
|
|
2240
|
-
);
|
|
2241
|
-
if (raw.version !== 1) {
|
|
2242
|
-
log6.warn(
|
|
2243
|
-
{ version: raw.version },
|
|
2244
|
-
"Unknown session store version, skipping load"
|
|
2245
|
-
);
|
|
2246
|
-
return;
|
|
2247
|
-
}
|
|
2248
|
-
for (const [id, record] of Object.entries(raw.sessions)) {
|
|
2249
|
-
this.records.set(id, record);
|
|
2250
|
-
}
|
|
2251
|
-
log6.debug({ count: this.records.size }, "Loaded session records");
|
|
2252
|
-
} catch (err) {
|
|
2253
|
-
log6.error({ err }, "Failed to load session store");
|
|
2254
|
-
}
|
|
2255
|
-
}
|
|
2256
|
-
cleanup() {
|
|
2257
|
-
const cutoff = Date.now() - this.ttlDays * 24 * 60 * 60 * 1e3;
|
|
2258
|
-
let removed = 0;
|
|
2259
|
-
for (const [id, record] of this.records) {
|
|
2260
|
-
if (record.status === "active" || record.status === "initializing")
|
|
2261
|
-
continue;
|
|
2262
|
-
const lastActive = new Date(record.lastActiveAt).getTime();
|
|
2263
|
-
if (lastActive < cutoff) {
|
|
2264
|
-
this.records.delete(id);
|
|
2265
|
-
removed++;
|
|
2266
|
-
}
|
|
2267
|
-
}
|
|
2268
|
-
if (removed > 0) {
|
|
2269
|
-
log6.info({ removed }, "Cleaned up expired session records");
|
|
2270
|
-
this.scheduleDiskWrite();
|
|
2271
|
-
}
|
|
2272
|
-
}
|
|
2273
|
-
scheduleDiskWrite() {
|
|
2274
|
-
if (this.debounceTimer) clearTimeout(this.debounceTimer);
|
|
2275
|
-
this.debounceTimer = setTimeout(() => {
|
|
2276
|
-
this.flushSync();
|
|
2277
|
-
}, DEBOUNCE_MS2);
|
|
2278
|
-
}
|
|
2279
|
-
};
|
|
2280
|
-
|
|
2281
|
-
// src/core/core.ts
|
|
2282
|
-
var log7 = createChildLogger({ module: "core" });
|
|
2283
|
-
var OpenACPCore = class {
|
|
2284
|
-
configManager;
|
|
2285
|
-
agentCatalog;
|
|
2286
|
-
agentManager;
|
|
2287
|
-
sessionManager;
|
|
2288
|
-
notificationManager;
|
|
2289
|
-
messageTransformer;
|
|
2290
|
-
fileService;
|
|
2291
|
-
speechService;
|
|
2292
|
-
securityGuard;
|
|
2293
|
-
adapters = /* @__PURE__ */ new Map();
|
|
2294
|
-
/** Set by main.ts — triggers graceful shutdown with restart exit code */
|
|
2295
|
-
requestRestart = null;
|
|
2296
|
-
_tunnelService;
|
|
2297
|
-
sessionStore = null;
|
|
2298
|
-
resumeLocks = /* @__PURE__ */ new Map();
|
|
2299
|
-
eventBus;
|
|
2300
|
-
sessionFactory;
|
|
2301
|
-
usageStore = null;
|
|
2302
|
-
usageBudget = null;
|
|
2303
|
-
constructor(configManager) {
|
|
2304
|
-
this.configManager = configManager;
|
|
2305
|
-
const config = configManager.get();
|
|
2306
|
-
this.agentCatalog = new AgentCatalog();
|
|
2307
|
-
this.agentCatalog.load();
|
|
2308
|
-
this.agentManager = new AgentManager(this.agentCatalog);
|
|
2309
|
-
const storePath = path5.join(os.homedir(), ".openacp", "sessions.json");
|
|
2310
|
-
this.sessionStore = new JsonFileSessionStore(
|
|
2311
|
-
storePath,
|
|
2312
|
-
config.sessionStore.ttlDays
|
|
2313
|
-
);
|
|
2314
|
-
this.sessionManager = new SessionManager(this.sessionStore);
|
|
2315
|
-
this.securityGuard = new SecurityGuard(configManager, this.sessionManager);
|
|
2316
|
-
this.notificationManager = new NotificationManager(this.adapters);
|
|
2317
|
-
const usageConfig = config.usage;
|
|
2318
|
-
if (usageConfig.enabled) {
|
|
2319
|
-
const usagePath = path5.join(os.homedir(), ".openacp", "usage.json");
|
|
2320
|
-
this.usageStore = new UsageStore(usagePath, usageConfig.retentionDays);
|
|
2321
|
-
this.usageBudget = new UsageBudget(this.usageStore, usageConfig);
|
|
2322
|
-
}
|
|
2323
|
-
this.messageTransformer = new MessageTransformer();
|
|
2324
|
-
this.eventBus = new EventBus();
|
|
2325
|
-
this.sessionManager.setEventBus(this.eventBus);
|
|
2326
|
-
this.fileService = new FileService(
|
|
2327
|
-
path5.join(os.homedir(), ".openacp", "files")
|
|
2328
|
-
);
|
|
2329
|
-
const speechConfig = config.speech ?? {
|
|
2330
|
-
stt: { provider: null, providers: {} },
|
|
2331
|
-
tts: { provider: "edge-tts", providers: {} }
|
|
2332
|
-
};
|
|
2333
|
-
if (speechConfig.tts.provider == null) {
|
|
2334
|
-
speechConfig.tts.provider = "edge-tts";
|
|
2335
|
-
}
|
|
2336
|
-
this.speechService = new SpeechService(speechConfig);
|
|
2337
|
-
const groqConfig = speechConfig.stt?.providers?.groq;
|
|
2338
|
-
if (groqConfig?.apiKey) {
|
|
2339
|
-
this.speechService.registerSTTProvider(
|
|
2340
|
-
"groq",
|
|
2341
|
-
new GroqSTT(groqConfig.apiKey, groqConfig.model)
|
|
2342
|
-
);
|
|
2343
|
-
}
|
|
2344
|
-
{
|
|
2345
|
-
const edgeConfig = speechConfig.tts?.providers?.["edge-tts"];
|
|
2346
|
-
const voice = edgeConfig?.voice;
|
|
2347
|
-
this.speechService.registerTTSProvider("edge-tts", new EdgeTTS(voice));
|
|
2348
|
-
}
|
|
2349
|
-
this.sessionFactory = new SessionFactory(
|
|
2350
|
-
this.agentManager,
|
|
2351
|
-
this.sessionManager,
|
|
2352
|
-
this.speechService,
|
|
2353
|
-
this.eventBus
|
|
2354
|
-
);
|
|
2355
|
-
this.configManager.on(
|
|
2356
|
-
"config:changed",
|
|
2357
|
-
async ({ path: configPath, value }) => {
|
|
2358
|
-
if (configPath === "logging.level" && typeof value === "string") {
|
|
2359
|
-
const { setLogLevel: setLogLevel2 } = await import("./log-RCVBXLTN.js");
|
|
2360
|
-
setLogLevel2(value);
|
|
2361
|
-
log7.info({ level: value }, "Log level changed at runtime");
|
|
2362
|
-
}
|
|
2363
|
-
if (configPath.startsWith("speech.")) {
|
|
2364
|
-
const newConfig = this.configManager.get();
|
|
2365
|
-
const newSpeechConfig = newConfig.speech ?? {
|
|
2366
|
-
stt: { provider: null, providers: {} },
|
|
2367
|
-
tts: { provider: null, providers: {} }
|
|
2368
|
-
};
|
|
2369
|
-
this.speechService.updateConfig(newSpeechConfig);
|
|
2370
|
-
const groqCfg = newSpeechConfig.stt?.providers?.groq;
|
|
2371
|
-
if (groqCfg?.apiKey) {
|
|
2372
|
-
this.speechService.registerSTTProvider(
|
|
2373
|
-
"groq",
|
|
2374
|
-
new GroqSTT(groqCfg.apiKey, groqCfg.model)
|
|
2375
|
-
);
|
|
2376
|
-
}
|
|
2377
|
-
{
|
|
2378
|
-
const edgeConfig = newSpeechConfig.tts?.providers?.["edge-tts"];
|
|
2379
|
-
const voice = edgeConfig?.voice;
|
|
2380
|
-
this.speechService.registerTTSProvider("edge-tts", new EdgeTTS(voice));
|
|
2381
|
-
}
|
|
2382
|
-
log7.info("Speech service config updated at runtime");
|
|
2383
|
-
}
|
|
2384
|
-
}
|
|
2385
|
-
);
|
|
2386
|
-
}
|
|
2387
|
-
get tunnelService() {
|
|
2388
|
-
return this._tunnelService;
|
|
2389
|
-
}
|
|
2390
|
-
set tunnelService(service) {
|
|
2391
|
-
this._tunnelService = service;
|
|
2392
|
-
this.messageTransformer = new MessageTransformer(service);
|
|
2393
|
-
}
|
|
2394
|
-
registerAdapter(name, adapter) {
|
|
2395
|
-
this.adapters.set(name, adapter);
|
|
2396
|
-
}
|
|
2397
|
-
async start() {
|
|
2398
|
-
this.agentCatalog.refreshRegistryIfStale().catch((err) => {
|
|
2399
|
-
log7.warn({ err }, "Background registry refresh failed");
|
|
2400
|
-
});
|
|
2401
|
-
for (const adapter of this.adapters.values()) {
|
|
2402
|
-
await adapter.start();
|
|
2403
|
-
}
|
|
2404
|
-
}
|
|
2405
|
-
async stop() {
|
|
2406
|
-
try {
|
|
2407
|
-
await this.notificationManager.notifyAll({
|
|
2408
|
-
sessionId: "system",
|
|
2409
|
-
type: "error",
|
|
2410
|
-
summary: "OpenACP is shutting down"
|
|
2411
|
-
});
|
|
2412
|
-
} catch {
|
|
2413
|
-
}
|
|
2414
|
-
await this.sessionManager.destroyAll();
|
|
2415
|
-
for (const adapter of this.adapters.values()) {
|
|
2416
|
-
await adapter.stop();
|
|
2417
|
-
}
|
|
2418
|
-
if (this.usageStore) {
|
|
2419
|
-
this.usageStore.destroy();
|
|
2420
|
-
}
|
|
2421
|
-
}
|
|
2422
|
-
// --- Archive ---
|
|
2423
|
-
async archiveSession(sessionId) {
|
|
2424
|
-
const session = this.sessionManager.getSession(sessionId);
|
|
2425
|
-
if (!session) return { ok: false, error: "Session not found" };
|
|
2426
|
-
if (session.status === "initializing")
|
|
2427
|
-
return { ok: false, error: "Session is still initializing" };
|
|
2428
|
-
if (session.status !== "active")
|
|
2429
|
-
return { ok: false, error: `Session is ${session.status}` };
|
|
2430
|
-
const adapter = this.adapters.get(session.channelId);
|
|
2431
|
-
if (!adapter) return { ok: false, error: "Adapter not found for session" };
|
|
2432
|
-
try {
|
|
2433
|
-
const result = await adapter.archiveSessionTopic(session.id);
|
|
2434
|
-
if (!result)
|
|
2435
|
-
return { ok: false, error: "Adapter does not support archiving" };
|
|
2436
|
-
return { ok: true, newThreadId: result.newThreadId };
|
|
2437
|
-
} catch (err) {
|
|
2438
|
-
return { ok: false, error: err.message };
|
|
2439
|
-
}
|
|
2440
|
-
}
|
|
2441
|
-
// --- Message Routing ---
|
|
2442
|
-
async handleMessage(message) {
|
|
2443
|
-
log7.debug(
|
|
2444
|
-
{
|
|
2445
|
-
channelId: message.channelId,
|
|
2446
|
-
threadId: message.threadId,
|
|
2447
|
-
userId: message.userId
|
|
2448
|
-
},
|
|
2449
|
-
"Incoming message"
|
|
2450
|
-
);
|
|
2451
|
-
const access = this.securityGuard.checkAccess(message);
|
|
2452
|
-
if (!access.allowed) {
|
|
2453
|
-
log7.warn({ userId: message.userId, reason: access.reason }, "Access denied");
|
|
2454
|
-
if (access.reason.includes("Session limit")) {
|
|
2455
|
-
const adapter = this.adapters.get(message.channelId);
|
|
2456
|
-
if (adapter) {
|
|
2457
|
-
await adapter.sendMessage(message.threadId, {
|
|
2458
|
-
type: "error",
|
|
2459
|
-
text: `\u26A0\uFE0F ${access.reason}. Please cancel existing sessions with /cancel before starting new ones.`
|
|
2460
|
-
});
|
|
2461
|
-
}
|
|
2462
|
-
}
|
|
2463
|
-
return;
|
|
2464
|
-
}
|
|
2465
|
-
let session = this.sessionManager.getSessionByThread(
|
|
2466
|
-
message.channelId,
|
|
2467
|
-
message.threadId
|
|
2468
|
-
);
|
|
2469
|
-
if (!session) {
|
|
2470
|
-
session = await this.lazyResume(message) ?? void 0;
|
|
2471
|
-
}
|
|
2472
|
-
if (!session) {
|
|
2473
|
-
log7.warn(
|
|
2474
|
-
{ channelId: message.channelId, threadId: message.threadId },
|
|
2475
|
-
"No session found for thread (in-memory miss + lazy resume returned null)"
|
|
2476
|
-
);
|
|
2477
|
-
return;
|
|
2478
|
-
}
|
|
2479
|
-
this.sessionManager.patchRecord(session.id, {
|
|
2480
|
-
lastActiveAt: (/* @__PURE__ */ new Date()).toISOString()
|
|
2481
|
-
});
|
|
2482
|
-
await session.enqueuePrompt(message.text, message.attachments);
|
|
2483
|
-
}
|
|
2484
|
-
// --- Unified Session Creation Pipeline ---
|
|
2485
|
-
async createSession(params) {
|
|
2486
|
-
const session = await this.sessionFactory.create(params);
|
|
2487
|
-
const adapter = this.adapters.get(params.channelId);
|
|
2488
|
-
if (params.createThread && adapter) {
|
|
2489
|
-
const threadId = await adapter.createSessionThread(
|
|
2490
|
-
session.id,
|
|
2491
|
-
params.initialName ?? `\u{1F504} ${params.agentName} \u2014 New Session`
|
|
2492
|
-
);
|
|
2493
|
-
session.threadId = threadId;
|
|
2494
|
-
}
|
|
2495
|
-
if (adapter) {
|
|
2496
|
-
const bridge = this.createBridge(session, adapter);
|
|
2497
|
-
bridge.connect();
|
|
2498
|
-
}
|
|
2499
|
-
this.sessionFactory.wireSideEffects(session, {
|
|
2500
|
-
usageStore: this.usageStore,
|
|
2501
|
-
usageBudget: this.usageBudget,
|
|
2502
|
-
notificationManager: this.notificationManager,
|
|
2503
|
-
tunnelService: this._tunnelService
|
|
2504
|
-
});
|
|
2505
|
-
const existingRecord = this.sessionStore?.get(session.id);
|
|
2506
|
-
const platform = {
|
|
2507
|
-
...existingRecord?.platform ?? {}
|
|
2508
|
-
};
|
|
2509
|
-
if (session.threadId) {
|
|
2510
|
-
if (params.channelId === "telegram") {
|
|
2511
|
-
platform.topicId = Number(session.threadId);
|
|
2512
|
-
} else {
|
|
2513
|
-
platform.threadId = session.threadId;
|
|
2514
|
-
}
|
|
2515
|
-
}
|
|
2516
|
-
await this.sessionManager.patchRecord(session.id, {
|
|
2517
|
-
sessionId: session.id,
|
|
2518
|
-
agentSessionId: session.agentSessionId,
|
|
2519
|
-
agentName: params.agentName,
|
|
2520
|
-
workingDir: params.workingDirectory,
|
|
2521
|
-
channelId: params.channelId,
|
|
2522
|
-
status: session.status,
|
|
2523
|
-
createdAt: session.createdAt.toISOString(),
|
|
2524
|
-
lastActiveAt: (/* @__PURE__ */ new Date()).toISOString(),
|
|
2525
|
-
name: session.name,
|
|
2526
|
-
platform
|
|
2527
|
-
});
|
|
2528
|
-
log7.info(
|
|
2529
|
-
{ sessionId: session.id, agentName: params.agentName },
|
|
2530
|
-
"Session created via pipeline"
|
|
2531
|
-
);
|
|
2532
|
-
return session;
|
|
2533
|
-
}
|
|
2534
|
-
async handleNewSession(channelId, agentName, workspacePath) {
|
|
2535
|
-
const config = this.configManager.get();
|
|
2536
|
-
const resolvedAgent = agentName || config.defaultAgent;
|
|
2537
|
-
log7.info({ channelId, agentName: resolvedAgent }, "New session request");
|
|
2538
|
-
const agentDef = this.agentCatalog.resolve(resolvedAgent);
|
|
2539
|
-
const resolvedWorkspace = this.configManager.resolveWorkspace(
|
|
2540
|
-
workspacePath || agentDef?.workingDirectory
|
|
2541
|
-
);
|
|
2542
|
-
return this.createSession({
|
|
2543
|
-
channelId,
|
|
2544
|
-
agentName: resolvedAgent,
|
|
2545
|
-
workingDirectory: resolvedWorkspace
|
|
2546
|
-
});
|
|
2547
|
-
}
|
|
2548
|
-
async adoptSession(agentName, agentSessionId, cwd, channelId) {
|
|
2549
|
-
const caps = getAgentCapabilities(agentName);
|
|
2550
|
-
if (!caps.supportsResume) {
|
|
2551
|
-
return {
|
|
2552
|
-
ok: false,
|
|
2553
|
-
error: "agent_not_supported",
|
|
2554
|
-
message: `Agent '${agentName}' does not support session resume`
|
|
2555
|
-
};
|
|
2556
|
-
}
|
|
2557
|
-
const agentDef = this.agentManager.getAgent(agentName);
|
|
2558
|
-
if (!agentDef) {
|
|
2559
|
-
return {
|
|
2560
|
-
ok: false,
|
|
2561
|
-
error: "agent_not_supported",
|
|
2562
|
-
message: `Agent '${agentName}' not found`
|
|
2563
|
-
};
|
|
2564
|
-
}
|
|
2565
|
-
const { existsSync: existsSync2 } = await import("fs");
|
|
2566
|
-
if (!existsSync2(cwd)) {
|
|
2567
|
-
return {
|
|
2568
|
-
ok: false,
|
|
2569
|
-
error: "invalid_cwd",
|
|
2570
|
-
message: `Directory does not exist: ${cwd}`
|
|
2571
|
-
};
|
|
2572
|
-
}
|
|
2573
|
-
const maxSessions = this.configManager.get().security.maxConcurrentSessions;
|
|
2574
|
-
if (this.sessionManager.listSessions().length >= maxSessions) {
|
|
2575
|
-
return {
|
|
2576
|
-
ok: false,
|
|
2577
|
-
error: "session_limit",
|
|
2578
|
-
message: "Maximum concurrent sessions reached"
|
|
2579
|
-
};
|
|
2580
|
-
}
|
|
2581
|
-
const existingRecord = this.sessionManager.getRecordByAgentSessionId(agentSessionId);
|
|
2582
|
-
if (existingRecord) {
|
|
2583
|
-
const sameChannel = !channelId || existingRecord.channelId === channelId;
|
|
2584
|
-
const platform = existingRecord.platform;
|
|
2585
|
-
const existingThreadId = platform?.topicId ? String(platform.topicId) : platform?.threadId;
|
|
2586
|
-
if (existingThreadId && sameChannel) {
|
|
2587
|
-
const adapter = this.adapters.get(existingRecord.channelId) ?? this.adapters.values().next().value;
|
|
2588
|
-
if (adapter) {
|
|
2589
|
-
try {
|
|
2590
|
-
await adapter.sendMessage(existingRecord.sessionId, {
|
|
2591
|
-
type: "text",
|
|
2592
|
-
text: "Session resumed from CLI."
|
|
2593
|
-
});
|
|
2594
|
-
} catch {
|
|
2595
|
-
}
|
|
2596
|
-
}
|
|
2597
|
-
return {
|
|
2598
|
-
ok: true,
|
|
2599
|
-
sessionId: existingRecord.sessionId,
|
|
2600
|
-
threadId: existingThreadId,
|
|
2601
|
-
status: "existing"
|
|
2602
|
-
};
|
|
2603
|
-
}
|
|
2604
|
-
}
|
|
2605
|
-
let adapterChannelId;
|
|
2606
|
-
if (channelId) {
|
|
2607
|
-
if (!this.adapters.has(channelId)) {
|
|
2608
|
-
const available = Array.from(this.adapters.keys()).join(", ") || "none";
|
|
2609
|
-
return { ok: false, error: "adapter_not_found", message: `Adapter '${channelId}' is not connected. Available: ${available}` };
|
|
2610
|
-
}
|
|
2611
|
-
adapterChannelId = channelId;
|
|
2612
|
-
} else {
|
|
2613
|
-
const firstEntry = this.adapters.entries().next().value;
|
|
2614
|
-
if (!firstEntry) {
|
|
2615
|
-
return { ok: false, error: "no_adapter", message: "No channel adapter registered" };
|
|
2616
|
-
}
|
|
2617
|
-
adapterChannelId = firstEntry[0];
|
|
2618
|
-
}
|
|
2619
|
-
let session;
|
|
2620
|
-
try {
|
|
2621
|
-
session = await this.createSession({
|
|
2622
|
-
channelId: adapterChannelId,
|
|
2623
|
-
agentName,
|
|
2624
|
-
workingDirectory: cwd,
|
|
2625
|
-
resumeAgentSessionId: agentSessionId,
|
|
2626
|
-
createThread: true,
|
|
2627
|
-
initialName: "Adopted session"
|
|
2628
|
-
});
|
|
2629
|
-
} catch (err) {
|
|
2630
|
-
return {
|
|
2631
|
-
ok: false,
|
|
2632
|
-
error: "resume_failed",
|
|
2633
|
-
message: `Failed to resume session: ${err instanceof Error ? err.message : String(err)}`
|
|
2634
|
-
};
|
|
2635
|
-
}
|
|
2636
|
-
const adoptPlatform = {};
|
|
2637
|
-
if (adapterChannelId === "telegram") {
|
|
2638
|
-
adoptPlatform.topicId = Number(session.threadId);
|
|
2639
|
-
} else {
|
|
2640
|
-
adoptPlatform.threadId = session.threadId;
|
|
2641
|
-
}
|
|
2642
|
-
await this.sessionManager.patchRecord(session.id, {
|
|
2643
|
-
originalAgentSessionId: agentSessionId,
|
|
2644
|
-
platform: adoptPlatform
|
|
2645
|
-
});
|
|
2646
|
-
return {
|
|
2647
|
-
ok: true,
|
|
2648
|
-
sessionId: session.id,
|
|
2649
|
-
threadId: session.threadId,
|
|
2650
|
-
status: "adopted"
|
|
2651
|
-
};
|
|
2652
|
-
}
|
|
2653
|
-
async handleNewChat(channelId, currentThreadId) {
|
|
2654
|
-
const currentSession = this.sessionManager.getSessionByThread(
|
|
2655
|
-
channelId,
|
|
2656
|
-
currentThreadId
|
|
2657
|
-
);
|
|
2658
|
-
if (currentSession) {
|
|
2659
|
-
return this.handleNewSession(
|
|
2660
|
-
channelId,
|
|
2661
|
-
currentSession.agentName,
|
|
2662
|
-
currentSession.workingDirectory
|
|
2663
|
-
);
|
|
2664
|
-
}
|
|
2665
|
-
const record = this.sessionManager.getRecordByThread(
|
|
2666
|
-
channelId,
|
|
2667
|
-
currentThreadId
|
|
2668
|
-
);
|
|
2669
|
-
if (!record || record.status === "cancelled" || record.status === "error")
|
|
2670
|
-
return null;
|
|
2671
|
-
return this.handleNewSession(
|
|
2672
|
-
channelId,
|
|
2673
|
-
record.agentName,
|
|
2674
|
-
record.workingDir
|
|
2675
|
-
);
|
|
2676
|
-
}
|
|
2677
|
-
// --- Lazy Resume ---
|
|
2678
|
-
/**
|
|
2679
|
-
* Get active session by thread, or attempt lazy resume from store.
|
|
2680
|
-
* Used by adapter command handlers that need a session but don't go through handleMessage().
|
|
2681
|
-
*/
|
|
2682
|
-
async getOrResumeSession(channelId, threadId) {
|
|
2683
|
-
const session = this.sessionManager.getSessionByThread(channelId, threadId);
|
|
2684
|
-
if (session) return session;
|
|
2685
|
-
return this.lazyResume({ channelId, threadId, userId: "", text: "" });
|
|
2686
|
-
}
|
|
2687
|
-
async lazyResume(message) {
|
|
2688
|
-
const store = this.sessionStore;
|
|
2689
|
-
if (!store) return null;
|
|
2690
|
-
const lockKey = `${message.channelId}:${message.threadId}`;
|
|
2691
|
-
const existing = this.resumeLocks.get(lockKey);
|
|
2692
|
-
if (existing) return existing;
|
|
2693
|
-
const record = store.findByPlatform(
|
|
2694
|
-
message.channelId,
|
|
2695
|
-
(p) => String(p.topicId) === message.threadId
|
|
2696
|
-
);
|
|
2697
|
-
if (!record) {
|
|
2698
|
-
log7.debug(
|
|
2699
|
-
{ threadId: message.threadId, channelId: message.channelId },
|
|
2700
|
-
"No session record found for thread"
|
|
2701
|
-
);
|
|
2702
|
-
return null;
|
|
2703
|
-
}
|
|
2704
|
-
if (record.status === "error") {
|
|
2705
|
-
log7.debug(
|
|
2706
|
-
{
|
|
2707
|
-
threadId: message.threadId,
|
|
2708
|
-
sessionId: record.sessionId,
|
|
2709
|
-
status: record.status
|
|
2710
|
-
},
|
|
2711
|
-
"Skipping resume of error session"
|
|
2712
|
-
);
|
|
2713
|
-
return null;
|
|
2714
|
-
}
|
|
2715
|
-
log7.info(
|
|
2716
|
-
{
|
|
2717
|
-
threadId: message.threadId,
|
|
2718
|
-
sessionId: record.sessionId,
|
|
2719
|
-
status: record.status
|
|
2720
|
-
},
|
|
2721
|
-
"Lazy resume: found record, attempting resume"
|
|
2722
|
-
);
|
|
2723
|
-
const resumePromise = (async () => {
|
|
2724
|
-
try {
|
|
2725
|
-
const session = await this.createSession({
|
|
2726
|
-
channelId: record.channelId,
|
|
2727
|
-
agentName: record.agentName,
|
|
2728
|
-
workingDirectory: record.workingDir,
|
|
2729
|
-
resumeAgentSessionId: record.agentSessionId,
|
|
2730
|
-
existingSessionId: record.sessionId,
|
|
2731
|
-
initialName: record.name
|
|
2732
|
-
});
|
|
2733
|
-
session.threadId = message.threadId;
|
|
2734
|
-
session.activate();
|
|
2735
|
-
session.dangerousMode = record.dangerousMode ?? false;
|
|
2736
|
-
log7.info(
|
|
2737
|
-
{ sessionId: session.id, threadId: message.threadId },
|
|
2738
|
-
"Lazy resume successful"
|
|
2739
|
-
);
|
|
2740
|
-
return session;
|
|
2741
|
-
} catch (err) {
|
|
2742
|
-
log7.error({ err, record }, "Lazy resume failed");
|
|
2743
|
-
const adapter = this.adapters.get(message.channelId);
|
|
2744
|
-
if (adapter) {
|
|
2745
|
-
try {
|
|
2746
|
-
await adapter.sendMessage(message.threadId, {
|
|
2747
|
-
type: "error",
|
|
2748
|
-
text: `\u26A0\uFE0F Failed to resume session: ${err instanceof Error ? err.message : String(err)}`
|
|
2749
|
-
});
|
|
2750
|
-
} catch {
|
|
2751
|
-
}
|
|
2752
|
-
}
|
|
2753
|
-
return null;
|
|
2754
|
-
} finally {
|
|
2755
|
-
this.resumeLocks.delete(lockKey);
|
|
2756
|
-
}
|
|
2757
|
-
})();
|
|
2758
|
-
this.resumeLocks.set(lockKey, resumePromise);
|
|
2759
|
-
return resumePromise;
|
|
2760
|
-
}
|
|
2761
|
-
// --- Event Wiring ---
|
|
2762
|
-
/** Create a SessionBridge for the given session and adapter */
|
|
2763
|
-
createBridge(session, adapter) {
|
|
2764
|
-
return new SessionBridge(session, adapter, {
|
|
2765
|
-
messageTransformer: this.messageTransformer,
|
|
2766
|
-
notificationManager: this.notificationManager,
|
|
2767
|
-
sessionManager: this.sessionManager,
|
|
2768
|
-
eventBus: this.eventBus,
|
|
2769
|
-
fileService: this.fileService
|
|
2770
|
-
});
|
|
2771
|
-
}
|
|
2772
|
-
};
|
|
2773
|
-
|
|
2774
|
-
// src/core/sse-manager.ts
|
|
2775
|
-
var SSEManager = class {
|
|
2776
|
-
constructor(eventBus, getSessionStats, startedAt) {
|
|
2777
|
-
this.eventBus = eventBus;
|
|
2778
|
-
this.getSessionStats = getSessionStats;
|
|
2779
|
-
this.startedAt = startedAt;
|
|
2780
|
-
}
|
|
2781
|
-
sseConnections = /* @__PURE__ */ new Set();
|
|
2782
|
-
sseCleanupHandlers = /* @__PURE__ */ new Map();
|
|
2783
|
-
healthInterval;
|
|
2784
|
-
boundHandlers = [];
|
|
2785
|
-
setup() {
|
|
2786
|
-
if (!this.eventBus) return;
|
|
2787
|
-
const events = [
|
|
2788
|
-
"session:created",
|
|
2789
|
-
"session:updated",
|
|
2790
|
-
"session:deleted",
|
|
2791
|
-
"agent:event",
|
|
2792
|
-
"permission:request"
|
|
2793
|
-
];
|
|
2794
|
-
for (const eventName of events) {
|
|
2795
|
-
const handler = (data) => {
|
|
2796
|
-
this.broadcast(eventName, data);
|
|
2797
|
-
};
|
|
2798
|
-
this.eventBus.on(eventName, handler);
|
|
2799
|
-
this.boundHandlers.push({ event: eventName, handler });
|
|
2800
|
-
}
|
|
2801
|
-
this.healthInterval = setInterval(() => {
|
|
2802
|
-
const mem = process.memoryUsage();
|
|
2803
|
-
const stats = this.getSessionStats();
|
|
2804
|
-
this.broadcast("health", {
|
|
2805
|
-
uptime: Date.now() - this.startedAt,
|
|
2806
|
-
memory: {
|
|
2807
|
-
rss: mem.rss,
|
|
2808
|
-
heapUsed: mem.heapUsed,
|
|
2809
|
-
heapTotal: mem.heapTotal
|
|
2810
|
-
},
|
|
2811
|
-
sessions: stats
|
|
2812
|
-
});
|
|
2813
|
-
}, 3e4);
|
|
2814
|
-
}
|
|
2815
|
-
handleRequest(req, res) {
|
|
2816
|
-
const parsedUrl = new URL(req.url || "", "http://localhost");
|
|
2817
|
-
const sessionFilter = parsedUrl.searchParams.get("sessionId");
|
|
2818
|
-
res.writeHead(200, {
|
|
2819
|
-
"Content-Type": "text/event-stream",
|
|
2820
|
-
"Cache-Control": "no-cache",
|
|
2821
|
-
Connection: "keep-alive"
|
|
2822
|
-
});
|
|
2823
|
-
res.flushHeaders();
|
|
2824
|
-
res.sessionFilter = sessionFilter ?? void 0;
|
|
2825
|
-
this.sseConnections.add(res);
|
|
2826
|
-
const cleanup = () => {
|
|
2827
|
-
this.sseConnections.delete(res);
|
|
2828
|
-
this.sseCleanupHandlers.delete(res);
|
|
2829
|
-
};
|
|
2830
|
-
this.sseCleanupHandlers.set(res, cleanup);
|
|
2831
|
-
req.on("close", cleanup);
|
|
2832
|
-
}
|
|
2833
|
-
broadcast(event, data) {
|
|
2834
|
-
const payload = `event: ${event}
|
|
2835
|
-
data: ${JSON.stringify(data)}
|
|
2836
|
-
|
|
2837
|
-
`;
|
|
2838
|
-
const sessionEvents = [
|
|
2839
|
-
"agent:event",
|
|
2840
|
-
"permission:request",
|
|
2841
|
-
"session:updated"
|
|
2842
|
-
];
|
|
2843
|
-
for (const res of this.sseConnections) {
|
|
2844
|
-
const filter = res.sessionFilter;
|
|
2845
|
-
if (filter && sessionEvents.includes(event)) {
|
|
2846
|
-
const eventData = data;
|
|
2847
|
-
if (eventData.sessionId !== filter) continue;
|
|
2848
|
-
}
|
|
2849
|
-
try {
|
|
2850
|
-
if (res.writable) res.write(payload);
|
|
2851
|
-
} catch {
|
|
2852
|
-
}
|
|
2853
|
-
}
|
|
2854
|
-
}
|
|
2855
|
-
stop() {
|
|
2856
|
-
if (this.healthInterval) clearInterval(this.healthInterval);
|
|
2857
|
-
if (this.eventBus) {
|
|
2858
|
-
for (const { event, handler } of this.boundHandlers) {
|
|
2859
|
-
this.eventBus.off(event, handler);
|
|
2860
|
-
}
|
|
2861
|
-
}
|
|
2862
|
-
this.boundHandlers = [];
|
|
2863
|
-
const entries = [...this.sseCleanupHandlers];
|
|
2864
|
-
for (const [res, cleanup] of entries) {
|
|
2865
|
-
res.end();
|
|
2866
|
-
cleanup();
|
|
2867
|
-
}
|
|
2868
|
-
}
|
|
2869
|
-
};
|
|
2870
|
-
|
|
2871
|
-
// src/core/static-server.ts
|
|
2872
|
-
import * as fs6 from "fs";
|
|
2873
|
-
import * as path6 from "path";
|
|
2874
|
-
import { fileURLToPath } from "url";
|
|
2875
|
-
var MIME_TYPES = {
|
|
2876
|
-
".html": "text/html; charset=utf-8",
|
|
2877
|
-
".js": "application/javascript; charset=utf-8",
|
|
2878
|
-
".css": "text/css; charset=utf-8",
|
|
2879
|
-
".json": "application/json; charset=utf-8",
|
|
2880
|
-
".png": "image/png",
|
|
2881
|
-
".jpg": "image/jpeg",
|
|
2882
|
-
".svg": "image/svg+xml",
|
|
2883
|
-
".ico": "image/x-icon",
|
|
2884
|
-
".woff": "font/woff",
|
|
2885
|
-
".woff2": "font/woff2"
|
|
2886
|
-
};
|
|
2887
|
-
var StaticServer = class {
|
|
2888
|
-
uiDir;
|
|
2889
|
-
constructor(uiDir) {
|
|
2890
|
-
this.uiDir = uiDir;
|
|
2891
|
-
if (!this.uiDir) {
|
|
2892
|
-
const __filename = fileURLToPath(import.meta.url);
|
|
2893
|
-
const candidate = path6.resolve(path6.dirname(__filename), "../../ui/dist");
|
|
2894
|
-
if (fs6.existsSync(path6.join(candidate, "index.html"))) {
|
|
2895
|
-
this.uiDir = candidate;
|
|
2896
|
-
}
|
|
2897
|
-
if (!this.uiDir) {
|
|
2898
|
-
const publishCandidate = path6.resolve(
|
|
2899
|
-
path6.dirname(__filename),
|
|
2900
|
-
"../ui"
|
|
2901
|
-
);
|
|
2902
|
-
if (fs6.existsSync(path6.join(publishCandidate, "index.html"))) {
|
|
2903
|
-
this.uiDir = publishCandidate;
|
|
2904
|
-
}
|
|
2905
|
-
}
|
|
2906
|
-
}
|
|
2907
|
-
}
|
|
2908
|
-
isAvailable() {
|
|
2909
|
-
return this.uiDir !== void 0;
|
|
2910
|
-
}
|
|
2911
|
-
serve(req, res) {
|
|
2912
|
-
if (!this.uiDir) return false;
|
|
2913
|
-
const urlPath = (req.url || "/").split("?")[0];
|
|
2914
|
-
const safePath = path6.normalize(urlPath);
|
|
2915
|
-
const filePath = path6.join(this.uiDir, safePath);
|
|
2916
|
-
if (!filePath.startsWith(this.uiDir + path6.sep) && filePath !== this.uiDir)
|
|
2917
|
-
return false;
|
|
2918
|
-
if (fs6.existsSync(filePath) && fs6.statSync(filePath).isFile()) {
|
|
2919
|
-
const ext = path6.extname(filePath);
|
|
2920
|
-
const contentType = MIME_TYPES[ext] ?? "application/octet-stream";
|
|
2921
|
-
const isHashed = /\.[a-zA-Z0-9]{8,}\.(js|css)$/.test(filePath);
|
|
2922
|
-
const cacheControl = isHashed ? "public, max-age=31536000, immutable" : "no-cache";
|
|
2923
|
-
res.writeHead(200, {
|
|
2924
|
-
"Content-Type": contentType,
|
|
2925
|
-
"Cache-Control": cacheControl
|
|
2926
|
-
});
|
|
2927
|
-
fs6.createReadStream(filePath).pipe(res);
|
|
2928
|
-
return true;
|
|
2929
|
-
}
|
|
2930
|
-
const indexPath = path6.join(this.uiDir, "index.html");
|
|
2931
|
-
if (fs6.existsSync(indexPath)) {
|
|
2932
|
-
res.writeHead(200, {
|
|
2933
|
-
"Content-Type": "text/html; charset=utf-8",
|
|
2934
|
-
"Cache-Control": "no-cache"
|
|
2935
|
-
});
|
|
2936
|
-
fs6.createReadStream(indexPath).pipe(res);
|
|
2937
|
-
return true;
|
|
2938
|
-
}
|
|
2939
|
-
return false;
|
|
2940
|
-
}
|
|
2941
|
-
};
|
|
2942
|
-
|
|
2943
|
-
// src/core/api/index.ts
|
|
2944
|
-
import * as http from "http";
|
|
2945
|
-
import * as fs7 from "fs";
|
|
2946
|
-
import * as path7 from "path";
|
|
2947
|
-
import * as os2 from "os";
|
|
2948
|
-
import * as crypto from "crypto";
|
|
2949
|
-
import { fileURLToPath as fileURLToPath2 } from "url";
|
|
2950
|
-
|
|
2951
|
-
// src/core/api/router.ts
|
|
2952
|
-
var Router = class {
|
|
2953
|
-
routes = [];
|
|
2954
|
-
get(path8, handler) {
|
|
2955
|
-
this.add("GET", path8, handler);
|
|
2956
|
-
}
|
|
2957
|
-
post(path8, handler) {
|
|
2958
|
-
this.add("POST", path8, handler);
|
|
2959
|
-
}
|
|
2960
|
-
put(path8, handler) {
|
|
2961
|
-
this.add("PUT", path8, handler);
|
|
2962
|
-
}
|
|
2963
|
-
patch(path8, handler) {
|
|
2964
|
-
this.add("PATCH", path8, handler);
|
|
2965
|
-
}
|
|
2966
|
-
delete(path8, handler) {
|
|
2967
|
-
this.add("DELETE", path8, handler);
|
|
2968
|
-
}
|
|
2969
|
-
match(method, url) {
|
|
2970
|
-
const pathname = url.split("?")[0];
|
|
2971
|
-
for (const route of this.routes) {
|
|
2972
|
-
if (route.method !== method) continue;
|
|
2973
|
-
const m = pathname.match(route.pattern);
|
|
2974
|
-
if (!m) continue;
|
|
2975
|
-
const params = {};
|
|
2976
|
-
for (let i = 0; i < route.keys.length; i++) {
|
|
2977
|
-
params[route.keys[i]] = m[i + 1];
|
|
2978
|
-
}
|
|
2979
|
-
return { handler: route.handler, params };
|
|
2980
|
-
}
|
|
2981
|
-
return null;
|
|
2982
|
-
}
|
|
2983
|
-
add(method, path8, handler) {
|
|
2984
|
-
const keys = [];
|
|
2985
|
-
const pattern = path8.replace(/:(\w+)/g, (_, key) => {
|
|
2986
|
-
keys.push(key);
|
|
2987
|
-
return "([^/]+)";
|
|
2988
|
-
});
|
|
2989
|
-
this.routes.push({
|
|
2990
|
-
method,
|
|
2991
|
-
pattern: new RegExp(`^${pattern}$`),
|
|
2992
|
-
keys,
|
|
2993
|
-
handler
|
|
2994
|
-
});
|
|
2995
|
-
}
|
|
2996
|
-
};
|
|
2997
|
-
|
|
2998
|
-
// src/core/api/routes/health.ts
|
|
2999
|
-
function registerHealthRoutes(router, deps) {
|
|
3000
|
-
router.get("/api/health", async (_req, res) => {
|
|
3001
|
-
const activeSessions = deps.core.sessionManager.listSessions();
|
|
3002
|
-
const allRecords = deps.core.sessionManager.listRecords();
|
|
3003
|
-
const mem = process.memoryUsage();
|
|
3004
|
-
const tunnel = deps.core.tunnelService;
|
|
3005
|
-
deps.sendJson(res, 200, {
|
|
3006
|
-
status: "ok",
|
|
3007
|
-
uptime: Date.now() - deps.startedAt,
|
|
3008
|
-
version: deps.getVersion(),
|
|
3009
|
-
memory: {
|
|
3010
|
-
rss: mem.rss,
|
|
3011
|
-
heapUsed: mem.heapUsed,
|
|
3012
|
-
heapTotal: mem.heapTotal
|
|
3013
|
-
},
|
|
3014
|
-
sessions: {
|
|
3015
|
-
active: activeSessions.filter(
|
|
3016
|
-
(s) => s.status === "active" || s.status === "initializing"
|
|
3017
|
-
).length,
|
|
3018
|
-
total: allRecords.length
|
|
3019
|
-
},
|
|
3020
|
-
adapters: Array.from(deps.core.adapters.keys()),
|
|
3021
|
-
tunnel: tunnel ? { enabled: true, url: tunnel.getPublicUrl() } : { enabled: false }
|
|
3022
|
-
});
|
|
3023
|
-
});
|
|
3024
|
-
router.get("/api/version", async (_req, res) => {
|
|
3025
|
-
deps.sendJson(res, 200, { version: deps.getVersion() });
|
|
3026
|
-
});
|
|
3027
|
-
router.post("/api/restart", async (_req, res) => {
|
|
3028
|
-
if (!deps.core.requestRestart) {
|
|
3029
|
-
deps.sendJson(res, 501, { error: "Restart not available" });
|
|
3030
|
-
return;
|
|
3031
|
-
}
|
|
3032
|
-
deps.sendJson(res, 200, { ok: true, message: "Restarting..." });
|
|
3033
|
-
setImmediate(() => deps.core.requestRestart());
|
|
3034
|
-
});
|
|
3035
|
-
router.get("/api/adapters", async (_req, res) => {
|
|
3036
|
-
const adapters = Array.from(deps.core.adapters.entries()).map(([name]) => ({
|
|
3037
|
-
name,
|
|
3038
|
-
type: "built-in"
|
|
3039
|
-
}));
|
|
3040
|
-
deps.sendJson(res, 200, { adapters });
|
|
3041
|
-
});
|
|
3042
|
-
}
|
|
3043
|
-
|
|
3044
|
-
// src/core/api/routes/sessions.ts
|
|
3045
|
-
var log8 = createChildLogger({ module: "api-server" });
|
|
3046
|
-
function registerSessionRoutes(router, deps) {
|
|
3047
|
-
router.post("/api/sessions/adopt", async (req, res) => {
|
|
3048
|
-
const body = await deps.readBody(req);
|
|
3049
|
-
if (body === null) {
|
|
3050
|
-
return deps.sendJson(res, 413, { error: "Request body too large" });
|
|
3051
|
-
}
|
|
3052
|
-
if (!body) {
|
|
3053
|
-
return deps.sendJson(res, 400, {
|
|
3054
|
-
error: "bad_request",
|
|
3055
|
-
message: "Empty request body"
|
|
3056
|
-
});
|
|
3057
|
-
}
|
|
3058
|
-
let parsed;
|
|
3059
|
-
try {
|
|
3060
|
-
parsed = JSON.parse(body);
|
|
3061
|
-
} catch {
|
|
3062
|
-
return deps.sendJson(res, 400, {
|
|
3063
|
-
error: "bad_request",
|
|
3064
|
-
message: "Invalid JSON"
|
|
3065
|
-
});
|
|
3066
|
-
}
|
|
3067
|
-
const { agent, agentSessionId, cwd, channel } = parsed;
|
|
3068
|
-
if (!agent || !agentSessionId) {
|
|
3069
|
-
return deps.sendJson(res, 400, {
|
|
3070
|
-
error: "bad_request",
|
|
3071
|
-
message: "Missing required fields: agent, agentSessionId"
|
|
3072
|
-
});
|
|
3073
|
-
}
|
|
3074
|
-
const result = await deps.core.adoptSession(
|
|
3075
|
-
agent,
|
|
3076
|
-
agentSessionId,
|
|
3077
|
-
cwd ?? process.cwd(),
|
|
3078
|
-
channel
|
|
3079
|
-
);
|
|
3080
|
-
if (result.ok) {
|
|
3081
|
-
return deps.sendJson(res, 200, result);
|
|
3082
|
-
} else {
|
|
3083
|
-
const status = result.error === "session_limit" ? 429 : result.error === "agent_not_supported" ? 400 : 500;
|
|
3084
|
-
return deps.sendJson(res, status, result);
|
|
3085
|
-
}
|
|
3086
|
-
});
|
|
3087
|
-
router.post("/api/sessions", async (req, res) => {
|
|
3088
|
-
const body = await deps.readBody(req);
|
|
3089
|
-
let agent;
|
|
3090
|
-
let workspace;
|
|
3091
|
-
if (body) {
|
|
3092
|
-
try {
|
|
3093
|
-
const parsed = JSON.parse(body);
|
|
3094
|
-
agent = parsed.agent;
|
|
3095
|
-
workspace = parsed.workspace;
|
|
3096
|
-
} catch {
|
|
3097
|
-
deps.sendJson(res, 400, { error: "Invalid JSON body" });
|
|
3098
|
-
return;
|
|
3099
|
-
}
|
|
3100
|
-
}
|
|
3101
|
-
const config = deps.core.configManager.get();
|
|
3102
|
-
const activeSessions = deps.core.sessionManager.listSessions().filter((s) => s.status === "active" || s.status === "initializing");
|
|
3103
|
-
if (activeSessions.length >= config.security.maxConcurrentSessions) {
|
|
3104
|
-
deps.sendJson(res, 429, {
|
|
3105
|
-
error: `Max concurrent sessions (${config.security.maxConcurrentSessions}) reached. Cancel a session first.`
|
|
3106
|
-
});
|
|
3107
|
-
return;
|
|
3108
|
-
}
|
|
3109
|
-
const [adapterId, adapter] = deps.core.adapters.entries().next().value ?? [
|
|
3110
|
-
null,
|
|
3111
|
-
null
|
|
3112
|
-
];
|
|
3113
|
-
const channelId = adapterId ?? "api";
|
|
3114
|
-
const resolvedAgent = agent || config.defaultAgent;
|
|
3115
|
-
const resolvedWorkspace = deps.core.configManager.resolveWorkspace(
|
|
3116
|
-
workspace || config.agents[resolvedAgent]?.workingDirectory
|
|
3117
|
-
);
|
|
3118
|
-
const session = await deps.core.createSession({
|
|
3119
|
-
channelId,
|
|
3120
|
-
agentName: resolvedAgent,
|
|
3121
|
-
workingDirectory: resolvedWorkspace,
|
|
3122
|
-
createThread: !!adapter,
|
|
3123
|
-
initialName: `\u{1F504} ${resolvedAgent} \u2014 New Session`
|
|
3124
|
-
});
|
|
3125
|
-
if (!adapter) {
|
|
3126
|
-
session.agentInstance.onPermissionRequest = async (request) => {
|
|
3127
|
-
const allowOption = request.options.find((o) => o.isAllow);
|
|
3128
|
-
log8.debug(
|
|
3129
|
-
{
|
|
3130
|
-
sessionId: session.id,
|
|
3131
|
-
permissionId: request.id,
|
|
3132
|
-
option: allowOption?.id
|
|
3133
|
-
},
|
|
3134
|
-
"Auto-approving permission for API session"
|
|
3135
|
-
);
|
|
3136
|
-
return allowOption?.id ?? request.options[0]?.id ?? "";
|
|
3137
|
-
};
|
|
3138
|
-
}
|
|
3139
|
-
session.warmup().catch(
|
|
3140
|
-
(err) => log8.warn({ err, sessionId: session.id }, "API session warmup failed")
|
|
3141
|
-
);
|
|
3142
|
-
deps.sendJson(res, 200, {
|
|
3143
|
-
sessionId: session.id,
|
|
3144
|
-
agent: session.agentName,
|
|
3145
|
-
status: session.status,
|
|
3146
|
-
workspace: session.workingDirectory
|
|
3147
|
-
});
|
|
3148
|
-
});
|
|
3149
|
-
router.post("/api/sessions/:sessionId/prompt", async (req, res, params) => {
|
|
3150
|
-
const sessionId = decodeURIComponent(params.sessionId);
|
|
3151
|
-
const session = deps.core.sessionManager.getSession(sessionId);
|
|
3152
|
-
if (!session) {
|
|
3153
|
-
deps.sendJson(res, 404, { error: `Session "${sessionId}" not found` });
|
|
3154
|
-
return;
|
|
3155
|
-
}
|
|
3156
|
-
if (session.status === "cancelled" || session.status === "finished" || session.status === "error") {
|
|
3157
|
-
deps.sendJson(res, 400, { error: `Session is ${session.status}` });
|
|
3158
|
-
return;
|
|
3159
|
-
}
|
|
3160
|
-
const body = await deps.readBody(req);
|
|
3161
|
-
let prompt;
|
|
3162
|
-
if (body) {
|
|
3163
|
-
try {
|
|
3164
|
-
const parsed = JSON.parse(body);
|
|
3165
|
-
prompt = parsed.prompt;
|
|
3166
|
-
} catch {
|
|
3167
|
-
deps.sendJson(res, 400, { error: "Invalid JSON body" });
|
|
3168
|
-
return;
|
|
3169
|
-
}
|
|
3170
|
-
}
|
|
3171
|
-
if (!prompt) {
|
|
3172
|
-
deps.sendJson(res, 400, { error: "Missing prompt" });
|
|
3173
|
-
return;
|
|
3174
|
-
}
|
|
3175
|
-
session.enqueuePrompt(prompt).catch(() => {
|
|
3176
|
-
});
|
|
3177
|
-
deps.sendJson(res, 200, {
|
|
3178
|
-
ok: true,
|
|
3179
|
-
sessionId,
|
|
3180
|
-
queueDepth: session.queueDepth
|
|
3181
|
-
});
|
|
3182
|
-
});
|
|
3183
|
-
router.post(
|
|
3184
|
-
"/api/sessions/:sessionId/permission",
|
|
3185
|
-
async (req, res, params) => {
|
|
3186
|
-
const sessionId = decodeURIComponent(params.sessionId);
|
|
3187
|
-
const session = deps.core.sessionManager.getSession(sessionId);
|
|
3188
|
-
if (!session) {
|
|
3189
|
-
deps.sendJson(res, 404, { error: `Session "${sessionId}" not found` });
|
|
3190
|
-
return;
|
|
3191
|
-
}
|
|
3192
|
-
const body = await deps.readBody(req);
|
|
3193
|
-
let permissionId;
|
|
3194
|
-
let optionId;
|
|
3195
|
-
if (body) {
|
|
3196
|
-
try {
|
|
3197
|
-
const parsed = JSON.parse(body);
|
|
3198
|
-
permissionId = parsed.permissionId;
|
|
3199
|
-
optionId = parsed.optionId;
|
|
3200
|
-
} catch {
|
|
3201
|
-
deps.sendJson(res, 400, { error: "Invalid JSON body" });
|
|
3202
|
-
return;
|
|
3203
|
-
}
|
|
3204
|
-
}
|
|
3205
|
-
if (!permissionId || !optionId) {
|
|
3206
|
-
deps.sendJson(res, 400, {
|
|
3207
|
-
error: "Missing permissionId or optionId"
|
|
3208
|
-
});
|
|
3209
|
-
return;
|
|
3210
|
-
}
|
|
3211
|
-
if (!session.permissionGate.isPending || session.permissionGate.requestId !== permissionId) {
|
|
3212
|
-
deps.sendJson(res, 400, {
|
|
3213
|
-
error: "No matching pending permission request"
|
|
3214
|
-
});
|
|
3215
|
-
return;
|
|
3216
|
-
}
|
|
3217
|
-
session.permissionGate.resolve(optionId);
|
|
3218
|
-
deps.sendJson(res, 200, { ok: true });
|
|
3219
|
-
}
|
|
3220
|
-
);
|
|
3221
|
-
router.patch(
|
|
3222
|
-
"/api/sessions/:sessionId/dangerous",
|
|
3223
|
-
async (req, res, params) => {
|
|
3224
|
-
const sessionId = decodeURIComponent(params.sessionId);
|
|
3225
|
-
const session = deps.core.sessionManager.getSession(sessionId);
|
|
3226
|
-
if (!session) {
|
|
3227
|
-
deps.sendJson(res, 404, { error: `Session "${sessionId}" not found` });
|
|
3228
|
-
return;
|
|
3229
|
-
}
|
|
3230
|
-
const body = await deps.readBody(req);
|
|
3231
|
-
let enabled;
|
|
3232
|
-
if (body) {
|
|
3233
|
-
try {
|
|
3234
|
-
const parsed = JSON.parse(body);
|
|
3235
|
-
enabled = parsed.enabled;
|
|
3236
|
-
} catch {
|
|
3237
|
-
deps.sendJson(res, 400, { error: "Invalid JSON body" });
|
|
3238
|
-
return;
|
|
3239
|
-
}
|
|
3240
|
-
}
|
|
3241
|
-
if (typeof enabled !== "boolean") {
|
|
3242
|
-
deps.sendJson(res, 400, { error: "Missing enabled boolean" });
|
|
3243
|
-
return;
|
|
3244
|
-
}
|
|
3245
|
-
session.dangerousMode = enabled;
|
|
3246
|
-
await deps.core.sessionManager.patchRecord(sessionId, {
|
|
3247
|
-
dangerousMode: enabled
|
|
3248
|
-
});
|
|
3249
|
-
deps.sendJson(res, 200, { ok: true, dangerousMode: enabled });
|
|
3250
|
-
}
|
|
3251
|
-
);
|
|
3252
|
-
router.get("/api/sessions/:sessionId", async (_req, res, params) => {
|
|
3253
|
-
const sessionId = decodeURIComponent(params.sessionId);
|
|
3254
|
-
const session = deps.core.sessionManager.getSession(sessionId);
|
|
3255
|
-
if (!session) {
|
|
3256
|
-
deps.sendJson(res, 404, { error: `Session "${sessionId}" not found` });
|
|
3257
|
-
return;
|
|
3258
|
-
}
|
|
3259
|
-
deps.sendJson(res, 200, {
|
|
3260
|
-
session: {
|
|
3261
|
-
id: session.id,
|
|
3262
|
-
agent: session.agentName,
|
|
3263
|
-
status: session.status,
|
|
3264
|
-
name: session.name ?? null,
|
|
3265
|
-
workspace: session.workingDirectory,
|
|
3266
|
-
createdAt: session.createdAt.toISOString(),
|
|
3267
|
-
dangerousMode: session.dangerousMode,
|
|
3268
|
-
queueDepth: session.queueDepth,
|
|
3269
|
-
promptRunning: session.promptRunning,
|
|
3270
|
-
threadId: session.threadId,
|
|
3271
|
-
channelId: session.channelId,
|
|
3272
|
-
agentSessionId: session.agentSessionId
|
|
3273
|
-
}
|
|
3274
|
-
});
|
|
3275
|
-
});
|
|
3276
|
-
router.post("/api/sessions/:sessionId/archive", async (_req, res, params) => {
|
|
3277
|
-
const sessionId = decodeURIComponent(params.sessionId);
|
|
3278
|
-
const result = await deps.core.archiveSession(sessionId);
|
|
3279
|
-
if (result.ok) {
|
|
3280
|
-
deps.sendJson(res, 200, result);
|
|
3281
|
-
} else {
|
|
3282
|
-
deps.sendJson(res, 400, result);
|
|
3283
|
-
}
|
|
3284
|
-
});
|
|
3285
|
-
router.delete("/api/sessions/:sessionId", async (_req, res, params) => {
|
|
3286
|
-
const sessionId = decodeURIComponent(params.sessionId);
|
|
3287
|
-
const session = deps.core.sessionManager.getSession(sessionId);
|
|
3288
|
-
if (!session) {
|
|
3289
|
-
deps.sendJson(res, 404, { error: `Session "${sessionId}" not found` });
|
|
3290
|
-
return;
|
|
3291
|
-
}
|
|
3292
|
-
await deps.core.sessionManager.cancelSession(sessionId);
|
|
3293
|
-
deps.sendJson(res, 200, { ok: true });
|
|
3294
|
-
});
|
|
3295
|
-
router.get("/api/sessions", async (_req, res) => {
|
|
3296
|
-
const sessions = deps.core.sessionManager.listSessions();
|
|
3297
|
-
deps.sendJson(res, 200, {
|
|
3298
|
-
sessions: sessions.map((s) => ({
|
|
3299
|
-
id: s.id,
|
|
3300
|
-
agent: s.agentName,
|
|
3301
|
-
status: s.status,
|
|
3302
|
-
name: s.name ?? null,
|
|
3303
|
-
workspace: s.workingDirectory,
|
|
3304
|
-
createdAt: s.createdAt.toISOString(),
|
|
3305
|
-
dangerousMode: s.dangerousMode,
|
|
3306
|
-
queueDepth: s.queueDepth,
|
|
3307
|
-
promptRunning: s.promptRunning,
|
|
3308
|
-
lastActiveAt: deps.core.sessionManager.getSessionRecord(s.id)?.lastActiveAt ?? null
|
|
3309
|
-
}))
|
|
3310
|
-
});
|
|
3311
|
-
});
|
|
3312
|
-
}
|
|
3313
|
-
|
|
3314
|
-
// src/core/api/routes/config.ts
|
|
3315
|
-
var SENSITIVE_KEYS = [
|
|
3316
|
-
"botToken",
|
|
3317
|
-
"token",
|
|
3318
|
-
"apiKey",
|
|
3319
|
-
"secret",
|
|
3320
|
-
"password",
|
|
3321
|
-
"webhookSecret"
|
|
3322
|
-
];
|
|
3323
|
-
function redactConfig(config) {
|
|
3324
|
-
const redacted = structuredClone(config);
|
|
3325
|
-
redactDeep(redacted);
|
|
3326
|
-
return redacted;
|
|
3327
|
-
}
|
|
3328
|
-
function redactDeep(obj) {
|
|
3329
|
-
for (const [key, value] of Object.entries(obj)) {
|
|
3330
|
-
if (SENSITIVE_KEYS.includes(key) && typeof value === "string") {
|
|
3331
|
-
obj[key] = "***";
|
|
3332
|
-
} else if (Array.isArray(value)) {
|
|
3333
|
-
for (const item of value) {
|
|
3334
|
-
if (item && typeof item === "object")
|
|
3335
|
-
redactDeep(item);
|
|
3336
|
-
}
|
|
3337
|
-
} else if (value && typeof value === "object") {
|
|
3338
|
-
redactDeep(value);
|
|
3339
|
-
}
|
|
3340
|
-
}
|
|
3341
|
-
}
|
|
3342
|
-
function registerConfigRoutes(router, deps) {
|
|
3343
|
-
router.get("/api/config/editable", async (_req, res) => {
|
|
3344
|
-
const { getSafeFields: getSafeFields2, resolveOptions: resolveOptions2, getConfigValue: getConfigValue2 } = await import("./config-registry-7I6GGDOY.js");
|
|
3345
|
-
const config = deps.core.configManager.get();
|
|
3346
|
-
const safeFields = getSafeFields2();
|
|
3347
|
-
const fields = safeFields.map((def) => ({
|
|
3348
|
-
path: def.path,
|
|
3349
|
-
displayName: def.displayName,
|
|
3350
|
-
group: def.group,
|
|
3351
|
-
type: def.type,
|
|
3352
|
-
options: resolveOptions2(def, config),
|
|
3353
|
-
value: getConfigValue2(config, def.path),
|
|
3354
|
-
hotReload: def.hotReload
|
|
3355
|
-
}));
|
|
3356
|
-
deps.sendJson(res, 200, { fields });
|
|
3357
|
-
});
|
|
3358
|
-
router.get("/api/config", async (_req, res) => {
|
|
3359
|
-
const config = deps.core.configManager.get();
|
|
3360
|
-
deps.sendJson(res, 200, { config: redactConfig(config) });
|
|
3361
|
-
});
|
|
3362
|
-
router.patch("/api/config", async (req, res) => {
|
|
3363
|
-
const body = await deps.readBody(req);
|
|
3364
|
-
let configPath;
|
|
3365
|
-
let value;
|
|
3366
|
-
if (body) {
|
|
3367
|
-
try {
|
|
3368
|
-
const parsed = JSON.parse(body);
|
|
3369
|
-
configPath = parsed.path;
|
|
3370
|
-
value = parsed.value;
|
|
3371
|
-
} catch {
|
|
3372
|
-
deps.sendJson(res, 400, { error: "Invalid JSON body" });
|
|
3373
|
-
return;
|
|
3374
|
-
}
|
|
3375
|
-
}
|
|
3376
|
-
if (!configPath) {
|
|
3377
|
-
deps.sendJson(res, 400, { error: "Missing path" });
|
|
3378
|
-
return;
|
|
3379
|
-
}
|
|
3380
|
-
const BLOCKED_KEYS = /* @__PURE__ */ new Set(["__proto__", "constructor", "prototype"]);
|
|
3381
|
-
const parts = configPath.split(".");
|
|
3382
|
-
if (parts.some((p) => BLOCKED_KEYS.has(p))) {
|
|
3383
|
-
deps.sendJson(res, 400, { error: "Invalid config path" });
|
|
3384
|
-
return;
|
|
3385
|
-
}
|
|
3386
|
-
const { getFieldDef: getFieldDef2 } = await import("./config-registry-7I6GGDOY.js");
|
|
3387
|
-
const fieldDef = getFieldDef2(configPath);
|
|
3388
|
-
if (!fieldDef || fieldDef.scope !== "safe") {
|
|
3389
|
-
deps.sendJson(res, 403, {
|
|
3390
|
-
error: "This config field cannot be modified via the API"
|
|
3391
|
-
});
|
|
3392
|
-
return;
|
|
3393
|
-
}
|
|
3394
|
-
const currentConfig = deps.core.configManager.get();
|
|
3395
|
-
const cloned = structuredClone(currentConfig);
|
|
3396
|
-
let target = cloned;
|
|
3397
|
-
for (let i = 0; i < parts.length - 1; i++) {
|
|
3398
|
-
const part = parts[i];
|
|
3399
|
-
if (target[part] && typeof target[part] === "object" && !Array.isArray(target[part])) {
|
|
3400
|
-
target = target[part];
|
|
3401
|
-
} else if (target[part] === void 0 || target[part] === null) {
|
|
3402
|
-
target[part] = {};
|
|
3403
|
-
target = target[part];
|
|
3404
|
-
} else {
|
|
3405
|
-
deps.sendJson(res, 400, { error: "Invalid config path" });
|
|
3406
|
-
return;
|
|
3407
|
-
}
|
|
3408
|
-
}
|
|
3409
|
-
const lastKey = parts[parts.length - 1];
|
|
3410
|
-
target[lastKey] = value;
|
|
3411
|
-
const { ConfigSchema } = await import("./config-XDUOULXX.js");
|
|
3412
|
-
const result = ConfigSchema.safeParse(cloned);
|
|
3413
|
-
if (!result.success) {
|
|
3414
|
-
deps.sendJson(res, 400, {
|
|
3415
|
-
error: "Validation failed",
|
|
3416
|
-
details: result.error.issues.map((i) => ({
|
|
3417
|
-
path: i.path.join("."),
|
|
3418
|
-
message: i.message
|
|
3419
|
-
}))
|
|
3420
|
-
});
|
|
3421
|
-
return;
|
|
3422
|
-
}
|
|
3423
|
-
const updates = {};
|
|
3424
|
-
let updateTarget = updates;
|
|
3425
|
-
for (let i = 0; i < parts.length - 1; i++) {
|
|
3426
|
-
updateTarget[parts[i]] = {};
|
|
3427
|
-
updateTarget = updateTarget[parts[i]];
|
|
3428
|
-
}
|
|
3429
|
-
updateTarget[lastKey] = value;
|
|
3430
|
-
await deps.core.configManager.save(updates, configPath);
|
|
3431
|
-
const { isHotReloadable: isHotReloadable2 } = await import("./config-registry-7I6GGDOY.js");
|
|
3432
|
-
const needsRestart = !isHotReloadable2(configPath);
|
|
3433
|
-
deps.sendJson(res, 200, {
|
|
3434
|
-
ok: true,
|
|
3435
|
-
needsRestart,
|
|
3436
|
-
config: redactConfig(deps.core.configManager.get())
|
|
3437
|
-
});
|
|
3438
|
-
});
|
|
3439
|
-
}
|
|
3440
|
-
|
|
3441
|
-
// src/core/api/routes/topics.ts
|
|
3442
|
-
function registerTopicRoutes(router, deps) {
|
|
3443
|
-
router.get("/api/topics", async (req, res) => {
|
|
3444
|
-
if (!deps.topicManager) {
|
|
3445
|
-
deps.sendJson(res, 501, { error: "Topic management not available" });
|
|
3446
|
-
return;
|
|
3447
|
-
}
|
|
3448
|
-
const url = req.url || "";
|
|
3449
|
-
const params = new URL(url, "http://localhost").searchParams;
|
|
3450
|
-
const statusParam = params.get("status");
|
|
3451
|
-
const filter = statusParam ? { statuses: statusParam.split(",") } : void 0;
|
|
3452
|
-
const topics = deps.topicManager.listTopics(filter);
|
|
3453
|
-
deps.sendJson(res, 200, { topics });
|
|
3454
|
-
});
|
|
3455
|
-
router.post("/api/topics/cleanup", async (req, res) => {
|
|
3456
|
-
if (!deps.topicManager) {
|
|
3457
|
-
deps.sendJson(res, 501, { error: "Topic management not available" });
|
|
3458
|
-
return;
|
|
3459
|
-
}
|
|
3460
|
-
const body = await deps.readBody(req);
|
|
3461
|
-
let statuses;
|
|
3462
|
-
if (body) {
|
|
3463
|
-
try {
|
|
3464
|
-
statuses = JSON.parse(body).statuses;
|
|
3465
|
-
} catch {
|
|
3466
|
-
}
|
|
3467
|
-
}
|
|
3468
|
-
const result = await deps.topicManager.cleanup(statuses);
|
|
3469
|
-
deps.sendJson(res, 200, result);
|
|
3470
|
-
});
|
|
3471
|
-
router.delete("/api/topics/:sessionId", async (req, res, params) => {
|
|
3472
|
-
if (!deps.topicManager) {
|
|
3473
|
-
deps.sendJson(res, 501, { error: "Topic management not available" });
|
|
3474
|
-
return;
|
|
3475
|
-
}
|
|
3476
|
-
const sessionId = decodeURIComponent(params.sessionId);
|
|
3477
|
-
const url = req.url || "";
|
|
3478
|
-
const urlParams = new URL(url, "http://localhost").searchParams;
|
|
3479
|
-
const force = urlParams.get("force") === "true";
|
|
3480
|
-
const result = await deps.topicManager.deleteTopic(
|
|
3481
|
-
sessionId,
|
|
3482
|
-
force ? { confirmed: true } : void 0
|
|
3483
|
-
);
|
|
3484
|
-
if (result.ok) {
|
|
3485
|
-
deps.sendJson(res, 200, result);
|
|
3486
|
-
} else if (result.needsConfirmation) {
|
|
3487
|
-
deps.sendJson(res, 409, {
|
|
3488
|
-
error: "Session is active",
|
|
3489
|
-
needsConfirmation: true,
|
|
3490
|
-
session: result.session
|
|
3491
|
-
});
|
|
3492
|
-
} else if (result.error === "Cannot delete system topic") {
|
|
3493
|
-
deps.sendJson(res, 403, { error: result.error });
|
|
3494
|
-
} else {
|
|
3495
|
-
deps.sendJson(res, 404, { error: result.error ?? "Not found" });
|
|
3496
|
-
}
|
|
3497
|
-
});
|
|
3498
|
-
}
|
|
3499
|
-
|
|
3500
|
-
// src/core/api/routes/tunnel.ts
|
|
3501
|
-
function registerTunnelRoutes(router, deps) {
|
|
3502
|
-
router.get("/api/tunnel", async (_req, res) => {
|
|
3503
|
-
const tunnel = deps.core.tunnelService;
|
|
3504
|
-
if (tunnel) {
|
|
3505
|
-
deps.sendJson(res, 200, {
|
|
3506
|
-
enabled: true,
|
|
3507
|
-
url: tunnel.getPublicUrl(),
|
|
3508
|
-
provider: deps.core.configManager.get().tunnel.provider
|
|
3509
|
-
});
|
|
3510
|
-
} else {
|
|
3511
|
-
deps.sendJson(res, 200, { enabled: false });
|
|
3512
|
-
}
|
|
3513
|
-
});
|
|
3514
|
-
router.get("/api/tunnel/list", async (_req, res) => {
|
|
3515
|
-
const tunnel = deps.core.tunnelService;
|
|
3516
|
-
if (!tunnel) {
|
|
3517
|
-
deps.sendJson(res, 200, []);
|
|
3518
|
-
return;
|
|
3519
|
-
}
|
|
3520
|
-
deps.sendJson(res, 200, tunnel.listTunnels());
|
|
3521
|
-
});
|
|
3522
|
-
router.post("/api/tunnel", async (req, res) => {
|
|
3523
|
-
const tunnel = deps.core.tunnelService;
|
|
3524
|
-
if (!tunnel) {
|
|
3525
|
-
deps.sendJson(res, 400, { error: "Tunnel service is not enabled" });
|
|
3526
|
-
return;
|
|
3527
|
-
}
|
|
3528
|
-
const body = await deps.readBody(req);
|
|
3529
|
-
if (body === null) {
|
|
3530
|
-
deps.sendJson(res, 413, { error: "Request body too large" });
|
|
3531
|
-
return;
|
|
3532
|
-
}
|
|
3533
|
-
if (!body) {
|
|
3534
|
-
deps.sendJson(res, 400, { error: "Missing request body" });
|
|
3535
|
-
return;
|
|
3536
|
-
}
|
|
3537
|
-
try {
|
|
3538
|
-
const { port, label, sessionId } = JSON.parse(body);
|
|
3539
|
-
if (!port || typeof port !== "number") {
|
|
3540
|
-
deps.sendJson(res, 400, {
|
|
3541
|
-
error: "port is required and must be a number"
|
|
3542
|
-
});
|
|
3543
|
-
return;
|
|
3544
|
-
}
|
|
3545
|
-
const entry = await tunnel.addTunnel(port, { label, sessionId });
|
|
3546
|
-
deps.sendJson(res, 200, entry);
|
|
3547
|
-
} catch (err) {
|
|
3548
|
-
deps.sendJson(res, 400, { error: err.message });
|
|
3549
|
-
}
|
|
3550
|
-
});
|
|
3551
|
-
router.delete("/api/tunnel/:port", async (_req, res, params) => {
|
|
3552
|
-
const tunnel = deps.core.tunnelService;
|
|
3553
|
-
if (!tunnel) {
|
|
3554
|
-
deps.sendJson(res, 400, { error: "Tunnel service is not enabled" });
|
|
3555
|
-
return;
|
|
3556
|
-
}
|
|
3557
|
-
const port = parseInt(params.port, 10);
|
|
3558
|
-
try {
|
|
3559
|
-
await tunnel.stopTunnel(port);
|
|
3560
|
-
deps.sendJson(res, 200, { ok: true });
|
|
3561
|
-
} catch (err) {
|
|
3562
|
-
deps.sendJson(res, 400, { error: err.message });
|
|
3563
|
-
}
|
|
3564
|
-
});
|
|
3565
|
-
router.delete("/api/tunnel", async (_req, res) => {
|
|
3566
|
-
const tunnel = deps.core.tunnelService;
|
|
3567
|
-
if (!tunnel) {
|
|
3568
|
-
deps.sendJson(res, 400, { error: "Tunnel service is not enabled" });
|
|
3569
|
-
return;
|
|
3570
|
-
}
|
|
3571
|
-
const count = tunnel.listTunnels().length;
|
|
3572
|
-
await tunnel.stopAllUser();
|
|
3573
|
-
deps.sendJson(res, 200, { ok: true, stopped: count });
|
|
3574
|
-
});
|
|
3575
|
-
}
|
|
3576
|
-
|
|
3577
|
-
// src/core/api/routes/agents.ts
|
|
3578
|
-
function registerAgentRoutes(router, deps) {
|
|
3579
|
-
router.get("/api/agents", async (_req, res) => {
|
|
3580
|
-
const agents = deps.core.agentManager.getAvailableAgents();
|
|
3581
|
-
const defaultAgent = deps.core.configManager.get().defaultAgent;
|
|
3582
|
-
const agentsWithCaps = agents.map((a) => ({
|
|
3583
|
-
...a,
|
|
3584
|
-
capabilities: getAgentCapabilities(a.name)
|
|
3585
|
-
}));
|
|
3586
|
-
deps.sendJson(res, 200, { agents: agentsWithCaps, default: defaultAgent });
|
|
3587
|
-
});
|
|
3588
|
-
}
|
|
3589
|
-
|
|
3590
|
-
// src/core/api/routes/notify.ts
|
|
3591
|
-
function registerNotifyRoutes(router, deps) {
|
|
3592
|
-
router.post("/api/notify", async (req, res) => {
|
|
3593
|
-
const body = await deps.readBody(req);
|
|
3594
|
-
let message;
|
|
3595
|
-
if (body) {
|
|
3596
|
-
try {
|
|
3597
|
-
const parsed = JSON.parse(body);
|
|
3598
|
-
message = parsed.message;
|
|
3599
|
-
} catch {
|
|
3600
|
-
deps.sendJson(res, 400, { error: "Invalid JSON body" });
|
|
3601
|
-
return;
|
|
3602
|
-
}
|
|
3603
|
-
}
|
|
3604
|
-
if (!message) {
|
|
3605
|
-
deps.sendJson(res, 400, { error: "Missing message" });
|
|
3606
|
-
return;
|
|
3607
|
-
}
|
|
3608
|
-
await deps.core.notificationManager.notifyAll({
|
|
3609
|
-
sessionId: "system",
|
|
3610
|
-
type: "completed",
|
|
3611
|
-
summary: message
|
|
3612
|
-
});
|
|
3613
|
-
deps.sendJson(res, 200, { ok: true });
|
|
3614
|
-
});
|
|
3615
|
-
}
|
|
3616
|
-
|
|
3617
|
-
// src/core/api/index.ts
|
|
3618
|
-
var log9 = createChildLogger({ module: "api-server" });
|
|
3619
|
-
var DEFAULT_PORT_FILE = path7.join(os2.homedir(), ".openacp", "api.port");
|
|
3620
|
-
var cachedVersion;
|
|
3621
|
-
function getVersion() {
|
|
3622
|
-
if (cachedVersion) return cachedVersion;
|
|
3623
|
-
try {
|
|
3624
|
-
const __filename = fileURLToPath2(import.meta.url);
|
|
3625
|
-
const pkgPath = path7.resolve(
|
|
3626
|
-
path7.dirname(__filename),
|
|
3627
|
-
"../../../package.json"
|
|
3628
|
-
);
|
|
3629
|
-
const pkg = JSON.parse(fs7.readFileSync(pkgPath, "utf-8"));
|
|
3630
|
-
cachedVersion = pkg.version ?? "0.0.0-dev";
|
|
3631
|
-
} catch {
|
|
3632
|
-
cachedVersion = "0.0.0-dev";
|
|
3633
|
-
}
|
|
3634
|
-
return cachedVersion;
|
|
3635
|
-
}
|
|
3636
|
-
var ApiServer = class {
|
|
3637
|
-
constructor(core, config, portFilePath, topicManager, secretFilePath, uiDir) {
|
|
3638
|
-
this.core = core;
|
|
3639
|
-
this.config = config;
|
|
3640
|
-
this.topicManager = topicManager;
|
|
3641
|
-
this.portFilePath = portFilePath ?? DEFAULT_PORT_FILE;
|
|
3642
|
-
this.secretFilePath = secretFilePath ?? path7.join(os2.homedir(), ".openacp", "api-secret");
|
|
3643
|
-
this.staticServer = new StaticServer(uiDir);
|
|
3644
|
-
this.sseManager = new SSEManager(
|
|
3645
|
-
core.eventBus,
|
|
3646
|
-
() => {
|
|
3647
|
-
const sessions = this.core.sessionManager.listSessions();
|
|
3648
|
-
return {
|
|
3649
|
-
active: sessions.filter(
|
|
3650
|
-
(s) => s.status === "active" || s.status === "initializing"
|
|
3651
|
-
).length,
|
|
3652
|
-
total: sessions.length
|
|
3653
|
-
};
|
|
3654
|
-
},
|
|
3655
|
-
this.startedAt
|
|
3656
|
-
);
|
|
3657
|
-
this.router = new Router();
|
|
3658
|
-
const deps = {
|
|
3659
|
-
core: this.core,
|
|
3660
|
-
topicManager: this.topicManager,
|
|
3661
|
-
startedAt: this.startedAt,
|
|
3662
|
-
getVersion,
|
|
3663
|
-
sendJson: this.sendJson.bind(this),
|
|
3664
|
-
readBody: this.readBody.bind(this)
|
|
3665
|
-
};
|
|
3666
|
-
registerHealthRoutes(this.router, deps);
|
|
3667
|
-
registerSessionRoutes(this.router, deps);
|
|
3668
|
-
registerConfigRoutes(this.router, deps);
|
|
3669
|
-
registerTopicRoutes(this.router, deps);
|
|
3670
|
-
registerTunnelRoutes(this.router, deps);
|
|
3671
|
-
registerAgentRoutes(this.router, deps);
|
|
3672
|
-
registerNotifyRoutes(this.router, deps);
|
|
3673
|
-
}
|
|
3674
|
-
server = null;
|
|
3675
|
-
actualPort = 0;
|
|
3676
|
-
portFilePath;
|
|
3677
|
-
startedAt = Date.now();
|
|
3678
|
-
secret = "";
|
|
3679
|
-
secretFilePath;
|
|
3680
|
-
sseManager;
|
|
3681
|
-
staticServer;
|
|
3682
|
-
router;
|
|
3683
|
-
async start() {
|
|
3684
|
-
this.loadOrCreateSecret();
|
|
3685
|
-
this.server = http.createServer((req, res) => this.handleRequest(req, res));
|
|
3686
|
-
await new Promise((resolve3, reject) => {
|
|
3687
|
-
this.server.on("error", (err) => {
|
|
3688
|
-
if (err.code === "EADDRINUSE") {
|
|
3689
|
-
log9.warn(
|
|
3690
|
-
{ port: this.config.port },
|
|
3691
|
-
"API port in use, continuing without API server"
|
|
3692
|
-
);
|
|
3693
|
-
this.server = null;
|
|
3694
|
-
resolve3();
|
|
3695
|
-
} else {
|
|
3696
|
-
reject(err);
|
|
3697
|
-
}
|
|
3698
|
-
});
|
|
3699
|
-
this.server.listen(this.config.port, this.config.host, () => {
|
|
3700
|
-
const addr = this.server.address();
|
|
3701
|
-
if (addr && typeof addr === "object") {
|
|
3702
|
-
this.actualPort = addr.port;
|
|
3703
|
-
}
|
|
3704
|
-
this.writePortFile();
|
|
3705
|
-
log9.info(
|
|
3706
|
-
{ host: this.config.host, port: this.actualPort },
|
|
3707
|
-
"API server listening"
|
|
3708
|
-
);
|
|
3709
|
-
this.sseManager.setup();
|
|
3710
|
-
if (this.config.host !== "127.0.0.1" && this.config.host !== "localhost") {
|
|
3711
|
-
log9.warn(
|
|
3712
|
-
"API server binding to non-localhost. Ensure api-secret file is secured."
|
|
3713
|
-
);
|
|
3714
|
-
}
|
|
3715
|
-
resolve3();
|
|
3716
|
-
});
|
|
3717
|
-
});
|
|
3718
|
-
}
|
|
3719
|
-
async stop() {
|
|
3720
|
-
this.sseManager.stop();
|
|
3721
|
-
this.removePortFile();
|
|
3722
|
-
if (this.server) {
|
|
3723
|
-
await new Promise((resolve3) => {
|
|
3724
|
-
this.server.close(() => resolve3());
|
|
3725
|
-
});
|
|
3726
|
-
this.server = null;
|
|
3727
|
-
}
|
|
3728
|
-
}
|
|
3729
|
-
getPort() {
|
|
3730
|
-
return this.actualPort;
|
|
3731
|
-
}
|
|
3732
|
-
getSecret() {
|
|
3733
|
-
return this.secret;
|
|
3734
|
-
}
|
|
3735
|
-
writePortFile() {
|
|
3736
|
-
const dir = path7.dirname(this.portFilePath);
|
|
3737
|
-
fs7.mkdirSync(dir, { recursive: true });
|
|
3738
|
-
fs7.writeFileSync(this.portFilePath, String(this.actualPort));
|
|
3739
|
-
}
|
|
3740
|
-
removePortFile() {
|
|
3741
|
-
try {
|
|
3742
|
-
fs7.unlinkSync(this.portFilePath);
|
|
3743
|
-
} catch {
|
|
3744
|
-
}
|
|
3745
|
-
}
|
|
3746
|
-
loadOrCreateSecret() {
|
|
3747
|
-
const dir = path7.dirname(this.secretFilePath);
|
|
3748
|
-
fs7.mkdirSync(dir, { recursive: true });
|
|
3749
|
-
try {
|
|
3750
|
-
this.secret = fs7.readFileSync(this.secretFilePath, "utf-8").trim();
|
|
3751
|
-
if (this.secret) {
|
|
3752
|
-
try {
|
|
3753
|
-
const stat = fs7.statSync(this.secretFilePath);
|
|
3754
|
-
const mode = stat.mode & 511;
|
|
3755
|
-
if (mode & 63) {
|
|
3756
|
-
log9.warn(
|
|
3757
|
-
{ path: this.secretFilePath, mode: "0" + mode.toString(8) },
|
|
3758
|
-
"API secret file has insecure permissions (should be 0600). Run: chmod 600 %s",
|
|
3759
|
-
this.secretFilePath
|
|
3760
|
-
);
|
|
3761
|
-
}
|
|
3762
|
-
} catch {
|
|
3763
|
-
}
|
|
3764
|
-
return;
|
|
3765
|
-
}
|
|
3766
|
-
} catch {
|
|
3767
|
-
}
|
|
3768
|
-
this.secret = crypto.randomBytes(32).toString("hex");
|
|
3769
|
-
fs7.writeFileSync(this.secretFilePath, this.secret, { mode: 384 });
|
|
3770
|
-
}
|
|
3771
|
-
authenticate(req, allowQueryParam = false) {
|
|
3772
|
-
const authHeader = req.headers.authorization;
|
|
3773
|
-
if (authHeader?.startsWith("Bearer ")) {
|
|
3774
|
-
const token = authHeader.slice(7);
|
|
3775
|
-
if (token.length === this.secret.length && crypto.timingSafeEqual(
|
|
3776
|
-
Buffer.from(token, "utf-8"),
|
|
3777
|
-
Buffer.from(this.secret, "utf-8")
|
|
3778
|
-
)) {
|
|
3779
|
-
return true;
|
|
3780
|
-
}
|
|
3781
|
-
}
|
|
3782
|
-
if (allowQueryParam) {
|
|
3783
|
-
const parsedUrl = new URL(req.url || "", "http://localhost");
|
|
3784
|
-
const qToken = parsedUrl.searchParams.get("token");
|
|
3785
|
-
if (qToken && qToken.length === this.secret.length && crypto.timingSafeEqual(
|
|
3786
|
-
Buffer.from(qToken, "utf-8"),
|
|
3787
|
-
Buffer.from(this.secret, "utf-8")
|
|
3788
|
-
)) {
|
|
3789
|
-
return true;
|
|
3790
|
-
}
|
|
3791
|
-
}
|
|
3792
|
-
return false;
|
|
3793
|
-
}
|
|
3794
|
-
async handleRequest(req, res) {
|
|
3795
|
-
const method = req.method?.toUpperCase();
|
|
3796
|
-
const url = req.url || "";
|
|
3797
|
-
if (url.startsWith("/api/")) {
|
|
3798
|
-
const isExempt = method === "GET" && (url === "/api/health" || url === "/api/version" || url.startsWith("/api/events"));
|
|
3799
|
-
if (!isExempt && !this.authenticate(req)) {
|
|
3800
|
-
this.sendJson(res, 401, { error: "Unauthorized" });
|
|
3801
|
-
return;
|
|
3802
|
-
}
|
|
3803
|
-
}
|
|
3804
|
-
try {
|
|
3805
|
-
if (method === "GET" && url.startsWith("/api/events")) {
|
|
3806
|
-
if (!this.authenticate(req, true)) {
|
|
3807
|
-
this.sendJson(res, 401, { error: "Unauthorized" });
|
|
3808
|
-
return;
|
|
3809
|
-
}
|
|
3810
|
-
this.sseManager.handleRequest(req, res);
|
|
3811
|
-
return;
|
|
3812
|
-
}
|
|
3813
|
-
if (url.startsWith("/api/")) {
|
|
3814
|
-
const match = this.router.match(method, url);
|
|
3815
|
-
if (match) {
|
|
3816
|
-
await match.handler(req, res, match.params);
|
|
3817
|
-
} else {
|
|
3818
|
-
this.sendJson(res, 404, { error: "Not found" });
|
|
3819
|
-
}
|
|
3820
|
-
return;
|
|
3821
|
-
}
|
|
3822
|
-
if (!this.staticServer.serve(req, res)) {
|
|
3823
|
-
this.sendJson(res, 404, { error: "Not found" });
|
|
3824
|
-
}
|
|
3825
|
-
} catch (err) {
|
|
3826
|
-
log9.error({ err }, "API request error");
|
|
3827
|
-
this.sendJson(res, 500, { error: "Internal server error" });
|
|
3828
|
-
}
|
|
3829
|
-
}
|
|
3830
|
-
sendJson(res, status, data) {
|
|
3831
|
-
res.writeHead(status, { "Content-Type": "application/json" });
|
|
3832
|
-
res.end(JSON.stringify(data));
|
|
3833
|
-
}
|
|
3834
|
-
readBody(req) {
|
|
3835
|
-
const MAX_BODY_SIZE = 1024 * 1024;
|
|
3836
|
-
return new Promise((resolve3) => {
|
|
3837
|
-
let data = "";
|
|
3838
|
-
let size = 0;
|
|
3839
|
-
let destroyed = false;
|
|
3840
|
-
req.on("data", (chunk) => {
|
|
3841
|
-
size += chunk.length;
|
|
3842
|
-
if (size > MAX_BODY_SIZE && !destroyed) {
|
|
3843
|
-
destroyed = true;
|
|
3844
|
-
req.destroy();
|
|
3845
|
-
resolve3(null);
|
|
3846
|
-
return;
|
|
3847
|
-
}
|
|
3848
|
-
if (!destroyed) data += chunk;
|
|
3849
|
-
});
|
|
3850
|
-
req.on("end", () => {
|
|
3851
|
-
if (!destroyed) resolve3(data);
|
|
3852
|
-
});
|
|
3853
|
-
req.on("error", () => {
|
|
3854
|
-
if (!destroyed) resolve3("");
|
|
3855
|
-
});
|
|
3856
|
-
});
|
|
3857
|
-
}
|
|
3858
|
-
};
|
|
3859
|
-
|
|
3860
|
-
// src/core/topic-manager.ts
|
|
3861
|
-
var log10 = createChildLogger({ module: "topic-manager" });
|
|
3862
|
-
var TopicManager = class {
|
|
3863
|
-
constructor(sessionManager, adapter, systemTopicIds) {
|
|
3864
|
-
this.sessionManager = sessionManager;
|
|
3865
|
-
this.adapter = adapter;
|
|
3866
|
-
this.systemTopicIds = systemTopicIds;
|
|
3867
|
-
}
|
|
3868
|
-
listTopics(filter) {
|
|
3869
|
-
const records = this.sessionManager.listRecords(filter);
|
|
3870
|
-
return records.filter((r) => !this.isSystemTopic(r)).filter((r) => !filter?.statuses?.length || filter.statuses.includes(r.status)).map((r) => ({
|
|
3871
|
-
sessionId: r.sessionId,
|
|
3872
|
-
topicId: r.platform?.topicId ?? null,
|
|
3873
|
-
name: r.name ?? null,
|
|
3874
|
-
status: r.status,
|
|
3875
|
-
agentName: r.agentName,
|
|
3876
|
-
lastActiveAt: r.lastActiveAt
|
|
3877
|
-
}));
|
|
3878
|
-
}
|
|
3879
|
-
async deleteTopic(sessionId, options) {
|
|
3880
|
-
const records = this.sessionManager.listRecords();
|
|
3881
|
-
const record = records.find((r) => r.sessionId === sessionId);
|
|
3882
|
-
if (!record) return { ok: false, error: "Session not found" };
|
|
3883
|
-
if (this.isSystemTopic(record)) return { ok: false, error: "Cannot delete system topic" };
|
|
3884
|
-
const isActive = record.status === "active" || record.status === "initializing";
|
|
3885
|
-
if (isActive && !options?.confirmed) {
|
|
3886
|
-
return {
|
|
3887
|
-
ok: false,
|
|
3888
|
-
needsConfirmation: true,
|
|
3889
|
-
session: { id: record.sessionId, name: record.name ?? null, status: record.status }
|
|
3890
|
-
};
|
|
3891
|
-
}
|
|
3892
|
-
if (isActive) {
|
|
3893
|
-
await this.sessionManager.cancelSession(sessionId);
|
|
3894
|
-
}
|
|
3895
|
-
const topicId = record.platform?.topicId ?? null;
|
|
3896
|
-
if (this.adapter && topicId) {
|
|
3897
|
-
try {
|
|
3898
|
-
await this.adapter.deleteSessionThread(sessionId);
|
|
3899
|
-
} catch (err) {
|
|
3900
|
-
log10.warn({ err, sessionId, topicId }, "Failed to delete platform thread, removing record anyway");
|
|
3901
|
-
}
|
|
3902
|
-
}
|
|
3903
|
-
await this.sessionManager.removeRecord(sessionId);
|
|
3904
|
-
return { ok: true, topicId };
|
|
3905
|
-
}
|
|
3906
|
-
async cleanup(statuses) {
|
|
3907
|
-
const targetStatuses = statuses?.length ? statuses : ["finished", "error", "cancelled"];
|
|
3908
|
-
const records = this.sessionManager.listRecords({ statuses: targetStatuses });
|
|
3909
|
-
const targets = records.filter((r) => !this.isSystemTopic(r)).filter((r) => targetStatuses.includes(r.status));
|
|
3910
|
-
const deleted = [];
|
|
3911
|
-
const failed = [];
|
|
3912
|
-
for (const record of targets) {
|
|
3913
|
-
try {
|
|
3914
|
-
const isActive = record.status === "active" || record.status === "initializing";
|
|
3915
|
-
if (isActive) {
|
|
3916
|
-
await this.sessionManager.cancelSession(record.sessionId);
|
|
3917
|
-
}
|
|
3918
|
-
const topicId = record.platform?.topicId;
|
|
3919
|
-
if (this.adapter && topicId) {
|
|
3920
|
-
try {
|
|
3921
|
-
await this.adapter.deleteSessionThread(record.sessionId);
|
|
3922
|
-
} catch (err) {
|
|
3923
|
-
log10.warn({ err, sessionId: record.sessionId }, "Failed to delete platform thread during cleanup");
|
|
3924
|
-
}
|
|
3925
|
-
}
|
|
3926
|
-
await this.sessionManager.removeRecord(record.sessionId);
|
|
3927
|
-
deleted.push(record.sessionId);
|
|
3928
|
-
} catch (err) {
|
|
3929
|
-
failed.push({ sessionId: record.sessionId, error: err instanceof Error ? err.message : String(err) });
|
|
3930
|
-
}
|
|
3931
|
-
}
|
|
3932
|
-
return { deleted, failed };
|
|
3933
|
-
}
|
|
3934
|
-
isSystemTopic(record) {
|
|
3935
|
-
const topicId = record.platform?.topicId;
|
|
3936
|
-
if (!topicId) return false;
|
|
3937
|
-
return topicId === this.systemTopicIds.notificationTopicId || topicId === this.systemTopicIds.assistantTopicId;
|
|
3938
|
-
}
|
|
3939
|
-
};
|
|
3940
|
-
|
|
3941
32
|
// src/adapters/telegram/adapter.ts
|
|
3942
33
|
import { Bot, InputFile } from "grammy";
|
|
3943
34
|
|
|
@@ -4173,14 +264,14 @@ function splitMessage(text, maxLength = 3800) {
|
|
|
4173
264
|
|
|
4174
265
|
// src/adapters/telegram/commands/admin.ts
|
|
4175
266
|
import { InlineKeyboard } from "grammy";
|
|
4176
|
-
var
|
|
267
|
+
var log = createChildLogger({ module: "telegram-cmd-admin" });
|
|
4177
268
|
function setupDangerousModeCallbacks(bot, core) {
|
|
4178
269
|
bot.callbackQuery(/^d:/, async (ctx) => {
|
|
4179
270
|
const sessionId = ctx.callbackQuery.data.slice(2);
|
|
4180
271
|
const session = core.sessionManager.getSession(sessionId);
|
|
4181
272
|
if (session) {
|
|
4182
273
|
session.dangerousMode = !session.dangerousMode;
|
|
4183
|
-
|
|
274
|
+
log.info({ sessionId, dangerousMode: session.dangerousMode }, "Dangerous mode toggled via button");
|
|
4184
275
|
core.sessionManager.patchRecord(sessionId, { dangerousMode: session.dangerousMode }).catch(() => {
|
|
4185
276
|
});
|
|
4186
277
|
const toastText2 = session.dangerousMode ? "\u2620\uFE0F Dangerous mode enabled \u2014 permissions auto-approved" : "\u{1F510} Dangerous mode disabled \u2014 permissions shown normally";
|
|
@@ -4207,7 +298,7 @@ function setupDangerousModeCallbacks(bot, core) {
|
|
|
4207
298
|
const newDangerousMode = !(record.dangerousMode ?? false);
|
|
4208
299
|
core.sessionManager.patchRecord(sessionId, { dangerousMode: newDangerousMode }).catch(() => {
|
|
4209
300
|
});
|
|
4210
|
-
|
|
301
|
+
log.info({ sessionId, dangerousMode: newDangerousMode }, "Dangerous mode toggled via button (store-only, session not in memory)");
|
|
4211
302
|
const toastText = newDangerousMode ? "\u2620\uFE0F Dangerous mode enabled \u2014 permissions auto-approved" : "\u{1F510} Dangerous mode disabled \u2014 permissions shown normally";
|
|
4212
303
|
try {
|
|
4213
304
|
await ctx.answerCallbackQuery({ text: toastText });
|
|
@@ -4395,7 +486,7 @@ async function handleRestart(ctx, core) {
|
|
|
4395
486
|
}
|
|
4396
487
|
|
|
4397
488
|
// src/adapters/telegram/commands/new-session.ts
|
|
4398
|
-
var
|
|
489
|
+
var log2 = createChildLogger({ module: "telegram-cmd-new-session" });
|
|
4399
490
|
var pendingNewSessions = /* @__PURE__ */ new Map();
|
|
4400
491
|
var PENDING_TIMEOUT_MS = 5 * 60 * 1e3;
|
|
4401
492
|
function cleanupPending(userId) {
|
|
@@ -4494,7 +585,7 @@ async function startConfirmStep(ctx, chatId, userId, agentName, workspace) {
|
|
|
4494
585
|
});
|
|
4495
586
|
}
|
|
4496
587
|
async function createSessionDirect(ctx, core, chatId, agentName, workspace) {
|
|
4497
|
-
|
|
588
|
+
log2.info({ userId: ctx.from?.id, agentName, workspace }, "New session command (direct)");
|
|
4498
589
|
let threadId;
|
|
4499
590
|
try {
|
|
4500
591
|
const topicName = `\u{1F504} New Session`;
|
|
@@ -4524,10 +615,10 @@ This is your coding session \u2014 chat here to work with the agent.`,
|
|
|
4524
615
|
reply_markup: buildSessionControlKeyboard(session.id, false, false)
|
|
4525
616
|
}
|
|
4526
617
|
);
|
|
4527
|
-
session.warmup().catch((err) =>
|
|
618
|
+
session.warmup().catch((err) => log2.error({ err }, "Warm-up error"));
|
|
4528
619
|
return threadId ?? null;
|
|
4529
620
|
} catch (err) {
|
|
4530
|
-
|
|
621
|
+
log2.error({ err }, "Session creation failed");
|
|
4531
622
|
if (threadId) {
|
|
4532
623
|
try {
|
|
4533
624
|
await ctx.api.deleteForumTopic(chatId, threadId);
|
|
@@ -4605,7 +696,7 @@ async function handleNewChat(ctx, core, chatId) {
|
|
|
4605
696
|
reply_markup: buildSessionControlKeyboard(session.id, false, false)
|
|
4606
697
|
}
|
|
4607
698
|
);
|
|
4608
|
-
session.warmup().catch((err) =>
|
|
699
|
+
session.warmup().catch((err) => log2.error({ err }, "Warm-up error"));
|
|
4609
700
|
} catch (err) {
|
|
4610
701
|
if (newThreadId) {
|
|
4611
702
|
try {
|
|
@@ -4636,7 +727,7 @@ async function executeNewSession(bot, core, chatId, agentName, workspace) {
|
|
|
4636
727
|
} });
|
|
4637
728
|
const finalName = `\u{1F504} ${session.agentName} \u2014 New Session`;
|
|
4638
729
|
await renameSessionTopic(bot, chatId, threadId, finalName);
|
|
4639
|
-
session.warmup().catch((err) =>
|
|
730
|
+
session.warmup().catch((err) => log2.error({ err }, "Warm-up error"));
|
|
4640
731
|
return { session, threadId, firstMsgId };
|
|
4641
732
|
} catch (err) {
|
|
4642
733
|
try {
|
|
@@ -4784,7 +875,7 @@ Or just the folder name like <code>my-project</code> (will use ${core.configMana
|
|
|
4784
875
|
|
|
4785
876
|
// src/adapters/telegram/commands/session.ts
|
|
4786
877
|
import { InlineKeyboard as InlineKeyboard3 } from "grammy";
|
|
4787
|
-
var
|
|
878
|
+
var log3 = createChildLogger({ module: "telegram-cmd-session" });
|
|
4788
879
|
async function handleCancel(ctx, core, assistant) {
|
|
4789
880
|
const threadId = ctx.message?.message_thread_id;
|
|
4790
881
|
if (!threadId) return;
|
|
@@ -4802,14 +893,14 @@ async function handleCancel(ctx, core, assistant) {
|
|
|
4802
893
|
String(threadId)
|
|
4803
894
|
);
|
|
4804
895
|
if (session) {
|
|
4805
|
-
|
|
896
|
+
log3.info({ sessionId: session.id }, "Abort prompt command");
|
|
4806
897
|
await session.abortPrompt();
|
|
4807
898
|
await ctx.reply("\u26D4 Prompt aborted. Session is still active \u2014 send a new message to continue.", { parse_mode: "HTML" });
|
|
4808
899
|
return;
|
|
4809
900
|
}
|
|
4810
901
|
const record = core.sessionManager.getRecordByThread("telegram", String(threadId));
|
|
4811
902
|
if (record && record.status !== "error") {
|
|
4812
|
-
|
|
903
|
+
log3.info({ sessionId: record.sessionId, status: record.status }, "Cancel command \u2014 no active prompt to abort");
|
|
4813
904
|
await ctx.reply("\u2139\uFE0F No active prompt to cancel. Send a new message to resume the session.", { parse_mode: "HTML" });
|
|
4814
905
|
}
|
|
4815
906
|
}
|
|
@@ -4913,7 +1004,7 @@ ${lines.join("\n")}${truncated}`,
|
|
|
4913
1004
|
{ parse_mode: "HTML", reply_markup: keyboard }
|
|
4914
1005
|
);
|
|
4915
1006
|
} catch (err) {
|
|
4916
|
-
|
|
1007
|
+
log3.error({ err }, "handleTopics error");
|
|
4917
1008
|
await ctx.reply("\u274C Failed to list sessions.", { parse_mode: "HTML" }).catch(() => {
|
|
4918
1009
|
});
|
|
4919
1010
|
}
|
|
@@ -4934,13 +1025,13 @@ async function handleCleanup(ctx, core, chatId, statuses) {
|
|
|
4934
1025
|
try {
|
|
4935
1026
|
await ctx.api.deleteForumTopic(chatId, topicId);
|
|
4936
1027
|
} catch (err) {
|
|
4937
|
-
|
|
1028
|
+
log3.warn({ err, sessionId: record.sessionId, topicId }, "Failed to delete forum topic during cleanup");
|
|
4938
1029
|
}
|
|
4939
1030
|
}
|
|
4940
1031
|
await core.sessionManager.removeRecord(record.sessionId);
|
|
4941
1032
|
deleted++;
|
|
4942
1033
|
} catch (err) {
|
|
4943
|
-
|
|
1034
|
+
log3.error({ err, sessionId: record.sessionId }, "Failed to cleanup session");
|
|
4944
1035
|
failed++;
|
|
4945
1036
|
}
|
|
4946
1037
|
}
|
|
@@ -5011,7 +1102,7 @@ async function handleCleanupEverythingConfirmed(ctx, core, chatId, systemTopicId
|
|
|
5011
1102
|
try {
|
|
5012
1103
|
await core.sessionManager.cancelSession(record.sessionId);
|
|
5013
1104
|
} catch (err) {
|
|
5014
|
-
|
|
1105
|
+
log3.warn({ err, sessionId: record.sessionId }, "Failed to cancel session during cleanup");
|
|
5015
1106
|
}
|
|
5016
1107
|
}
|
|
5017
1108
|
const topicId = record.platform?.topicId;
|
|
@@ -5019,13 +1110,13 @@ async function handleCleanupEverythingConfirmed(ctx, core, chatId, systemTopicId
|
|
|
5019
1110
|
try {
|
|
5020
1111
|
await ctx.api.deleteForumTopic(chatId, topicId);
|
|
5021
1112
|
} catch (err) {
|
|
5022
|
-
|
|
1113
|
+
log3.warn({ err, sessionId: record.sessionId, topicId }, "Failed to delete forum topic during cleanup");
|
|
5023
1114
|
}
|
|
5024
1115
|
}
|
|
5025
1116
|
await core.sessionManager.removeRecord(record.sessionId);
|
|
5026
1117
|
deleted++;
|
|
5027
1118
|
} catch (err) {
|
|
5028
|
-
|
|
1119
|
+
log3.error({ err, sessionId: record.sessionId }, "Failed to cleanup session");
|
|
5029
1120
|
failed++;
|
|
5030
1121
|
}
|
|
5031
1122
|
}
|
|
@@ -5347,8 +1438,8 @@ Downloading... ${bar} ${percent}%`, { parse_mode: "HTML" });
|
|
|
5347
1438
|
};
|
|
5348
1439
|
const result = await catalog.install(nameOrId, progress);
|
|
5349
1440
|
if (result.ok) {
|
|
5350
|
-
const { getAgentCapabilities
|
|
5351
|
-
const caps =
|
|
1441
|
+
const { getAgentCapabilities } = await import("./agent-dependencies-QY5QSULV.js");
|
|
1442
|
+
const caps = getAgentCapabilities(result.agentKey);
|
|
5352
1443
|
if (caps.integration) {
|
|
5353
1444
|
const { installIntegration } = await import("./integrate-O4OCR4SN.js");
|
|
5354
1445
|
const intResult = await installIntegration(result.agentKey, caps.integration);
|
|
@@ -5535,13 +1626,290 @@ ${resultText}`,
|
|
|
5535
1626
|
});
|
|
5536
1627
|
}
|
|
5537
1628
|
|
|
5538
|
-
// src/adapters/telegram/commands/
|
|
1629
|
+
// src/adapters/telegram/commands/resume.ts
|
|
1630
|
+
import * as fs from "fs";
|
|
1631
|
+
import * as path from "path";
|
|
1632
|
+
import * as os from "os";
|
|
5539
1633
|
import { InlineKeyboard as InlineKeyboard6 } from "grammy";
|
|
5540
|
-
var
|
|
1634
|
+
var log4 = createChildLogger({ module: "telegram-cmd-resume" });
|
|
1635
|
+
var PENDING_TIMEOUT_MS2 = 5 * 60 * 1e3;
|
|
1636
|
+
function botFromCtx2(ctx) {
|
|
1637
|
+
return { api: ctx.api };
|
|
1638
|
+
}
|
|
1639
|
+
var pendingResumes = /* @__PURE__ */ new Map();
|
|
1640
|
+
function cleanupPending2(userId) {
|
|
1641
|
+
const pending = pendingResumes.get(userId);
|
|
1642
|
+
if (pending) {
|
|
1643
|
+
clearTimeout(pending.timer);
|
|
1644
|
+
pendingResumes.delete(userId);
|
|
1645
|
+
}
|
|
1646
|
+
}
|
|
1647
|
+
function parseResumeArgs(matchStr) {
|
|
1648
|
+
const args = matchStr.split(" ").filter(Boolean);
|
|
1649
|
+
if (args.length === 0) return { query: { type: "latest", value: "5" } };
|
|
1650
|
+
const first = args[0];
|
|
1651
|
+
if (first === "pr") return args[1] ? { query: { type: "pr", value: args[1] } } : null;
|
|
1652
|
+
if (first === "branch") return args[1] ? { query: { type: "branch", value: args[1] } } : null;
|
|
1653
|
+
if (first === "commit") return args[1] ? { query: { type: "commit", value: args[1] } } : null;
|
|
1654
|
+
if (CheckpointReader.isCheckpointId(first)) return { query: { type: "checkpoint", value: first } };
|
|
1655
|
+
if (CheckpointReader.isSessionId(first)) return { query: { type: "session", value: first } };
|
|
1656
|
+
if (first.includes("/pull/")) {
|
|
1657
|
+
const prMatch = first.match(/\/pull\/(\d+)/);
|
|
1658
|
+
return prMatch ? { query: { type: "pr", value: prMatch[1] } } : null;
|
|
1659
|
+
}
|
|
1660
|
+
const ghCommitMatch = first.match(/github\.com\/[^/]+\/[^/]+\/commit\/([0-9a-f]+)/);
|
|
1661
|
+
if (ghCommitMatch) return { query: { type: "commit", value: ghCommitMatch[1] } };
|
|
1662
|
+
const ghBranchMatch = first.match(/github\.com\/[^/]+\/[^/]+\/tree\/(.+?)(?:\?|#|$)/);
|
|
1663
|
+
if (ghBranchMatch) return { query: { type: "branch", value: ghBranchMatch[1] } };
|
|
1664
|
+
const ghCompareMatch = first.match(/github\.com\/[^/]+\/[^/]+\/compare\/(?:[^.]+\.{2,3})(.+?)(?:\?|#|$)/);
|
|
1665
|
+
if (ghCompareMatch) return { query: { type: "branch", value: ghCompareMatch[1] } };
|
|
1666
|
+
if (first.match(/github\.com\/[^/]+\/[^/]+\/?$/) && !first.includes("/tree/") && !first.includes("/pull/") && !first.includes("/commit/") && !first.includes("/compare/")) {
|
|
1667
|
+
return { query: { type: "latest", value: "5" } };
|
|
1668
|
+
}
|
|
1669
|
+
const entireCheckpointMatch = first.match(/entire\.io\/gh\/[^/]+\/[^/]+\/checkpoints\/[^/]+\/([0-9a-f]{12})/);
|
|
1670
|
+
if (entireCheckpointMatch) return { query: { type: "checkpoint", value: entireCheckpointMatch[1] } };
|
|
1671
|
+
const entireCommitMatch = first.match(/entire\.io\/gh\/[^/]+\/[^/]+\/commit\/([0-9a-f]+)/);
|
|
1672
|
+
if (entireCommitMatch) return { query: { type: "commit", value: entireCommitMatch[1] } };
|
|
1673
|
+
return { query: { type: "latest", value: "5" } };
|
|
1674
|
+
}
|
|
1675
|
+
function looksLikePath(text) {
|
|
1676
|
+
return text.startsWith("/") || text.startsWith("~") || text.startsWith(".");
|
|
1677
|
+
}
|
|
1678
|
+
function listWorkspaceDirs(baseDir, maxItems = 10) {
|
|
1679
|
+
const resolved = baseDir.replace(/^~/, os.homedir());
|
|
1680
|
+
try {
|
|
1681
|
+
if (!fs.existsSync(resolved)) return [];
|
|
1682
|
+
return fs.readdirSync(resolved, { withFileTypes: true }).filter((d) => d.isDirectory() && !d.name.startsWith(".")).map((d) => d.name).sort().slice(0, maxItems);
|
|
1683
|
+
} catch {
|
|
1684
|
+
return [];
|
|
1685
|
+
}
|
|
1686
|
+
}
|
|
1687
|
+
async function showWorkspacePicker(ctx, core, chatId, userId, query) {
|
|
1688
|
+
const config = core.configManager.get();
|
|
1689
|
+
const baseDir = config.workspace.baseDir;
|
|
1690
|
+
const resolvedBase = baseDir.replace(/^~/, os.homedir());
|
|
1691
|
+
const subdirs = listWorkspaceDirs(baseDir);
|
|
1692
|
+
const keyboard = new InlineKeyboard6();
|
|
1693
|
+
for (const dir of subdirs) {
|
|
1694
|
+
const fullPath = path.join(resolvedBase, dir);
|
|
1695
|
+
keyboard.text(`\u{1F4C1} ${dir}`, `m:resume:ws:${dir}`).row();
|
|
1696
|
+
}
|
|
1697
|
+
keyboard.text(`\u{1F4C1} Use ${baseDir}`, "m:resume:ws:default").row();
|
|
1698
|
+
keyboard.text("\u270F\uFE0F Enter project path", "m:resume:ws:custom");
|
|
1699
|
+
const queryLabel = query.type === "latest" ? "latest sessions" : `${query.type}: ${query.value}`;
|
|
1700
|
+
const text = `\u{1F4C1} <b>Select project directory for resume</b>
|
|
1701
|
+
|
|
1702
|
+
Query: <code>${escapeHtml(queryLabel)}</code>
|
|
1703
|
+
|
|
1704
|
+
Choose the repo that has Entire checkpoints enabled:`;
|
|
1705
|
+
const msg = await ctx.reply(text, { parse_mode: "HTML", reply_markup: keyboard });
|
|
1706
|
+
cleanupPending2(userId);
|
|
1707
|
+
pendingResumes.set(userId, {
|
|
1708
|
+
query,
|
|
1709
|
+
step: "workspace",
|
|
1710
|
+
messageId: msg.message_id,
|
|
1711
|
+
threadId: ctx.message?.message_thread_id,
|
|
1712
|
+
timer: setTimeout(() => pendingResumes.delete(userId), PENDING_TIMEOUT_MS2)
|
|
1713
|
+
});
|
|
1714
|
+
}
|
|
1715
|
+
async function executeResume(ctx, core, chatId, query, repoPath) {
|
|
1716
|
+
const provider = await core.contextManager.getProvider(repoPath);
|
|
1717
|
+
if (!provider) {
|
|
1718
|
+
await ctx.reply(
|
|
1719
|
+
`\u26A0\uFE0F <b>Entire not enabled in <code>${escapeHtml(repoPath)}</code></b>
|
|
1720
|
+
|
|
1721
|
+
To enable conversation history tracking:
|
|
1722
|
+
<code>cd ${escapeHtml(repoPath)} && npx entire enable</code>
|
|
1723
|
+
|
|
1724
|
+
Learn more: https://docs.entire.io/getting-started`,
|
|
1725
|
+
{ parse_mode: "HTML" }
|
|
1726
|
+
);
|
|
1727
|
+
return;
|
|
1728
|
+
}
|
|
1729
|
+
const fullQuery = { ...query, repoPath };
|
|
1730
|
+
await ctx.reply(`\u{1F50D} Scanning ${query.type === "latest" ? "latest sessions" : `${query.type}: ${escapeHtml(query.value)}`}...`, { parse_mode: "HTML" });
|
|
1731
|
+
const listResult = await core.contextManager.listSessions(fullQuery);
|
|
1732
|
+
if (!listResult || listResult.sessions.length === 0) {
|
|
1733
|
+
await ctx.reply(
|
|
1734
|
+
`\u{1F50D} <b>No sessions found</b>
|
|
1735
|
+
|
|
1736
|
+
Query: <code>${escapeHtml(query.type)}: ${escapeHtml(query.value)}</code>
|
|
1737
|
+
Repo: <code>${escapeHtml(repoPath)}</code>`,
|
|
1738
|
+
{ parse_mode: "HTML" }
|
|
1739
|
+
);
|
|
1740
|
+
return;
|
|
1741
|
+
}
|
|
1742
|
+
const config = core.configManager.get();
|
|
1743
|
+
const agentName = config.defaultAgent;
|
|
1744
|
+
let threadId;
|
|
1745
|
+
try {
|
|
1746
|
+
const queryLabel = query.type === "latest" ? "latest" : `${query.type}: ${query.value.slice(0, 20)}`;
|
|
1747
|
+
const topicName = `\u{1F4DC} Resume \u2014 ${queryLabel}`;
|
|
1748
|
+
threadId = await createSessionTopic(botFromCtx2(ctx), chatId, topicName);
|
|
1749
|
+
await ctx.api.sendMessage(chatId, `\u23F3 Loading context and starting session...`, {
|
|
1750
|
+
message_thread_id: threadId,
|
|
1751
|
+
parse_mode: "HTML"
|
|
1752
|
+
});
|
|
1753
|
+
const { session, contextResult } = await core.createSessionWithContext({
|
|
1754
|
+
channelId: "telegram",
|
|
1755
|
+
agentName,
|
|
1756
|
+
workingDirectory: repoPath,
|
|
1757
|
+
contextQuery: fullQuery,
|
|
1758
|
+
contextOptions: { maxTokens: DEFAULT_MAX_TOKENS }
|
|
1759
|
+
});
|
|
1760
|
+
session.threadId = String(threadId);
|
|
1761
|
+
await core.sessionManager.patchRecord(session.id, { platform: { topicId: threadId } });
|
|
1762
|
+
const sessionCount = contextResult?.sessionCount ?? listResult.sessions.length;
|
|
1763
|
+
const mode = contextResult?.mode ?? "full";
|
|
1764
|
+
const tokens = contextResult?.tokenEstimate ?? listResult.estimatedTokens;
|
|
1765
|
+
const topicLink = buildDeepLink(chatId, threadId);
|
|
1766
|
+
const replyTarget = ctx.message?.message_thread_id;
|
|
1767
|
+
if (replyTarget !== threadId) {
|
|
1768
|
+
await ctx.reply(
|
|
1769
|
+
`\u2705 Session resumed \u2192 <a href="${topicLink}">Open topic</a>`,
|
|
1770
|
+
{ parse_mode: "HTML" }
|
|
1771
|
+
);
|
|
1772
|
+
}
|
|
1773
|
+
await ctx.api.sendMessage(
|
|
1774
|
+
chatId,
|
|
1775
|
+
`\u2705 <b>Session resumed with context</b>
|
|
1776
|
+
<b>Agent:</b> ${escapeHtml(session.agentName)}
|
|
1777
|
+
<b>Workspace:</b> <code>${escapeHtml(session.workingDirectory)}</code>
|
|
1778
|
+
<b>Sessions loaded:</b> ${sessionCount}
|
|
1779
|
+
<b>Mode:</b> ${escapeHtml(mode)}
|
|
1780
|
+
<b>~Tokens:</b> ${tokens.toLocaleString()}
|
|
1781
|
+
|
|
1782
|
+
Context is ready \u2014 chat here to continue working with the agent.`,
|
|
1783
|
+
{
|
|
1784
|
+
message_thread_id: threadId,
|
|
1785
|
+
parse_mode: "HTML",
|
|
1786
|
+
reply_markup: buildSessionControlKeyboard(session.id, false, false)
|
|
1787
|
+
}
|
|
1788
|
+
);
|
|
1789
|
+
session.warmup().catch((err) => log4.error({ err }, "Warm-up error"));
|
|
1790
|
+
} catch (err) {
|
|
1791
|
+
log4.error({ err }, "Resume session creation failed");
|
|
1792
|
+
if (threadId) {
|
|
1793
|
+
try {
|
|
1794
|
+
await ctx.api.deleteForumTopic(chatId, threadId);
|
|
1795
|
+
} catch {
|
|
1796
|
+
}
|
|
1797
|
+
}
|
|
1798
|
+
const message = err instanceof Error ? err.message : typeof err === "object" ? JSON.stringify(err) : String(err);
|
|
1799
|
+
await ctx.reply(`\u274C ${escapeHtml(message)}`, { parse_mode: "HTML" });
|
|
1800
|
+
}
|
|
1801
|
+
}
|
|
1802
|
+
async function handleResume(ctx, core, chatId, assistant) {
|
|
1803
|
+
const rawMatch = ctx.match;
|
|
1804
|
+
const matchStr = typeof rawMatch === "string" ? rawMatch : "";
|
|
1805
|
+
const parsed = parseResumeArgs(matchStr);
|
|
1806
|
+
if (!parsed) {
|
|
1807
|
+
await ctx.reply(
|
|
1808
|
+
`\u274C <b>Invalid arguments.</b>
|
|
1809
|
+
|
|
1810
|
+
Usage examples:
|
|
1811
|
+
\u2022 <code>/resume</code> \u2014 latest 5 sessions
|
|
1812
|
+
\u2022 <code>/resume pr 19</code>
|
|
1813
|
+
\u2022 <code>/resume branch main</code>
|
|
1814
|
+
\u2022 <code>/resume commit e0dd2fa4</code>
|
|
1815
|
+
\u2022 <code>/resume f634acf05138</code> \u2014 checkpoint ID
|
|
1816
|
+
\u2022 <code>/resume https://entire.io/gh/.../checkpoints/.../2e884e2c402a</code>
|
|
1817
|
+
\u2022 <code>/resume https://entire.io/gh/.../commit/e0dd2fa4...</code>`,
|
|
1818
|
+
{ parse_mode: "HTML" }
|
|
1819
|
+
);
|
|
1820
|
+
return;
|
|
1821
|
+
}
|
|
1822
|
+
const { query } = parsed;
|
|
1823
|
+
const userId = ctx.from?.id;
|
|
1824
|
+
if (!userId) return;
|
|
1825
|
+
await showWorkspacePicker(ctx, core, chatId, userId, query);
|
|
1826
|
+
}
|
|
1827
|
+
async function handlePendingResumeInput(ctx, core, chatId, assistantTopicId) {
|
|
1828
|
+
const userId = ctx.from?.id;
|
|
1829
|
+
if (!userId) return false;
|
|
1830
|
+
const pending = pendingResumes.get(userId);
|
|
1831
|
+
if (!pending || !ctx.message?.text) return false;
|
|
1832
|
+
if (pending.step !== "workspace_input" && pending.step !== "workspace") return false;
|
|
1833
|
+
const threadId = ctx.message.message_thread_id;
|
|
1834
|
+
if (threadId && threadId !== assistantTopicId) return false;
|
|
1835
|
+
if (pending.step === "workspace" && !looksLikePath(ctx.message.text.trim())) return false;
|
|
1836
|
+
let workspace = ctx.message.text.trim();
|
|
1837
|
+
if (!workspace) {
|
|
1838
|
+
await ctx.reply("\u26A0\uFE0F Please enter a valid directory path.", { parse_mode: "HTML" });
|
|
1839
|
+
return true;
|
|
1840
|
+
}
|
|
1841
|
+
if (!workspace.startsWith("/") && !workspace.startsWith("~")) {
|
|
1842
|
+
const baseDir = core.configManager.get().workspace.baseDir;
|
|
1843
|
+
workspace = `${baseDir.replace(/\/$/, "")}/${workspace}`;
|
|
1844
|
+
}
|
|
1845
|
+
const resolved = core.configManager.resolveWorkspace(workspace);
|
|
1846
|
+
cleanupPending2(userId);
|
|
1847
|
+
await executeResume(ctx, core, chatId, pending.query, resolved);
|
|
1848
|
+
return true;
|
|
1849
|
+
}
|
|
1850
|
+
function setupResumeCallbacks(bot, core, chatId) {
|
|
1851
|
+
bot.callbackQuery(/^m:resume:/, async (ctx) => {
|
|
1852
|
+
const data = ctx.callbackQuery.data;
|
|
1853
|
+
const userId = ctx.from?.id;
|
|
1854
|
+
if (!userId) return;
|
|
1855
|
+
try {
|
|
1856
|
+
await ctx.answerCallbackQuery();
|
|
1857
|
+
} catch {
|
|
1858
|
+
}
|
|
1859
|
+
const pending = pendingResumes.get(userId);
|
|
1860
|
+
if (!pending) return;
|
|
1861
|
+
if (data === "m:resume:ws:default") {
|
|
1862
|
+
const baseDir = core.configManager.get().workspace.baseDir;
|
|
1863
|
+
const resolved = core.configManager.resolveWorkspace(baseDir);
|
|
1864
|
+
cleanupPending2(userId);
|
|
1865
|
+
try {
|
|
1866
|
+
await ctx.api.editMessageText(chatId, pending.messageId, `\u23F3 Using <code>${escapeHtml(resolved)}</code>...`, { parse_mode: "HTML" });
|
|
1867
|
+
} catch {
|
|
1868
|
+
}
|
|
1869
|
+
await executeResume(ctx, core, chatId, pending.query, resolved);
|
|
1870
|
+
return;
|
|
1871
|
+
}
|
|
1872
|
+
if (data === "m:resume:ws:custom") {
|
|
1873
|
+
try {
|
|
1874
|
+
await ctx.api.editMessageText(
|
|
1875
|
+
chatId,
|
|
1876
|
+
pending.messageId,
|
|
1877
|
+
`\u270F\uFE0F <b>Enter project path:</b>
|
|
1878
|
+
|
|
1879
|
+
Full path like <code>~/code/my-project</code>
|
|
1880
|
+
Or just the folder name (will use workspace baseDir)`,
|
|
1881
|
+
{ parse_mode: "HTML" }
|
|
1882
|
+
);
|
|
1883
|
+
} catch {
|
|
1884
|
+
await ctx.reply(`\u270F\uFE0F <b>Enter project path:</b>`, { parse_mode: "HTML" });
|
|
1885
|
+
}
|
|
1886
|
+
clearTimeout(pending.timer);
|
|
1887
|
+
pending.step = "workspace_input";
|
|
1888
|
+
pending.timer = setTimeout(() => pendingResumes.delete(userId), PENDING_TIMEOUT_MS2);
|
|
1889
|
+
return;
|
|
1890
|
+
}
|
|
1891
|
+
if (data.startsWith("m:resume:ws:")) {
|
|
1892
|
+
const dirName = data.replace("m:resume:ws:", "");
|
|
1893
|
+
const baseDir = core.configManager.get().workspace.baseDir;
|
|
1894
|
+
const resolved = core.configManager.resolveWorkspace(path.join(baseDir.replace(/^~/, os.homedir()), dirName));
|
|
1895
|
+
cleanupPending2(userId);
|
|
1896
|
+
try {
|
|
1897
|
+
await ctx.api.editMessageText(chatId, pending.messageId, `\u23F3 Using <code>${escapeHtml(resolved)}</code>...`, { parse_mode: "HTML" });
|
|
1898
|
+
} catch {
|
|
1899
|
+
}
|
|
1900
|
+
await executeResume(ctx, core, chatId, pending.query, resolved);
|
|
1901
|
+
return;
|
|
1902
|
+
}
|
|
1903
|
+
});
|
|
1904
|
+
}
|
|
1905
|
+
|
|
1906
|
+
// src/adapters/telegram/commands/settings.ts
|
|
1907
|
+
import { InlineKeyboard as InlineKeyboard7 } from "grammy";
|
|
1908
|
+
var log5 = createChildLogger({ module: "telegram-settings" });
|
|
5541
1909
|
function buildSettingsKeyboard(core) {
|
|
5542
1910
|
const config = core.configManager.get();
|
|
5543
1911
|
const fields = getSafeFields();
|
|
5544
|
-
const kb = new
|
|
1912
|
+
const kb = new InlineKeyboard7();
|
|
5545
1913
|
for (const field of fields) {
|
|
5546
1914
|
const value = getConfigValue(config, field.path);
|
|
5547
1915
|
const label = formatFieldLabel(field, value);
|
|
@@ -5600,7 +1968,7 @@ function setupSettingsCallbacks(bot, core, getAssistantSession) {
|
|
|
5600
1968
|
} catch {
|
|
5601
1969
|
}
|
|
5602
1970
|
} catch (err) {
|
|
5603
|
-
|
|
1971
|
+
log5.error({ err, fieldPath }, "Failed to toggle config");
|
|
5604
1972
|
try {
|
|
5605
1973
|
await ctx.answerCallbackQuery({ text: "\u274C Failed to update" });
|
|
5606
1974
|
} catch {
|
|
@@ -5614,7 +1982,7 @@ function setupSettingsCallbacks(bot, core, getAssistantSession) {
|
|
|
5614
1982
|
if (!fieldDef) return;
|
|
5615
1983
|
const options = resolveOptions(fieldDef, config) ?? [];
|
|
5616
1984
|
const currentValue = getConfigValue(config, fieldPath);
|
|
5617
|
-
const kb = new
|
|
1985
|
+
const kb = new InlineKeyboard7();
|
|
5618
1986
|
for (const opt of options) {
|
|
5619
1987
|
const marker = opt === String(currentValue) ? " \u2713" : "";
|
|
5620
1988
|
kb.text(`${opt}${marker}`, `s:pick:${fieldPath}:${opt}`).row();
|
|
@@ -5674,7 +2042,7 @@ Tap to change:`, {
|
|
|
5674
2042
|
} catch {
|
|
5675
2043
|
}
|
|
5676
2044
|
} catch (err) {
|
|
5677
|
-
|
|
2045
|
+
log5.error({ err, fieldPath }, "Failed to set config");
|
|
5678
2046
|
try {
|
|
5679
2047
|
await ctx.answerCallbackQuery({ text: "\u274C Failed to update" });
|
|
5680
2048
|
} catch {
|
|
@@ -5745,8 +2113,8 @@ function buildNestedUpdate(dotPath, value) {
|
|
|
5745
2113
|
}
|
|
5746
2114
|
|
|
5747
2115
|
// src/adapters/telegram/commands/doctor.ts
|
|
5748
|
-
import { InlineKeyboard as
|
|
5749
|
-
var
|
|
2116
|
+
import { InlineKeyboard as InlineKeyboard8 } from "grammy";
|
|
2117
|
+
var log6 = createChildLogger({ module: "telegram-cmd-doctor" });
|
|
5750
2118
|
var pendingFixesStore = /* @__PURE__ */ new Map();
|
|
5751
2119
|
function renderReport(report) {
|
|
5752
2120
|
const icons = { pass: "\u2705", warn: "\u26A0\uFE0F", fail: "\u274C" };
|
|
@@ -5763,7 +2131,7 @@ function renderReport(report) {
|
|
|
5763
2131
|
lines.push(`<b>Result:</b> ${passed} passed, ${warnings} warnings, ${failed} failed${fixedStr}`);
|
|
5764
2132
|
let keyboard;
|
|
5765
2133
|
if (report.pendingFixes.length > 0) {
|
|
5766
|
-
keyboard = new
|
|
2134
|
+
keyboard = new InlineKeyboard8();
|
|
5767
2135
|
for (let i = 0; i < report.pendingFixes.length; i++) {
|
|
5768
2136
|
const label = `\u{1F527} Fix: ${report.pendingFixes[i].message.slice(0, 30)}`;
|
|
5769
2137
|
keyboard.text(label, `m:doctor:fix:${i}`).row();
|
|
@@ -5789,7 +2157,7 @@ async function handleDoctor(ctx) {
|
|
|
5789
2157
|
reply_markup: keyboard
|
|
5790
2158
|
});
|
|
5791
2159
|
} catch (err) {
|
|
5792
|
-
|
|
2160
|
+
log6.error({ err }, "Doctor command failed");
|
|
5793
2161
|
await ctx.api.editMessageText(
|
|
5794
2162
|
ctx.chat.id,
|
|
5795
2163
|
statusMsg.message_id,
|
|
@@ -5838,7 +2206,7 @@ function setupDoctorCallbacks(bot) {
|
|
|
5838
2206
|
}
|
|
5839
2207
|
}
|
|
5840
2208
|
} catch (err) {
|
|
5841
|
-
|
|
2209
|
+
log6.error({ err, index }, "Doctor fix callback failed");
|
|
5842
2210
|
}
|
|
5843
2211
|
});
|
|
5844
2212
|
bot.callbackQuery("m:doctor", async (ctx) => {
|
|
@@ -5851,8 +2219,8 @@ function setupDoctorCallbacks(bot) {
|
|
|
5851
2219
|
}
|
|
5852
2220
|
|
|
5853
2221
|
// src/adapters/telegram/commands/tunnel.ts
|
|
5854
|
-
import { InlineKeyboard as
|
|
5855
|
-
var
|
|
2222
|
+
import { InlineKeyboard as InlineKeyboard9 } from "grammy";
|
|
2223
|
+
var log7 = createChildLogger({ module: "telegram-cmd-tunnel" });
|
|
5856
2224
|
async function handleTunnel(ctx, core) {
|
|
5857
2225
|
if (!core.tunnelService) {
|
|
5858
2226
|
await ctx.reply("\u274C Tunnel service is not enabled.", { parse_mode: "HTML" });
|
|
@@ -5939,7 +2307,7 @@ async function handleTunnels(ctx, core) {
|
|
|
5939
2307
|
\u2192 <a href="${escapeHtml(e.publicUrl)}">${escapeHtml(e.publicUrl)}</a>` : "";
|
|
5940
2308
|
return `${status} Port <b>${e.port}</b>${label}${url}`;
|
|
5941
2309
|
});
|
|
5942
|
-
const keyboard = new
|
|
2310
|
+
const keyboard = new InlineKeyboard9();
|
|
5943
2311
|
for (const e of entries) {
|
|
5944
2312
|
keyboard.text(`\u{1F50C} Stop ${e.port}${e.label ? ` (${e.label})` : ""}`, `tn:stop:${e.port}`).row();
|
|
5945
2313
|
}
|
|
@@ -5979,7 +2347,7 @@ function setupTunnelCallbacks(bot, core) {
|
|
|
5979
2347
|
if (remaining.length === 0) {
|
|
5980
2348
|
await ctx.editMessageText("\u{1F50C} All tunnels stopped.", { parse_mode: "HTML" });
|
|
5981
2349
|
} else {
|
|
5982
|
-
const kb = new
|
|
2350
|
+
const kb = new InlineKeyboard9();
|
|
5983
2351
|
for (const e of remaining) {
|
|
5984
2352
|
kb.text(`\u{1F50C} Stop ${e.port}${e.label ? ` (${e.label})` : ""}`, `tn:stop:${e.port}`).row();
|
|
5985
2353
|
}
|
|
@@ -6029,9 +2397,11 @@ function setupCommands(bot, core, chatId, assistant) {
|
|
|
6029
2397
|
bot.command("tunnels", (ctx) => handleTunnels(ctx, core));
|
|
6030
2398
|
bot.command("archive", (ctx) => handleArchive(ctx, core));
|
|
6031
2399
|
bot.command("text_to_speech", (ctx) => handleTTS(ctx, core));
|
|
2400
|
+
bot.command("resume", (ctx) => handleResume(ctx, core, chatId, assistant));
|
|
6032
2401
|
}
|
|
6033
2402
|
function setupAllCallbacks(bot, core, chatId, systemTopicIds, getAssistantSession) {
|
|
6034
2403
|
setupNewSessionCallbacks(bot, core, chatId);
|
|
2404
|
+
setupResumeCallbacks(bot, core, chatId);
|
|
6035
2405
|
setupSessionCallbacks(bot, core, chatId, systemTopicIds);
|
|
6036
2406
|
setupSettingsCallbacks(bot, core, getAssistantSession ?? (() => void 0));
|
|
6037
2407
|
setupDoctorCallbacks(bot);
|
|
@@ -6102,13 +2472,14 @@ var STATIC_COMMANDS = [
|
|
|
6102
2472
|
{ command: "tunnel", description: "Create/stop tunnel for a local port" },
|
|
6103
2473
|
{ command: "tunnels", description: "List active tunnels" },
|
|
6104
2474
|
{ command: "archive", description: "Archive session topic (recreate with clean history)" },
|
|
6105
|
-
{ command: "text_to_speech", description: "Toggle Text to Speech (/text_to_speech on, /text_to_speech off)" }
|
|
2475
|
+
{ command: "text_to_speech", description: "Toggle Text to Speech (/text_to_speech on, /text_to_speech off)" },
|
|
2476
|
+
{ command: "resume", description: "Resume with conversation history from Entire checkpoints" }
|
|
6106
2477
|
];
|
|
6107
2478
|
|
|
6108
2479
|
// src/adapters/telegram/permissions.ts
|
|
6109
|
-
import { InlineKeyboard as
|
|
6110
|
-
import { nanoid
|
|
6111
|
-
var
|
|
2480
|
+
import { InlineKeyboard as InlineKeyboard10 } from "grammy";
|
|
2481
|
+
import { nanoid } from "nanoid";
|
|
2482
|
+
var log8 = createChildLogger({ module: "telegram-permissions" });
|
|
6112
2483
|
var PermissionHandler = class {
|
|
6113
2484
|
constructor(bot, chatId, getSession, sendNotification) {
|
|
6114
2485
|
this.bot = bot;
|
|
@@ -6119,13 +2490,13 @@ var PermissionHandler = class {
|
|
|
6119
2490
|
pending = /* @__PURE__ */ new Map();
|
|
6120
2491
|
async sendPermissionRequest(session, request) {
|
|
6121
2492
|
const threadId = Number(session.threadId);
|
|
6122
|
-
const callbackKey =
|
|
2493
|
+
const callbackKey = nanoid(8);
|
|
6123
2494
|
this.pending.set(callbackKey, {
|
|
6124
2495
|
sessionId: session.id,
|
|
6125
2496
|
requestId: request.id,
|
|
6126
2497
|
options: request.options.map((o) => ({ id: o.id, isAllow: o.isAllow }))
|
|
6127
2498
|
});
|
|
6128
|
-
const keyboard = new
|
|
2499
|
+
const keyboard = new InlineKeyboard10();
|
|
6129
2500
|
for (const option of request.options) {
|
|
6130
2501
|
const emoji = option.isAllow ? "\u2705" : "\u274C";
|
|
6131
2502
|
keyboard.text(`${emoji} ${option.label}`, `p:${callbackKey}:${option.id}`);
|
|
@@ -6168,7 +2539,7 @@ ${escapeHtml(request.description)}`,
|
|
|
6168
2539
|
}
|
|
6169
2540
|
const session = this.getSession(pending.sessionId);
|
|
6170
2541
|
const isAllow = pending.options.find((o) => o.id === optionId)?.isAllow ?? false;
|
|
6171
|
-
|
|
2542
|
+
log8.info({ requestId: pending.requestId, optionId, isAllow }, "Permission responded");
|
|
6172
2543
|
if (session?.permissionGate.requestId === pending.requestId) {
|
|
6173
2544
|
session.permissionGate.resolve(optionId);
|
|
6174
2545
|
}
|
|
@@ -6186,10 +2557,10 @@ ${escapeHtml(request.description)}`,
|
|
|
6186
2557
|
};
|
|
6187
2558
|
|
|
6188
2559
|
// src/adapters/telegram/assistant.ts
|
|
6189
|
-
var
|
|
2560
|
+
var log9 = createChildLogger({ module: "telegram-assistant" });
|
|
6190
2561
|
async function spawnAssistant(core, adapter, assistantTopicId) {
|
|
6191
2562
|
const config = core.configManager.get();
|
|
6192
|
-
|
|
2563
|
+
log9.info({ agent: config.defaultAgent }, "Creating assistant session...");
|
|
6193
2564
|
const session = await core.createSession({
|
|
6194
2565
|
channelId: "telegram",
|
|
6195
2566
|
agentName: config.defaultAgent,
|
|
@@ -6198,7 +2569,7 @@ async function spawnAssistant(core, adapter, assistantTopicId) {
|
|
|
6198
2569
|
// Prevent auto-naming from triggering after system prompt
|
|
6199
2570
|
});
|
|
6200
2571
|
session.threadId = String(assistantTopicId);
|
|
6201
|
-
|
|
2572
|
+
log9.info({ sessionId: session.id }, "Assistant agent spawned");
|
|
6202
2573
|
const allRecords = core.sessionManager.listRecords();
|
|
6203
2574
|
const activeCount = allRecords.filter((r) => r.status === "active" || r.status === "initializing").length;
|
|
6204
2575
|
const statusCounts = /* @__PURE__ */ new Map();
|
|
@@ -6219,9 +2590,9 @@ async function spawnAssistant(core, adapter, assistantTopicId) {
|
|
|
6219
2590
|
};
|
|
6220
2591
|
const systemPrompt = buildAssistantSystemPrompt(ctx);
|
|
6221
2592
|
const ready = session.enqueuePrompt(systemPrompt).then(() => {
|
|
6222
|
-
|
|
2593
|
+
log9.info({ sessionId: session.id }, "Assistant system prompt completed");
|
|
6223
2594
|
}).catch((err) => {
|
|
6224
|
-
|
|
2595
|
+
log9.warn({ err }, "Assistant system prompt failed");
|
|
6225
2596
|
});
|
|
6226
2597
|
return { session, ready };
|
|
6227
2598
|
}
|
|
@@ -6387,7 +2758,7 @@ function redirectToAssistant(chatId, assistantTopicId) {
|
|
|
6387
2758
|
}
|
|
6388
2759
|
|
|
6389
2760
|
// src/adapters/telegram/activity.ts
|
|
6390
|
-
var
|
|
2761
|
+
var log10 = createChildLogger({ module: "telegram:activity" });
|
|
6391
2762
|
var THINKING_REFRESH_MS = 15e3;
|
|
6392
2763
|
var THINKING_MAX_MS = 3 * 60 * 1e3;
|
|
6393
2764
|
var ThinkingIndicator = class {
|
|
@@ -6419,7 +2790,7 @@ var ThinkingIndicator = class {
|
|
|
6419
2790
|
this.startRefreshTimer();
|
|
6420
2791
|
}
|
|
6421
2792
|
} catch (err) {
|
|
6422
|
-
|
|
2793
|
+
log10.warn({ err }, "ThinkingIndicator.show() failed");
|
|
6423
2794
|
} finally {
|
|
6424
2795
|
this.sending = false;
|
|
6425
2796
|
}
|
|
@@ -6492,7 +2863,7 @@ var UsageMessage = class {
|
|
|
6492
2863
|
if (result) this.msgId = result.message_id;
|
|
6493
2864
|
}
|
|
6494
2865
|
} catch (err) {
|
|
6495
|
-
|
|
2866
|
+
log10.warn({ err }, "UsageMessage.send() failed");
|
|
6496
2867
|
}
|
|
6497
2868
|
}
|
|
6498
2869
|
getMsgId() {
|
|
@@ -6505,7 +2876,7 @@ var UsageMessage = class {
|
|
|
6505
2876
|
try {
|
|
6506
2877
|
await this.sendQueue.enqueue(() => this.api.deleteMessage(this.chatId, id));
|
|
6507
2878
|
} catch (err) {
|
|
6508
|
-
|
|
2879
|
+
log10.warn({ err }, "UsageMessage.delete() failed");
|
|
6509
2880
|
}
|
|
6510
2881
|
}
|
|
6511
2882
|
};
|
|
@@ -6591,7 +2962,7 @@ var PlanCard = class {
|
|
|
6591
2962
|
if (result) this.msgId = result.message_id;
|
|
6592
2963
|
}
|
|
6593
2964
|
} catch (err) {
|
|
6594
|
-
|
|
2965
|
+
log10.warn({ err }, "PlanCard flush failed");
|
|
6595
2966
|
}
|
|
6596
2967
|
}
|
|
6597
2968
|
};
|
|
@@ -6654,7 +3025,7 @@ var ActivityTracker = class {
|
|
|
6654
3025
|
})
|
|
6655
3026
|
);
|
|
6656
3027
|
} catch (err) {
|
|
6657
|
-
|
|
3028
|
+
log10.warn({ err }, "ActivityTracker.onComplete() Done send failed");
|
|
6658
3029
|
}
|
|
6659
3030
|
}
|
|
6660
3031
|
}
|
|
@@ -6681,19 +3052,19 @@ var TelegramSendQueue = class {
|
|
|
6681
3052
|
enqueue(fn, opts) {
|
|
6682
3053
|
const type = opts?.type ?? "other";
|
|
6683
3054
|
const key = opts?.key;
|
|
6684
|
-
return new Promise((
|
|
3055
|
+
return new Promise((resolve, reject) => {
|
|
6685
3056
|
if (type === "text" && key) {
|
|
6686
3057
|
const idx = this.items.findIndex(
|
|
6687
3058
|
(item) => item.type === "text" && item.key === key
|
|
6688
3059
|
);
|
|
6689
3060
|
if (idx !== -1) {
|
|
6690
3061
|
this.items[idx].resolve(void 0);
|
|
6691
|
-
this.items[idx] = { fn, type, key, resolve
|
|
3062
|
+
this.items[idx] = { fn, type, key, resolve, reject };
|
|
6692
3063
|
this.scheduleProcess();
|
|
6693
3064
|
return;
|
|
6694
3065
|
}
|
|
6695
3066
|
}
|
|
6696
|
-
this.items.push({ fn, type, key, resolve
|
|
3067
|
+
this.items.push({ fn, type, key, resolve, reject });
|
|
6697
3068
|
this.scheduleProcess();
|
|
6698
3069
|
});
|
|
6699
3070
|
}
|
|
@@ -6736,8 +3107,8 @@ var TelegramSendQueue = class {
|
|
|
6736
3107
|
};
|
|
6737
3108
|
|
|
6738
3109
|
// src/adapters/telegram/action-detect.ts
|
|
6739
|
-
import { nanoid as
|
|
6740
|
-
import { InlineKeyboard as
|
|
3110
|
+
import { nanoid as nanoid2 } from "nanoid";
|
|
3111
|
+
import { InlineKeyboard as InlineKeyboard11 } from "grammy";
|
|
6741
3112
|
var CMD_NEW_RE = /\/new(?:\s+([^\s\u0080-\uFFFF]+)(?:\s+([^\s\u0080-\uFFFF]+))?)?/;
|
|
6742
3113
|
var CMD_CANCEL_RE = /\/cancel\b/;
|
|
6743
3114
|
var KW_NEW_RE = /(?:create|new)\s+session/i;
|
|
@@ -6762,7 +3133,7 @@ function detectAction(text) {
|
|
|
6762
3133
|
var ACTION_TTL_MS = 5 * 60 * 1e3;
|
|
6763
3134
|
var actionMap = /* @__PURE__ */ new Map();
|
|
6764
3135
|
function storeAction(action) {
|
|
6765
|
-
const id =
|
|
3136
|
+
const id = nanoid2(10);
|
|
6766
3137
|
actionMap.set(id, { action, createdAt: Date.now() });
|
|
6767
3138
|
for (const [key, entry] of actionMap) {
|
|
6768
3139
|
if (Date.now() - entry.createdAt > ACTION_TTL_MS) {
|
|
@@ -6784,7 +3155,7 @@ function removeAction(id) {
|
|
|
6784
3155
|
actionMap.delete(id);
|
|
6785
3156
|
}
|
|
6786
3157
|
function buildActionKeyboard(actionId, action) {
|
|
6787
|
-
const keyboard = new
|
|
3158
|
+
const keyboard = new InlineKeyboard11();
|
|
6788
3159
|
if (action.action === "new_session") {
|
|
6789
3160
|
keyboard.text("\u2705 Create session", `a:${actionId}`);
|
|
6790
3161
|
keyboard.text("\u274C Cancel", `a:dismiss:${actionId}`);
|
|
@@ -6893,7 +3264,7 @@ function setupActionCallbacks(bot, core, chatId, getAssistantSessionId) {
|
|
|
6893
3264
|
}
|
|
6894
3265
|
|
|
6895
3266
|
// src/adapters/telegram/tool-call-tracker.ts
|
|
6896
|
-
var
|
|
3267
|
+
var log11 = createChildLogger({ module: "tool-call-tracker" });
|
|
6897
3268
|
var ToolCallTracker = class {
|
|
6898
3269
|
constructor(bot, chatId, sendQueue) {
|
|
6899
3270
|
this.bot = bot;
|
|
@@ -6937,7 +3308,7 @@ var ToolCallTracker = class {
|
|
|
6937
3308
|
if (!toolState) return;
|
|
6938
3309
|
if (meta.viewerLinks) {
|
|
6939
3310
|
toolState.viewerLinks = meta.viewerLinks;
|
|
6940
|
-
|
|
3311
|
+
log11.debug({ toolId: meta.id, viewerLinks: meta.viewerLinks }, "Accumulated viewerLinks");
|
|
6941
3312
|
}
|
|
6942
3313
|
if (meta.viewerFilePath) toolState.viewerFilePath = meta.viewerFilePath;
|
|
6943
3314
|
if (meta.name) toolState.name = meta.name;
|
|
@@ -6945,7 +3316,7 @@ var ToolCallTracker = class {
|
|
|
6945
3316
|
const isTerminal = meta.status === "completed" || meta.status === "failed";
|
|
6946
3317
|
if (!isTerminal) return;
|
|
6947
3318
|
await toolState.ready;
|
|
6948
|
-
|
|
3319
|
+
log11.debug(
|
|
6949
3320
|
{
|
|
6950
3321
|
toolId: meta.id,
|
|
6951
3322
|
status: meta.status,
|
|
@@ -6974,7 +3345,7 @@ var ToolCallTracker = class {
|
|
|
6974
3345
|
)
|
|
6975
3346
|
);
|
|
6976
3347
|
} catch (err) {
|
|
6977
|
-
|
|
3348
|
+
log11.warn(
|
|
6978
3349
|
{
|
|
6979
3350
|
err,
|
|
6980
3351
|
msgId: toolState.msgId,
|
|
@@ -7245,7 +3616,7 @@ var DraftManager = class {
|
|
|
7245
3616
|
};
|
|
7246
3617
|
|
|
7247
3618
|
// src/adapters/telegram/skill-command-manager.ts
|
|
7248
|
-
var
|
|
3619
|
+
var log12 = createChildLogger({ module: "skill-commands" });
|
|
7249
3620
|
var SkillCommandManager = class {
|
|
7250
3621
|
// sessionId → pinned msgId
|
|
7251
3622
|
constructor(bot, chatId, sendQueue, sessionManager) {
|
|
@@ -7311,7 +3682,7 @@ var SkillCommandManager = class {
|
|
|
7311
3682
|
disable_notification: true
|
|
7312
3683
|
});
|
|
7313
3684
|
} catch (err) {
|
|
7314
|
-
|
|
3685
|
+
log12.error({ err, sessionId }, "Failed to send skill commands");
|
|
7315
3686
|
}
|
|
7316
3687
|
}
|
|
7317
3688
|
async cleanup(sessionId) {
|
|
@@ -7340,7 +3711,7 @@ var SkillCommandManager = class {
|
|
|
7340
3711
|
};
|
|
7341
3712
|
|
|
7342
3713
|
// src/adapters/telegram/adapter.ts
|
|
7343
|
-
var
|
|
3714
|
+
var log13 = createChildLogger({ module: "telegram" });
|
|
7344
3715
|
function patchedFetch(input, init) {
|
|
7345
3716
|
if (init?.signal && !(init.signal instanceof AbortSignal)) {
|
|
7346
3717
|
const nativeController = new AbortController();
|
|
@@ -7404,7 +3775,7 @@ var TelegramAdapter = class extends ChannelAdapter {
|
|
|
7404
3775
|
);
|
|
7405
3776
|
this.bot.catch((err) => {
|
|
7406
3777
|
const rootCause = err.error instanceof Error ? err.error : err;
|
|
7407
|
-
|
|
3778
|
+
log13.error({ err: rootCause }, "Telegram bot error");
|
|
7408
3779
|
});
|
|
7409
3780
|
this.bot.api.config.use(async (prev, method, payload, signal) => {
|
|
7410
3781
|
const maxRetries = 3;
|
|
@@ -7418,7 +3789,7 @@ var TelegramAdapter = class extends ChannelAdapter {
|
|
|
7418
3789
|
if (rateLimitedMethods.includes(method)) {
|
|
7419
3790
|
this.sendQueue.onRateLimited();
|
|
7420
3791
|
}
|
|
7421
|
-
|
|
3792
|
+
log13.warn(
|
|
7422
3793
|
{ method, retryAfter, attempt: attempt + 1 },
|
|
7423
3794
|
"Rate limited by Telegram, retrying"
|
|
7424
3795
|
);
|
|
@@ -7529,8 +3900,8 @@ var TelegramAdapter = class extends ChannelAdapter {
|
|
|
7529
3900
|
});
|
|
7530
3901
|
return;
|
|
7531
3902
|
}
|
|
7532
|
-
const { getAgentCapabilities
|
|
7533
|
-
const caps =
|
|
3903
|
+
const { getAgentCapabilities } = await import("./agent-registry-KZFSIRSJ.js");
|
|
3904
|
+
const caps = getAgentCapabilities(agentName);
|
|
7534
3905
|
if (!caps.supportsResume || !caps.resumeCommand) {
|
|
7535
3906
|
await ctx.reply("This agent does not support session transfer.", {
|
|
7536
3907
|
message_thread_id: threadId
|
|
@@ -7551,7 +3922,7 @@ var TelegramAdapter = class extends ChannelAdapter {
|
|
|
7551
3922
|
this.setupRoutes();
|
|
7552
3923
|
this.bot.start({
|
|
7553
3924
|
allowed_updates: ["message", "callback_query"],
|
|
7554
|
-
onStart: () =>
|
|
3925
|
+
onStart: () => log13.info(
|
|
7555
3926
|
{ chatId: this.telegramConfig.chatId },
|
|
7556
3927
|
"Telegram bot started"
|
|
7557
3928
|
)
|
|
@@ -7573,10 +3944,10 @@ var TelegramAdapter = class extends ChannelAdapter {
|
|
|
7573
3944
|
reply_markup: buildMenuKeyboard()
|
|
7574
3945
|
});
|
|
7575
3946
|
} catch (err) {
|
|
7576
|
-
|
|
3947
|
+
log13.warn({ err }, "Failed to send welcome message");
|
|
7577
3948
|
}
|
|
7578
3949
|
try {
|
|
7579
|
-
|
|
3950
|
+
log13.info("Spawning assistant session...");
|
|
7580
3951
|
const { session, ready } = await spawnAssistant(
|
|
7581
3952
|
this.core,
|
|
7582
3953
|
this,
|
|
@@ -7584,13 +3955,13 @@ var TelegramAdapter = class extends ChannelAdapter {
|
|
|
7584
3955
|
);
|
|
7585
3956
|
this.assistantSession = session;
|
|
7586
3957
|
this.assistantInitializing = true;
|
|
7587
|
-
|
|
3958
|
+
log13.info({ sessionId: session.id }, "Assistant session ready, system prompt running in background");
|
|
7588
3959
|
ready.then(() => {
|
|
7589
3960
|
this.assistantInitializing = false;
|
|
7590
|
-
|
|
3961
|
+
log13.info({ sessionId: session.id }, "Assistant ready for user messages");
|
|
7591
3962
|
});
|
|
7592
3963
|
} catch (err) {
|
|
7593
|
-
|
|
3964
|
+
log13.error({ err }, "Failed to spawn assistant");
|
|
7594
3965
|
this.bot.api.sendMessage(
|
|
7595
3966
|
this.telegramConfig.chatId,
|
|
7596
3967
|
`\u26A0\uFE0F <b>Failed to start assistant session.</b>
|
|
@@ -7606,7 +3977,7 @@ var TelegramAdapter = class extends ChannelAdapter {
|
|
|
7606
3977
|
await this.assistantSession.destroy();
|
|
7607
3978
|
}
|
|
7608
3979
|
await this.bot.stop();
|
|
7609
|
-
|
|
3980
|
+
log13.info("Telegram bot stopped");
|
|
7610
3981
|
}
|
|
7611
3982
|
setupRoutes() {
|
|
7612
3983
|
this.bot.on("message:text", async (ctx) => {
|
|
@@ -7615,6 +3986,9 @@ var TelegramAdapter = class extends ChannelAdapter {
|
|
|
7615
3986
|
if (await handlePendingWorkspaceInput(ctx, this.core, this.telegramConfig.chatId, this.assistantTopicId)) {
|
|
7616
3987
|
return;
|
|
7617
3988
|
}
|
|
3989
|
+
if (await handlePendingResumeInput(ctx, this.core, this.telegramConfig.chatId, this.assistantTopicId)) {
|
|
3990
|
+
return;
|
|
3991
|
+
}
|
|
7618
3992
|
if (!threadId) {
|
|
7619
3993
|
const html = redirectToAssistant(
|
|
7620
3994
|
this.telegramConfig.chatId,
|
|
@@ -7634,7 +4008,7 @@ var TelegramAdapter = class extends ChannelAdapter {
|
|
|
7634
4008
|
ctx.replyWithChatAction("typing").catch(() => {
|
|
7635
4009
|
});
|
|
7636
4010
|
handleAssistantMessage(this.assistantSession, forwardText).catch(
|
|
7637
|
-
(err) =>
|
|
4011
|
+
(err) => log13.error({ err }, "Assistant error")
|
|
7638
4012
|
);
|
|
7639
4013
|
return;
|
|
7640
4014
|
}
|
|
@@ -7651,7 +4025,7 @@ var TelegramAdapter = class extends ChannelAdapter {
|
|
|
7651
4025
|
threadId: String(threadId),
|
|
7652
4026
|
userId: String(ctx.from.id),
|
|
7653
4027
|
text: forwardText
|
|
7654
|
-
}).catch((err) =>
|
|
4028
|
+
}).catch((err) => log13.error({ err }, "handleMessage error"));
|
|
7655
4029
|
});
|
|
7656
4030
|
this.bot.on("message:photo", async (ctx) => {
|
|
7657
4031
|
const threadId = ctx.message.message_thread_id;
|
|
@@ -7793,7 +4167,7 @@ Task completed.
|
|
|
7793
4167
|
if (!content.attachment) return;
|
|
7794
4168
|
const { attachment } = content;
|
|
7795
4169
|
if (attachment.size > 50 * 1024 * 1024) {
|
|
7796
|
-
|
|
4170
|
+
log13.warn({ sessionId: ctx.sessionId, fileName: attachment.fileName, size: attachment.size }, "File too large for Telegram (>50MB)");
|
|
7797
4171
|
await this.sendQueue.enqueue(
|
|
7798
4172
|
() => this.bot.api.sendMessage(
|
|
7799
4173
|
this.telegramConfig.chatId,
|
|
@@ -7830,7 +4204,7 @@ Task completed.
|
|
|
7830
4204
|
);
|
|
7831
4205
|
}
|
|
7832
4206
|
} catch (err) {
|
|
7833
|
-
|
|
4207
|
+
log13.error({ err, sessionId: ctx.sessionId, fileName: attachment.fileName }, "Failed to send attachment");
|
|
7834
4208
|
}
|
|
7835
4209
|
},
|
|
7836
4210
|
onSessionEnd: async (ctx, _content) => {
|
|
@@ -7898,14 +4272,14 @@ Task completed.
|
|
|
7898
4272
|
if (session.archiving) return;
|
|
7899
4273
|
const threadId = Number(session.threadId);
|
|
7900
4274
|
if (!threadId || isNaN(threadId)) {
|
|
7901
|
-
|
|
4275
|
+
log13.warn({ sessionId, threadId: session.threadId }, "Session has no valid threadId, skipping message");
|
|
7902
4276
|
return;
|
|
7903
4277
|
}
|
|
7904
4278
|
const ctx = { sessionId, threadId };
|
|
7905
4279
|
await dispatchMessage(this.messageHandlers, ctx, content);
|
|
7906
4280
|
}
|
|
7907
4281
|
async sendPermissionRequest(sessionId, request) {
|
|
7908
|
-
|
|
4282
|
+
log13.info({ sessionId, requestId: request.id }, "Permission request sent");
|
|
7909
4283
|
const session = this.core.sessionManager.getSession(sessionId);
|
|
7910
4284
|
if (!session) return;
|
|
7911
4285
|
await this.sendQueue.enqueue(
|
|
@@ -7914,7 +4288,7 @@ Task completed.
|
|
|
7914
4288
|
}
|
|
7915
4289
|
async sendNotification(notification) {
|
|
7916
4290
|
if (notification.sessionId === this.assistantSession?.id) return;
|
|
7917
|
-
|
|
4291
|
+
log13.info(
|
|
7918
4292
|
{ sessionId: notification.sessionId, type: notification.type },
|
|
7919
4293
|
"Notification sent"
|
|
7920
4294
|
);
|
|
@@ -7950,7 +4324,7 @@ Task completed.
|
|
|
7950
4324
|
);
|
|
7951
4325
|
}
|
|
7952
4326
|
async createSessionThread(sessionId, name) {
|
|
7953
|
-
|
|
4327
|
+
log13.info({ sessionId, name }, "Session topic created");
|
|
7954
4328
|
return String(
|
|
7955
4329
|
await createSessionTopic(this.bot, this.telegramConfig.chatId, name)
|
|
7956
4330
|
);
|
|
@@ -7974,7 +4348,7 @@ Task completed.
|
|
|
7974
4348
|
try {
|
|
7975
4349
|
await this.bot.api.deleteForumTopic(this.telegramConfig.chatId, topicId);
|
|
7976
4350
|
} catch (err) {
|
|
7977
|
-
|
|
4351
|
+
log13.warn({ err, sessionId, topicId }, "Failed to delete forum topic (may already be deleted)");
|
|
7978
4352
|
}
|
|
7979
4353
|
}
|
|
7980
4354
|
async sendSkillCommands(sessionId, commands) {
|
|
@@ -7998,7 +4372,7 @@ Task completed.
|
|
|
7998
4372
|
const buffer = Buffer.from(await response.arrayBuffer());
|
|
7999
4373
|
return { buffer, filePath: file.file_path };
|
|
8000
4374
|
} catch (err) {
|
|
8001
|
-
|
|
4375
|
+
log13.error({ err }, "Failed to download file from Telegram");
|
|
8002
4376
|
return null;
|
|
8003
4377
|
}
|
|
8004
4378
|
}
|
|
@@ -8014,7 +4388,7 @@ Task completed.
|
|
|
8014
4388
|
try {
|
|
8015
4389
|
buffer = await this.fileService.convertOggToWav(buffer);
|
|
8016
4390
|
} catch (err) {
|
|
8017
|
-
|
|
4391
|
+
log13.warn({ err }, "OGG\u2192WAV conversion failed, saving original OGG");
|
|
8018
4392
|
fileName = "voice.ogg";
|
|
8019
4393
|
mimeType = "audio/ogg";
|
|
8020
4394
|
originalFilePath = void 0;
|
|
@@ -8040,7 +4414,7 @@ Task completed.
|
|
|
8040
4414
|
userId: String(userId),
|
|
8041
4415
|
text,
|
|
8042
4416
|
attachments: [att]
|
|
8043
|
-
}).catch((err) =>
|
|
4417
|
+
}).catch((err) => log13.error({ err }, "handleMessage error"));
|
|
8044
4418
|
}
|
|
8045
4419
|
async cleanupSkillCommands(sessionId) {
|
|
8046
4420
|
await this.skillManager.cleanup(sessionId);
|
|
@@ -8089,32 +4463,6 @@ Task completed.
|
|
|
8089
4463
|
};
|
|
8090
4464
|
|
|
8091
4465
|
export {
|
|
8092
|
-
nodeToWebWritable,
|
|
8093
|
-
nodeToWebReadable,
|
|
8094
|
-
StderrCapture,
|
|
8095
|
-
TypedEmitter,
|
|
8096
|
-
AgentInstance,
|
|
8097
|
-
AgentManager,
|
|
8098
|
-
PromptQueue,
|
|
8099
|
-
PermissionGate,
|
|
8100
|
-
Session,
|
|
8101
|
-
SessionManager,
|
|
8102
|
-
FileService,
|
|
8103
|
-
SessionBridge,
|
|
8104
|
-
NotificationManager,
|
|
8105
|
-
MessageTransformer,
|
|
8106
|
-
UsageStore,
|
|
8107
|
-
UsageBudget,
|
|
8108
|
-
SecurityGuard,
|
|
8109
|
-
SessionFactory,
|
|
8110
|
-
EventBus,
|
|
8111
|
-
SpeechService,
|
|
8112
|
-
GroqSTT,
|
|
8113
|
-
OpenACPCore,
|
|
8114
|
-
SSEManager,
|
|
8115
|
-
StaticServer,
|
|
8116
|
-
ApiServer,
|
|
8117
|
-
TopicManager,
|
|
8118
4466
|
TelegramAdapter
|
|
8119
4467
|
};
|
|
8120
|
-
//# sourceMappingURL=chunk-
|
|
4468
|
+
//# sourceMappingURL=chunk-AHPRT3RY.js.map
|