@openacp/cli 0.6.4 → 0.6.5

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.
Files changed (49) hide show
  1. package/dist/adapter-YSEIZJBA.js +798 -0
  2. package/dist/adapter-YSEIZJBA.js.map +1 -0
  3. package/dist/{chunk-DJIXG62C.js → chunk-3IRAWHMC.js} +3 -3
  4. package/dist/{chunk-L5KZXYJD.js → chunk-3ZO3MHZN.js} +21 -2
  5. package/dist/chunk-3ZO3MHZN.js.map +1 -0
  6. package/dist/chunk-4GQ3I65A.js +23 -0
  7. package/dist/chunk-4GQ3I65A.js.map +1 -0
  8. package/dist/{chunk-TNFXJQZP.js → chunk-7KZI2236.js} +2 -2
  9. package/dist/{chunk-FMWSVLRM.js → chunk-FCLGYYTY.js} +1 -21
  10. package/dist/chunk-FCLGYYTY.js.map +1 -0
  11. package/dist/chunk-FEWSQT3U.js +3949 -0
  12. package/dist/chunk-FEWSQT3U.js.map +1 -0
  13. package/dist/{chunk-E56PPPAE.js → chunk-FW6HM4VU.js} +90 -4029
  14. package/dist/chunk-FW6HM4VU.js.map +1 -0
  15. package/dist/{chunk-TOQPQB5Q.js → chunk-PJVKOZTR.js} +2 -2
  16. package/dist/{chunk-DOCFD5JR.js → chunk-WVMSP4AF.js} +2 -2
  17. package/dist/{chunk-3CHBVO4T.js → chunk-XVL6AGMG.js} +2 -2
  18. package/dist/{chunk-N6E3HE42.js → chunk-ZKTIZME6.js} +2 -2
  19. package/dist/cli.js +21 -21
  20. package/dist/{config-XDUOULXX.js → config-B26J3XXN.js} +2 -2
  21. package/dist/{config-editor-3GGBY7NL.js → config-editor-QTGUK3CD.js} +4 -4
  22. package/dist/{daemon-QY7WXHQ3.js → daemon-5DS5BQXJ.js} +3 -3
  23. package/dist/{discord-4DE22BQC.js → discord-QKT3JMRW.js} +14 -12
  24. package/dist/{discord-4DE22BQC.js.map → discord-QKT3JMRW.js.map} +1 -1
  25. package/dist/doctor-6SUCVUZB.js +9 -0
  26. package/dist/{doctor-D3YZ6VHJ.js → doctor-QQ3YZEYV.js} +4 -4
  27. package/dist/index.d.ts +166 -11
  28. package/dist/index.js +13 -10
  29. package/dist/{main-GVTLD7VI.js → main-TSZR4HPP.js} +25 -17
  30. package/dist/main-TSZR4HPP.js.map +1 -0
  31. package/dist/{setup-D6BU36ZL.js → setup-5ZKSUR26.js} +3 -3
  32. package/package.json +5 -1
  33. package/dist/chunk-E56PPPAE.js.map +0 -1
  34. package/dist/chunk-FMWSVLRM.js.map +0 -1
  35. package/dist/chunk-L5KZXYJD.js.map +0 -1
  36. package/dist/doctor-SNSQ5SS2.js +0 -9
  37. package/dist/main-GVTLD7VI.js.map +0 -1
  38. /package/dist/{chunk-DJIXG62C.js.map → chunk-3IRAWHMC.js.map} +0 -0
  39. /package/dist/{chunk-TNFXJQZP.js.map → chunk-7KZI2236.js.map} +0 -0
  40. /package/dist/{chunk-TOQPQB5Q.js.map → chunk-PJVKOZTR.js.map} +0 -0
  41. /package/dist/{chunk-DOCFD5JR.js.map → chunk-WVMSP4AF.js.map} +0 -0
  42. /package/dist/{chunk-3CHBVO4T.js.map → chunk-XVL6AGMG.js.map} +0 -0
  43. /package/dist/{chunk-N6E3HE42.js.map → chunk-ZKTIZME6.js.map} +0 -0
  44. /package/dist/{config-XDUOULXX.js.map → config-B26J3XXN.js.map} +0 -0
  45. /package/dist/{config-editor-3GGBY7NL.js.map → config-editor-QTGUK3CD.js.map} +0 -0
  46. /package/dist/{daemon-QY7WXHQ3.js.map → daemon-5DS5BQXJ.js.map} +0 -0
  47. /package/dist/{doctor-D3YZ6VHJ.js.map → doctor-6SUCVUZB.js.map} +0 -0
  48. /package/dist/{doctor-SNSQ5SS2.js.map → doctor-QQ3YZEYV.js.map} +0 -0
  49. /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-FMWSVLRM.js";
4
+ } from "./chunk-FCLGYYTY.js";
6
5
  import {
7
6
  DoctorEngine
8
- } from "./chunk-TOQPQB5Q.js";
7
+ } from "./chunk-PJVKOZTR.js";
9
8
  import {
10
9
  buildMenuKeyboard,
11
10
  buildSkillMessages,
@@ -14,3929 +13,17 @@ import {
14
13
  handleMenu
15
14
  } from "./chunk-7QJS2XBD.js";
16
15
  import {
17
- AgentCatalog
18
- } from "./chunk-UKT3G5IA.js";
16
+ ChannelAdapter
17
+ } from "./chunk-4GQ3I65A.js";
19
18
  import {
20
- getAgentCapabilities
21
- } from "./chunk-JKBFUAJK.js";
22
- import {
23
- getConfigValue,
24
- getSafeFields,
25
- isHotReloadable,
26
- resolveOptions
27
- } from "./chunk-F3AICYO4.js";
28
- import {
29
- createChildLogger,
30
- createSessionLogger
31
- } from "./chunk-GAK6PIBW.js";
32
-
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
- };
19
+ getConfigValue,
20
+ getSafeFields,
21
+ isHotReloadable,
22
+ resolveOptions
23
+ } from "./chunk-F3AICYO4.js";
24
+ import {
25
+ createChildLogger
26
+ } from "./chunk-GAK6PIBW.js";
3940
27
 
