@openacp/cli 0.6.1 → 0.6.3

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 (101) hide show
  1. package/dist/{admin-IKPS5PFC.js → admin-3ZHEO5VP.js} +8 -4
  2. package/dist/{agent-catalog-T5ECPEDA.js → agent-catalog-IVU2KANH.js} +2 -2
  3. package/dist/{agents-55NX3DHM.js → agents-SXIY4IEF.js} +2 -2
  4. package/dist/{chunk-H7ZMPBZC.js → chunk-3KGRVAEV.js} +1 -1
  5. package/dist/chunk-3KGRVAEV.js.map +1 -0
  6. package/dist/{chunk-GINCOFNW.js → chunk-6LSFRNHE.js} +2 -2
  7. package/dist/chunk-6LSFRNHE.js.map +1 -0
  8. package/dist/{chunk-UB7XUO7C.js → chunk-AVCHZESZ.js} +3 -3
  9. package/dist/{chunk-J6X5SW6O.js → chunk-CKOK7JW6.js} +3 -2
  10. package/dist/{chunk-J6X5SW6O.js.map → chunk-CKOK7JW6.js.map} +1 -1
  11. package/dist/{chunk-4LFDEW22.js → chunk-EWYNCHUH.js} +33 -20
  12. package/dist/chunk-EWYNCHUH.js.map +1 -0
  13. package/dist/{chunk-4TR5Y3MP.js → chunk-F3AICYO4.js} +39 -1
  14. package/dist/chunk-F3AICYO4.js.map +1 -0
  15. package/dist/{chunk-LGQYTK55.js → chunk-FMWSVLRM.js} +31 -2
  16. package/dist/chunk-FMWSVLRM.js.map +1 -0
  17. package/dist/{chunk-AKIU4JBF.js → chunk-FZ5BIWG5.js} +6 -6
  18. package/dist/chunk-FZ5BIWG5.js.map +1 -0
  19. package/dist/{chunk-2KJC3ILH.js → chunk-G3OHCXZG.js} +34 -24
  20. package/dist/chunk-G3OHCXZG.js.map +1 -0
  21. package/dist/{chunk-R3UJUOXI.js → chunk-HP2IJYCA.js} +1817 -1015
  22. package/dist/chunk-HP2IJYCA.js.map +1 -0
  23. package/dist/chunk-IER5UCY7.js +298 -0
  24. package/dist/chunk-IER5UCY7.js.map +1 -0
  25. package/dist/{chunk-TOZQ3JFN.js → chunk-KO5RL7MZ.js} +2 -2
  26. package/dist/{chunk-7G5QKLLF.js → chunk-NXEQXRQR.js} +53 -9
  27. package/dist/chunk-NXEQXRQR.js.map +1 -0
  28. package/dist/{chunk-ZCHNAM3B.js → chunk-OHR6SBMC.js} +3 -3
  29. package/dist/chunk-PWFPTG5X.js +101 -0
  30. package/dist/chunk-PWFPTG5X.js.map +1 -0
  31. package/dist/{chunk-IMILOCR5.js → chunk-TMCQZAXN.js} +2 -2
  32. package/dist/chunk-TMCQZAXN.js.map +1 -0
  33. package/dist/{chunk-T22OLSET.js → chunk-TTDSLV35.js} +1 -1
  34. package/dist/chunk-TTDSLV35.js.map +1 -0
  35. package/dist/cli.js +41 -36
  36. package/dist/cli.js.map +1 -1
  37. package/dist/{config-AK2W3E67.js → config-4YSJ4NCI.js} +2 -2
  38. package/dist/{config-editor-VIA7A72X.js → config-editor-F25HEMGL.js} +4 -4
  39. package/dist/{config-registry-QQOJ2GQP.js → config-registry-7I6GGDOY.js} +2 -2
  40. package/dist/{daemon-G27YZUWB.js → daemon-I6XMRQ6P.js} +3 -3
  41. package/dist/{discord-2DKRH45T.js → discord-VHCBN3JJ.js} +226 -172
  42. package/dist/discord-VHCBN3JJ.js.map +1 -0
  43. package/dist/{doctor-CHCYUTV5.js → doctor-GPW5ECK6.js} +4 -4
  44. package/dist/doctor-Y3SCSVPI.js +9 -0
  45. package/dist/index.d.ts +176 -90
  46. package/dist/index.js +22 -12
  47. package/dist/install-cloudflared-G2GUKCHA.js +32 -0
  48. package/dist/install-cloudflared-G2GUKCHA.js.map +1 -0
  49. package/dist/install-jq-7QTU7XYY.js +31 -0
  50. package/dist/install-jq-7QTU7XYY.js.map +1 -0
  51. package/dist/{integrate-VOUYBPPZ.js → integrate-O4OCR4SN.js} +23 -11
  52. package/dist/integrate-O4OCR4SN.js.map +1 -0
  53. package/dist/{main-56SPFYW4.js → main-P4X6SAPZ.js} +29 -18
  54. package/dist/main-P4X6SAPZ.js.map +1 -0
  55. package/dist/{new-session-DRRP2J7E.js → new-session-PUNUHGYP.js} +3 -3
  56. package/dist/post-upgrade-6N4JCV5S.js +79 -0
  57. package/dist/post-upgrade-6N4JCV5S.js.map +1 -0
  58. package/dist/{session-FVFLBREJ.js → session-ZMAM67AA.js} +2 -2
  59. package/dist/{settings-LPOLJ6SA.js → settings-OEQEZS5Y.js} +3 -2
  60. package/dist/{setup-IPWJCIJM.js → setup-7YBFKRG7.js} +5 -3
  61. package/dist/{tunnel-service-U6V4HQOO.js → tunnel-service-BMIBHUBK.js} +35 -17
  62. package/dist/tunnel-service-BMIBHUBK.js.map +1 -0
  63. package/package.json +2 -1
  64. package/dist/chunk-2KJC3ILH.js.map +0 -1
  65. package/dist/chunk-4LFDEW22.js.map +0 -1
  66. package/dist/chunk-4TR5Y3MP.js.map +0 -1
  67. package/dist/chunk-7G5QKLLF.js.map +0 -1
  68. package/dist/chunk-AKIU4JBF.js.map +0 -1
  69. package/dist/chunk-GINCOFNW.js.map +0 -1
  70. package/dist/chunk-H7ZMPBZC.js.map +0 -1
  71. package/dist/chunk-IMILOCR5.js.map +0 -1
  72. package/dist/chunk-LGQYTK55.js.map +0 -1
  73. package/dist/chunk-R3UJUOXI.js.map +0 -1
  74. package/dist/chunk-RF3DUYFO.js +0 -103
  75. package/dist/chunk-RF3DUYFO.js.map +0 -1
  76. package/dist/chunk-T22OLSET.js.map +0 -1
  77. package/dist/chunk-THBR6OXH.js +0 -62
  78. package/dist/chunk-THBR6OXH.js.map +0 -1
  79. package/dist/discord-2DKRH45T.js.map +0 -1
  80. package/dist/doctor-AN6AZ3PF.js +0 -9
  81. package/dist/install-cloudflared-BTGUD7SW.js +0 -8
  82. package/dist/integrate-VOUYBPPZ.js.map +0 -1
  83. package/dist/main-56SPFYW4.js.map +0 -1
  84. package/dist/setup-IPWJCIJM.js.map +0 -1
  85. package/dist/tunnel-service-U6V4HQOO.js.map +0 -1
  86. /package/dist/{admin-IKPS5PFC.js.map → admin-3ZHEO5VP.js.map} +0 -0
  87. /package/dist/{agent-catalog-T5ECPEDA.js.map → agent-catalog-IVU2KANH.js.map} +0 -0
  88. /package/dist/{agents-55NX3DHM.js.map → agents-SXIY4IEF.js.map} +0 -0
  89. /package/dist/{chunk-UB7XUO7C.js.map → chunk-AVCHZESZ.js.map} +0 -0
  90. /package/dist/{chunk-TOZQ3JFN.js.map → chunk-KO5RL7MZ.js.map} +0 -0
  91. /package/dist/{chunk-ZCHNAM3B.js.map → chunk-OHR6SBMC.js.map} +0 -0
  92. /package/dist/{config-AK2W3E67.js.map → config-4YSJ4NCI.js.map} +0 -0
  93. /package/dist/{config-editor-VIA7A72X.js.map → config-editor-F25HEMGL.js.map} +0 -0
  94. /package/dist/{config-registry-QQOJ2GQP.js.map → config-registry-7I6GGDOY.js.map} +0 -0
  95. /package/dist/{daemon-G27YZUWB.js.map → daemon-I6XMRQ6P.js.map} +0 -0
  96. /package/dist/{doctor-AN6AZ3PF.js.map → doctor-GPW5ECK6.js.map} +0 -0
  97. /package/dist/{doctor-CHCYUTV5.js.map → doctor-Y3SCSVPI.js.map} +0 -0
  98. /package/dist/{install-cloudflared-BTGUD7SW.js.map → new-session-PUNUHGYP.js.map} +0 -0
  99. /package/dist/{new-session-DRRP2J7E.js.map → session-ZMAM67AA.js.map} +0 -0
  100. /package/dist/{session-FVFLBREJ.js.map → settings-OEQEZS5Y.js.map} +0 -0
  101. /package/dist/{settings-LPOLJ6SA.js.map → setup-7YBFKRG7.js.map} +0 -0
@@ -1,10 +1,11 @@
1
1
  import {
2
2
  ChannelAdapter,
3
- PRODUCT_GUIDE
4
- } from "./chunk-LGQYTK55.js";
3
+ PRODUCT_GUIDE,
4
+ dispatchMessage
5
+ } from "./chunk-FMWSVLRM.js";
5
6
  import {
6
7
  DoctorEngine
7
- } from "./chunk-ZCHNAM3B.js";
8
+ } from "./chunk-OHR6SBMC.js";
8
9
  import {
9
10
  buildMenuKeyboard,
10
11
  buildSkillMessages,
@@ -14,7 +15,7 @@ import {
14
15
  } from "./chunk-7QJS2XBD.js";
15
16
  import {
16
17
  AgentCatalog
17
- } from "./chunk-J6X5SW6O.js";
18
+ } from "./chunk-CKOK7JW6.js";
18
19
  import {
19
20
  getAgentCapabilities
20
21
  } from "./chunk-JKBFUAJK.js";
