@openacp/cli 0.6.10 → 2026.41.2

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 (133) hide show
  1. package/README.md +34 -16
  2. package/dist/cli.d.ts +11 -0
  3. package/dist/cli.js +28362 -449
  4. package/dist/cli.js.map +1 -1
  5. package/dist/data/registry-snapshot.json +1 -1
  6. package/dist/index.d.ts +1930 -467
  7. package/dist/index.js +17331 -102
  8. package/dist/index.js.map +1 -1
  9. package/package.json +13 -7
  10. package/dist/action-detect-P7ZE4NEM.js +0 -16
  11. package/dist/action-detect-P7ZE4NEM.js.map +0 -1
  12. package/dist/adapter-ZOANORGM.js +0 -799
  13. package/dist/adapter-ZOANORGM.js.map +0 -1
  14. package/dist/admin-6SYB6XCZ.js +0 -23
  15. package/dist/admin-6SYB6XCZ.js.map +0 -1
  16. package/dist/agent-catalog-FC3HGDEQ.js +0 -11
  17. package/dist/agent-catalog-FC3HGDEQ.js.map +0 -1
  18. package/dist/agent-dependencies-4OWBMZWZ.js +0 -24
  19. package/dist/agent-dependencies-4OWBMZWZ.js.map +0 -1
  20. package/dist/agent-registry-WT4NXPYG.js +0 -9
  21. package/dist/agent-registry-WT4NXPYG.js.map +0 -1
  22. package/dist/agent-store-VZLFPTZU.js +0 -9
  23. package/dist/agent-store-VZLFPTZU.js.map +0 -1
  24. package/dist/agents-QO7DKARJ.js +0 -15
  25. package/dist/agents-QO7DKARJ.js.map +0 -1
  26. package/dist/api-client-CFQT5U7D.js +0 -14
  27. package/dist/api-client-CFQT5U7D.js.map +0 -1
  28. package/dist/autostart-X33OGMX6.js +0 -23
  29. package/dist/autostart-X33OGMX6.js.map +0 -1
  30. package/dist/chunk-2CJ46J3C.js +0 -154
  31. package/dist/chunk-2CJ46J3C.js.map +0 -1
  32. package/dist/chunk-2HMQOC7N.js +0 -134
  33. package/dist/chunk-2HMQOC7N.js.map +0 -1
  34. package/dist/chunk-33RP6K2O.js +0 -435
  35. package/dist/chunk-33RP6K2O.js.map +0 -1
  36. package/dist/chunk-34M4OS5P.js +0 -83
  37. package/dist/chunk-34M4OS5P.js.map +0 -1
  38. package/dist/chunk-4CTX774K.js +0 -265
  39. package/dist/chunk-4CTX774K.js.map +0 -1
  40. package/dist/chunk-7QJS2XBD.js +0 -92
  41. package/dist/chunk-7QJS2XBD.js.map +0 -1
  42. package/dist/chunk-BNLGTZ34.js +0 -122
  43. package/dist/chunk-BNLGTZ34.js.map +0 -1
  44. package/dist/chunk-CS3KCJ5D.js +0 -4788
  45. package/dist/chunk-CS3KCJ5D.js.map +0 -1
  46. package/dist/chunk-GAK6PIBW.js +0 -224
  47. package/dist/chunk-GAK6PIBW.js.map +0 -1
  48. package/dist/chunk-I7WC6E5S.js +0 -71
  49. package/dist/chunk-I7WC6E5S.js.map +0 -1
  50. package/dist/chunk-J4SJTKIK.js +0 -203
  51. package/dist/chunk-J4SJTKIK.js.map +0 -1
  52. package/dist/chunk-JHYXKVV2.js +0 -183
  53. package/dist/chunk-JHYXKVV2.js.map +0 -1
  54. package/dist/chunk-JKBFUAJK.js +0 -282
  55. package/dist/chunk-JKBFUAJK.js.map +0 -1
  56. package/dist/chunk-KIRH7TUJ.js +0 -219
  57. package/dist/chunk-KIRH7TUJ.js.map +0 -1
  58. package/dist/chunk-LBIKITQT.js +0 -22
  59. package/dist/chunk-LBIKITQT.js.map +0 -1
  60. package/dist/chunk-LCRLAV4G.js +0 -1085
  61. package/dist/chunk-LCRLAV4G.js.map +0 -1
  62. package/dist/chunk-LGP2YGRL.js +0 -4880
  63. package/dist/chunk-LGP2YGRL.js.map +0 -1
  64. package/dist/chunk-MKHUZLII.js +0 -738
  65. package/dist/chunk-MKHUZLII.js.map +0 -1
  66. package/dist/chunk-NAMYZIS5.js +0 -1
  67. package/dist/chunk-NAMYZIS5.js.map +0 -1
  68. package/dist/chunk-NVPG6JCL.js +0 -724
  69. package/dist/chunk-NVPG6JCL.js.map +0 -1
  70. package/dist/chunk-O7CPGUAI.js +0 -298
  71. package/dist/chunk-O7CPGUAI.js.map +0 -1
  72. package/dist/chunk-OWP7RZ62.js +0 -697
  73. package/dist/chunk-OWP7RZ62.js.map +0 -1
  74. package/dist/chunk-S64CB6J3.js +0 -98
  75. package/dist/chunk-S64CB6J3.js.map +0 -1
  76. package/dist/chunk-UKT3G5IA.js +0 -484
  77. package/dist/chunk-UKT3G5IA.js.map +0 -1
  78. package/dist/chunk-V5GZQEIY.js +0 -101
  79. package/dist/chunk-V5GZQEIY.js.map +0 -1
  80. package/dist/chunk-VOIJ6OY4.js +0 -63
  81. package/dist/chunk-VOIJ6OY4.js.map +0 -1
  82. package/dist/chunk-VUNV25KB.js +0 -16
  83. package/dist/chunk-VUNV25KB.js.map +0 -1
  84. package/dist/chunk-W3EYKZNQ.js +0 -45
  85. package/dist/chunk-W3EYKZNQ.js.map +0 -1
  86. package/dist/chunk-WTZDAYZX.js +0 -172
  87. package/dist/chunk-WTZDAYZX.js.map +0 -1
  88. package/dist/chunk-XANPHG7W.js +0 -145
  89. package/dist/chunk-XANPHG7W.js.map +0 -1
  90. package/dist/config-6S355X75.js +0 -15
  91. package/dist/config-6S355X75.js.map +0 -1
  92. package/dist/config-editor-QQTZMWGD.js +0 -13
  93. package/dist/config-editor-QQTZMWGD.js.map +0 -1
  94. package/dist/config-registry-AHYI4MYL.js +0 -18
  95. package/dist/config-registry-AHYI4MYL.js.map +0 -1
  96. package/dist/daemon-4CS6HMB5.js +0 -30
  97. package/dist/daemon-4CS6HMB5.js.map +0 -1
  98. package/dist/discord-OMC52Y54.js +0 -2239
  99. package/dist/discord-OMC52Y54.js.map +0 -1
  100. package/dist/dist-UHQK5CXN.js +0 -21151
  101. package/dist/dist-UHQK5CXN.js.map +0 -1
  102. package/dist/doctor-HZZ5BSHB.js +0 -10
  103. package/dist/doctor-HZZ5BSHB.js.map +0 -1
  104. package/dist/doctor-OLYBO3V3.js +0 -15
  105. package/dist/doctor-OLYBO3V3.js.map +0 -1
  106. package/dist/install-cloudflared-Z7VCGOVG.js +0 -33
  107. package/dist/install-cloudflared-Z7VCGOVG.js.map +0 -1
  108. package/dist/install-jq-HUYSQWKR.js +0 -32
  109. package/dist/install-jq-HUYSQWKR.js.map +0 -1
  110. package/dist/integrate-PNEHRY2I.js +0 -373
  111. package/dist/integrate-PNEHRY2I.js.map +0 -1
  112. package/dist/log-NXABYJTT.js +0 -24
  113. package/dist/log-NXABYJTT.js.map +0 -1
  114. package/dist/main-XOZCLFUK.js +0 -238
  115. package/dist/main-XOZCLFUK.js.map +0 -1
  116. package/dist/menu-YY5MKHEK.js +0 -16
  117. package/dist/menu-YY5MKHEK.js.map +0 -1
  118. package/dist/new-session-FEO4J4VU.js +0 -17
  119. package/dist/new-session-FEO4J4VU.js.map +0 -1
  120. package/dist/post-upgrade-CJG5I7M2.js +0 -80
  121. package/dist/post-upgrade-CJG5I7M2.js.map +0 -1
  122. package/dist/session-IUSI7P5S.js +0 -20
  123. package/dist/session-IUSI7P5S.js.map +0 -1
  124. package/dist/settings-RQPAM4KC.js +0 -14
  125. package/dist/settings-RQPAM4KC.js.map +0 -1
  126. package/dist/setup-XHS4OMPM.js +0 -37
  127. package/dist/setup-XHS4OMPM.js.map +0 -1
  128. package/dist/suggest-7D6B542M.js +0 -38
  129. package/dist/suggest-7D6B542M.js.map +0 -1
  130. package/dist/tunnel-service-CJLUH6SZ.js +0 -1174
  131. package/dist/tunnel-service-CJLUH6SZ.js.map +0 -1
  132. package/dist/version-NQZBM5M7.js +0 -16
  133. package/dist/version-NQZBM5M7.js.map +0 -1
