@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.
- package/dist/{admin-IKPS5PFC.js → admin-3ZHEO5VP.js} +8 -4
- package/dist/{agent-catalog-T5ECPEDA.js → agent-catalog-IVU2KANH.js} +2 -2
- package/dist/{agents-55NX3DHM.js → agents-SXIY4IEF.js} +2 -2
- package/dist/{chunk-H7ZMPBZC.js → chunk-3KGRVAEV.js} +1 -1
- package/dist/chunk-3KGRVAEV.js.map +1 -0
- package/dist/{chunk-GINCOFNW.js → chunk-6LSFRNHE.js} +2 -2
- package/dist/chunk-6LSFRNHE.js.map +1 -0
- package/dist/{chunk-UB7XUO7C.js → chunk-AVCHZESZ.js} +3 -3
- package/dist/{chunk-J6X5SW6O.js → chunk-CKOK7JW6.js} +3 -2
- package/dist/{chunk-J6X5SW6O.js.map → chunk-CKOK7JW6.js.map} +1 -1
- package/dist/{chunk-4LFDEW22.js → chunk-EWYNCHUH.js} +33 -20
- package/dist/chunk-EWYNCHUH.js.map +1 -0
- package/dist/{chunk-4TR5Y3MP.js → chunk-F3AICYO4.js} +39 -1
- package/dist/chunk-F3AICYO4.js.map +1 -0
- package/dist/{chunk-LGQYTK55.js → chunk-FMWSVLRM.js} +31 -2
- package/dist/chunk-FMWSVLRM.js.map +1 -0
- package/dist/{chunk-AKIU4JBF.js → chunk-FZ5BIWG5.js} +6 -6
- package/dist/chunk-FZ5BIWG5.js.map +1 -0
- package/dist/{chunk-2KJC3ILH.js → chunk-G3OHCXZG.js} +34 -24
- package/dist/chunk-G3OHCXZG.js.map +1 -0
- package/dist/{chunk-R3UJUOXI.js → chunk-HP2IJYCA.js} +1817 -1015
- package/dist/chunk-HP2IJYCA.js.map +1 -0
- package/dist/chunk-IER5UCY7.js +298 -0
- package/dist/chunk-IER5UCY7.js.map +1 -0
- package/dist/{chunk-TOZQ3JFN.js → chunk-KO5RL7MZ.js} +2 -2
- package/dist/{chunk-7G5QKLLF.js → chunk-NXEQXRQR.js} +53 -9
- package/dist/chunk-NXEQXRQR.js.map +1 -0
- package/dist/{chunk-ZCHNAM3B.js → chunk-OHR6SBMC.js} +3 -3
- package/dist/chunk-PWFPTG5X.js +101 -0
- package/dist/chunk-PWFPTG5X.js.map +1 -0
- package/dist/{chunk-IMILOCR5.js → chunk-TMCQZAXN.js} +2 -2
- package/dist/chunk-TMCQZAXN.js.map +1 -0
- package/dist/{chunk-T22OLSET.js → chunk-TTDSLV35.js} +1 -1
- package/dist/chunk-TTDSLV35.js.map +1 -0
- package/dist/cli.js +41 -36
- package/dist/cli.js.map +1 -1
- package/dist/{config-AK2W3E67.js → config-4YSJ4NCI.js} +2 -2
- package/dist/{config-editor-VIA7A72X.js → config-editor-F25HEMGL.js} +4 -4
- package/dist/{config-registry-QQOJ2GQP.js → config-registry-7I6GGDOY.js} +2 -2
- package/dist/{daemon-G27YZUWB.js → daemon-I6XMRQ6P.js} +3 -3
- package/dist/{discord-2DKRH45T.js → discord-VHCBN3JJ.js} +226 -172
- package/dist/discord-VHCBN3JJ.js.map +1 -0
- package/dist/{doctor-CHCYUTV5.js → doctor-GPW5ECK6.js} +4 -4
- package/dist/doctor-Y3SCSVPI.js +9 -0
- package/dist/index.d.ts +176 -90
- package/dist/index.js +22 -12
- package/dist/install-cloudflared-G2GUKCHA.js +32 -0
- package/dist/install-cloudflared-G2GUKCHA.js.map +1 -0
- package/dist/install-jq-7QTU7XYY.js +31 -0
- package/dist/install-jq-7QTU7XYY.js.map +1 -0
- package/dist/{integrate-VOUYBPPZ.js → integrate-O4OCR4SN.js} +23 -11
- package/dist/integrate-O4OCR4SN.js.map +1 -0
- package/dist/{main-56SPFYW4.js → main-P4X6SAPZ.js} +29 -18
- package/dist/main-P4X6SAPZ.js.map +1 -0
- package/dist/{new-session-DRRP2J7E.js → new-session-PUNUHGYP.js} +3 -3
- package/dist/post-upgrade-6N4JCV5S.js +79 -0
- package/dist/post-upgrade-6N4JCV5S.js.map +1 -0
- package/dist/{session-FVFLBREJ.js → session-ZMAM67AA.js} +2 -2
- package/dist/{settings-LPOLJ6SA.js → settings-OEQEZS5Y.js} +3 -2
- package/dist/{setup-IPWJCIJM.js → setup-7YBFKRG7.js} +5 -3
- package/dist/{tunnel-service-U6V4HQOO.js → tunnel-service-BMIBHUBK.js} +35 -17
- package/dist/tunnel-service-BMIBHUBK.js.map +1 -0
- package/package.json +2 -1
- package/dist/chunk-2KJC3ILH.js.map +0 -1
- package/dist/chunk-4LFDEW22.js.map +0 -1
- package/dist/chunk-4TR5Y3MP.js.map +0 -1
- package/dist/chunk-7G5QKLLF.js.map +0 -1
- package/dist/chunk-AKIU4JBF.js.map +0 -1
- package/dist/chunk-GINCOFNW.js.map +0 -1
- package/dist/chunk-H7ZMPBZC.js.map +0 -1
- package/dist/chunk-IMILOCR5.js.map +0 -1
- package/dist/chunk-LGQYTK55.js.map +0 -1
- package/dist/chunk-R3UJUOXI.js.map +0 -1
- package/dist/chunk-RF3DUYFO.js +0 -103
- package/dist/chunk-RF3DUYFO.js.map +0 -1
- package/dist/chunk-T22OLSET.js.map +0 -1
- package/dist/chunk-THBR6OXH.js +0 -62
- package/dist/chunk-THBR6OXH.js.map +0 -1
- package/dist/discord-2DKRH45T.js.map +0 -1
- package/dist/doctor-AN6AZ3PF.js +0 -9
- package/dist/install-cloudflared-BTGUD7SW.js +0 -8
- package/dist/integrate-VOUYBPPZ.js.map +0 -1
- package/dist/main-56SPFYW4.js.map +0 -1
- package/dist/setup-IPWJCIJM.js.map +0 -1
- package/dist/tunnel-service-U6V4HQOO.js.map +0 -1
- /package/dist/{admin-IKPS5PFC.js.map → admin-3ZHEO5VP.js.map} +0 -0
- /package/dist/{agent-catalog-T5ECPEDA.js.map → agent-catalog-IVU2KANH.js.map} +0 -0
- /package/dist/{agents-55NX3DHM.js.map → agents-SXIY4IEF.js.map} +0 -0
- /package/dist/{chunk-UB7XUO7C.js.map → chunk-AVCHZESZ.js.map} +0 -0
- /package/dist/{chunk-TOZQ3JFN.js.map → chunk-KO5RL7MZ.js.map} +0 -0
- /package/dist/{chunk-ZCHNAM3B.js.map → chunk-OHR6SBMC.js.map} +0 -0
- /package/dist/{config-AK2W3E67.js.map → config-4YSJ4NCI.js.map} +0 -0
- /package/dist/{config-editor-VIA7A72X.js.map → config-editor-F25HEMGL.js.map} +0 -0
- /package/dist/{config-registry-QQOJ2GQP.js.map → config-registry-7I6GGDOY.js.map} +0 -0
- /package/dist/{daemon-G27YZUWB.js.map → daemon-I6XMRQ6P.js.map} +0 -0
- /package/dist/{doctor-AN6AZ3PF.js.map → doctor-GPW5ECK6.js.map} +0 -0
- /package/dist/{doctor-CHCYUTV5.js.map → doctor-Y3SCSVPI.js.map} +0 -0
- /package/dist/{install-cloudflared-BTGUD7SW.js.map → new-session-PUNUHGYP.js.map} +0 -0
- /package/dist/{new-session-DRRP2J7E.js.map → session-ZMAM67AA.js.map} +0 -0
- /package/dist/{session-FVFLBREJ.js.map → settings-OEQEZS5Y.js.map} +0 -0
- /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
|
-
|
|
3
|
+
PRODUCT_GUIDE,
|
|
4
|
+
dispatchMessage
|
|
5
|
+
} from "./chunk-FMWSVLRM.js";
|
|
5
6
|
import {
|
|
6
7
|
DoctorEngine
|
|
7
|
-
} from "./chunk-
|
|
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-
|
|
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-
|
|
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((
|
|
37
|
+
return new Promise((resolve3, reject) => {
|
|
37
38
|
nodeStream.write(Buffer.from(chunk), (err) => {
|
|
38
39
|
if (err) reject(err);
|
|
39
|
-
else
|
|
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
|
-
//
|
|
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((
|
|
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", () =>
|
|
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.
|
|
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.
|
|
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((
|
|
543
|
+
return new Promise((resolve3) => {
|
|
470
544
|
state.process.on("exit", (code, signal) => {
|
|
471
|
-
|
|
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((
|
|
652
|
-
this.queue.push({ text, attachments, resolve:
|
|
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((
|
|
718
|
-
this.resolveFn =
|
|
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
|
-
|
|
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
|
|
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.
|
|
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.
|
|
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.
|
|
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
|
|
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
|
-
|
|
1291
|
-
this.adapter.sendMessage(sid, {
|
|
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
|
|
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
|
-
|
|
1304
|
-
this.adapter.sendMessage(sid, {
|
|
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.
|
|
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
|
|
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
|
-
|
|
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/
|
|
1784
|
-
var
|
|
1785
|
-
constructor(
|
|
1786
|
-
this.
|
|
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
|
-
|
|
1816
|
-
const
|
|
1817
|
-
if (
|
|
1818
|
-
|
|
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
|
|
1821
|
-
if (
|
|
1822
|
-
|
|
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
|
|
1825
|
-
}
|
|
1826
|
-
updateConfig(config) {
|
|
1827
|
-
this.config = config;
|
|
1927
|
+
return { allowed: true };
|
|
1828
1928
|
}
|
|
1829
1929
|
};
|
|
1830
1930
|
|
|
1831
|
-
// src/core/
|
|
1832
|
-
|
|
1833
|
-
var
|
|
1834
|
-
|
|
1835
|
-
|
|
1836
|
-
this.
|
|
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
|
-
|
|
1839
|
-
|
|
1840
|
-
|
|
1841
|
-
|
|
1842
|
-
|
|
1843
|
-
|
|
1844
|
-
|
|
1845
|
-
|
|
1846
|
-
|
|
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
|
|
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
|
-
|
|
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
|
-
|
|
2251
|
+
log6.info({ count: this.records.size }, "Loaded session records");
|
|
1985
2252
|
} catch (err) {
|
|
1986
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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.
|
|
2055
|
-
|
|
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(
|
|
2060
|
-
|
|
2061
|
-
|
|
2062
|
-
|
|
2063
|
-
|
|
2064
|
-
|
|
2065
|
-
|
|
2066
|
-
|
|
2067
|
-
|
|
2068
|
-
|
|
2069
|
-
|
|
2070
|
-
|
|
2071
|
-
|
|
2072
|
-
|
|
2073
|
-
|
|
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
|
-
|
|
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")
|
|
2119
|
-
|
|
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)
|
|
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
|
-
|
|
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
|
-
|
|
2142
|
-
|
|
2143
|
-
|
|
2144
|
-
|
|
2145
|
-
|
|
2146
|
-
|
|
2147
|
-
|
|
2148
|
-
|
|
2149
|
-
|
|
2150
|
-
|
|
2151
|
-
|
|
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
|
-
|
|
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, {
|
|
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
|
|
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
|
-
|
|
2223
|
-
|
|
2224
|
-
|
|
2225
|
-
|
|
2226
|
-
|
|
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:
|
|
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
|
-
|
|
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
|
-
|
|
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 {
|
|
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 {
|
|
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 (!
|
|
2318
|
-
return {
|
|
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 {
|
|
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
|
-
|
|
2328
|
-
|
|
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:
|
|
2600
|
+
threadId: existingThreadId,
|
|
2342
2601
|
status: "existing"
|
|
2343
2602
|
};
|
|
2344
2603
|
}
|
|
2345
2604
|
}
|
|
2346
|
-
|
|
2347
|
-
if (
|
|
2348
|
-
|
|
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:
|
|
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(
|
|
2392
|
-
|
|
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
|
-
|
|
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
|
-
|
|
2419
|
-
{
|
|
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
|
-
|
|
2425
|
-
{
|
|
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
|
-
|
|
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
|
-
|
|
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/
|
|
2479
|
-
|
|
2480
|
-
|
|
2481
|
-
|
|
2482
|
-
|
|
2483
|
-
|
|
2484
|
-
|
|
2485
|
-
|
|
2486
|
-
|
|
2487
|
-
|
|
2488
|
-
|
|
2489
|
-
|
|
2490
|
-
|
|
2491
|
-
const
|
|
2492
|
-
|
|
2493
|
-
|
|
2494
|
-
|
|
2495
|
-
|
|
2496
|
-
|
|
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
|
-
|
|
2499
|
-
}
|
|
2500
|
-
|
|
2501
|
-
|
|
2502
|
-
|
|
2503
|
-
|
|
2504
|
-
|
|
2505
|
-
|
|
2506
|
-
|
|
2507
|
-
|
|
2508
|
-
|
|
2509
|
-
|
|
2510
|
-
|
|
2511
|
-
|
|
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
|
-
|
|
2516
|
-
|
|
2517
|
-
|
|
2518
|
-
|
|
2519
|
-
|
|
2520
|
-
|
|
2521
|
-
this.
|
|
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
|
-
|
|
2524
|
-
|
|
2525
|
-
|
|
2526
|
-
|
|
2527
|
-
|
|
2528
|
-
|
|
2529
|
-
|
|
2530
|
-
|
|
2531
|
-
|
|
2532
|
-
|
|
2533
|
-
|
|
2534
|
-
|
|
2535
|
-
|
|
2536
|
-
|
|
2537
|
-
|
|
2538
|
-
|
|
2539
|
-
|
|
2540
|
-
|
|
2541
|
-
|
|
2542
|
-
|
|
2543
|
-
|
|
2544
|
-
|
|
2545
|
-
|
|
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
|
-
|
|
2548
|
-
|
|
2549
|
-
resolve2();
|
|
2550
|
-
});
|
|
2551
|
-
});
|
|
2905
|
+
}
|
|
2906
|
+
}
|
|
2552
2907
|
}
|
|
2553
|
-
|
|
2554
|
-
this.
|
|
2555
|
-
|
|
2556
|
-
|
|
2557
|
-
|
|
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
|
-
|
|
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
|
-
|
|
2563
|
-
|
|
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
|
-
|
|
2566
|
-
|
|
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
|
-
|
|
2571
|
-
|
|
2572
|
-
fs6.unlinkSync(this.portFilePath);
|
|
2573
|
-
} catch {
|
|
2574
|
-
}
|
|
2960
|
+
put(path8, handler) {
|
|
2961
|
+
this.add("PUT", path8, handler);
|
|
2575
2962
|
}
|
|
2576
|
-
|
|
2577
|
-
|
|
2578
|
-
|
|
2579
|
-
|
|
2580
|
-
|
|
2581
|
-
|
|
2582
|
-
|
|
2583
|
-
|
|
2584
|
-
|
|
2585
|
-
|
|
2586
|
-
|
|
2587
|
-
|
|
2588
|
-
|
|
2589
|
-
|
|
2590
|
-
|
|
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
|
-
|
|
2979
|
+
return { handler: route.handler, params };
|
|
2593
2980
|
}
|
|
2594
|
-
|
|
2595
|
-
fs6.writeFileSync(this.secretFilePath, this.secret, { mode: 384 });
|
|
2981
|
+
return null;
|
|
2596
2982
|
}
|
|
2597
|
-
|
|
2598
|
-
const
|
|
2599
|
-
|
|
2600
|
-
|
|
2601
|
-
|
|
2602
|
-
|
|
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
|
-
|
|
2605
|
-
|
|
2606
|
-
|
|
2607
|
-
|
|
2608
|
-
|
|
2609
|
-
|
|
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
|
-
|
|
2614
|
-
|
|
2615
|
-
|
|
2616
|
-
|
|
2617
|
-
|
|
2618
|
-
|
|
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
|
-
|
|
2679
|
-
|
|
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
|
-
|
|
3097
|
+
deps.sendJson(res, 400, { error: "Invalid JSON body" });
|
|
2689
3098
|
return;
|
|
2690
3099
|
}
|
|
2691
3100
|
}
|
|
2692
|
-
const config =
|
|
2693
|
-
const activeSessions =
|
|
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
|
-
|
|
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] =
|
|
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 =
|
|
3115
|
+
const resolvedWorkspace = deps.core.configManager.resolveWorkspace(
|
|
2704
3116
|
workspace || config.agents[resolvedAgent]?.workingDirectory
|
|
2705
3117
|
);
|
|
2706
|
-
const session = await
|
|
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
|
-
|
|
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(
|
|
2721
|
-
|
|
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
|
-
|
|
2729
|
-
const
|
|
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
|
-
|
|
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
|
-
|
|
3157
|
+
deps.sendJson(res, 400, { error: `Session is ${session.status}` });
|
|
2736
3158
|
return;
|
|
2737
3159
|
}
|
|
2738
|
-
const body = await
|
|
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
|
-
|
|
3167
|
+
deps.sendJson(res, 400, { error: "Invalid JSON body" });
|
|
2746
3168
|
return;
|
|
2747
3169
|
}
|
|
2748
3170
|
}
|
|
2749
3171
|
if (!prompt) {
|
|
2750
|
-
|
|
3172
|
+
deps.sendJson(res, 400, { error: "Missing prompt" });
|
|
2751
3173
|
return;
|
|
2752
3174
|
}
|
|
2753
3175
|
session.enqueuePrompt(prompt).catch(() => {
|
|
2754
3176
|
});
|
|
2755
|
-
|
|
2756
|
-
|
|
2757
|
-
|
|
2758
|
-
|
|
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
|
-
|
|
3256
|
+
deps.sendJson(res, 404, { error: `Session "${sessionId}" not found` });
|
|
2761
3257
|
return;
|
|
2762
3258
|
}
|
|
2763
|
-
|
|
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
|
|
2781
|
-
const
|
|
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
|
-
|
|
3289
|
+
deps.sendJson(res, 404, { error: `Session "${sessionId}" not found` });
|
|
2784
3290
|
return;
|
|
2785
3291
|
}
|
|
2786
|
-
|
|
2787
|
-
|
|
2788
|
-
|
|
2789
|
-
|
|
2790
|
-
|
|
2791
|
-
|
|
2792
|
-
|
|
2793
|
-
|
|
2794
|
-
|
|
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
|
-
|
|
2806
|
-
|
|
2807
|
-
|
|
2808
|
-
const
|
|
2809
|
-
const
|
|
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
|
-
|
|
2844
|
-
}
|
|
2845
|
-
async
|
|
2846
|
-
const config =
|
|
2847
|
-
|
|
2848
|
-
}
|
|
2849
|
-
async
|
|
2850
|
-
const body = await
|
|
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
|
-
|
|
3372
|
+
deps.sendJson(res, 400, { error: "Invalid JSON body" });
|
|
2860
3373
|
return;
|
|
2861
3374
|
}
|
|
2862
3375
|
}
|
|
2863
3376
|
if (!configPath) {
|
|
2864
|
-
|
|
3377
|
+
deps.sendJson(res, 400, { error: "Missing path" });
|
|
2865
3378
|
return;
|
|
2866
3379
|
}
|
|
2867
|
-
const
|
|
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
|
-
|
|
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-
|
|
3411
|
+
const { ConfigSchema } = await import("./config-4YSJ4NCI.js");
|
|
2886
3412
|
const result = ConfigSchema.safeParse(cloned);
|
|
2887
3413
|
if (!result.success) {
|
|
2888
|
-
|
|
3414
|
+
deps.sendJson(res, 400, {
|
|
2889
3415
|
error: "Validation failed",
|
|
2890
|
-
details: result.error.issues.map((i) => ({
|
|
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
|
|
2902
|
-
const { isHotReloadable: isHotReloadable2 } = await import("./config-registry-
|
|
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
|
-
|
|
3433
|
+
deps.sendJson(res, 200, {
|
|
2905
3434
|
ok: true,
|
|
2906
3435
|
needsRestart,
|
|
2907
|
-
config: redactConfig(
|
|
3436
|
+
config: redactConfig(deps.core.configManager.get())
|
|
2908
3437
|
});
|
|
2909
|
-
}
|
|
2910
|
-
|
|
2911
|
-
|
|
2912
|
-
|
|
2913
|
-
|
|
2914
|
-
|
|
2915
|
-
|
|
2916
|
-
|
|
2917
|
-
|
|
2918
|
-
|
|
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
|
-
|
|
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
|
-
|
|
3511
|
+
deps.sendJson(res, 200, { enabled: false });
|
|
2923
3512
|
}
|
|
2924
|
-
}
|
|
2925
|
-
async
|
|
2926
|
-
const tunnel =
|
|
3513
|
+
});
|
|
3514
|
+
router.get("/api/tunnel/list", async (_req, res) => {
|
|
3515
|
+
const tunnel = deps.core.tunnelService;
|
|
2927
3516
|
if (!tunnel) {
|
|
2928
|
-
|
|
3517
|
+
deps.sendJson(res, 200, []);
|
|
2929
3518
|
return;
|
|
2930
3519
|
}
|
|
2931
|
-
|
|
2932
|
-
}
|
|
2933
|
-
async
|
|
2934
|
-
const tunnel =
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
3546
|
+
deps.sendJson(res, 200, entry);
|
|
2952
3547
|
} catch (err) {
|
|
2953
|
-
|
|
3548
|
+
deps.sendJson(res, 400, { error: err.message });
|
|
2954
3549
|
}
|
|
2955
|
-
}
|
|
2956
|
-
async
|
|
2957
|
-
const tunnel =
|
|
3550
|
+
});
|
|
3551
|
+
router.delete("/api/tunnel/:port", async (_req, res, params) => {
|
|
3552
|
+
const tunnel = deps.core.tunnelService;
|
|
2958
3553
|
if (!tunnel) {
|
|
2959
|
-
|
|
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
|
-
|
|
3560
|
+
deps.sendJson(res, 200, { ok: true });
|
|
2965
3561
|
} catch (err) {
|
|
2966
|
-
|
|
3562
|
+
deps.sendJson(res, 400, { error: err.message });
|
|
2967
3563
|
}
|
|
2968
|
-
}
|
|
2969
|
-
async
|
|
2970
|
-
const tunnel =
|
|
3564
|
+
});
|
|
3565
|
+
router.delete("/api/tunnel", async (_req, res) => {
|
|
3566
|
+
const tunnel = deps.core.tunnelService;
|
|
2971
3567
|
if (!tunnel) {
|
|
2972
|
-
|
|
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
|
-
|
|
2978
|
-
}
|
|
2979
|
-
|
|
2980
|
-
|
|
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
|
-
|
|
3600
|
+
deps.sendJson(res, 400, { error: "Invalid JSON body" });
|
|
2988
3601
|
return;
|
|
2989
3602
|
}
|
|
2990
3603
|
}
|
|
2991
3604
|
if (!message) {
|
|
2992
|
-
|
|
3605
|
+
deps.sendJson(res, 400, { error: "Missing message" });
|
|
2993
3606
|
return;
|
|
2994
3607
|
}
|
|
2995
|
-
await
|
|
3608
|
+
await deps.core.notificationManager.notifyAll({
|
|
2996
3609
|
sessionId: "system",
|
|
2997
3610
|
type: "completed",
|
|
2998
3611
|
summary: message
|
|
2999
3612
|
});
|
|
3000
|
-
|
|
3001
|
-
}
|
|
3002
|
-
|
|
3003
|
-
|
|
3004
|
-
|
|
3005
|
-
|
|
3006
|
-
|
|
3007
|
-
|
|
3008
|
-
|
|
3009
|
-
|
|
3010
|
-
|
|
3011
|
-
const
|
|
3012
|
-
|
|
3013
|
-
|
|
3014
|
-
|
|
3015
|
-
|
|
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
|
-
|
|
3019
|
-
|
|
3020
|
-
|
|
3021
|
-
|
|
3022
|
-
|
|
3023
|
-
|
|
3024
|
-
|
|
3025
|
-
this.
|
|
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
|
-
|
|
3028
|
-
|
|
3029
|
-
|
|
3030
|
-
|
|
3031
|
-
|
|
3032
|
-
|
|
3033
|
-
|
|
3034
|
-
|
|
3035
|
-
|
|
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
|
|
3040
|
-
|
|
3041
|
-
|
|
3042
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
3063
|
-
const
|
|
3064
|
-
|
|
3065
|
-
|
|
3066
|
-
|
|
3067
|
-
|
|
3068
|
-
|
|
3069
|
-
|
|
3070
|
-
|
|
3071
|
-
|
|
3072
|
-
|
|
3073
|
-
|
|
3074
|
-
|
|
3075
|
-
|
|
3076
|
-
|
|
3077
|
-
|
|
3078
|
-
|
|
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
|
-
|
|
3081
|
-
|
|
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
|
-
|
|
3087
|
-
|
|
3088
|
-
|
|
3089
|
-
|
|
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
|
-
|
|
3092
|
-
|
|
3093
|
-
|
|
3094
|
-
|
|
3095
|
-
|
|
3096
|
-
|
|
3097
|
-
|
|
3098
|
-
|
|
3099
|
-
|
|
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
|
|
3105
|
-
|
|
3106
|
-
|
|
3107
|
-
|
|
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
|
-
|
|
3110
|
-
|
|
3111
|
-
|
|
3112
|
-
|
|
3113
|
-
|
|
3114
|
-
|
|
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
|
-
|
|
3118
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
|
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
|
-
|
|
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:
|
|
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
|
-
|
|
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:
|
|
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
|
|
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
|
-
|
|
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:
|
|
4524
|
+
reply_markup: buildSessionControlKeyboard(session.id, false, false)
|
|
3741
4525
|
}
|
|
3742
4526
|
);
|
|
3743
|
-
session.warmup().catch((err) =>
|
|
4527
|
+
session.warmup().catch((err) => log13.error({ err }, "Warm-up error"));
|
|
3744
4528
|
return threadId ?? null;
|
|
3745
4529
|
} catch (err) {
|
|
3746
|
-
|
|
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:
|
|
4605
|
+
reply_markup: buildSessionControlKeyboard(session.id, false, false)
|
|
3822
4606
|
}
|
|
3823
4607
|
);
|
|
3824
|
-
session.warmup().catch((err) =>
|
|
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) =>
|
|
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
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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-
|
|
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-
|
|
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-
|
|
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-
|
|
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-
|
|
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
|
|
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
|
-
|
|
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
|
-
|
|
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
|
|
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
|
-
|
|
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
|
-
|
|
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
|
|
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
|
|
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
|
-
|
|
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
|
|
6189
|
+
var log19 = createChildLogger({ module: "telegram-assistant" });
|
|
5404
6190
|
async function spawnAssistant(core, adapter, assistantTopicId) {
|
|
5405
6191
|
const config = core.configManager.get();
|
|
5406
|
-
|
|
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
|
-
|
|
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
|
-
|
|
6222
|
+
log19.info({ sessionId: session.id }, "Assistant system prompt completed");
|
|
5437
6223
|
}).catch((err) => {
|
|
5438
|
-
|
|
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
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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((
|
|
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:
|
|
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:
|
|
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
|
|
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
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
|
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
|
-
|
|
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
|
|
6526
|
-
|
|
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
|
|
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, {
|
|
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
|
-
|
|
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
|
-
|
|
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: () =>
|
|
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
|
-
|
|
7576
|
+
log23.warn({ err }, "Failed to send welcome message");
|
|
6760
7577
|
}
|
|
6761
7578
|
try {
|
|
6762
|
-
|
|
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
|
-
|
|
7587
|
+
log23.info({ sessionId: session.id }, "Assistant session ready, system prompt running in background");
|
|
6771
7588
|
ready.then(() => {
|
|
6772
7589
|
this.assistantInitializing = false;
|
|
6773
|
-
|
|
7590
|
+
log23.info({ sessionId: session.id }, "Assistant ready for user messages");
|
|
6774
7591
|
});
|
|
6775
7592
|
} catch (err) {
|
|
6776
|
-
|
|
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
|
-
|
|
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) =>
|
|
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) =>
|
|
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
|
-
// ---
|
|
6908
|
-
|
|
6909
|
-
|
|
6910
|
-
|
|
6911
|
-
|
|
6912
|
-
|
|
6913
|
-
|
|
6914
|
-
|
|
6915
|
-
|
|
6916
|
-
|
|
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
|
-
|
|
6947
|
-
|
|
6948
|
-
|
|
6949
|
-
|
|
6950
|
-
|
|
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
|
-
|
|
6955
|
-
|
|
6956
|
-
|
|
6957
|
-
|
|
6958
|
-
|
|
6959
|
-
|
|
6960
|
-
|
|
6961
|
-
|
|
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
|
-
|
|
7804
|
+
return;
|
|
6965
7805
|
}
|
|
6966
|
-
|
|
6967
|
-
const
|
|
6968
|
-
|
|
6969
|
-
|
|
6970
|
-
|
|
6971
|
-
|
|
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
|
-
)
|
|
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.
|
|
7000
|
-
|
|
7001
|
-
|
|
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
|
-
|
|
7006
|
-
|
|
7007
|
-
|
|
7008
|
-
|
|
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.
|
|
7046
|
-
|
|
7047
|
-
|
|
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
|
-
|
|
7832
|
+
} catch (err) {
|
|
7833
|
+
log23.error({ err, sessionId: ctx.sessionId, fileName: attachment.fileName }, "Failed to send attachment");
|
|
7057
7834
|
}
|
|
7058
|
-
|
|
7059
|
-
|
|
7060
|
-
|
|
7061
|
-
|
|
7062
|
-
|
|
7063
|
-
|
|
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
|
-
`\
|
|
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
|
-
|
|
7079
|
-
|
|
7080
|
-
|
|
7081
|
-
|
|
7082
|
-
|
|
7083
|
-
|
|
7084
|
-
|
|
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
|
-
|
|
7095
|
-
|
|
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 (
|
|
7099
|
-
|
|
7100
|
-
|
|
7101
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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) =>
|
|
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-
|
|
8120
|
+
//# sourceMappingURL=chunk-HP2IJYCA.js.map
|