3941
28
  // src/adapters/telegram/adapter.ts
3942
29
  import { Bot, InputFile } from "grammy";
@@ -4173,14 +260,14 @@ function splitMessage(text, maxLength = 3800) {
4173
260
 
4174
261
  // src/adapters/telegram/commands/admin.ts
4175
262
  import { InlineKeyboard } from "grammy";
4176
- var log12 = createChildLogger({ module: "telegram-cmd-admin" });
263
+ var log = createChildLogger({ module: "telegram-cmd-admin" });
4177
264
  function setupDangerousModeCallbacks(bot, core) {
4178
265
  bot.callbackQuery(/^d:/, async (ctx) => {
4179
266
  const sessionId = ctx.callbackQuery.data.slice(2);
4180
267
  const session = core.sessionManager.getSession(sessionId);
4181
268
  if (session) {
4182
269
  session.dangerousMode = !session.dangerousMode;
4183
- log12.info({ sessionId, dangerousMode: session.dangerousMode }, "Dangerous mode toggled via button");
270
+ log.info({ sessionId, dangerousMode: session.dangerousMode }, "Dangerous mode toggled via button");
4184
271
  core.sessionManager.patchRecord(sessionId, { dangerousMode: session.dangerousMode }).catch(() => {
4185
272
  });
4186
273
  const toastText2 = session.dangerousMode ? "\u2620\uFE0F Dangerous mode enabled \u2014 permissions auto-approved" : "\u{1F510} Dangerous mode disabled \u2014 permissions shown normally";
@@ -4207,7 +294,7 @@ function setupDangerousModeCallbacks(bot, core) {
4207
294
  const newDangerousMode = !(record.dangerousMode ?? false);
4208
295
  core.sessionManager.patchRecord(sessionId, { dangerousMode: newDangerousMode }).catch(() => {
4209
296
  });
4210
- log12.info({ sessionId, dangerousMode: newDangerousMode }, "Dangerous mode toggled via button (store-only, session not in memory)");
297
+ log.info({ sessionId, dangerousMode: newDangerousMode }, "Dangerous mode toggled via button (store-only, session not in memory)");
4211
298
  const toastText = newDangerousMode ? "\u2620\uFE0F Dangerous mode enabled \u2014 permissions auto-approved" : "\u{1F510} Dangerous mode disabled \u2014 permissions shown normally";
4212
299
  try {
4213
300
  await ctx.answerCallbackQuery({ text: toastText });
@@ -4395,7 +482,7 @@ async function handleRestart(ctx, core) {
4395
482
  }
4396
483
 
4397
484
  // src/adapters/telegram/commands/new-session.ts
4398
- var log13 = createChildLogger({ module: "telegram-cmd-new-session" });
485
+ var log2 = createChildLogger({ module: "telegram-cmd-new-session" });
4399
486
  var pendingNewSessions = /* @__PURE__ */ new Map();
4400
487
  var PENDING_TIMEOUT_MS = 5 * 60 * 1e3;
4401
488
  function cleanupPending(userId) {
@@ -4494,7 +581,7 @@ async function startConfirmStep(ctx, chatId, userId, agentName, workspace) {
4494
581
  });
4495
582
  }
4496
583
  async function createSessionDirect(ctx, core, chatId, agentName, workspace) {
4497
- log13.info({ userId: ctx.from?.id, agentName, workspace }, "New session command (direct)");
584
+ log2.info({ userId: ctx.from?.id, agentName, workspace }, "New session command (direct)");
4498
585
  let threadId;
4499
586
  try {
4500
587
  const topicName = `\u{1F504} New Session`;
@@ -4524,10 +611,10 @@ This is your coding session \u2014 chat here to work with the agent.`,
4524
611
  reply_markup: buildSessionControlKeyboard(session.id, false, false)
4525
612
  }
4526
613
  );
4527
- session.warmup().catch((err) => log13.error({ err }, "Warm-up error"));
614
+ session.warmup().catch((err) => log2.error({ err }, "Warm-up error"));
4528
615
  return threadId ?? null;
4529
616
  } catch (err) {
4530
- log13.error({ err }, "Session creation failed");
617
+ log2.error({ err }, "Session creation failed");
4531
618
  if (threadId) {
4532
619
  try {
4533
620
  await ctx.api.deleteForumTopic(chatId, threadId);
@@ -4605,7 +692,7 @@ async function handleNewChat(ctx, core, chatId) {
4605
692
  reply_markup: buildSessionControlKeyboard(session.id, false, false)
4606
693
  }
4607
694
  );
4608
- session.warmup().catch((err) => log13.error({ err }, "Warm-up error"));
695
+ session.warmup().catch((err) => log2.error({ err }, "Warm-up error"));
4609
696
  } catch (err) {
4610
697
  if (newThreadId) {
4611
698
  try {
@@ -4636,7 +723,7 @@ async function executeNewSession(bot, core, chatId, agentName, workspace) {
4636
723
  } });
4637
724
  const finalName = `\u{1F504} ${session.agentName} \u2014 New Session`;
4638
725
  await renameSessionTopic(bot, chatId, threadId, finalName);
4639
- session.warmup().catch((err) => log13.error({ err }, "Warm-up error"));
726
+ session.warmup().catch((err) => log2.error({ err }, "Warm-up error"));
4640
727
  return { session, threadId, firstMsgId };
4641
728
  } catch (err) {
4642
729
  try {
@@ -4784,7 +871,7 @@ Or just the folder name like <code>my-project</code> (will use ${core.configMana
4784
871
 
4785
872
  // src/adapters/telegram/commands/session.ts
4786
873
  import { InlineKeyboard as InlineKeyboard3 } from "grammy";
4787
- var log14 = createChildLogger({ module: "telegram-cmd-session" });
874
+ var log3 = createChildLogger({ module: "telegram-cmd-session" });
4788
875
  async function handleCancel(ctx, core, assistant) {
4789
876
  const threadId = ctx.message?.message_thread_id;
4790
877
  if (!threadId) return;
@@ -4802,14 +889,14 @@ async function handleCancel(ctx, core, assistant) {
4802
889
  String(threadId)
4803
890
  );
4804
891
  if (session) {
4805
- log14.info({ sessionId: session.id }, "Abort prompt command");
892
+ log3.info({ sessionId: session.id }, "Abort prompt command");
4806
893
  await session.abortPrompt();
4807
894
  await ctx.reply("\u26D4 Prompt aborted. Session is still active \u2014 send a new message to continue.", { parse_mode: "HTML" });
4808
895
  return;
4809
896
  }
4810
897
  const record = core.sessionManager.getRecordByThread("telegram", String(threadId));
4811
898
  if (record && record.status !== "error") {
4812
- log14.info({ sessionId: record.sessionId, status: record.status }, "Cancel command \u2014 no active prompt to abort");
899
+ log3.info({ sessionId: record.sessionId, status: record.status }, "Cancel command \u2014 no active prompt to abort");
4813
900
  await ctx.reply("\u2139\uFE0F No active prompt to cancel. Send a new message to resume the session.", { parse_mode: "HTML" });
4814
901
  }
4815
902
  }
@@ -4913,7 +1000,7 @@ ${lines.join("\n")}${truncated}`,
4913
1000
  { parse_mode: "HTML", reply_markup: keyboard }
4914
1001
  );
4915
1002
  } catch (err) {
4916
- log14.error({ err }, "handleTopics error");
1003
+ log3.error({ err }, "handleTopics error");
4917
1004
  await ctx.reply("\u274C Failed to list sessions.", { parse_mode: "HTML" }).catch(() => {
4918
1005
  });
4919
1006
  }
@@ -4934,13 +1021,13 @@ async function handleCleanup(ctx, core, chatId, statuses) {
4934
1021
  try {
4935
1022
  await ctx.api.deleteForumTopic(chatId, topicId);
4936
1023
  } catch (err) {
4937
- log14.warn({ err, sessionId: record.sessionId, topicId }, "Failed to delete forum topic during cleanup");
1024
+ log3.warn({ err, sessionId: record.sessionId, topicId }, "Failed to delete forum topic during cleanup");
4938
1025
  }
4939
1026
  }
4940
1027
  await core.sessionManager.removeRecord(record.sessionId);
4941
1028
  deleted++;
4942
1029
  } catch (err) {
4943
- log14.error({ err, sessionId: record.sessionId }, "Failed to cleanup session");
1030
+ log3.error({ err, sessionId: record.sessionId }, "Failed to cleanup session");
4944
1031
  failed++;
4945
1032
  }
4946
1033
  }
@@ -5011,7 +1098,7 @@ async function handleCleanupEverythingConfirmed(ctx, core, chatId, systemTopicId
5011
1098
  try {
5012
1099
  await core.sessionManager.cancelSession(record.sessionId);
5013
1100
  } catch (err) {
5014
- log14.warn({ err, sessionId: record.sessionId }, "Failed to cancel session during cleanup");
1101
+ log3.warn({ err, sessionId: record.sessionId }, "Failed to cancel session during cleanup");
5015
1102
  }
5016
1103
  }
5017
1104
  const topicId = record.platform?.topicId;
@@ -5019,13 +1106,13 @@ async function handleCleanupEverythingConfirmed(ctx, core, chatId, systemTopicId
5019
1106
  try {
5020
1107
  await ctx.api.deleteForumTopic(chatId, topicId);
5021
1108
  } catch (err) {
5022
- log14.warn({ err, sessionId: record.sessionId, topicId }, "Failed to delete forum topic during cleanup");
1109
+ log3.warn({ err, sessionId: record.sessionId, topicId }, "Failed to delete forum topic during cleanup");
5023
1110
  }
5024
1111
  }
5025
1112
  await core.sessionManager.removeRecord(record.sessionId);
5026
1113
  deleted++;
5027
1114
  } catch (err) {
5028
- log14.error({ err, sessionId: record.sessionId }, "Failed to cleanup session");
1115
+ log3.error({ err, sessionId: record.sessionId }, "Failed to cleanup session");
5029
1116
  failed++;
5030
1117
  }
5031
1118
  }
@@ -5347,8 +1434,8 @@ Downloading... ${bar} ${percent}%`, { parse_mode: "HTML" });
5347
1434
  };
5348
1435
  const result = await catalog.install(nameOrId, progress);
5349
1436
  if (result.ok) {
5350
- const { getAgentCapabilities: getAgentCapabilities2 } = await import("./agent-dependencies-QY5QSULV.js");
5351
- const caps = getAgentCapabilities2(result.agentKey);
1437
+ const { getAgentCapabilities } = await import("./agent-dependencies-QY5QSULV.js");
1438
+ const caps = getAgentCapabilities(result.agentKey);
5352
1439
  if (caps.integration) {
5353
1440
  const { installIntegration } = await import("./integrate-O4OCR4SN.js");
5354
1441
  const intResult = await installIntegration(result.agentKey, caps.integration);
@@ -5537,7 +1624,7 @@ ${resultText}`,
5537
1624
 
5538
1625
  // src/adapters/telegram/commands/settings.ts
5539
1626
  import { InlineKeyboard as InlineKeyboard6 } from "grammy";
5540
- var log15 = createChildLogger({ module: "telegram-settings" });
1627
+ var log4 = createChildLogger({ module: "telegram-settings" });
5541
1628
  function buildSettingsKeyboard(core) {
5542
1629
  const config = core.configManager.get();
5543
1630
  const fields = getSafeFields();
@@ -5600,7 +1687,7 @@ function setupSettingsCallbacks(bot, core, getAssistantSession) {
5600
1687
  } catch {
5601
1688
  }
5602
1689
  } catch (err) {
5603
- log15.error({ err, fieldPath }, "Failed to toggle config");
1690
+ log4.error({ err, fieldPath }, "Failed to toggle config");
5604
1691
  try {
5605
1692
  await ctx.answerCallbackQuery({ text: "\u274C Failed to update" });
5606
1693
  } catch {
@@ -5674,7 +1761,7 @@ Tap to change:`, {
5674
1761
  } catch {
5675
1762
  }
5676
1763
  } catch (err) {
5677
- log15.error({ err, fieldPath }, "Failed to set config");
1764
+ log4.error({ err, fieldPath }, "Failed to set config");
5678
1765
  try {
5679
1766
  await ctx.answerCallbackQuery({ text: "\u274C Failed to update" });
5680
1767
  } catch {
@@ -5746,7 +1833,7 @@ function buildNestedUpdate(dotPath, value) {
5746
1833
 
5747
1834
  // src/adapters/telegram/commands/doctor.ts
5748
1835
  import { InlineKeyboard as InlineKeyboard7 } from "grammy";
5749
- var log16 = createChildLogger({ module: "telegram-cmd-doctor" });
1836
+ var log5 = createChildLogger({ module: "telegram-cmd-doctor" });
5750
1837
  var pendingFixesStore = /* @__PURE__ */ new Map();
5751
1838
  function renderReport(report) {
5752
1839
  const icons = { pass: "\u2705", warn: "\u26A0\uFE0F", fail: "\u274C" };
@@ -5789,7 +1876,7 @@ async function handleDoctor(ctx) {
5789
1876
  reply_markup: keyboard
5790
1877
  });
5791
1878
  } catch (err) {
5792
- log16.error({ err }, "Doctor command failed");
1879
+ log5.error({ err }, "Doctor command failed");
5793
1880
  await ctx.api.editMessageText(
5794
1881
  ctx.chat.id,
5795
1882
  statusMsg.message_id,
@@ -5838,7 +1925,7 @@ function setupDoctorCallbacks(bot) {
5838
1925
  }
5839
1926
  }
5840
1927
  } catch (err) {
5841
- log16.error({ err, index }, "Doctor fix callback failed");
1928
+ log5.error({ err, index }, "Doctor fix callback failed");
5842
1929
  }
5843
1930
  });
5844
1931
  bot.callbackQuery("m:doctor", async (ctx) => {
@@ -5852,7 +1939,7 @@ function setupDoctorCallbacks(bot) {
5852
1939
 
5853
1940
  // src/adapters/telegram/commands/tunnel.ts
5854
1941
  import { InlineKeyboard as InlineKeyboard8 } from "grammy";
5855
- var log17 = createChildLogger({ module: "telegram-cmd-tunnel" });
1942
+ var log6 = createChildLogger({ module: "telegram-cmd-tunnel" });
5856
1943
  async function handleTunnel(ctx, core) {
5857
1944
  if (!core.tunnelService) {
5858
1945
  await ctx.reply("\u274C Tunnel service is not enabled.", { parse_mode: "HTML" });
@@ -6107,8 +2194,8 @@ var STATIC_COMMANDS = [
6107
2194
 
6108
2195
  // src/adapters/telegram/permissions.ts
6109
2196
  import { InlineKeyboard as InlineKeyboard9 } from "grammy";
6110
- import { nanoid as nanoid3 } from "nanoid";
6111
- var log18 = createChildLogger({ module: "telegram-permissions" });
2197
+ import { nanoid } from "nanoid";
2198
+ var log7 = createChildLogger({ module: "telegram-permissions" });
6112
2199
  var PermissionHandler = class {
6113
2200
  constructor(bot, chatId, getSession, sendNotification) {
6114
2201
  this.bot = bot;
@@ -6119,7 +2206,7 @@ var PermissionHandler = class {
6119
2206
  pending = /* @__PURE__ */ new Map();
6120
2207
  async sendPermissionRequest(session, request) {
6121
2208
  const threadId = Number(session.threadId);
6122
- const callbackKey = nanoid3(8);
2209
+ const callbackKey = nanoid(8);
6123
2210
  this.pending.set(callbackKey, {
6124
2211
  sessionId: session.id,
6125
2212
  requestId: request.id,
@@ -6168,7 +2255,7 @@ ${escapeHtml(request.description)}`,
6168
2255
  }
6169
2256
  const session = this.getSession(pending.sessionId);
6170
2257
  const isAllow = pending.options.find((o) => o.id === optionId)?.isAllow ?? false;
6171
- log18.info({ requestId: pending.requestId, optionId, isAllow }, "Permission responded");
2258
+ log7.info({ requestId: pending.requestId, optionId, isAllow }, "Permission responded");
6172
2259
  if (session?.permissionGate.requestId === pending.requestId) {
6173
2260
  session.permissionGate.resolve(optionId);
6174
2261
  }
@@ -6186,10 +2273,10 @@ ${escapeHtml(request.description)}`,
6186
2273
  };
6187
2274
 
6188
2275
  // src/adapters/telegram/assistant.ts
6189
- var log19 = createChildLogger({ module: "telegram-assistant" });
2276
+ var log8 = createChildLogger({ module: "telegram-assistant" });
6190
2277
  async function spawnAssistant(core, adapter, assistantTopicId) {
6191
2278
  const config = core.configManager.get();
6192
- log19.info({ agent: config.defaultAgent }, "Creating assistant session...");
2279
+ log8.info({ agent: config.defaultAgent }, "Creating assistant session...");
6193
2280
  const session = await core.createSession({
6194
2281
  channelId: "telegram",
6195
2282
  agentName: config.defaultAgent,
@@ -6198,7 +2285,7 @@ async function spawnAssistant(core, adapter, assistantTopicId) {
6198
2285
  // Prevent auto-naming from triggering after system prompt
6199
2286
  });
6200
2287
  session.threadId = String(assistantTopicId);
6201
- log19.info({ sessionId: session.id }, "Assistant agent spawned");
2288
+ log8.info({ sessionId: session.id }, "Assistant agent spawned");
6202
2289
  const allRecords = core.sessionManager.listRecords();
6203
2290
  const activeCount = allRecords.filter((r) => r.status === "active" || r.status === "initializing").length;
6204
2291
  const statusCounts = /* @__PURE__ */ new Map();
@@ -6219,9 +2306,9 @@ async function spawnAssistant(core, adapter, assistantTopicId) {
6219
2306
  };
6220
2307
  const systemPrompt = buildAssistantSystemPrompt(ctx);
6221
2308
  const ready = session.enqueuePrompt(systemPrompt).then(() => {
6222
- log19.info({ sessionId: session.id }, "Assistant system prompt completed");
2309
+ log8.info({ sessionId: session.id }, "Assistant system prompt completed");
6223
2310
  }).catch((err) => {
6224
- log19.warn({ err }, "Assistant system prompt failed");
2311
+ log8.warn({ err }, "Assistant system prompt failed");
6225
2312
  });
6226
2313
  return { session, ready };
6227
2314
  }
@@ -6387,7 +2474,7 @@ function redirectToAssistant(chatId, assistantTopicId) {
6387
2474
  }
6388
2475
 
6389
2476
  // src/adapters/telegram/activity.ts
6390
- var log20 = createChildLogger({ module: "telegram:activity" });
2477
+ var log9 = createChildLogger({ module: "telegram:activity" });
6391
2478
  var THINKING_REFRESH_MS = 15e3;
6392
2479
  var THINKING_MAX_MS = 3 * 60 * 1e3;
6393
2480
  var ThinkingIndicator = class {
@@ -6419,7 +2506,7 @@ var ThinkingIndicator = class {
6419
2506
  this.startRefreshTimer();
6420
2507
  }
6421
2508
  } catch (err) {
6422
- log20.warn({ err }, "ThinkingIndicator.show() failed");
2509
+ log9.warn({ err }, "ThinkingIndicator.show() failed");
6423
2510
  } finally {
6424
2511
  this.sending = false;
6425
2512
  }
@@ -6492,7 +2579,7 @@ var UsageMessage = class {
6492
2579
  if (result) this.msgId = result.message_id;
6493
2580
  }
6494
2581
  } catch (err) {
6495
- log20.warn({ err }, "UsageMessage.send() failed");
2582
+ log9.warn({ err }, "UsageMessage.send() failed");
6496
2583
  }
6497
2584
  }
6498
2585
  getMsgId() {
@@ -6505,7 +2592,7 @@ var UsageMessage = class {
6505
2592
  try {
6506
2593
  await this.sendQueue.enqueue(() => this.api.deleteMessage(this.chatId, id));
6507
2594
  } catch (err) {
6508
- log20.warn({ err }, "UsageMessage.delete() failed");
2595
+ log9.warn({ err }, "UsageMessage.delete() failed");
6509
2596
  }
6510
2597
  }
6511
2598
  };
@@ -6591,7 +2678,7 @@ var PlanCard = class {
6591
2678
  if (result) this.msgId = result.message_id;
6592
2679
  }
6593
2680
  } catch (err) {
6594
- log20.warn({ err }, "PlanCard flush failed");
2681
+ log9.warn({ err }, "PlanCard flush failed");
6595
2682
  }
6596
2683
  }
6597
2684
  };
@@ -6654,7 +2741,7 @@ var ActivityTracker = class {
6654
2741
  })
6655
2742
  );
6656
2743
  } catch (err) {
6657
- log20.warn({ err }, "ActivityTracker.onComplete() Done send failed");
2744
+ log9.warn({ err }, "ActivityTracker.onComplete() Done send failed");
6658
2745
  }
6659
2746
  }
6660
2747
  }
@@ -6681,19 +2768,19 @@ var TelegramSendQueue = class {
6681
2768
  enqueue(fn, opts) {
6682
2769
  const type = opts?.type ?? "other";
6683
2770
  const key = opts?.key;
6684
- return new Promise((resolve3, reject) => {
2771
+ return new Promise((resolve, reject) => {
6685
2772
  if (type === "text" && key) {
6686
2773
  const idx = this.items.findIndex(
6687
2774
  (item) => item.type === "text" && item.key === key
6688
2775
  );
6689
2776
  if (idx !== -1) {
6690
2777
  this.items[idx].resolve(void 0);
6691
- this.items[idx] = { fn, type, key, resolve: resolve3, reject };
2778
+ this.items[idx] = { fn, type, key, resolve, reject };
6692
2779
  this.scheduleProcess();
6693
2780
  return;
6694
2781
  }
6695
2782
  }
6696
- this.items.push({ fn, type, key, resolve: resolve3, reject });
2783
+ this.items.push({ fn, type, key, resolve, reject });
6697
2784
  this.scheduleProcess();
6698
2785
  });
6699
2786
  }
@@ -6736,7 +2823,7 @@ var TelegramSendQueue = class {
6736
2823
  };
6737
2824
 
6738
2825
  // src/adapters/telegram/action-detect.ts
6739
- import { nanoid as nanoid4 } from "nanoid";
2826
+ import { nanoid as nanoid2 } from "nanoid";
6740
2827
  import { InlineKeyboard as InlineKeyboard10 } from "grammy";
6741
2828
  var CMD_NEW_RE = /\/new(?:\s+([^\s\u0080-\uFFFF]+)(?:\s+([^\s\u0080-\uFFFF]+))?)?/;
6742
2829
  var CMD_CANCEL_RE = /\/cancel\b/;
@@ -6762,7 +2849,7 @@ function detectAction(text) {
6762
2849
  var ACTION_TTL_MS = 5 * 60 * 1e3;
6763
2850
  var actionMap = /* @__PURE__ */ new Map();
6764
2851
  function storeAction(action) {
6765
- const id = nanoid4(10);
2852
+ const id = nanoid2(10);
6766
2853
  actionMap.set(id, { action, createdAt: Date.now() });
6767
2854
  for (const [key, entry] of actionMap) {
6768
2855
  if (Date.now() - entry.createdAt > ACTION_TTL_MS) {
@@ -6893,7 +2980,7 @@ function setupActionCallbacks(bot, core, chatId, getAssistantSessionId) {
6893
2980
  }
6894
2981
 
6895
2982
  // src/adapters/telegram/tool-call-tracker.ts
6896
- var log21 = createChildLogger({ module: "tool-call-tracker" });
2983
+ var log10 = createChildLogger({ module: "tool-call-tracker" });
6897
2984
  var ToolCallTracker = class {
6898
2985
  constructor(bot, chatId, sendQueue) {
6899
2986
  this.bot = bot;
@@ -6937,7 +3024,7 @@ var ToolCallTracker = class {
6937
3024
  if (!toolState) return;
6938
3025
  if (meta.viewerLinks) {
6939
3026
  toolState.viewerLinks = meta.viewerLinks;
6940
- log21.debug({ toolId: meta.id, viewerLinks: meta.viewerLinks }, "Accumulated viewerLinks");
3027
+ log10.debug({ toolId: meta.id, viewerLinks: meta.viewerLinks }, "Accumulated viewerLinks");
6941
3028
  }
6942
3029
  if (meta.viewerFilePath) toolState.viewerFilePath = meta.viewerFilePath;
6943
3030
  if (meta.name) toolState.name = meta.name;
@@ -6945,7 +3032,7 @@ var ToolCallTracker = class {
6945
3032
  const isTerminal = meta.status === "completed" || meta.status === "failed";
6946
3033
  if (!isTerminal) return;
6947
3034
  await toolState.ready;
6948
- log21.debug(
3035
+ log10.debug(
6949
3036
  {
6950
3037
  toolId: meta.id,
6951
3038
  status: meta.status,
@@ -6974,7 +3061,7 @@ var ToolCallTracker = class {
6974
3061
  )
6975
3062
  );
6976
3063
  } catch (err) {
6977
- log21.warn(
3064
+ log10.warn(
6978
3065
  {
6979
3066
  err,
6980
3067
  msgId: toolState.msgId,
@@ -7245,7 +3332,7 @@ var DraftManager = class {
7245
3332
  };
7246
3333
 
7247
3334
  // src/adapters/telegram/skill-command-manager.ts
7248
- var log22 = createChildLogger({ module: "skill-commands" });
3335
+ var log11 = createChildLogger({ module: "skill-commands" });
7249
3336
  var SkillCommandManager = class {
7250
3337
  // sessionId → pinned msgId
7251
3338
  constructor(bot, chatId, sendQueue, sessionManager) {
@@ -7311,7 +3398,7 @@ var SkillCommandManager = class {
7311
3398
  disable_notification: true
7312
3399
  });
7313
3400
  } catch (err) {
7314
- log22.error({ err, sessionId }, "Failed to send skill commands");
3401
+ log11.error({ err, sessionId }, "Failed to send skill commands");
7315
3402
  }
7316
3403
  }
7317
3404
  async cleanup(sessionId) {
@@ -7340,7 +3427,7 @@ var SkillCommandManager = class {
7340
3427
  };
7341
3428
 
7342
3429
  // src/adapters/telegram/adapter.ts
7343
- var log23 = createChildLogger({ module: "telegram" });
3430
+ var log12 = createChildLogger({ module: "telegram" });
7344
3431
  function patchedFetch(input, init) {
7345
3432
  if (init?.signal && !(init.signal instanceof AbortSignal)) {
7346
3433
  const nativeController = new AbortController();
@@ -7404,7 +3491,7 @@ var TelegramAdapter = class extends ChannelAdapter {
7404
3491
  );
7405
3492
  this.bot.catch((err) => {
7406
3493
  const rootCause = err.error instanceof Error ? err.error : err;
7407
- log23.error({ err: rootCause }, "Telegram bot error");
3494
+ log12.error({ err: rootCause }, "Telegram bot error");
7408
3495
  });
7409
3496
  this.bot.api.config.use(async (prev, method, payload, signal) => {
7410
3497
  const maxRetries = 3;
@@ -7418,7 +3505,7 @@ var TelegramAdapter = class extends ChannelAdapter {
7418
3505
  if (rateLimitedMethods.includes(method)) {
7419
3506
  this.sendQueue.onRateLimited();
7420
3507
  }
7421
- log23.warn(
3508
+ log12.warn(
7422
3509
  { method, retryAfter, attempt: attempt + 1 },
7423
3510
  "Rate limited by Telegram, retrying"
7424
3511
  );
@@ -7529,8 +3616,8 @@ var TelegramAdapter = class extends ChannelAdapter {
7529
3616
  });
7530
3617
  return;
7531
3618
  }
7532
- const { getAgentCapabilities: getAgentCapabilities2 } = await import("./agent-registry-KZFSIRSJ.js");
7533
- const caps = getAgentCapabilities2(agentName);
3619
+ const { getAgentCapabilities } = await import("./agent-registry-KZFSIRSJ.js");
3620
+ const caps = getAgentCapabilities(agentName);
7534
3621
  if (!caps.supportsResume || !caps.resumeCommand) {
7535
3622
  await ctx.reply("This agent does not support session transfer.", {
7536
3623
  message_thread_id: threadId
@@ -7551,7 +3638,7 @@ var TelegramAdapter = class extends ChannelAdapter {
7551
3638
  this.setupRoutes();
7552
3639
  this.bot.start({
7553
3640
  allowed_updates: ["message", "callback_query"],
7554
- onStart: () => log23.info(
3641
+ onStart: () => log12.info(
7555
3642
  { chatId: this.telegramConfig.chatId },
7556
3643
  "Telegram bot started"
7557
3644
  )
@@ -7573,10 +3660,10 @@ var TelegramAdapter = class extends ChannelAdapter {
7573
3660
  reply_markup: buildMenuKeyboard()
7574
3661
  });
7575
3662
  } catch (err) {
7576
- log23.warn({ err }, "Failed to send welcome message");
3663
+ log12.warn({ err }, "Failed to send welcome message");
7577
3664
  }
7578
3665
  try {
7579
- log23.info("Spawning assistant session...");
3666
+ log12.info("Spawning assistant session...");
7580
3667
  const { session, ready } = await spawnAssistant(
7581
3668
  this.core,
7582
3669
  this,
@@ -7584,13 +3671,13 @@ var TelegramAdapter = class extends ChannelAdapter {
7584
3671
  );
7585
3672
  this.assistantSession = session;
7586
3673
  this.assistantInitializing = true;
7587
- log23.info({ sessionId: session.id }, "Assistant session ready, system prompt running in background");
3674
+ log12.info({ sessionId: session.id }, "Assistant session ready, system prompt running in background");
7588
3675
  ready.then(() => {
7589
3676
  this.assistantInitializing = false;
7590
- log23.info({ sessionId: session.id }, "Assistant ready for user messages");
3677
+ log12.info({ sessionId: session.id }, "Assistant ready for user messages");
7591
3678
  });
7592
3679
  } catch (err) {
7593
- log23.error({ err }, "Failed to spawn assistant");
3680
+ log12.error({ err }, "Failed to spawn assistant");
7594
3681
  this.bot.api.sendMessage(
7595
3682
  this.telegramConfig.chatId,
7596
3683
  `\u26A0\uFE0F <b>Failed to start assistant session.</b>
@@ -7606,7 +3693,7 @@ var TelegramAdapter = class extends ChannelAdapter {
7606
3693
  await this.assistantSession.destroy();
7607
3694
  }
7608
3695
  await this.bot.stop();
7609
- log23.info("Telegram bot stopped");
3696
+ log12.info("Telegram bot stopped");
7610
3697
  }
7611
3698
  setupRoutes() {
7612
3699
  this.bot.on("message:text", async (ctx) => {
@@ -7634,7 +3721,7 @@ var TelegramAdapter = class extends ChannelAdapter {
7634
3721
  ctx.replyWithChatAction("typing").catch(() => {
7635
3722
  });
7636
3723
  handleAssistantMessage(this.assistantSession, forwardText).catch(
7637
- (err) => log23.error({ err }, "Assistant error")
3724
+ (err) => log12.error({ err }, "Assistant error")
7638
3725
  );
7639
3726
  return;
7640
3727
  }
@@ -7651,7 +3738,7 @@ var TelegramAdapter = class extends ChannelAdapter {
7651
3738
  threadId: String(threadId),
7652
3739
  userId: String(ctx.from.id),
7653
3740
  text: forwardText
7654
- }).catch((err) => log23.error({ err }, "handleMessage error"));
3741
+ }).catch((err) => log12.error({ err }, "handleMessage error"));
7655
3742
  });