@@ -1,4880 +0,0 @@
1
- import {
2
- AgentCatalog
3
- } from "./chunk-UKT3G5IA.js";
4
- import {
5
- getAgentCapabilities
6
- } from "./chunk-JKBFUAJK.js";
7
- import {
8
- createChildLogger,
9
- createSessionLogger
10
- } from "./chunk-GAK6PIBW.js";
11
-
12
- // src/core/streams.ts
13
- function nodeToWebWritable(nodeStream) {
14
- return new WritableStream({
15
- write(chunk) {
16
- return new Promise((resolve3, reject) => {
17
- nodeStream.write(Buffer.from(chunk), (err) => {
18
- if (err) reject(err);
19
- else resolve3();
20
- });
21
- });
22
- }
23
- });
24
- }
25
- function nodeToWebReadable(nodeStream) {
26
- return new ReadableStream({
27
- start(controller) {
28
- nodeStream.on("data", (chunk) => {
29
- controller.enqueue(new Uint8Array(chunk));
30
- });
31
- nodeStream.on("end", () => controller.close());
32
- nodeStream.on("error", (err) => controller.error(err));
33
- }
34
- });
35
- }
36
-
37
- // src/core/stderr-capture.ts
38
- var StderrCapture = class {
39
- constructor(maxLines = 50) {
40
- this.maxLines = maxLines;
41
- }
42
- lines = [];
43
- append(chunk) {
44
- this.lines.push(...chunk.split("\n").filter(Boolean));
45
- if (this.lines.length > this.maxLines) {
46
- this.lines = this.lines.slice(-this.maxLines);
47
- }
48
- }
49
- getLastLines() {
50
- return this.lines.join("\n");
51
- }
52
- };
53
-
54
- // src/core/typed-emitter.ts
55
- var TypedEmitter = class {
56
- // eslint-disable-next-line @typescript-eslint/no-explicit-any
57
- listeners = /* @__PURE__ */ new Map();
58
- paused = false;
59
- buffer = [];
60
- on(event, listener) {
61
- let set = this.listeners.get(event);
62
- if (!set) {
63
- set = /* @__PURE__ */ new Set();
64
- this.listeners.set(event, set);
65
- }
66
- set.add(listener);
67
- return this;
68
- }
69
- off(event, listener) {
70
- this.listeners.get(event)?.delete(listener);
71
- return this;
72
- }
73
- emit(event, ...args) {
74
- if (this.paused) {
75
- if (this.passthroughFn?.(event, args)) {
76
- this.deliver(event, args);
77
- } else {
78
- this.buffer.push({ event, args });
79
- }
80
- return;
81
- }
82
- this.deliver(event, args);
83
- }
84
- /**
85
- * Pause event delivery. Events emitted while paused are buffered.
86
- * Optionally pass a filter to allow specific events through even while paused.
87
- */
88
- pause(passthrough) {
89
- this.paused = true;
90
- this.passthroughFn = passthrough;
91
- }
92
- passthroughFn;
93
- /** Resume event delivery and replay buffered events in order. */
94
- resume() {
95
- this.paused = false;
96
- this.passthroughFn = void 0;
97
- const buffered = this.buffer.splice(0);
98
- for (const { event, args } of buffered) {
99
- this.deliver(event, args);
100
- }
101
- }
102
- /** Discard all buffered events without delivering them. */
103
- clearBuffer() {
104
- this.buffer.length = 0;
105
- }
106
- get isPaused() {
107
- return this.paused;
108
- }
109
- get bufferSize() {
110
- return this.buffer.length;
111
- }
112
- removeAllListeners(event) {
113
- if (event) {
114
- this.listeners.delete(event);
115
- } else {
116
- this.listeners.clear();
117
- }
118
- }
119
- deliver(event, args) {
120
- const set = this.listeners.get(event);
121
- if (!set) return;
122
- for (const listener of set) {
123
- listener(...args);
124
- }
125
- }
126
- };
127
-
128
- // src/core/agent-instance.ts
129
- import { spawn, execFileSync } from "child_process";
130
- import { Transform } from "stream";
131
- import fs from "fs";
132
- import path from "path";
133
- import { randomUUID } from "crypto";
134
- import { ClientSideConnection, ndJsonStream } from "@agentclientprotocol/sdk";
135
- var log = createChildLogger({ module: "agent-instance" });
136
- function findPackageRoot(startDir) {
137
- let dir = startDir;
138
- while (dir !== path.dirname(dir)) {
139
- if (fs.existsSync(path.join(dir, "package.json"))) {
140
- return dir;
141
- }
142
- dir = path.dirname(dir);
143
- }
144
- return startDir;
145
- }
146
- function resolveAgentCommand(cmd) {
147
- const searchRoots = [process.cwd()];
148
- const ownDir = findPackageRoot(import.meta.dirname);
149
- if (ownDir !== process.cwd()) {
150
- searchRoots.push(ownDir);
151
- }
152
- for (const root of searchRoots) {
153
- const packageDirs = [
154
- path.resolve(root, "node_modules", "@zed-industries", cmd, "dist", "index.js"),
155
- path.resolve(root, "node_modules", cmd, "dist", "index.js")
156
- ];
157
- for (const jsPath of packageDirs) {
158
- if (fs.existsSync(jsPath)) {
159
- return { command: process.execPath, args: [jsPath] };
160
- }
161
- }
162
- }
163
- for (const root of searchRoots) {
164
- const localBin = path.resolve(root, "node_modules", ".bin", cmd);
165
- if (fs.existsSync(localBin)) {
166
- const content = fs.readFileSync(localBin, "utf-8");
167
- if (content.startsWith("#!/usr/bin/env node")) {
168
- return { command: process.execPath, args: [localBin] };
169
- }
170
- const match = content.match(/"([^"]+\.js)"/);
171
- if (match) {
172
- const target = path.resolve(path.dirname(localBin), match[1]);
173
- if (fs.existsSync(target)) {
174
- return { command: process.execPath, args: [target] };
175
- }
176
- }
177
- }
178
- }
179
- try {
180
- const fullPath = execFileSync("which", [cmd], { encoding: "utf-8" }).trim();
181
- if (fullPath) {
182
- const content = fs.readFileSync(fullPath, "utf-8");
183
- if (content.startsWith("#!/usr/bin/env node")) {
184
- return { command: process.execPath, args: [fullPath] };
185
- }
186
- }
187
- } catch {
188
- }
189
- return { command: cmd, args: [] };
190
- }
191
- var AgentInstance = class _AgentInstance extends TypedEmitter {
192
- connection;
193
- child;
194
- stderrCapture;
195
- terminals = /* @__PURE__ */ new Map();
196
- sessionId;
197
- agentName;
198
- promptCapabilities;
199
- // Callback — set by core when wiring events
200
- onPermissionRequest = async () => "";
201
- constructor(agentName) {
202
- super();
203
- this.agentName = agentName;
204
- }
205
- static async spawnSubprocess(agentDef, workingDirectory) {
206
- const instance = new _AgentInstance(agentDef.name);
207
- const resolved = resolveAgentCommand(agentDef.command);
208
- log.debug(
209
- {
210
- agentName: agentDef.name,
211
- command: resolved.command,
212
- args: resolved.args
213
- },
214
- "Resolved agent command"
215
- );
216
- instance.child = spawn(
217
- resolved.command,
218
- [...resolved.args, ...agentDef.args],
219
- {
220
- stdio: ["pipe", "pipe", "pipe"],
221
- cwd: workingDirectory,
222
- env: { ...process.env, ...agentDef.env }
223
- }
224
- );
225
- await new Promise((resolve3, reject) => {
226
- instance.child.on("error", (err) => {
227
- reject(
228
- new Error(
229
- `Failed to spawn agent "${agentDef.name}": ${err.message}. Is "${agentDef.command}" installed?`
230
- )
231
- );
232
- });
233
- instance.child.on("spawn", () => resolve3());
234
- });
235
- instance.stderrCapture = new StderrCapture(50);
236
- instance.child.stderr.on("data", (chunk) => {
237
- instance.stderrCapture.append(chunk.toString());
238
- });
239
- const stdinLogger = new Transform({
240
- transform(chunk, _enc, cb) {
241
- log.debug(
242
- { direction: "send", raw: chunk.toString().trimEnd() },
243
- "ACP raw"
244
- );
245
- cb(null, chunk);
246
- }
247
- });
248
- stdinLogger.pipe(instance.child.stdin);
249
- const stdoutLogger = new Transform({
250
- transform(chunk, _enc, cb) {
251
- log.debug(
252
- { direction: "recv", raw: chunk.toString().trimEnd() },
253
- "ACP raw"
254
- );
255
- cb(null, chunk);
256
- }
257
- });
258
- instance.child.stdout.pipe(stdoutLogger);
259
- const toAgent = nodeToWebWritable(stdinLogger);
260
- const fromAgent = nodeToWebReadable(stdoutLogger);
261
- const stream = ndJsonStream(toAgent, fromAgent);
262
- instance.connection = new ClientSideConnection(
263
- (_agent) => instance.createClient(_agent),
264
- stream
265
- );
266
- const initResponse = await instance.connection.initialize({
267
- protocolVersion: 1,
268
- clientCapabilities: {
269
- fs: { readTextFile: true, writeTextFile: true },
270
- terminal: true
271
- }
272
- });
273
- instance.promptCapabilities = initResponse.agentCapabilities?.promptCapabilities;
274
- log.info(
275
- { promptCapabilities: instance.promptCapabilities ?? {} },
276
- "Agent prompt capabilities"
277
- );
278
- return instance;
279
- }
280
- setupCrashDetection() {
281
- this.child.on("exit", (code, signal) => {
282
- log.info(
283
- { sessionId: this.sessionId, exitCode: code, signal },
284
- "Agent process exited"
285
- );
286
- if (code !== 0 && code !== null) {
287
- const stderr = this.stderrCapture.getLastLines();
288
- this.emit("agent_event", {
289
- type: "error",
290
- message: `Agent crashed (exit code ${code})
291
- ${stderr}`
292
- });
293
- }
294
- });
295
- this.connection.closed.then(() => {
296
- log.debug({ sessionId: this.sessionId }, "ACP connection closed");
297
- });
298
- }
299
- static async spawn(agentDef, workingDirectory) {
300
- log.debug(
301
- { agentName: agentDef.name, command: agentDef.command },
302
- "Spawning agent"
303
- );
304
- const spawnStart = Date.now();
305
- const instance = await _AgentInstance.spawnSubprocess(
306
- agentDef,
307
- workingDirectory
308
- );
309
- const response = await instance.connection.newSession({
310
- cwd: workingDirectory,
311
- mcpServers: []
312
- });
313
- instance.sessionId = response.sessionId;
314
- instance.setupCrashDetection();
315
- log.info(
316
- { sessionId: response.sessionId, durationMs: Date.now() - spawnStart },
317
- "Agent spawn complete"
318
- );
319
- return instance;
320
- }
321
- static async resume(agentDef, workingDirectory, agentSessionId) {
322
- log.debug({ agentName: agentDef.name, agentSessionId }, "Resuming agent");
323
- const spawnStart = Date.now();
324
- const instance = await _AgentInstance.spawnSubprocess(
325
- agentDef,
326
- workingDirectory
327
- );
328
- try {
329
- const response = await instance.connection.unstable_resumeSession({
330
- sessionId: agentSessionId,
331
- cwd: workingDirectory
332
- });
333
- instance.sessionId = response.sessionId;
334
- log.info(
335
- { sessionId: response.sessionId, durationMs: Date.now() - spawnStart },
336
- "Agent resume complete"
337
- );
338
- } catch (err) {
339
- log.warn(
340
- { err, agentSessionId },
341
- "Resume failed, falling back to new session"
342
- );
343
- const response = await instance.connection.newSession({
344
- cwd: workingDirectory,
345
- mcpServers: []
346
- });
347
- instance.sessionId = response.sessionId;
348
- log.info(
349
- { sessionId: response.sessionId, durationMs: Date.now() - spawnStart },
350
- "Agent fallback spawn complete"
351
- );
352
- }
353
- instance.setupCrashDetection();
354
- return instance;
355
- }
356
- // createClient — implemented in Task 6b
357
- createClient(_agent) {
358
- const self = this;
359
- const MAX_OUTPUT_BYTES = 1024 * 1024;
360
- return {
361
- // ── Session updates ──────────────────────────────────────────────────
362
- async sessionUpdate(params) {
363
- const update = params.update;
364
- let event = null;
365
- switch (update.sessionUpdate) {
366
- case "agent_message_chunk":
367
- if (update.content.type === "text") {
368
- event = { type: "text", content: update.content.text };
369
- } else if (update.content.type === "image") {
370
- const c = update.content;
371
- event = { type: "image_content", data: c.data, mimeType: c.mimeType };
372
- } else if (update.content.type === "audio") {
373
- const c = update.content;
374
- event = { type: "audio_content", data: c.data, mimeType: c.mimeType };
375
- }
376
- break;
377
- case "agent_thought_chunk":
378
- if (update.content.type === "text") {
379
- event = { type: "thought", content: update.content.text };
380
- }
381
- break;
382
- case "tool_call":
383
- event = {
384
- type: "tool_call",
385
- id: update.toolCallId,
386
- name: update.title,
387
- kind: update.kind ?? void 0,
388
- status: update.status ?? "pending",
389
- content: update.content ?? void 0,
390
- rawInput: update.rawInput ?? void 0,
391
- meta: update._meta ?? void 0
392
- };
393
- break;
394
- case "tool_call_update":
395
- event = {
396
- type: "tool_update",
397
- id: update.toolCallId,
398
- name: update.title ?? void 0,
399
- kind: update.kind ?? void 0,
400
- status: update.status ?? "pending",
401
- content: update.content ?? void 0,
402
- rawInput: update.rawInput ?? void 0,
403
- meta: update._meta ?? void 0
404
- };
405
- break;
406
- case "plan":
407
- event = { type: "plan", entries: update.entries };
408
- break;
409
- case "usage_update":
410
- event = {
411
- type: "usage",
412
- tokensUsed: update.used,
413
- contextSize: update.size,
414
- cost: update.cost ?? void 0
415
- };
416
- break;
417
- case "available_commands_update":
418
- event = {
419
- type: "commands_update",
420
- commands: update.availableCommands
421
- };
422
- break;
423
- default:
424
- return;
425
- }
426
- if (event !== null) {
427
- self.emit("agent_event", event);
428
- }
429
- },
430
- // ── Permission requests ──────────────────────────────────────────────
431
- async requestPermission(params) {
432
- const permissionRequest = {
433
- id: params.toolCall.toolCallId,
434
- description: params.toolCall.title ?? params.toolCall.toolCallId,
435
- options: params.options.map((opt) => ({
436
- id: opt.optionId,
437
- label: opt.name,
438
- isAllow: opt.kind === "allow_once" || opt.kind === "allow_always"
439
- }))
440
- };
441
- const selectedOptionId = await self.onPermissionRequest(permissionRequest);
442
- return {
443
- outcome: { outcome: "selected", optionId: selectedOptionId }
444
- };
445
- },
446
- // ── File operations ──────────────────────────────────────────────────
447
- async readTextFile(params) {
448
- const content = await fs.promises.readFile(params.path, "utf-8");
449
- return { content };
450
- },
451
- async writeTextFile(params) {
452
- await fs.promises.mkdir(path.dirname(params.path), { recursive: true });
453
- await fs.promises.writeFile(params.path, params.content, "utf-8");
454
- return {};
455
- },
456
- // ── Terminal operations ──────────────────────────────────────────────
457
- async createTerminal(params) {
458
- const terminalId = randomUUID();
459
- const args = params.args ?? [];
460
- const env = {};
461
- for (const ev of params.env ?? []) {
462
- env[ev.name] = ev.value;
463
- }
464
- const childProcess = spawn(params.command, args, {
465
- cwd: params.cwd ?? void 0,
466
- env: { ...process.env, ...env },
467
- shell: false
468
- });
469
- const state = {
470
- process: childProcess,
471
- output: "",
472
- exitStatus: null
473
- };
474
- self.terminals.set(terminalId, state);
475
- const outputByteLimit = params.outputByteLimit ?? MAX_OUTPUT_BYTES;
476
- const appendOutput = (chunk) => {
477
- state.output += chunk;
478
- const bytes = Buffer.byteLength(state.output, "utf-8");
479
- if (bytes > outputByteLimit) {
480
- const excess = bytes - outputByteLimit;
481
- state.output = state.output.slice(excess);
482
- }
483
- };
484
- childProcess.stdout?.on(
485
- "data",
486
- (chunk) => appendOutput(chunk.toString())
487
- );
488
- childProcess.stderr?.on(
489
- "data",
490
- (chunk) => appendOutput(chunk.toString())
491
- );
492
- childProcess.on("exit", (code, signal) => {
493
- state.exitStatus = { exitCode: code, signal };
494
- });
495
- return { terminalId };
496
- },
497
- async terminalOutput(params) {
498
- const state = self.terminals.get(params.terminalId);
499
- if (!state) {
500
- throw new Error(`Terminal not found: ${params.terminalId}`);
501
- }
502
- return {
503
- output: state.output,
504
- truncated: false,
505
- exitStatus: state.exitStatus ? {
506
- exitCode: state.exitStatus.exitCode,
507
- signal: state.exitStatus.signal
508
- } : void 0
509
- };
510
- },
511
- async waitForTerminalExit(params) {
512
- const state = self.terminals.get(params.terminalId);
513
- if (!state) {
514
- throw new Error(`Terminal not found: ${params.terminalId}`);
515
- }
516
- if (state.exitStatus !== null) {
517
- return {
518
- exitCode: state.exitStatus.exitCode,
519
- signal: state.exitStatus.signal
520
- };
521
- }
522
- return new Promise((resolve3) => {
523
- state.process.on("exit", (code, signal) => {
524
- resolve3({ exitCode: code, signal });
525
- });
526
- });
527
- },
528
- async killTerminal(params) {
529
- const state = self.terminals.get(params.terminalId);
530
- if (!state) {
531
- throw new Error(`Terminal not found: ${params.terminalId}`);
532
- }
533
- state.process.kill("SIGTERM");
534
- return {};
535
- },
536
- async releaseTerminal(params) {
537
- const state = self.terminals.get(params.terminalId);
538
- if (!state) {
539
- return;
540
- }
541
- state.process.kill("SIGKILL");
542
- self.terminals.delete(params.terminalId);
543
- }
544
- };
545
- }
546
- async prompt(text, attachments) {
547
- const contentBlocks = [{ type: "text", text }];
548
- const SUPPORTED_IMAGE_MIMES = /* @__PURE__ */ new Set(["image/jpeg", "image/png", "image/gif", "image/webp"]);
549
- for (const att of attachments ?? []) {
550
- const tooLarge = att.size > 10 * 1024 * 1024;
551
- if (att.type === "image" && this.promptCapabilities?.image && !tooLarge && SUPPORTED_IMAGE_MIMES.has(att.mimeType)) {
552
- const data = await fs.promises.readFile(att.filePath);
553
- contentBlocks.push({ type: "image", data: data.toString("base64"), mimeType: att.mimeType });
554
- } else if (att.type === "audio" && this.promptCapabilities?.audio && !tooLarge) {
555
- const data = await fs.promises.readFile(att.filePath);
556
- contentBlocks.push({ type: "audio", data: data.toString("base64"), mimeType: att.mimeType });
557
- } else {
558
- if ((att.type === "image" || att.type === "audio") && !tooLarge) {
559
- log.debug(
560
- { type: att.type, capabilities: this.promptCapabilities ?? {} },
561
- "Agent does not support %s content, falling back to file path",
562
- att.type
563
- );
564
- }
565
- contentBlocks[0].text += `
566
-
567
- [Attached file: ${att.filePath}]`;
568
- }
569
- }
570
- return this.connection.prompt({
571
- sessionId: this.sessionId,
572
- prompt: contentBlocks
573
- });
574
- }
575
- async cancel() {
576
- await this.connection.cancel({ sessionId: this.sessionId });
577
- }
578
- async destroy() {
579
- for (const [, t] of this.terminals) {
580
- t.process.kill("SIGKILL");
581
- }
582
- this.terminals.clear();
583
- this.child.kill("SIGTERM");
584
- setTimeout(() => {
585
- if (!this.child.killed) this.child.kill("SIGKILL");
586
- }, 1e4);
587
- }
588
- };
589
-
590
- // src/core/agent-manager.ts
591
- var AgentManager = class {
592
- constructor(catalog) {
593
- this.catalog = catalog;
594
- }
595
- getAvailableAgents() {
596
- const installed = this.catalog.getInstalledEntries();
597
- return Object.entries(installed).map(([key, agent]) => ({
598
- name: key,
599
- command: agent.command,
600
- args: agent.args,
601
- env: agent.env
602
- }));
603
- }
604
- getAgent(name) {
605
- return this.catalog.resolve(name);
606
- }
607
- async spawn(agentName, workingDirectory) {
608
- const agentDef = this.getAgent(agentName);
609
- if (!agentDef) throw new Error(`Agent "${agentName}" is not installed. Run "openacp agents install ${agentName}" to add it.`);
610
- return AgentInstance.spawn(agentDef, workingDirectory);
611
- }
612
- async resume(agentName, workingDirectory, agentSessionId) {
613
- const agentDef = this.getAgent(agentName);
614
- if (!agentDef) throw new Error(`Agent "${agentName}" is not installed. Run "openacp agents install ${agentName}" to add it.`);
615
- return AgentInstance.resume(agentDef, workingDirectory, agentSessionId);
616
- }
617
- };
618
-
619
- // src/core/prompt-queue.ts
620
- var PromptQueue = class {
621
- constructor(processor, onError) {
622
- this.processor = processor;
623
- this.onError = onError;
624
- }
625
- queue = [];
626
- processing = false;
627
- abortController = null;
628
- async enqueue(text, attachments) {
629
- if (this.processing) {
630
- return new Promise((resolve3) => {
631
- this.queue.push({ text, attachments, resolve: resolve3 });
632
- });
633
- }
634
- await this.process(text, attachments);
635
- }
636
- async process(text, attachments) {
637
- this.processing = true;
638
- this.abortController = new AbortController();
639
- const { signal } = this.abortController;
640
- try {
641
- await Promise.race([
642
- this.processor(text, attachments),
643
- new Promise((_, reject) => {
644
- signal.addEventListener("abort", () => reject(new Error("Prompt aborted")), { once: true });
645
- })
646
- ]);
647
- } catch (err) {
648
- if (!(err instanceof Error && err.message === "Prompt aborted")) {
649
- this.onError?.(err);
650
- }
651
- } finally {
652
- this.abortController = null;
653
- this.processing = false;
654
- this.drainNext();
655
- }
656
- }
657
- drainNext() {
658
- const next = this.queue.shift();
659
- if (next) {
660
- this.process(next.text, next.attachments).then(next.resolve);
661
- }
662
- }
663
- clear() {
664
- if (this.abortController) {
665
- this.abortController.abort();
666
- }
667
- for (const item of this.queue) {
668
- item.resolve();
669
- }
670
- this.queue = [];
671
- }
672
- get pending() {
673
- return this.queue.length;
674
- }
675
- get isProcessing() {
676
- return this.processing;
677
- }
678
- };
679
-
680
- // src/core/permission-gate.ts
681
- var DEFAULT_TIMEOUT_MS = 10 * 60 * 1e3;
682
- var PermissionGate = class {
683
- request;
684
- resolveFn;
685
- rejectFn;
686
- settled = false;
687
- timeoutTimer;
688
- timeoutMs;
689
- constructor(timeoutMs) {
690
- this.timeoutMs = timeoutMs ?? DEFAULT_TIMEOUT_MS;
691
- }
692
- setPending(request) {
693
- this.request = request;
694
- this.settled = false;
695
- this.clearTimeout();
696
- return new Promise((resolve3, reject) => {
697
- this.resolveFn = resolve3;
698
- this.rejectFn = reject;
699
- this.timeoutTimer = setTimeout(() => {
700
- this.reject("Permission request timed out (no response received)");
701
- }, this.timeoutMs);
702
- });
703
- }
704
- resolve(optionId) {
705
- if (this.settled || !this.resolveFn) return;
706
- this.settled = true;
707
- this.clearTimeout();
708
- this.resolveFn(optionId);
709
- this.cleanup();
710
- }
711
- reject(reason) {
712
- if (this.settled || !this.rejectFn) return;
713
- this.settled = true;
714
- this.clearTimeout();
715
- this.rejectFn(new Error(reason ?? "Permission rejected"));
716
- this.cleanup();
717
- }
718
- get isPending() {
719
- return !!this.request && !this.settled;
720
- }
721
- get currentRequest() {
722
- return this.isPending ? this.request : void 0;
723
- }
724
- /** The request ID of the current pending request, undefined after settlement */
725
- get requestId() {
726
- return this.request?.id;
727
- }
728
- clearTimeout() {
729
- if (this.timeoutTimer) {
730
- clearTimeout(this.timeoutTimer);
731
- this.timeoutTimer = void 0;
732
- }
733
- }
734
- cleanup() {
735
- this.request = void 0;
736
- this.resolveFn = void 0;
737
- this.rejectFn = void 0;
738
- }
739
- };
740
-
741
- // src/core/session.ts
742
- import { nanoid } from "nanoid";
743
- import * as fs2 from "fs";
744
- var moduleLog = createChildLogger({ module: "session" });
745
- var TTS_PROMPT_INSTRUCTION = `
746
-
747
- 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.`;
748
- var TTS_BLOCK_REGEX = /\[TTS\]([\s\S]*?)\[\/TTS\]/;
749
- var TTS_MAX_LENGTH = 5e3;
750
- var TTS_TIMEOUT_MS = 3e4;
751
- var VALID_TRANSITIONS = {
752
- initializing: /* @__PURE__ */ new Set(["active", "error"]),
753
- active: /* @__PURE__ */ new Set(["error", "finished", "cancelled"]),
754
- error: /* @__PURE__ */ new Set(["active"]),
755
- cancelled: /* @__PURE__ */ new Set(["active"]),
756
- finished: /* @__PURE__ */ new Set()
757
- };
758
- var Session = class extends TypedEmitter {
759
- id;
760
- channelId;
761
- threadId = "";
762
- agentName;
763
- workingDirectory;
764
- agentInstance;
765
- agentSessionId = "";
766
- _status = "initializing";
767
- name;
768
- createdAt = /* @__PURE__ */ new Date();
769
- voiceMode = "off";
770
- dangerousMode = false;
771
- archiving = false;
772
- promptCount = 0;
773
- log;
774
- permissionGate = new PermissionGate();
775
- queue;
776
- speechService;
777
- pendingContext = null;
778
- constructor(opts) {
779
- super();
780
- this.id = opts.id || nanoid(12);
781
- this.channelId = opts.channelId;
782
- this.agentName = opts.agentName;
783
- this.workingDirectory = opts.workingDirectory;
784
- this.agentInstance = opts.agentInstance;
785
- this.speechService = opts.speechService;
786
- this.log = createSessionLogger(this.id, moduleLog);
787
- this.log.info({ agentName: this.agentName }, "Session created");
788
- this.queue = new PromptQueue(
789
- (text, attachments) => this.processPrompt(text, attachments),
790
- (err) => {
791
- this.fail("Prompt execution failed");
792
- this.log.error({ err }, "Prompt execution failed");
793
- }
794
- );
795
- }
796
- // --- State Machine ---
797
- get status() {
798
- return this._status;
799
- }
800
- /** Transition to active — from initializing, error, or cancelled */
801
- activate() {
802
- this.transition("active");
803
- }
804
- /** Transition to error — from initializing or active */
805
- fail(reason) {
806
- this.transition("error");
807
- this.emit("error", new Error(reason));
808
- }
809
- /** Transition to finished — from active only. Emits session_end for backward compat. */
810
- finish(reason) {
811
- this.transition("finished");
812
- this.emit("session_end", reason ?? "completed");
813
- }
814
- /** Transition to cancelled — from active only (terminal session cancel) */
815
- markCancelled() {
816
- this.transition("cancelled");
817
- }
818
- transition(to) {
819
- const from = this._status;
820
- const allowed = VALID_TRANSITIONS[from];
821
- if (!allowed?.has(to)) {
822
- throw new Error(
823
- `Invalid session transition: ${from} \u2192 ${to}`
824
- );
825
- }
826
- this._status = to;
827
- this.log.debug({ from, to }, "Session status transition");
828
- this.emit("status_change", from, to);
829
- }
830
- /** Number of prompts waiting in queue */
831
- get queueDepth() {
832
- return this.queue.pending;
833
- }
834
- get promptRunning() {
835
- return this.queue.isProcessing;
836
- }
837
- // --- Context Injection ---
838
- setContext(markdown) {
839
- this.pendingContext = markdown;
840
- }
841
- // --- Voice Mode ---
842
- setVoiceMode(mode) {
843
- this.voiceMode = mode;
844
- this.log.info({ voiceMode: mode }, "TTS mode changed");
845
- }
846
- // --- Public API ---
847
- async enqueuePrompt(text, attachments) {
848
- await this.queue.enqueue(text, attachments);
849
- }
850
- async processPrompt(text, attachments) {
851
- if (text === "\0__warmup__") {
852
- await this.runWarmup();
853
- return;
854
- }
855
- this.promptCount++;
856
- if (this._status === "initializing") {
857
- this.activate();
858
- }
859
- const promptStart = Date.now();
860
- this.log.debug("Prompt execution started");
861
- if (this.pendingContext) {
862
- text = `[CONVERSATION HISTORY - This is context from previous sessions, not current conversation]
863
-
864
- ${this.pendingContext}
865
-
866
- [END CONVERSATION HISTORY]
867
-
868
- ${text}`;
869
- this.pendingContext = null;
870
- this.log.debug("Context injected into prompt");
871
- }
872
- const processed = await this.maybeTranscribeAudio(text, attachments);
873
- const ttsActive = this.voiceMode !== "off" && !!this.speechService?.isTTSAvailable();
874
- if (ttsActive) {
875
- processed.text += TTS_PROMPT_INSTRUCTION;
876
- if (this.voiceMode === "next") {
877
- this.voiceMode = "off";
878
- }
879
- }
880
- let accumulatedText = "";
881
- const accumulatorListener = ttsActive ? (event) => {
882
- if (event.type === "text") {
883
- accumulatedText += event.content;
884
- }
885
- } : null;
886
- if (accumulatorListener) {
887
- this.on("agent_event", accumulatorListener);
888
- }
889
- try {
890
- await this.agentInstance.prompt(processed.text, processed.attachments);
891
- } finally {
892
- if (accumulatorListener) {
893
- this.off("agent_event", accumulatorListener);
894
- }
895
- }
896
- this.log.info(
897
- { durationMs: Date.now() - promptStart },
898
- "Prompt execution completed"
899
- );
900
- if (ttsActive && accumulatedText) {
901
- this.processTTSResponse(accumulatedText).catch((err) => {
902
- this.log.warn({ err }, "TTS post-processing failed");
903
- });
904
- }
905
- if (!this.name) {
906
- await this.autoName();
907
- }
908
- }
909
- async maybeTranscribeAudio(text, attachments) {
910
- if (!attachments?.length || !this.speechService) {
911
- return { text, attachments };
912
- }
913
- const hasAudioCapability = this.agentInstance.promptCapabilities?.audio === true;
914
- if (hasAudioCapability) {
915
- return { text, attachments };
916
- }
917
- if (!this.speechService.isSTTAvailable()) {
918
- return { text, attachments };
919
- }
920
- let transcribedText = text;
921
- const remainingAttachments = [];
922
- for (const att of attachments) {
923
- if (att.type !== "audio") {
924
- remainingAttachments.push(att);
925
- continue;
926
- }
927
- try {
928
- const audioPath = att.originalFilePath || att.filePath;
929
- const audioMime = att.originalFilePath ? "audio/ogg" : att.mimeType;
930
- const audioBuffer = await fs2.promises.readFile(audioPath);
931
- const result = await this.speechService.transcribe(audioBuffer, audioMime);
932
- this.log.info({ provider: "stt", duration: result.duration }, "Voice transcribed");
933
- this.emit("agent_event", {
934
- type: "system_message",
935
- message: `\u{1F3A4} You said: ${result.text}`
936
- });
937
- transcribedText = transcribedText.replace(/\[Audio:\s*[^\]]*\]\s*/g, "").trim();
938
- transcribedText = transcribedText ? `${transcribedText}
939
- ${result.text}` : result.text;
940
- } catch (err) {
941
- this.log.warn({ err }, "STT transcription failed, keeping audio attachment");
942
- this.emit("agent_event", {
943
- type: "error",
944
- message: `Voice transcription failed: ${err.message}`
945
- });
946
- remainingAttachments.push(att);
947
- }
948
- }
949
- return {
950
- text: transcribedText,
951
- attachments: remainingAttachments.length > 0 ? remainingAttachments : void 0
952
- };
953
- }
954
- async processTTSResponse(responseText) {
955
- const match = TTS_BLOCK_REGEX.exec(responseText);
956
- if (!match?.[1]) {
957
- this.log.debug("No [TTS] block found in response, skipping synthesis");
958
- return;
959
- }
960
- let ttsText = match[1].trim();
961
- if (!ttsText) return;
962
- if (ttsText.length > TTS_MAX_LENGTH) {
963
- ttsText = ttsText.slice(0, TTS_MAX_LENGTH);
964
- }
965
- try {
966
- const timeoutPromise = new Promise(
967
- (_, reject) => setTimeout(() => reject(new Error("TTS synthesis timed out")), TTS_TIMEOUT_MS)
968
- );
969
- const result = await Promise.race([
970
- this.speechService.synthesize(ttsText),
971
- timeoutPromise
972
- ]);
973
- const base64 = result.audioBuffer.toString("base64");
974
- this.emit("agent_event", {
975
- type: "audio_content",
976
- data: base64,
977
- mimeType: result.mimeType
978
- });
979
- this.log.info("TTS synthesis completed");
980
- } catch (err) {
981
- this.log.warn({ err }, "TTS synthesis failed, skipping");
982
- }
983
- }
984
- // NOTE: This injects a summary prompt into the agent's conversation history.
985
- async autoName() {
986
- let title = "";
987
- const captureHandler = (event) => {
988
- if (event.type === "text") title += event.content;
989
- };
990
- this.pause((event) => event !== "agent_event");
991
- this.agentInstance.on("agent_event", captureHandler);
992
- try {
993
- await this.agentInstance.prompt(
994
- "Summarize this conversation in max 5 words for a topic title. Reply ONLY with the title, nothing else."
995
- );
996
- this.name = title.trim().slice(0, 50) || `Session ${this.id.slice(0, 6)}`;
997
- this.log.info({ name: this.name }, "Session auto-named");
998
- this.emit("named", this.name);
999
- } catch {
1000
- this.name = `Session ${this.id.slice(0, 6)}`;
1001
- } finally {
1002
- this.agentInstance.off("agent_event", captureHandler);
1003
- this.clearBuffer();
1004
- this.resume();
1005
- }
1006
- }
1007
- async generateSummary(timeoutMs = 15e3) {
1008
- let summary = "";
1009
- let timer;
1010
- const captureHandler = (event) => {
1011
- if (event.type === "text") summary += event.content;
1012
- };
1013
- this.pause((event) => event !== "agent_event");
1014
- this.agentInstance.on("agent_event", captureHandler);
1015
- try {
1016
- const promptPromise = this.agentInstance.prompt(
1017
- "Summarize what you've accomplished so far in this session in 2-3 sentences. Include: key files changed, decisions made, and current status. Reply ONLY with the summary, nothing else."
1018
- );
1019
- const timeoutPromise = new Promise((_, reject) => {
1020
- timer = setTimeout(() => reject(new Error("summary timeout")), timeoutMs);
1021
- });
1022
- await Promise.race([promptPromise, timeoutPromise]);
1023
- return summary.trim().slice(0, 500);
1024
- } catch {
1025
- this.log.warn("Failed to generate session summary");
1026
- return "";
1027
- } finally {
1028
- if (timer) clearTimeout(timer);
1029
- this.agentInstance.off("agent_event", captureHandler);
1030
- this.clearBuffer();
1031
- this.resume();
1032
- }
1033
- }
1034
- /** Fire-and-forget warm-up: primes model cache while user types their first message */
1035
- async warmup() {
1036
- await this.queue.enqueue("\0__warmup__");
1037
- }
1038
- async runWarmup() {
1039
- this.pause((_event, args) => {
1040
- const agentEvent = args[0];
1041
- return agentEvent?.type === "commands_update";
1042
- });
1043
- try {
1044
- const start = Date.now();
1045
- await this.agentInstance.prompt('Reply with only "ready".');
1046
- this.activate();
1047
- this.log.info({ durationMs: Date.now() - start }, "Warm-up complete");
1048
- } catch (err) {
1049
- this.log.error({ err }, "Warm-up failed");
1050
- } finally {
1051
- this.clearBuffer();
1052
- this.resume();
1053
- }
1054
- }
1055
- /** Cancel the current prompt and clear the queue. Stays in active state. */
1056
- async abortPrompt() {
1057
- this.queue.clear();
1058
- this.log.info("Prompt aborted");
1059
- await this.agentInstance.cancel();
1060
- }
1061
- async destroy() {
1062
- this.log.info("Session destroyed");
1063
- await this.agentInstance.destroy();
1064
- }
1065
- };
1066
-
1067
- // src/core/session-manager.ts
1068
- var SessionManager = class {
1069
- sessions = /* @__PURE__ */ new Map();
1070
- store;
1071
- eventBus;
1072
- setEventBus(eventBus) {
1073
- this.eventBus = eventBus;
1074
- }
1075
- constructor(store = null) {
1076
- this.store = store;
1077
- }
1078
- async createSession(channelId, agentName, workingDirectory, agentManager) {
1079
- const agentInstance = await agentManager.spawn(agentName, workingDirectory);
1080
- const session = new Session({
1081
- channelId,
1082
- agentName,
1083
- workingDirectory,
1084
- agentInstance
1085
- });
1086
- this.sessions.set(session.id, session);
1087
- session.agentSessionId = session.agentInstance.sessionId;
1088
- if (this.store) {
1089
- await this.store.save({
1090
- sessionId: session.id,
1091
- agentSessionId: session.agentInstance.sessionId,
1092
- agentName: session.agentName,
1093
- workingDir: session.workingDirectory,
1094
- channelId,
1095
- status: session.status,
1096
- createdAt: session.createdAt.toISOString(),
1097
- lastActiveAt: (/* @__PURE__ */ new Date()).toISOString(),
1098
- name: session.name,
1099
- dangerousMode: false,
1100
- platform: {}
1101
- });
1102
- }
1103
- return session;
1104
- }
1105
- getSession(sessionId) {
1106
- return this.sessions.get(sessionId);
1107
- }
1108
- getSessionByThread(channelId, threadId) {
1109
- for (const session of this.sessions.values()) {
1110
- if (session.channelId === channelId && session.threadId === threadId) {
1111
- return session;
1112
- }
1113
- }
1114
- return void 0;
1115
- }
1116
- getSessionByAgentSessionId(agentSessionId) {
1117
- for (const session of this.sessions.values()) {
1118
- if (session.agentSessionId === agentSessionId) {
1119
- return session;
1120
- }
1121
- }
1122
- return void 0;
1123
- }
1124
- getRecordByAgentSessionId(agentSessionId) {
1125
- return this.store?.findByAgentSessionId(agentSessionId);
1126
- }
1127
- getRecordByThread(channelId, threadId) {
1128
- return this.store?.findByPlatform(
1129
- channelId,
1130
- (p) => String(p.topicId) === threadId || p.threadId === threadId
1131
- );
1132
- }
1133
- registerSession(session) {
1134
- this.sessions.set(session.id, session);
1135
- }
1136
- async patchRecord(sessionId, patch) {
1137
- if (!this.store) return;
1138
- const record = this.store.get(sessionId);
1139
- if (record) {
1140
- await this.store.save({ ...record, ...patch });
1141
- } else if (patch.sessionId) {
1142
- await this.store.save(patch);
1143
- }
1144
- }
1145
- getSessionRecord(sessionId) {
1146
- return this.store?.get(sessionId);
1147
- }
1148
- async cancelSession(sessionId) {
1149
- const session = this.sessions.get(sessionId);
1150
- if (session) {
1151
- await session.abortPrompt();
1152
- session.markCancelled();
1153
- }
1154
- if (this.store) {
1155
- const record = this.store.get(sessionId);
1156
- if (record && record.status !== "cancelled") {
1157
- await this.store.save({ ...record, status: "cancelled" });
1158
- }
1159
- }
1160
- }
1161
- listSessions(channelId) {
1162
- const all = Array.from(this.sessions.values());
1163
- if (channelId) return all.filter((s) => s.channelId === channelId);
1164
- return all;
1165
- }
1166
- listRecords(filter) {
1167
- if (!this.store) return [];
1168
- let records = this.store.list();
1169
- if (filter?.statuses?.length) {
1170
- records = records.filter((r) => filter.statuses.includes(r.status));
1171
- }
1172
- return records;
1173
- }
1174
- async removeRecord(sessionId) {
1175
- if (!this.store) return;
1176
- await this.store.remove(sessionId);
1177
- this.eventBus?.emit("session:deleted", { sessionId });
1178
- }
1179
- async destroyAll() {
1180
- if (this.store) {
1181
- for (const session of this.sessions.values()) {
1182
- const record = this.store.get(session.id);
1183
- if (record) {
1184
- await this.store.save({ ...record, status: "finished" });
1185
- }
1186
- }
1187
- }
1188
- for (const session of this.sessions.values()) {
1189
- await session.destroy();
1190
- }
1191
- this.sessions.clear();
1192
- }
1193
- };
1194
-
1195
- // src/core/file-service.ts
1196
- import fs3 from "fs";
1197
- import path2 from "path";
1198
- import { OggOpusDecoder } from "ogg-opus-decoder";
1199
- import wav from "node-wav";
1200
- var MIME_TO_EXT = {
1201
- "image/jpeg": ".jpg",
1202
- "image/png": ".png",
1203
- "image/gif": ".gif",
1204
- "image/webp": ".webp",
1205
- "image/svg+xml": ".svg",
1206
- "audio/ogg": ".ogg",
1207
- "audio/mpeg": ".mp3",
1208
- "audio/wav": ".wav",
1209
- "audio/webm": ".webm",
1210
- "audio/mp4": ".m4a",
1211
- "video/mp4": ".mp4",
1212
- "video/webm": ".webm",
1213
- "application/pdf": ".pdf",
1214
- "text/plain": ".txt"
1215
- };
1216
- var EXT_TO_MIME = {
1217
- ".jpg": "image/jpeg",
1218
- ".jpeg": "image/jpeg",
1219
- ".png": "image/png",
1220
- ".gif": "image/gif",
1221
- ".webp": "image/webp",
1222
- ".svg": "image/svg+xml",
1223
- ".ogg": "audio/ogg",
1224
- ".oga": "audio/ogg",
1225
- ".mp3": "audio/mpeg",
1226
- ".wav": "audio/wav",
1227
- ".m4a": "audio/mp4",
1228
- ".mp4": "video/mp4",
1229
- ".pdf": "application/pdf",
1230
- ".txt": "text/plain"
1231
- };
1232
- function classifyMime(mimeType) {
1233
- if (mimeType.startsWith("image/")) return "image";
1234
- if (mimeType.startsWith("audio/")) return "audio";
1235
- return "file";
1236
- }
1237
- var FileService = class {
1238
- constructor(baseDir) {
1239
- this.baseDir = baseDir;
1240
- }
1241
- async saveFile(sessionId, fileName, data, mimeType) {
1242
- const sessionDir = path2.join(this.baseDir, sessionId);
1243
- await fs3.promises.mkdir(sessionDir, { recursive: true });
1244
- const safeName = `${Date.now()}-${fileName.replace(/[^a-zA-Z0-9._-]/g, "_")}`;
1245
- const filePath = path2.join(sessionDir, safeName);
1246
- await fs3.promises.writeFile(filePath, data);
1247
- return {
1248
- type: classifyMime(mimeType),
1249
- filePath,
1250
- fileName,
1251
- mimeType,
1252
- size: data.length
1253
- };
1254
- }
1255
- async resolveFile(filePath) {
1256
- try {
1257
- const stat = await fs3.promises.stat(filePath);
1258
- if (!stat.isFile()) return null;
1259
- const ext = path2.extname(filePath).toLowerCase();
1260
- const mimeType = EXT_TO_MIME[ext] || "application/octet-stream";
1261
- return {
1262
- type: classifyMime(mimeType),
1263
- filePath,
1264
- fileName: path2.basename(filePath),
1265
- mimeType,
1266
- size: stat.size
1267
- };
1268
- } catch {
1269
- return null;
1270
- }
1271
- }
1272
- /**
1273
- * Convert OGG Opus audio to WAV format.
1274
- * Telegram voice messages use OGG Opus which many AI agents can't read.
1275
- */
1276
- async convertOggToWav(oggData) {
1277
- const decoder = new OggOpusDecoder();
1278
- await decoder.ready;
1279
- try {
1280
- const { channelData, sampleRate } = await decoder.decode(new Uint8Array(oggData));
1281
- const wavData = wav.encode(channelData, { sampleRate, float: true, bitDepth: 32 });
1282
- return Buffer.from(wavData);
1283
- } finally {
1284
- decoder.free();
1285
- }
1286
- }
1287
- static extensionFromMime(mimeType) {
1288
- return MIME_TO_EXT[mimeType] || ".bin";
1289
- }
1290
- };
1291
-
1292
- // src/core/session-bridge.ts
1293
- var log2 = createChildLogger({ module: "session-bridge" });
1294
- var SessionBridge = class {
1295
- constructor(session, adapter, deps) {
1296
- this.session = session;
1297
- this.adapter = adapter;
1298
- this.deps = deps;
1299
- }
1300
- connected = false;
1301
- agentEventHandler;
1302
- sessionEventHandler;
1303
- statusChangeHandler;
1304
- namedHandler;
1305
- connect() {
1306
- if (this.connected) return;
1307
- this.connected = true;
1308
- this.wireAgentToSession();
1309
- this.wireSessionToAdapter();
1310
- this.wirePermissions();
1311
- this.wireLifecycle();
1312
- }
1313
- disconnect() {
1314
- if (!this.connected) return;
1315
- this.connected = false;
1316
- if (this.agentEventHandler) {
1317
- this.session.agentInstance.off("agent_event", this.agentEventHandler);
1318
- }
1319
- if (this.sessionEventHandler) {
1320
- this.session.off("agent_event", this.sessionEventHandler);
1321
- }
1322
- if (this.statusChangeHandler) {
1323
- this.session.off("status_change", this.statusChangeHandler);
1324
- }
1325
- if (this.namedHandler) {
1326
- this.session.off("named", this.namedHandler);
1327
- }
1328
- this.session.agentInstance.onPermissionRequest = async () => "";
1329
- }
1330
- wireAgentToSession() {
1331
- this.agentEventHandler = (event) => {
1332
- this.session.emit("agent_event", event);
1333
- };
1334
- this.session.agentInstance.on("agent_event", this.agentEventHandler);
1335
- }
1336
- wireSessionToAdapter() {
1337
- const session = this.session;
1338
- const ctx = {
1339
- get id() {
1340
- return session.id;
1341
- },
1342
- get workingDirectory() {
1343
- return session.workingDirectory;
1344
- }
1345
- };
1346
- this.sessionEventHandler = (event) => {
1347
- switch (event.type) {
1348
- case "text":
1349
- case "thought":
1350
- case "tool_call":
1351
- case "tool_update":
1352
- case "plan":
1353
- case "usage":
1354
- this.adapter.sendMessage(
1355
- this.session.id,
1356
- this.deps.messageTransformer.transform(event, ctx)
1357
- );
1358
- break;
1359
- case "session_end":
1360
- this.session.finish(event.reason);
1361
- this.adapter.cleanupSkillCommands(this.session.id);
1362
- this.adapter.sendMessage(
1363
- this.session.id,
1364
- this.deps.messageTransformer.transform(event)
1365
- );
1366
- this.deps.notificationManager.notify(this.session.channelId, {
1367
- sessionId: this.session.id,
1368
- sessionName: this.session.name,
1369
- type: "completed",
1370
- summary: `Session "${this.session.name || this.session.id}" completed
1371
- \u23F1 ${Math.round((Date.now() - this.session.createdAt.getTime()) / 6e4)} min \xB7 \u{1F4AC} ${this.session.promptCount} prompts`
1372
- });
1373
- break;
1374
- case "error":
1375
- this.session.fail(event.message);
1376
- this.adapter.cleanupSkillCommands(this.session.id);
1377
- this.adapter.sendMessage(
1378
- this.session.id,
1379
- this.deps.messageTransformer.transform(event)
1380
- );
1381
- this.deps.notificationManager.notify(this.session.channelId, {
1382
- sessionId: this.session.id,
1383
- sessionName: this.session.name,
1384
- type: "error",
1385
- summary: event.message
1386
- });
1387
- break;
1388
- case "image_content": {
1389
- if (this.deps.fileService) {
1390
- const fs9 = this.deps.fileService;
1391
- const sid = this.session.id;
1392
- const { data, mimeType } = event;
1393
- const buffer = Buffer.from(data, "base64");
1394
- const ext = FileService.extensionFromMime(mimeType);
1395
- fs9.saveFile(sid, `agent-image${ext}`, buffer, mimeType).then((att) => {
1396
- this.adapter.sendMessage(sid, {
1397
- type: "attachment",
1398
- text: "",
1399
- attachment: att
1400
- });
1401
- }).catch((err) => log2.error({ err }, "Failed to save agent image"));
1402
- }
1403
- break;
1404
- }
1405
- case "audio_content": {
1406
- if (this.deps.fileService) {
1407
- const fs9 = this.deps.fileService;
1408
- const sid = this.session.id;
1409
- const { data, mimeType } = event;
1410
- const buffer = Buffer.from(data, "base64");
1411
- const ext = FileService.extensionFromMime(mimeType);
1412
- fs9.saveFile(sid, `agent-audio${ext}`, buffer, mimeType).then((att) => {
1413
- this.adapter.sendMessage(sid, {
1414
- type: "attachment",
1415
- text: "",
1416
- attachment: att
1417
- });
1418
- }).catch((err) => log2.error({ err }, "Failed to save agent audio"));
1419
- }
1420
- break;
1421
- }
1422
- case "commands_update":
1423
- log2.debug({ commands: event.commands }, "Commands available");
1424
- this.adapter.sendSkillCommands(this.session.id, event.commands);
1425
- break;
1426
- case "system_message":
1427
- this.adapter.sendMessage(
1428
- this.session.id,
1429
- this.deps.messageTransformer.transform(event)
1430
- );
1431
- break;
1432
- }
1433
- this.deps.eventBus?.emit("agent:event", {
1434
- sessionId: this.session.id,
1435
- event
1436
- });
1437
- };
1438
- this.session.on("agent_event", this.sessionEventHandler);
1439
- }
1440
- wirePermissions() {
1441
- this.session.agentInstance.onPermissionRequest = async (request) => {
1442
- this.session.emit("permission_request", request);
1443
- this.deps.eventBus?.emit("permission:request", {
1444
- sessionId: this.session.id,
1445
- permission: request
1446
- });
1447
- if (request.description.toLowerCase().includes("openacp")) {
1448
- const allowOption = request.options.find((o) => o.isAllow);
1449
- if (allowOption) {
1450
- log2.info(
1451
- { sessionId: this.session.id, requestId: request.id },
1452
- "Auto-approving openacp command"
1453
- );
1454
- return allowOption.id;
1455
- }
1456
- }
1457
- if (this.session.dangerousMode) {
1458
- const allowOption = request.options.find((o) => o.isAllow);
1459
- if (allowOption) {
1460
- log2.info(
1461
- { sessionId: this.session.id, requestId: request.id, optionId: allowOption.id },
1462
- "Dangerous mode: auto-approving permission"
1463
- );
1464
- return allowOption.id;
1465
- }
1466
- }
1467
- const promise = this.session.permissionGate.setPending(request);
1468
- await this.adapter.sendPermissionRequest(this.session.id, request);
1469
- return promise;
1470
- };
1471
- }
1472
- wireLifecycle() {
1473
- this.statusChangeHandler = (from, to) => {
1474
- this.deps.sessionManager.patchRecord(this.session.id, {
1475
- status: to,
1476
- lastActiveAt: (/* @__PURE__ */ new Date()).toISOString()
1477
- });
1478
- this.deps.eventBus?.emit("session:updated", {
1479
- sessionId: this.session.id,
1480
- status: to
1481
- });
1482
- if (to === "finished" || to === "cancelled") {
1483
- queueMicrotask(() => this.disconnect());
1484
- }
1485
- };
1486
- this.session.on("status_change", this.statusChangeHandler);
1487
- this.namedHandler = (name) => {
1488
- this.deps.sessionManager.patchRecord(this.session.id, { name });
1489
- this.deps.eventBus?.emit("session:updated", {
1490
- sessionId: this.session.id,
1491
- name
1492
- });
1493
- this.adapter.renameSessionThread(this.session.id, name);
1494
- };
1495
- this.session.on("named", this.namedHandler);
1496
- }
1497
- };
1498
-
1499
- // src/core/notification.ts
1500
- var NotificationManager = class {
1501
- constructor(adapters) {
1502
- this.adapters = adapters;
1503
- }
1504
- async notify(channelId, notification) {
1505
- const adapter = this.adapters.get(channelId);
1506
- if (adapter) {
1507
- await adapter.sendNotification(notification);
1508
- }
1509
- }
1510
- async notifyAll(notification) {
1511
- for (const adapter of this.adapters.values()) {
1512
- await adapter.sendNotification(notification);
1513
- }
1514
- }
1515
- };
1516
-
1517
- // src/tunnel/extract-file-info.ts
1518
- function extractFileInfo(name, kind, content, rawInput, meta) {
1519
- if (kind && !["read", "edit", "write"].includes(kind)) return null;
1520
- let info = null;
1521
- if (meta) {
1522
- const m = meta;
1523
- const claudeCode = m?.claudeCode;
1524
- const tr = claudeCode?.toolResponse;
1525
- const file = tr?.file;
1526
- if (typeof file?.filePath === "string" && typeof file?.content === "string") {
1527
- info = { filePath: file.filePath, content: file.content };
1528
- }
1529
- if (!info && typeof tr?.filePath === "string" && typeof tr?.content === "string") {
1530
- info = { filePath: tr.filePath, content: tr.content };
1531
- }
1532
- }
1533
- if (!info && rawInput && typeof rawInput === "object") {
1534
- const ri = rawInput;
1535
- const filePath = ri?.file_path || ri?.filePath || ri?.path;
1536
- if (typeof filePath === "string") {
1537
- const parsed = content ? parseContent(content) : null;
1538
- const riContent = typeof ri?.content === "string" ? ri.content : void 0;
1539
- info = { filePath, content: parsed?.content || riContent, oldContent: parsed?.oldContent };
1540
- }
1541
- }
1542
- if (!info && content) {
1543
- info = parseContent(content);
1544
- }
1545
- if (!info) return null;
1546
- if (!info.filePath) {
1547
- const pathMatch = name.match(/(?:Read|Edit|Write|View)\s+(.+)/i);
1548
- if (pathMatch) info.filePath = pathMatch[1].trim();
1549
- }
1550
- if (!info.filePath || !info.content) return null;
1551
- return info;
1552
- }
1553
- function parseContent(content) {
1554
- if (typeof content === "string") {
1555
- return { content };
1556
- }
1557
- if (Array.isArray(content)) {
1558
- for (const block of content) {
1559
- const result = parseContent(block);
1560
- if (result?.content || result?.filePath) return result;
1561
- }
1562
- return null;
1563
- }
1564
- if (typeof content === "object" && content !== null) {
1565
- const c = content;
1566
- if (c.type === "diff" && typeof c.path === "string") {
1567
- const newText = c.newText;
1568
- const oldText = c.oldText;
1569
- if (newText) {
1570
- return {
1571
- filePath: c.path,
1572
- content: newText,
1573
- oldContent: oldText ?? void 0
1574
- };
1575
- }
1576
- }
1577
- if (c.type === "content" && c.content) {
1578
- return parseContent(c.content);
1579
- }
1580
- if (c.type === "text" && typeof c.text === "string") {
1581
- return { content: c.text, filePath: c.filePath };
1582
- }
1583
- if (typeof c.text === "string") {
1584
- return { content: c.text, filePath: c.filePath };
1585
- }
1586
- if (typeof c.file_path === "string" || typeof c.filePath === "string" || typeof c.path === "string") {
1587
- const filePath = c.file_path || c.filePath || c.path;
1588
- const fileContent = c.content || c.text || c.output || c.newText;
1589
- if (typeof fileContent === "string") {
1590
- return {
1591
- filePath,
1592
- content: fileContent,
1593
- oldContent: c.old_content || c.oldText
1594
- };
1595
- }
1596
- }
1597
- if (c.input) {
1598
- const result = parseContent(c.input);
1599
- if (result) return result;
1600
- }
1601
- if (c.output) {
1602
- const result = parseContent(c.output);
1603
- if (result) return result;
1604
- }
1605
- }
1606
- return null;
1607
- }
1608
-
1609
- // src/core/message-transformer.ts
1610
- var log3 = createChildLogger({ module: "message-transformer" });
1611
- var MessageTransformer = class {
1612
- constructor(tunnelService) {
1613
- this.tunnelService = tunnelService;
1614
- }
1615
- transform(event, sessionContext) {
1616
- switch (event.type) {
1617
- case "text":
1618
- return { type: "text", text: event.content };
1619
- case "thought":
1620
- return { type: "thought", text: event.content };
1621
- case "tool_call": {
1622
- const meta = event.meta;
1623
- const metadata = {
1624
- id: event.id,
1625
- name: event.name,
1626
- kind: event.kind,
1627
- status: event.status,
1628
- content: event.content,
1629
- locations: event.locations,
1630
- rawInput: event.rawInput,
1631
- displaySummary: meta?.displaySummary,
1632
- displayTitle: meta?.displayTitle,
1633
- displayKind: meta?.displayKind
1634
- };
1635
- this.enrichWithViewerLinks(event, metadata, sessionContext);
1636
- return { type: "tool_call", text: event.name, metadata };
1637
- }
1638
- case "tool_update": {
1639
- const meta = event.meta;
1640
- const metadata = {
1641
- id: event.id,
1642
- name: event.name,
1643
- kind: event.kind,
1644
- status: event.status,
1645
- content: event.content,
1646
- rawInput: event.rawInput,
1647
- displaySummary: meta?.displaySummary,
1648
- displayTitle: meta?.displayTitle,
1649
- displayKind: meta?.displayKind
1650
- };
1651
- this.enrichWithViewerLinks(event, metadata, sessionContext);
1652
- return { type: "tool_update", text: "", metadata };
1653
- }
1654
- case "plan":
1655
- return {
1656
- type: "plan",
1657
- text: "",
1658
- metadata: { entries: event.entries }
1659
- };
1660
- case "usage":
1661
- return {
1662
- type: "usage",
1663
- text: "",
1664
- metadata: {
1665
- tokensUsed: event.tokensUsed,
1666
- contextSize: event.contextSize,
1667
- cost: event.cost
1668
- }
1669
- };
1670
- case "session_end":
1671
- return { type: "session_end", text: `Done (${event.reason})` };
1672
- case "error":
1673
- return { type: "error", text: event.message };
1674
- case "system_message":
1675
- return { type: "system_message", text: event.message };
1676
- default:
1677
- return { type: "text", text: "" };
1678
- }
1679
- }
1680
- enrichWithViewerLinks(event, metadata, sessionContext) {
1681
- if (!this.tunnelService || !sessionContext) return;
1682
- const name = "name" in event ? event.name || "" : "";
1683
- const kind = "kind" in event ? event.kind : void 0;
1684
- log3.debug(
1685
- { name, kind, status: event.status, hasContent: !!event.content },
1686
- "enrichWithViewerLinks: inspecting event"
1687
- );
1688
- const fileInfo = extractFileInfo(
1689
- name,
1690
- kind,
1691
- event.content,
1692
- event.rawInput,
1693
- event.meta
1694
- );
1695
- if (!fileInfo) return;
1696
- log3.info(
1697
- {
1698
- name,
1699
- kind,
1700
- filePath: fileInfo.filePath,
1701
- hasOldContent: !!fileInfo.oldContent
1702
- },
1703
- "enrichWithViewerLinks: extracted file info"
1704
- );
1705
- const store = this.tunnelService.getStore();
1706
- const viewerLinks = {};
1707
- if (fileInfo.oldContent) {
1708
- const id2 = store.storeDiff(
1709
- sessionContext.id,
1710
- fileInfo.filePath,
1711
- fileInfo.oldContent,
1712
- fileInfo.content,
1713
- sessionContext.workingDirectory
1714
- );
1715
- if (id2) viewerLinks.diff = this.tunnelService.diffUrl(id2);
1716
- }
1717
- const id = store.storeFile(
1718
- sessionContext.id,
1719
- fileInfo.filePath,
1720
- fileInfo.content,
1721
- sessionContext.workingDirectory
1722
- );
1723
- if (id) viewerLinks.file = this.tunnelService.fileUrl(id);
1724
- if (Object.keys(viewerLinks).length > 0) {
1725
- metadata.viewerLinks = viewerLinks;
1726
- metadata.viewerFilePath = fileInfo.filePath;
1727
- }
1728
- }
1729
- };
1730
-
1731
- // src/core/usage-store.ts
1732
- import fs4 from "fs";
1733
- import path3 from "path";
1734
- var log4 = createChildLogger({ module: "usage-store" });
1735
- var DEBOUNCE_MS = 2e3;
1736
- var UsageStore = class {
1737
- constructor(filePath, retentionDays) {
1738
- this.filePath = filePath;
1739
- this.retentionDays = retentionDays;
1740
- this.load();
1741
- this.cleanup();
1742
- this.cleanupInterval = setInterval(
1743
- () => this.cleanup(),
1744
- 24 * 60 * 60 * 1e3
1745
- );
1746
- this.flushHandler = () => {
1747
- try {
1748
- this.flushSync();
1749
- } catch {
1750
- }
1751
- };
1752
- process.on("SIGTERM", this.flushHandler);
1753
- process.on("SIGINT", this.flushHandler);
1754
- process.on("exit", this.flushHandler);
1755
- }
1756
- records = [];
1757
- debounceTimer = null;
1758
- cleanupInterval = null;
1759
- flushHandler = null;
1760
- append(record) {
1761
- this.records.push(record);
1762
- this.scheduleDiskWrite();
1763
- }
1764
- query(period) {
1765
- const cutoff = this.getCutoff(period);
1766
- const filtered = cutoff ? this.records.filter((r) => new Date(r.timestamp).getTime() >= cutoff) : this.records;
1767
- const totalTokens = filtered.reduce((sum, r) => sum + r.tokensUsed, 0);
1768
- const totalCost = filtered.reduce(
1769
- (sum, r) => sum + (r.cost?.amount ?? 0),
1770
- 0
1771
- );
1772
- const sessionIds = new Set(filtered.map((r) => r.sessionId));
1773
- const currency = filtered.find((r) => r.cost?.currency)?.cost?.currency ?? "USD";
1774
- return {
1775
- period,
1776
- totalTokens,
1777
- totalCost,
1778
- currency,
1779
- sessionCount: sessionIds.size,
1780
- recordCount: filtered.length
1781
- };
1782
- }
1783
- getMonthlyTotal() {
1784
- const now = /* @__PURE__ */ new Date();
1785
- const startOfMonth = new Date(now.getFullYear(), now.getMonth(), 1);
1786
- const cutoff = startOfMonth.getTime();
1787
- const filtered = this.records.filter(
1788
- (r) => new Date(r.timestamp).getTime() >= cutoff
1789
- );
1790
- const totalCost = filtered.reduce(
1791
- (sum, r) => sum + (r.cost?.amount ?? 0),
1792
- 0
1793
- );
1794
- const currency = filtered.find((r) => r.cost?.currency)?.cost?.currency ?? "USD";
1795
- return { totalCost, currency };
1796
- }
1797
- cleanup() {
1798
- const cutoff = Date.now() - this.retentionDays * 24 * 60 * 60 * 1e3;
1799
- const before = this.records.length;
1800
- this.records = this.records.filter(
1801
- (r) => new Date(r.timestamp).getTime() >= cutoff
1802
- );
1803
- const removed = before - this.records.length;
1804
- if (removed > 0) {
1805
- log4.info({ removed }, "Cleaned up expired usage records");
1806
- this.scheduleDiskWrite();
1807
- }
1808
- }
1809
- flushSync() {
1810
- if (this.debounceTimer) {
1811
- clearTimeout(this.debounceTimer);
1812
- this.debounceTimer = null;
1813
- }
1814
- const data = { version: 1, records: this.records };
1815
- const dir = path3.dirname(this.filePath);
1816
- if (!fs4.existsSync(dir)) fs4.mkdirSync(dir, { recursive: true });
1817
- fs4.writeFileSync(this.filePath, JSON.stringify(data, null, 2));
1818
- }
1819
- destroy() {
1820
- if (this.debounceTimer) this.flushSync();
1821
- if (this.cleanupInterval) clearInterval(this.cleanupInterval);
1822
- if (this.flushHandler) {
1823
- process.removeListener("SIGTERM", this.flushHandler);
1824
- process.removeListener("SIGINT", this.flushHandler);
1825
- process.removeListener("exit", this.flushHandler);
1826
- this.flushHandler = null;
1827
- }
1828
- }
1829
- load() {
1830
- if (!fs4.existsSync(this.filePath)) return;
1831
- try {
1832
- const raw = JSON.parse(
1833
- fs4.readFileSync(this.filePath, "utf-8")
1834
- );
1835
- if (raw.version !== 1) {
1836
- log4.warn(
1837
- { version: raw.version },
1838
- "Unknown usage store version, skipping load"
1839
- );
1840
- return;
1841
- }
1842
- this.records = raw.records || [];
1843
- log4.debug({ count: this.records.length }, "Loaded usage records");
1844
- } catch (err) {
1845
- log4.error({ err }, "Failed to load usage store, backing up corrupt file");
1846
- try {
1847
- fs4.copyFileSync(this.filePath, this.filePath + ".bak");
1848
- } catch {
1849
- }
1850
- this.records = [];
1851
- }
1852
- }
1853
- getCutoff(period) {
1854
- const now = /* @__PURE__ */ new Date();
1855
- switch (period) {
1856
- case "today": {
1857
- const start = new Date(now);
1858
- start.setHours(0, 0, 0, 0);
1859
- return start.getTime();
1860
- }
1861
- case "week":
1862
- return Date.now() - 7 * 24 * 60 * 60 * 1e3;
1863
- case "month": {
1864
- const startOfMonth = new Date(now.getFullYear(), now.getMonth(), 1);
1865
- return startOfMonth.getTime();
1866
- }
1867
- case "all":
1868
- return null;
1869
- }
1870
- }
1871
- scheduleDiskWrite() {
1872
- if (this.debounceTimer) clearTimeout(this.debounceTimer);
1873
- this.debounceTimer = setTimeout(() => {
1874
- this.flushSync();
1875
- }, DEBOUNCE_MS);
1876
- }
1877
- };
1878
-
1879
- // src/core/usage-budget.ts
1880
- var UsageBudget = class {
1881
- constructor(store, config, now = () => /* @__PURE__ */ new Date()) {
1882
- this.store = store;
1883
- this.config = config;
1884
- this.now = now;
1885
- this.lastNotifiedMonth = this.now().getMonth();
1886
- }
1887
- lastNotifiedStatus = "ok";
1888
- lastNotifiedMonth;
1889
- check() {
1890
- if (!this.config.monthlyBudget) {
1891
- return { status: "ok" };
1892
- }
1893
- const currentMonth = this.now().getMonth();
1894
- if (currentMonth !== this.lastNotifiedMonth) {
1895
- this.lastNotifiedStatus = "ok";
1896
- this.lastNotifiedMonth = currentMonth;
1897
- }
1898
- const { totalCost } = this.store.getMonthlyTotal();
1899
- const budget = this.config.monthlyBudget;
1900
- const threshold = this.config.warningThreshold;
1901
- let status;
1902
- if (totalCost >= budget) {
1903
- status = "exceeded";
1904
- } else if (totalCost >= threshold * budget) {
1905
- status = "warning";
1906
- } else {
1907
- status = "ok";
1908
- }
1909
- let message;
1910
- if (status !== "ok" && status !== this.lastNotifiedStatus) {
1911
- const pct = Math.round(totalCost / budget * 100);
1912
- const filled = Math.round(Math.min(totalCost / budget, 1) * 10);
1913
- const bar = "\u2593".repeat(filled) + "\u2591".repeat(10 - filled);
1914
- if (status === "warning") {
1915
- message = `\u26A0\uFE0F <b>Budget Warning</b>
1916
- Monthly usage: $${totalCost.toFixed(2)} / $${budget.toFixed(2)} (${pct}%)
1917
- ${bar} ${pct}%`;
1918
- } else {
1919
- message = `\u{1F6A8} <b>Budget Exceeded</b>
1920
- Monthly usage: $${totalCost.toFixed(2)} / $${budget.toFixed(2)} (${pct}%)
1921
- ${bar} ${pct}%
1922
- Sessions are NOT blocked \u2014 this is a warning only.`;
1923
- }
1924
- }
1925
- this.lastNotifiedStatus = status;
1926
- return { status, message };
1927
- }
1928
- getStatus() {
1929
- const { totalCost } = this.store.getMonthlyTotal();
1930
- const budget = this.config.monthlyBudget ?? 0;
1931
- let status = "ok";
1932
- if (budget > 0) {
1933
- if (totalCost >= budget) {
1934
- status = "exceeded";
1935
- } else if (totalCost >= this.config.warningThreshold * budget) {
1936
- status = "warning";
1937
- }
1938
- }
1939
- const percent = budget > 0 ? Math.round(totalCost / budget * 100) : 0;
1940
- return { status, used: totalCost, budget, percent };
1941
- }
1942
- };
1943
-
1944
- // src/core/security-guard.ts
1945
- var SecurityGuard = class {
1946
- constructor(configManager, sessionManager) {
1947
- this.configManager = configManager;
1948
- this.sessionManager = sessionManager;
1949
- }
1950
- checkAccess(message) {
1951
- const config = this.configManager.get();
1952
- if (config.security.allowedUserIds.length > 0) {
1953
- const userId = String(message.userId);
1954
- if (!config.security.allowedUserIds.includes(userId)) {
1955
- return { allowed: false, reason: "Unauthorized user" };
1956
- }
1957
- }
1958
- const active = this.sessionManager.listSessions().filter((s) => s.status === "active" || s.status === "initializing");
1959
- if (active.length >= config.security.maxConcurrentSessions) {
1960
- return { allowed: false, reason: `Session limit reached (${config.security.maxConcurrentSessions})` };
1961
- }
1962
- return { allowed: true };
1963
- }
1964
- };
1965
-
1966
- // src/core/session-factory.ts
1967
- import { nanoid as nanoid2 } from "nanoid";
1968
- var log5 = createChildLogger({ module: "session-factory" });
1969
- var SessionFactory = class {
1970
- constructor(agentManager, sessionManager, speechService, eventBus) {
1971
- this.agentManager = agentManager;
1972
- this.sessionManager = sessionManager;
1973
- this.speechService = speechService;
1974
- this.eventBus = eventBus;
1975
- }
1976
- async create(params) {
1977
- const agentInstance = params.resumeAgentSessionId ? await this.agentManager.resume(
1978
- params.agentName,
1979
- params.workingDirectory,
1980
- params.resumeAgentSessionId
1981
- ) : await this.agentManager.spawn(
1982
- params.agentName,
1983
- params.workingDirectory
1984
- );
1985
- const session = new Session({
1986
- id: params.existingSessionId,
1987
- channelId: params.channelId,
1988
- agentName: params.agentName,
1989
- workingDirectory: params.workingDirectory,
1990
- agentInstance,
1991
- speechService: this.speechService
1992
- });
1993
- session.agentSessionId = agentInstance.sessionId;
1994
- if (params.initialName) {
1995
- session.name = params.initialName;
1996
- }
1997
- this.sessionManager.registerSession(session);
1998
- this.eventBus.emit("session:created", {
1999
- sessionId: session.id,
2000
- agent: session.agentName,
2001
- status: session.status
2002
- });
2003
- return session;
2004
- }
2005
- wireSideEffects(session, deps) {
2006
- if (deps.usageStore) {
2007
- const usageStore = deps.usageStore;
2008
- const usageBudget = deps.usageBudget;
2009
- const notificationManager = deps.notificationManager;
2010
- session.on("agent_event", (event) => {
2011
- if (event.type !== "usage") return;
2012
- const record = {
2013
- id: nanoid2(),
2014
- sessionId: session.id,
2015
- agentName: session.agentName,
2016
- tokensUsed: event.tokensUsed ?? 0,
2017
- contextSize: event.contextSize ?? 0,
2018
- cost: event.cost,
2019
- timestamp: (/* @__PURE__ */ new Date()).toISOString()
2020
- };
2021
- usageStore.append(record);
2022
- if (usageBudget) {
2023
- const result = usageBudget.check();
2024
- if (result.message) {
2025
- notificationManager.notifyAll({
2026
- sessionId: session.id,
2027
- sessionName: session.name,
2028
- type: "budget_warning",
2029
- summary: result.message
2030
- });
2031
- }
2032
- }
2033
- });
2034
- }
2035
- session.on("status_change", (_from, to) => {
2036
- if ((to === "finished" || to === "cancelled") && deps.tunnelService) {
2037
- deps.tunnelService.stopBySession(session.id).then((stopped) => {
2038
- for (const entry of stopped) {
2039
- deps.notificationManager.notifyAll({
2040
- sessionId: session.id,
2041
- sessionName: session.name,
2042
- type: "completed",
2043
- summary: `Tunnel stopped: port ${entry.port}${entry.label ? ` (${entry.label})` : ""} \u2014 session ended`
2044
- }).catch(() => {
2045
- });
2046
- }
2047
- }).catch(() => {
2048
- });
2049
- }
2050
- });
2051
- }
2052
- };
2053
-
2054
- // src/core/event-bus.ts
2055
- var EventBus = class extends TypedEmitter {
2056
- };
2057
-
2058
- // src/core/speech/speech-service.ts
2059
- var SpeechService = class {
2060
- constructor(config) {
2061
- this.config = config;
2062
- }
2063
- sttProviders = /* @__PURE__ */ new Map();
2064
- ttsProviders = /* @__PURE__ */ new Map();
2065
- registerSTTProvider(name, provider) {
2066
- this.sttProviders.set(name, provider);
2067
- }
2068
- registerTTSProvider(name, provider) {
2069
- this.ttsProviders.set(name, provider);
2070
- }
2071
- isSTTAvailable() {
2072
- const { provider, providers } = this.config.stt;
2073
- return provider !== null && providers[provider]?.apiKey !== void 0;
2074
- }
2075
- isTTSAvailable() {
2076
- const provider = this.config.tts.provider;
2077
- return provider !== null && this.ttsProviders.has(provider);
2078
- }
2079
- async transcribe(audioBuffer, mimeType, options) {
2080
- const providerName = this.config.stt.provider;
2081
- if (!providerName || !this.config.stt.providers[providerName]?.apiKey) {
2082
- throw new Error("STT not configured. Set speech.stt.provider and API key in config.");
2083
- }
2084
- const provider = this.sttProviders.get(providerName);
2085
- if (!provider) {
2086
- throw new Error(`STT provider "${providerName}" not registered. Available: ${[...this.sttProviders.keys()].join(", ") || "none"}`);
2087
- }
2088
- return provider.transcribe(audioBuffer, mimeType, options);
2089
- }
2090
- async synthesize(text, options) {
2091
- const providerName = this.config.tts.provider;
2092
- if (!providerName) {
2093
- throw new Error("TTS not configured. Set speech.tts.provider in config.");
2094
- }
2095
- const provider = this.ttsProviders.get(providerName);
2096
- if (!provider) {
2097
- throw new Error(`TTS provider "${providerName}" not registered. Available: ${[...this.ttsProviders.keys()].join(", ") || "none"}`);
2098
- }
2099
- return provider.synthesize(text, options);
2100
- }
2101
- updateConfig(config) {
2102
- this.config = config;
2103
- }
2104
- };
2105
-
2106
- // src/core/speech/providers/groq.ts
2107
- var GROQ_API_URL = "https://api.groq.com/openai/v1/audio/transcriptions";
2108
- var GroqSTT = class {
2109
- constructor(apiKey, defaultModel = "whisper-large-v3-turbo") {
2110
- this.apiKey = apiKey;
2111
- this.defaultModel = defaultModel;
2112
- }
2113
- name = "groq";
2114
- async transcribe(audioBuffer, mimeType, options) {
2115
- const ext = mimeToExt(mimeType);
2116
- const form = new FormData();
2117
- form.append("file", new Blob([new Uint8Array(audioBuffer)], { type: mimeType }), `audio${ext}`);
2118
- form.append("model", options?.model || this.defaultModel);
2119
- form.append("response_format", "verbose_json");
2120
- if (options?.language) {
2121
- form.append("language", options.language);
2122
- }
2123
- const resp = await fetch(GROQ_API_URL, {
2124
- method: "POST",
2125
- headers: { Authorization: `Bearer ${this.apiKey}` },
2126
- body: form
2127
- });
2128
- if (!resp.ok) {
2129
- const body = await resp.text();
2130
- if (resp.status === 401) {
2131
- throw new Error("Invalid Groq API key. Check your key at console.groq.com.");
2132
- }
2133
- if (resp.status === 413) {
2134
- throw new Error("Audio file too large for Groq API (max 25MB).");
2135
- }
2136
- if (resp.status === 429) {
2137
- throw new Error("Groq rate limit exceeded. Free tier: 28,800 seconds/day. Try again later.");
2138
- }
2139
- throw new Error(`Groq STT error (${resp.status}): ${body}`);
2140
- }
2141
- const data = await resp.json();
2142
- return {
2143
- text: data.text,
2144
- language: data.language,
2145
- duration: data.duration
2146
- };
2147
- }
2148
- };
2149
- function mimeToExt(mimeType) {
2150
- const map = {
2151
- "audio/ogg": ".ogg",
2152
- "audio/wav": ".wav",
2153
- "audio/mpeg": ".mp3",
2154
- "audio/mp4": ".m4a",
2155
- "audio/webm": ".webm",
2156
- "audio/flac": ".flac"
2157
- };
2158
- return map[mimeType] || ".bin";
2159
- }
2160
-
2161
- // src/core/speech/providers/edge-tts.ts
2162
- var DEFAULT_VOICE = "en-US-AriaNeural";
2163
- var EdgeTTS = class {
2164
- name = "edge-tts";
2165
- voice;
2166
- constructor(voice) {
2167
- this.voice = voice || DEFAULT_VOICE;
2168
- }
2169
- async synthesize(text, options) {
2170
- const { MsEdgeTTS, OUTPUT_FORMAT } = await import("./dist-UHQK5CXN.js");
2171
- const tts = new MsEdgeTTS();
2172
- const voice = options?.voice || this.voice;
2173
- const format = OUTPUT_FORMAT.AUDIO_24KHZ_48KBITRATE_MONO_MP3;
2174
- await tts.setMetadata(voice, format);
2175
- const { audioStream } = tts.toStream(text);
2176
- const chunks = [];
2177
- for await (const chunk of audioStream) {
2178
- chunks.push(Buffer.isBuffer(chunk) ? chunk : Buffer.from(chunk));
2179
- }
2180
- tts.close();
2181
- return {
2182
- audioBuffer: Buffer.concat(chunks),
2183
- mimeType: "audio/mpeg"
2184
- };
2185
- }
2186
- };
2187
-
2188
- // src/core/context/context-manager.ts
2189
- import * as os from "os";
2190
- import * as path5 from "path";
2191
-
2192
- // src/core/context/context-cache.ts
2193
- import * as fs5 from "fs";
2194
- import * as path4 from "path";
2195
- import * as crypto from "crypto";
2196
- var DEFAULT_TTL_MS = 60 * 60 * 1e3;
2197
- var ContextCache = class {
2198
- constructor(cacheDir, ttlMs = DEFAULT_TTL_MS) {
2199
- this.cacheDir = cacheDir;
2200
- this.ttlMs = ttlMs;
2201
- fs5.mkdirSync(cacheDir, { recursive: true });
2202
- }
2203
- keyHash(repoPath, queryKey) {
2204
- return crypto.createHash("sha256").update(`${repoPath}:${queryKey}`).digest("hex").slice(0, 16);
2205
- }
2206
- filePath(repoPath, queryKey) {
2207
- return path4.join(this.cacheDir, `${this.keyHash(repoPath, queryKey)}.json`);
2208
- }
2209
- get(repoPath, queryKey) {
2210
- const fp = this.filePath(repoPath, queryKey);
2211
- try {
2212
- const stat = fs5.statSync(fp);
2213
- if (Date.now() - stat.mtimeMs > this.ttlMs) {
2214
- fs5.unlinkSync(fp);
2215
- return null;
2216
- }
2217
- return JSON.parse(fs5.readFileSync(fp, "utf-8"));
2218
- } catch {
2219
- return null;
2220
- }
2221
- }
2222
- set(repoPath, queryKey, result) {
2223
- fs5.writeFileSync(this.filePath(repoPath, queryKey), JSON.stringify(result));
2224
- }
2225
- };
2226
-
2227
- // src/core/context/context-manager.ts
2228
- var ContextManager = class {
2229
- providers = [];
2230
- cache;
2231
- constructor() {
2232
- this.cache = new ContextCache(path5.join(os.homedir(), ".openacp", "cache", "entire"));
2233
- }
2234
- register(provider) {
2235
- this.providers.push(provider);
2236
- }
2237
- async getProvider(repoPath) {
2238
- for (const provider of this.providers) {
2239
- if (await provider.isAvailable(repoPath)) return provider;
2240
- }
2241
- return null;
2242
- }
2243
- async listSessions(query) {
2244
- const provider = await this.getProvider(query.repoPath);
2245
- if (!provider) return null;
2246
- return provider.listSessions(query);
2247
- }
2248
- async buildContext(query, options) {
2249
- const queryKey = `${query.type}:${query.value}:${options?.limit ?? ""}:${options?.maxTokens ?? ""}`;
2250
- const cached = this.cache.get(query.repoPath, queryKey);
2251
- if (cached) return cached;
2252
- const provider = await this.getProvider(query.repoPath);
2253
- if (!provider) return null;
2254
- const result = await provider.buildContext(query, options);
2255
- if (result) this.cache.set(query.repoPath, queryKey, result);
2256
- return result;
2257
- }
2258
- };
2259
-
2260
- // src/core/context/context-provider.ts
2261
- var DEFAULT_MAX_TOKENS = 3e4;
2262
- var TOKENS_PER_TURN_ESTIMATE = 400;
2263
-
2264
- // src/core/context/entire/checkpoint-reader.ts
2265
- import { execFileSync as execFileSync2 } from "child_process";
2266
- var ENTIRE_BRANCH = "origin/entire/checkpoints/v1";
2267
- var CHECKPOINT_ID_RE = /^[0-9a-f]{12}$/;
2268
- var SESSION_ID_RE = /^[0-9a-f]{8}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{12}$/;
2269
- var CheckpointReader = class _CheckpointReader {
2270
- constructor(repoPath) {
2271
- this.repoPath = repoPath;
2272
- }
2273
- // ─── Git execution ───────────────────────────────────────────────────────────
2274
- /**
2275
- * Run a git command in the repo directory.
2276
- * Returns trimmed stdout on success, empty string on failure.
2277
- */
2278
- git(...args) {
2279
- try {
2280
- return execFileSync2("git", ["-C", this.repoPath, ...args], {
2281
- encoding: "utf-8"
2282
- }).trim();
2283
- } catch {
2284
- return "";
2285
- }
2286
- }
2287
- // ─── Static helpers ──────────────────────────────────────────────────────────
2288
- /**
2289
- * Convert a 12-char checkpoint ID to its shard path: "f634acf05138" → "f6/34acf05138"
2290
- */
2291
- static shardPath(cpId) {
2292
- return `${cpId.slice(0, 2)}/${cpId.slice(2)}`;
2293
- }
2294
- /**
2295
- * Returns true when value looks like a 12-char lowercase hex checkpoint ID.
2296
- */
2297
- static isCheckpointId(value) {
2298
- return CHECKPOINT_ID_RE.test(value);
2299
- }
2300
- /**
2301
- * Returns true when value looks like a UUID (session ID).
2302
- */
2303
- static isSessionId(value) {
2304
- return SESSION_ID_RE.test(value);
2305
- }
2306
- /**
2307
- * Parse checkpoint-level metadata JSON. Returns null on error.
2308
- */
2309
- static parseCheckpointMeta(json) {
2310
- try {
2311
- const parsed = JSON.parse(json);
2312
- if (!parsed || typeof parsed !== "object") return null;
2313
- if (!Array.isArray(parsed.sessions)) return null;
2314
- return parsed;
2315
- } catch {
2316
- return null;
2317
- }
2318
- }
2319
- /**
2320
- * Extract Entire-Checkpoint trailer IDs from `git log --format="%H|%(trailers:...)"` output.
2321
- * Each line is: `<hash>|<trailer_value_or_empty>`. Returns only non-empty trailer values.
2322
- * Uses the last pipe on each line to locate the trailer value, to be robust against
2323
- * subject lines that contain pipes.
2324
- */
2325
- static parseCheckpointTrailers(output) {
2326
- const ids = [];
2327
- for (const line of output.split("\n")) {
2328
- const pipe = line.lastIndexOf("|");
2329
- if (pipe === -1) continue;
2330
- const trailerId = line.slice(pipe + 1).trim();
2331
- if (trailerId) ids.push(trailerId);
2332
- }
2333
- return ids;
2334
- }
2335
- // ─── Branch check ────────────────────────────────────────────────────────────
2336
- async hasEntireBranch() {
2337
- const out = this.git("branch", "-r");
2338
- return out.includes("entire/checkpoints/v1");
2339
- }
2340
- // ─── Core session fetching ───────────────────────────────────────────────────
2341
- listAllCheckpointIds() {
2342
- const out = this.git(
2343
- "ls-tree",
2344
- "-r",
2345
- ENTIRE_BRANCH,
2346
- "--name-only"
2347
- );
2348
- if (!out) return [];
2349
- const ids = /* @__PURE__ */ new Set();
2350
- for (const file of out.split("\n")) {
2351
- const parts = file.split("/");
2352
- if (parts.length === 3 && parts[2] === "metadata.json") {
2353
- ids.add(parts[0] + parts[1]);
2354
- }
2355
- }
2356
- return [...ids];
2357
- }
2358
- fetchCheckpointMeta(cpId) {
2359
- const shard = _CheckpointReader.shardPath(cpId);
2360
- const raw = this.git("show", `${ENTIRE_BRANCH}:${shard}/metadata.json`);
2361
- if (!raw) return null;
2362
- return _CheckpointReader.parseCheckpointMeta(raw);
2363
- }
2364
- fetchSessionMeta(metaPath) {
2365
- const normalized = metaPath.startsWith("/") ? metaPath.slice(1) : metaPath;
2366
- const raw = this.git("show", `${ENTIRE_BRANCH}:${normalized}`);
2367
- if (!raw) return {};
2368
- try {
2369
- return JSON.parse(raw);
2370
- } catch {
2371
- return {};
2372
- }
2373
- }
2374
- /**
2375
- * Build SessionInfo[] from a single checkpoint's metadata.
2376
- */
2377
- buildSessionsForCheckpoint(cpId, cpMeta) {
2378
- const sessions = [];
2379
- for (let idx = 0; idx < cpMeta.sessions.length; idx++) {
2380
- const sess = cpMeta.sessions[idx];
2381
- const transcriptPath = (sess.transcript ?? "").replace(/^\//, "");
2382
- const metaPath = sess.metadata ?? "";
2383
- const smeta = this.fetchSessionMeta(metaPath);
2384
- const createdAt = smeta.created_at ?? "";
2385
- sessions.push({
2386
- checkpointId: cpId,
2387
- sessionIndex: String(idx),
2388
- transcriptPath,
2389
- createdAt,
2390
- endedAt: createdAt,
2391
- // will be filled from JSONL by conversation builder
2392
- branch: smeta.branch ?? cpMeta.branch ?? "",
2393
- agent: smeta.agent ?? "",
2394
- turnCount: smeta.session_metrics?.turn_count ?? 0,
2395
- filesTouched: smeta.files_touched ?? cpMeta.files_touched ?? [],
2396
- sessionId: smeta.session_id ?? ""
2397
- });
2398
- }
2399
- return sessions;
2400
- }
2401
- getSessionsForCheckpoint(cpId) {
2402
- const meta = this.fetchCheckpointMeta(cpId);
2403
- if (!meta) return [];
2404
- return this.buildSessionsForCheckpoint(cpId, meta);
2405
- }
2406
- // ─── Public resolvers ────────────────────────────────────────────────────────
2407
- /**
2408
- * All sessions recorded on a given branch, sorted by createdAt ascending.
2409
- */
2410
- async resolveByBranch(branchName) {
2411
- const cpIds = this.listAllCheckpointIds();
2412
- const sessions = [];
2413
- for (const cpId of cpIds) {
2414
- const meta = this.fetchCheckpointMeta(cpId);
2415
- if (!meta) continue;
2416
- if (meta.branch !== branchName) continue;
2417
- sessions.push(...this.buildSessionsForCheckpoint(cpId, meta));
2418
- }
2419
- sessions.sort((a, b) => a.createdAt.localeCompare(b.createdAt));
2420
- return sessions;
2421
- }
2422
- /**
2423
- * Sessions linked to a specific commit via the Entire-Checkpoint git trailer.
2424
- */
2425
- async resolveByCommit(commitHash) {
2426
- const fullHash = this.git("rev-parse", commitHash);
2427
- if (!fullHash) return [];
2428
- const cpId = this.git(
2429
- "log",
2430
- "-1",
2431
- "--format=%(trailers:key=Entire-Checkpoint,valueonly)",
2432
- fullHash
2433
- );
2434
- if (!cpId) return [];
2435
- return this.getSessionsForCheckpoint(cpId.trim());
2436
- }
2437
- /**
2438
- * All sessions from a merged PR (by number or GitHub URL).
2439
- */
2440
- async resolveByPr(prInput) {
2441
- let prNumber;
2442
- if (/^\d+$/.test(prInput)) {
2443
- prNumber = prInput;
2444
- } else {
2445
- const m = /\/pull\/(\d+)/.exec(prInput);
2446
- if (!m) return [];
2447
- prNumber = m[1];
2448
- }
2449
- const mergeOut = this.git(
2450
- "log",
2451
- "--all",
2452
- "--oneline",
2453
- "--grep",
2454
- `Merge pull request #${prNumber}`
2455
- );
2456
- if (!mergeOut) return [];
2457
- const mergeCommit = mergeOut.split("\n")[0].split(" ")[0];
2458
- const logOut = this.git(
2459
- "log",
2460
- "--format=%H|%(trailers:key=Entire-Checkpoint,valueonly)",
2461
- `${mergeCommit}^2`,
2462
- "--not",
2463
- `${mergeCommit}^1`
2464
- );
2465
- if (!logOut) return [];
2466
- const cpIds = _CheckpointReader.parseCheckpointTrailers(logOut);
2467
- const sessions = [];
2468
- for (const cpId of cpIds) {
2469
- sessions.push(...this.getSessionsForCheckpoint(cpId));
2470
- }
2471
- sessions.sort((a, b) => a.createdAt.localeCompare(b.createdAt));
2472
- return sessions;
2473
- }
2474
- /**
2475
- * Sessions matching a specific checkpoint ID.
2476
- */
2477
- async resolveByCheckpoint(checkpointId) {
2478
- return this.getSessionsForCheckpoint(checkpointId);
2479
- }
2480
- /**
2481
- * Find a session by its UUID.
2482
- */
2483
- async resolveBySessionId(sessionId) {
2484
- const cpIds = this.listAllCheckpointIds();
2485
- for (const cpId of cpIds) {
2486
- const sessions = this.getSessionsForCheckpoint(cpId);
2487
- const match = sessions.find((s) => s.sessionId === sessionId);
2488
- if (match) return [match];
2489
- }
2490
- return [];
2491
- }
2492
- /**
2493
- * Latest N sessions across all checkpoints, sorted by createdAt descending.
2494
- */
2495
- async resolveLatest(count) {
2496
- const cpIds = this.listAllCheckpointIds();
2497
- const all = [];
2498
- for (const cpId of cpIds) {
2499
- all.push(...this.getSessionsForCheckpoint(cpId));
2500
- }
2501
- all.sort((a, b) => b.createdAt.localeCompare(a.createdAt));
2502
- return all.slice(0, count);
2503
- }
2504
- /**
2505
- * Read the full JSONL transcript content from the entire branch.
2506
- */
2507
- getTranscript(transcriptPath) {
2508
- const normalized = transcriptPath.startsWith("/") ? transcriptPath.slice(1) : transcriptPath;
2509
- return this.git("show", `${ENTIRE_BRANCH}:${normalized}`);
2510
- }
2511
- };
2512
-
2513
- // src/core/context/entire/message-cleaner.ts
2514
- var SYSTEM_TAG_PATTERNS = [
2515
- /<system-reminder>[\s\S]*?<\/system-reminder>/g,
2516
- /<local-command-caveat>[\s\S]*?<\/local-command-caveat>/g,
2517
- /<local-command-stdout>[\s\S]*?<\/local-command-stdout>/g,
2518
- /<command-name>[\s\S]*?<\/command-name>/g,
2519
- /<command-message>[\s\S]*?<\/command-message>/g,
2520
- /<user-prompt-submit-hook>[\s\S]*?<\/user-prompt-submit-hook>/g,
2521
- /<ide_selection>[\s\S]*?<\/ide_selection>/g,
2522
- /<ide_context>[\s\S]*?<\/ide_context>/g,
2523
- /<ide_opened_file>[\s\S]*?<\/ide_opened_file>/g,
2524
- /<cursor_context>[\s\S]*?<\/cursor_context>/g,
2525
- /<attached_files>[\s\S]*?<\/attached_files>/g,
2526
- /<repo_context>[\s\S]*?<\/repo_context>/g,
2527
- /<task-notification>[\s\S]*?<\/task-notification>/g
2528
- ];
2529
- var COMMAND_ARGS_RE = /<command-args>([\s\S]*?)<\/command-args>/;
2530
- function cleanSystemTags(text) {
2531
- const argsMatch = COMMAND_ARGS_RE.exec(text);
2532
- const userArgs = argsMatch?.[1]?.trim() ?? "";
2533
- text = text.replace(/<command-args>[\s\S]*?<\/command-args>/g, "");
2534
- for (const pat of SYSTEM_TAG_PATTERNS) {
2535
- text = text.replace(new RegExp(pat.source, pat.flags), "");
2536
- }
2537
- text = text.trim();
2538
- if (!text && userArgs) return userArgs;
2539
- if (text && userArgs && text !== userArgs) return `${text}
2540
- ${userArgs}`;
2541
- return text || userArgs;
2542
- }
2543
- var SKILL_INDICATORS = [
2544
- "Base directory for this skill:",
2545
- "<HARD-GATE>",
2546
- "## Checklist",
2547
- "## Process Flow",
2548
- "## Key Principles",
2549
- "digraph brainstorming",
2550
- "You MUST create a task for each"
2551
- ];
2552
- function isSkillPrompt(text) {
2553
- for (const indicator of SKILL_INDICATORS) {
2554
- if (text.includes(indicator)) return true;
2555
- }
2556
- if (text.length > 2e3) {
2557
- const headerCount = (text.match(/## /g) || []).length;
2558
- if (headerCount >= 3) return true;
2559
- }
2560
- return false;
2561
- }
2562
- function isNoiseMessage(text) {
2563
- const cleaned = cleanSystemTags(text);
2564
- if (!cleaned) return true;
2565
- if (/^(ready|ready\.)$/i.test(cleaned)) return true;
2566
- if (cleaned.includes("Tell your human partner that this command is deprecated")) return true;
2567
- if (cleaned.startsWith("Read the output file to retrieve the result:")) return true;
2568
- if (/^(opus|sonnet|haiku|claude)(\[.*\])?$/i.test(cleaned)) return true;
2569
- return false;
2570
- }
2571
-
2572
- // src/core/context/entire/conversation-builder.ts
2573
- function selectMode(totalTurns) {
2574
- if (totalTurns <= 10) return "full";
2575
- if (totalTurns <= 25) return "balanced";
2576
- return "compact";
2577
- }
2578
- function estimateTokens(text) {
2579
- return Math.floor(text.length / 4);
2580
- }
2581
- function shortenPath(fp) {
2582
- const parts = fp.split("/");
2583
- if (parts.length >= 2) return parts.slice(-2).join("/");
2584
- return fp;
2585
- }
2586
- function countLines(s) {
2587
- const trimmed = s.trim();
2588
- if (!trimmed) return 0;
2589
- return trimmed.split("\n").length;
2590
- }
2591
- function extractText(content) {
2592
- if (typeof content === "string") return content;
2593
- if (Array.isArray(content)) {
2594
- return content.filter((b) => typeof b === "object" && b !== null && b.type === "text").map((b) => b.text).join("\n");
2595
- }
2596
- return "";
2597
- }
2598
- function extractContentBlocks(content) {
2599
- if (typeof content === "string") return [{ type: "text", text: content }];
2600
- if (Array.isArray(content)) {
2601
- return content.filter((b) => typeof b === "object" && b !== null);
2602
- }
2603
- return [];
2604
- }
2605
- function isToolResultOnly(content) {
2606
- if (typeof content === "string") return false;
2607
- if (!Array.isArray(content)) return true;
2608
- for (const block of content) {
2609
- if (typeof block === "object" && block !== null) {
2610
- const b = block;
2611
- if (b.type === "text" && typeof b.text === "string" && b.text.trim()) return false;
2612
- if (b.type === "image") return false;
2613
- }
2614
- }
2615
- return true;
2616
- }
2617
- function hasImage(content) {
2618
- if (!Array.isArray(content)) return false;
2619
- return content.some((b) => typeof b === "object" && b !== null && b.type === "image");
2620
- }
2621
- function formatEditFull(filePath, oldStr, newStr) {
2622
- const lines = [];
2623
- lines.push(`\u270F\uFE0F \`${filePath}\``);
2624
- lines.push("```diff");
2625
- for (const line of oldStr.split("\n")) lines.push(`- ${line}`);
2626
- for (const line of newStr.split("\n")) lines.push(`+ ${line}`);
2627
- lines.push("```");
2628
- return lines.join("\n");
2629
- }
2630
- function formatEditBalanced(filePath, oldStr, newStr, maxDiffLines = 12) {
2631
- const oldLines = oldStr.split("\n");
2632
- const newLines = newStr.split("\n");
2633
- const total = oldLines.length + newLines.length;
2634
- const lines = [];
2635
- lines.push(`\u270F\uFE0F \`${filePath}\``);
2636
- lines.push("```diff");
2637
- if (total <= maxDiffLines) {
2638
- for (const line of oldLines) lines.push(`- ${line}`);
2639
- for (const line of newLines) lines.push(`+ ${line}`);
2640
- } else {
2641
- const half = Math.floor(maxDiffLines / 2);
2642
- for (const line of oldLines.slice(0, half)) lines.push(`- ${line}`);
2643
- if (oldLines.length > half) lines.push(` ... (-${oldLines.length} lines total)`);
2644
- for (const line of newLines.slice(0, half)) lines.push(`+ ${line}`);
2645
- if (newLines.length > half) lines.push(` ... (+${newLines.length} lines total)`);
2646
- }
2647
- lines.push("```");
2648
- return lines.join("\n");
2649
- }
2650
- function formatEditCompact(filePath, oldStr, newStr) {
2651
- const oldLines = countLines(oldStr);
2652
- const newLines = countLines(newStr);
2653
- let firstNew = "";
2654
- for (const line of newStr.split("\n")) {
2655
- const stripped = line.trim();
2656
- if (stripped && !stripped.startsWith("//") && !stripped.startsWith("*")) {
2657
- firstNew = stripped.slice(0, 80);
2658
- break;
2659
- }
2660
- }
2661
- if (firstNew) {
2662
- return `\u270F\uFE0F \`${filePath}\` (-${oldLines}/+${newLines} lines): \`${firstNew}\``;
2663
- }
2664
- return `\u270F\uFE0F \`${filePath}\` (-${oldLines}/+${newLines} lines)`;
2665
- }
2666
- function formatWriteFull(filePath, content) {
2667
- const lines = [];
2668
- lines.push(`\u{1F4DD} \`${filePath}\``);
2669
- lines.push("```");
2670
- lines.push(content);
2671
- lines.push("```");
2672
- return lines.join("\n");
2673
- }
2674
- function formatWriteBalanced(filePath, content, maxLines = 15) {
2675
- const contentLines = content.split("\n");
2676
- const lines = [];
2677
- lines.push(`\u{1F4DD} \`${filePath}\` (${contentLines.length} lines)`);
2678
- lines.push("```");
2679
- for (const line of contentLines.slice(0, maxLines)) lines.push(line);
2680
- if (contentLines.length > maxLines) lines.push(`... (${contentLines.length - maxLines} more lines)`);
2681
- lines.push("```");
2682
- return lines.join("\n");
2683
- }
2684
- function formatWriteCompact(filePath, content) {
2685
- const numLines = countLines(content);
2686
- return `\u{1F4DD} \`${filePath}\` (${numLines} lines written)`;
2687
- }
2688
- function parseJsonlToTurns(jsonl) {
2689
- const events = [];
2690
- for (const rawLine of jsonl.split("\n")) {
2691
- const line = rawLine.trim();
2692
- if (!line) continue;
2693
- try {
2694
- events.push(JSON.parse(line));
2695
- } catch {
2696
- }
2697
- }
2698
- let branch = "unknown";
2699
- for (const e of events) {
2700
- if (e.gitBranch) {
2701
- branch = e.gitBranch;
2702
- break;
2703
- }
2704
- }
2705
- const convEvents = events.filter((e) => e.type === "user" || e.type === "assistant");
2706
- const turns = [];
2707
- let currentTurn = null;
2708
- for (const e of convEvents) {
2709
- const etype = e.type;
2710
- const content = e.message?.content ?? [];
2711
- const ts = e.timestamp ?? "";
2712
- if (etype === "user") {
2713
- if (isToolResultOnly(content)) continue;
2714
- const text = extractText(content);
2715
- if (isSkillPrompt(text)) continue;
2716
- if (isNoiseMessage(text)) continue;
2717
- const cleaned = cleanSystemTags(text);
2718
- if (!cleaned) continue;
2719
- if (currentTurn) turns.push(currentTurn);
2720
- const imgSuffix = hasImage(content) ? " [image]" : "";
2721
- currentTurn = {
2722
- userText: cleaned + imgSuffix,
2723
- userTimestamp: ts,
2724
- assistantParts: []
2725
- };
2726
- } else if (etype === "assistant" && currentTurn) {
2727
- const blocks = extractContentBlocks(content);
2728
- let pendingText = null;
2729
- for (const block of blocks) {
2730
- const btype = block.type;
2731
- if (btype === "text") {
2732
- const text = typeof block.text === "string" ? block.text.trim() : "";
2733
- if (text) pendingText = text;
2734
- } else if (btype === "tool_use") {
2735
- const name = typeof block.name === "string" ? block.name : "";
2736
- const inp = typeof block.input === "object" && block.input !== null ? block.input : {};
2737
- if (name === "Edit") {
2738
- if (pendingText) {
2739
- currentTurn.assistantParts.push({ type: "text", content: pendingText });
2740
- pendingText = null;
2741
- }
2742
- currentTurn.assistantParts.push({
2743
- type: "edit",
2744
- file: shortenPath(inp.file_path ?? ""),
2745
- old: inp.old_string ?? "",
2746
- new: inp.new_string ?? ""
2747
- });
2748
- } else if (name === "Write") {
2749
- if (pendingText) {
2750
- currentTurn.assistantParts.push({ type: "text", content: pendingText });
2751
- pendingText = null;
2752
- }
2753
- currentTurn.assistantParts.push({
2754
- type: "write",
2755
- file: shortenPath(inp.file_path ?? ""),
2756
- fileContent: inp.content ?? ""
2757
- });
2758
- }
2759
- }
2760
- }
2761
- if (pendingText) {
2762
- currentTurn.assistantParts.push({ type: "text", content: pendingText });
2763
- }
2764
- }
2765
- }
2766
- if (currentTurn) turns.push(currentTurn);
2767
- const firstTimestamp = turns[0]?.userTimestamp ?? "";
2768
- const lastTimestamp = turns[turns.length - 1]?.userTimestamp ?? "";
2769
- return { turns, branch, firstTimestamp, lastTimestamp };
2770
- }
2771
- function buildSessionMarkdown(turns, mode) {
2772
- const out = [];
2773
- for (let i = 0; i < turns.length; i++) {
2774
- const turn = turns[i];
2775
- const userText = turn.userText.trim();
2776
- if (!userText) continue;
2777
- out.push(`**User [${i + 1}]:**`);
2778
- out.push(userText);
2779
- out.push("");
2780
- let hasContent = false;
2781
- for (const part of turn.assistantParts) {
2782
- if (part.type === "text") {
2783
- if (!hasContent) {
2784
- out.push("**Assistant:**");
2785
- hasContent = true;
2786
- }
2787
- out.push(part.content ?? "");
2788
- out.push("");
2789
- } else if (part.type === "edit") {
2790
- if (!hasContent) {
2791
- out.push("**Assistant:**");
2792
- hasContent = true;
2793
- }
2794
- const file = part.file ?? "";
2795
- const oldStr = part.old ?? "";
2796
- const newStr = part.new ?? "";
2797
- if (mode === "full") {
2798
- out.push(formatEditFull(file, oldStr, newStr));
2799
- } else if (mode === "balanced") {
2800
- out.push(formatEditBalanced(file, oldStr, newStr));
2801
- } else {
2802
- out.push(formatEditCompact(file, oldStr, newStr));
2803
- }
2804
- out.push("");
2805
- } else if (part.type === "write") {
2806
- if (!hasContent) {
2807
- out.push("**Assistant:**");
2808
- hasContent = true;
2809
- }
2810
- const file = part.file ?? "";
2811
- const content = part.fileContent ?? "";
2812
- if (mode === "full") {
2813
- out.push(formatWriteFull(file, content));
2814
- } else if (mode === "balanced") {
2815
- out.push(formatWriteBalanced(file, content));
2816
- } else {
2817
- out.push(formatWriteCompact(file, content));
2818
- }
2819
- out.push("");
2820
- }
2821
- }
2822
- out.push("---");
2823
- out.push("");
2824
- }
2825
- return out.join("\n");
2826
- }
2827
- var DISCLAIMER = `> **Note:** This conversation history may contain outdated information. File contents, code, and project state may have changed since these sessions were recorded. Use this as context only \u2014 always verify against current files before acting.`;
2828
- function mergeSessionsMarkdown(sessions, mode, title) {
2829
- const sorted = [...sessions].sort((a, b) => a.startTime.localeCompare(b.startTime));
2830
- const totalTurns = sorted.reduce((sum, s) => sum + s.turns, 0);
2831
- const overallStart = sorted[0]?.startTime.slice(0, 16) ?? "?";
2832
- const overallEnd = sorted[sorted.length - 1]?.endTime.slice(0, 16) ?? "?";
2833
- const out = [];
2834
- out.push(`# Conversation History from ${title}`);
2835
- out.push(`${sorted.length} sessions | ${totalTurns} turns | ${overallStart} \u2192 ${overallEnd} | mode: ${mode}`);
2836
- out.push("");
2837
- for (let i = 0; i < sorted.length; i++) {
2838
- const s = sorted[i];
2839
- const start = s.startTime.slice(0, 16);
2840
- const end = s.endTime.slice(0, 16);
2841
- out.push(`## Session Conversation History ${i + 1} \u2014 ${start} \u2192 ${end} (${s.agent}, ${s.turns} turns, branch: ${s.branch})`);
2842
- out.push("");
2843
- out.push(s.markdown);
2844
- }
2845
- out.push(DISCLAIMER);
2846
- out.push("");
2847
- return out.join("\n");
2848
- }
2849
-
2850
- // src/core/context/entire/entire-provider.ts
2851
- var EntireProvider = class {
2852
- name = "entire";
2853
- async isAvailable(repoPath) {
2854
- return new CheckpointReader(repoPath).hasEntireBranch();
2855
- }
2856
- async listSessions(query) {
2857
- const reader = new CheckpointReader(query.repoPath);
2858
- const sessions = await this.resolveSessions(reader, query);
2859
- const estimatedTokens = sessions.reduce((sum, s) => sum + s.turnCount * TOKENS_PER_TURN_ESTIMATE, 0);
2860
- return { sessions, estimatedTokens };
2861
- }
2862
- async buildContext(query, options) {
2863
- const maxTokens = options?.maxTokens ?? DEFAULT_MAX_TOKENS;
2864
- const reader = new CheckpointReader(query.repoPath);
2865
- let sessions = await this.resolveSessions(reader, query);
2866
- if (options?.limit && sessions.length > options.limit) {
2867
- sessions = sessions.slice(-options.limit);
2868
- }
2869
- if (sessions.length === 0) {
2870
- return { markdown: "", tokenEstimate: 0, sessionCount: 0, totalTurns: 0, mode: "full", truncated: false, timeRange: { start: "", end: "" } };
2871
- }
2872
- const parsedSessions = [];
2873
- for (const sess of sessions) {
2874
- const jsonl = reader.getTranscript(sess.transcriptPath);
2875
- if (jsonl) parsedSessions.push({ session: sess, jsonl });
2876
- }
2877
- if (parsedSessions.length === 0) {
2878
- return { markdown: "", tokenEstimate: 0, sessionCount: 0, totalTurns: 0, mode: "full", truncated: false, timeRange: { start: "", end: "" } };
2879
- }
2880
- const totalTurns = parsedSessions.reduce((sum, ps) => {
2881
- const parsed = parseJsonlToTurns(ps.jsonl);
2882
- return sum + parsed.turns.length;
2883
- }, 0);
2884
- let mode = selectMode(totalTurns);
2885
- const title = this.buildTitle(query);
2886
- let sessionMarkdowns = this.buildSessionMarkdowns(parsedSessions, mode);
2887
- let merged = mergeSessionsMarkdown(sessionMarkdowns, mode, title);
2888
- let tokens = estimateTokens(merged);
2889
- if (tokens > maxTokens && mode !== "compact") {
2890
- mode = "compact";
2891
- sessionMarkdowns = this.buildSessionMarkdowns(parsedSessions, "compact");
2892
- merged = mergeSessionsMarkdown(sessionMarkdowns, "compact", title);
2893
- tokens = estimateTokens(merged);
2894
- }
2895
- let truncated = false;
2896
- while (tokens > maxTokens && sessionMarkdowns.length > 1) {
2897
- sessionMarkdowns = sessionMarkdowns.slice(1);
2898
- truncated = true;
2899
- merged = mergeSessionsMarkdown(sessionMarkdowns, mode, title);
2900
- tokens = estimateTokens(merged);
2901
- }
2902
- const allTimes = sessionMarkdowns.flatMap((s) => [s.startTime, s.endTime]).filter(Boolean).sort();
2903
- const finalTurns = sessionMarkdowns.reduce((sum, s) => sum + s.turns, 0);
2904
- return {
2905
- markdown: merged,
2906
- tokenEstimate: tokens,
2907
- sessionCount: sessionMarkdowns.length,
2908
- totalTurns: finalTurns,
2909
- mode,
2910
- truncated,
2911
- timeRange: { start: allTimes[0] ?? "", end: allTimes[allTimes.length - 1] ?? "" }
2912
- };
2913
- }
2914
- buildSessionMarkdowns(parsedSessions, mode) {
2915
- return parsedSessions.map((ps) => {
2916
- const parsed = parseJsonlToTurns(ps.jsonl);
2917
- return {
2918
- markdown: buildSessionMarkdown(parsed.turns, mode),
2919
- startTime: parsed.firstTimestamp,
2920
- endTime: parsed.lastTimestamp,
2921
- agent: ps.session.agent,
2922
- turns: parsed.turns.length,
2923
- branch: ps.session.branch,
2924
- files: ps.session.filesTouched.map((f) => f.split("/").pop() ?? f)
2925
- };
2926
- });
2927
- }
2928
- async resolveSessions(reader, query) {
2929
- switch (query.type) {
2930
- case "branch":
2931
- return reader.resolveByBranch(query.value);
2932
- case "commit":
2933
- return reader.resolveByCommit(query.value);
2934
- case "pr":
2935
- return reader.resolveByPr(query.value);
2936
- case "checkpoint":
2937
- return reader.resolveByCheckpoint(query.value);
2938
- case "session":
2939
- return reader.resolveBySessionId(query.value);
2940
- case "latest":
2941
- return reader.resolveLatest(parseInt(query.value) || 5);
2942
- default:
2943
- return [];
2944
- }
2945
- }
2946
- buildTitle(query) {
2947
- switch (query.type) {
2948
- case "pr":
2949
- return `PR #${query.value.replace(/.*\/pull\//, "")}`;
2950
- case "branch":
2951
- return `branch \`${query.value}\``;
2952
- case "commit":
2953
- return `commit \`${query.value.slice(0, 8)}\``;
2954
- case "checkpoint":
2955
- return `checkpoint \`${query.value}\``;
2956
- case "session":
2957
- return `session \`${query.value.slice(0, 8)}...\``;
2958
- case "latest":
2959
- return `latest ${query.value} sessions`;
2960
- default:
2961
- return "unknown";
2962
- }
2963
- }
2964
- };
2965
-
2966
- // src/core/core.ts
2967
- import path7 from "path";
2968
- import os2 from "os";
2969
-
2970
- // src/core/session-store.ts
2971
- import fs6 from "fs";
2972
- import path6 from "path";
2973
- var log6 = createChildLogger({ module: "session-store" });
2974
- var DEBOUNCE_MS2 = 2e3;
2975
- var JsonFileSessionStore = class {
2976
- records = /* @__PURE__ */ new Map();
2977
- filePath;
2978
- ttlDays;
2979
- debounceTimer = null;
2980
- cleanupInterval = null;
2981
- flushHandler = null;
2982
- constructor(filePath, ttlDays) {
2983
- this.filePath = filePath;
2984
- this.ttlDays = ttlDays;
2985
- this.load();
2986
- this.cleanup();
2987
- this.cleanupInterval = setInterval(
2988
- () => this.cleanup(),
2989
- 24 * 60 * 60 * 1e3
2990
- );
2991
- this.flushHandler = () => this.flushSync();
2992
- process.on("SIGTERM", this.flushHandler);
2993
- process.on("SIGINT", this.flushHandler);
2994
- process.on("exit", this.flushHandler);
2995
- }
2996
- async save(record) {
2997
- this.records.set(record.sessionId, { ...record });
2998
- this.scheduleDiskWrite();
2999
- }
3000
- get(sessionId) {
3001
- return this.records.get(sessionId);
3002
- }
3003
- findByPlatform(channelId, predicate) {
3004
- for (const record of this.records.values()) {
3005
- if (record.channelId === channelId && predicate(record.platform)) {
3006
- return record;
3007
- }
3008
- }
3009
- return void 0;
3010
- }
3011
- findByAgentSessionId(agentSessionId) {
3012
- return [...this.records.values()].find(
3013
- (r) => r.agentSessionId === agentSessionId || r.originalAgentSessionId === agentSessionId
3014
- );
3015
- }
3016
- list(channelId) {
3017
- const all = [...this.records.values()];
3018
- if (channelId) return all.filter((r) => r.channelId === channelId);
3019
- return all;
3020
- }
3021
- async remove(sessionId) {
3022
- this.records.delete(sessionId);
3023
- this.scheduleDiskWrite();
3024
- }
3025
- flushSync() {
3026
- if (this.debounceTimer) {
3027
- clearTimeout(this.debounceTimer);
3028
- this.debounceTimer = null;
3029
- }
3030
- const data = {
3031
- version: 1,
3032
- sessions: Object.fromEntries(this.records)
3033
- };
3034
- const dir = path6.dirname(this.filePath);
3035
- if (!fs6.existsSync(dir)) fs6.mkdirSync(dir, { recursive: true });
3036
- fs6.writeFileSync(this.filePath, JSON.stringify(data, null, 2));
3037
- }
3038
- destroy() {
3039
- if (this.debounceTimer) clearTimeout(this.debounceTimer);
3040
- if (this.cleanupInterval) clearInterval(this.cleanupInterval);
3041
- if (this.flushHandler) {
3042
- process.removeListener("SIGTERM", this.flushHandler);
3043
- process.removeListener("SIGINT", this.flushHandler);
3044
- process.removeListener("exit", this.flushHandler);
3045
- this.flushHandler = null;
3046
- }
3047
- }
3048
- load() {
3049
- if (!fs6.existsSync(this.filePath)) return;
3050
- try {
3051
- const raw = JSON.parse(
3052
- fs6.readFileSync(this.filePath, "utf-8")
3053
- );
3054
- if (raw.version !== 1) {
3055
- log6.warn(
3056
- { version: raw.version },
3057
- "Unknown session store version, skipping load"
3058
- );
3059
- return;
3060
- }
3061
- for (const [id, record] of Object.entries(raw.sessions)) {
3062
- this.records.set(id, record);
3063
- }
3064
- log6.debug({ count: this.records.size }, "Loaded session records");
3065
- } catch (err) {
3066
- log6.error({ err }, "Failed to load session store");
3067
- }
3068
- }
3069
- cleanup() {
3070
- const cutoff = Date.now() - this.ttlDays * 24 * 60 * 60 * 1e3;
3071
- let removed = 0;
3072
- for (const [id, record] of this.records) {
3073
- if (record.status === "active" || record.status === "initializing")
3074
- continue;
3075
- const lastActive = new Date(record.lastActiveAt).getTime();
3076
- if (lastActive < cutoff) {
3077
- this.records.delete(id);
3078
- removed++;
3079
- }
3080
- }
3081
- if (removed > 0) {
3082
- log6.info({ removed }, "Cleaned up expired session records");
3083
- this.scheduleDiskWrite();
3084
- }
3085
- }
3086
- scheduleDiskWrite() {
3087
- if (this.debounceTimer) clearTimeout(this.debounceTimer);
3088
- this.debounceTimer = setTimeout(() => {
3089
- this.flushSync();
3090
- }, DEBOUNCE_MS2);
3091
- }
3092
- };
3093
-
3094
- // src/core/core.ts
3095
- var log7 = createChildLogger({ module: "core" });
3096
- var OpenACPCore = class {
3097
- configManager;
3098
- agentCatalog;
3099
- agentManager;
3100
- sessionManager;
3101
- notificationManager;
3102
- messageTransformer;
3103
- fileService;
3104
- speechService;
3105
- securityGuard;
3106
- adapters = /* @__PURE__ */ new Map();
3107
- /** Set by main.ts — triggers graceful shutdown with restart exit code */
3108
- requestRestart = null;
3109
- _tunnelService;
3110
- sessionStore = null;
3111
- resumeLocks = /* @__PURE__ */ new Map();
3112
- eventBus;
3113
- sessionFactory;
3114
- usageStore = null;
3115
- usageBudget = null;
3116
- contextManager;
3117
- constructor(configManager) {
3118
- this.configManager = configManager;
3119
- const config = configManager.get();
3120
- this.agentCatalog = new AgentCatalog();
3121
- this.agentCatalog.load();
3122
- this.agentManager = new AgentManager(this.agentCatalog);
3123
- const storePath = path7.join(os2.homedir(), ".openacp", "sessions.json");
3124
- this.sessionStore = new JsonFileSessionStore(
3125
- storePath,
3126
- config.sessionStore.ttlDays
3127
- );
3128
- this.sessionManager = new SessionManager(this.sessionStore);
3129
- this.securityGuard = new SecurityGuard(configManager, this.sessionManager);
3130
- this.notificationManager = new NotificationManager(this.adapters);
3131
- const usageConfig = config.usage;
3132
- if (usageConfig.enabled) {
3133
- const usagePath = path7.join(os2.homedir(), ".openacp", "usage.json");
3134
- this.usageStore = new UsageStore(usagePath, usageConfig.retentionDays);
3135
- this.usageBudget = new UsageBudget(this.usageStore, usageConfig);
3136
- }
3137
- this.messageTransformer = new MessageTransformer();
3138
- this.eventBus = new EventBus();
3139
- this.sessionManager.setEventBus(this.eventBus);
3140
- this.contextManager = new ContextManager();
3141
- this.contextManager.register(new EntireProvider());
3142
- this.fileService = new FileService(
3143
- path7.join(os2.homedir(), ".openacp", "files")
3144
- );
3145
- const speechConfig = config.speech ?? {
3146
- stt: { provider: null, providers: {} },
3147
- tts: { provider: "edge-tts", providers: {} }
3148
- };
3149
- if (speechConfig.tts.provider == null) {
3150
- speechConfig.tts.provider = "edge-tts";
3151
- }
3152
- this.speechService = new SpeechService(speechConfig);
3153
- const groqConfig = speechConfig.stt?.providers?.groq;
3154
- if (groqConfig?.apiKey) {
3155
- this.speechService.registerSTTProvider(
3156
- "groq",
3157
- new GroqSTT(groqConfig.apiKey, groqConfig.model)
3158
- );
3159
- }
3160
- {
3161
- const edgeConfig = speechConfig.tts?.providers?.["edge-tts"];
3162
- const voice = edgeConfig?.voice;
3163
- this.speechService.registerTTSProvider("edge-tts", new EdgeTTS(voice));
3164
- }
3165
- this.sessionFactory = new SessionFactory(
3166
- this.agentManager,
3167
- this.sessionManager,
3168
- this.speechService,
3169
- this.eventBus
3170
- );
3171
- this.configManager.on(
3172
- "config:changed",
3173
- async ({ path: configPath, value }) => {
3174
- if (configPath === "logging.level" && typeof value === "string") {
3175
- const { setLogLevel: setLogLevel2 } = await import("./log-NXABYJTT.js");
3176
- setLogLevel2(value);
3177
- log7.info({ level: value }, "Log level changed at runtime");
3178
- }
3179
- if (configPath.startsWith("speech.")) {
3180
- const newConfig = this.configManager.get();
3181
- const newSpeechConfig = newConfig.speech ?? {
3182
- stt: { provider: null, providers: {} },
3183
- tts: { provider: null, providers: {} }
3184
- };
3185
- this.speechService.updateConfig(newSpeechConfig);
3186
- const groqCfg = newSpeechConfig.stt?.providers?.groq;
3187
- if (groqCfg?.apiKey) {
3188
- this.speechService.registerSTTProvider(
3189
- "groq",
3190
- new GroqSTT(groqCfg.apiKey, groqCfg.model)
3191
- );
3192
- }
3193
- {
3194
- const edgeConfig = newSpeechConfig.tts?.providers?.["edge-tts"];
3195
- const voice = edgeConfig?.voice;
3196
- this.speechService.registerTTSProvider("edge-tts", new EdgeTTS(voice));
3197
- }
3198
- log7.info("Speech service config updated at runtime");
3199
- }
3200
- }
3201
- );
3202
- }
3203
- get tunnelService() {
3204
- return this._tunnelService;
3205
- }
3206
- set tunnelService(service) {
3207
- this._tunnelService = service;
3208
- this.messageTransformer = new MessageTransformer(service);
3209
- }
3210
- registerAdapter(name, adapter) {
3211
- this.adapters.set(name, adapter);
3212
- }
3213
- async start() {
3214
- this.agentCatalog.refreshRegistryIfStale().catch((err) => {
3215
- log7.warn({ err }, "Background registry refresh failed");
3216
- });
3217
- for (const adapter of this.adapters.values()) {
3218
- await adapter.start();
3219
- }
3220
- }
3221
- async stop() {
3222
- try {
3223
- await this.notificationManager.notifyAll({
3224
- sessionId: "system",
3225
- type: "error",
3226
- summary: "OpenACP is shutting down"
3227
- });
3228
- } catch {
3229
- }
3230
- await this.sessionManager.destroyAll();
3231
- for (const adapter of this.adapters.values()) {
3232
- await adapter.stop();
3233
- }
3234
- if (this.usageStore) {
3235
- this.usageStore.destroy();
3236
- }
3237
- }
3238
- // --- Summary ---
3239
- async summarizeSession(sessionId) {
3240
- const session = this.sessionManager.getSession(sessionId);
3241
- if (session && session.status === "active") {
3242
- try {
3243
- const summary = await session.generateSummary();
3244
- if (!summary) return { ok: false, error: "Agent could not generate summary" };
3245
- return { ok: true, summary };
3246
- } catch (err) {
3247
- return { ok: false, error: err.message };
3248
- }
3249
- }
3250
- const record = this.sessionManager.getSessionRecord(sessionId);
3251
- if (!record?.agentSessionId) {
3252
- return { ok: false, error: "Session not found or has no agent history" };
3253
- }
3254
- const caps = getAgentCapabilities(record.agentName);
3255
- if (!caps.supportsResume) {
3256
- return { ok: false, error: `Agent "${record.agentName}" does not support resume \u2014 cannot summarize ended session` };
3257
- }
3258
- let tempSession;
3259
- try {
3260
- const agentInstance = await this.agentManager.resume(
3261
- record.agentName,
3262
- record.workingDir,
3263
- record.agentSessionId
3264
- );
3265
- tempSession = new Session({
3266
- id: `summary-${sessionId}`,
3267
- channelId: record.channelId,
3268
- agentName: record.agentName,
3269
- workingDirectory: record.workingDir,
3270
- agentInstance
3271
- });
3272
- tempSession.activate();
3273
- const summary = await tempSession.generateSummary();
3274
- if (!summary) return { ok: false, error: "Agent could not generate summary" };
3275
- return { ok: true, summary };
3276
- } catch (err) {
3277
- return { ok: false, error: err.message };
3278
- } finally {
3279
- if (tempSession) {
3280
- try {
3281
- await tempSession.destroy();
3282
- } catch {
3283
- }
3284
- }
3285
- }
3286
- }
3287
- // --- Archive ---
3288
- async archiveSession(sessionId) {
3289
- const session = this.sessionManager.getSession(sessionId);
3290
- const record = this.sessionManager.getSessionRecord(sessionId);
3291
- if (!session && !record) return { ok: false, error: "Session not found" };
3292
- const channelId = session?.channelId ?? record?.channelId;
3293
- if (!channelId) return { ok: false, error: "No channel for session" };
3294
- const adapter = this.adapters.get(channelId);
3295
- if (!adapter) return { ok: false, error: "Adapter not found for session" };
3296
- try {
3297
- if (session) {
3298
- await adapter.archiveSessionTopic(session.id);
3299
- } else {
3300
- await adapter.deleteSessionThread(sessionId);
3301
- }
3302
- if (session) {
3303
- try {
3304
- await this.sessionManager.cancelSession(sessionId);
3305
- } catch {
3306
- } finally {
3307
- session.archiving = false;
3308
- }
3309
- }
3310
- await this.sessionManager.removeRecord(sessionId);
3311
- return { ok: true };
3312
- } catch (err) {
3313
- if (session) session.archiving = false;
3314
- return { ok: false, error: err.message };
3315
- }
3316
- }
3317
- // --- Message Routing ---
3318
- async handleMessage(message) {
3319
- log7.debug(
3320
- {
3321
- channelId: message.channelId,
3322
- threadId: message.threadId,
3323
- userId: message.userId
3324
- },
3325
- "Incoming message"
3326
- );
3327
- const access = this.securityGuard.checkAccess(message);
3328
- if (!access.allowed) {
3329
- log7.warn({ userId: message.userId, reason: access.reason }, "Access denied");
3330
- if (access.reason.includes("Session limit")) {
3331
- const adapter = this.adapters.get(message.channelId);
3332
- if (adapter) {
3333
- await adapter.sendMessage(message.threadId, {
3334
- type: "error",
3335
- text: `\u26A0\uFE0F ${access.reason}. Please cancel existing sessions with /cancel before starting new ones.`
3336
- });
3337
- }
3338
- }
3339
- return;
3340
- }
3341
- let session = this.sessionManager.getSessionByThread(
3342
- message.channelId,
3343
- message.threadId
3344
- );
3345
- if (!session) {
3346
- session = await this.lazyResume(message) ?? void 0;
3347
- }
3348
- if (!session) {
3349
- log7.warn(
3350
- { channelId: message.channelId, threadId: message.threadId },
3351
- "No session found for thread (in-memory miss + lazy resume returned null)"
3352
- );
3353
- return;
3354
- }
3355
- this.sessionManager.patchRecord(session.id, {
3356
- lastActiveAt: (/* @__PURE__ */ new Date()).toISOString()
3357
- });
3358
- await session.enqueuePrompt(message.text, message.attachments);
3359
- }
3360
- // --- Unified Session Creation Pipeline ---
3361
- async createSession(params) {
3362
- const session = await this.sessionFactory.create(params);
3363
- const adapter = this.adapters.get(params.channelId);
3364
- if (params.createThread && adapter) {
3365
- const threadId = await adapter.createSessionThread(
3366
- session.id,
3367
- params.initialName ?? `\u{1F504} ${params.agentName} \u2014 New Session`
3368
- );
3369
- session.threadId = threadId;
3370
- }
3371
- if (adapter) {
3372
- const bridge = this.createBridge(session, adapter);
3373
- bridge.connect();
3374
- }
3375
- this.sessionFactory.wireSideEffects(session, {
3376
- usageStore: this.usageStore,
3377
- usageBudget: this.usageBudget,
3378
- notificationManager: this.notificationManager,
3379
- tunnelService: this._tunnelService
3380
- });
3381
- const existingRecord = this.sessionStore?.get(session.id);
3382
- const platform = {
3383
- ...existingRecord?.platform ?? {}
3384
- };
3385
- if (session.threadId) {
3386
- if (params.channelId === "telegram") {
3387
- platform.topicId = Number(session.threadId);
3388
- } else {
3389
- platform.threadId = session.threadId;
3390
- }
3391
- }
3392
- await this.sessionManager.patchRecord(session.id, {
3393
- sessionId: session.id,
3394
- agentSessionId: session.agentSessionId,
3395
- agentName: params.agentName,
3396
- workingDir: params.workingDirectory,
3397
- channelId: params.channelId,
3398
- status: session.status,
3399
- createdAt: session.createdAt.toISOString(),
3400
- lastActiveAt: (/* @__PURE__ */ new Date()).toISOString(),
3401
- name: session.name,
3402
- platform
3403
- });
3404
- log7.info(
3405
- { sessionId: session.id, agentName: params.agentName },
3406
- "Session created via pipeline"
3407
- );
3408
- return session;
3409
- }
3410
- async handleNewSession(channelId, agentName, workspacePath, options) {
3411
- const config = this.configManager.get();
3412
- const resolvedAgent = agentName || config.defaultAgent;
3413
- log7.info({ channelId, agentName: resolvedAgent }, "New session request");
3414
- const agentDef = this.agentCatalog.resolve(resolvedAgent);
3415
- const resolvedWorkspace = this.configManager.resolveWorkspace(
3416
- workspacePath || agentDef?.workingDirectory
3417
- );
3418
- return this.createSession({
3419
- channelId,
3420
- agentName: resolvedAgent,
3421
- workingDirectory: resolvedWorkspace,
3422
- createThread: options?.createThread
3423
- });
3424
- }
3425
- async adoptSession(agentName, agentSessionId, cwd, channelId) {
3426
- const caps = getAgentCapabilities(agentName);
3427
- if (!caps.supportsResume) {
3428
- return {
3429
- ok: false,
3430
- error: "agent_not_supported",
3431
- message: `Agent '${agentName}' does not support session resume`
3432
- };
3433
- }
3434
- const agentDef = this.agentManager.getAgent(agentName);
3435
- if (!agentDef) {
3436
- return {
3437
- ok: false,
3438
- error: "agent_not_supported",
3439
- message: `Agent '${agentName}' not found`
3440
- };
3441
- }
3442
- const { existsSync: existsSync2 } = await import("fs");
3443
- if (!existsSync2(cwd)) {
3444
- return {
3445
- ok: false,
3446
- error: "invalid_cwd",
3447
- message: `Directory does not exist: ${cwd}`
3448
- };
3449
- }
3450
- const maxSessions = this.configManager.get().security.maxConcurrentSessions;
3451
- if (this.sessionManager.listSessions().length >= maxSessions) {
3452
- return {
3453
- ok: false,
3454
- error: "session_limit",
3455
- message: "Maximum concurrent sessions reached"
3456
- };
3457
- }
3458
- const existingRecord = this.sessionManager.getRecordByAgentSessionId(agentSessionId);
3459
- if (existingRecord) {
3460
- const sameChannel = !channelId || existingRecord.channelId === channelId;
3461
- const platform = existingRecord.platform;
3462
- const existingThreadId = platform?.topicId ? String(platform.topicId) : platform?.threadId;
3463
- if (existingThreadId && sameChannel) {
3464
- const adapter = this.adapters.get(existingRecord.channelId) ?? this.adapters.values().next().value;
3465
- if (adapter) {
3466
- try {
3467
- await adapter.sendMessage(existingRecord.sessionId, {
3468
- type: "text",
3469
- text: "Session resumed from CLI."
3470
- });
3471
- } catch {
3472
- }
3473
- }
3474
- return {
3475
- ok: true,
3476
- sessionId: existingRecord.sessionId,
3477
- threadId: existingThreadId,
3478
- status: "existing"
3479
- };
3480
- }
3481
- }
3482
- let adapterChannelId;
3483
- if (channelId) {
3484
- if (!this.adapters.has(channelId)) {
3485
- const available = Array.from(this.adapters.keys()).join(", ") || "none";
3486
- return { ok: false, error: "adapter_not_found", message: `Adapter '${channelId}' is not connected. Available: ${available}` };
3487
- }
3488
- adapterChannelId = channelId;
3489
- } else {
3490
- const firstEntry = this.adapters.entries().next().value;
3491
- if (!firstEntry) {
3492
- return { ok: false, error: "no_adapter", message: "No channel adapter registered" };
3493
- }
3494
- adapterChannelId = firstEntry[0];
3495
- }
3496
- let session;
3497
- try {
3498
- session = await this.createSession({
3499
- channelId: adapterChannelId,
3500
- agentName,
3501
- workingDirectory: cwd,
3502
- resumeAgentSessionId: agentSessionId,
3503
- createThread: true,
3504
- initialName: "Adopted session"
3505
- });
3506
- } catch (err) {
3507
- return {
3508
- ok: false,
3509
- error: "resume_failed",
3510
- message: `Failed to resume session: ${err instanceof Error ? err.message : String(err)}`
3511
- };
3512
- }
3513
- const adoptPlatform = {};
3514
- if (adapterChannelId === "telegram") {
3515
- adoptPlatform.topicId = Number(session.threadId);
3516
- } else {
3517
- adoptPlatform.threadId = session.threadId;
3518
- }
3519
- await this.sessionManager.patchRecord(session.id, {
3520
- originalAgentSessionId: agentSessionId,
3521
- platform: adoptPlatform
3522
- });
3523
- return {
3524
- ok: true,
3525
- sessionId: session.id,
3526
- threadId: session.threadId,
3527
- status: "adopted"
3528
- };
3529
- }
3530
- async handleNewChat(channelId, currentThreadId) {
3531
- const currentSession = this.sessionManager.getSessionByThread(
3532
- channelId,
3533
- currentThreadId
3534
- );
3535
- if (currentSession) {
3536
- return this.handleNewSession(
3537
- channelId,
3538
- currentSession.agentName,
3539
- currentSession.workingDirectory
3540
- );
3541
- }
3542
- const record = this.sessionManager.getRecordByThread(
3543
- channelId,
3544
- currentThreadId
3545
- );
3546
- if (!record || record.status === "cancelled" || record.status === "error")
3547
- return null;
3548
- return this.handleNewSession(
3549
- channelId,
3550
- record.agentName,
3551
- record.workingDir
3552
- );
3553
- }
3554
- async createSessionWithContext(params) {
3555
- let contextResult = null;
3556
- try {
3557
- contextResult = await this.contextManager.buildContext(
3558
- params.contextQuery,
3559
- params.contextOptions
3560
- );
3561
- } catch (err) {
3562
- log7.warn({ err }, "Context building failed, proceeding without context");
3563
- }
3564
- const session = await this.createSession({
3565
- channelId: params.channelId,
3566
- agentName: params.agentName,
3567
- workingDirectory: params.workingDirectory,
3568
- createThread: params.createThread
3569
- });
3570
- if (contextResult) {
3571
- session.setContext(contextResult.markdown);
3572
- }
3573
- return { session, contextResult };
3574
- }
3575
- // --- Lazy Resume ---
3576
- /**
3577
- * Get active session by thread, or attempt lazy resume from store.
3578
- * Used by adapter command handlers that need a session but don't go through handleMessage().
3579
- */
3580
- async getOrResumeSession(channelId, threadId) {
3581
- const session = this.sessionManager.getSessionByThread(channelId, threadId);
3582
- if (session) return session;
3583
- return this.lazyResume({ channelId, threadId, userId: "", text: "" });
3584
- }
3585
- async lazyResume(message) {
3586
- const store = this.sessionStore;
3587
- if (!store) return null;
3588
- const lockKey = `${message.channelId}:${message.threadId}`;
3589
- const existing = this.resumeLocks.get(lockKey);
3590
- if (existing) return existing;
3591
- const record = store.findByPlatform(
3592
- message.channelId,
3593
- (p) => String(p.topicId) === message.threadId
3594
- );
3595
- if (!record) {
3596
- log7.debug(
3597
- { threadId: message.threadId, channelId: message.channelId },
3598
- "No session record found for thread"
3599
- );
3600
- return null;
3601
- }
3602
- if (record.status === "error") {
3603
- log7.debug(
3604
- {
3605
- threadId: message.threadId,
3606
- sessionId: record.sessionId,
3607
- status: record.status
3608
- },
3609
- "Skipping resume of error session"
3610
- );
3611
- return null;
3612
- }
3613
- log7.info(
3614
- {
3615
- threadId: message.threadId,
3616
- sessionId: record.sessionId,
3617
- status: record.status
3618
- },
3619
- "Lazy resume: found record, attempting resume"
3620
- );
3621
- const resumePromise = (async () => {
3622
- try {
3623
- const session = await this.createSession({
3624
- channelId: record.channelId,
3625
- agentName: record.agentName,
3626
- workingDirectory: record.workingDir,
3627
- resumeAgentSessionId: record.agentSessionId,
3628
- existingSessionId: record.sessionId,
3629
- initialName: record.name
3630
- });
3631
- session.threadId = message.threadId;
3632
- session.activate();
3633
- session.dangerousMode = record.dangerousMode ?? false;
3634
- log7.info(
3635
- { sessionId: session.id, threadId: message.threadId },
3636
- "Lazy resume successful"
3637
- );
3638
- return session;
3639
- } catch (err) {
3640
- log7.error({ err, record }, "Lazy resume failed");
3641
- const adapter = this.adapters.get(message.channelId);
3642
- if (adapter) {
3643
- try {
3644
- await adapter.sendMessage(message.threadId, {
3645
- type: "error",
3646
- text: `\u26A0\uFE0F Failed to resume session: ${err instanceof Error ? err.message : String(err)}`
3647
- });
3648
- } catch {
3649
- }
3650
- }
3651
- return null;
3652
- } finally {
3653
- this.resumeLocks.delete(lockKey);
3654
- }
3655
- })();
3656
- this.resumeLocks.set(lockKey, resumePromise);
3657
- return resumePromise;
3658
- }
3659
- // --- Event Wiring ---
3660
- /** Create a SessionBridge for the given session and adapter */
3661
- createBridge(session, adapter) {
3662
- return new SessionBridge(session, adapter, {
3663
- messageTransformer: this.messageTransformer,
3664
- notificationManager: this.notificationManager,
3665
- sessionManager: this.sessionManager,
3666
- eventBus: this.eventBus,
3667
- fileService: this.fileService
3668
- });
3669
- }
3670
- };
3671
-
3672
- // src/core/sse-manager.ts
3673
- var SSEManager = class {
3674
- constructor(eventBus, getSessionStats, startedAt) {
3675
- this.eventBus = eventBus;
3676
- this.getSessionStats = getSessionStats;
3677
- this.startedAt = startedAt;
3678
- }
3679
- sseConnections = /* @__PURE__ */ new Set();
3680
- sseCleanupHandlers = /* @__PURE__ */ new Map();
3681
- healthInterval;
3682
- boundHandlers = [];
3683
- setup() {
3684
- if (!this.eventBus) return;
3685
- const events = [
3686
- "session:created",
3687
- "session:updated",
3688
- "session:deleted",
3689
- "agent:event",
3690
- "permission:request"
3691
- ];
3692
- for (const eventName of events) {
3693
- const handler = (data) => {
3694
- this.broadcast(eventName, data);
3695
- };
3696
- this.eventBus.on(eventName, handler);
3697
- this.boundHandlers.push({ event: eventName, handler });
3698
- }
3699
- this.healthInterval = setInterval(() => {
3700
- const mem = process.memoryUsage();
3701
- const stats = this.getSessionStats();
3702
- this.broadcast("health", {
3703
- uptime: Date.now() - this.startedAt,
3704
- memory: {
3705
- rss: mem.rss,
3706
- heapUsed: mem.heapUsed,
3707
- heapTotal: mem.heapTotal
3708
- },
3709
- sessions: stats
3710
- });
3711
- }, 3e4);
3712
- }
3713
- handleRequest(req, res) {
3714
- const parsedUrl = new URL(req.url || "", "http://localhost");
3715
- const sessionFilter = parsedUrl.searchParams.get("sessionId");
3716
- res.writeHead(200, {
3717
- "Content-Type": "text/event-stream",
3718
- "Cache-Control": "no-cache",
3719
- Connection: "keep-alive"
3720
- });
3721
- res.flushHeaders();
3722
- res.sessionFilter = sessionFilter ?? void 0;
3723
- this.sseConnections.add(res);
3724
- const cleanup = () => {
3725
- this.sseConnections.delete(res);
3726
- this.sseCleanupHandlers.delete(res);
3727
- };
3728
- this.sseCleanupHandlers.set(res, cleanup);
3729
- req.on("close", cleanup);
3730
- }
3731
- broadcast(event, data) {
3732
- const payload = `event: ${event}
3733
- data: ${JSON.stringify(data)}
3734
-
3735
- `;
3736
- const sessionEvents = [
3737
- "agent:event",
3738
- "permission:request",
3739
- "session:updated"
3740
- ];
3741
- for (const res of this.sseConnections) {
3742
- const filter = res.sessionFilter;
3743
- if (filter && sessionEvents.includes(event)) {
3744
- const eventData = data;
3745
- if (eventData.sessionId !== filter) continue;
3746
- }
3747
- try {
3748
- if (res.writable) res.write(payload);
3749
- } catch {
3750
- }
3751
- }
3752
- }
3753
- stop() {
3754
- if (this.healthInterval) clearInterval(this.healthInterval);
3755
- if (this.eventBus) {
3756
- for (const { event, handler } of this.boundHandlers) {
3757
- this.eventBus.off(event, handler);
3758
- }
3759
- }
3760
- this.boundHandlers = [];
3761
- const entries = [...this.sseCleanupHandlers];
3762
- for (const [res, cleanup] of entries) {
3763
- res.end();
3764
- cleanup();
3765
- }
3766
- }
3767
- };
3768
-
3769
- // src/core/static-server.ts
3770
- import * as fs7 from "fs";
3771
- import * as path8 from "path";
3772
- import { fileURLToPath } from "url";
3773
- var MIME_TYPES = {
3774
- ".html": "text/html; charset=utf-8",
3775
- ".js": "application/javascript; charset=utf-8",
3776
- ".css": "text/css; charset=utf-8",
3777
- ".json": "application/json; charset=utf-8",
3778
- ".png": "image/png",
3779
- ".jpg": "image/jpeg",
3780
- ".svg": "image/svg+xml",
3781
- ".ico": "image/x-icon",
3782
- ".woff": "font/woff",
3783
- ".woff2": "font/woff2"
3784
- };
3785
- var StaticServer = class {
3786
- uiDir;
3787
- constructor(uiDir) {
3788
- this.uiDir = uiDir;
3789
- if (!this.uiDir) {
3790
- const __filename = fileURLToPath(import.meta.url);
3791
- const candidate = path8.resolve(path8.dirname(__filename), "../../ui/dist");
3792
- if (fs7.existsSync(path8.join(candidate, "index.html"))) {
3793
- this.uiDir = candidate;
3794
- }
3795
- if (!this.uiDir) {
3796
- const publishCandidate = path8.resolve(
3797
- path8.dirname(__filename),
3798
- "../ui"
3799
- );
3800
- if (fs7.existsSync(path8.join(publishCandidate, "index.html"))) {
3801
- this.uiDir = publishCandidate;
3802
- }
3803
- }
3804
- }
3805
- }
3806
- isAvailable() {
3807
- return this.uiDir !== void 0;
3808
- }
3809
- serve(req, res) {
3810
- if (!this.uiDir) return false;
3811
- const urlPath = (req.url || "/").split("?")[0];
3812
- const safePath = path8.normalize(urlPath);
3813
- const filePath = path8.join(this.uiDir, safePath);
3814
- if (!filePath.startsWith(this.uiDir + path8.sep) && filePath !== this.uiDir)
3815
- return false;
3816
- if (fs7.existsSync(filePath) && fs7.statSync(filePath).isFile()) {
3817
- const ext = path8.extname(filePath);
3818
- const contentType = MIME_TYPES[ext] ?? "application/octet-stream";
3819
- const isHashed = /\.[a-zA-Z0-9]{8,}\.(js|css)$/.test(filePath);
3820
- const cacheControl = isHashed ? "public, max-age=31536000, immutable" : "no-cache";
3821
- res.writeHead(200, {
3822
- "Content-Type": contentType,
3823
- "Cache-Control": cacheControl
3824
- });
3825
- fs7.createReadStream(filePath).pipe(res);
3826
- return true;
3827
- }
3828
- const indexPath = path8.join(this.uiDir, "index.html");
3829
- if (fs7.existsSync(indexPath)) {
3830
- res.writeHead(200, {
3831
- "Content-Type": "text/html; charset=utf-8",
3832
- "Cache-Control": "no-cache"
3833
- });
3834
- fs7.createReadStream(indexPath).pipe(res);
3835
- return true;
3836
- }
3837
- return false;
3838
- }
3839
- };
3840
-
3841
- // src/core/api/index.ts
3842
- import * as http from "http";
3843
- import * as fs8 from "fs";
3844
- import * as path9 from "path";
3845
- import * as os3 from "os";
3846
- import * as crypto2 from "crypto";
3847
- import { fileURLToPath as fileURLToPath2 } from "url";
3848
-
3849
- // src/core/api/router.ts
3850
- var Router = class {
3851
- routes = [];
3852
- get(path10, handler) {
3853
- this.add("GET", path10, handler);
3854
- }
3855
- post(path10, handler) {
3856
- this.add("POST", path10, handler);
3857
- }
3858
- put(path10, handler) {
3859
- this.add("PUT", path10, handler);
3860
- }
3861
- patch(path10, handler) {
3862
- this.add("PATCH", path10, handler);
3863
- }
3864
- delete(path10, handler) {
3865
- this.add("DELETE", path10, handler);
3866
- }
3867
- match(method, url) {
3868
- const pathname = url.split("?")[0];
3869
- for (const route of this.routes) {
3870
- if (route.method !== method) continue;
3871
- const m = pathname.match(route.pattern);
3872
- if (!m) continue;
3873
- const params = {};
3874
- for (let i = 0; i < route.keys.length; i++) {
3875
- params[route.keys[i]] = m[i + 1];
3876
- }
3877
- return { handler: route.handler, params };
3878
- }
3879
- return null;
3880
- }
3881
- add(method, path10, handler) {
3882
- const keys = [];
3883
- const pattern = path10.replace(/:(\w+)/g, (_, key) => {
3884
- keys.push(key);
3885
- return "([^/]+)";
3886
- });
3887
- this.routes.push({
3888
- method,
3889
- pattern: new RegExp(`^${pattern}$`),
3890
- keys,
3891
- handler
3892
- });
3893
- }
3894
- };
3895
-
3896
- // src/core/api/routes/health.ts
3897
- function registerHealthRoutes(router, deps) {
3898
- router.get("/api/health", async (_req, res) => {
3899
- const activeSessions = deps.core.sessionManager.listSessions();
3900
- const allRecords = deps.core.sessionManager.listRecords();
3901
- const mem = process.memoryUsage();
3902
- const tunnel = deps.core.tunnelService;
3903
- deps.sendJson(res, 200, {
3904
- status: "ok",
3905
- uptime: Date.now() - deps.startedAt,
3906
- version: deps.getVersion(),
3907
- memory: {
3908
- rss: mem.rss,
3909
- heapUsed: mem.heapUsed,
3910
- heapTotal: mem.heapTotal
3911
- },
3912
- sessions: {
3913
- active: activeSessions.filter(
3914
- (s) => s.status === "active" || s.status === "initializing"
3915
- ).length,
3916
- total: allRecords.length
3917
- },
3918
- adapters: Array.from(deps.core.adapters.keys()),
3919
- tunnel: tunnel ? { enabled: true, url: tunnel.getPublicUrl() } : { enabled: false }
3920
- });
3921
- });
3922
- router.get("/api/version", async (_req, res) => {
3923
- deps.sendJson(res, 200, { version: deps.getVersion() });
3924
- });
3925
- router.post("/api/restart", async (_req, res) => {
3926
- if (!deps.core.requestRestart) {
3927
- deps.sendJson(res, 501, { error: "Restart not available" });
3928
- return;
3929
- }
3930
- deps.sendJson(res, 200, { ok: true, message: "Restarting..." });
3931
- setImmediate(() => deps.core.requestRestart());
3932
- });
3933
- router.get("/api/adapters", async (_req, res) => {
3934
- const adapters = Array.from(deps.core.adapters.entries()).map(([name]) => ({
3935
- name,
3936
- type: "built-in"
3937
- }));
3938
- deps.sendJson(res, 200, { adapters });
3939
- });
3940
- }
3941
-
3942
- // src/core/api/routes/sessions.ts
3943
- var log8 = createChildLogger({ module: "api-server" });
3944
- function registerSessionRoutes(router, deps) {
3945
- router.post("/api/sessions/adopt", async (req, res) => {
3946
- const body = await deps.readBody(req);
3947
- if (body === null) {
3948
- return deps.sendJson(res, 413, { error: "Request body too large" });
3949
- }
3950
- if (!body) {
3951
- return deps.sendJson(res, 400, {
3952
- error: "bad_request",
3953
- message: "Empty request body"
3954
- });
3955
- }
3956
- let parsed;
3957
- try {
3958
- parsed = JSON.parse(body);
3959
- } catch {
3960
- return deps.sendJson(res, 400, {
3961
- error: "bad_request",
3962
- message: "Invalid JSON"
3963
- });
3964
- }
3965
- const { agent, agentSessionId, cwd, channel } = parsed;
3966
- if (!agent || !agentSessionId) {
3967
- return deps.sendJson(res, 400, {
3968
- error: "bad_request",
3969
- message: "Missing required fields: agent, agentSessionId"
3970
- });
3971
- }
3972
- const result = await deps.core.adoptSession(
3973
- agent,
3974
- agentSessionId,
3975
- cwd ?? process.cwd(),
3976
- channel
3977
- );
3978
- if (result.ok) {
3979
- return deps.sendJson(res, 200, result);
3980
- } else {
3981
- const status = result.error === "session_limit" ? 429 : result.error === "agent_not_supported" ? 400 : 500;
3982
- return deps.sendJson(res, status, result);
3983
- }
3984
- });
3985
- router.post("/api/sessions", async (req, res) => {
3986
- const body = await deps.readBody(req);
3987
- let agent;
3988
- let workspace;
3989
- if (body) {
3990
- try {
3991
- const parsed = JSON.parse(body);
3992
- agent = parsed.agent;
3993
- workspace = parsed.workspace;
3994
- } catch {
3995
- deps.sendJson(res, 400, { error: "Invalid JSON body" });
3996
- return;
3997
- }
3998
- }
3999
- const config = deps.core.configManager.get();
4000
- const activeSessions = deps.core.sessionManager.listSessions().filter((s) => s.status === "active" || s.status === "initializing");
4001
- if (activeSessions.length >= config.security.maxConcurrentSessions) {
4002
- deps.sendJson(res, 429, {
4003
- error: `Max concurrent sessions (${config.security.maxConcurrentSessions}) reached. Cancel a session first.`
4004
- });
4005
- return;
4006
- }
4007
- const [adapterId, adapter] = deps.core.adapters.entries().next().value ?? [
4008
- null,
4009
- null
4010
- ];
4011
- const channelId = adapterId ?? "api";
4012
- const resolvedAgent = agent || config.defaultAgent;
4013
- const resolvedWorkspace = deps.core.configManager.resolveWorkspace(
4014
- workspace || config.agents[resolvedAgent]?.workingDirectory
4015
- );
4016
- const session = await deps.core.createSession({
4017
- channelId,
4018
- agentName: resolvedAgent,
4019
- workingDirectory: resolvedWorkspace,
4020
- createThread: !!adapter,
4021
- initialName: `\u{1F504} ${resolvedAgent} \u2014 New Session`
4022
- });
4023
- if (!adapter) {
4024
- session.agentInstance.onPermissionRequest = async (request) => {
4025
- const allowOption = request.options.find((o) => o.isAllow);
4026
- log8.debug(
4027
- {
4028
- sessionId: session.id,
4029
- permissionId: request.id,
4030
- option: allowOption?.id
4031
- },
4032
- "Auto-approving permission for API session"
4033
- );
4034
- return allowOption?.id ?? request.options[0]?.id ?? "";
4035
- };
4036
- }
4037
- session.warmup().catch(
4038
- (err) => log8.warn({ err, sessionId: session.id }, "API session warmup failed")
4039
- );
4040
- deps.sendJson(res, 200, {
4041
- sessionId: session.id,
4042
- agent: session.agentName,
4043
- status: session.status,
4044
- workspace: session.workingDirectory
4045
- });
4046
- });
4047
- router.post("/api/sessions/:sessionId/prompt", async (req, res, params) => {
4048
- const sessionId = decodeURIComponent(params.sessionId);
4049
- const session = deps.core.sessionManager.getSession(sessionId);
4050
- if (!session) {
4051
- deps.sendJson(res, 404, { error: `Session "${sessionId}" not found` });
4052
- return;
4053
- }
4054
- if (session.status === "cancelled" || session.status === "finished" || session.status === "error") {
4055
- deps.sendJson(res, 400, { error: `Session is ${session.status}` });
4056
- return;
4057
- }
4058
- const body = await deps.readBody(req);
4059
- let prompt;
4060
- if (body) {
4061
- try {
4062
- const parsed = JSON.parse(body);
4063
- prompt = parsed.prompt;
4064
- } catch {
4065
- deps.sendJson(res, 400, { error: "Invalid JSON body" });
4066
- return;
4067
- }
4068
- }
4069
- if (!prompt) {
4070
- deps.sendJson(res, 400, { error: "Missing prompt" });
4071
- return;
4072
- }
4073
- session.enqueuePrompt(prompt).catch(() => {
4074
- });
4075
- deps.sendJson(res, 200, {
4076
- ok: true,
4077
- sessionId,
4078
- queueDepth: session.queueDepth
4079
- });
4080
- });
4081
- router.post(
4082
- "/api/sessions/:sessionId/permission",
4083
- async (req, res, params) => {
4084
- const sessionId = decodeURIComponent(params.sessionId);
4085
- const session = deps.core.sessionManager.getSession(sessionId);
4086
- if (!session) {
4087
- deps.sendJson(res, 404, { error: `Session "${sessionId}" not found` });
4088
- return;
4089
- }
4090
- const body = await deps.readBody(req);
4091
- let permissionId;
4092
- let optionId;
4093
- if (body) {
4094
- try {
4095
- const parsed = JSON.parse(body);
4096
- permissionId = parsed.permissionId;
4097
- optionId = parsed.optionId;
4098
- } catch {
4099
- deps.sendJson(res, 400, { error: "Invalid JSON body" });
4100
- return;
4101
- }
4102
- }
4103
- if (!permissionId || !optionId) {
4104
- deps.sendJson(res, 400, {
4105
- error: "Missing permissionId or optionId"
4106
- });
4107
- return;
4108
- }
4109
- if (!session.permissionGate.isPending || session.permissionGate.requestId !== permissionId) {
4110
- deps.sendJson(res, 400, {
4111
- error: "No matching pending permission request"
4112
- });
4113
- return;
4114
- }
4115
- session.permissionGate.resolve(optionId);
4116
- deps.sendJson(res, 200, { ok: true });
4117
- }
4118
- );
4119
- router.patch(
4120
- "/api/sessions/:sessionId/dangerous",
4121
- async (req, res, params) => {
4122
- const sessionId = decodeURIComponent(params.sessionId);
4123
- const session = deps.core.sessionManager.getSession(sessionId);
4124
- if (!session) {
4125
- deps.sendJson(res, 404, { error: `Session "${sessionId}" not found` });
4126
- return;
4127
- }
4128
- const body = await deps.readBody(req);
4129
- let enabled;
4130
- if (body) {
4131
- try {
4132
- const parsed = JSON.parse(body);
4133
- enabled = parsed.enabled;
4134
- } catch {
4135
- deps.sendJson(res, 400, { error: "Invalid JSON body" });
4136
- return;
4137
- }
4138
- }
4139
- if (typeof enabled !== "boolean") {
4140
- deps.sendJson(res, 400, { error: "Missing enabled boolean" });
4141
- return;
4142
- }
4143
- session.dangerousMode = enabled;
4144
- await deps.core.sessionManager.patchRecord(sessionId, {
4145
- dangerousMode: enabled
4146
- });
4147
- deps.sendJson(res, 200, { ok: true, dangerousMode: enabled });
4148
- }
4149
- );
4150
- router.get("/api/sessions/:sessionId", async (_req, res, params) => {
4151
- const sessionId = decodeURIComponent(params.sessionId);
4152
- const session = deps.core.sessionManager.getSession(sessionId);
4153
- if (!session) {
4154
- deps.sendJson(res, 404, { error: `Session "${sessionId}" not found` });
4155
- return;
4156
- }
4157
- deps.sendJson(res, 200, {
4158
- session: {
4159
- id: session.id,
4160
- agent: session.agentName,
4161
- status: session.status,
4162
- name: session.name ?? null,
4163
- workspace: session.workingDirectory,
4164
- createdAt: session.createdAt.toISOString(),
4165
- dangerousMode: session.dangerousMode,
4166
- queueDepth: session.queueDepth,
4167
- promptRunning: session.promptRunning,
4168
- threadId: session.threadId,
4169
- channelId: session.channelId,
4170
- agentSessionId: session.agentSessionId
4171
- }
4172
- });
4173
- });
4174
- router.post("/api/sessions/:sessionId/summary", async (_req, res, params) => {
4175
- const sessionId = decodeURIComponent(params.sessionId);
4176
- const result = await deps.core.summarizeSession(sessionId);
4177
- if (result.ok) {
4178
- deps.sendJson(res, 200, result);
4179
- } else {
4180
- deps.sendJson(res, 400, result);
4181
- }
4182
- });
4183
- router.post("/api/sessions/:sessionId/archive", async (_req, res, params) => {
4184
- const sessionId = decodeURIComponent(params.sessionId);
4185
- const result = await deps.core.archiveSession(sessionId);
4186
- if (result.ok) {
4187
- deps.sendJson(res, 200, result);
4188
- } else {
4189
- deps.sendJson(res, 400, result);
4190
- }
4191
- });
4192
- router.delete("/api/sessions/:sessionId", async (_req, res, params) => {
4193
- const sessionId = decodeURIComponent(params.sessionId);
4194
- const session = deps.core.sessionManager.getSession(sessionId);
4195
- if (!session) {
4196
- deps.sendJson(res, 404, { error: `Session "${sessionId}" not found` });
4197
- return;
4198
- }
4199
- await deps.core.sessionManager.cancelSession(sessionId);
4200
- deps.sendJson(res, 200, { ok: true });
4201
- });
4202
- router.get("/api/sessions", async (_req, res) => {
4203
- const sessions = deps.core.sessionManager.listSessions();
4204
- deps.sendJson(res, 200, {
4205
- sessions: sessions.map((s) => ({
4206
- id: s.id,
4207
- agent: s.agentName,
4208
- status: s.status,
4209
- name: s.name ?? null,
4210
- workspace: s.workingDirectory,
4211
- createdAt: s.createdAt.toISOString(),
4212
- dangerousMode: s.dangerousMode,
4213
- queueDepth: s.queueDepth,
4214
- promptRunning: s.promptRunning,
4215
- lastActiveAt: deps.core.sessionManager.getSessionRecord(s.id)?.lastActiveAt ?? null
4216
- }))
4217
- });
4218
- });
4219
- }
4220
-
4221
- // src/core/api/routes/config.ts
4222
- var SENSITIVE_KEYS = [
4223
- "botToken",
4224
- "token",
4225
- "apiKey",
4226
- "secret",
4227
- "password",
4228
- "webhookSecret"
4229
- ];
4230
- function redactConfig(config) {
4231
- const redacted = structuredClone(config);
4232
- redactDeep(redacted);
4233
- return redacted;
4234
- }
4235
- function redactDeep(obj) {
4236
- for (const [key, value] of Object.entries(obj)) {
4237
- if (SENSITIVE_KEYS.includes(key) && typeof value === "string") {
4238
- obj[key] = "***";
4239
- } else if (Array.isArray(value)) {
4240
- for (const item of value) {
4241
- if (item && typeof item === "object")
4242
- redactDeep(item);
4243
- }
4244
- } else if (value && typeof value === "object") {
4245
- redactDeep(value);
4246
- }
4247
- }
4248
- }
4249
- function registerConfigRoutes(router, deps) {
4250
- router.get("/api/config/editable", async (_req, res) => {
4251
- const { getSafeFields: getSafeFields2, resolveOptions: resolveOptions2, getConfigValue: getConfigValue2 } = await import("./config-registry-AHYI4MYL.js");
4252
- const config = deps.core.configManager.get();
4253
- const safeFields = getSafeFields2();
4254
- const fields = safeFields.map((def) => ({
4255
- path: def.path,
4256
- displayName: def.displayName,
4257
- group: def.group,
4258
- type: def.type,
4259
- options: resolveOptions2(def, config),
4260
- value: getConfigValue2(config, def.path),
4261
- hotReload: def.hotReload
4262
- }));
4263
- deps.sendJson(res, 200, { fields });
4264
- });
4265
- router.get("/api/config", async (_req, res) => {
4266
- const config = deps.core.configManager.get();
4267
- deps.sendJson(res, 200, { config: redactConfig(config) });
4268
- });
4269
- router.patch("/api/config", async (req, res) => {
4270
- const body = await deps.readBody(req);
4271
- let configPath;
4272
- let value;
4273
- if (body) {
4274
- try {
4275
- const parsed = JSON.parse(body);
4276
- configPath = parsed.path;
4277
- value = parsed.value;
4278
- } catch {
4279
- deps.sendJson(res, 400, { error: "Invalid JSON body" });
4280
- return;
4281
- }
4282
- }
4283
- if (!configPath) {
4284
- deps.sendJson(res, 400, { error: "Missing path" });
4285
- return;
4286
- }
4287
- const BLOCKED_KEYS = /* @__PURE__ */ new Set(["__proto__", "constructor", "prototype"]);
4288
- const parts = configPath.split(".");
4289
- if (parts.some((p) => BLOCKED_KEYS.has(p))) {
4290
- deps.sendJson(res, 400, { error: "Invalid config path" });
4291
- return;
4292
- }
4293
- const { getFieldDef: getFieldDef2 } = await import("./config-registry-AHYI4MYL.js");
4294
- const fieldDef = getFieldDef2(configPath);
4295
- if (!fieldDef || fieldDef.scope !== "safe") {
4296
- deps.sendJson(res, 403, {
4297
- error: "This config field cannot be modified via the API"
4298
- });
4299
- return;
4300
- }
4301
- const currentConfig = deps.core.configManager.get();
4302
- const cloned = structuredClone(currentConfig);
4303
- let target = cloned;
4304
- for (let i = 0; i < parts.length - 1; i++) {
4305
- const part = parts[i];
4306
- if (target[part] && typeof target[part] === "object" && !Array.isArray(target[part])) {
4307
- target = target[part];
4308
- } else if (target[part] === void 0 || target[part] === null) {
4309
- target[part] = {};
4310
- target = target[part];
4311
- } else {
4312
- deps.sendJson(res, 400, { error: "Invalid config path" });
4313
- return;
4314
- }
4315
- }
4316
- const lastKey = parts[parts.length - 1];
4317
- target[lastKey] = value;
4318
- const { ConfigSchema } = await import("./config-6S355X75.js");
4319
- const result = ConfigSchema.safeParse(cloned);
4320
- if (!result.success) {
4321
- deps.sendJson(res, 400, {
4322
- error: "Validation failed",
4323
- details: result.error.issues.map((i) => ({
4324
- path: i.path.join("."),
4325
- message: i.message
4326
- }))
4327
- });
4328
- return;
4329
- }
4330
- const updates = {};
4331
- let updateTarget = updates;
4332
- for (let i = 0; i < parts.length - 1; i++) {
4333
- updateTarget[parts[i]] = {};
4334
- updateTarget = updateTarget[parts[i]];
4335
- }
4336
- updateTarget[lastKey] = value;
4337
- await deps.core.configManager.save(updates, configPath);
4338
- const { isHotReloadable: isHotReloadable2 } = await import("./config-registry-AHYI4MYL.js");
4339
- const needsRestart = !isHotReloadable2(configPath);
4340
- deps.sendJson(res, 200, {
4341
- ok: true,
4342
- needsRestart,
4343
- config: redactConfig(deps.core.configManager.get())
4344
- });
4345
- });
4346
- }
4347
-
4348
- // src/core/api/routes/topics.ts
4349
- function registerTopicRoutes(router, deps) {
4350
- router.get("/api/topics", async (req, res) => {
4351
- if (!deps.topicManager) {
4352
- deps.sendJson(res, 501, { error: "Topic management not available" });
4353
- return;
4354
- }
4355
- const url = req.url || "";
4356
- const params = new URL(url, "http://localhost").searchParams;
4357
- const statusParam = params.get("status");
4358
- const filter = statusParam ? { statuses: statusParam.split(",") } : void 0;
4359
- const topics = deps.topicManager.listTopics(filter);
4360
- deps.sendJson(res, 200, { topics });
4361
- });
4362
- router.post("/api/topics/cleanup", async (req, res) => {
4363
- if (!deps.topicManager) {
4364
- deps.sendJson(res, 501, { error: "Topic management not available" });
4365
- return;
4366
- }
4367
- const body = await deps.readBody(req);
4368
- let statuses;
4369
- if (body) {
4370
- try {
4371
- statuses = JSON.parse(body).statuses;
4372
- } catch {
4373
- }
4374
- }
4375
- const result = await deps.topicManager.cleanup(statuses);
4376
- deps.sendJson(res, 200, result);
4377
- });
4378
- router.delete("/api/topics/:sessionId", async (req, res, params) => {
4379
- if (!deps.topicManager) {
4380
- deps.sendJson(res, 501, { error: "Topic management not available" });
4381
- return;
4382
- }
4383
- const sessionId = decodeURIComponent(params.sessionId);
4384
- const url = req.url || "";
4385
- const urlParams = new URL(url, "http://localhost").searchParams;
4386
- const force = urlParams.get("force") === "true";
4387
- const result = await deps.topicManager.deleteTopic(
4388
- sessionId,
4389
- force ? { confirmed: true } : void 0
4390
- );
4391
- if (result.ok) {
4392
- deps.sendJson(res, 200, result);
4393
- } else if (result.needsConfirmation) {
4394
- deps.sendJson(res, 409, {
4395
- error: "Session is active",
4396
- needsConfirmation: true,
4397
- session: result.session
4398
- });
4399
- } else if (result.error === "Cannot delete system topic") {
4400
- deps.sendJson(res, 403, { error: result.error });
4401
- } else {
4402
- deps.sendJson(res, 404, { error: result.error ?? "Not found" });
4403
- }
4404
- });
4405
- }
4406
-
4407
- // src/core/api/routes/tunnel.ts
4408
- function registerTunnelRoutes(router, deps) {
4409
- router.get("/api/tunnel", async (_req, res) => {
4410
- const tunnel = deps.core.tunnelService;
4411
- if (tunnel) {
4412
- deps.sendJson(res, 200, {
4413
- enabled: true,
4414
- url: tunnel.getPublicUrl(),
4415
- provider: deps.core.configManager.get().tunnel.provider
4416
- });
4417
- } else {
4418
- deps.sendJson(res, 200, { enabled: false });
4419
- }
4420
- });
4421
- router.get("/api/tunnel/list", async (_req, res) => {
4422
- const tunnel = deps.core.tunnelService;
4423
- if (!tunnel) {
4424
- deps.sendJson(res, 200, []);
4425
- return;
4426
- }
4427
- deps.sendJson(res, 200, tunnel.listTunnels());
4428
- });
4429
- router.post("/api/tunnel", async (req, res) => {
4430
- const tunnel = deps.core.tunnelService;
4431
- if (!tunnel) {
4432
- deps.sendJson(res, 400, { error: "Tunnel service is not enabled" });
4433
- return;
4434
- }
4435
- const body = await deps.readBody(req);
4436
- if (body === null) {
4437
- deps.sendJson(res, 413, { error: "Request body too large" });
4438
- return;
4439
- }
4440
- if (!body) {
4441
- deps.sendJson(res, 400, { error: "Missing request body" });
4442
- return;
4443
- }
4444
- try {
4445
- const { port, label, sessionId } = JSON.parse(body);
4446
- if (!port || typeof port !== "number") {
4447
- deps.sendJson(res, 400, {
4448
- error: "port is required and must be a number"
4449
- });
4450
- return;
4451
- }
4452
- const entry = await tunnel.addTunnel(port, { label, sessionId });
4453
- deps.sendJson(res, 200, entry);
4454
- } catch (err) {
4455
- deps.sendJson(res, 400, { error: err.message });
4456
- }
4457
- });
4458
- router.delete("/api/tunnel/:port", async (_req, res, params) => {
4459
- const tunnel = deps.core.tunnelService;
4460
- if (!tunnel) {
4461
- deps.sendJson(res, 400, { error: "Tunnel service is not enabled" });
4462
- return;
4463
- }
4464
- const port = parseInt(params.port, 10);
4465
- try {
4466
- await tunnel.stopTunnel(port);
4467
- deps.sendJson(res, 200, { ok: true });
4468
- } catch (err) {
4469
- deps.sendJson(res, 400, { error: err.message });
4470
- }
4471
- });
4472
- router.delete("/api/tunnel", async (_req, res) => {
4473
- const tunnel = deps.core.tunnelService;
4474
- if (!tunnel) {
4475
- deps.sendJson(res, 400, { error: "Tunnel service is not enabled" });
4476
- return;
4477
- }
4478
- const count = tunnel.listTunnels().length;
4479
- await tunnel.stopAllUser();
4480
- deps.sendJson(res, 200, { ok: true, stopped: count });
4481
- });
4482
- }
4483
-
4484
- // src/core/api/routes/agents.ts
4485
- function registerAgentRoutes(router, deps) {
4486
- router.get("/api/agents", async (_req, res) => {
4487
- const agents = deps.core.agentManager.getAvailableAgents();
4488
- const defaultAgent = deps.core.configManager.get().defaultAgent;
4489
- const agentsWithCaps = agents.map((a) => ({
4490
- ...a,
4491
- capabilities: getAgentCapabilities(a.name)
4492
- }));
4493
- deps.sendJson(res, 200, { agents: agentsWithCaps, default: defaultAgent });
4494
- });
4495
- }
4496
-
4497
- // src/core/api/routes/notify.ts
4498
- function registerNotifyRoutes(router, deps) {
4499
- router.post("/api/notify", async (req, res) => {
4500
- const body = await deps.readBody(req);
4501
- let message;
4502
- if (body) {
4503
- try {
4504
- const parsed = JSON.parse(body);
4505
- message = parsed.message;
4506
- } catch {
4507
- deps.sendJson(res, 400, { error: "Invalid JSON body" });
4508
- return;
4509
- }
4510
- }
4511
- if (!message) {
4512
- deps.sendJson(res, 400, { error: "Missing message" });
4513
- return;
4514
- }
4515
- await deps.core.notificationManager.notifyAll({
4516
- sessionId: "system",
4517
- type: "completed",
4518
- summary: message
4519
- });
4520
- deps.sendJson(res, 200, { ok: true });
4521
- });
4522
- }
4523
-
4524
- // src/core/api/index.ts
4525
- var log9 = createChildLogger({ module: "api-server" });
4526
- var DEFAULT_PORT_FILE = path9.join(os3.homedir(), ".openacp", "api.port");
4527
- var cachedVersion;
4528
- function getVersion() {
4529
- if (cachedVersion) return cachedVersion;
4530
- try {
4531
- const __filename = fileURLToPath2(import.meta.url);
4532
- const pkgPath = path9.resolve(
4533
- path9.dirname(__filename),
4534
- "../../../package.json"
4535
- );
4536
- const pkg = JSON.parse(fs8.readFileSync(pkgPath, "utf-8"));
4537
- cachedVersion = pkg.version ?? "0.0.0-dev";
4538
- } catch {
4539
- cachedVersion = "0.0.0-dev";
4540
- }
4541
- return cachedVersion;
4542
- }
4543
- var ApiServer = class {
4544
- constructor(core, config, portFilePath, topicManager, secretFilePath, uiDir) {
4545
- this.core = core;
4546
- this.config = config;
4547
- this.topicManager = topicManager;
4548
- this.portFilePath = portFilePath ?? DEFAULT_PORT_FILE;
4549
- this.secretFilePath = secretFilePath ?? path9.join(os3.homedir(), ".openacp", "api-secret");
4550
- this.staticServer = new StaticServer(uiDir);
4551
- this.sseManager = new SSEManager(
4552
- core.eventBus,
4553
- () => {
4554
- const sessions = this.core.sessionManager.listSessions();
4555
- return {
4556
- active: sessions.filter(
4557
- (s) => s.status === "active" || s.status === "initializing"
4558
- ).length,
4559
- total: sessions.length
4560
- };
4561
- },
4562
- this.startedAt
4563
- );
4564
- this.router = new Router();
4565
- const deps = {
4566
- core: this.core,
4567
- topicManager: this.topicManager,
4568
- startedAt: this.startedAt,
4569
- getVersion,
4570
- sendJson: this.sendJson.bind(this),
4571
- readBody: this.readBody.bind(this)
4572
- };
4573
- registerHealthRoutes(this.router, deps);
4574
- registerSessionRoutes(this.router, deps);
4575
- registerConfigRoutes(this.router, deps);
4576
- registerTopicRoutes(this.router, deps);
4577
- registerTunnelRoutes(this.router, deps);
4578
- registerAgentRoutes(this.router, deps);
4579
- registerNotifyRoutes(this.router, deps);
4580
- }
4581
- server = null;
4582
- actualPort = 0;
4583
- portFilePath;
4584
- startedAt = Date.now();
4585
- secret = "";
4586
- secretFilePath;
4587
- sseManager;
4588
- staticServer;
4589
- router;
4590
- async start() {
4591
- this.loadOrCreateSecret();
4592
- this.server = http.createServer((req, res) => this.handleRequest(req, res));
4593
- await new Promise((resolve3, reject) => {
4594
- this.server.on("error", (err) => {
4595
- if (err.code === "EADDRINUSE") {
4596
- log9.warn(
4597
- { port: this.config.port },
4598
- "API port in use, continuing without API server"
4599
- );
4600
- this.server = null;
4601
- resolve3();
4602
- } else {
4603
- reject(err);
4604
- }
4605
- });
4606
- this.server.listen(this.config.port, this.config.host, () => {
4607
- const addr = this.server.address();
4608
- if (addr && typeof addr === "object") {
4609
- this.actualPort = addr.port;
4610
- }
4611
- this.writePortFile();
4612
- log9.info(
4613
- { host: this.config.host, port: this.actualPort },
4614
- "API server listening"
4615
- );
4616
- this.sseManager.setup();
4617
- if (this.config.host !== "127.0.0.1" && this.config.host !== "localhost") {
4618
- log9.warn(
4619
- "API server binding to non-localhost. Ensure api-secret file is secured."
4620
- );
4621
- }
4622
- resolve3();
4623
- });
4624
- });
4625
- }
4626
- async stop() {
4627
- this.sseManager.stop();
4628
- this.removePortFile();
4629
- if (this.server) {
4630
- await new Promise((resolve3) => {
4631
- this.server.close(() => resolve3());
4632
- });
4633
- this.server = null;
4634
- }
4635
- }
4636
- getPort() {
4637
- return this.actualPort;
4638
- }
4639
- getSecret() {
4640
- return this.secret;
4641
- }
4642
- writePortFile() {
4643
- const dir = path9.dirname(this.portFilePath);
4644
- fs8.mkdirSync(dir, { recursive: true });
4645
- fs8.writeFileSync(this.portFilePath, String(this.actualPort));
4646
- }
4647
- removePortFile() {
4648
- try {
4649
- fs8.unlinkSync(this.portFilePath);
4650
- } catch {
4651
- }
4652
- }
4653
- loadOrCreateSecret() {
4654
- const dir = path9.dirname(this.secretFilePath);
4655
- fs8.mkdirSync(dir, { recursive: true });
4656
- try {
4657
- this.secret = fs8.readFileSync(this.secretFilePath, "utf-8").trim();
4658
- if (this.secret) {
4659
- try {
4660
- const stat = fs8.statSync(this.secretFilePath);
4661
- const mode = stat.mode & 511;
4662
- if (mode & 63) {
4663
- log9.warn(
4664
- { path: this.secretFilePath, mode: "0" + mode.toString(8) },
4665
- "API secret file has insecure permissions (should be 0600). Run: chmod 600 %s",
4666
- this.secretFilePath
4667
- );
4668
- }
4669
- } catch {
4670
- }
4671
- return;
4672
- }
4673
- } catch {
4674
- }
4675
- this.secret = crypto2.randomBytes(32).toString("hex");
4676
- fs8.writeFileSync(this.secretFilePath, this.secret, { mode: 384 });
4677
- }
4678
- authenticate(req, allowQueryParam = false) {
4679
- const authHeader = req.headers.authorization;
4680
- if (authHeader?.startsWith("Bearer ")) {
4681
- const token = authHeader.slice(7);
4682
- if (token.length === this.secret.length && crypto2.timingSafeEqual(
4683
- Buffer.from(token, "utf-8"),
4684
- Buffer.from(this.secret, "utf-8")
4685
- )) {
4686
- return true;
4687
- }
4688
- }
4689
- if (allowQueryParam) {
4690
- const parsedUrl = new URL(req.url || "", "http://localhost");
4691
- const qToken = parsedUrl.searchParams.get("token");
4692
- if (qToken && qToken.length === this.secret.length && crypto2.timingSafeEqual(
4693
- Buffer.from(qToken, "utf-8"),
4694
- Buffer.from(this.secret, "utf-8")
4695
- )) {
4696
- return true;
4697
- }
4698
- }
4699
- return false;
4700
- }
4701
- async handleRequest(req, res) {
4702
- const method = req.method?.toUpperCase();
4703
- const url = req.url || "";
4704
- if (url.startsWith("/api/")) {
4705
- const isExempt = method === "GET" && (url === "/api/health" || url === "/api/version" || url.startsWith("/api/events"));
4706
- if (!isExempt && !this.authenticate(req)) {
4707
- this.sendJson(res, 401, { error: "Unauthorized" });
4708
- return;
4709
- }
4710
- }
4711
- try {
4712
- if (method === "GET" && url.startsWith("/api/events")) {
4713
- if (!this.authenticate(req, true)) {
4714
- this.sendJson(res, 401, { error: "Unauthorized" });
4715
- return;
4716
- }
4717
- this.sseManager.handleRequest(req, res);
4718
- return;
4719
- }
4720
- if (url.startsWith("/api/")) {
4721
- const match = this.router.match(method, url);
4722
- if (match) {
4723
- await match.handler(req, res, match.params);
4724
- } else {
4725
- this.sendJson(res, 404, { error: "Not found" });
4726
- }
4727
- return;
4728
- }
4729
- if (!this.staticServer.serve(req, res)) {
4730
- this.sendJson(res, 404, { error: "Not found" });
4731
- }
4732
- } catch (err) {
4733
- log9.error({ err }, "API request error");
4734
- this.sendJson(res, 500, { error: "Internal server error" });
4735
- }
4736
- }
4737
- sendJson(res, status, data) {
4738
- res.writeHead(status, { "Content-Type": "application/json" });
4739
- res.end(JSON.stringify(data));
4740
- }
4741
- readBody(req) {
4742
- const MAX_BODY_SIZE = 1024 * 1024;
4743
- return new Promise((resolve3) => {
4744
- let data = "";
4745
- let size = 0;
4746
- let destroyed = false;
4747
- req.on("data", (chunk) => {
4748
- size += chunk.length;
4749
- if (size > MAX_BODY_SIZE && !destroyed) {
4750
- destroyed = true;
4751
- req.destroy();
4752
- resolve3(null);
4753
- return;
4754
- }
4755
- if (!destroyed) data += chunk;
4756
- });
4757
- req.on("end", () => {
4758
- if (!destroyed) resolve3(data);
4759
- });
4760
- req.on("error", () => {
4761
- if (!destroyed) resolve3("");
4762
- });
4763
- });
4764
- }
4765
- };
4766
-
4767
- // src/core/topic-manager.ts
4768
- var log10 = createChildLogger({ module: "topic-manager" });
4769
- var TopicManager = class {
4770
- constructor(sessionManager, adapter, systemTopicIds) {
4771
- this.sessionManager = sessionManager;
4772
- this.adapter = adapter;
4773
- this.systemTopicIds = systemTopicIds;
4774
- }
4775
- listTopics(filter) {
4776
- const records = this.sessionManager.listRecords(filter);
4777
- return records.filter((r) => !this.isSystemTopic(r)).filter((r) => !filter?.statuses?.length || filter.statuses.includes(r.status)).map((r) => ({
4778
- sessionId: r.sessionId,
4779
- topicId: r.platform?.topicId ?? null,
4780
- name: r.name ?? null,
4781
- status: r.status,
4782
- agentName: r.agentName,
4783
- lastActiveAt: r.lastActiveAt
4784
- }));
4785
- }
4786
- async deleteTopic(sessionId, options) {
4787
- const records = this.sessionManager.listRecords();
4788
- const record = records.find((r) => r.sessionId === sessionId);
4789
- if (!record) return { ok: false, error: "Session not found" };
4790
- if (this.isSystemTopic(record)) return { ok: false, error: "Cannot delete system topic" };
4791
- const isActive = record.status === "active" || record.status === "initializing";
4792
- if (isActive && !options?.confirmed) {
4793
- return {
4794
- ok: false,
4795
- needsConfirmation: true,
4796
- session: { id: record.sessionId, name: record.name ?? null, status: record.status }
4797
- };
4798
- }
4799
- if (isActive) {
4800
- await this.sessionManager.cancelSession(sessionId);
4801
- }
4802
- const topicId = record.platform?.topicId ?? null;
4803
- if (this.adapter && topicId) {
4804
- try {
4805
- await this.adapter.deleteSessionThread(sessionId);
4806
- } catch (err) {
4807
- log10.warn({ err, sessionId, topicId }, "Failed to delete platform thread, removing record anyway");
4808
- }
4809
- }
4810
- await this.sessionManager.removeRecord(sessionId);
4811
- return { ok: true, topicId };
4812
- }
4813
- async cleanup(statuses) {
4814
- const targetStatuses = statuses?.length ? statuses : ["finished", "error", "cancelled"];
4815
- const records = this.sessionManager.listRecords({ statuses: targetStatuses });
4816
- const targets = records.filter((r) => !this.isSystemTopic(r)).filter((r) => targetStatuses.includes(r.status));
4817
- const deleted = [];
4818
- const failed = [];
4819
- for (const record of targets) {
4820
- try {
4821
- const isActive = record.status === "active" || record.status === "initializing";
4822
- if (isActive) {
4823
- await this.sessionManager.cancelSession(record.sessionId);
4824
- }
4825
- const topicId = record.platform?.topicId;
4826
- if (this.adapter && topicId) {
4827
- try {
4828
- await this.adapter.deleteSessionThread(record.sessionId);
4829
- } catch (err) {
4830
- log10.warn({ err, sessionId: record.sessionId }, "Failed to delete platform thread during cleanup");
4831
- }
4832
- }
4833
- await this.sessionManager.removeRecord(record.sessionId);
4834
- deleted.push(record.sessionId);
4835
- } catch (err) {
4836
- failed.push({ sessionId: record.sessionId, error: err instanceof Error ? err.message : String(err) });
4837
- }
4838
- }
4839
- return { deleted, failed };
4840
- }
4841
- isSystemTopic(record) {
4842
- const topicId = record.platform?.topicId;
4843
- if (!topicId) return false;
4844
- return topicId === this.systemTopicIds.notificationTopicId || topicId === this.systemTopicIds.assistantTopicId;
4845
- }
4846
- };
4847
-
4848
- export {
4849
- nodeToWebWritable,
4850
- nodeToWebReadable,
4851
- StderrCapture,
4852
- TypedEmitter,
4853
- AgentInstance,
4854
- AgentManager,
4855
- PromptQueue,
4856
- PermissionGate,
4857
- Session,
4858
- SessionManager,
4859
- FileService,
4860
- SessionBridge,
4861
- NotificationManager,
4862
- MessageTransformer,
4863
- UsageStore,
4864
- UsageBudget,
4865
- SecurityGuard,
4866
- SessionFactory,
4867
- EventBus,
4868
- SpeechService,
4869
- GroqSTT,
4870
- ContextManager,
4871
- DEFAULT_MAX_TOKENS,
4872
- CheckpointReader,
4873
- EntireProvider,
4874
- OpenACPCore,
4875
- SSEManager,
4876
- StaticServer,
4877
- ApiServer,
4878
- TopicManager
4879
- };
4880
- //# sourceMappingURL=chunk-LGP2YGRL.js.map