@@ -23,7 +24,7 @@ import {
23
24
  getSafeFields,
24
25
  isHotReloadable,
25
26
  resolveOptions
26
- } from "./chunk-4TR5Y3MP.js";
27
+ } from "./chunk-F3AICYO4.js";
27
28
  import {
28
29
  createChildLogger,
29
30
  createSessionLogger
@@ -33,10 +34,10 @@ import {
33
34
  function nodeToWebWritable(nodeStream) {
34
35
  return new WritableStream({
35
36
  write(chunk) {
36
- return new Promise((resolve2, reject) => {
37
+ return new Promise((resolve3, reject) => {
37
38
  nodeStream.write(Buffer.from(chunk), (err) => {
38
39
  if (err) reject(err);
39
- else resolve2();
40
+ else resolve3();
40
41
  });
41
42
  });
42
43
  }
@@ -71,6 +72,80 @@ var StderrCapture = class {
71
72
  }
72
73
  };
73
74
 
75
+ // src/core/typed-emitter.ts
76
+ var TypedEmitter = class {
77
+ // eslint-disable-next-line @typescript-eslint/no-explicit-any
78
+ listeners = /* @__PURE__ */ new Map();
79
+ paused = false;
80
+ buffer = [];
81
+ on(event, listener) {
82
+ let set = this.listeners.get(event);
83
+ if (!set) {
84
+ set = /* @__PURE__ */ new Set();
85
+ this.listeners.set(event, set);
86
+ }
87
+ set.add(listener);
88
+ return this;
89
+ }
90
+ off(event, listener) {
91
+ this.listeners.get(event)?.delete(listener);
92
+ return this;
93
+ }
94
+ emit(event, ...args) {
95
+ if (this.paused) {
96
+ if (this.passthroughFn?.(event, args)) {
97
+ this.deliver(event, args);
98
+ } else {
99
+ this.buffer.push({ event, args });
100
+ }
101
+ return;
102
+ }
103
+ this.deliver(event, args);
104
+ }
105
+ /**
106
+ * Pause event delivery. Events emitted while paused are buffered.
107
+ * Optionally pass a filter to allow specific events through even while paused.
108
+ */
109
+ pause(passthrough) {
110
+ this.paused = true;
111
+ this.passthroughFn = passthrough;
112
+ }
113
+ passthroughFn;
114
+ /** Resume event delivery and replay buffered events in order. */
115
+ resume() {
116
+ this.paused = false;
117
+ this.passthroughFn = void 0;
118
+ const buffered = this.buffer.splice(0);
119
+ for (const { event, args } of buffered) {
120
+ this.deliver(event, args);
121
+ }
122
+ }
123
+ /** Discard all buffered events without delivering them. */
124
+ clearBuffer() {
125
+ this.buffer.length = 0;
126
+ }
127
+ get isPaused() {
128
+ return this.paused;
129
+ }
130
+ get bufferSize() {
131
+ return this.buffer.length;
132
+ }
133
+ removeAllListeners(event) {
134
+ if (event) {
135
+ this.listeners.delete(event);
136
+ } else {
137
+ this.listeners.clear();
138
+ }
139
+ }
140
+ deliver(event, args) {
141
+ const set = this.listeners.get(event);
142
+ if (!set) return;
143
+ for (const listener of set) {
144
+ listener(...args);
145
+ }
146
+ }
147
+ };
148
+
74
149
  // src/core/agent-instance.ts
75
150
  import { spawn, execFileSync } from "child_process";
76
151
  import { Transform } from "stream";
@@ -134,7 +209,7 @@ function resolveAgentCommand(cmd) {
134
209
  }
135
210
  return { command: cmd, args: [] };
136
211
  }
137
- var AgentInstance = class _AgentInstance {
212
+ var AgentInstance = class _AgentInstance extends TypedEmitter {
138
213
  connection;
139
214
  child;
140
215
  stderrCapture;
@@ -142,11 +217,10 @@ var AgentInstance = class _AgentInstance {
142
217
  sessionId;
143
218
  agentName;
144
219
  promptCapabilities;
145
- // Callbacks — set by core when wiring events
146
- onSessionUpdate = () => {
147
- };
220
+ // Callback — set by core when wiring events
148
221
  onPermissionRequest = async () => "";
149
222
  constructor(agentName) {
223
+ super();
150
224
  this.agentName = agentName;
151
225
  }
152
226
  static async spawnSubprocess(agentDef, workingDirectory) {
@@ -169,7 +243,7 @@ var AgentInstance = class _AgentInstance {
169
243
  env: { ...process.env, ...agentDef.env }
170
244
  }
171
245
  );
172
- await new Promise((resolve2, reject) => {
246
+ await new Promise((resolve3, reject) => {
173
247
  instance.child.on("error", (err) => {
174
248
  reject(
175
249
  new Error(
@@ -177,7 +251,7 @@ var AgentInstance = class _AgentInstance {
177
251
  )
178
252
  );
179
253
  });
180
- instance.child.on("spawn", () => resolve2());
254
+ instance.child.on("spawn", () => resolve3());
181
255
  });
182
256
  instance.stderrCapture = new StderrCapture(50);
183
257
  instance.child.stderr.on("data", (chunk) => {
@@ -232,7 +306,7 @@ var AgentInstance = class _AgentInstance {
232
306
  );
233
307
  if (code !== 0 && code !== null) {
234
308
  const stderr = this.stderrCapture.getLastLines();
235
- this.onSessionUpdate({
309
+ this.emit("agent_event", {
236
310
  type: "error",
237
311
  message: `Agent crashed (exit code ${code})
238
312
  ${stderr}`
@@ -371,7 +445,7 @@ ${stderr}`
371
445
  return;
372
446
  }
373
447
  if (event !== null) {
374
- self.onSessionUpdate(event);
448
+ self.emit("agent_event", event);
375
449
  }
376
450
  },
377
451
  // ── Permission requests ──────────────────────────────────────────────
@@ -466,9 +540,9 @@ ${stderr}`
466
540
  signal: state.exitStatus.signal
467
541
  };
468
542
  }
469
- return new Promise((resolve2) => {
543
+ return new Promise((resolve3) => {
470
544
  state.process.on("exit", (code, signal) => {
471
- resolve2({ exitCode: code, signal });
545
+ resolve3({ exitCode: code, signal });
472
546
  });
473
547
  });
474
548
  },
@@ -563,80 +637,6 @@ var AgentManager = class {
563
637
  }
564
638
  };
565
639
 
566
- // src/core/typed-emitter.ts
567
- var TypedEmitter = class {
568
- // eslint-disable-next-line @typescript-eslint/no-explicit-any
569
- listeners = /* @__PURE__ */ new Map();
570
- paused = false;
571
- buffer = [];
572
- on(event, listener) {
573
- let set = this.listeners.get(event);
574
- if (!set) {
575
- set = /* @__PURE__ */ new Set();
576
- this.listeners.set(event, set);
577
- }
578
- set.add(listener);
579
- return this;
580
- }
581
- off(event, listener) {
582
- this.listeners.get(event)?.delete(listener);
583
- return this;
584
- }
585
- emit(event, ...args) {
586
- if (this.paused) {
587
- if (this.passthroughFn?.(event, args)) {
588
- this.deliver(event, args);
589
- } else {
590
- this.buffer.push({ event, args });
591
- }
592
- return;
593
- }
594
- this.deliver(event, args);
595
- }
596
- /**
597
- * Pause event delivery. Events emitted while paused are buffered.
598
- * Optionally pass a filter to allow specific events through even while paused.
599
- */
600
- pause(passthrough) {
601
- this.paused = true;
602
- this.passthroughFn = passthrough;
603
- }
604
- passthroughFn;
605
- /** Resume event delivery and replay buffered events in order. */
606
- resume() {
607
- this.paused = false;
608
- this.passthroughFn = void 0;
609
- const buffered = this.buffer.splice(0);
610
- for (const { event, args } of buffered) {
611
- this.deliver(event, args);
612
- }
613
- }
614
- /** Discard all buffered events without delivering them. */
615
- clearBuffer() {
616
- this.buffer.length = 0;
617
- }
618
- get isPaused() {
619
- return this.paused;
620
- }
621
- get bufferSize() {
622
- return this.buffer.length;
623
- }
624
- removeAllListeners(event) {
625
- if (event) {
626
- this.listeners.delete(event);
627
- } else {
628
- this.listeners.clear();
629
- }
630
- }
631
- deliver(event, args) {
632
- const set = this.listeners.get(event);
633
- if (!set) return;
634
- for (const listener of set) {
635
- listener(...args);
636
- }
637
- }
638
- };
639
-
640
640
  // src/core/prompt-queue.ts
641
641
  var PromptQueue = class {
642
642
  constructor(processor, onError) {
@@ -648,8 +648,8 @@ var PromptQueue = class {
648
648
  abortController = null;
649
649
  async enqueue(text, attachments) {
650
650
  if (this.processing) {
651
- return new Promise((resolve2) => {
652
- this.queue.push({ text, attachments, resolve: resolve2 });
651
+ return new Promise((resolve3) => {
652
+ this.queue.push({ text, attachments, resolve: resolve3 });
653
653
  });
654
654
  }
655
655
  await this.process(text, attachments);
@@ -714,8 +714,8 @@ var PermissionGate = class {
714
714
  this.request = request;
715
715
  this.settled = false;
716
716
  this.clearTimeout();
717
- return new Promise((resolve2, reject) => {
718
- this.resolveFn = resolve2;
717
+ return new Promise((resolve3, reject) => {
718
+ this.resolveFn = resolve3;
719
719
  this.rejectFn = reject;
720
720
  this.timeoutTimer = setTimeout(() => {
721
721
  this.reject("Permission request timed out (no response received)");
@@ -763,6 +763,12 @@ var PermissionGate = class {
763
763
  import { nanoid } from "nanoid";
764
764
  import * as fs2 from "fs";
765
765
  var moduleLog = createChildLogger({ module: "session" });
766
+ var TTS_PROMPT_INSTRUCTION = `
767
+
768
+ Additionally, include a [TTS]...[/TTS] block with a spoken-friendly summary of your response. Focus on key information, decisions the user needs to make, or actions required. The agent decides what to say and how long. Respond in the same language the user is using. This instruction applies to this message only.`;
769
+ var TTS_BLOCK_REGEX = /\[TTS\]([\s\S]*?)\[\/TTS\]/;
770
+ var TTS_MAX_LENGTH = 5e3;
771
+ var TTS_TIMEOUT_MS = 3e4;
766
772
  var VALID_TRANSITIONS = {
767
773
  initializing: /* @__PURE__ */ new Set(["active", "error"]),
768
774
  active: /* @__PURE__ */ new Set(["error", "finished", "cancelled"]),
@@ -781,6 +787,7 @@ var Session = class extends TypedEmitter {
781
787
  _status = "initializing";
782
788
  name;
783
789
  createdAt = /* @__PURE__ */ new Date();
790
+ voiceMode = "off";
784
791
  dangerousMode = false;
785
792
  archiving = false;
786
793
  log;
@@ -846,6 +853,11 @@ var Session = class extends TypedEmitter {
846
853
  get promptRunning() {
847
854
  return this.queue.isProcessing;
848
855
  }
856
+ // --- Voice Mode ---
857
+ setVoiceMode(mode) {
858
+ this.voiceMode = mode;
859
+ this.log.info({ voiceMode: mode }, "TTS mode changed");
860
+ }
849
861
  // --- Public API ---
850
862
  async enqueuePrompt(text, attachments) {
851
863
  await this.queue.enqueue(text, attachments);
@@ -861,11 +873,38 @@ var Session = class extends TypedEmitter {
861
873
  const promptStart = Date.now();
862
874
  this.log.debug("Prompt execution started");
863
875
  const processed = await this.maybeTranscribeAudio(text, attachments);
864
- await this.agentInstance.prompt(processed.text, processed.attachments);
876
+ const ttsActive = this.voiceMode !== "off" && !!this.speechService?.isTTSAvailable();
877
+ if (ttsActive) {
878
+ processed.text += TTS_PROMPT_INSTRUCTION;
879
+ if (this.voiceMode === "next") {
880
+ this.voiceMode = "off";
881
+ }
882
+ }
883
+ let accumulatedText = "";
884
+ const accumulatorListener = ttsActive ? (event) => {
885
+ if (event.type === "text") {
886
+ accumulatedText += event.content;
887
+ }
888
+ } : null;
889
+ if (accumulatorListener) {
890
+ this.on("agent_event", accumulatorListener);
891
+ }
892
+ try {
893
+ await this.agentInstance.prompt(processed.text, processed.attachments);
894
+ } finally {
895
+ if (accumulatorListener) {
896
+ this.off("agent_event", accumulatorListener);
897
+ }
898
+ }
865
899
  this.log.info(
866
900
  { durationMs: Date.now() - promptStart },
867
901
  "Prompt execution completed"
868
902
  );
903
+ if (ttsActive && accumulatedText) {
904
+ this.processTTSResponse(accumulatedText).catch((err) => {
905
+ this.log.warn({ err }, "TTS post-processing failed");
906
+ });
907
+ }
869
908
  if (!this.name) {
870
909
  await this.autoName();
871
910
  }
@@ -915,13 +954,44 @@ ${result.text}` : result.text;
915
954
  attachments: remainingAttachments.length > 0 ? remainingAttachments : void 0
916
955
  };
917
956
  }
957
+ async processTTSResponse(responseText) {
958
+ const match = TTS_BLOCK_REGEX.exec(responseText);
959
+ if (!match?.[1]) {
960
+ this.log.debug("No [TTS] block found in response, skipping synthesis");
961
+ return;
962
+ }
963
+ let ttsText = match[1].trim();
964
+ if (!ttsText) return;
965
+ if (ttsText.length > TTS_MAX_LENGTH) {
966
+ ttsText = ttsText.slice(0, TTS_MAX_LENGTH);
967
+ }
968
+ try {
969
+ const timeoutPromise = new Promise(
970
+ (_, reject) => setTimeout(() => reject(new Error("TTS synthesis timed out")), TTS_TIMEOUT_MS)
971
+ );
972
+ const result = await Promise.race([
973
+ this.speechService.synthesize(ttsText),
974
+ timeoutPromise
975
+ ]);
976
+ const base64 = result.audioBuffer.toString("base64");
977
+ this.emit("agent_event", {
978
+ type: "audio_content",
979
+ data: base64,
980
+ mimeType: result.mimeType
981
+ });
982
+ this.log.info("TTS synthesis completed");
983
+ } catch (err) {
984
+ this.log.warn({ err }, "TTS synthesis failed, skipping");
985
+ }
986
+ }
918
987
  // NOTE: This injects a summary prompt into the agent's conversation history.
919
988
  async autoName() {
920
989
  let title = "";
921
- const originalHandler = this.agentInstance.onSessionUpdate;
922
- this.agentInstance.onSessionUpdate = (event) => {
990
+ const captureHandler = (event) => {
923
991
  if (event.type === "text") title += event.content;
924
992
  };
993
+ this.pause((event) => event !== "agent_event");
994
+ this.agentInstance.on("agent_event", captureHandler);
925
995
  try {
926
996
  await this.agentInstance.prompt(
927
997
  "Summarize this conversation in max 5 words for a topic title. Reply ONLY with the title, nothing else."
@@ -932,7 +1002,9 @@ ${result.text}` : result.text;
932
1002
  } catch {
933
1003
  this.name = `Session ${this.id.slice(0, 6)}`;
934
1004
  } finally {
935
- this.agentInstance.onSessionUpdate = originalHandler;
1005
+ this.agentInstance.off("agent_event", captureHandler);
1006
+ this.clearBuffer();
1007
+ this.resume();
936
1008
  }
937
1009
  }
938
1010
  /** Fire-and-forget warm-up: primes model cache while user types their first message */
@@ -972,6 +1044,10 @@ ${result.text}` : result.text;
972
1044
  var SessionManager = class {
973
1045
  sessions = /* @__PURE__ */ new Map();
974
1046
  store;
1047
+ eventBus;
1048
+ setEventBus(eventBus) {
1049
+ this.eventBus = eventBus;
1050
+ }
975
1051
  constructor(store = null) {
976
1052
  this.store = store;
977
1053
  }
@@ -1074,6 +1150,7 @@ var SessionManager = class {
1074
1150
  async removeRecord(sessionId) {
1075
1151
  if (!this.store) return;
1076
1152
  await this.store.remove(sessionId);
1153
+ this.eventBus?.emit("session:deleted", { sessionId });
1077
1154
  }
1078
1155
  async destroyAll() {
1079
1156
  if (this.store) {
@@ -1198,6 +1275,7 @@ var SessionBridge = class {
1198
1275
  }
1199
1276
  connected = false;
1200
1277
  agentEventHandler;
1278
+ sessionEventHandler;
1201
1279
  statusChangeHandler;
1202
1280
  namedHandler;
1203
1281
  connect() {
@@ -1212,7 +1290,10 @@ var SessionBridge = class {
1212
1290
  if (!this.connected) return;
1213
1291
  this.connected = false;
1214
1292
  if (this.agentEventHandler) {
1215
- this.session.off("agent_event", this.agentEventHandler);
1293
+ this.session.agentInstance.off("agent_event", this.agentEventHandler);
1294
+ }
1295
+ if (this.sessionEventHandler) {
1296
+ this.session.off("agent_event", this.sessionEventHandler);
1216
1297
  }
1217
1298
  if (this.statusChangeHandler) {
1218
1299
  this.session.off("status_change", this.statusChangeHandler);
@@ -1220,14 +1301,13 @@ var SessionBridge = class {
1220
1301
  if (this.namedHandler) {
1221
1302
  this.session.off("named", this.namedHandler);
1222
1303
  }
1223
- this.session.agentInstance.onSessionUpdate = () => {
1224
- };
1225
1304
  this.session.agentInstance.onPermissionRequest = async () => "";
1226
1305
  }
1227
1306
  wireAgentToSession() {
1228
- this.session.agentInstance.onSessionUpdate = (event) => {
1307
+ this.agentEventHandler = (event) => {
1229
1308
  this.session.emit("agent_event", event);
1230
1309
  };
1310
+ this.session.agentInstance.on("agent_event", this.agentEventHandler);
1231
1311
  }
1232
1312
  wireSessionToAdapter() {
1233
1313
  const session = this.session;
@@ -1239,7 +1319,7 @@ var SessionBridge = class {
1239
1319
  return session.workingDirectory;
1240
1320
  }
1241
1321
  };
1242
- this.agentEventHandler = (event) => {
1322
+ this.sessionEventHandler = (event) => {
1243
1323
  switch (event.type) {
1244
1324
  case "text":
1245
1325
  case "thought":
@@ -1282,26 +1362,34 @@ var SessionBridge = class {
1282
1362
  break;
1283
1363
  case "image_content": {
1284
1364
  if (this.deps.fileService) {
1285
- const fs7 = this.deps.fileService;
1365
+ const fs8 = this.deps.fileService;
1286
1366
  const sid = this.session.id;
1287
1367
  const { data, mimeType } = event;
1288
1368
  const buffer = Buffer.from(data, "base64");
1289
1369
  const ext = FileService.extensionFromMime(mimeType);
1290
- fs7.saveFile(sid, `agent-image${ext}`, buffer, mimeType).then((att) => {
1291
- this.adapter.sendMessage(sid, { type: "attachment", text: "", attachment: att });
1370
+ fs8.saveFile(sid, `agent-image${ext}`, buffer, mimeType).then((att) => {
1371
+ this.adapter.sendMessage(sid, {
1372
+ type: "attachment",
1373
+ text: "",
1374
+ attachment: att
1375
+ });
1292
1376
  }).catch((err) => log2.error({ err }, "Failed to save agent image"));
1293
1377
  }
1294
1378
  break;
1295
1379
  }
1296
1380
  case "audio_content": {
1297
1381
  if (this.deps.fileService) {
1298
- const fs7 = this.deps.fileService;
1382
+ const fs8 = this.deps.fileService;
1299
1383
  const sid = this.session.id;
1300
1384
  const { data, mimeType } = event;
1301
1385
  const buffer = Buffer.from(data, "base64");
1302
1386
  const ext = FileService.extensionFromMime(mimeType);
1303
- fs7.saveFile(sid, `agent-audio${ext}`, buffer, mimeType).then((att) => {
1304
- this.adapter.sendMessage(sid, { type: "attachment", text: "", attachment: att });
1387
+ fs8.saveFile(sid, `agent-audio${ext}`, buffer, mimeType).then((att) => {
1388
+ this.adapter.sendMessage(sid, {
1389
+ type: "attachment",
1390
+ text: "",
1391
+ attachment: att
1392
+ });
1305
1393
  }).catch((err) => log2.error({ err }, "Failed to save agent audio"));
1306
1394
  }
1307
1395
  break;
@@ -1317,12 +1405,40 @@ var SessionBridge = class {
1317
1405
  );
1318
1406
  break;
1319
1407
  }
1408
+ this.deps.eventBus?.emit("agent:event", {
1409
+ sessionId: this.session.id,
1410
+ event
1411
+ });
1320
1412
  };
1321
- this.session.on("agent_event", this.agentEventHandler);
1413
+ this.session.on("agent_event", this.sessionEventHandler);
1322
1414
  }
1323
1415
  wirePermissions() {
1324
1416
  this.session.agentInstance.onPermissionRequest = async (request) => {
1325
1417
  this.session.emit("permission_request", request);
1418
+ this.deps.eventBus?.emit("permission:request", {
1419
+ sessionId: this.session.id,
1420
+ permission: request
1421
+ });
1422
+ if (request.description.toLowerCase().includes("openacp")) {
1423
+ const allowOption = request.options.find((o) => o.isAllow);
1424
+ if (allowOption) {
1425
+ log2.info(
1426
+ { sessionId: this.session.id, requestId: request.id },
1427
+ "Auto-approving openacp command"
1428
+ );
1429
+ return allowOption.id;
1430
+ }
1431
+ }
1432
+ if (this.session.dangerousMode) {
1433
+ const allowOption = request.options.find((o) => o.isAllow);
1434
+ if (allowOption) {
1435
+ log2.info(
1436
+ { sessionId: this.session.id, requestId: request.id, optionId: allowOption.id },
1437
+ "Dangerous mode: auto-approving permission"
1438
+ );
1439
+ return allowOption.id;
1440
+ }
1441
+ }
1326
1442
  const promise = this.session.permissionGate.setPending(request);
1327
1443
  await this.adapter.sendPermissionRequest(this.session.id, request);
1328
1444
  return promise;
@@ -1334,6 +1450,10 @@ var SessionBridge = class {
1334
1450
  status: to,
1335
1451
  lastActiveAt: (/* @__PURE__ */ new Date()).toISOString()
1336
1452
  });
1453
+ this.deps.eventBus?.emit("session:updated", {
1454
+ sessionId: this.session.id,
1455
+ status: to
1456
+ });
1337
1457
  if (to === "finished" || to === "cancelled") {
1338
1458
  queueMicrotask(() => this.disconnect());
1339
1459
  }
@@ -1341,6 +1461,10 @@ var SessionBridge = class {
1341
1461
  this.session.on("status_change", this.statusChangeHandler);
1342
1462
  this.namedHandler = (name) => {
1343
1463
  this.deps.sessionManager.patchRecord(this.session.id, { name });
1464
+ this.deps.eventBus?.emit("session:updated", {
1465
+ sessionId: this.session.id,
1466
+ name
1467
+ });
1344
1468
  this.adapter.renameSessionThread(this.session.id, name);
1345
1469
  };
1346
1470
  this.session.on("named", this.namedHandler);
@@ -1371,21 +1495,23 @@ function extractFileInfo(name, kind, content, rawInput, meta) {
1371
1495
  let info = null;
1372
1496
  if (meta) {
1373
1497
  const m = meta;
1374
- const tr = m?.claudeCode?.toolResponse;
1498
+ const claudeCode = m?.claudeCode;
1499
+ const tr = claudeCode?.toolResponse;
1375
1500
  const file = tr?.file;
1376
- if (file?.filePath && file?.content) {
1501
+ if (typeof file?.filePath === "string" && typeof file?.content === "string") {
1377
1502
  info = { filePath: file.filePath, content: file.content };
1378
1503
  }
1379
- if (!info && tr?.filePath && tr?.content) {
1504
+ if (!info && typeof tr?.filePath === "string" && typeof tr?.content === "string") {
1380
1505
  info = { filePath: tr.filePath, content: tr.content };
1381
1506
  }
1382
1507
  }
1383
- if (!info && rawInput) {
1508
+ if (!info && rawInput && typeof rawInput === "object") {
1384
1509
  const ri = rawInput;
1385
1510
  const filePath = ri?.file_path || ri?.filePath || ri?.path;
1386
1511
  if (typeof filePath === "string") {
1387
1512
  const parsed = content ? parseContent(content) : null;
1388
- info = { filePath, content: parsed?.content || ri?.content, oldContent: parsed?.oldContent };
1513
+ const riContent = typeof ri?.content === "string" ? ri.content : void 0;
1514
+ info = { filePath, content: parsed?.content || riContent, oldContent: parsed?.oldContent };
1389
1515
  }
1390
1516
  }
1391
1517
  if (!info && content) {
@@ -1780,71 +1906,185 @@ Sessions are NOT blocked \u2014 this is a warning only.`;
1780
1906
  }
1781
1907
  };
1782
1908
 
1783
- // src/core/speech/speech-service.ts
1784
- var SpeechService = class {
1785
- constructor(config) {
1786
- this.config = config;
1787
- }
1788
- sttProviders = /* @__PURE__ */ new Map();
1789
- ttsProviders = /* @__PURE__ */ new Map();
1790
- registerSTTProvider(name, provider) {
1791
- this.sttProviders.set(name, provider);
1792
- }
1793
- registerTTSProvider(name, provider) {
1794
- this.ttsProviders.set(name, provider);
1795
- }
1796
- isSTTAvailable() {
1797
- const { provider, providers } = this.config.stt;
1798
- return provider !== null && providers[provider]?.apiKey !== void 0;
1799
- }
1800
- isTTSAvailable() {
1801
- const { provider, providers } = this.config.tts;
1802
- return provider !== null && providers[provider]?.apiKey !== void 0;
1803
- }
1804
- async transcribe(audioBuffer, mimeType, options) {
1805
- const providerName = this.config.stt.provider;
1806
- if (!providerName || !this.config.stt.providers[providerName]?.apiKey) {
1807
- throw new Error("STT not configured. Set speech.stt.provider and API key in config.");
1808
- }
1809
- const provider = this.sttProviders.get(providerName);
1810
- if (!provider) {
1811
- throw new Error(`STT provider "${providerName}" not registered. Available: ${[...this.sttProviders.keys()].join(", ") || "none"}`);
1812
- }
1813
- return provider.transcribe(audioBuffer, mimeType, options);
1909
+ // src/core/security-guard.ts
1910
+ var SecurityGuard = class {
1911
+ constructor(configManager, sessionManager) {
1912
+ this.configManager = configManager;
1913
+ this.sessionManager = sessionManager;
1814
1914
  }
1815
- async synthesize(text, options) {
1816
- const providerName = this.config.tts.provider;
1817
- if (!providerName || !this.config.tts.providers[providerName]?.apiKey) {
1818
- throw new Error("TTS not configured. Set speech.tts.provider and API key in config.");
1915
+ checkAccess(message) {
1916
+ const config = this.configManager.get();
1917
+ if (config.security.allowedUserIds.length > 0) {
1918
+ const userId = String(message.userId);
1919
+ if (!config.security.allowedUserIds.includes(userId)) {
1920
+ return { allowed: false, reason: "Unauthorized user" };
1921
+ }
1819
1922
  }
1820
- const provider = this.ttsProviders.get(providerName);
1821
- if (!provider) {
1822
- throw new Error(`TTS provider "${providerName}" not registered. Available: ${[...this.ttsProviders.keys()].join(", ") || "none"}`);
1923
+ const active = this.sessionManager.listSessions().filter((s) => s.status === "active" || s.status === "initializing");
1924
+ if (active.length >= config.security.maxConcurrentSessions) {
1925
+ return { allowed: false, reason: `Session limit reached (${config.security.maxConcurrentSessions})` };
1823
1926
  }
1824
- return provider.synthesize(text, options);
1825
- }
1826
- updateConfig(config) {
1827
- this.config = config;
1927
+ return { allowed: true };
1828
1928
  }
1829
1929
  };
1830
1930
 
1831
- // src/core/speech/providers/groq.ts
1832
- var GROQ_API_URL = "https://api.groq.com/openai/v1/audio/transcriptions";
1833
- var GroqSTT = class {
1834
- constructor(apiKey, defaultModel = "whisper-large-v3-turbo") {
1835
- this.apiKey = apiKey;
1836
- this.defaultModel = defaultModel;
1931
+ // src/core/session-factory.ts
1932
+ import { nanoid as nanoid2 } from "nanoid";
1933
+ var log5 = createChildLogger({ module: "session-factory" });
1934
+ var SessionFactory = class {
1935
+ constructor(agentManager, sessionManager, speechService, eventBus) {
1936
+ this.agentManager = agentManager;
1937
+ this.sessionManager = sessionManager;
1938
+ this.speechService = speechService;
1939
+ this.eventBus = eventBus;
1837
1940
  }
1838
- name = "groq";
1839
- async transcribe(audioBuffer, mimeType, options) {
1840
- const ext = mimeToExt(mimeType);
1841
- const form = new FormData();
1842
- form.append("file", new Blob([new Uint8Array(audioBuffer)], { type: mimeType }), `audio${ext}`);
1843
- form.append("model", options?.model || this.defaultModel);
1844
- form.append("response_format", "verbose_json");
1845
- if (options?.language) {
1846
- form.append("language", options.language);
1847
- }
1941
+ async create(params) {
1942
+ const agentInstance = params.resumeAgentSessionId ? await this.agentManager.resume(
1943
+ params.agentName,
1944
+ params.workingDirectory,
1945
+ params.resumeAgentSessionId
1946
+ ) : await this.agentManager.spawn(
1947
+ params.agentName,
1948
+ params.workingDirectory
1949
+ );
1950
+ const session = new Session({
1951
+ id: params.existingSessionId,
1952
+ channelId: params.channelId,
1953
+ agentName: params.agentName,
1954
+ workingDirectory: params.workingDirectory,
1955
+ agentInstance,
1956
+ speechService: this.speechService
1957
+ });
1958
+ session.agentSessionId = agentInstance.sessionId;
1959
+ if (params.initialName) {
1960
+ session.name = params.initialName;
1961
+ }
1962
+ this.sessionManager.registerSession(session);
1963
+ this.eventBus.emit("session:created", {
1964
+ sessionId: session.id,
1965
+ agent: session.agentName,
1966
+ status: session.status
1967
+ });
1968
+ return session;
1969
+ }
1970
+ wireSideEffects(session, deps) {
1971
+ if (deps.usageStore) {
1972
+ const usageStore = deps.usageStore;
1973
+ const usageBudget = deps.usageBudget;
1974
+ const notificationManager = deps.notificationManager;
1975
+ session.on("agent_event", (event) => {
1976
+ if (event.type !== "usage") return;
1977
+ const record = {
1978
+ id: nanoid2(),
1979
+ sessionId: session.id,
1980
+ agentName: session.agentName,
1981
+ tokensUsed: event.tokensUsed ?? 0,
1982
+ contextSize: event.contextSize ?? 0,
1983
+ cost: event.cost,
1984
+ timestamp: (/* @__PURE__ */ new Date()).toISOString()
1985
+ };
1986
+ usageStore.append(record);
1987
+ if (usageBudget) {
1988
+ const result = usageBudget.check();
1989
+ if (result.message) {
1990
+ notificationManager.notifyAll({
1991
+ sessionId: session.id,
1992
+ sessionName: session.name,
1993
+ type: "budget_warning",
1994
+ summary: result.message
1995
+ });
1996
+ }
1997
+ }
1998
+ });
1999
+ }
2000
+ session.on("status_change", (_from, to) => {
2001
+ if ((to === "finished" || to === "cancelled") && deps.tunnelService) {
2002
+ deps.tunnelService.stopBySession(session.id).then((stopped) => {
2003
+ for (const entry of stopped) {
2004
+ deps.notificationManager.notifyAll({
2005
+ sessionId: session.id,
2006
+ sessionName: session.name,
2007
+ type: "completed",
2008
+ summary: `Tunnel stopped: port ${entry.port}${entry.label ? ` (${entry.label})` : ""} \u2014 session ended`
2009
+ }).catch(() => {
2010
+ });
2011
+ }
2012
+ }).catch(() => {
2013
+ });
2014
+ }
2015
+ });
2016
+ }
2017
+ };
2018
+
2019
+ // src/core/event-bus.ts
2020
+ var EventBus = class extends TypedEmitter {
2021
+ };
2022
+
2023
+ // src/core/speech/speech-service.ts
2024
+ var SpeechService = class {
2025
+ constructor(config) {
2026
+ this.config = config;
2027
+ }
2028
+ sttProviders = /* @__PURE__ */ new Map();
2029
+ ttsProviders = /* @__PURE__ */ new Map();
2030
+ registerSTTProvider(name, provider) {
2031
+ this.sttProviders.set(name, provider);
2032
+ }
2033
+ registerTTSProvider(name, provider) {
2034
+ this.ttsProviders.set(name, provider);
2035
+ }
2036
+ isSTTAvailable() {
2037
+ const { provider, providers } = this.config.stt;
2038
+ return provider !== null && providers[provider]?.apiKey !== void 0;
2039
+ }
2040
+ isTTSAvailable() {
2041
+ const provider = this.config.tts.provider;
2042
+ return provider !== null && this.ttsProviders.has(provider);
2043
+ }
2044
+ async transcribe(audioBuffer, mimeType, options) {
2045
+ const providerName = this.config.stt.provider;
2046
+ if (!providerName || !this.config.stt.providers[providerName]?.apiKey) {
2047
+ throw new Error("STT not configured. Set speech.stt.provider and API key in config.");
2048
+ }
2049
+ const provider = this.sttProviders.get(providerName);
2050
+ if (!provider) {
2051
+ throw new Error(`STT provider "${providerName}" not registered. Available: ${[...this.sttProviders.keys()].join(", ") || "none"}`);
2052
+ }
2053
+ return provider.transcribe(audioBuffer, mimeType, options);
2054
+ }
2055
+ async synthesize(text, options) {
2056
+ const providerName = this.config.tts.provider;
2057
+ if (!providerName) {
2058
+ throw new Error("TTS not configured. Set speech.tts.provider in config.");
2059
+ }
2060
+ const provider = this.ttsProviders.get(providerName);
2061
+ if (!provider) {
2062
+ throw new Error(`TTS provider "${providerName}" not registered. Available: ${[...this.ttsProviders.keys()].join(", ") || "none"}`);
2063
+ }
2064
+ return provider.synthesize(text, options);
2065
+ }
2066
+ updateConfig(config) {
2067
+ this.config = config;
2068
+ }
2069
+ };
2070
+
2071
+ // src/core/speech/providers/groq.ts
2072
+ var GROQ_API_URL = "https://api.groq.com/openai/v1/audio/transcriptions";
2073
+ var GroqSTT = class {
2074
+ constructor(apiKey, defaultModel = "whisper-large-v3-turbo") {
2075
+ this.apiKey = apiKey;
2076
+ this.defaultModel = defaultModel;
2077
+ }
2078
+ name = "groq";
2079
+ async transcribe(audioBuffer, mimeType, options) {
2080
+ const ext = mimeToExt(mimeType);
2081
+ const form = new FormData();
2082
+ form.append("file", new Blob([new Uint8Array(audioBuffer)], { type: mimeType }), `audio${ext}`);
2083
+ form.append("model", options?.model || this.defaultModel);
2084
+ form.append("response_format", "verbose_json");
2085
+ if (options?.language) {
2086
+ form.append("language", options.language);
2087
+ }
1848
2088
  const resp = await fetch(GROQ_API_URL, {
1849
2089
  method: "POST",
1850
2090
  headers: { Authorization: `Bearer ${this.apiKey}` },
@@ -1883,6 +2123,33 @@ function mimeToExt(mimeType) {
1883
2123
  return map[mimeType] || ".bin";
1884
2124
  }
1885
2125
 
2126
+ // src/core/speech/providers/edge-tts.ts
2127
+ var DEFAULT_VOICE = "en-US-AriaNeural";
2128
+ var EdgeTTS = class {
2129
+ name = "edge-tts";
2130
+ voice;
2131
+ constructor(voice) {
2132
+ this.voice = voice || DEFAULT_VOICE;
2133
+ }
2134
+ async synthesize(text, options) {
2135
+ const { MsEdgeTTS, OUTPUT_FORMAT } = await import("msedge-tts");
2136
+ const tts = new MsEdgeTTS();
2137
+ const voice = options?.voice || this.voice;
2138
+ const format = OUTPUT_FORMAT.AUDIO_24KHZ_48KBITRATE_MONO_MP3;
2139
+ await tts.setMetadata(voice, format);
2140
+ const { audioStream } = tts.toStream(text);
2141
+ const chunks = [];
2142
+ for await (const chunk of audioStream) {
2143
+ chunks.push(Buffer.isBuffer(chunk) ? chunk : Buffer.from(chunk));
2144
+ }
2145
+ tts.close();
2146
+ return {
2147
+ audioBuffer: Buffer.concat(chunks),
2148
+ mimeType: "audio/mpeg"
2149
+ };
2150
+ }
2151
+ };
2152
+
1886
2153
  // src/core/core.ts
1887
2154
  import path5 from "path";
1888
2155
  import os from "os";
@@ -1890,7 +2157,7 @@ import os from "os";
1890
2157
  // src/core/session-store.ts
1891
2158
  import fs5 from "fs";
1892
2159
  import path4 from "path";
1893
- var log5 = createChildLogger({ module: "session-store" });
2160
+ var log6 = createChildLogger({ module: "session-store" });
1894
2161
  var DEBOUNCE_MS2 = 2e3;
1895
2162
  var JsonFileSessionStore = class {
1896
2163
  records = /* @__PURE__ */ new Map();
@@ -1972,7 +2239,7 @@ var JsonFileSessionStore = class {
1972
2239
  fs5.readFileSync(this.filePath, "utf-8")
1973
2240
  );
1974
2241
  if (raw.version !== 1) {
1975
- log5.warn(
2242
+ log6.warn(
1976
2243
  { version: raw.version },
1977
2244
  "Unknown session store version, skipping load"
1978
2245
  );
@@ -1981,9 +2248,9 @@ var JsonFileSessionStore = class {
1981
2248
  for (const [id, record] of Object.entries(raw.sessions)) {
1982
2249
  this.records.set(id, record);
1983
2250
  }
1984
- log5.info({ count: this.records.size }, "Loaded session records");
2251
+ log6.info({ count: this.records.size }, "Loaded session records");
1985
2252
  } catch (err) {
1986
- log5.error({ err }, "Failed to load session store");
2253
+ log6.error({ err }, "Failed to load session store");
1987
2254
  }
1988
2255
  }
1989
2256
  cleanup() {
@@ -1999,7 +2266,7 @@ var JsonFileSessionStore = class {
1999
2266
  }
2000
2267
  }
2001
2268
  if (removed > 0) {
2002
- log5.info({ removed }, "Cleaned up expired session records");
2269
+ log6.info({ removed }, "Cleaned up expired session records");
2003
2270
  this.scheduleDiskWrite();
2004
2271
  }
2005
2272
  }
@@ -2012,8 +2279,7 @@ var JsonFileSessionStore = class {
2012
2279
  };
2013
2280
 
2014
2281
  // src/core/core.ts
2015
- import { nanoid as nanoid2 } from "nanoid";
2016
- var log6 = createChildLogger({ module: "core" });
2282
+ var log7 = createChildLogger({ module: "core" });
2017
2283
  var OpenACPCore = class {
2018
2284
  configManager;
2019
2285
  agentCatalog;
@@ -2023,12 +2289,15 @@ var OpenACPCore = class {
2023
2289
  messageTransformer;
2024
2290
  fileService;
2025
2291
  speechService;
2292
+ securityGuard;
2026
2293
  adapters = /* @__PURE__ */ new Map();
2027
2294
  /** Set by main.ts — triggers graceful shutdown with restart exit code */
2028
2295
  requestRestart = null;
2029
2296
  _tunnelService;
2030
2297
  sessionStore = null;
2031
2298
  resumeLocks = /* @__PURE__ */ new Map();
2299
+ eventBus;
2300
+ sessionFactory;
2032
2301
  usageStore = null;
2033
2302
  usageBudget = null;
2034
2303
  constructor(configManager) {
@@ -2043,6 +2312,7 @@ var OpenACPCore = class {
2043
2312
  config.sessionStore.ttlDays
2044
2313
  );
2045
2314
  this.sessionManager = new SessionManager(this.sessionStore);
2315
+ this.securityGuard = new SecurityGuard(configManager, this.sessionManager);
2046
2316
  this.notificationManager = new NotificationManager(this.adapters);
2047
2317
  const usageConfig = config.usage;
2048
2318
  if (usageConfig.enabled) {
@@ -2051,30 +2321,68 @@ var OpenACPCore = class {
2051
2321
  this.usageBudget = new UsageBudget(this.usageStore, usageConfig);
2052
2322
  }
2053
2323
  this.messageTransformer = new MessageTransformer();
2054
- this.fileService = new FileService(path5.join(os.homedir(), ".openacp", "files"));
2055
- const speechConfig = config.speech ?? { stt: { provider: null, providers: {} }, tts: { provider: null, providers: {} } };
2324
+ this.eventBus = new EventBus();
2325
+ this.sessionManager.setEventBus(this.eventBus);
2326
+ this.fileService = new FileService(
2327
+ path5.join(os.homedir(), ".openacp", "files")
2328
+ );
2329
+ const speechConfig = config.speech ?? {
2330
+ stt: { provider: null, providers: {} },
2331
+ tts: { provider: "edge-tts", providers: {} }
2332
+ };
2333
+ if (speechConfig.tts.provider == null) {
2334
+ speechConfig.tts.provider = "edge-tts";
2335
+ }
2056
2336
  this.speechService = new SpeechService(speechConfig);
2057
2337
  const groqConfig = speechConfig.stt?.providers?.groq;
2058
2338
  if (groqConfig?.apiKey) {
2059
- this.speechService.registerSTTProvider("groq", new GroqSTT(groqConfig.apiKey, groqConfig.model));
2060
- }
2061
- this.configManager.on("config:changed", async ({ path: configPath, value }) => {
2062
- if (configPath === "logging.level" && typeof value === "string") {
2063
- const { setLogLevel: setLogLevel2 } = await import("./log-SPS2S6FO.js");
2064
- setLogLevel2(value);
2065
- log6.info({ level: value }, "Log level changed at runtime");
2066
- }
2067
- if (configPath.startsWith("speech.")) {
2068
- const newConfig = this.configManager.get();
2069
- const newSpeechConfig = newConfig.speech ?? { stt: { provider: null, providers: {} }, tts: { provider: null, providers: {} } };
2070
- this.speechService.updateConfig(newSpeechConfig);
2071
- const groqCfg = newSpeechConfig.stt?.providers?.groq;
2072
- if (groqCfg?.apiKey) {
2073
- this.speechService.registerSTTProvider("groq", new GroqSTT(groqCfg.apiKey, groqCfg.model));
2339
+ this.speechService.registerSTTProvider(
2340
+ "groq",
2341
+ new GroqSTT(groqConfig.apiKey, groqConfig.model)
2342
+ );
2343
+ }
2344
+ {
2345
+ const edgeConfig = speechConfig.tts?.providers?.["edge-tts"];
2346
+ const voice = edgeConfig?.voice;
2347
+ this.speechService.registerTTSProvider("edge-tts", new EdgeTTS(voice));
2348
+ }
2349
+ this.sessionFactory = new SessionFactory(
2350
+ this.agentManager,
2351
+ this.sessionManager,
2352
+ this.speechService,
2353
+ this.eventBus
2354
+ );
2355
+ this.configManager.on(
2356
+ "config:changed",
2357
+ async ({ path: configPath, value }) => {
2358
+ if (configPath === "logging.level" && typeof value === "string") {
2359
+ const { setLogLevel: setLogLevel2 } = await import("./log-SPS2S6FO.js");
2360
+ setLogLevel2(value);
2361
+ log7.info({ level: value }, "Log level changed at runtime");
2362
+ }
2363
+ if (configPath.startsWith("speech.")) {
2364
+ const newConfig = this.configManager.get();
2365
+ const newSpeechConfig = newConfig.speech ?? {
2366
+ stt: { provider: null, providers: {} },
2367
+ tts: { provider: null, providers: {} }
2368
+ };
2369
+ this.speechService.updateConfig(newSpeechConfig);
2370
+ const groqCfg = newSpeechConfig.stt?.providers?.groq;
2371
+ if (groqCfg?.apiKey) {
2372
+ this.speechService.registerSTTProvider(
2373
+ "groq",
2374
+ new GroqSTT(groqCfg.apiKey, groqCfg.model)
2375
+ );
2376
+ }
2377
+ {
2378
+ const edgeConfig = newSpeechConfig.tts?.providers?.["edge-tts"];
2379
+ const voice = edgeConfig?.voice;
2380
+ this.speechService.registerTTSProvider("edge-tts", new EdgeTTS(voice));
2381
+ }
2382
+ log7.info("Speech service config updated at runtime");
2074
2383
  }
2075
- log6.info("Speech service config updated at runtime");
2076
2384
  }
2077
- });
2385
+ );
2078
2386
  }
2079
2387
  get tunnelService() {
2080
2388
  return this._tunnelService;
@@ -2088,7 +2396,7 @@ var OpenACPCore = class {
2088
2396
  }
2089
2397
  async start() {
2090
2398
  this.agentCatalog.refreshRegistryIfStale().catch((err) => {
2091
- log6.warn({ err }, "Background registry refresh failed");
2399
+ log7.warn({ err }, "Background registry refresh failed");
2092
2400
  });
2093
2401
  for (const adapter of this.adapters.values()) {
2094
2402
  await adapter.start();
@@ -2115,13 +2423,16 @@ var OpenACPCore = class {
2115
2423
  async archiveSession(sessionId) {
2116
2424
  const session = this.sessionManager.getSession(sessionId);
2117
2425
  if (!session) return { ok: false, error: "Session not found" };
2118
- if (session.status === "initializing") return { ok: false, error: "Session is still initializing" };
2119
- if (session.status !== "active") return { ok: false, error: `Session is ${session.status}` };
2426
+ if (session.status === "initializing")
2427
+ return { ok: false, error: "Session is still initializing" };
2428
+ if (session.status !== "active")
2429
+ return { ok: false, error: `Session is ${session.status}` };
2120
2430
  const adapter = this.adapters.get(session.channelId);
2121
2431
  if (!adapter) return { ok: false, error: "Adapter not found for session" };
2122
2432
  try {
2123
2433
  const result = await adapter.archiveSessionTopic(session.id);
2124
- if (!result) return { ok: false, error: "Adapter does not support archiving" };
2434
+ if (!result)
2435
+ return { ok: false, error: "Adapter does not support archiving" };
2125
2436
  return { ok: true, newThreadId: result.newThreadId };
2126
2437
  } catch (err) {
2127
2438
  return { ok: false, error: err.message };
@@ -2129,8 +2440,7 @@ var OpenACPCore = class {
2129
2440
  }
2130
2441
  // --- Message Routing ---
2131
2442
  async handleMessage(message) {
2132
- const config = this.configManager.get();
2133
- log6.debug(
2443
+ log7.debug(
2134
2444
  {
2135
2445
  channelId: message.channelId,
2136
2446
  threadId: message.threadId,
@@ -2138,32 +2448,17 @@ var OpenACPCore = class {
2138
2448
  },
2139
2449
  "Incoming message"
2140
2450
  );
2141
- if (config.security.allowedUserIds.length > 0) {
2142
- const userId = String(message.userId);
2143
- if (!config.security.allowedUserIds.includes(userId)) {
2144
- log6.warn(
2145
- { userId },
2146
- "Rejected message from unauthorized user"
2147
- );
2148
- return;
2149
- }
2150
- }
2151
- const activeSessions = this.sessionManager.listSessions().filter((s) => s.status === "active" || s.status === "initializing");
2152
- if (activeSessions.length >= config.security.maxConcurrentSessions) {
2153
- log6.warn(
2154
- {
2155
- userId: message.userId,
2156
- currentCount: activeSessions.length,
2157
- max: config.security.maxConcurrentSessions
2158
- },
2159
- "Session limit reached"
2160
- );
2161
- const adapter = this.adapters.get(message.channelId);
2162
- if (adapter) {
2163
- await adapter.sendMessage(message.threadId, {
2164
- type: "error",
2165
- text: `\u26A0\uFE0F Session limit reached (${config.security.maxConcurrentSessions}). Please cancel existing sessions with /cancel before starting new ones.`
2166
- });
2451
+ const access = this.securityGuard.checkAccess(message);
2452
+ if (!access.allowed) {
2453
+ log7.warn({ userId: message.userId, reason: access.reason }, "Access denied");
2454
+ if (access.reason.includes("Session limit")) {
2455
+ const adapter = this.adapters.get(message.channelId);
2456
+ if (adapter) {
2457
+ await adapter.sendMessage(message.threadId, {
2458
+ type: "error",
2459
+ text: `\u26A0\uFE0F ${access.reason}. Please cancel existing sessions with /cancel before starting new ones.`
2460
+ });
2461
+ }
2167
2462
  }
2168
2463
  return;
2169
2464
  }
@@ -2175,38 +2470,20 @@ var OpenACPCore = class {
2175
2470
  session = await this.lazyResume(message) ?? void 0;
2176
2471
  }
2177
2472
  if (!session) {
2178
- log6.warn(
2473
+ log7.warn(
2179
2474
  { channelId: message.channelId, threadId: message.threadId },
2180
2475
  "No session found for thread (in-memory miss + lazy resume returned null)"
2181
2476
  );
2182
2477
  return;
2183
2478
  }
2184
- this.sessionManager.patchRecord(session.id, { lastActiveAt: (/* @__PURE__ */ new Date()).toISOString() });
2479
+ this.sessionManager.patchRecord(session.id, {
2480
+ lastActiveAt: (/* @__PURE__ */ new Date()).toISOString()
2481
+ });
2185
2482
  await session.enqueuePrompt(message.text, message.attachments);
2186
2483
  }
2187
2484
  // --- Unified Session Creation Pipeline ---
2188
2485
  async createSession(params) {
2189
- const agentInstance = params.resumeAgentSessionId ? await this.agentManager.resume(
2190
- params.agentName,
2191
- params.workingDirectory,
2192
- params.resumeAgentSessionId
2193
- ) : await this.agentManager.spawn(
2194
- params.agentName,
2195
- params.workingDirectory
2196
- );
2197
- const session = new Session({
2198
- id: params.existingSessionId,
2199
- channelId: params.channelId,
2200
- agentName: params.agentName,
2201
- workingDirectory: params.workingDirectory,
2202
- agentInstance,
2203
- speechService: this.speechService
2204
- });
2205
- session.agentSessionId = agentInstance.sessionId;
2206
- if (params.initialName) {
2207
- session.name = params.initialName;
2208
- }
2209
- this.sessionManager.registerSession(session);
2486
+ const session = await this.sessionFactory.create(params);
2210
2487
  const adapter = this.adapters.get(params.channelId);
2211
2488
  if (params.createThread && adapter) {
2212
2489
  const threadId = await adapter.createSessionThread(
@@ -2219,47 +2496,11 @@ var OpenACPCore = class {
2219
2496
  const bridge = this.createBridge(session, adapter);
2220
2497
  bridge.connect();
2221
2498
  }
2222
- if (this.usageStore) {
2223
- session.on("agent_event", (event) => {
2224
- if (event.type !== "usage") return;
2225
- const record = {
2226
- id: nanoid2(),
2227
- sessionId: session.id,
2228
- agentName: session.agentName,
2229
- tokensUsed: event.tokensUsed ?? 0,
2230
- contextSize: event.contextSize ?? 0,
2231
- cost: event.cost,
2232
- timestamp: (/* @__PURE__ */ new Date()).toISOString()
2233
- };
2234
- this.usageStore.append(record);
2235
- if (this.usageBudget) {
2236
- const result = this.usageBudget.check();
2237
- if (result.message) {
2238
- this.notificationManager.notifyAll({
2239
- sessionId: session.id,
2240
- sessionName: session.name,
2241
- type: "budget_warning",
2242
- summary: result.message
2243
- });
2244
- }
2245
- }
2246
- });
2247
- }
2248
- session.on("status_change", (_from, to) => {
2249
- if ((to === "finished" || to === "cancelled") && this._tunnelService) {
2250
- this._tunnelService.stopBySession(session.id).then((stopped) => {
2251
- for (const entry of stopped) {
2252
- this.notificationManager.notifyAll({
2253
- sessionId: session.id,
2254
- sessionName: session.name,
2255
- type: "completed",
2256
- summary: `Tunnel stopped: port ${entry.port}${entry.label ? ` (${entry.label})` : ""} \u2014 session ended`
2257
- }).catch(() => {
2258
- });
2259
- }
2260
- }).catch(() => {
2261
- });
2262
- }
2499
+ this.sessionFactory.wireSideEffects(session, {
2500
+ usageStore: this.usageStore,
2501
+ usageBudget: this.usageBudget,
2502
+ notificationManager: this.notificationManager,
2503
+ tunnelService: this._tunnelService
2263
2504
  });
2264
2505
  const existingRecord = this.sessionStore?.get(session.id);
2265
2506
  const platform = {
@@ -2274,7 +2515,7 @@ var OpenACPCore = class {
2274
2515
  }
2275
2516
  await this.sessionManager.patchRecord(session.id, {
2276
2517
  sessionId: session.id,
2277
- agentSessionId: agentInstance.sessionId,
2518
+ agentSessionId: session.agentSessionId,
2278
2519
  agentName: params.agentName,
2279
2520
  workingDir: params.workingDirectory,
2280
2521
  channelId: params.channelId,
@@ -2284,7 +2525,7 @@ var OpenACPCore = class {
2284
2525
  name: session.name,
2285
2526
  platform
2286
2527
  });
2287
- log6.info(
2528
+ log7.info(
2288
2529
  { sessionId: session.id, agentName: params.agentName },
2289
2530
  "Session created via pipeline"
2290
2531
  );
@@ -2293,7 +2534,7 @@ var OpenACPCore = class {
2293
2534
  async handleNewSession(channelId, agentName, workspacePath) {
2294
2535
  const config = this.configManager.get();
2295
2536
  const resolvedAgent = agentName || config.defaultAgent;
2296
- log6.info({ channelId, agentName: resolvedAgent }, "New session request");
2537
+ log7.info({ channelId, agentName: resolvedAgent }, "New session request");
2297
2538
  const agentDef = this.agentCatalog.resolve(resolvedAgent);
2298
2539
  const resolvedWorkspace = this.configManager.resolveWorkspace(
2299
2540
  workspacePath || agentDef?.workingDirectory
@@ -2304,28 +2545,46 @@ var OpenACPCore = class {
2304
2545
  workingDirectory: resolvedWorkspace
2305
2546
  });
2306
2547
  }
2307
- async adoptSession(agentName, agentSessionId, cwd) {
2548
+ async adoptSession(agentName, agentSessionId, cwd, channelId) {
2308
2549
  const caps = getAgentCapabilities(agentName);
2309
2550
  if (!caps.supportsResume) {
2310
- return { ok: false, error: "agent_not_supported", message: `Agent '${agentName}' does not support session resume` };
2551
+ return {
2552
+ ok: false,
2553
+ error: "agent_not_supported",
2554
+ message: `Agent '${agentName}' does not support session resume`
2555
+ };
2311
2556
  }
2312
2557
  const agentDef = this.agentManager.getAgent(agentName);
2313
2558
  if (!agentDef) {
2314
- return { ok: false, error: "agent_not_supported", message: `Agent '${agentName}' not found` };
2559
+ return {
2560
+ ok: false,
2561
+ error: "agent_not_supported",
2562
+ message: `Agent '${agentName}' not found`
2563
+ };
2315
2564
  }
2316
- const { existsSync } = await import("fs");
2317
- if (!existsSync(cwd)) {
2318
- return { ok: false, error: "invalid_cwd", message: `Directory does not exist: ${cwd}` };
2565
+ const { existsSync: existsSync2 } = await import("fs");
2566
+ if (!existsSync2(cwd)) {
2567
+ return {
2568
+ ok: false,
2569
+ error: "invalid_cwd",
2570
+ message: `Directory does not exist: ${cwd}`
2571
+ };
2319
2572
  }
2320
2573
  const maxSessions = this.configManager.get().security.maxConcurrentSessions;
2321
2574
  if (this.sessionManager.listSessions().length >= maxSessions) {
2322
- return { ok: false, error: "session_limit", message: "Maximum concurrent sessions reached" };
2575
+ return {
2576
+ ok: false,
2577
+ error: "session_limit",
2578
+ message: "Maximum concurrent sessions reached"
2579
+ };
2323
2580
  }
2324
2581
  const existingRecord = this.sessionManager.getRecordByAgentSessionId(agentSessionId);
2325
2582
  if (existingRecord) {
2583
+ const sameChannel = !channelId || existingRecord.channelId === channelId;
2326
2584
  const platform = existingRecord.platform;
2327
- if (platform?.topicId) {
2328
- const adapter = this.adapters.values().next().value;
2585
+ const existingThreadId = platform?.topicId ? String(platform.topicId) : platform?.threadId;
2586
+ if (existingThreadId && sameChannel) {
2587
+ const adapter = this.adapters.get(existingRecord.channelId) ?? this.adapters.values().next().value;
2329
2588
  if (adapter) {
2330
2589
  try {
2331
2590
  await adapter.sendMessage(existingRecord.sessionId, {
@@ -2338,16 +2597,25 @@ var OpenACPCore = class {
2338
2597
  return {
2339
2598
  ok: true,
2340
2599
  sessionId: existingRecord.sessionId,
2341
- threadId: String(platform.topicId),
2600
+ threadId: existingThreadId,
2342
2601
  status: "existing"
2343
2602
  };
2344
2603
  }
2345
2604
  }
2346
- const firstEntry = this.adapters.entries().next().value;
2347
- if (!firstEntry) {
2348
- return { ok: false, error: "no_adapter", message: "No channel adapter registered" };
2605
+ let adapterChannelId;
2606
+ if (channelId) {
2607
+ if (!this.adapters.has(channelId)) {
2608
+ const available = Array.from(this.adapters.keys()).join(", ") || "none";
2609
+ return { ok: false, error: "adapter_not_found", message: `Adapter '${channelId}' is not connected. Available: ${available}` };
2610
+ }
2611
+ adapterChannelId = channelId;
2612
+ } else {
2613
+ const firstEntry = this.adapters.entries().next().value;
2614
+ if (!firstEntry) {
2615
+ return { ok: false, error: "no_adapter", message: "No channel adapter registered" };
2616
+ }
2617
+ adapterChannelId = firstEntry[0];
2349
2618
  }
2350
- const [adapterChannelId] = firstEntry;
2351
2619
  let session;
2352
2620
  try {
2353
2621
  session = await this.createSession({
@@ -2365,9 +2633,15 @@ var OpenACPCore = class {
2365
2633
  message: `Failed to resume session: ${err instanceof Error ? err.message : String(err)}`
2366
2634
  };
2367
2635
  }
2636
+ const adoptPlatform = {};
2637
+ if (adapterChannelId === "telegram") {
2638
+ adoptPlatform.topicId = Number(session.threadId);
2639
+ } else {
2640
+ adoptPlatform.threadId = session.threadId;
2641
+ }
2368
2642
  await this.sessionManager.patchRecord(session.id, {
2369
2643
  originalAgentSessionId: agentSessionId,
2370
- platform: { topicId: Number(session.threadId) }
2644
+ platform: adoptPlatform
2371
2645
  });
2372
2646
  return {
2373
2647
  ok: true,
@@ -2388,8 +2662,12 @@ var OpenACPCore = class {
2388
2662
  currentSession.workingDirectory
2389
2663
  );
2390
2664
  }
2391
- const record = this.sessionManager.getRecordByThread(channelId, currentThreadId);
2392
- if (!record || record.status === "cancelled" || record.status === "error") return null;
2665
+ const record = this.sessionManager.getRecordByThread(
2666
+ channelId,
2667
+ currentThreadId
2668
+ );
2669
+ if (!record || record.status === "cancelled" || record.status === "error")
2670
+ return null;
2393
2671
  return this.handleNewSession(
2394
2672
  channelId,
2395
2673
  record.agentName,
@@ -2397,6 +2675,15 @@ var OpenACPCore = class {
2397
2675
  );
2398
2676
  }
2399
2677
  // --- Lazy Resume ---
2678
+ /**
2679
+ * Get active session by thread, or attempt lazy resume from store.
2680
+ * Used by adapter command handlers that need a session but don't go through handleMessage().
2681
+ */
2682
+ async getOrResumeSession(channelId, threadId) {
2683
+ const session = this.sessionManager.getSessionByThread(channelId, threadId);
2684
+ if (session) return session;
2685
+ return this.lazyResume({ channelId, threadId, userId: "", text: "" });
2686
+ }
2400
2687
  async lazyResume(message) {
2401
2688
  const store = this.sessionStore;
2402
2689
  if (!store) return null;
@@ -2408,21 +2695,29 @@ var OpenACPCore = class {
2408
2695
  (p) => String(p.topicId) === message.threadId
2409
2696
  );
2410
2697
  if (!record) {
2411
- log6.debug(
2698
+ log7.debug(
2412
2699
  { threadId: message.threadId, channelId: message.channelId },
2413
2700
  "No session record found for thread"
2414
2701
  );
2415
2702
  return null;
2416
2703
  }
2417
2704
  if (record.status === "error") {
2418
- log6.debug(
2419
- { threadId: message.threadId, sessionId: record.sessionId, status: record.status },
2705
+ log7.debug(
2706
+ {
2707
+ threadId: message.threadId,
2708
+ sessionId: record.sessionId,
2709
+ status: record.status
2710
+ },
2420
2711
  "Skipping resume of error session"
2421
2712
  );
2422
2713
  return null;
2423
2714
  }
2424
- log6.info(
2425
- { threadId: message.threadId, sessionId: record.sessionId, status: record.status },
2715
+ log7.info(
2716
+ {
2717
+ threadId: message.threadId,
2718
+ sessionId: record.sessionId,
2719
+ status: record.status
2720
+ },
2426
2721
  "Lazy resume: found record, attempting resume"
2427
2722
  );
2428
2723
  const resumePromise = (async () => {
@@ -2438,13 +2733,13 @@ var OpenACPCore = class {
2438
2733
  session.threadId = message.threadId;
2439
2734
  session.activate();
2440
2735
  session.dangerousMode = record.dangerousMode ?? false;
2441
- log6.info(
2736
+ log7.info(
2442
2737
  { sessionId: session.id, threadId: message.threadId },
2443
2738
  "Lazy resume successful"
2444
2739
  );
2445
2740
  return session;
2446
2741
  } catch (err) {
2447
- log6.error({ err, record }, "Lazy resume failed");
2742
+ log7.error({ err, record }, "Lazy resume failed");
2448
2743
  const adapter = this.adapters.get(message.channelId);
2449
2744
  if (adapter) {
2450
2745
  try {
@@ -2470,213 +2765,327 @@ var OpenACPCore = class {
2470
2765
  messageTransformer: this.messageTransformer,
2471
2766
  notificationManager: this.notificationManager,
2472
2767
  sessionManager: this.sessionManager,
2768
+ eventBus: this.eventBus,
2473
2769
  fileService: this.fileService
2474
2770
  });
2475
2771
  }
2476
2772
  };
2477
2773
 
2478
- // src/core/api-server.ts
2479
- import * as http from "http";
2480
- import * as fs6 from "fs";
2481
- import * as path6 from "path";
2482
- import * as os2 from "os";
2483
- import * as crypto from "crypto";
2484
- import { fileURLToPath } from "url";
2485
- var log7 = createChildLogger({ module: "api-server" });
2486
- var DEFAULT_PORT_FILE = path6.join(os2.homedir(), ".openacp", "api.port");
2487
- var cachedVersion;
2488
- function getVersion() {
2489
- if (cachedVersion) return cachedVersion;
2490
- try {
2491
- const __filename = fileURLToPath(import.meta.url);
2492
- const pkgPath = path6.resolve(path6.dirname(__filename), "../../package.json");
2493
- const pkg = JSON.parse(fs6.readFileSync(pkgPath, "utf-8"));
2494
- cachedVersion = pkg.version ?? "0.0.0-dev";
2495
- } catch {
2496
- cachedVersion = "0.0.0-dev";
2774
+ // src/core/sse-manager.ts
2775
+ var SSEManager = class {
2776
+ constructor(eventBus, getSessionStats, startedAt) {
2777
+ this.eventBus = eventBus;
2778
+ this.getSessionStats = getSessionStats;
2779
+ this.startedAt = startedAt;
2780
+ }
2781
+ sseConnections = /* @__PURE__ */ new Set();
2782
+ sseCleanupHandlers = /* @__PURE__ */ new Map();
2783
+ healthInterval;
2784
+ boundHandlers = [];
2785
+ setup() {
2786
+ if (!this.eventBus) return;
2787
+ const events = [
2788
+ "session:created",
2789
+ "session:updated",
2790
+ "session:deleted",
2791
+ "agent:event",
2792
+ "permission:request"
2793
+ ];
2794
+ for (const eventName of events) {
2795
+ const handler = (data) => {
2796
+ this.broadcast(eventName, data);
2797
+ };
2798
+ this.eventBus.on(eventName, handler);
2799
+ this.boundHandlers.push({ event: eventName, handler });
2800
+ }
2801
+ this.healthInterval = setInterval(() => {
2802
+ const mem = process.memoryUsage();
2803
+ const stats = this.getSessionStats();
2804
+ this.broadcast("health", {
2805
+ uptime: Date.now() - this.startedAt,
2806
+ memory: {
2807
+ rss: mem.rss,
2808
+ heapUsed: mem.heapUsed,
2809
+ heapTotal: mem.heapTotal
2810
+ },
2811
+ sessions: stats
2812
+ });
2813
+ }, 3e4);
2814
+ }
2815
+ handleRequest(req, res) {
2816
+ const parsedUrl = new URL(req.url || "", "http://localhost");
2817
+ const sessionFilter = parsedUrl.searchParams.get("sessionId");
2818
+ res.writeHead(200, {
2819
+ "Content-Type": "text/event-stream",
2820
+ "Cache-Control": "no-cache",
2821
+ Connection: "keep-alive"
2822
+ });
2823
+ res.flushHeaders();
2824
+ res.sessionFilter = sessionFilter ?? void 0;
2825
+ this.sseConnections.add(res);
2826
+ const cleanup = () => {
2827
+ this.sseConnections.delete(res);
2828
+ this.sseCleanupHandlers.delete(res);
2829
+ };
2830
+ this.sseCleanupHandlers.set(res, cleanup);
2831
+ req.on("close", cleanup);
2497
2832
  }
2498
- return cachedVersion;
2499
- }
2500
- var SENSITIVE_KEYS = ["botToken", "token", "apiKey", "secret", "password", "webhookSecret"];
2501
- function redactConfig(config) {
2502
- const redacted = structuredClone(config);
2503
- redactDeep(redacted);
2504
- return redacted;
2505
- }
2506
- function redactDeep(obj) {
2507
- for (const [key, value] of Object.entries(obj)) {
2508
- if (SENSITIVE_KEYS.includes(key) && typeof value === "string") {
2509
- obj[key] = "***";
2510
- } else if (value && typeof value === "object" && !Array.isArray(value)) {
2511
- redactDeep(value);
2833
+ broadcast(event, data) {
2834
+ const payload = `event: ${event}
2835
+ data: ${JSON.stringify(data)}
2836
+
2837
+ `;
2838
+ const sessionEvents = [
2839
+ "agent:event",
2840
+ "permission:request",
2841
+ "session:updated"
2842
+ ];
2843
+ for (const res of this.sseConnections) {
2844
+ const filter = res.sessionFilter;
2845
+ if (filter && sessionEvents.includes(event)) {
2846
+ const eventData = data;
2847
+ if (eventData.sessionId !== filter) continue;
2848
+ }
2849
+ try {
2850
+ if (res.writable) res.write(payload);
2851
+ } catch {
2852
+ }
2512
2853
  }
2513
2854
  }
2514
- }
2515
- var ApiServer = class {
2516
- constructor(core, config, portFilePath, topicManager, secretFilePath) {
2517
- this.core = core;
2518
- this.config = config;
2519
- this.topicManager = topicManager;
2520
- this.portFilePath = portFilePath ?? DEFAULT_PORT_FILE;
2521
- this.secretFilePath = secretFilePath ?? path6.join(os2.homedir(), ".openacp", "api-secret");
2855
+ stop() {
2856
+ if (this.healthInterval) clearInterval(this.healthInterval);
2857
+ if (this.eventBus) {
2858
+ for (const { event, handler } of this.boundHandlers) {
2859
+ this.eventBus.off(event, handler);
2860
+ }
2861
+ }
2862
+ this.boundHandlers = [];
2863
+ const entries = [...this.sseCleanupHandlers];
2864
+ for (const [res, cleanup] of entries) {
2865
+ res.end();
2866
+ cleanup();
2867
+ }
2522
2868
  }
2523
- server = null;
2524
- actualPort = 0;
2525
- portFilePath;
2526
- startedAt = Date.now();
2527
- secret = "";
2528
- secretFilePath;
2529
- async start() {
2530
- this.loadOrCreateSecret();
2531
- this.server = http.createServer((req, res) => this.handleRequest(req, res));
2532
- await new Promise((resolve2, reject) => {
2533
- this.server.on("error", (err) => {
2534
- if (err.code === "EADDRINUSE") {
2535
- log7.warn({ port: this.config.port }, "API port in use, continuing without API server");
2536
- this.server = null;
2537
- resolve2();
2538
- } else {
2539
- reject(err);
2540
- }
2541
- });
2542
- this.server.listen(this.config.port, this.config.host, () => {
2543
- const addr = this.server.address();
2544
- if (addr && typeof addr === "object") {
2545
- this.actualPort = addr.port;
2869
+ };
2870
+
2871
+ // src/core/static-server.ts
2872
+ import * as fs6 from "fs";
2873
+ import * as path6 from "path";
2874
+ import { fileURLToPath } from "url";
2875
+ var MIME_TYPES = {
2876
+ ".html": "text/html; charset=utf-8",
2877
+ ".js": "application/javascript; charset=utf-8",
2878
+ ".css": "text/css; charset=utf-8",
2879
+ ".json": "application/json; charset=utf-8",
2880
+ ".png": "image/png",
2881
+ ".jpg": "image/jpeg",
2882
+ ".svg": "image/svg+xml",
2883
+ ".ico": "image/x-icon",
2884
+ ".woff": "font/woff",
2885
+ ".woff2": "font/woff2"
2886
+ };
2887
+ var StaticServer = class {
2888
+ uiDir;
2889
+ constructor(uiDir) {
2890
+ this.uiDir = uiDir;
2891
+ if (!this.uiDir) {
2892
+ const __filename = fileURLToPath(import.meta.url);
2893
+ const candidate = path6.resolve(path6.dirname(__filename), "../../ui/dist");
2894
+ if (fs6.existsSync(path6.join(candidate, "index.html"))) {
2895
+ this.uiDir = candidate;
2896
+ }
2897
+ if (!this.uiDir) {
2898
+ const publishCandidate = path6.resolve(
2899
+ path6.dirname(__filename),
2900
+ "../ui"
2901
+ );
2902
+ if (fs6.existsSync(path6.join(publishCandidate, "index.html"))) {
2903
+ this.uiDir = publishCandidate;
2546
2904
  }
2547
- this.writePortFile();
2548
- log7.info({ host: this.config.host, port: this.actualPort }, "API server listening");
2549
- resolve2();
2550
- });
2551
- });
2905
+ }
2906
+ }
2552
2907
  }
2553
- async stop() {
2554
- this.removePortFile();
2555
- if (this.server) {
2556
- await new Promise((resolve2) => {
2557
- this.server.close(() => resolve2());
2908
+ isAvailable() {
2909
+ return this.uiDir !== void 0;
2910
+ }
2911
+ serve(req, res) {
2912
+ if (!this.uiDir) return false;
2913
+ const urlPath = (req.url || "/").split("?")[0];
2914
+ const safePath = path6.normalize(urlPath);
2915
+ const filePath = path6.join(this.uiDir, safePath);
2916
+ if (!filePath.startsWith(this.uiDir + path6.sep) && filePath !== this.uiDir)
2917
+ return false;
2918
+ if (fs6.existsSync(filePath) && fs6.statSync(filePath).isFile()) {
2919
+ const ext = path6.extname(filePath);
2920
+ const contentType = MIME_TYPES[ext] ?? "application/octet-stream";
2921
+ const isHashed = /\.[a-zA-Z0-9]{8,}\.(js|css)$/.test(filePath);
2922
+ const cacheControl = isHashed ? "public, max-age=31536000, immutable" : "no-cache";
2923
+ res.writeHead(200, {
2924
+ "Content-Type": contentType,
2925
+ "Cache-Control": cacheControl
2558
2926
  });
2559
- this.server = null;
2927
+ fs6.createReadStream(filePath).pipe(res);
2928
+ return true;
2929
+ }
2930
+ const indexPath = path6.join(this.uiDir, "index.html");
2931
+ if (fs6.existsSync(indexPath)) {
2932
+ res.writeHead(200, {
2933
+ "Content-Type": "text/html; charset=utf-8",
2934
+ "Cache-Control": "no-cache"
2935
+ });
2936
+ fs6.createReadStream(indexPath).pipe(res);
2937
+ return true;
2560
2938
  }
2939
+ return false;
2561
2940
  }
2562
- getPort() {
2563
- return this.actualPort;
2941
+ };
2942
+
2943
+ // src/core/api/index.ts
2944
+ import * as http from "http";
2945
+ import * as fs7 from "fs";
2946
+ import * as path7 from "path";
2947
+ import * as os2 from "os";
2948
+ import * as crypto from "crypto";
2949
+ import { fileURLToPath as fileURLToPath2 } from "url";
2950
+
2951
+ // src/core/api/router.ts
2952
+ var Router = class {
2953
+ routes = [];
2954
+ get(path8, handler) {
2955
+ this.add("GET", path8, handler);
2564
2956
  }
2565
- writePortFile() {
2566
- const dir = path6.dirname(this.portFilePath);
2567
- fs6.mkdirSync(dir, { recursive: true });
2568
- fs6.writeFileSync(this.portFilePath, String(this.actualPort));
2957
+ post(path8, handler) {
2958
+ this.add("POST", path8, handler);
2569
2959
  }
2570
- removePortFile() {
2571
- try {
2572
- fs6.unlinkSync(this.portFilePath);
2573
- } catch {
2574
- }
2960
+ put(path8, handler) {
2961
+ this.add("PUT", path8, handler);
2575
2962
  }
2576
- loadOrCreateSecret() {
2577
- const dir = path6.dirname(this.secretFilePath);
2578
- fs6.mkdirSync(dir, { recursive: true });
2579
- try {
2580
- this.secret = fs6.readFileSync(this.secretFilePath, "utf-8").trim();
2581
- if (this.secret) {
2582
- try {
2583
- const stat = fs6.statSync(this.secretFilePath);
2584
- const mode = stat.mode & 511;
2585
- if (mode & 63) {
2586
- log7.warn({ path: this.secretFilePath, mode: "0" + mode.toString(8) }, "API secret file has insecure permissions (should be 0600). Run: chmod 600 %s", this.secretFilePath);
2587
- }
2588
- } catch {
2589
- }
2590
- return;
2963
+ patch(path8, handler) {
2964
+ this.add("PATCH", path8, handler);
2965
+ }
2966
+ delete(path8, handler) {
2967
+ this.add("DELETE", path8, handler);
2968
+ }
2969
+ match(method, url) {
2970
+ const pathname = url.split("?")[0];
2971
+ for (const route of this.routes) {
2972
+ if (route.method !== method) continue;
2973
+ const m = pathname.match(route.pattern);
2974
+ if (!m) continue;
2975
+ const params = {};
2976
+ for (let i = 0; i < route.keys.length; i++) {
2977
+ params[route.keys[i]] = m[i + 1];
2591
2978
  }
2592
- } catch {
2979
+ return { handler: route.handler, params };
2593
2980
  }
2594
- this.secret = crypto.randomBytes(32).toString("hex");
2595
- fs6.writeFileSync(this.secretFilePath, this.secret, { mode: 384 });
2981
+ return null;
2596
2982
  }
2597
- authenticate(req) {
2598
- const authHeader = req.headers.authorization;
2599
- if (!authHeader?.startsWith("Bearer ")) return false;
2600
- const token = authHeader.slice(7);
2601
- if (token.length !== this.secret.length) return false;
2602
- return crypto.timingSafeEqual(Buffer.from(token, "utf-8"), Buffer.from(this.secret, "utf-8"));
2983
+ add(method, path8, handler) {
2984
+ const keys = [];
2985
+ const pattern = path8.replace(/:(\w+)/g, (_, key) => {
2986
+ keys.push(key);
2987
+ return "([^/]+)";
2988
+ });
2989
+ this.routes.push({
2990
+ method,
2991
+ pattern: new RegExp(`^${pattern}$`),
2992
+ keys,
2993
+ handler
2994
+ });
2603
2995
  }
2604
- async handleRequest(req, res) {
2605
- const method = req.method?.toUpperCase();
2606
- const url = req.url || "";
2607
- const isExempt = method === "GET" && (url === "/api/health" || url === "/api/version");
2608
- if (!isExempt && !this.authenticate(req)) {
2609
- this.sendJson(res, 401, { error: "Unauthorized" });
2996
+ };
2997
+
2998
+ // src/core/api/routes/health.ts
2999
+ function registerHealthRoutes(router, deps) {
3000
+ router.get("/api/health", async (_req, res) => {
3001
+ const activeSessions = deps.core.sessionManager.listSessions();
3002
+ const allRecords = deps.core.sessionManager.listRecords();
3003
+ const mem = process.memoryUsage();
3004
+ const tunnel = deps.core.tunnelService;
3005
+ deps.sendJson(res, 200, {
3006
+ status: "ok",
3007
+ uptime: Date.now() - deps.startedAt,
3008
+ version: deps.getVersion(),
3009
+ memory: {
3010
+ rss: mem.rss,
3011
+ heapUsed: mem.heapUsed,
3012
+ heapTotal: mem.heapTotal
3013
+ },
3014
+ sessions: {
3015
+ active: activeSessions.filter(
3016
+ (s) => s.status === "active" || s.status === "initializing"
3017
+ ).length,
3018
+ total: allRecords.length
3019
+ },
3020
+ adapters: Array.from(deps.core.adapters.keys()),
3021
+ tunnel: tunnel ? { enabled: true, url: tunnel.getPublicUrl() } : { enabled: false }
3022
+ });
3023
+ });
3024
+ router.get("/api/version", async (_req, res) => {
3025
+ deps.sendJson(res, 200, { version: deps.getVersion() });
3026
+ });
3027
+ router.post("/api/restart", async (_req, res) => {
3028
+ if (!deps.core.requestRestart) {
3029
+ deps.sendJson(res, 501, { error: "Restart not available" });
2610
3030
  return;
2611
3031
  }
3032
+ deps.sendJson(res, 200, { ok: true, message: "Restarting..." });
3033
+ setImmediate(() => deps.core.requestRestart());
3034
+ });
3035
+ router.get("/api/adapters", async (_req, res) => {
3036
+ const adapters = Array.from(deps.core.adapters.entries()).map(([name]) => ({
3037
+ name,
3038
+ type: "built-in"
3039
+ }));
3040
+ deps.sendJson(res, 200, { adapters });
3041
+ });
3042
+ }
3043
+
3044
+ // src/core/api/routes/sessions.ts
3045
+ var log8 = createChildLogger({ module: "api-server" });
3046
+ function registerSessionRoutes(router, deps) {
3047
+ router.post("/api/sessions/adopt", async (req, res) => {
3048
+ const body = await deps.readBody(req);
3049
+ if (body === null) {
3050
+ return deps.sendJson(res, 413, { error: "Request body too large" });
3051
+ }
3052
+ if (!body) {
3053
+ return deps.sendJson(res, 400, {
3054
+ error: "bad_request",
3055
+ message: "Empty request body"
3056
+ });
3057
+ }
3058
+ let parsed;
2612
3059
  try {
2613
- if (method === "POST" && url === "/api/sessions/adopt") {
2614
- await this.handleAdoptSession(req, res);
2615
- } else if (method === "POST" && url === "/api/sessions") {
2616
- await this.handleCreateSession(req, res);
2617
- } else if (method === "POST" && url.match(/^\/api\/sessions\/([^/]+)\/prompt$/)) {
2618
- const sessionId = decodeURIComponent(url.match(/^\/api\/sessions\/([^/]+)\/prompt$/)[1]);
2619
- await this.handleSendPrompt(sessionId, req, res);
2620
- } else if (method === "PATCH" && url.match(/^\/api\/sessions\/([^/]+)\/dangerous$/)) {
2621
- const sessionId = decodeURIComponent(url.match(/^\/api\/sessions\/([^/]+)\/dangerous$/)[1]);
2622
- await this.handleToggleDangerous(sessionId, req, res);
2623
- } else if (method === "GET" && url.match(/^\/api\/sessions\/([^/]+)$/)) {
2624
- const sessionId = decodeURIComponent(url.match(/^\/api\/sessions\/([^/]+)$/)[1]);
2625
- await this.handleGetSession(sessionId, res);
2626
- } else if (method === "POST" && url.match(/^\/api\/sessions\/([^/]+)\/archive$/)) {
2627
- const sessionId = decodeURIComponent(url.match(/^\/api\/sessions\/([^/]+)\/archive$/)[1]);
2628
- await this.handleArchiveSession(sessionId, res);
2629
- } else if (method === "DELETE" && url.match(/^\/api\/sessions\/([^/]+)$/)) {
2630
- const sessionId = decodeURIComponent(url.match(/^\/api\/sessions\/([^/]+)$/)[1]);
2631
- await this.handleCancelSession(sessionId, res);
2632
- } else if (method === "GET" && url === "/api/sessions") {
2633
- await this.handleListSessions(res);
2634
- } else if (method === "GET" && url === "/api/agents") {
2635
- await this.handleListAgents(res);
2636
- } else if (method === "GET" && url === "/api/health") {
2637
- await this.handleHealth(res);
2638
- } else if (method === "GET" && url === "/api/version") {
2639
- await this.handleVersion(res);
2640
- } else if (method === "GET" && url === "/api/config/editable") {
2641
- await this.handleGetEditableConfig(res);
2642
- } else if (method === "GET" && url === "/api/config") {
2643
- await this.handleGetConfig(res);
2644
- } else if (method === "PATCH" && url === "/api/config") {
2645
- await this.handleUpdateConfig(req, res);
2646
- } else if (method === "GET" && url === "/api/adapters") {
2647
- await this.handleListAdapters(res);
2648
- } else if (method === "GET" && url === "/api/tunnel") {
2649
- await this.handleTunnelStatus(res);
2650
- } else if (method === "GET" && url === "/api/tunnel/list") {
2651
- await this.handleTunnelList(res);
2652
- } else if (method === "POST" && url === "/api/tunnel") {
2653
- await this.handleTunnelAdd(req, res);
2654
- } else if (method === "DELETE" && url === "/api/tunnel") {
2655
- await this.handleTunnelStopAll(res);
2656
- } else if (method === "DELETE" && url.match(/^\/api\/tunnel\/(\d+)$/)) {
2657
- const port = parseInt(url.match(/^\/api\/tunnel\/(\d+)$/)[1], 10);
2658
- await this.handleTunnelStop(port, res);
2659
- } else if (method === "POST" && url === "/api/notify") {
2660
- await this.handleNotify(req, res);
2661
- } else if (method === "POST" && url === "/api/restart") {
2662
- await this.handleRestart(res);
2663
- } else if (method === "GET" && url.match(/^\/api\/topics(\?.*)?$/)) {
2664
- await this.handleListTopics(url, res);
2665
- } else if (method === "POST" && url === "/api/topics/cleanup") {
2666
- await this.handleCleanupTopics(req, res);
2667
- } else if (method === "DELETE" && url.match(/^\/api\/topics\/([^/?]+)/)) {
2668
- const match = url.match(/^\/api\/topics\/([^/?]+)/);
2669
- await this.handleDeleteTopic(decodeURIComponent(match[1]), url, res);
2670
- } else {
2671
- this.sendJson(res, 404, { error: "Not found" });
2672
- }
2673
- } catch (err) {
2674
- log7.error({ err }, "API request error");
2675
- this.sendJson(res, 500, { error: "Internal server error" });
3060
+ parsed = JSON.parse(body);
3061
+ } catch {
3062
+ return deps.sendJson(res, 400, {
3063
+ error: "bad_request",
3064
+ message: "Invalid JSON"
3065
+ });
2676
3066
  }
2677
- }
2678
- async handleCreateSession(req, res) {
2679
- const body = await this.readBody(req);
3067
+ const { agent, agentSessionId, cwd, channel } = parsed;
3068
+ if (!agent || !agentSessionId) {
3069
+ return deps.sendJson(res, 400, {
3070
+ error: "bad_request",
3071
+ message: "Missing required fields: agent, agentSessionId"
3072
+ });
3073
+ }
3074
+ const result = await deps.core.adoptSession(
3075
+ agent,
3076
+ agentSessionId,
3077
+ cwd ?? process.cwd(),
3078
+ channel
3079
+ );
3080
+ if (result.ok) {
3081
+ return deps.sendJson(res, 200, result);
3082
+ } else {
3083
+ const status = result.error === "session_limit" ? 429 : result.error === "agent_not_supported" ? 400 : 500;
3084
+ return deps.sendJson(res, status, result);
3085
+ }
3086
+ });
3087
+ router.post("/api/sessions", async (req, res) => {
3088
+ const body = await deps.readBody(req);
2680
3089
  let agent;
2681
3090
  let workspace;
2682
3091
  if (body) {
@@ -2685,25 +3094,28 @@ var ApiServer = class {
2685
3094
  agent = parsed.agent;
2686
3095
  workspace = parsed.workspace;
2687
3096
  } catch {
2688
- this.sendJson(res, 400, { error: "Invalid JSON body" });
3097
+ deps.sendJson(res, 400, { error: "Invalid JSON body" });
2689
3098
  return;
2690
3099
  }
2691
3100
  }
2692
- const config = this.core.configManager.get();
2693
- const activeSessions = this.core.sessionManager.listSessions().filter((s) => s.status === "active" || s.status === "initializing");
3101
+ const config = deps.core.configManager.get();
3102
+ const activeSessions = deps.core.sessionManager.listSessions().filter((s) => s.status === "active" || s.status === "initializing");
2694
3103
  if (activeSessions.length >= config.security.maxConcurrentSessions) {
2695
- this.sendJson(res, 429, {
3104
+ deps.sendJson(res, 429, {
2696
3105
  error: `Max concurrent sessions (${config.security.maxConcurrentSessions}) reached. Cancel a session first.`
2697
3106
  });
2698
3107
  return;
2699
3108
  }
2700
- const [adapterId, adapter] = this.core.adapters.entries().next().value ?? [null, null];
3109
+ const [adapterId, adapter] = deps.core.adapters.entries().next().value ?? [
3110
+ null,
3111
+ null
3112
+ ];
2701
3113
  const channelId = adapterId ?? "api";
2702
3114
  const resolvedAgent = agent || config.defaultAgent;
2703
- const resolvedWorkspace = this.core.configManager.resolveWorkspace(
3115
+ const resolvedWorkspace = deps.core.configManager.resolveWorkspace(
2704
3116
  workspace || config.agents[resolvedAgent]?.workingDirectory
2705
3117
  );
2706
- const session = await this.core.createSession({
3118
+ const session = await deps.core.createSession({
2707
3119
  channelId,
2708
3120
  agentName: resolvedAgent,
2709
3121
  workingDirectory: resolvedWorkspace,
@@ -2713,54 +3125,138 @@ var ApiServer = class {
2713
3125
  if (!adapter) {
2714
3126
  session.agentInstance.onPermissionRequest = async (request) => {
2715
3127
  const allowOption = request.options.find((o) => o.isAllow);
2716
- log7.debug({ sessionId: session.id, permissionId: request.id, option: allowOption?.id }, "Auto-approving permission for API session");
3128
+ log8.debug(
3129
+ {
3130
+ sessionId: session.id,
3131
+ permissionId: request.id,
3132
+ option: allowOption?.id
3133
+ },
3134
+ "Auto-approving permission for API session"
3135
+ );
2717
3136
  return allowOption?.id ?? request.options[0]?.id ?? "";
2718
3137
  };
2719
3138
  }
2720
- session.warmup().catch((err) => log7.warn({ err, sessionId: session.id }, "API session warmup failed"));
2721
- this.sendJson(res, 200, {
3139
+ session.warmup().catch(
3140
+ (err) => log8.warn({ err, sessionId: session.id }, "API session warmup failed")
3141
+ );
3142
+ deps.sendJson(res, 200, {
2722
3143
  sessionId: session.id,
2723
3144
  agent: session.agentName,
2724
3145
  status: session.status,
2725
3146
  workspace: session.workingDirectory
2726
3147
  });
2727
- }
2728
- async handleSendPrompt(sessionId, req, res) {
2729
- const session = this.core.sessionManager.getSession(sessionId);
3148
+ });
3149
+ router.post("/api/sessions/:sessionId/prompt", async (req, res, params) => {
3150
+ const sessionId = decodeURIComponent(params.sessionId);
3151
+ const session = deps.core.sessionManager.getSession(sessionId);
2730
3152
  if (!session) {
2731
- this.sendJson(res, 404, { error: `Session "${sessionId}" not found` });
3153
+ deps.sendJson(res, 404, { error: `Session "${sessionId}" not found` });
2732
3154
  return;
2733
3155
  }
2734
3156
  if (session.status === "cancelled" || session.status === "finished" || session.status === "error") {
2735
- this.sendJson(res, 400, { error: `Session is ${session.status}` });
3157
+ deps.sendJson(res, 400, { error: `Session is ${session.status}` });
2736
3158
  return;
2737
3159
  }
2738
- const body = await this.readBody(req);
3160
+ const body = await deps.readBody(req);
2739
3161
  let prompt;
2740
3162
  if (body) {
2741
3163
  try {
2742
3164
  const parsed = JSON.parse(body);
2743
3165
  prompt = parsed.prompt;
2744
3166
  } catch {
2745
- this.sendJson(res, 400, { error: "Invalid JSON body" });
3167
+ deps.sendJson(res, 400, { error: "Invalid JSON body" });
2746
3168
  return;
2747
3169
  }
2748
3170
  }
2749
3171
  if (!prompt) {
2750
- this.sendJson(res, 400, { error: "Missing prompt" });
3172
+ deps.sendJson(res, 400, { error: "Missing prompt" });
2751
3173
  return;
2752
3174
  }
2753
3175
  session.enqueuePrompt(prompt).catch(() => {
2754
3176
  });
2755
- this.sendJson(res, 200, { ok: true, sessionId, queueDepth: session.queueDepth });
2756
- }
2757
- async handleGetSession(sessionId, res) {
2758
- const session = this.core.sessionManager.getSession(sessionId);
3177
+ deps.sendJson(res, 200, {
3178
+ ok: true,
3179
+ sessionId,
3180
+ queueDepth: session.queueDepth
3181
+ });
3182
+ });
3183
+ router.post(
3184
+ "/api/sessions/:sessionId/permission",
3185
+ async (req, res, params) => {
3186
+ const sessionId = decodeURIComponent(params.sessionId);
3187
+ const session = deps.core.sessionManager.getSession(sessionId);
3188
+ if (!session) {
3189
+ deps.sendJson(res, 404, { error: `Session "${sessionId}" not found` });
3190
+ return;
3191
+ }
3192
+ const body = await deps.readBody(req);
3193
+ let permissionId;
3194
+ let optionId;
3195
+ if (body) {
3196
+ try {
3197
+ const parsed = JSON.parse(body);
3198
+ permissionId = parsed.permissionId;
3199
+ optionId = parsed.optionId;
3200
+ } catch {
3201
+ deps.sendJson(res, 400, { error: "Invalid JSON body" });
3202
+ return;
3203
+ }
3204
+ }
3205
+ if (!permissionId || !optionId) {
3206
+ deps.sendJson(res, 400, {
3207
+ error: "Missing permissionId or optionId"
3208
+ });
3209
+ return;
3210
+ }
3211
+ if (!session.permissionGate.isPending || session.permissionGate.requestId !== permissionId) {
3212
+ deps.sendJson(res, 400, {
3213
+ error: "No matching pending permission request"
3214
+ });
3215
+ return;
3216
+ }
3217
+ session.permissionGate.resolve(optionId);
3218
+ deps.sendJson(res, 200, { ok: true });
3219
+ }
3220
+ );
3221
+ router.patch(
3222
+ "/api/sessions/:sessionId/dangerous",
3223
+ async (req, res, params) => {
3224
+ const sessionId = decodeURIComponent(params.sessionId);
3225
+ const session = deps.core.sessionManager.getSession(sessionId);
3226
+ if (!session) {
3227
+ deps.sendJson(res, 404, { error: `Session "${sessionId}" not found` });
3228
+ return;
3229
+ }
3230
+ const body = await deps.readBody(req);
3231
+ let enabled;
3232
+ if (body) {
3233
+ try {
3234
+ const parsed = JSON.parse(body);
3235
+ enabled = parsed.enabled;
3236
+ } catch {
3237
+ deps.sendJson(res, 400, { error: "Invalid JSON body" });
3238
+ return;
3239
+ }
3240
+ }
3241
+ if (typeof enabled !== "boolean") {
3242
+ deps.sendJson(res, 400, { error: "Missing enabled boolean" });
3243
+ return;
3244
+ }
3245
+ session.dangerousMode = enabled;
3246
+ await deps.core.sessionManager.patchRecord(sessionId, {
3247
+ dangerousMode: enabled
3248
+ });
3249
+ deps.sendJson(res, 200, { ok: true, dangerousMode: enabled });
3250
+ }
3251
+ );
3252
+ router.get("/api/sessions/:sessionId", async (_req, res, params) => {
3253
+ const sessionId = decodeURIComponent(params.sessionId);
3254
+ const session = deps.core.sessionManager.getSession(sessionId);
2759
3255
  if (!session) {
2760
- this.sendJson(res, 404, { error: `Session "${sessionId}" not found` });
3256
+ deps.sendJson(res, 404, { error: `Session "${sessionId}" not found` });
2761
3257
  return;
2762
3258
  }
2763
- this.sendJson(res, 200, {
3259
+ deps.sendJson(res, 200, {
2764
3260
  session: {
2765
3261
  id: session.id,
2766
3262
  agent: session.agentName,
@@ -2776,60 +3272,77 @@ var ApiServer = class {
2776
3272
  agentSessionId: session.agentSessionId
2777
3273
  }
2778
3274
  });
2779
- }
2780
- async handleToggleDangerous(sessionId, req, res) {
2781
- const session = this.core.sessionManager.getSession(sessionId);
3275
+ });
3276
+ router.post("/api/sessions/:sessionId/archive", async (_req, res, params) => {
3277
+ const sessionId = decodeURIComponent(params.sessionId);
3278
+ const result = await deps.core.archiveSession(sessionId);
3279
+ if (result.ok) {
3280
+ deps.sendJson(res, 200, result);
3281
+ } else {
3282
+ deps.sendJson(res, 400, result);
3283
+ }
3284
+ });
3285
+ router.delete("/api/sessions/:sessionId", async (_req, res, params) => {
3286
+ const sessionId = decodeURIComponent(params.sessionId);
3287
+ const session = deps.core.sessionManager.getSession(sessionId);
2782
3288
  if (!session) {
2783
- this.sendJson(res, 404, { error: `Session "${sessionId}" not found` });
3289
+ deps.sendJson(res, 404, { error: `Session "${sessionId}" not found` });
2784
3290
  return;
2785
3291
  }
2786
- const body = await this.readBody(req);
2787
- let enabled;
2788
- if (body) {
2789
- try {
2790
- const parsed = JSON.parse(body);
2791
- enabled = parsed.enabled;
2792
- } catch {
2793
- this.sendJson(res, 400, { error: "Invalid JSON body" });
2794
- return;
3292
+ await deps.core.sessionManager.cancelSession(sessionId);
3293
+ deps.sendJson(res, 200, { ok: true });
3294
+ });
3295
+ router.get("/api/sessions", async (_req, res) => {
3296
+ const sessions = deps.core.sessionManager.listSessions();
3297
+ deps.sendJson(res, 200, {
3298
+ sessions: sessions.map((s) => ({
3299
+ id: s.id,
3300
+ agent: s.agentName,
3301
+ status: s.status,
3302
+ name: s.name ?? null,
3303
+ workspace: s.workingDirectory,
3304
+ createdAt: s.createdAt.toISOString(),
3305
+ dangerousMode: s.dangerousMode,
3306
+ queueDepth: s.queueDepth,
3307
+ promptRunning: s.promptRunning,
3308
+ lastActiveAt: deps.core.sessionManager.getSessionRecord(s.id)?.lastActiveAt ?? null
3309
+ }))
3310
+ });
3311
+ });
3312
+ }
3313
+
3314
+ // src/core/api/routes/config.ts
3315
+ var SENSITIVE_KEYS = [
3316
+ "botToken",
3317
+ "token",
3318
+ "apiKey",
3319
+ "secret",
3320
+ "password",
3321
+ "webhookSecret"
3322
+ ];
3323
+ function redactConfig(config) {
3324
+ const redacted = structuredClone(config);
3325
+ redactDeep(redacted);
3326
+ return redacted;
3327
+ }
3328
+ function redactDeep(obj) {
3329
+ for (const [key, value] of Object.entries(obj)) {
3330
+ if (SENSITIVE_KEYS.includes(key) && typeof value === "string") {
3331
+ obj[key] = "***";
3332
+ } else if (Array.isArray(value)) {
3333
+ for (const item of value) {
3334
+ if (item && typeof item === "object")
3335
+ redactDeep(item);
2795
3336
  }
3337
+ } else if (value && typeof value === "object") {
3338
+ redactDeep(value);
2796
3339
  }
2797
- if (typeof enabled !== "boolean") {
2798
- this.sendJson(res, 400, { error: "Missing enabled boolean" });
2799
- return;
2800
- }
2801
- session.dangerousMode = enabled;
2802
- await this.core.sessionManager.patchRecord(sessionId, { dangerousMode: enabled });
2803
- this.sendJson(res, 200, { ok: true, dangerousMode: enabled });
2804
3340
  }
2805
- async handleHealth(res) {
2806
- const activeSessions = this.core.sessionManager.listSessions();
2807
- const allRecords = this.core.sessionManager.listRecords();
2808
- const mem = process.memoryUsage();
2809
- const tunnel = this.core.tunnelService;
2810
- this.sendJson(res, 200, {
2811
- status: "ok",
2812
- uptime: Date.now() - this.startedAt,
2813
- version: getVersion(),
2814
- memory: {
2815
- rss: mem.rss,
2816
- heapUsed: mem.heapUsed,
2817
- heapTotal: mem.heapTotal
2818
- },
2819
- sessions: {
2820
- active: activeSessions.filter((s) => s.status === "active" || s.status === "initializing").length,
2821
- total: allRecords.length
2822
- },
2823
- adapters: Array.from(this.core.adapters.keys()),
2824
- tunnel: tunnel ? { enabled: true, url: tunnel.getPublicUrl() } : { enabled: false }
2825
- });
2826
- }
2827
- async handleVersion(res) {
2828
- this.sendJson(res, 200, { version: getVersion() });
2829
- }
2830
- async handleGetEditableConfig(res) {
2831
- const { getSafeFields: getSafeFields2, resolveOptions: resolveOptions2, getConfigValue: getConfigValue2 } = await import("./config-registry-QQOJ2GQP.js");
2832
- const config = this.core.configManager.get();
3341
+ }
3342
+ function registerConfigRoutes(router, deps) {
3343
+ router.get("/api/config/editable", async (_req, res) => {
3344
+ const { getSafeFields: getSafeFields2, resolveOptions: resolveOptions2, getConfigValue: getConfigValue2 } = await import("./config-registry-7I6GGDOY.js");
3345
+ const config = deps.core.configManager.get();
2833
3346
  const safeFields = getSafeFields2();
2834
3347
  const fields = safeFields.map((def) => ({
2835
3348
  path: def.path,
@@ -2840,14 +3353,14 @@ var ApiServer = class {
2840
3353
  value: getConfigValue2(config, def.path),
2841
3354
  hotReload: def.hotReload
2842
3355
  }));
2843
- this.sendJson(res, 200, { fields });
2844
- }
2845
- async handleGetConfig(res) {
2846
- const config = this.core.configManager.get();
2847
- this.sendJson(res, 200, { config: redactConfig(config) });
2848
- }
2849
- async handleUpdateConfig(req, res) {
2850
- const body = await this.readBody(req);
3356
+ deps.sendJson(res, 200, { fields });
3357
+ });
3358
+ router.get("/api/config", async (_req, res) => {
3359
+ const config = deps.core.configManager.get();
3360
+ deps.sendJson(res, 200, { config: redactConfig(config) });
3361
+ });
3362
+ router.patch("/api/config", async (req, res) => {
3363
+ const body = await deps.readBody(req);
2851
3364
  let configPath;
2852
3365
  let value;
2853
3366
  if (body) {
@@ -2856,17 +3369,30 @@ var ApiServer = class {
2856
3369
  configPath = parsed.path;
2857
3370
  value = parsed.value;
2858
3371
  } catch {
2859
- this.sendJson(res, 400, { error: "Invalid JSON body" });
3372
+ deps.sendJson(res, 400, { error: "Invalid JSON body" });
2860
3373
  return;
2861
3374
  }
2862
3375
  }
2863
3376
  if (!configPath) {
2864
- this.sendJson(res, 400, { error: "Missing path" });
3377
+ deps.sendJson(res, 400, { error: "Missing path" });
2865
3378
  return;
2866
3379
  }
2867
- const currentConfig = this.core.configManager.get();
2868
- const cloned = structuredClone(currentConfig);
3380
+ const BLOCKED_KEYS = /* @__PURE__ */ new Set(["__proto__", "constructor", "prototype"]);
2869
3381
  const parts = configPath.split(".");
3382
+ if (parts.some((p) => BLOCKED_KEYS.has(p))) {
3383
+ deps.sendJson(res, 400, { error: "Invalid config path" });
3384
+ return;
3385
+ }
3386
+ const { getFieldDef: getFieldDef2 } = await import("./config-registry-7I6GGDOY.js");
3387
+ const fieldDef = getFieldDef2(configPath);
3388
+ if (!fieldDef || fieldDef.scope !== "safe") {
3389
+ deps.sendJson(res, 403, {
3390
+ error: "This config field cannot be modified via the API"
3391
+ });
3392
+ return;
3393
+ }
3394
+ const currentConfig = deps.core.configManager.get();
3395
+ const cloned = structuredClone(currentConfig);
2870
3396
  let target = cloned;
2871
3397
  for (let i = 0; i < parts.length - 1; i++) {
2872
3398
  const part = parts[i];
@@ -2876,18 +3402,21 @@ var ApiServer = class {
2876
3402
  target[part] = {};
2877
3403
  target = target[part];
2878
3404
  } else {
2879
- this.sendJson(res, 400, { error: "Invalid config path" });
3405
+ deps.sendJson(res, 400, { error: "Invalid config path" });
2880
3406
  return;
2881
3407
  }
2882
3408
  }
2883
3409
  const lastKey = parts[parts.length - 1];
2884
3410
  target[lastKey] = value;
2885
- const { ConfigSchema } = await import("./config-AK2W3E67.js");
3411
+ const { ConfigSchema } = await import("./config-4YSJ4NCI.js");
2886
3412
  const result = ConfigSchema.safeParse(cloned);
2887
3413
  if (!result.success) {
2888
- this.sendJson(res, 400, {
3414
+ deps.sendJson(res, 400, {
2889
3415
  error: "Validation failed",
2890
- details: result.error.issues.map((i) => ({ path: i.path.join("."), message: i.message }))
3416
+ details: result.error.issues.map((i) => ({
3417
+ path: i.path.join("."),
3418
+ message: i.message
3419
+ }))
2891
3420
  });
2892
3421
  return;
2893
3422
  }
@@ -2898,239 +3427,438 @@ var ApiServer = class {
2898
3427
  updateTarget = updateTarget[parts[i]];
2899
3428
  }
2900
3429
  updateTarget[lastKey] = value;
2901
- await this.core.configManager.save(updates, configPath);
2902
- const { isHotReloadable: isHotReloadable2 } = await import("./config-registry-QQOJ2GQP.js");
3430
+ await deps.core.configManager.save(updates, configPath);
3431
+ const { isHotReloadable: isHotReloadable2 } = await import("./config-registry-7I6GGDOY.js");
2903
3432
  const needsRestart = !isHotReloadable2(configPath);
2904
- this.sendJson(res, 200, {
3433
+ deps.sendJson(res, 200, {
2905
3434
  ok: true,
2906
3435
  needsRestart,
2907
- config: redactConfig(this.core.configManager.get())
3436
+ config: redactConfig(deps.core.configManager.get())
2908
3437
  });
2909
- }
2910
- async handleListAdapters(res) {
2911
- const adapters = Array.from(this.core.adapters.entries()).map(([name]) => ({
2912
- name,
2913
- type: "built-in"
2914
- }));
2915
- this.sendJson(res, 200, { adapters });
2916
- }
2917
- async handleTunnelStatus(res) {
2918
- const tunnel = this.core.tunnelService;
3438
+ });
3439
+ }
3440
+
3441
+ // src/core/api/routes/topics.ts
3442
+ function registerTopicRoutes(router, deps) {
3443
+ router.get("/api/topics", async (req, res) => {
3444
+ if (!deps.topicManager) {
3445
+ deps.sendJson(res, 501, { error: "Topic management not available" });
3446
+ return;
3447
+ }
3448
+ const url = req.url || "";
3449
+ const params = new URL(url, "http://localhost").searchParams;
3450
+ const statusParam = params.get("status");
3451
+ const filter = statusParam ? { statuses: statusParam.split(",") } : void 0;
3452
+ const topics = deps.topicManager.listTopics(filter);
3453
+ deps.sendJson(res, 200, { topics });
3454
+ });
3455
+ router.post("/api/topics/cleanup", async (req, res) => {
3456
+ if (!deps.topicManager) {
3457
+ deps.sendJson(res, 501, { error: "Topic management not available" });
3458
+ return;
3459
+ }
3460
+ const body = await deps.readBody(req);
3461
+ let statuses;
3462
+ if (body) {
3463
+ try {
3464
+ statuses = JSON.parse(body).statuses;
3465
+ } catch {
3466
+ }
3467
+ }
3468
+ const result = await deps.topicManager.cleanup(statuses);
3469
+ deps.sendJson(res, 200, result);
3470
+ });
3471
+ router.delete("/api/topics/:sessionId", async (req, res, params) => {
3472
+ if (!deps.topicManager) {
3473
+ deps.sendJson(res, 501, { error: "Topic management not available" });
3474
+ return;
3475
+ }
3476
+ const sessionId = decodeURIComponent(params.sessionId);
3477
+ const url = req.url || "";
3478
+ const urlParams = new URL(url, "http://localhost").searchParams;
3479
+ const force = urlParams.get("force") === "true";
3480
+ const result = await deps.topicManager.deleteTopic(
3481
+ sessionId,
3482
+ force ? { confirmed: true } : void 0
3483
+ );
3484
+ if (result.ok) {
3485
+ deps.sendJson(res, 200, result);
3486
+ } else if (result.needsConfirmation) {
3487
+ deps.sendJson(res, 409, {
3488
+ error: "Session is active",
3489
+ needsConfirmation: true,
3490
+ session: result.session
3491
+ });
3492
+ } else if (result.error === "Cannot delete system topic") {
3493
+ deps.sendJson(res, 403, { error: result.error });
3494
+ } else {
3495
+ deps.sendJson(res, 404, { error: result.error ?? "Not found" });
3496
+ }
3497
+ });
3498
+ }
3499
+
3500
+ // src/core/api/routes/tunnel.ts
3501
+ function registerTunnelRoutes(router, deps) {
3502
+ router.get("/api/tunnel", async (_req, res) => {
3503
+ const tunnel = deps.core.tunnelService;
2919
3504
  if (tunnel) {
2920
- this.sendJson(res, 200, { enabled: true, url: tunnel.getPublicUrl(), provider: this.core.configManager.get().tunnel.provider });
3505
+ deps.sendJson(res, 200, {
3506
+ enabled: true,
3507
+ url: tunnel.getPublicUrl(),
3508
+ provider: deps.core.configManager.get().tunnel.provider
3509
+ });
2921
3510
  } else {
2922
- this.sendJson(res, 200, { enabled: false });
3511
+ deps.sendJson(res, 200, { enabled: false });
2923
3512
  }
2924
- }
2925
- async handleTunnelList(res) {
2926
- const tunnel = this.core.tunnelService;
3513
+ });
3514
+ router.get("/api/tunnel/list", async (_req, res) => {
3515
+ const tunnel = deps.core.tunnelService;
2927
3516
  if (!tunnel) {
2928
- this.sendJson(res, 200, []);
3517
+ deps.sendJson(res, 200, []);
2929
3518
  return;
2930
3519
  }
2931
- this.sendJson(res, 200, tunnel.listTunnels());
2932
- }
2933
- async handleTunnelAdd(req, res) {
2934
- const tunnel = this.core.tunnelService;
3520
+ deps.sendJson(res, 200, tunnel.listTunnels());
3521
+ });
3522
+ router.post("/api/tunnel", async (req, res) => {
3523
+ const tunnel = deps.core.tunnelService;
2935
3524
  if (!tunnel) {
2936
- this.sendJson(res, 400, { error: "Tunnel service is not enabled" });
3525
+ deps.sendJson(res, 400, { error: "Tunnel service is not enabled" });
3526
+ return;
3527
+ }
3528
+ const body = await deps.readBody(req);
3529
+ if (body === null) {
3530
+ deps.sendJson(res, 413, { error: "Request body too large" });
2937
3531
  return;
2938
3532
  }
2939
- const body = await this.readBody(req);
2940
3533
  if (!body) {
2941
- this.sendJson(res, 400, { error: "Missing request body" });
3534
+ deps.sendJson(res, 400, { error: "Missing request body" });
2942
3535
  return;
2943
3536
  }
2944
3537
  try {
2945
3538
  const { port, label, sessionId } = JSON.parse(body);
2946
3539
  if (!port || typeof port !== "number") {
2947
- this.sendJson(res, 400, { error: "port is required and must be a number" });
3540
+ deps.sendJson(res, 400, {
3541
+ error: "port is required and must be a number"
3542
+ });
2948
3543
  return;
2949
3544
  }
2950
3545
  const entry = await tunnel.addTunnel(port, { label, sessionId });
2951
- this.sendJson(res, 200, entry);
3546
+ deps.sendJson(res, 200, entry);
2952
3547
  } catch (err) {
2953
- this.sendJson(res, 400, { error: err.message });
3548
+ deps.sendJson(res, 400, { error: err.message });
2954
3549
  }
2955
- }
2956
- async handleTunnelStop(port, res) {
2957
- const tunnel = this.core.tunnelService;
3550
+ });
3551
+ router.delete("/api/tunnel/:port", async (_req, res, params) => {
3552
+ const tunnel = deps.core.tunnelService;
2958
3553
  if (!tunnel) {
2959
- this.sendJson(res, 400, { error: "Tunnel service is not enabled" });
3554
+ deps.sendJson(res, 400, { error: "Tunnel service is not enabled" });
2960
3555
  return;
2961
3556
  }
3557
+ const port = parseInt(params.port, 10);
2962
3558
  try {
2963
3559
  await tunnel.stopTunnel(port);
2964
- this.sendJson(res, 200, { ok: true });
3560
+ deps.sendJson(res, 200, { ok: true });
2965
3561
  } catch (err) {
2966
- this.sendJson(res, 400, { error: err.message });
3562
+ deps.sendJson(res, 400, { error: err.message });
2967
3563
  }
2968
- }
2969
- async handleTunnelStopAll(res) {
2970
- const tunnel = this.core.tunnelService;
3564
+ });
3565
+ router.delete("/api/tunnel", async (_req, res) => {
3566
+ const tunnel = deps.core.tunnelService;
2971
3567
  if (!tunnel) {
2972
- this.sendJson(res, 400, { error: "Tunnel service is not enabled" });
3568
+ deps.sendJson(res, 400, { error: "Tunnel service is not enabled" });
2973
3569
  return;
2974
3570
  }
2975
3571
  const count = tunnel.listTunnels().length;
2976
3572
  await tunnel.stopAllUser();
2977
- this.sendJson(res, 200, { ok: true, stopped: count });
2978
- }
2979
- async handleNotify(req, res) {
2980
- const body = await this.readBody(req);
3573
+ deps.sendJson(res, 200, { ok: true, stopped: count });
3574
+ });
3575
+ }
3576
+
3577
+ // src/core/api/routes/agents.ts
3578
+ function registerAgentRoutes(router, deps) {
3579
+ router.get("/api/agents", async (_req, res) => {
3580
+ const agents = deps.core.agentManager.getAvailableAgents();
3581
+ const defaultAgent = deps.core.configManager.get().defaultAgent;
3582
+ const agentsWithCaps = agents.map((a) => ({
3583
+ ...a,
3584
+ capabilities: getAgentCapabilities(a.name)
3585
+ }));
3586
+ deps.sendJson(res, 200, { agents: agentsWithCaps, default: defaultAgent });
3587
+ });
3588
+ }
3589
+
3590
+ // src/core/api/routes/notify.ts
3591
+ function registerNotifyRoutes(router, deps) {
3592
+ router.post("/api/notify", async (req, res) => {
3593
+ const body = await deps.readBody(req);
2981
3594
  let message;
2982
3595
  if (body) {
2983
3596
  try {
2984
3597
  const parsed = JSON.parse(body);
2985
3598
  message = parsed.message;
2986
3599
  } catch {
2987
- this.sendJson(res, 400, { error: "Invalid JSON body" });
3600
+ deps.sendJson(res, 400, { error: "Invalid JSON body" });
2988
3601
  return;
2989
3602
  }
2990
3603
  }
2991
3604
  if (!message) {
2992
- this.sendJson(res, 400, { error: "Missing message" });
3605
+ deps.sendJson(res, 400, { error: "Missing message" });
2993
3606
  return;
2994
3607
  }
2995
- await this.core.notificationManager.notifyAll({
3608
+ await deps.core.notificationManager.notifyAll({
2996
3609
  sessionId: "system",
2997
3610
  type: "completed",
2998
3611
  summary: message
2999
3612
  });
3000
- this.sendJson(res, 200, { ok: true });
3001
- }
3002
- async handleRestart(res) {
3003
- if (!this.core.requestRestart) {
3004
- this.sendJson(res, 501, { error: "Restart not available" });
3005
- return;
3006
- }
3007
- this.sendJson(res, 200, { ok: true, message: "Restarting..." });
3008
- setImmediate(() => this.core.requestRestart());
3009
- }
3010
- async handleArchiveSession(sessionId, res) {
3011
- const result = await this.core.archiveSession(sessionId);
3012
- if (result.ok) {
3013
- this.sendJson(res, 200, result);
3014
- } else {
3015
- this.sendJson(res, 400, result);
3016
- }
3613
+ deps.sendJson(res, 200, { ok: true });
3614
+ });
3615
+ }
3616
+
3617
+ // src/core/api/index.ts
3618
+ var log9 = createChildLogger({ module: "api-server" });
3619
+ var DEFAULT_PORT_FILE = path7.join(os2.homedir(), ".openacp", "api.port");
3620
+ var cachedVersion;
3621
+ function getVersion() {
3622
+ if (cachedVersion) return cachedVersion;
3623
+ try {
3624
+ const __filename = fileURLToPath2(import.meta.url);
3625
+ const pkgPath = path7.resolve(
3626
+ path7.dirname(__filename),
3627
+ "../../../package.json"
3628
+ );
3629
+ const pkg = JSON.parse(fs7.readFileSync(pkgPath, "utf-8"));
3630
+ cachedVersion = pkg.version ?? "0.0.0-dev";
3631
+ } catch {
3632
+ cachedVersion = "0.0.0-dev";
3017
3633
  }
3018
- async handleCancelSession(sessionId, res) {
3019
- const session = this.core.sessionManager.getSession(sessionId);
3020
- if (!session) {
3021
- this.sendJson(res, 404, { error: `Session "${sessionId}" not found` });
3022
- return;
3023
- }
3024
- await session.abortPrompt();
3025
- this.sendJson(res, 200, { ok: true });
3634
+ return cachedVersion;
3635
+ }
3636
+ var ApiServer = class {
3637
+ constructor(core, config, portFilePath, topicManager, secretFilePath, uiDir) {
3638
+ this.core = core;
3639
+ this.config = config;
3640
+ this.topicManager = topicManager;
3641
+ this.portFilePath = portFilePath ?? DEFAULT_PORT_FILE;
3642
+ this.secretFilePath = secretFilePath ?? path7.join(os2.homedir(), ".openacp", "api-secret");
3643
+ this.staticServer = new StaticServer(uiDir);
3644
+ this.sseManager = new SSEManager(
3645
+ core.eventBus,
3646
+ () => {
3647
+ const sessions = this.core.sessionManager.listSessions();
3648
+ return {
3649
+ active: sessions.filter(
3650
+ (s) => s.status === "active" || s.status === "initializing"
3651
+ ).length,
3652
+ total: sessions.length
3653
+ };
3654
+ },
3655
+ this.startedAt
3656
+ );
3657
+ this.router = new Router();
3658
+ const deps = {
3659
+ core: this.core,
3660
+ topicManager: this.topicManager,
3661
+ startedAt: this.startedAt,
3662
+ getVersion,
3663
+ sendJson: this.sendJson.bind(this),
3664
+ readBody: this.readBody.bind(this)
3665
+ };
3666
+ registerHealthRoutes(this.router, deps);
3667
+ registerSessionRoutes(this.router, deps);
3668
+ registerConfigRoutes(this.router, deps);
3669
+ registerTopicRoutes(this.router, deps);
3670
+ registerTunnelRoutes(this.router, deps);
3671
+ registerAgentRoutes(this.router, deps);
3672
+ registerNotifyRoutes(this.router, deps);
3026
3673
  }
3027
- async handleListSessions(res) {
3028
- const sessions = this.core.sessionManager.listSessions();
3029
- this.sendJson(res, 200, {
3030
- sessions: sessions.map((s) => ({
3031
- id: s.id,
3032
- agent: s.agentName,
3033
- status: s.status,
3034
- name: s.name ?? null,
3035
- workspace: s.workingDirectory
3036
- }))
3674
+ server = null;
3675
+ actualPort = 0;
3676
+ portFilePath;
3677
+ startedAt = Date.now();
3678
+ secret = "";
3679
+ secretFilePath;
3680
+ sseManager;
3681
+ staticServer;
3682
+ router;
3683
+ async start() {
3684
+ this.loadOrCreateSecret();
3685
+ this.server = http.createServer((req, res) => this.handleRequest(req, res));
3686
+ await new Promise((resolve3, reject) => {
3687
+ this.server.on("error", (err) => {
3688
+ if (err.code === "EADDRINUSE") {
3689
+ log9.warn(
3690
+ { port: this.config.port },
3691
+ "API port in use, continuing without API server"
3692
+ );
3693
+ this.server = null;
3694
+ resolve3();
3695
+ } else {
3696
+ reject(err);
3697
+ }
3698
+ });
3699
+ this.server.listen(this.config.port, this.config.host, () => {
3700
+ const addr = this.server.address();
3701
+ if (addr && typeof addr === "object") {
3702
+ this.actualPort = addr.port;
3703
+ }
3704
+ this.writePortFile();
3705
+ log9.info(
3706
+ { host: this.config.host, port: this.actualPort },
3707
+ "API server listening"
3708
+ );
3709
+ this.sseManager.setup();
3710
+ if (this.config.host !== "127.0.0.1" && this.config.host !== "localhost") {
3711
+ log9.warn(
3712
+ "API server binding to non-localhost. Ensure api-secret file is secured."
3713
+ );
3714
+ }
3715
+ resolve3();
3716
+ });
3037
3717
  });
3038
3718
  }
3039
- async handleAdoptSession(req, res) {
3040
- const body = await this.readBody(req);
3041
- if (!body) {
3042
- return this.sendJson(res, 400, { error: "bad_request", message: "Empty request body" });
3719
+ async stop() {
3720
+ this.sseManager.stop();
3721
+ this.removePortFile();
3722
+ if (this.server) {
3723
+ await new Promise((resolve3) => {
3724
+ this.server.close(() => resolve3());
3725
+ });
3726
+ this.server = null;
3043
3727
  }
3044
- let parsed;
3728
+ }
3729
+ getPort() {
3730
+ return this.actualPort;
3731
+ }
3732
+ getSecret() {
3733
+ return this.secret;
3734
+ }
3735
+ writePortFile() {
3736
+ const dir = path7.dirname(this.portFilePath);
3737
+ fs7.mkdirSync(dir, { recursive: true });
3738
+ fs7.writeFileSync(this.portFilePath, String(this.actualPort));
3739
+ }
3740
+ removePortFile() {
3045
3741
  try {
3046
- parsed = JSON.parse(body);
3742
+ fs7.unlinkSync(this.portFilePath);
3047
3743
  } catch {
3048
- return this.sendJson(res, 400, { error: "bad_request", message: "Invalid JSON" });
3049
- }
3050
- const { agent, agentSessionId, cwd } = parsed;
3051
- if (!agent || !agentSessionId) {
3052
- return this.sendJson(res, 400, { error: "bad_request", message: "Missing required fields: agent, agentSessionId" });
3053
- }
3054
- const result = await this.core.adoptSession(agent, agentSessionId, cwd ?? process.cwd());
3055
- if (result.ok) {
3056
- return this.sendJson(res, 200, result);
3057
- } else {
3058
- const status = result.error === "session_limit" ? 429 : result.error === "agent_not_supported" ? 400 : 500;
3059
- return this.sendJson(res, status, result);
3060
3744
  }
3061
3745
  }
3062
- async handleListAgents(res) {
3063
- const agents = this.core.agentManager.getAvailableAgents();
3064
- const defaultAgent = this.core.configManager.get().defaultAgent;
3065
- const agentsWithCaps = agents.map((a) => ({
3066
- ...a,
3067
- capabilities: getAgentCapabilities(a.name)
3068
- }));
3069
- this.sendJson(res, 200, { agents: agentsWithCaps, default: defaultAgent });
3070
- }
3071
- sendJson(res, status, data) {
3072
- res.writeHead(status, { "Content-Type": "application/json" });
3073
- res.end(JSON.stringify(data));
3074
- }
3075
- async handleListTopics(url, res) {
3076
- if (!this.topicManager) {
3077
- this.sendJson(res, 501, { error: "Topic management not available" });
3078
- return;
3746
+ loadOrCreateSecret() {
3747
+ const dir = path7.dirname(this.secretFilePath);
3748
+ fs7.mkdirSync(dir, { recursive: true });
3749
+ try {
3750
+ this.secret = fs7.readFileSync(this.secretFilePath, "utf-8").trim();
3751
+ if (this.secret) {
3752
+ try {
3753
+ const stat = fs7.statSync(this.secretFilePath);
3754
+ const mode = stat.mode & 511;
3755
+ if (mode & 63) {
3756
+ log9.warn(
3757
+ { path: this.secretFilePath, mode: "0" + mode.toString(8) },
3758
+ "API secret file has insecure permissions (should be 0600). Run: chmod 600 %s",
3759
+ this.secretFilePath
3760
+ );
3761
+ }
3762
+ } catch {
3763
+ }
3764
+ return;
3765
+ }
3766
+ } catch {
3079
3767
  }
3080
- const params = new URL(url, "http://localhost").searchParams;
3081
- const statusParam = params.get("status");
3082
- const filter = statusParam ? { statuses: statusParam.split(",") } : void 0;
3083
- const topics = this.topicManager.listTopics(filter);
3084
- this.sendJson(res, 200, { topics });
3768
+ this.secret = crypto.randomBytes(32).toString("hex");
3769
+ fs7.writeFileSync(this.secretFilePath, this.secret, { mode: 384 });
3085
3770
  }
3086
- async handleDeleteTopic(sessionId, url, res) {
3087
- if (!this.topicManager) {
3088
- this.sendJson(res, 501, { error: "Topic management not available" });
3089
- return;
3771
+ authenticate(req, allowQueryParam = false) {
3772
+ const authHeader = req.headers.authorization;
3773
+ if (authHeader?.startsWith("Bearer ")) {
3774
+ const token = authHeader.slice(7);
3775
+ if (token.length === this.secret.length && crypto.timingSafeEqual(
3776
+ Buffer.from(token, "utf-8"),
3777
+ Buffer.from(this.secret, "utf-8")
3778
+ )) {
3779
+ return true;
3780
+ }
3090
3781
  }
3091
- const params = new URL(url, "http://localhost").searchParams;
3092
- const force = params.get("force") === "true";
3093
- const result = await this.topicManager.deleteTopic(sessionId, force ? { confirmed: true } : void 0);
3094
- if (result.ok) {
3095
- this.sendJson(res, 200, result);
3096
- } else if (result.needsConfirmation) {
3097
- this.sendJson(res, 409, { error: "Session is active", needsConfirmation: true, session: result.session });
3098
- } else if (result.error === "Cannot delete system topic") {
3099
- this.sendJson(res, 403, { error: result.error });
3100
- } else {
3101
- this.sendJson(res, 404, { error: result.error ?? "Not found" });
3782
+ if (allowQueryParam) {
3783
+ const parsedUrl = new URL(req.url || "", "http://localhost");
3784
+ const qToken = parsedUrl.searchParams.get("token");
3785
+ if (qToken && qToken.length === this.secret.length && crypto.timingSafeEqual(
3786
+ Buffer.from(qToken, "utf-8"),
3787
+ Buffer.from(this.secret, "utf-8")
3788
+ )) {
3789
+ return true;
3790
+ }
3102
3791
  }
3792
+ return false;
3103
3793
  }
3104
- async handleCleanupTopics(req, res) {
3105
- if (!this.topicManager) {
3106
- this.sendJson(res, 501, { error: "Topic management not available" });
3107
- return;
3794
+ async handleRequest(req, res) {
3795
+ const method = req.method?.toUpperCase();
3796
+ const url = req.url || "";
3797
+ if (url.startsWith("/api/")) {
3798
+ const isExempt = method === "GET" && (url === "/api/health" || url === "/api/version" || url.startsWith("/api/events"));
3799
+ if (!isExempt && !this.authenticate(req)) {
3800
+ this.sendJson(res, 401, { error: "Unauthorized" });
3801
+ return;
3802
+ }
3108
3803
  }
3109
- const body = await this.readBody(req);
3110
- let statuses;
3111
- if (body) {
3112
- try {
3113
- statuses = JSON.parse(body).statuses;
3114
- } catch {
3804
+ try {
3805
+ if (method === "GET" && url.startsWith("/api/events")) {
3806
+ if (!this.authenticate(req, true)) {
3807
+ this.sendJson(res, 401, { error: "Unauthorized" });
3808
+ return;
3809
+ }
3810
+ this.sseManager.handleRequest(req, res);
3811
+ return;
3812
+ }
3813
+ if (url.startsWith("/api/")) {
3814
+ const match = this.router.match(method, url);
3815
+ if (match) {
3816
+ await match.handler(req, res, match.params);
3817
+ } else {
3818
+ this.sendJson(res, 404, { error: "Not found" });
3819
+ }
3820
+ return;
3821
+ }
3822
+ if (!this.staticServer.serve(req, res)) {
3823
+ this.sendJson(res, 404, { error: "Not found" });
3115
3824
  }
3825
+ } catch (err) {
3826
+ log9.error({ err }, "API request error");
3827
+ this.sendJson(res, 500, { error: "Internal server error" });
3116
3828
  }
3117
- const result = await this.topicManager.cleanup(statuses);
3118
- this.sendJson(res, 200, result);
3829
+ }
3830
+ sendJson(res, status, data) {
3831
+ res.writeHead(status, { "Content-Type": "application/json" });
3832
+ res.end(JSON.stringify(data));
3119
3833
  }
3120
3834
  readBody(req) {
3121
- return new Promise((resolve2) => {
3835
+ const MAX_BODY_SIZE = 1024 * 1024;
3836
+ return new Promise((resolve3) => {
3122
3837
  let data = "";
3838
+ let size = 0;
3839
+ let destroyed = false;
3123
3840
  req.on("data", (chunk) => {
3124
- data += chunk;
3841
+ size += chunk.length;
3842
+ if (size > MAX_BODY_SIZE && !destroyed) {
3843
+ destroyed = true;
3844
+ req.destroy();
3845
+ resolve3(null);
3846
+ return;
3847
+ }
3848
+ if (!destroyed) data += chunk;
3849
+ });
3850
+ req.on("end", () => {
3851
+ if (!destroyed) resolve3(data);
3852
+ });
3853
+ req.on("error", () => {
3854
+ if (!destroyed) resolve3("");
3125
3855
  });
3126
- req.on("end", () => resolve2(data));
3127
- req.on("error", () => resolve2(""));
3128
3856
  });
3129
3857
  }
3130
3858
  };
3131
3859
 
3132
3860
  // src/core/topic-manager.ts
3133
- var log8 = createChildLogger({ module: "topic-manager" });
3861
+ var log10 = createChildLogger({ module: "topic-manager" });
3134
3862
  var TopicManager = class {
3135
3863
  constructor(sessionManager, adapter, systemTopicIds) {
3136
3864
  this.sessionManager = sessionManager;
@@ -3169,7 +3897,7 @@ var TopicManager = class {
3169
3897
  try {
3170
3898
  await this.adapter.deleteSessionThread(sessionId);
3171
3899
  } catch (err) {
3172
- log8.warn({ err, sessionId, topicId }, "Failed to delete platform thread, removing record anyway");
3900
+ log10.warn({ err, sessionId, topicId }, "Failed to delete platform thread, removing record anyway");
3173
3901
  }
3174
3902
  }
3175
3903
  await this.sessionManager.removeRecord(sessionId);
@@ -3192,7 +3920,7 @@ var TopicManager = class {
3192
3920
  try {
3193
3921
  await this.adapter.deleteSessionThread(record.sessionId);
3194
3922
  } catch (err) {
3195
- log8.warn({ err, sessionId: record.sessionId }, "Failed to delete platform thread during cleanup");
3923
+ log10.warn({ err, sessionId: record.sessionId }, "Failed to delete platform thread during cleanup");
3196
3924
  }
3197
3925
  }
3198
3926
  await this.sessionManager.removeRecord(record.sessionId);
@@ -3242,9 +3970,12 @@ async function renameSessionTopic(bot, chatId, threadId, name) {
3242
3970
  async function deleteSessionTopic(bot, chatId, threadId) {
3243
3971
  await bot.api.deleteForumTopic(chatId, threadId);
3244
3972
  }
3245
- function buildDeepLink(chatId, messageId) {
3973
+ function buildDeepLink(chatId, threadId, messageId) {
3246
3974
  const cleanId = String(chatId).replace("-100", "");
3247
- return `https://t.me/c/${cleanId}/${messageId}`;
3975
+ if (messageId && messageId !== threadId) {
3976
+ return `https://t.me/c/${cleanId}/${threadId}/${messageId}`;
3977
+ }
3978
+ return `https://t.me/c/${cleanId}/${threadId}`;
3248
3979
  }
3249
3980
 
3250
3981
  // src/adapters/telegram/commands/new-session.ts
@@ -3442,20 +4173,14 @@ function splitMessage(text, maxLength = 3800) {
3442
4173
 
3443
4174
  // src/adapters/telegram/commands/admin.ts
3444
4175
  import { InlineKeyboard } from "grammy";
3445
- var log10 = createChildLogger({ module: "telegram-cmd-admin" });
3446
- function buildDangerousModeKeyboard(sessionId, enabled) {
3447
- return new InlineKeyboard().text(
3448
- enabled ? "\u{1F510} Disable Dangerous Mode" : "\u2620\uFE0F Enable Dangerous Mode",
3449
- `d:${sessionId}`
3450
- );
3451
- }
4176
+ var log12 = createChildLogger({ module: "telegram-cmd-admin" });
3452
4177
  function setupDangerousModeCallbacks(bot, core) {
3453
4178
  bot.callbackQuery(/^d:/, async (ctx) => {
3454
4179
  const sessionId = ctx.callbackQuery.data.slice(2);
3455
4180
  const session = core.sessionManager.getSession(sessionId);
3456
4181
  if (session) {
3457
4182
  session.dangerousMode = !session.dangerousMode;
3458
- log10.info({ sessionId, dangerousMode: session.dangerousMode }, "Dangerous mode toggled via button");
4183
+ log12.info({ sessionId, dangerousMode: session.dangerousMode }, "Dangerous mode toggled via button");
3459
4184
  core.sessionManager.patchRecord(sessionId, { dangerousMode: session.dangerousMode }).catch(() => {
3460
4185
  });
3461
4186
  const toastText2 = session.dangerousMode ? "\u2620\uFE0F Dangerous mode enabled \u2014 permissions auto-approved" : "\u{1F510} Dangerous mode disabled \u2014 permissions shown normally";
@@ -3465,7 +4190,7 @@ function setupDangerousModeCallbacks(bot, core) {
3465
4190
  }
3466
4191
  try {
3467
4192
  await ctx.editMessageReplyMarkup({
3468
- reply_markup: buildDangerousModeKeyboard(sessionId, session.dangerousMode)
4193
+ reply_markup: buildSessionControlKeyboard(sessionId, session.dangerousMode, session.voiceMode === "on")
3469
4194
  });
3470
4195
  } catch {
3471
4196
  }
@@ -3482,7 +4207,7 @@ function setupDangerousModeCallbacks(bot, core) {
3482
4207
  const newDangerousMode = !(record.dangerousMode ?? false);
3483
4208
  core.sessionManager.patchRecord(sessionId, { dangerousMode: newDangerousMode }).catch(() => {
3484
4209
  });
3485
- log10.info({ sessionId, dangerousMode: newDangerousMode }, "Dangerous mode toggled via button (store-only, session not in memory)");
4210
+ log12.info({ sessionId, dangerousMode: newDangerousMode }, "Dangerous mode toggled via button (store-only, session not in memory)");
3486
4211
  const toastText = newDangerousMode ? "\u2620\uFE0F Dangerous mode enabled \u2014 permissions auto-approved" : "\u{1F510} Dangerous mode disabled \u2014 permissions shown normally";
3487
4212
  try {
3488
4213
  await ctx.answerCallbackQuery({ text: toastText });
@@ -3490,7 +4215,7 @@ function setupDangerousModeCallbacks(bot, core) {
3490
4215
  }
3491
4216
  try {
3492
4217
  await ctx.editMessageReplyMarkup({
3493
- reply_markup: buildDangerousModeKeyboard(sessionId, newDangerousMode)
4218
+ reply_markup: buildSessionControlKeyboard(sessionId, newDangerousMode, false)
3494
4219
  });
3495
4220
  } catch {
3496
4221
  }
@@ -3563,6 +4288,65 @@ async function handleDisableDangerous(ctx, core) {
3563
4288
  }
3564
4289
  await ctx.reply("\u{1F510} <b>Dangerous mode disabled</b>\n\nPermission requests will be shown normally.", { parse_mode: "HTML" });
3565
4290
  }
4291
+ function buildSessionControlKeyboard(sessionId, dangerousMode, voiceMode) {
4292
+ return new InlineKeyboard().text(
4293
+ dangerousMode ? "\u{1F510} Disable Dangerous Mode" : "\u2620\uFE0F Enable Dangerous Mode",
4294
+ `d:${sessionId}`
4295
+ ).row().text(
4296
+ voiceMode ? "\u{1F50A} Text to Speech" : "\u{1F507} Text to Speech",
4297
+ `v:${sessionId}`
4298
+ );
4299
+ }
4300
+ function setupTTSCallbacks(bot, core) {
4301
+ bot.callbackQuery(/^v:/, async (ctx) => {
4302
+ const sessionId = ctx.callbackQuery.data.slice(2);
4303
+ const session = core.sessionManager.getSession(sessionId);
4304
+ if (!session) {
4305
+ try {
4306
+ await ctx.answerCallbackQuery({ text: "\u26A0\uFE0F Session not found or not active." });
4307
+ } catch {
4308
+ }
4309
+ return;
4310
+ }
4311
+ const newMode = session.voiceMode === "on" ? "off" : "on";
4312
+ session.setVoiceMode(newMode);
4313
+ const toastText = newMode === "on" ? "\u{1F50A} Text to Speech enabled" : "\u{1F507} Text to Speech disabled";
4314
+ try {
4315
+ await ctx.answerCallbackQuery({ text: toastText });
4316
+ } catch {
4317
+ }
4318
+ try {
4319
+ await ctx.editMessageReplyMarkup({
4320
+ reply_markup: buildSessionControlKeyboard(sessionId, session.dangerousMode, newMode === "on")
4321
+ });
4322
+ } catch {
4323
+ }
4324
+ });
4325
+ }
4326
+ async function handleTTS(ctx, core) {
4327
+ const threadId = ctx.message?.message_thread_id;
4328
+ if (!threadId) {
4329
+ await ctx.reply("\u26A0\uFE0F This command only works inside a session topic.", { parse_mode: "HTML" });
4330
+ return;
4331
+ }
4332
+ const session = await core.getOrResumeSession("telegram", String(threadId));
4333
+ if (!session) {
4334
+ await ctx.reply("\u26A0\uFE0F No active session in this topic.", { parse_mode: "HTML" });
4335
+ return;
4336
+ }
4337
+ const args = ctx.message?.text?.split(/\s+/).slice(1) ?? [];
4338
+ const arg = args[0]?.toLowerCase();
4339
+ if (arg === "on") {
4340
+ session.setVoiceMode("on");
4341
+ await ctx.reply("\u{1F50A} Text to Speech enabled for this session.", { parse_mode: "HTML" });
4342
+ } else if (arg === "off") {
4343
+ session.setVoiceMode("off");
4344
+ await ctx.reply("\u{1F507} Text to Speech disabled.", { parse_mode: "HTML" });
4345
+ } else {
4346
+ session.setVoiceMode("next");
4347
+ await ctx.reply("\u{1F50A} Text to Speech enabled for the next message.", { parse_mode: "HTML" });
4348
+ }
4349
+ }
3566
4350
  async function handleUpdate(ctx, core) {
3567
4351
  if (!core.requestRestart) {
3568
4352
  await ctx.reply("\u26A0\uFE0F Update is not available (no restart handler registered).", { parse_mode: "HTML" });
@@ -3611,7 +4395,7 @@ async function handleRestart(ctx, core) {
3611
4395
  }
3612
4396
 
3613
4397
  // src/adapters/telegram/commands/new-session.ts
3614
- var log11 = createChildLogger({ module: "telegram-cmd-new-session" });
4398
+ var log13 = createChildLogger({ module: "telegram-cmd-new-session" });
3615
4399
  var pendingNewSessions = /* @__PURE__ */ new Map();
3616
4400
  var PENDING_TIMEOUT_MS = 5 * 60 * 1e3;
3617
4401
  function cleanupPending(userId) {
@@ -3710,7 +4494,7 @@ async function startConfirmStep(ctx, chatId, userId, agentName, workspace) {
3710
4494
  });
3711
4495
  }
3712
4496
  async function createSessionDirect(ctx, core, chatId, agentName, workspace) {
3713
- log11.info({ userId: ctx.from?.id, agentName, workspace }, "New session command (direct)");
4497
+ log13.info({ userId: ctx.from?.id, agentName, workspace }, "New session command (direct)");
3714
4498
  let threadId;
3715
4499
  try {
3716
4500
  const topicName = `\u{1F504} New Session`;
@@ -3737,13 +4521,13 @@ This is your coding session \u2014 chat here to work with the agent.`,
3737
4521
  {
3738
4522
  message_thread_id: threadId,
3739
4523
  parse_mode: "HTML",
3740
- reply_markup: buildDangerousModeKeyboard(session.id, false)
4524
+ reply_markup: buildSessionControlKeyboard(session.id, false, false)
3741
4525
  }
3742
4526
  );
3743
- session.warmup().catch((err) => log11.error({ err }, "Warm-up error"));
4527
+ session.warmup().catch((err) => log13.error({ err }, "Warm-up error"));
3744
4528
  return threadId ?? null;
3745
4529
  } catch (err) {
3746
- log11.error({ err }, "Session creation failed");
4530
+ log13.error({ err }, "Session creation failed");
3747
4531
  if (threadId) {
3748
4532
  try {
3749
4533
  await ctx.api.deleteForumTopic(chatId, threadId);
@@ -3818,10 +4602,10 @@ async function handleNewChat(ctx, core, chatId) {
3818
4602
  {
3819
4603
  message_thread_id: newThreadId,
3820
4604
  parse_mode: "HTML",
3821
- reply_markup: buildDangerousModeKeyboard(session.id, false)
4605
+ reply_markup: buildSessionControlKeyboard(session.id, false, false)
3822
4606
  }
3823
4607
  );
3824
- session.warmup().catch((err) => log11.error({ err }, "Warm-up error"));
4608
+ session.warmup().catch((err) => log13.error({ err }, "Warm-up error"));
3825
4609
  } catch (err) {
3826
4610
  if (newThreadId) {
3827
4611
  try {
@@ -3852,7 +4636,7 @@ async function executeNewSession(bot, core, chatId, agentName, workspace) {
3852
4636
  } });
3853
4637
  const finalName = `\u{1F504} ${session.agentName} \u2014 New Session`;
3854
4638
  await renameSessionTopic(bot, chatId, threadId, finalName);
3855
- session.warmup().catch((err) => log11.error({ err }, "Warm-up error"));
4639
+ session.warmup().catch((err) => log13.error({ err }, "Warm-up error"));
3856
4640
  return { session, threadId, firstMsgId };
3857
4641
  } catch (err) {
3858
4642
  try {
@@ -4000,7 +4784,7 @@ Or just the folder name like <code>my-project</code> (will use ${core.configMana
4000
4784
 
4001
4785
  // src/adapters/telegram/commands/session.ts
4002
4786
  import { InlineKeyboard as InlineKeyboard3 } from "grammy";
4003
- var log12 = createChildLogger({ module: "telegram-cmd-session" });
4787
+ var log14 = createChildLogger({ module: "telegram-cmd-session" });
4004
4788
  async function handleCancel(ctx, core, assistant) {
4005
4789
  const threadId = ctx.message?.message_thread_id;
4006
4790
  if (!threadId) return;
@@ -4018,14 +4802,14 @@ async function handleCancel(ctx, core, assistant) {
4018
4802
  String(threadId)
4019
4803
  );
4020
4804
  if (session) {
4021
- log12.info({ sessionId: session.id }, "Abort prompt command");
4805
+ log14.info({ sessionId: session.id }, "Abort prompt command");
4022
4806
  await session.abortPrompt();
4023
4807
  await ctx.reply("\u26D4 Prompt aborted. Session is still active \u2014 send a new message to continue.", { parse_mode: "HTML" });
4024
4808
  return;
4025
4809
  }
4026
4810
  const record = core.sessionManager.getRecordByThread("telegram", String(threadId));
4027
4811
  if (record && record.status !== "error") {
4028
- log12.info({ sessionId: record.sessionId, status: record.status }, "Cancel command \u2014 no active prompt to abort");
4812
+ log14.info({ sessionId: record.sessionId, status: record.status }, "Cancel command \u2014 no active prompt to abort");
4029
4813
  await ctx.reply("\u2139\uFE0F No active prompt to cancel. Send a new message to resume the session.", { parse_mode: "HTML" });
4030
4814
  }
4031
4815
  }
@@ -4129,7 +4913,7 @@ ${lines.join("\n")}${truncated}`,
4129
4913
  { parse_mode: "HTML", reply_markup: keyboard }
4130
4914
  );
4131
4915
  } catch (err) {
4132
- log12.error({ err }, "handleTopics error");
4916
+ log14.error({ err }, "handleTopics error");
4133
4917
  await ctx.reply("\u274C Failed to list sessions.", { parse_mode: "HTML" }).catch(() => {
4134
4918
  });
4135
4919
  }
@@ -4150,13 +4934,13 @@ async function handleCleanup(ctx, core, chatId, statuses) {
4150
4934
  try {
4151
4935
  await ctx.api.deleteForumTopic(chatId, topicId);
4152
4936
  } catch (err) {
4153
- log12.warn({ err, sessionId: record.sessionId, topicId }, "Failed to delete forum topic during cleanup");
4937
+ log14.warn({ err, sessionId: record.sessionId, topicId }, "Failed to delete forum topic during cleanup");
4154
4938
  }
4155
4939
  }
4156
4940
  await core.sessionManager.removeRecord(record.sessionId);
4157
4941
  deleted++;
4158
4942
  } catch (err) {
4159
- log12.error({ err, sessionId: record.sessionId }, "Failed to cleanup session");
4943
+ log14.error({ err, sessionId: record.sessionId }, "Failed to cleanup session");
4160
4944
  failed++;
4161
4945
  }
4162
4946
  }
@@ -4227,7 +5011,7 @@ async function handleCleanupEverythingConfirmed(ctx, core, chatId, systemTopicId
4227
5011
  try {
4228
5012
  await core.sessionManager.cancelSession(record.sessionId);
4229
5013
  } catch (err) {
4230
- log12.warn({ err, sessionId: record.sessionId }, "Failed to cancel session during cleanup");
5014
+ log14.warn({ err, sessionId: record.sessionId }, "Failed to cancel session during cleanup");
4231
5015
  }
4232
5016
  }
4233
5017
  const topicId = record.platform?.topicId;
@@ -4235,13 +5019,13 @@ async function handleCleanupEverythingConfirmed(ctx, core, chatId, systemTopicId
4235
5019
  try {
4236
5020
  await ctx.api.deleteForumTopic(chatId, topicId);
4237
5021
  } catch (err) {
4238
- log12.warn({ err, sessionId: record.sessionId, topicId }, "Failed to delete forum topic during cleanup");
5022
+ log14.warn({ err, sessionId: record.sessionId, topicId }, "Failed to delete forum topic during cleanup");
4239
5023
  }
4240
5024
  }
4241
5025
  await core.sessionManager.removeRecord(record.sessionId);
4242
5026
  deleted++;
4243
5027
  } catch (err) {
4244
- log12.error({ err, sessionId: record.sessionId }, "Failed to cleanup session");
5028
+ log14.error({ err, sessionId: record.sessionId }, "Failed to cleanup session");
4245
5029
  failed++;
4246
5030
  }
4247
5031
  }
@@ -4566,7 +5350,7 @@ Downloading... ${bar} ${percent}%`, { parse_mode: "HTML" });
4566
5350
  const { getAgentCapabilities: getAgentCapabilities2 } = await import("./agent-dependencies-QY5QSULV.js");
4567
5351
  const caps = getAgentCapabilities2(result.agentKey);
4568
5352
  if (caps.integration) {
4569
- const { installIntegration } = await import("./integrate-VOUYBPPZ.js");
5353
+ const { installIntegration } = await import("./integrate-O4OCR4SN.js");
4570
5354
  const intResult = await installIntegration(result.agentKey, caps.integration);
4571
5355
  if (intResult.success) {
4572
5356
  try {
@@ -4636,7 +5420,7 @@ function buildProgressBar(percent) {
4636
5420
  // src/adapters/telegram/commands/integrate.ts
4637
5421
  import { InlineKeyboard as InlineKeyboard5 } from "grammy";
4638
5422
  async function handleIntegrate(ctx, _core) {
4639
- const { listIntegrations } = await import("./integrate-VOUYBPPZ.js");
5423
+ const { listIntegrations } = await import("./integrate-O4OCR4SN.js");
4640
5424
  const agents = listIntegrations();
4641
5425
  const keyboard = new InlineKeyboard5();
4642
5426
  for (const agent of agents) {
@@ -4669,7 +5453,7 @@ function setupIntegrateCallbacks(bot, core) {
4669
5453
  } catch {
4670
5454
  }
4671
5455
  if (data === "i:back") {
4672
- const { listIntegrations } = await import("./integrate-VOUYBPPZ.js");
5456
+ const { listIntegrations } = await import("./integrate-O4OCR4SN.js");
4673
5457
  const agents = listIntegrations();
4674
5458
  const keyboard2 = new InlineKeyboard5();
4675
5459
  for (const agent of agents) {
@@ -4689,7 +5473,7 @@ Select an agent to manage its integrations.`,
4689
5473
  const agentMatch = data.match(/^i:agent:(.+)$/);
4690
5474
  if (agentMatch) {
4691
5475
  const agentName2 = agentMatch[1];
4692
- const { getIntegration: getIntegration2 } = await import("./integrate-VOUYBPPZ.js");
5476
+ const { getIntegration: getIntegration2 } = await import("./integrate-O4OCR4SN.js");
4693
5477
  const integration2 = getIntegration2(agentName2);
4694
5478
  if (!integration2) {
4695
5479
  await ctx.reply(`\u274C No integration available for '${escapeHtml(agentName2)}'.`, { parse_mode: "HTML" });
@@ -4716,7 +5500,7 @@ ${integration2.items.map((i) => `\u2022 <b>${escapeHtml(i.name)}</b> \u2014 ${es
4716
5500
  const action = actionMatch[1];
4717
5501
  const agentName = actionMatch[2];
4718
5502
  const itemId = actionMatch[3];
4719
- const { getIntegration } = await import("./integrate-VOUYBPPZ.js");
5503
+ const { getIntegration } = await import("./integrate-O4OCR4SN.js");
4720
5504
  const integration = getIntegration(agentName);
4721
5505
  if (!integration) return;
4722
5506
  const item = integration.items.find((i) => i.id === itemId);
@@ -4753,7 +5537,7 @@ ${resultText}`,
4753
5537
 
4754
5538
  // src/adapters/telegram/commands/settings.ts
4755
5539
  import { InlineKeyboard as InlineKeyboard6 } from "grammy";
4756
- var log13 = createChildLogger({ module: "telegram-settings" });
5540
+ var log15 = createChildLogger({ module: "telegram-settings" });
4757
5541
  function buildSettingsKeyboard(core) {
4758
5542
  const config = core.configManager.get();
4759
5543
  const fields = getSafeFields();
@@ -4816,7 +5600,7 @@ function setupSettingsCallbacks(bot, core, getAssistantSession) {
4816
5600
  } catch {
4817
5601
  }
4818
5602
  } catch (err) {
4819
- log13.error({ err, fieldPath }, "Failed to toggle config");
5603
+ log15.error({ err, fieldPath }, "Failed to toggle config");
4820
5604
  try {
4821
5605
  await ctx.answerCallbackQuery({ text: "\u274C Failed to update" });
4822
5606
  } catch {
@@ -4890,7 +5674,7 @@ Tap to change:`, {
4890
5674
  } catch {
4891
5675
  }
4892
5676
  } catch (err) {
4893
- log13.error({ err, fieldPath }, "Failed to set config");
5677
+ log15.error({ err, fieldPath }, "Failed to set config");
4894
5678
  try {
4895
5679
  await ctx.answerCallbackQuery({ text: "\u274C Failed to update" });
4896
5680
  } catch {
@@ -4962,7 +5746,7 @@ function buildNestedUpdate(dotPath, value) {
4962
5746
 
4963
5747
  // src/adapters/telegram/commands/doctor.ts
4964
5748
  import { InlineKeyboard as InlineKeyboard7 } from "grammy";
4965
- var log14 = createChildLogger({ module: "telegram-cmd-doctor" });
5749
+ var log16 = createChildLogger({ module: "telegram-cmd-doctor" });
4966
5750
  var pendingFixesStore = /* @__PURE__ */ new Map();
4967
5751
  function renderReport(report) {
4968
5752
  const icons = { pass: "\u2705", warn: "\u26A0\uFE0F", fail: "\u274C" };
@@ -5005,7 +5789,7 @@ async function handleDoctor(ctx) {
5005
5789
  reply_markup: keyboard
5006
5790
  });
5007
5791
  } catch (err) {
5008
- log14.error({ err }, "Doctor command failed");
5792
+ log16.error({ err }, "Doctor command failed");
5009
5793
  await ctx.api.editMessageText(
5010
5794
  ctx.chat.id,
5011
5795
  statusMsg.message_id,
@@ -5054,7 +5838,7 @@ function setupDoctorCallbacks(bot) {
5054
5838
  }
5055
5839
  }
5056
5840
  } catch (err) {
5057
- log14.error({ err, index }, "Doctor fix callback failed");
5841
+ log16.error({ err, index }, "Doctor fix callback failed");
5058
5842
  }
5059
5843
  });
5060
5844
  bot.callbackQuery("m:doctor", async (ctx) => {
@@ -5068,7 +5852,7 @@ function setupDoctorCallbacks(bot) {
5068
5852
 
5069
5853
  // src/adapters/telegram/commands/tunnel.ts
5070
5854
  import { InlineKeyboard as InlineKeyboard8 } from "grammy";
5071
- var log15 = createChildLogger({ module: "telegram-cmd-tunnel" });
5855
+ var log17 = createChildLogger({ module: "telegram-cmd-tunnel" });
5072
5856
  async function handleTunnel(ctx, core) {
5073
5857
  if (!core.tunnelService) {
5074
5858
  await ctx.reply("\u274C Tunnel service is not enabled.", { parse_mode: "HTML" });
@@ -5244,6 +6028,7 @@ function setupCommands(bot, core, chatId, assistant) {
5244
6028
  bot.command("tunnel", (ctx) => handleTunnel(ctx, core));
5245
6029
  bot.command("tunnels", (ctx) => handleTunnels(ctx, core));
5246
6030
  bot.command("archive", (ctx) => handleArchive(ctx, core));
6031
+ bot.command("text_to_speech", (ctx) => handleTTS(ctx, core));
5247
6032
  }
5248
6033
  function setupAllCallbacks(bot, core, chatId, systemTopicIds, getAssistantSession) {
5249
6034
  setupNewSessionCallbacks(bot, core, chatId);
@@ -5316,13 +6101,14 @@ var STATIC_COMMANDS = [
5316
6101
  { command: "usage", description: "View token usage and cost report" },
5317
6102
  { command: "tunnel", description: "Create/stop tunnel for a local port" },
5318
6103
  { command: "tunnels", description: "List active tunnels" },
5319
- { command: "archive", description: "Archive session topic (recreate with clean history)" }
6104
+ { command: "archive", description: "Archive session topic (recreate with clean history)" },
6105
+ { command: "text_to_speech", description: "Toggle Text to Speech (/text_to_speech on, /text_to_speech off)" }
5320
6106
  ];
5321
6107
 
5322
6108
  // src/adapters/telegram/permissions.ts
5323
6109
  import { InlineKeyboard as InlineKeyboard9 } from "grammy";
5324
6110
  import { nanoid as nanoid3 } from "nanoid";
5325
- var log16 = createChildLogger({ module: "telegram-permissions" });
6111
+ var log18 = createChildLogger({ module: "telegram-permissions" });
5326
6112
  var PermissionHandler = class {
5327
6113
  constructor(bot, chatId, getSession, sendNotification) {
5328
6114
  this.bot = bot;
@@ -5356,7 +6142,7 @@ ${escapeHtml(request.description)}`,
5356
6142
  disable_notification: false
5357
6143
  }
5358
6144
  );
5359
- const deepLink = buildDeepLink(this.chatId, msg.message_id);
6145
+ const deepLink = buildDeepLink(this.chatId, threadId, msg.message_id);
5360
6146
  void this.sendNotification({
5361
6147
  sessionId: session.id,
5362
6148
  sessionName: session.name,
@@ -5382,7 +6168,7 @@ ${escapeHtml(request.description)}`,
5382
6168
  }
5383
6169
  const session = this.getSession(pending.sessionId);
5384
6170
  const isAllow = pending.options.find((o) => o.id === optionId)?.isAllow ?? false;
5385
- log16.info({ requestId: pending.requestId, optionId, isAllow }, "Permission responded");
6171
+ log18.info({ requestId: pending.requestId, optionId, isAllow }, "Permission responded");
5386
6172
  if (session?.permissionGate.requestId === pending.requestId) {
5387
6173
  session.permissionGate.resolve(optionId);
5388
6174
  }
@@ -5400,10 +6186,10 @@ ${escapeHtml(request.description)}`,
5400
6186
  };
5401
6187
 
5402
6188
  // src/adapters/telegram/assistant.ts
5403
- var log17 = createChildLogger({ module: "telegram-assistant" });
6189
+ var log19 = createChildLogger({ module: "telegram-assistant" });
5404
6190
  async function spawnAssistant(core, adapter, assistantTopicId) {
5405
6191
  const config = core.configManager.get();
5406
- log17.info({ agent: config.defaultAgent }, "Creating assistant session...");
6192
+ log19.info({ agent: config.defaultAgent }, "Creating assistant session...");
5407
6193
  const session = await core.createSession({
5408
6194
  channelId: "telegram",
5409
6195
  agentName: config.defaultAgent,
@@ -5412,7 +6198,7 @@ async function spawnAssistant(core, adapter, assistantTopicId) {
5412
6198
  // Prevent auto-naming from triggering after system prompt
5413
6199
  });
5414
6200
  session.threadId = String(assistantTopicId);
5415
- log17.info({ sessionId: session.id }, "Assistant agent spawned");
6201
+ log19.info({ sessionId: session.id }, "Assistant agent spawned");
5416
6202
  const allRecords = core.sessionManager.listRecords();
5417
6203
  const activeCount = allRecords.filter((r) => r.status === "active" || r.status === "initializing").length;
5418
6204
  const statusCounts = /* @__PURE__ */ new Map();
@@ -5433,9 +6219,9 @@ async function spawnAssistant(core, adapter, assistantTopicId) {
5433
6219
  };
5434
6220
  const systemPrompt = buildAssistantSystemPrompt(ctx);
5435
6221
  const ready = session.enqueuePrompt(systemPrompt).then(() => {
5436
- log17.info({ sessionId: session.id }, "Assistant system prompt completed");
6222
+ log19.info({ sessionId: session.id }, "Assistant system prompt completed");
5437
6223
  }).catch((err) => {
5438
- log17.warn({ err }, "Assistant system prompt failed");
6224
+ log19.warn({ err }, "Assistant system prompt failed");
5439
6225
  });
5440
6226
  return { session, ready };
5441
6227
  }
@@ -5601,7 +6387,7 @@ function redirectToAssistant(chatId, assistantTopicId) {
5601
6387
  }
5602
6388
 
5603
6389
  // src/adapters/telegram/activity.ts
5604
- var log18 = createChildLogger({ module: "telegram:activity" });
6390
+ var log20 = createChildLogger({ module: "telegram:activity" });
5605
6391
  var THINKING_REFRESH_MS = 15e3;
5606
6392
  var THINKING_MAX_MS = 3 * 60 * 1e3;
5607
6393
  var ThinkingIndicator = class {
@@ -5633,7 +6419,7 @@ var ThinkingIndicator = class {
5633
6419
  this.startRefreshTimer();
5634
6420
  }
5635
6421
  } catch (err) {
5636
- log18.warn({ err }, "ThinkingIndicator.show() failed");
6422
+ log20.warn({ err }, "ThinkingIndicator.show() failed");
5637
6423
  } finally {
5638
6424
  this.sending = false;
5639
6425
  }
@@ -5706,7 +6492,7 @@ var UsageMessage = class {
5706
6492
  if (result) this.msgId = result.message_id;
5707
6493
  }
5708
6494
  } catch (err) {
5709
- log18.warn({ err }, "UsageMessage.send() failed");
6495
+ log20.warn({ err }, "UsageMessage.send() failed");
5710
6496
  }
5711
6497
  }
5712
6498
  getMsgId() {
@@ -5719,7 +6505,7 @@ var UsageMessage = class {
5719
6505
  try {
5720
6506
  await this.sendQueue.enqueue(() => this.api.deleteMessage(this.chatId, id));
5721
6507
  } catch (err) {
5722
- log18.warn({ err }, "UsageMessage.delete() failed");
6508
+ log20.warn({ err }, "UsageMessage.delete() failed");
5723
6509
  }
5724
6510
  }
5725
6511
  };
@@ -5805,7 +6591,7 @@ var PlanCard = class {
5805
6591
  if (result) this.msgId = result.message_id;
5806
6592
  }
5807
6593
  } catch (err) {
5808
- log18.warn({ err }, "PlanCard flush failed");
6594
+ log20.warn({ err }, "PlanCard flush failed");
5809
6595
  }
5810
6596
  }
5811
6597
  };
@@ -5868,7 +6654,7 @@ var ActivityTracker = class {
5868
6654
  })
5869
6655
  );
5870
6656
  } catch (err) {
5871
- log18.warn({ err }, "ActivityTracker.onComplete() Done send failed");
6657
+ log20.warn({ err }, "ActivityTracker.onComplete() Done send failed");
5872
6658
  }
5873
6659
  }
5874
6660
  }
@@ -5895,19 +6681,19 @@ var TelegramSendQueue = class {
5895
6681
  enqueue(fn, opts) {
5896
6682
  const type = opts?.type ?? "other";
5897
6683
  const key = opts?.key;
5898
- return new Promise((resolve2, reject) => {
6684
+ return new Promise((resolve3, reject) => {
5899
6685
  if (type === "text" && key) {
5900
6686
  const idx = this.items.findIndex(
5901
6687
  (item) => item.type === "text" && item.key === key
5902
6688
  );
5903
6689
  if (idx !== -1) {
5904
6690
  this.items[idx].resolve(void 0);
5905
- this.items[idx] = { fn, type, key, resolve: resolve2, reject };
6691
+ this.items[idx] = { fn, type, key, resolve: resolve3, reject };
5906
6692
  this.scheduleProcess();
5907
6693
  return;
5908
6694
  }
5909
6695
  }
5910
- this.items.push({ fn, type, key, resolve: resolve2, reject });
6696
+ this.items.push({ fn, type, key, resolve: resolve3, reject });
5911
6697
  this.scheduleProcess();
5912
6698
  });
5913
6699
  }
@@ -6039,7 +6825,8 @@ function setupActionCallbacks(bot, core, chatId, getAssistantSessionId) {
6039
6825
  action.agent,
6040
6826
  action.workspace
6041
6827
  );
6042
- const topicLink = `https://t.me/c/${String(chatId).replace("-100", "")}/${firstMsgId ?? threadId}`;
6828
+ const cleanId = String(chatId).replace("-100", "");
6829
+ const topicLink = firstMsgId ? `https://t.me/c/${cleanId}/${threadId}/${firstMsgId}` : `https://t.me/c/${cleanId}/${threadId}`;
6043
6830
  const originalText = ctx.callbackQuery.message?.text ?? "";
6044
6831
  try {
6045
6832
  await ctx.editMessageText(
@@ -6106,7 +6893,7 @@ function setupActionCallbacks(bot, core, chatId, getAssistantSessionId) {
6106
6893
  }
6107
6894
 
6108
6895
  // src/adapters/telegram/tool-call-tracker.ts
6109
- var log19 = createChildLogger({ module: "tool-call-tracker" });
6896
+ var log21 = createChildLogger({ module: "tool-call-tracker" });
6110
6897
  var ToolCallTracker = class {
6111
6898
  constructor(bot, chatId, sendQueue) {
6112
6899
  this.bot = bot;
@@ -6150,7 +6937,7 @@ var ToolCallTracker = class {
6150
6937
  if (!toolState) return;
6151
6938
  if (meta.viewerLinks) {
6152
6939
  toolState.viewerLinks = meta.viewerLinks;
6153
- log19.debug({ toolId: meta.id, viewerLinks: meta.viewerLinks }, "Accumulated viewerLinks");
6940
+ log21.debug({ toolId: meta.id, viewerLinks: meta.viewerLinks }, "Accumulated viewerLinks");
6154
6941
  }
6155
6942
  if (meta.viewerFilePath) toolState.viewerFilePath = meta.viewerFilePath;
6156
6943
  if (meta.name) toolState.name = meta.name;
@@ -6158,7 +6945,7 @@ var ToolCallTracker = class {
6158
6945
  const isTerminal = meta.status === "completed" || meta.status === "failed";
6159
6946
  if (!isTerminal) return;
6160
6947
  await toolState.ready;
6161
- log19.debug(
6948
+ log21.debug(
6162
6949
  {
6163
6950
  toolId: meta.id,
6164
6951
  status: meta.status,
@@ -6187,7 +6974,7 @@ var ToolCallTracker = class {
6187
6974
  )
6188
6975
  );
6189
6976
  } catch (err) {
6190
- log19.warn(
6977
+ log21.warn(
6191
6978
  {
6192
6979
  err,
6193
6980
  msgId: toolState.msgId,
@@ -6365,6 +7152,24 @@ var MessageDraft = class {
6365
7152
  getMessageId() {
6366
7153
  return this.messageId;
6367
7154
  }
7155
+ async stripPattern(pattern) {
7156
+ if (!this.messageId || !this.buffer) return;
7157
+ const stripped = this.buffer.replace(pattern, "").trim();
7158
+ if (stripped === this.buffer.trim()) return;
7159
+ this.buffer = stripped;
7160
+ this.lastSentBuffer = stripped;
7161
+ const html = markdownToTelegramHtml(stripped);
7162
+ if (!html) return;
7163
+ try {
7164
+ await this.sendQueue.enqueue(
7165
+ () => this.bot.api.editMessageText(this.chatId, this.messageId, html, {
7166
+ parse_mode: "HTML"
7167
+ }),
7168
+ { type: "other" }
7169
+ );
7170
+ } catch {
7171
+ }
7172
+ }
6368
7173
  };
6369
7174
 
6370
7175
  // src/adapters/telegram/draft-manager.ts
@@ -6393,6 +7198,9 @@ var DraftManager = class {
6393
7198
  hasDraft(sessionId) {
6394
7199
  return this.drafts.has(sessionId);
6395
7200
  }
7201
+ getDraft(sessionId) {
7202
+ return this.drafts.get(sessionId);
7203
+ }
6396
7204
  appendText(sessionId, text) {
6397
7205
  this.textBuffers.set(
6398
7206
  sessionId,
@@ -6437,7 +7245,7 @@ var DraftManager = class {
6437
7245
  };
6438
7246
 
6439
7247
  // src/adapters/telegram/skill-command-manager.ts
6440
- var log20 = createChildLogger({ module: "skill-commands" });
7248
+ var log22 = createChildLogger({ module: "skill-commands" });
6441
7249
  var SkillCommandManager = class {
6442
7250
  // sessionId → pinned msgId
6443
7251
  constructor(bot, chatId, sendQueue, sessionManager) {
@@ -6503,7 +7311,7 @@ var SkillCommandManager = class {
6503
7311
  disable_notification: true
6504
7312
  });
6505
7313
  } catch (err) {
6506
- log20.error({ err, sessionId }, "Failed to send skill commands");
7314
+ log22.error({ err, sessionId }, "Failed to send skill commands");
6507
7315
  }
6508
7316
  }
6509
7317
  async cleanup(sessionId) {
@@ -6522,14 +7330,17 @@ var SkillCommandManager = class {
6522
7330
  this.messages.delete(sessionId);
6523
7331
  const record = this.sessionManager.getSessionRecord(sessionId);
6524
7332
  if (record) {
6525
- const { skillMsgId: _removed, ...rest } = record.platform;
6526
- await this.sessionManager.patchRecord(sessionId, { platform: rest });
7333
+ const platform = record.platform;
7334
+ if (platform && typeof platform === "object" && "topicId" in platform) {
7335
+ const { skillMsgId: _removed, ...rest } = platform;
7336
+ await this.sessionManager.patchRecord(sessionId, { platform: rest });
7337
+ }
6527
7338
  }
6528
7339
  }
6529
7340
  };
6530
7341
 
6531
7342
  // src/adapters/telegram/adapter.ts
6532
- var log21 = createChildLogger({ module: "telegram" });
7343
+ var log23 = createChildLogger({ module: "telegram" });
6533
7344
  function patchedFetch(input, init) {
6534
7345
  if (init?.signal && !(init.signal instanceof AbortSignal)) {
6535
7346
  const nativeController = new AbortController();
@@ -6576,7 +7387,12 @@ var TelegramAdapter = class extends ChannelAdapter {
6576
7387
  this.telegramConfig = config;
6577
7388
  }
6578
7389
  async start() {
6579
- this.bot = new Bot(this.telegramConfig.botToken, { client: { fetch: patchedFetch } });
7390
+ this.bot = new Bot(this.telegramConfig.botToken, {
7391
+ client: {
7392
+ baseFetchConfig: { duplex: "half" },
7393
+ fetch: patchedFetch
7394
+ }
7395
+ });
6580
7396
  this.fileService = this.core.fileService;
6581
7397
  this.toolTracker = new ToolCallTracker(this.bot, this.telegramConfig.chatId, this.sendQueue);
6582
7398
  this.draftManager = new DraftManager(this.bot, this.telegramConfig.chatId, this.sendQueue);
@@ -6588,7 +7404,7 @@ var TelegramAdapter = class extends ChannelAdapter {
6588
7404
  );
6589
7405
  this.bot.catch((err) => {
6590
7406
  const rootCause = err.error instanceof Error ? err.error : err;
6591
- log21.error({ err: rootCause }, "Telegram bot error");
7407
+ log23.error({ err: rootCause }, "Telegram bot error");
6592
7408
  });
6593
7409
  this.bot.api.config.use(async (prev, method, payload, signal) => {
6594
7410
  const maxRetries = 3;
@@ -6602,7 +7418,7 @@ var TelegramAdapter = class extends ChannelAdapter {
6602
7418
  if (rateLimitedMethods.includes(method)) {
6603
7419
  this.sendQueue.onRateLimited();
6604
7420
  }
6605
- log21.warn(
7421
+ log23.warn(
6606
7422
  { method, retryAfter, attempt: attempt + 1 },
6607
7423
  "Rate limited by Telegram, retrying"
6608
7424
  );
@@ -6647,6 +7463,7 @@ var TelegramAdapter = class extends ChannelAdapter {
6647
7463
  (notification) => this.sendNotification(notification)
6648
7464
  );
6649
7465
  setupDangerousModeCallbacks(this.bot, this.core);
7466
+ setupTTSCallbacks(this.bot, this.core);
6650
7467
  setupActionCallbacks(
6651
7468
  this.bot,
6652
7469
  this.core,
@@ -6734,7 +7551,7 @@ var TelegramAdapter = class extends ChannelAdapter {
6734
7551
  this.setupRoutes();
6735
7552
  this.bot.start({
6736
7553
  allowed_updates: ["message", "callback_query"],
6737
- onStart: () => log21.info(
7554
+ onStart: () => log23.info(
6738
7555
  { chatId: this.telegramConfig.chatId },
6739
7556
  "Telegram bot started"
6740
7557
  )
@@ -6756,10 +7573,10 @@ var TelegramAdapter = class extends ChannelAdapter {
6756
7573
  reply_markup: buildMenuKeyboard()
6757
7574
  });
6758
7575
  } catch (err) {
6759
- log21.warn({ err }, "Failed to send welcome message");
7576
+ log23.warn({ err }, "Failed to send welcome message");
6760
7577
  }
6761
7578
  try {
6762
- log21.info("Spawning assistant session...");
7579
+ log23.info("Spawning assistant session...");
6763
7580
  const { session, ready } = await spawnAssistant(
6764
7581
  this.core,
6765
7582
  this,
@@ -6767,13 +7584,13 @@ var TelegramAdapter = class extends ChannelAdapter {
6767
7584
  );
6768
7585
  this.assistantSession = session;
6769
7586
  this.assistantInitializing = true;
6770
- log21.info({ sessionId: session.id }, "Assistant session ready, system prompt running in background");
7587
+ log23.info({ sessionId: session.id }, "Assistant session ready, system prompt running in background");
6771
7588
  ready.then(() => {
6772
7589
  this.assistantInitializing = false;
6773
- log21.info({ sessionId: session.id }, "Assistant ready for user messages");
7590
+ log23.info({ sessionId: session.id }, "Assistant ready for user messages");
6774
7591
  });
6775
7592
  } catch (err) {
6776
- log21.error({ err }, "Failed to spawn assistant");
7593
+ log23.error({ err }, "Failed to spawn assistant");
6777
7594
  this.bot.api.sendMessage(
6778
7595
  this.telegramConfig.chatId,
6779
7596
  `\u26A0\uFE0F <b>Failed to start assistant session.</b>
@@ -6789,7 +7606,7 @@ var TelegramAdapter = class extends ChannelAdapter {
6789
7606
  await this.assistantSession.destroy();
6790
7607
  }
6791
7608
  await this.bot.stop();
6792
- log21.info("Telegram bot stopped");
7609
+ log23.info("Telegram bot stopped");
6793
7610
  }
6794
7611
  setupRoutes() {
6795
7612
  this.bot.on("message:text", async (ctx) => {
@@ -6817,7 +7634,7 @@ var TelegramAdapter = class extends ChannelAdapter {
6817
7634
  ctx.replyWithChatAction("typing").catch(() => {
6818
7635
  });
6819
7636
  handleAssistantMessage(this.assistantSession, forwardText).catch(
6820
- (err) => log21.error({ err }, "Assistant error")
7637
+ (err) => log23.error({ err }, "Assistant error")
6821
7638
  );
6822
7639
  return;
6823
7640
  }
@@ -6834,7 +7651,7 @@ var TelegramAdapter = class extends ChannelAdapter {
6834
7651
  threadId: String(threadId),
6835
7652
  userId: String(ctx.from.id),
6836
7653
  text: forwardText
6837
- }).catch((err) => log21.error({ err }, "handleMessage error"));
7654
+ }).catch((err) => log23.error({ err }, "handleMessage error"));
6838
7655
  });
6839
7656
  this.bot.on("message:photo", async (ctx) => {
6840
7657
  const threadId = ctx.message.message_thread_id;
@@ -6904,220 +7721,200 @@ var TelegramAdapter = class extends ChannelAdapter {
6904
7721
  );
6905
7722
  });
6906
7723
  }
6907
- // --- ChannelAdapter implementations ---
6908
- async sendMessage(sessionId, content) {
6909
- if (this.assistantInitializing && sessionId === this.assistantSession?.id) return;
6910
- const session = this.core.sessionManager.getSession(sessionId);
6911
- if (!session) return;
6912
- if (session.archiving) return;
6913
- const threadId = Number(session.threadId);
6914
- if (!threadId || isNaN(threadId)) {
6915
- log21.warn({ sessionId, threadId: session.threadId }, "Session has no valid threadId, skipping message");
6916
- return;
6917
- }
6918
- switch (content.type) {
6919
- case "thought": {
6920
- const tracker = this.getOrCreateTracker(sessionId, threadId);
6921
- await tracker.onThought();
6922
- break;
6923
- }
6924
- case "text": {
6925
- if (!this.draftManager.hasDraft(sessionId)) {
6926
- const tracker = this.getOrCreateTracker(sessionId, threadId);
6927
- tracker.onTextStart().catch(() => {
6928
- });
6929
- }
6930
- const draft = this.draftManager.getOrCreate(sessionId, threadId);
6931
- draft.append(content.text);
6932
- this.draftManager.appendText(sessionId, content.text);
6933
- break;
6934
- }
6935
- case "tool_call": {
6936
- const tracker = this.getOrCreateTracker(sessionId, threadId);
6937
- await tracker.onToolCall();
6938
- await this.draftManager.finalize(sessionId, this.assistantSession?.id);
6939
- const meta = content.metadata;
6940
- await this.toolTracker.trackNewCall(sessionId, threadId, {
6941
- ...meta,
6942
- viewerFilePath: content.metadata?.viewerFilePath
7724
+ // --- MessageHandlers for dispatchMessage ---
7725
+ messageHandlers = {
7726
+ onThought: async (ctx, _content) => {
7727
+ const tracker = this.getOrCreateTracker(ctx.sessionId, ctx.threadId);
7728
+ await tracker.onThought();
7729
+ },
7730
+ onText: async (ctx, content) => {
7731
+ if (!this.draftManager.hasDraft(ctx.sessionId)) {
7732
+ const tracker = this.getOrCreateTracker(ctx.sessionId, ctx.threadId);
7733
+ tracker.onTextStart().catch(() => {
6943
7734
  });
6944
- break;
6945
7735
  }
6946
- case "tool_update": {
6947
- const meta = content.metadata;
6948
- await this.toolTracker.updateCall(sessionId, {
6949
- ...meta,
6950
- viewerFilePath: content.metadata?.viewerFilePath
7736
+ const draft = this.draftManager.getOrCreate(ctx.sessionId, ctx.threadId);
7737
+ draft.append(content.text);
7738
+ this.draftManager.appendText(ctx.sessionId, content.text);
7739
+ },
7740
+ onToolCall: async (ctx, content) => {
7741
+ const tracker = this.getOrCreateTracker(ctx.sessionId, ctx.threadId);
7742
+ await tracker.onToolCall();
7743
+ await this.draftManager.finalize(ctx.sessionId, this.assistantSession?.id);
7744
+ const meta = content.metadata;
7745
+ await this.toolTracker.trackNewCall(ctx.sessionId, ctx.threadId, {
7746
+ ...meta
7747
+ });
7748
+ },
7749
+ onToolUpdate: async (ctx, content) => {
7750
+ const meta = content.metadata;
7751
+ await this.toolTracker.updateCall(ctx.sessionId, {
7752
+ ...meta
7753
+ });
7754
+ },
7755
+ onPlan: async (ctx, content) => {
7756
+ const meta = content.metadata;
7757
+ const tracker = this.getOrCreateTracker(ctx.sessionId, ctx.threadId);
7758
+ await tracker.onPlan(
7759
+ meta.entries.map((e) => ({
7760
+ content: e.content,
7761
+ status: e.status,
7762
+ priority: e.priority ?? "medium"
7763
+ }))
7764
+ );
7765
+ },
7766
+ onUsage: async (ctx, content) => {
7767
+ const meta = content.metadata;
7768
+ await this.draftManager.finalize(ctx.sessionId, this.assistantSession?.id);
7769
+ const tracker = this.getOrCreateTracker(ctx.sessionId, ctx.threadId);
7770
+ await tracker.sendUsage(meta ?? {});
7771
+ if (this.notificationTopicId && ctx.sessionId !== this.assistantSession?.id) {
7772
+ const sess = this.core.sessionManager.getSession(ctx.sessionId);
7773
+ const sessionName = sess?.name || "Session";
7774
+ const chatIdStr = String(this.telegramConfig.chatId);
7775
+ const numericId = chatIdStr.startsWith("-100") ? chatIdStr.slice(4) : chatIdStr.replace("-", "");
7776
+ const usageMsgId = tracker.getUsageMsgId();
7777
+ const deepLink = usageMsgId ? `https://t.me/c/${numericId}/${ctx.threadId}/${usageMsgId}` : `https://t.me/c/${numericId}/${ctx.threadId}`;
7778
+ const text = `\u2705 <b>${escapeHtml(sessionName)}</b>
7779
+ Task completed.
7780
+
7781
+ <a href="${deepLink}">\u2192 Go to topic</a>`;
7782
+ this.sendQueue.enqueue(
7783
+ () => this.bot.api.sendMessage(this.telegramConfig.chatId, text, {
7784
+ message_thread_id: this.notificationTopicId,
7785
+ parse_mode: "HTML",
7786
+ disable_notification: false
7787
+ })
7788
+ ).catch(() => {
6951
7789
  });
6952
- break;
6953
7790
  }
6954
- case "plan": {
6955
- const meta = content.metadata;
6956
- const tracker = this.getOrCreateTracker(sessionId, threadId);
6957
- await tracker.onPlan(
6958
- meta.entries.map((e) => ({
6959
- content: e.content,
6960
- status: e.status,
6961
- priority: e.priority ?? "medium"
6962
- }))
7791
+ },
7792
+ onAttachment: async (ctx, content) => {
7793
+ if (!content.attachment) return;
7794
+ const { attachment } = content;
7795
+ if (attachment.size > 50 * 1024 * 1024) {
7796
+ log23.warn({ sessionId: ctx.sessionId, fileName: attachment.fileName, size: attachment.size }, "File too large for Telegram (>50MB)");
7797
+ await this.sendQueue.enqueue(
7798
+ () => this.bot.api.sendMessage(
7799
+ this.telegramConfig.chatId,
7800
+ `\u26A0\uFE0F File too large to send (${Math.round(attachment.size / 1024 / 1024)}MB): ${escapeHtml(attachment.fileName)}`,
7801
+ { message_thread_id: ctx.threadId, parse_mode: "HTML" }
7802
+ )
6963
7803
  );
6964
- break;
7804
+ return;
6965
7805
  }
6966
- case "usage": {
6967
- const meta = content.metadata;
6968
- await this.draftManager.finalize(sessionId, this.assistantSession?.id);
6969
- const tracker = this.getOrCreateTracker(sessionId, threadId);
6970
- await tracker.sendUsage(meta);
6971
- if (this.notificationTopicId && sessionId !== this.assistantSession?.id) {
6972
- const sess = this.core.sessionManager.getSession(sessionId);
6973
- const sessionName = sess?.name || "Session";
6974
- const chatIdStr = String(this.telegramConfig.chatId);
6975
- const numericId = chatIdStr.startsWith("-100") ? chatIdStr.slice(4) : chatIdStr.replace("-", "");
6976
- const usageMsgId = tracker.getUsageMsgId();
6977
- const deepLink = `https://t.me/c/${numericId}/${usageMsgId ?? threadId}`;
6978
- const text = `\u2705 <b>${escapeHtml(sessionName)}</b>
6979
- Task completed.
6980
-
6981
- <a href="${deepLink}">\u2192 Go to topic</a>`;
6982
- this.sendQueue.enqueue(
6983
- () => this.bot.api.sendMessage(this.telegramConfig.chatId, text, {
6984
- message_thread_id: this.notificationTopicId,
6985
- parse_mode: "HTML",
6986
- disable_notification: false
7806
+ try {
7807
+ const inputFile = new InputFile(attachment.filePath);
7808
+ if (attachment.type === "image") {
7809
+ await this.sendQueue.enqueue(
7810
+ () => this.bot.api.sendPhoto(this.telegramConfig.chatId, inputFile, {
7811
+ message_thread_id: ctx.threadId
6987
7812
  })
6988
- ).catch(() => {
6989
- });
6990
- }
6991
- break;
6992
- }
6993
- case "attachment": {
6994
- if (!content.attachment) break;
6995
- const { attachment } = content;
6996
- if (attachment.size > 50 * 1024 * 1024) {
6997
- log21.warn({ sessionId, fileName: attachment.fileName, size: attachment.size }, "File too large for Telegram (>50MB)");
7813
+ );
7814
+ } else if (attachment.type === "audio") {
6998
7815
  await this.sendQueue.enqueue(
6999
- () => this.bot.api.sendMessage(
7000
- this.telegramConfig.chatId,
7001
- `\u26A0\uFE0F File too large to send (${Math.round(attachment.size / 1024 / 1024)}MB): ${escapeHtml(attachment.fileName)}`,
7002
- { message_thread_id: threadId, parse_mode: "HTML" }
7003
- )
7816
+ () => this.bot.api.sendVoice(this.telegramConfig.chatId, inputFile, {
7817
+ message_thread_id: ctx.threadId
7818
+ })
7004
7819
  );
7005
- break;
7006
- }
7007
- try {
7008
- const inputFile = new InputFile(attachment.filePath);
7009
- if (attachment.type === "image") {
7010
- await this.sendQueue.enqueue(
7011
- () => this.bot.api.sendPhoto(this.telegramConfig.chatId, inputFile, {
7012
- message_thread_id: threadId
7013
- })
7014
- );
7015
- } else if (attachment.type === "audio") {
7016
- await this.sendQueue.enqueue(
7017
- () => this.bot.api.sendVoice(this.telegramConfig.chatId, inputFile, {
7018
- message_thread_id: threadId
7019
- })
7020
- );
7021
- } else {
7022
- await this.sendQueue.enqueue(
7023
- () => this.bot.api.sendDocument(this.telegramConfig.chatId, inputFile, {
7024
- message_thread_id: threadId
7025
- })
7026
- );
7820
+ const draft = this.draftManager.getDraft(ctx.sessionId);
7821
+ if (draft) {
7822
+ draft.stripPattern(/\[TTS\][\s\S]*?\[\/TTS\]/g).catch(() => {
7823
+ });
7027
7824
  }
7028
- } catch (err) {
7029
- log21.error({ err, sessionId, fileName: attachment.fileName }, "Failed to send attachment");
7030
- }
7031
- break;
7032
- }
7033
- case "session_end": {
7034
- await this.draftManager.finalize(sessionId, this.assistantSession?.id);
7035
- this.draftManager.cleanup(sessionId);
7036
- this.toolTracker.cleanup(sessionId);
7037
- await this.skillManager.cleanup(sessionId);
7038
- const tracker = this.sessionTrackers.get(sessionId);
7039
- if (tracker) {
7040
- await tracker.onComplete();
7041
- tracker.destroy();
7042
- this.sessionTrackers.delete(sessionId);
7043
7825
  } else {
7044
7826
  await this.sendQueue.enqueue(
7045
- () => this.bot.api.sendMessage(
7046
- this.telegramConfig.chatId,
7047
- `\u2705 <b>Done</b>`,
7048
- {
7049
- message_thread_id: threadId,
7050
- parse_mode: "HTML",
7051
- disable_notification: true
7052
- }
7053
- )
7827
+ () => this.bot.api.sendDocument(this.telegramConfig.chatId, inputFile, {
7828
+ message_thread_id: ctx.threadId
7829
+ })
7054
7830
  );
7055
7831
  }
7056
- break;
7832
+ } catch (err) {
7833
+ log23.error({ err, sessionId: ctx.sessionId, fileName: attachment.fileName }, "Failed to send attachment");
7057
7834
  }
7058
- case "error": {
7059
- await this.draftManager.finalize(sessionId, this.assistantSession?.id);
7060
- const tracker = this.sessionTrackers.get(sessionId);
7061
- if (tracker) {
7062
- tracker.destroy();
7063
- this.sessionTrackers.delete(sessionId);
7064
- }
7835
+ },
7836
+ onSessionEnd: async (ctx, _content) => {
7837
+ await this.draftManager.finalize(ctx.sessionId, this.assistantSession?.id);
7838
+ this.draftManager.cleanup(ctx.sessionId);
7839
+ this.toolTracker.cleanup(ctx.sessionId);
7840
+ await this.skillManager.cleanup(ctx.sessionId);
7841
+ const tracker = this.sessionTrackers.get(ctx.sessionId);
7842
+ if (tracker) {
7843
+ await tracker.onComplete();
7844
+ tracker.destroy();
7845
+ this.sessionTrackers.delete(ctx.sessionId);
7846
+ } else {
7065
7847
  await this.sendQueue.enqueue(
7066
7848
  () => this.bot.api.sendMessage(
7067
7849
  this.telegramConfig.chatId,
7068
- `\u274C <b>Error:</b> ${escapeHtml(content.text)}`,
7850
+ `\u2705 <b>Done</b>`,
7069
7851
  {
7070
- message_thread_id: threadId,
7852
+ message_thread_id: ctx.threadId,
7071
7853
  parse_mode: "HTML",
7072
7854
  disable_notification: true
7073
7855
  }
7074
7856
  )
7075
7857
  );
7076
- break;
7077
7858
  }
7078
- case "system_message": {
7079
- await this.sendQueue.enqueue(
7080
- () => this.bot.api.sendMessage(
7081
- this.telegramConfig.chatId,
7082
- escapeHtml(content.text),
7083
- {
7084
- message_thread_id: threadId,
7085
- parse_mode: "HTML",
7086
- disable_notification: true
7087
- }
7088
- )
7089
- );
7090
- break;
7859
+ },
7860
+ onError: async (ctx, content) => {
7861
+ await this.draftManager.finalize(ctx.sessionId, this.assistantSession?.id);
7862
+ const tracker = this.sessionTrackers.get(ctx.sessionId);
7863
+ if (tracker) {
7864
+ tracker.destroy();
7865
+ this.sessionTrackers.delete(ctx.sessionId);
7091
7866
  }
7867
+ await this.sendQueue.enqueue(
7868
+ () => this.bot.api.sendMessage(
7869
+ this.telegramConfig.chatId,
7870
+ `\u274C <b>Error:</b> ${escapeHtml(content.text)}`,
7871
+ {
7872
+ message_thread_id: ctx.threadId,
7873
+ parse_mode: "HTML",
7874
+ disable_notification: true
7875
+ }
7876
+ )
7877
+ );
7878
+ },
7879
+ onSystemMessage: async (ctx, content) => {
7880
+ await this.sendQueue.enqueue(
7881
+ () => this.bot.api.sendMessage(
7882
+ this.telegramConfig.chatId,
7883
+ escapeHtml(content.text),
7884
+ {
7885
+ message_thread_id: ctx.threadId,
7886
+ parse_mode: "HTML",
7887
+ disable_notification: true
7888
+ }
7889
+ )
7890
+ );
7092
7891
  }
7093
- }
7094
- async sendPermissionRequest(sessionId, request) {
7095
- log21.info({ sessionId, requestId: request.id }, "Permission request sent");
7892
+ };
7893
+ // --- ChannelAdapter implementations ---
7894
+ async sendMessage(sessionId, content) {
7895
+ if (this.assistantInitializing && sessionId === this.assistantSession?.id) return;
7096
7896
  const session = this.core.sessionManager.getSession(sessionId);
7097
7897
  if (!session) return;
7098
- if (request.description.includes("openacp")) {
7099
- const allowOption = request.options.find((o) => o.isAllow);
7100
- if (allowOption && session.permissionGate.requestId === request.id) {
7101
- log21.info({ sessionId, requestId: request.id }, "Auto-approving openacp command");
7102
- session.permissionGate.resolve(allowOption.id);
7103
- }
7104
- return;
7105
- }
7106
- if (session.dangerousMode) {
7107
- const allowOption = request.options.find((o) => o.isAllow);
7108
- if (allowOption && session.permissionGate.requestId === request.id) {
7109
- log21.info({ sessionId, requestId: request.id, optionId: allowOption.id }, "Dangerous mode: auto-approving permission");
7110
- session.permissionGate.resolve(allowOption.id);
7111
- }
7898
+ if (session.archiving) return;
7899
+ const threadId = Number(session.threadId);
7900
+ if (!threadId || isNaN(threadId)) {
7901
+ log23.warn({ sessionId, threadId: session.threadId }, "Session has no valid threadId, skipping message");
7112
7902
  return;
7113
7903
  }
7904
+ const ctx = { sessionId, threadId };
7905
+ await dispatchMessage(this.messageHandlers, ctx, content);
7906
+ }
7907
+ async sendPermissionRequest(sessionId, request) {
7908
+ log23.info({ sessionId, requestId: request.id }, "Permission request sent");
7909
+ const session = this.core.sessionManager.getSession(sessionId);
7910
+ if (!session) return;
7114
7911
  await this.sendQueue.enqueue(
7115
7912
  () => this.permissionHandler.sendPermissionRequest(session, request)
7116
7913
  );
7117
7914
  }
7118
7915
  async sendNotification(notification) {
7119
7916
  if (notification.sessionId === this.assistantSession?.id) return;
7120
- log21.info(
7917
+ log23.info(
7121
7918
  { sessionId: notification.sessionId, type: notification.type },
7122
7919
  "Notification sent"
7123
7920
  );
@@ -7153,7 +7950,7 @@ Task completed.
7153
7950
  );
7154
7951
  }
7155
7952
  async createSessionThread(sessionId, name) {
7156
- log21.info({ sessionId, name }, "Session topic created");
7953
+ log23.info({ sessionId, name }, "Session topic created");
7157
7954
  return String(
7158
7955
  await createSessionTopic(this.bot, this.telegramConfig.chatId, name)
7159
7956
  );
@@ -7177,7 +7974,7 @@ Task completed.
7177
7974
  try {
7178
7975
  await this.bot.api.deleteForumTopic(this.telegramConfig.chatId, topicId);
7179
7976
  } catch (err) {
7180
- log21.warn({ err, sessionId, topicId }, "Failed to delete forum topic (may already be deleted)");
7977
+ log23.warn({ err, sessionId, topicId }, "Failed to delete forum topic (may already be deleted)");
7181
7978
  }
7182
7979
  }
7183
7980
  async sendSkillCommands(sessionId, commands) {
@@ -7201,7 +7998,7 @@ Task completed.
7201
7998
  const buffer = Buffer.from(await response.arrayBuffer());
7202
7999
  return { buffer, filePath: file.file_path };
7203
8000
  } catch (err) {
7204
- log21.error({ err }, "Failed to download file from Telegram");
8001
+ log23.error({ err }, "Failed to download file from Telegram");
7205
8002
  return null;
7206
8003
  }
7207
8004
  }
@@ -7217,7 +8014,7 @@ Task completed.
7217
8014
  try {
7218
8015
  buffer = await this.fileService.convertOggToWav(buffer);
7219
8016
  } catch (err) {
7220
- log21.warn({ err }, "OGG\u2192WAV conversion failed, saving original OGG");
8017
+ log23.warn({ err }, "OGG\u2192WAV conversion failed, saving original OGG");
7221
8018
  fileName = "voice.ogg";
7222
8019
  mimeType = "audio/ogg";
7223
8020
  originalFilePath = void 0;
@@ -7243,7 +8040,7 @@ Task completed.
7243
8040
  userId: String(userId),
7244
8041
  text,
7245
8042
  attachments: [att]
7246
- }).catch((err) => log21.error({ err }, "handleMessage error"));
8043
+ }).catch((err) => log23.error({ err }, "handleMessage error"));
7247
8044
  }
7248
8045
  async cleanupSkillCommands(sessionId) {
7249
8046
  await this.skillManager.cleanup(sessionId);
@@ -7295,9 +8092,9 @@ export {
7295
8092
  nodeToWebWritable,
7296
8093
  nodeToWebReadable,
7297
8094
  StderrCapture,
8095
+ TypedEmitter,
7298
8096
  AgentInstance,
7299
8097
  AgentManager,
7300
- TypedEmitter,
7301
8098
  PromptQueue,
7302
8099
  PermissionGate,
7303
8100
  Session,
@@ -7308,11 +8105,16 @@ export {
7308
8105
  MessageTransformer,
7309
8106
  UsageStore,
7310
8107
  UsageBudget,
8108
+ SecurityGuard,
8109
+ SessionFactory,
8110
+ EventBus,
7311
8111
  SpeechService,
7312
8112
  GroqSTT,
7313
8113
  OpenACPCore,
8114
+ SSEManager,
8115
+ StaticServer,
7314
8116
  ApiServer,
7315
8117
  TopicManager,
7316
8118
  TelegramAdapter
7317
8119
  };
7318
- //# sourceMappingURL=chunk-R3UJUOXI.js.map
8120
+ //# sourceMappingURL=chunk-HP2IJYCA.js.map