7656
3743
  this.bot.on("message:photo", async (ctx) => {
7657
3744
  const threadId = ctx.message.message_thread_id;
@@ -7793,7 +3880,7 @@ Task completed.
7793
3880
  if (!content.attachment) return;
7794
3881
  const { attachment } = content;
7795
3882
  if (attachment.size > 50 * 1024 * 1024) {
7796
- log23.warn({ sessionId: ctx.sessionId, fileName: attachment.fileName, size: attachment.size }, "File too large for Telegram (>50MB)");
3883
+ log12.warn({ sessionId: ctx.sessionId, fileName: attachment.fileName, size: attachment.size }, "File too large for Telegram (>50MB)");
7797
3884
  await this.sendQueue.enqueue(
7798
3885
  () => this.bot.api.sendMessage(
7799
3886
  this.telegramConfig.chatId,
@@ -7830,7 +3917,7 @@ Task completed.
7830
3917
  );
7831
3918
  }
7832
3919
  } catch (err) {
7833
- log23.error({ err, sessionId: ctx.sessionId, fileName: attachment.fileName }, "Failed to send attachment");
3920
+ log12.error({ err, sessionId: ctx.sessionId, fileName: attachment.fileName }, "Failed to send attachment");
7834
3921
  }
7835
3922
  },
7836
3923
  onSessionEnd: async (ctx, _content) => {
@@ -7898,14 +3985,14 @@ Task completed.
7898
3985
  if (session.archiving) return;
7899
3986
  const threadId = Number(session.threadId);
7900
3987
  if (!threadId || isNaN(threadId)) {
7901
- log23.warn({ sessionId, threadId: session.threadId }, "Session has no valid threadId, skipping message");
3988
+ log12.warn({ sessionId, threadId: session.threadId }, "Session has no valid threadId, skipping message");
7902
3989
  return;
7903
3990
  }
7904
3991
  const ctx = { sessionId, threadId };
7905
3992
  await dispatchMessage(this.messageHandlers, ctx, content);
7906
3993
  }
7907
3994
  async sendPermissionRequest(sessionId, request) {
7908
- log23.info({ sessionId, requestId: request.id }, "Permission request sent");
3995
+ log12.info({ sessionId, requestId: request.id }, "Permission request sent");
7909
3996
  const session = this.core.sessionManager.getSession(sessionId);
7910
3997
  if (!session) return;
7911
3998
  await this.sendQueue.enqueue(
@@ -7914,7 +4001,7 @@ Task completed.
7914
4001
  }
7915
4002
  async sendNotification(notification) {
7916
4003
  if (notification.sessionId === this.assistantSession?.id) return;
7917
- log23.info(
4004
+ log12.info(
7918
4005
  { sessionId: notification.sessionId, type: notification.type },
7919
4006
  "Notification sent"
7920
4007
  );
@@ -7950,7 +4037,7 @@ Task completed.
7950
4037
  );
7951
4038
  }
7952
4039
  async createSessionThread(sessionId, name) {
7953
- log23.info({ sessionId, name }, "Session topic created");
4040
+ log12.info({ sessionId, name }, "Session topic created");
7954
4041
  return String(
7955
4042
  await createSessionTopic(this.bot, this.telegramConfig.chatId, name)
7956
4043
  );
@@ -7974,7 +4061,7 @@ Task completed.
7974
4061
  try {
7975
4062
  await this.bot.api.deleteForumTopic(this.telegramConfig.chatId, topicId);
7976
4063
  } catch (err) {
7977
- log23.warn({ err, sessionId, topicId }, "Failed to delete forum topic (may already be deleted)");
4064
+ log12.warn({ err, sessionId, topicId }, "Failed to delete forum topic (may already be deleted)");
7978
4065
  }
7979
4066
  }
7980
4067
  async sendSkillCommands(sessionId, commands) {
@@ -7998,7 +4085,7 @@ Task completed.
7998
4085
  const buffer = Buffer.from(await response.arrayBuffer());
7999
4086
  return { buffer, filePath: file.file_path };
8000
4087
  } catch (err) {
8001
- log23.error({ err }, "Failed to download file from Telegram");
4088
+ log12.error({ err }, "Failed to download file from Telegram");
8002
4089
  return null;
8003
4090
  }
8004
4091
  }
@@ -8014,7 +4101,7 @@ Task completed.
8014
4101
  try {
8015
4102
  buffer = await this.fileService.convertOggToWav(buffer);
8016
4103
  } catch (err) {
8017
- log23.warn({ err }, "OGG\u2192WAV conversion failed, saving original OGG");
4104
+ log12.warn({ err }, "OGG\u2192WAV conversion failed, saving original OGG");
8018
4105
  fileName = "voice.ogg";
8019
4106
  mimeType = "audio/ogg";
8020
4107
  originalFilePath = void 0;
@@ -8040,7 +4127,7 @@ Task completed.
8040
4127
  userId: String(userId),
8041
4128
  text,
8042
4129
  attachments: [att]
8043
- }).catch((err) => log23.error({ err }, "handleMessage error"));
4130
+ }).catch((err) => log12.error({ err }, "handleMessage error"));
8044
4131
  }
8045
4132
  async cleanupSkillCommands(sessionId) {
8046
4133
  await this.skillManager.cleanup(sessionId);
@@ -8089,32 +4176,6 @@ Task completed.
8089
4176
  };
8090
4177
 
8091
4178
  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
4179
  TelegramAdapter
8119
4180
  };
8120
- //# sourceMappingURL=chunk-E56PPPAE.js.map
4181
+ //# sourceMappingURL=chunk-FW6HM4VU.js.map