@openacp/cli 0.6.1 → 0.6.2
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/{admin-IKPS5PFC.js → admin-IVQTC72V.js} +2 -2
- 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-UB7XUO7C.js → chunk-2OFIWTYD.js} +3 -3
- package/dist/{chunk-H7ZMPBZC.js → chunk-3KGRVAEV.js} +1 -1
- package/dist/chunk-3KGRVAEV.js.map +1 -0
- package/dist/{chunk-2KJC3ILH.js → chunk-7WQH4SOY.js} +4 -4
- package/dist/{chunk-2KJC3ILH.js.map → chunk-7WQH4SOY.js.map} +1 -1
- 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-GINCOFNW.js → chunk-FF4C3ZE4.js} +2 -2
- package/dist/chunk-FF4C3ZE4.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-THBR6OXH.js → chunk-HSGUPJU5.js} +1 -1
- package/dist/chunk-HSGUPJU5.js.map +1 -0
- package/dist/{chunk-R3UJUOXI.js → chunk-IFTYEG5J.js} +1805 -1003
- package/dist/chunk-IFTYEG5J.js.map +1 -0
- package/dist/{chunk-7G5QKLLF.js → chunk-IUIMBEBX.js} +1 -1
- package/dist/chunk-IUIMBEBX.js.map +1 -0
- package/dist/{chunk-TOZQ3JFN.js → chunk-KO5RL7MZ.js} +2 -2
- package/dist/{chunk-AKIU4JBF.js → chunk-ONENT7JQ.js} +2 -2
- package/dist/chunk-ONENT7JQ.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/{chunk-ZCHNAM3B.js → chunk-WQZ4RXH7.js} +2 -2
- 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-TOZUBMO7.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-7B5NWW5Z.js} +154 -156
- package/dist/discord-7B5NWW5Z.js.map +1 -0
- package/dist/{doctor-CHCYUTV5.js → doctor-FR5GASOQ.js} +4 -4
- package/dist/doctor-UOH7YCT2.js +9 -0
- package/dist/index.d.ts +176 -90
- package/dist/index.js +20 -10
- package/dist/{integrate-VOUYBPPZ.js → integrate-QTK4PPYQ.js} +15 -6
- package/dist/integrate-QTK4PPYQ.js.map +1 -0
- package/dist/{main-56SPFYW4.js → main-46BVXFWI.js} +19 -18
- package/dist/main-46BVXFWI.js.map +1 -0
- package/dist/{new-session-DRRP2J7E.js → new-session-BVNE6S3A.js} +3 -3
- package/dist/{session-FVFLBREJ.js → session-ZMAM67AA.js} +2 -2
- package/dist/{settings-LPOLJ6SA.js → settings-LQ57CFY4.js} +2 -2
- package/dist/{setup-IPWJCIJM.js → setup-DZXZTQRD.js} +3 -3
- package/dist/{tunnel-service-U6V4HQOO.js → tunnel-service-O5EKGFLO.js} +19 -1
- package/dist/tunnel-service-O5EKGFLO.js.map +1 -0
- package/package.json +2 -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-T22OLSET.js.map +0 -1
- 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/integrate-VOUYBPPZ.js.map +0 -1
- package/dist/main-56SPFYW4.js.map +0 -1
- package/dist/tunnel-service-U6V4HQOO.js.map +0 -1
- /package/dist/{admin-IKPS5PFC.js.map → admin-IVQTC72V.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-2OFIWTYD.js.map} +0 -0
- /package/dist/{chunk-TOZQ3JFN.js.map → chunk-KO5RL7MZ.js.map} +0 -0
- /package/dist/{chunk-ZCHNAM3B.js.map → chunk-WQZ4RXH7.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-TOZUBMO7.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-FR5GASOQ.js.map} +0 -0
- /package/dist/{doctor-CHCYUTV5.js.map → doctor-UOH7YCT2.js.map} +0 -0
- /package/dist/{new-session-DRRP2J7E.js.map → new-session-BVNE6S3A.js.map} +0 -0
- /package/dist/{session-FVFLBREJ.js.map → session-ZMAM67AA.js.map} +0 -0
- /package/dist/{settings-LPOLJ6SA.js.map → settings-LQ57CFY4.js.map} +0 -0
- /package/dist/{setup-IPWJCIJM.js.map → setup-DZXZTQRD.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-WQZ4RXH7.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,66 @@ 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: null, providers: {} }
|
|
2332
|
+
};
|
|
2056
2333
|
this.speechService = new SpeechService(speechConfig);
|
|
2057
2334
|
const groqConfig = speechConfig.stt?.providers?.groq;
|
|
2058
2335
|
if (groqConfig?.apiKey) {
|
|
2059
|
-
this.speechService.registerSTTProvider(
|
|
2060
|
-
|
|
2061
|
-
|
|
2062
|
-
|
|
2063
|
-
|
|
2064
|
-
|
|
2065
|
-
|
|
2066
|
-
|
|
2067
|
-
|
|
2068
|
-
|
|
2069
|
-
|
|
2070
|
-
|
|
2071
|
-
|
|
2072
|
-
|
|
2073
|
-
|
|
2336
|
+
this.speechService.registerSTTProvider(
|
|
2337
|
+
"groq",
|
|
2338
|
+
new GroqSTT(groqConfig.apiKey, groqConfig.model)
|
|
2339
|
+
);
|
|
2340
|
+
}
|
|
2341
|
+
if (speechConfig.tts?.provider === "edge-tts") {
|
|
2342
|
+
const edgeConfig = speechConfig.tts.providers?.["edge-tts"];
|
|
2343
|
+
const voice = edgeConfig?.voice;
|
|
2344
|
+
this.speechService.registerTTSProvider("edge-tts", new EdgeTTS(voice));
|
|
2345
|
+
}
|
|
2346
|
+
this.sessionFactory = new SessionFactory(
|
|
2347
|
+
this.agentManager,
|
|
2348
|
+
this.sessionManager,
|
|
2349
|
+
this.speechService,
|
|
2350
|
+
this.eventBus
|
|
2351
|
+
);
|
|
2352
|
+
this.configManager.on(
|
|
2353
|
+
"config:changed",
|
|
2354
|
+
async ({ path: configPath, value }) => {
|
|
2355
|
+
if (configPath === "logging.level" && typeof value === "string") {
|
|
2356
|
+
const { setLogLevel: setLogLevel2 } = await import("./log-SPS2S6FO.js");
|
|
2357
|
+
setLogLevel2(value);
|
|
2358
|
+
log7.info({ level: value }, "Log level changed at runtime");
|
|
2359
|
+
}
|
|
2360
|
+
if (configPath.startsWith("speech.")) {
|
|
2361
|
+
const newConfig = this.configManager.get();
|
|
2362
|
+
const newSpeechConfig = newConfig.speech ?? {
|
|
2363
|
+
stt: { provider: null, providers: {} },
|
|
2364
|
+
tts: { provider: null, providers: {} }
|
|
2365
|
+
};
|
|
2366
|
+
this.speechService.updateConfig(newSpeechConfig);
|
|
2367
|
+
const groqCfg = newSpeechConfig.stt?.providers?.groq;
|
|
2368
|
+
if (groqCfg?.apiKey) {
|
|
2369
|
+
this.speechService.registerSTTProvider(
|
|
2370
|
+
"groq",
|
|
2371
|
+
new GroqSTT(groqCfg.apiKey, groqCfg.model)
|
|
2372
|
+
);
|
|
2373
|
+
}
|
|
2374
|
+
const ttsCfg = newSpeechConfig.tts;
|
|
2375
|
+
if (ttsCfg?.provider === "edge-tts") {
|
|
2376
|
+
const edgeConfig = ttsCfg.providers?.["edge-tts"];
|
|
2377
|
+
const voice = edgeConfig?.voice;
|
|
2378
|
+
this.speechService.registerTTSProvider("edge-tts", new EdgeTTS(voice));
|
|
2379
|
+
}
|
|
2380
|
+
log7.info("Speech service config updated at runtime");
|
|
2074
2381
|
}
|
|
2075
|
-
log6.info("Speech service config updated at runtime");
|
|
2076
2382
|
}
|
|
2077
|
-
|
|
2383
|
+
);
|
|
2078
2384
|
}
|
|
2079
2385
|
get tunnelService() {
|
|
2080
2386
|
return this._tunnelService;
|
|
@@ -2088,7 +2394,7 @@ var OpenACPCore = class {
|
|
|
2088
2394
|
}
|
|
2089
2395
|
async start() {
|
|
2090
2396
|
this.agentCatalog.refreshRegistryIfStale().catch((err) => {
|
|
2091
|
-
|
|
2397
|
+
log7.warn({ err }, "Background registry refresh failed");
|
|
2092
2398
|
});
|
|
2093
2399
|
for (const adapter of this.adapters.values()) {
|
|
2094
2400
|
await adapter.start();
|
|
@@ -2115,13 +2421,16 @@ var OpenACPCore = class {
|
|
|
2115
2421
|
async archiveSession(sessionId) {
|
|
2116
2422
|
const session = this.sessionManager.getSession(sessionId);
|
|
2117
2423
|
if (!session) return { ok: false, error: "Session not found" };
|
|
2118
|
-
if (session.status === "initializing")
|
|
2119
|
-
|
|
2424
|
+
if (session.status === "initializing")
|
|
2425
|
+
return { ok: false, error: "Session is still initializing" };
|
|
2426
|
+
if (session.status !== "active")
|
|
2427
|
+
return { ok: false, error: `Session is ${session.status}` };
|
|
2120
2428
|
const adapter = this.adapters.get(session.channelId);
|
|
2121
2429
|
if (!adapter) return { ok: false, error: "Adapter not found for session" };
|
|
2122
2430
|
try {
|
|
2123
2431
|
const result = await adapter.archiveSessionTopic(session.id);
|
|
2124
|
-
if (!result)
|
|
2432
|
+
if (!result)
|
|
2433
|
+
return { ok: false, error: "Adapter does not support archiving" };
|
|
2125
2434
|
return { ok: true, newThreadId: result.newThreadId };
|
|
2126
2435
|
} catch (err) {
|
|
2127
2436
|
return { ok: false, error: err.message };
|
|
@@ -2129,8 +2438,7 @@ var OpenACPCore = class {
|
|
|
2129
2438
|
}
|
|
2130
2439
|
// --- Message Routing ---
|
|
2131
2440
|
async handleMessage(message) {
|
|
2132
|
-
|
|
2133
|
-
log6.debug(
|
|
2441
|
+
log7.debug(
|
|
2134
2442
|
{
|
|
2135
2443
|
channelId: message.channelId,
|
|
2136
2444
|
threadId: message.threadId,
|
|
@@ -2138,32 +2446,17 @@ var OpenACPCore = class {
|
|
|
2138
2446
|
},
|
|
2139
2447
|
"Incoming message"
|
|
2140
2448
|
);
|
|
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
|
-
});
|
|
2449
|
+
const access = this.securityGuard.checkAccess(message);
|
|
2450
|
+
if (!access.allowed) {
|
|
2451
|
+
log7.warn({ userId: message.userId, reason: access.reason }, "Access denied");
|
|
2452
|
+
if (access.reason.includes("Session limit")) {
|
|
2453
|
+
const adapter = this.adapters.get(message.channelId);
|
|
2454
|
+
if (adapter) {
|
|
2455
|
+
await adapter.sendMessage(message.threadId, {
|
|
2456
|
+
type: "error",
|
|
2457
|
+
text: `\u26A0\uFE0F ${access.reason}. Please cancel existing sessions with /cancel before starting new ones.`
|
|
2458
|
+
});
|
|
2459
|
+
}
|
|
2167
2460
|
}
|
|
2168
2461
|
return;
|
|
2169
2462
|
}
|
|
@@ -2175,38 +2468,20 @@ var OpenACPCore = class {
|
|
|
2175
2468
|
session = await this.lazyResume(message) ?? void 0;
|
|
2176
2469
|
}
|
|
2177
2470
|
if (!session) {
|
|
2178
|
-
|
|
2471
|
+
log7.warn(
|
|
2179
2472
|
{ channelId: message.channelId, threadId: message.threadId },
|
|
2180
2473
|
"No session found for thread (in-memory miss + lazy resume returned null)"
|
|
2181
2474
|
);
|
|
2182
2475
|
return;
|
|
2183
2476
|
}
|
|
2184
|
-
this.sessionManager.patchRecord(session.id, {
|
|
2477
|
+
this.sessionManager.patchRecord(session.id, {
|
|
2478
|
+
lastActiveAt: (/* @__PURE__ */ new Date()).toISOString()
|
|
2479
|
+
});
|
|
2185
2480
|
await session.enqueuePrompt(message.text, message.attachments);
|
|
2186
2481
|
}
|
|
2187
2482
|
// --- Unified Session Creation Pipeline ---
|
|
2188
2483
|
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);
|
|
2484
|
+
const session = await this.sessionFactory.create(params);
|
|
2210
2485
|
const adapter = this.adapters.get(params.channelId);
|
|
2211
2486
|
if (params.createThread && adapter) {
|
|
2212
2487
|
const threadId = await adapter.createSessionThread(
|
|
@@ -2219,47 +2494,11 @@ var OpenACPCore = class {
|
|
|
2219
2494
|
const bridge = this.createBridge(session, adapter);
|
|
2220
2495
|
bridge.connect();
|
|
2221
2496
|
}
|
|
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
|
-
}
|
|
2497
|
+
this.sessionFactory.wireSideEffects(session, {
|
|
2498
|
+
usageStore: this.usageStore,
|
|
2499
|
+
usageBudget: this.usageBudget,
|
|
2500
|
+
notificationManager: this.notificationManager,
|
|
2501
|
+
tunnelService: this._tunnelService
|
|
2263
2502
|
});
|
|
2264
2503
|
const existingRecord = this.sessionStore?.get(session.id);
|
|
2265
2504
|
const platform = {
|
|
@@ -2274,7 +2513,7 @@ var OpenACPCore = class {
|
|
|
2274
2513
|
}
|
|
2275
2514
|
await this.sessionManager.patchRecord(session.id, {
|
|
2276
2515
|
sessionId: session.id,
|
|
2277
|
-
agentSessionId:
|
|
2516
|
+
agentSessionId: session.agentSessionId,
|
|
2278
2517
|
agentName: params.agentName,
|
|
2279
2518
|
workingDir: params.workingDirectory,
|
|
2280
2519
|
channelId: params.channelId,
|
|
@@ -2284,7 +2523,7 @@ var OpenACPCore = class {
|
|
|
2284
2523
|
name: session.name,
|
|
2285
2524
|
platform
|
|
2286
2525
|
});
|
|
2287
|
-
|
|
2526
|
+
log7.info(
|
|
2288
2527
|
{ sessionId: session.id, agentName: params.agentName },
|
|
2289
2528
|
"Session created via pipeline"
|
|
2290
2529
|
);
|
|
@@ -2293,7 +2532,7 @@ var OpenACPCore = class {
|
|
|
2293
2532
|
async handleNewSession(channelId, agentName, workspacePath) {
|
|
2294
2533
|
const config = this.configManager.get();
|
|
2295
2534
|
const resolvedAgent = agentName || config.defaultAgent;
|
|
2296
|
-
|
|
2535
|
+
log7.info({ channelId, agentName: resolvedAgent }, "New session request");
|
|
2297
2536
|
const agentDef = this.agentCatalog.resolve(resolvedAgent);
|
|
2298
2537
|
const resolvedWorkspace = this.configManager.resolveWorkspace(
|
|
2299
2538
|
workspacePath || agentDef?.workingDirectory
|
|
@@ -2304,28 +2543,46 @@ var OpenACPCore = class {
|
|
|
2304
2543
|
workingDirectory: resolvedWorkspace
|
|
2305
2544
|
});
|
|
2306
2545
|
}
|
|
2307
|
-
async adoptSession(agentName, agentSessionId, cwd) {
|
|
2546
|
+
async adoptSession(agentName, agentSessionId, cwd, channelId) {
|
|
2308
2547
|
const caps = getAgentCapabilities(agentName);
|
|
2309
2548
|
if (!caps.supportsResume) {
|
|
2310
|
-
return {
|
|
2549
|
+
return {
|
|
2550
|
+
ok: false,
|
|
2551
|
+
error: "agent_not_supported",
|
|
2552
|
+
message: `Agent '${agentName}' does not support session resume`
|
|
2553
|
+
};
|
|
2311
2554
|
}
|
|
2312
2555
|
const agentDef = this.agentManager.getAgent(agentName);
|
|
2313
2556
|
if (!agentDef) {
|
|
2314
|
-
return {
|
|
2557
|
+
return {
|
|
2558
|
+
ok: false,
|
|
2559
|
+
error: "agent_not_supported",
|
|
2560
|
+
message: `Agent '${agentName}' not found`
|
|
2561
|
+
};
|
|
2315
2562
|
}
|
|
2316
|
-
const { existsSync } = await import("fs");
|
|
2317
|
-
if (!
|
|
2318
|
-
return {
|
|
2563
|
+
const { existsSync: existsSync2 } = await import("fs");
|
|
2564
|
+
if (!existsSync2(cwd)) {
|
|
2565
|
+
return {
|
|
2566
|
+
ok: false,
|
|
2567
|
+
error: "invalid_cwd",
|
|
2568
|
+
message: `Directory does not exist: ${cwd}`
|
|
2569
|
+
};
|
|
2319
2570
|
}
|
|
2320
2571
|
const maxSessions = this.configManager.get().security.maxConcurrentSessions;
|
|
2321
2572
|
if (this.sessionManager.listSessions().length >= maxSessions) {
|
|
2322
|
-
return {
|
|
2573
|
+
return {
|
|
2574
|
+
ok: false,
|
|
2575
|
+
error: "session_limit",
|
|
2576
|
+
message: "Maximum concurrent sessions reached"
|
|
2577
|
+
};
|
|
2323
2578
|
}
|
|
2324
2579
|
const existingRecord = this.sessionManager.getRecordByAgentSessionId(agentSessionId);
|
|
2325
2580
|
if (existingRecord) {
|
|
2581
|
+
const sameChannel = !channelId || existingRecord.channelId === channelId;
|
|
2326
2582
|
const platform = existingRecord.platform;
|
|
2327
|
-
|
|
2328
|
-
|
|
2583
|
+
const existingThreadId = platform?.topicId ? String(platform.topicId) : platform?.threadId;
|
|
2584
|
+
if (existingThreadId && sameChannel) {
|
|
2585
|
+
const adapter = this.adapters.get(existingRecord.channelId) ?? this.adapters.values().next().value;
|
|
2329
2586
|
if (adapter) {
|
|
2330
2587
|
try {
|
|
2331
2588
|
await adapter.sendMessage(existingRecord.sessionId, {
|
|
@@ -2338,16 +2595,25 @@ var OpenACPCore = class {
|
|
|
2338
2595
|
return {
|
|
2339
2596
|
ok: true,
|
|
2340
2597
|
sessionId: existingRecord.sessionId,
|
|
2341
|
-
threadId:
|
|
2598
|
+
threadId: existingThreadId,
|
|
2342
2599
|
status: "existing"
|
|
2343
2600
|
};
|
|
2344
2601
|
}
|
|
2345
2602
|
}
|
|
2346
|
-
|
|
2347
|
-
if (
|
|
2348
|
-
|
|
2603
|
+
let adapterChannelId;
|
|
2604
|
+
if (channelId) {
|
|
2605
|
+
if (!this.adapters.has(channelId)) {
|
|
2606
|
+
const available = Array.from(this.adapters.keys()).join(", ") || "none";
|
|
2607
|
+
return { ok: false, error: "adapter_not_found", message: `Adapter '${channelId}' is not connected. Available: ${available}` };
|
|
2608
|
+
}
|
|
2609
|
+
adapterChannelId = channelId;
|
|
2610
|
+
} else {
|
|
2611
|
+
const firstEntry = this.adapters.entries().next().value;
|
|
2612
|
+
if (!firstEntry) {
|
|
2613
|
+
return { ok: false, error: "no_adapter", message: "No channel adapter registered" };
|
|
2614
|
+
}
|
|
2615
|
+
adapterChannelId = firstEntry[0];
|
|
2349
2616
|
}
|
|
2350
|
-
const [adapterChannelId] = firstEntry;
|
|
2351
2617
|
let session;
|
|
2352
2618
|
try {
|
|
2353
2619
|
session = await this.createSession({
|
|
@@ -2365,9 +2631,15 @@ var OpenACPCore = class {
|
|
|
2365
2631
|
message: `Failed to resume session: ${err instanceof Error ? err.message : String(err)}`
|
|
2366
2632
|
};
|
|
2367
2633
|
}
|
|
2634
|
+
const adoptPlatform = {};
|
|
2635
|
+
if (adapterChannelId === "telegram") {
|
|
2636
|
+
adoptPlatform.topicId = Number(session.threadId);
|
|
2637
|
+
} else {
|
|
2638
|
+
adoptPlatform.threadId = session.threadId;
|
|
2639
|
+
}
|
|
2368
2640
|
await this.sessionManager.patchRecord(session.id, {
|
|
2369
2641
|
originalAgentSessionId: agentSessionId,
|
|
2370
|
-
platform:
|
|
2642
|
+
platform: adoptPlatform
|
|
2371
2643
|
});
|
|
2372
2644
|
return {
|
|
2373
2645
|
ok: true,
|
|
@@ -2388,8 +2660,12 @@ var OpenACPCore = class {
|
|
|
2388
2660
|
currentSession.workingDirectory
|
|
2389
2661
|
);
|
|
2390
2662
|
}
|
|
2391
|
-
const record = this.sessionManager.getRecordByThread(
|
|
2392
|
-
|
|
2663
|
+
const record = this.sessionManager.getRecordByThread(
|
|
2664
|
+
channelId,
|
|
2665
|
+
currentThreadId
|
|
2666
|
+
);
|
|
2667
|
+
if (!record || record.status === "cancelled" || record.status === "error")
|
|
2668
|
+
return null;
|
|
2393
2669
|
return this.handleNewSession(
|
|
2394
2670
|
channelId,
|
|
2395
2671
|
record.agentName,
|
|
@@ -2397,6 +2673,15 @@ var OpenACPCore = class {
|
|
|
2397
2673
|
);
|
|
2398
2674
|
}
|
|
2399
2675
|
// --- Lazy Resume ---
|
|
2676
|
+
/**
|
|
2677
|
+
* Get active session by thread, or attempt lazy resume from store.
|
|
2678
|
+
* Used by adapter command handlers that need a session but don't go through handleMessage().
|
|
2679
|
+
*/
|
|
2680
|
+
async getOrResumeSession(channelId, threadId) {
|
|
2681
|
+
const session = this.sessionManager.getSessionByThread(channelId, threadId);
|
|
2682
|
+
if (session) return session;
|
|
2683
|
+
return this.lazyResume({ channelId, threadId, userId: "", text: "" });
|
|
2684
|
+
}
|
|
2400
2685
|
async lazyResume(message) {
|
|
2401
2686
|
const store = this.sessionStore;
|
|
2402
2687
|
if (!store) return null;
|
|
@@ -2408,21 +2693,29 @@ var OpenACPCore = class {
|
|
|
2408
2693
|
(p) => String(p.topicId) === message.threadId
|
|
2409
2694
|
);
|
|
2410
2695
|
if (!record) {
|
|
2411
|
-
|
|
2696
|
+
log7.debug(
|
|
2412
2697
|
{ threadId: message.threadId, channelId: message.channelId },
|
|
2413
2698
|
"No session record found for thread"
|
|
2414
2699
|
);
|
|
2415
2700
|
return null;
|
|
2416
2701
|
}
|
|
2417
2702
|
if (record.status === "error") {
|
|
2418
|
-
|
|
2419
|
-
{
|
|
2703
|
+
log7.debug(
|
|
2704
|
+
{
|
|
2705
|
+
threadId: message.threadId,
|
|
2706
|
+
sessionId: record.sessionId,
|
|
2707
|
+
status: record.status
|
|
2708
|
+
},
|
|
2420
2709
|
"Skipping resume of error session"
|
|
2421
2710
|
);
|
|
2422
2711
|
return null;
|
|
2423
2712
|
}
|
|
2424
|
-
|
|
2425
|
-
{
|
|
2713
|
+
log7.info(
|
|
2714
|
+
{
|
|
2715
|
+
threadId: message.threadId,
|
|
2716
|
+
sessionId: record.sessionId,
|
|
2717
|
+
status: record.status
|
|
2718
|
+
},
|
|
2426
2719
|
"Lazy resume: found record, attempting resume"
|
|
2427
2720
|
);
|
|
2428
2721
|
const resumePromise = (async () => {
|
|
@@ -2438,13 +2731,13 @@ var OpenACPCore = class {
|
|
|
2438
2731
|
session.threadId = message.threadId;
|
|
2439
2732
|
session.activate();
|
|
2440
2733
|
session.dangerousMode = record.dangerousMode ?? false;
|
|
2441
|
-
|
|
2734
|
+
log7.info(
|
|
2442
2735
|
{ sessionId: session.id, threadId: message.threadId },
|
|
2443
2736
|
"Lazy resume successful"
|
|
2444
2737
|
);
|
|
2445
2738
|
return session;
|
|
2446
2739
|
} catch (err) {
|
|
2447
|
-
|
|
2740
|
+
log7.error({ err, record }, "Lazy resume failed");
|
|
2448
2741
|
const adapter = this.adapters.get(message.channelId);
|
|
2449
2742
|
if (adapter) {
|
|
2450
2743
|
try {
|
|
@@ -2470,213 +2763,327 @@ var OpenACPCore = class {
|
|
|
2470
2763
|
messageTransformer: this.messageTransformer,
|
|
2471
2764
|
notificationManager: this.notificationManager,
|
|
2472
2765
|
sessionManager: this.sessionManager,
|
|
2766
|
+
eventBus: this.eventBus,
|
|
2473
2767
|
fileService: this.fileService
|
|
2474
2768
|
});
|
|
2475
2769
|
}
|
|
2476
2770
|
};
|
|
2477
2771
|
|
|
2478
|
-
// src/core/
|
|
2479
|
-
|
|
2480
|
-
|
|
2481
|
-
|
|
2482
|
-
|
|
2483
|
-
|
|
2484
|
-
|
|
2485
|
-
|
|
2486
|
-
|
|
2487
|
-
|
|
2488
|
-
|
|
2489
|
-
|
|
2490
|
-
|
|
2491
|
-
const
|
|
2492
|
-
|
|
2493
|
-
|
|
2494
|
-
|
|
2495
|
-
|
|
2496
|
-
|
|
2772
|
+
// src/core/sse-manager.ts
|
|
2773
|
+
var SSEManager = class {
|
|
2774
|
+
constructor(eventBus, getSessionStats, startedAt) {
|
|
2775
|
+
this.eventBus = eventBus;
|
|
2776
|
+
this.getSessionStats = getSessionStats;
|
|
2777
|
+
this.startedAt = startedAt;
|
|
2778
|
+
}
|
|
2779
|
+
sseConnections = /* @__PURE__ */ new Set();
|
|
2780
|
+
sseCleanupHandlers = /* @__PURE__ */ new Map();
|
|
2781
|
+
healthInterval;
|
|
2782
|
+
boundHandlers = [];
|
|
2783
|
+
setup() {
|
|
2784
|
+
if (!this.eventBus) return;
|
|
2785
|
+
const events = [
|
|
2786
|
+
"session:created",
|
|
2787
|
+
"session:updated",
|
|
2788
|
+
"session:deleted",
|
|
2789
|
+
"agent:event",
|
|
2790
|
+
"permission:request"
|
|
2791
|
+
];
|
|
2792
|
+
for (const eventName of events) {
|
|
2793
|
+
const handler = (data) => {
|
|
2794
|
+
this.broadcast(eventName, data);
|
|
2795
|
+
};
|
|
2796
|
+
this.eventBus.on(eventName, handler);
|
|
2797
|
+
this.boundHandlers.push({ event: eventName, handler });
|
|
2798
|
+
}
|
|
2799
|
+
this.healthInterval = setInterval(() => {
|
|
2800
|
+
const mem = process.memoryUsage();
|
|
2801
|
+
const stats = this.getSessionStats();
|
|
2802
|
+
this.broadcast("health", {
|
|
2803
|
+
uptime: Date.now() - this.startedAt,
|
|
2804
|
+
memory: {
|
|
2805
|
+
rss: mem.rss,
|
|
2806
|
+
heapUsed: mem.heapUsed,
|
|
2807
|
+
heapTotal: mem.heapTotal
|
|
2808
|
+
},
|
|
2809
|
+
sessions: stats
|
|
2810
|
+
});
|
|
2811
|
+
}, 3e4);
|
|
2812
|
+
}
|
|
2813
|
+
handleRequest(req, res) {
|
|
2814
|
+
const parsedUrl = new URL(req.url || "", "http://localhost");
|
|
2815
|
+
const sessionFilter = parsedUrl.searchParams.get("sessionId");
|
|
2816
|
+
res.writeHead(200, {
|
|
2817
|
+
"Content-Type": "text/event-stream",
|
|
2818
|
+
"Cache-Control": "no-cache",
|
|
2819
|
+
Connection: "keep-alive"
|
|
2820
|
+
});
|
|
2821
|
+
res.flushHeaders();
|
|
2822
|
+
res.sessionFilter = sessionFilter ?? void 0;
|
|
2823
|
+
this.sseConnections.add(res);
|
|
2824
|
+
const cleanup = () => {
|
|
2825
|
+
this.sseConnections.delete(res);
|
|
2826
|
+
this.sseCleanupHandlers.delete(res);
|
|
2827
|
+
};
|
|
2828
|
+
this.sseCleanupHandlers.set(res, cleanup);
|
|
2829
|
+
req.on("close", cleanup);
|
|
2497
2830
|
}
|
|
2498
|
-
|
|
2499
|
-
}
|
|
2500
|
-
|
|
2501
|
-
|
|
2502
|
-
|
|
2503
|
-
|
|
2504
|
-
|
|
2505
|
-
|
|
2506
|
-
|
|
2507
|
-
|
|
2508
|
-
|
|
2509
|
-
|
|
2510
|
-
|
|
2511
|
-
|
|
2831
|
+
broadcast(event, data) {
|
|
2832
|
+
const payload = `event: ${event}
|
|
2833
|
+
data: ${JSON.stringify(data)}
|
|
2834
|
+
|
|
2835
|
+
`;
|
|
2836
|
+
const sessionEvents = [
|
|
2837
|
+
"agent:event",
|
|
2838
|
+
"permission:request",
|
|
2839
|
+
"session:updated"
|
|
2840
|
+
];
|
|
2841
|
+
for (const res of this.sseConnections) {
|
|
2842
|
+
const filter = res.sessionFilter;
|
|
2843
|
+
if (filter && sessionEvents.includes(event)) {
|
|
2844
|
+
const eventData = data;
|
|
2845
|
+
if (eventData.sessionId !== filter) continue;
|
|
2846
|
+
}
|
|
2847
|
+
try {
|
|
2848
|
+
if (res.writable) res.write(payload);
|
|
2849
|
+
} catch {
|
|
2850
|
+
}
|
|
2512
2851
|
}
|
|
2513
2852
|
}
|
|
2514
|
-
|
|
2515
|
-
|
|
2516
|
-
|
|
2517
|
-
|
|
2518
|
-
|
|
2519
|
-
|
|
2520
|
-
|
|
2521
|
-
this.
|
|
2853
|
+
stop() {
|
|
2854
|
+
if (this.healthInterval) clearInterval(this.healthInterval);
|
|
2855
|
+
if (this.eventBus) {
|
|
2856
|
+
for (const { event, handler } of this.boundHandlers) {
|
|
2857
|
+
this.eventBus.off(event, handler);
|
|
2858
|
+
}
|
|
2859
|
+
}
|
|
2860
|
+
this.boundHandlers = [];
|
|
2861
|
+
const entries = [...this.sseCleanupHandlers];
|
|
2862
|
+
for (const [res, cleanup] of entries) {
|
|
2863
|
+
res.end();
|
|
2864
|
+
cleanup();
|
|
2865
|
+
}
|
|
2522
2866
|
}
|
|
2523
|
-
|
|
2524
|
-
|
|
2525
|
-
|
|
2526
|
-
|
|
2527
|
-
|
|
2528
|
-
|
|
2529
|
-
|
|
2530
|
-
|
|
2531
|
-
|
|
2532
|
-
|
|
2533
|
-
|
|
2534
|
-
|
|
2535
|
-
|
|
2536
|
-
|
|
2537
|
-
|
|
2538
|
-
|
|
2539
|
-
|
|
2540
|
-
|
|
2541
|
-
|
|
2542
|
-
|
|
2543
|
-
|
|
2544
|
-
|
|
2545
|
-
|
|
2867
|
+
};
|
|
2868
|
+
|
|
2869
|
+
// src/core/static-server.ts
|
|
2870
|
+
import * as fs6 from "fs";
|
|
2871
|
+
import * as path6 from "path";
|
|
2872
|
+
import { fileURLToPath } from "url";
|
|
2873
|
+
var MIME_TYPES = {
|
|
2874
|
+
".html": "text/html; charset=utf-8",
|
|
2875
|
+
".js": "application/javascript; charset=utf-8",
|
|
2876
|
+
".css": "text/css; charset=utf-8",
|
|
2877
|
+
".json": "application/json; charset=utf-8",
|
|
2878
|
+
".png": "image/png",
|
|
2879
|
+
".jpg": "image/jpeg",
|
|
2880
|
+
".svg": "image/svg+xml",
|
|
2881
|
+
".ico": "image/x-icon",
|
|
2882
|
+
".woff": "font/woff",
|
|
2883
|
+
".woff2": "font/woff2"
|
|
2884
|
+
};
|
|
2885
|
+
var StaticServer = class {
|
|
2886
|
+
uiDir;
|
|
2887
|
+
constructor(uiDir) {
|
|
2888
|
+
this.uiDir = uiDir;
|
|
2889
|
+
if (!this.uiDir) {
|
|
2890
|
+
const __filename = fileURLToPath(import.meta.url);
|
|
2891
|
+
const candidate = path6.resolve(path6.dirname(__filename), "../../ui/dist");
|
|
2892
|
+
if (fs6.existsSync(path6.join(candidate, "index.html"))) {
|
|
2893
|
+
this.uiDir = candidate;
|
|
2894
|
+
}
|
|
2895
|
+
if (!this.uiDir) {
|
|
2896
|
+
const publishCandidate = path6.resolve(
|
|
2897
|
+
path6.dirname(__filename),
|
|
2898
|
+
"../ui"
|
|
2899
|
+
);
|
|
2900
|
+
if (fs6.existsSync(path6.join(publishCandidate, "index.html"))) {
|
|
2901
|
+
this.uiDir = publishCandidate;
|
|
2546
2902
|
}
|
|
2547
|
-
|
|
2548
|
-
|
|
2549
|
-
resolve2();
|
|
2550
|
-
});
|
|
2551
|
-
});
|
|
2903
|
+
}
|
|
2904
|
+
}
|
|
2552
2905
|
}
|
|
2553
|
-
|
|
2554
|
-
this.
|
|
2555
|
-
|
|
2556
|
-
|
|
2557
|
-
|
|
2906
|
+
isAvailable() {
|
|
2907
|
+
return this.uiDir !== void 0;
|
|
2908
|
+
}
|
|
2909
|
+
serve(req, res) {
|
|
2910
|
+
if (!this.uiDir) return false;
|
|
2911
|
+
const urlPath = (req.url || "/").split("?")[0];
|
|
2912
|
+
const safePath = path6.normalize(urlPath);
|
|
2913
|
+
const filePath = path6.join(this.uiDir, safePath);
|
|
2914
|
+
if (!filePath.startsWith(this.uiDir + path6.sep) && filePath !== this.uiDir)
|
|
2915
|
+
return false;
|
|
2916
|
+
if (fs6.existsSync(filePath) && fs6.statSync(filePath).isFile()) {
|
|
2917
|
+
const ext = path6.extname(filePath);
|
|
2918
|
+
const contentType = MIME_TYPES[ext] ?? "application/octet-stream";
|
|
2919
|
+
const isHashed = /\.[a-zA-Z0-9]{8,}\.(js|css)$/.test(filePath);
|
|
2920
|
+
const cacheControl = isHashed ? "public, max-age=31536000, immutable" : "no-cache";
|
|
2921
|
+
res.writeHead(200, {
|
|
2922
|
+
"Content-Type": contentType,
|
|
2923
|
+
"Cache-Control": cacheControl
|
|
2558
2924
|
});
|
|
2559
|
-
|
|
2925
|
+
fs6.createReadStream(filePath).pipe(res);
|
|
2926
|
+
return true;
|
|
2927
|
+
}
|
|
2928
|
+
const indexPath = path6.join(this.uiDir, "index.html");
|
|
2929
|
+
if (fs6.existsSync(indexPath)) {
|
|
2930
|
+
res.writeHead(200, {
|
|
2931
|
+
"Content-Type": "text/html; charset=utf-8",
|
|
2932
|
+
"Cache-Control": "no-cache"
|
|
2933
|
+
});
|
|
2934
|
+
fs6.createReadStream(indexPath).pipe(res);
|
|
2935
|
+
return true;
|
|
2560
2936
|
}
|
|
2937
|
+
return false;
|
|
2561
2938
|
}
|
|
2562
|
-
|
|
2563
|
-
|
|
2939
|
+
};
|
|
2940
|
+
|
|
2941
|
+
// src/core/api/index.ts
|
|
2942
|
+
import * as http from "http";
|
|
2943
|
+
import * as fs7 from "fs";
|
|
2944
|
+
import * as path7 from "path";
|
|
2945
|
+
import * as os2 from "os";
|
|
2946
|
+
import * as crypto from "crypto";
|
|
2947
|
+
import { fileURLToPath as fileURLToPath2 } from "url";
|
|
2948
|
+
|
|
2949
|
+
// src/core/api/router.ts
|
|
2950
|
+
var Router = class {
|
|
2951
|
+
routes = [];
|
|
2952
|
+
get(path8, handler) {
|
|
2953
|
+
this.add("GET", path8, handler);
|
|
2564
2954
|
}
|
|
2565
|
-
|
|
2566
|
-
|
|
2567
|
-
fs6.mkdirSync(dir, { recursive: true });
|
|
2568
|
-
fs6.writeFileSync(this.portFilePath, String(this.actualPort));
|
|
2955
|
+
post(path8, handler) {
|
|
2956
|
+
this.add("POST", path8, handler);
|
|
2569
2957
|
}
|
|
2570
|
-
|
|
2571
|
-
|
|
2572
|
-
fs6.unlinkSync(this.portFilePath);
|
|
2573
|
-
} catch {
|
|
2574
|
-
}
|
|
2958
|
+
put(path8, handler) {
|
|
2959
|
+
this.add("PUT", path8, handler);
|
|
2575
2960
|
}
|
|
2576
|
-
|
|
2577
|
-
|
|
2578
|
-
|
|
2579
|
-
|
|
2580
|
-
|
|
2581
|
-
|
|
2582
|
-
|
|
2583
|
-
|
|
2584
|
-
|
|
2585
|
-
|
|
2586
|
-
|
|
2587
|
-
|
|
2588
|
-
|
|
2589
|
-
|
|
2590
|
-
|
|
2961
|
+
patch(path8, handler) {
|
|
2962
|
+
this.add("PATCH", path8, handler);
|
|
2963
|
+
}
|
|
2964
|
+
delete(path8, handler) {
|
|
2965
|
+
this.add("DELETE", path8, handler);
|
|
2966
|
+
}
|
|
2967
|
+
match(method, url) {
|
|
2968
|
+
const pathname = url.split("?")[0];
|
|
2969
|
+
for (const route of this.routes) {
|
|
2970
|
+
if (route.method !== method) continue;
|
|
2971
|
+
const m = pathname.match(route.pattern);
|
|
2972
|
+
if (!m) continue;
|
|
2973
|
+
const params = {};
|
|
2974
|
+
for (let i = 0; i < route.keys.length; i++) {
|
|
2975
|
+
params[route.keys[i]] = m[i + 1];
|
|
2591
2976
|
}
|
|
2592
|
-
|
|
2977
|
+
return { handler: route.handler, params };
|
|
2593
2978
|
}
|
|
2594
|
-
|
|
2595
|
-
fs6.writeFileSync(this.secretFilePath, this.secret, { mode: 384 });
|
|
2979
|
+
return null;
|
|
2596
2980
|
}
|
|
2597
|
-
|
|
2598
|
-
const
|
|
2599
|
-
|
|
2600
|
-
|
|
2601
|
-
|
|
2602
|
-
|
|
2981
|
+
add(method, path8, handler) {
|
|
2982
|
+
const keys = [];
|
|
2983
|
+
const pattern = path8.replace(/:(\w+)/g, (_, key) => {
|
|
2984
|
+
keys.push(key);
|
|
2985
|
+
return "([^/]+)";
|
|
2986
|
+
});
|
|
2987
|
+
this.routes.push({
|
|
2988
|
+
method,
|
|
2989
|
+
pattern: new RegExp(`^${pattern}$`),
|
|
2990
|
+
keys,
|
|
2991
|
+
handler
|
|
2992
|
+
});
|
|
2603
2993
|
}
|
|
2604
|
-
|
|
2605
|
-
|
|
2606
|
-
|
|
2607
|
-
|
|
2608
|
-
|
|
2609
|
-
|
|
2994
|
+
};
|
|
2995
|
+
|
|
2996
|
+
// src/core/api/routes/health.ts
|
|
2997
|
+
function registerHealthRoutes(router, deps) {
|
|
2998
|
+
router.get("/api/health", async (_req, res) => {
|
|
2999
|
+
const activeSessions = deps.core.sessionManager.listSessions();
|
|
3000
|
+
const allRecords = deps.core.sessionManager.listRecords();
|
|
3001
|
+
const mem = process.memoryUsage();
|
|
3002
|
+
const tunnel = deps.core.tunnelService;
|
|
3003
|
+
deps.sendJson(res, 200, {
|
|
3004
|
+
status: "ok",
|
|
3005
|
+
uptime: Date.now() - deps.startedAt,
|
|
3006
|
+
version: deps.getVersion(),
|
|
3007
|
+
memory: {
|
|
3008
|
+
rss: mem.rss,
|
|
3009
|
+
heapUsed: mem.heapUsed,
|
|
3010
|
+
heapTotal: mem.heapTotal
|
|
3011
|
+
},
|
|
3012
|
+
sessions: {
|
|
3013
|
+
active: activeSessions.filter(
|
|
3014
|
+
(s) => s.status === "active" || s.status === "initializing"
|
|
3015
|
+
).length,
|
|
3016
|
+
total: allRecords.length
|
|
3017
|
+
},
|
|
3018
|
+
adapters: Array.from(deps.core.adapters.keys()),
|
|
3019
|
+
tunnel: tunnel ? { enabled: true, url: tunnel.getPublicUrl() } : { enabled: false }
|
|
3020
|
+
});
|
|
3021
|
+
});
|
|
3022
|
+
router.get("/api/version", async (_req, res) => {
|
|
3023
|
+
deps.sendJson(res, 200, { version: deps.getVersion() });
|
|
3024
|
+
});
|
|
3025
|
+
router.post("/api/restart", async (_req, res) => {
|
|
3026
|
+
if (!deps.core.requestRestart) {
|
|
3027
|
+
deps.sendJson(res, 501, { error: "Restart not available" });
|
|
2610
3028
|
return;
|
|
2611
3029
|
}
|
|
3030
|
+
deps.sendJson(res, 200, { ok: true, message: "Restarting..." });
|
|
3031
|
+
setImmediate(() => deps.core.requestRestart());
|
|
3032
|
+
});
|
|
3033
|
+
router.get("/api/adapters", async (_req, res) => {
|
|
3034
|
+
const adapters = Array.from(deps.core.adapters.entries()).map(([name]) => ({
|
|
3035
|
+
name,
|
|
3036
|
+
type: "built-in"
|
|
3037
|
+
}));
|
|
3038
|
+
deps.sendJson(res, 200, { adapters });
|
|
3039
|
+
});
|
|
3040
|
+
}
|
|
3041
|
+
|
|
3042
|
+
// src/core/api/routes/sessions.ts
|
|
3043
|
+
var log8 = createChildLogger({ module: "api-server" });
|
|
3044
|
+
function registerSessionRoutes(router, deps) {
|
|
3045
|
+
router.post("/api/sessions/adopt", async (req, res) => {
|
|
3046
|
+
const body = await deps.readBody(req);
|
|
3047
|
+
if (body === null) {
|
|
3048
|
+
return deps.sendJson(res, 413, { error: "Request body too large" });
|
|
3049
|
+
}
|
|
3050
|
+
if (!body) {
|
|
3051
|
+
return deps.sendJson(res, 400, {
|
|
3052
|
+
error: "bad_request",
|
|
3053
|
+
message: "Empty request body"
|
|
3054
|
+
});
|
|
3055
|
+
}
|
|
3056
|
+
let parsed;
|
|
2612
3057
|
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" });
|
|
3058
|
+
parsed = JSON.parse(body);
|
|
3059
|
+
} catch {
|
|
3060
|
+
return deps.sendJson(res, 400, {
|
|
3061
|
+
error: "bad_request",
|
|
3062
|
+
message: "Invalid JSON"
|
|
3063
|
+
});
|
|
2676
3064
|
}
|
|
2677
|
-
|
|
2678
|
-
|
|
2679
|
-
|
|
3065
|
+
const { agent, agentSessionId, cwd, channel } = parsed;
|
|
3066
|
+
if (!agent || !agentSessionId) {
|
|
3067
|
+
return deps.sendJson(res, 400, {
|
|
3068
|
+
error: "bad_request",
|
|
3069
|
+
message: "Missing required fields: agent, agentSessionId"
|
|
3070
|
+
});
|
|
3071
|
+
}
|
|
3072
|
+
const result = await deps.core.adoptSession(
|
|
3073
|
+
agent,
|
|
3074
|
+
agentSessionId,
|
|
3075
|
+
cwd ?? process.cwd(),
|
|
3076
|
+
channel
|
|
3077
|
+
);
|
|
3078
|
+
if (result.ok) {
|
|
3079
|
+
return deps.sendJson(res, 200, result);
|
|
3080
|
+
} else {
|
|
3081
|
+
const status = result.error === "session_limit" ? 429 : result.error === "agent_not_supported" ? 400 : 500;
|
|
3082
|
+
return deps.sendJson(res, status, result);
|
|
3083
|
+
}
|
|
3084
|
+
});
|
|
3085
|
+
router.post("/api/sessions", async (req, res) => {
|
|
3086
|
+
const body = await deps.readBody(req);
|
|
2680
3087
|
let agent;
|
|
2681
3088
|
let workspace;
|
|
2682
3089
|
if (body) {
|
|
@@ -2685,25 +3092,28 @@ var ApiServer = class {
|
|
|
2685
3092
|
agent = parsed.agent;
|
|
2686
3093
|
workspace = parsed.workspace;
|
|
2687
3094
|
} catch {
|
|
2688
|
-
|
|
3095
|
+
deps.sendJson(res, 400, { error: "Invalid JSON body" });
|
|
2689
3096
|
return;
|
|
2690
3097
|
}
|
|
2691
3098
|
}
|
|
2692
|
-
const config =
|
|
2693
|
-
const activeSessions =
|
|
3099
|
+
const config = deps.core.configManager.get();
|
|
3100
|
+
const activeSessions = deps.core.sessionManager.listSessions().filter((s) => s.status === "active" || s.status === "initializing");
|
|
2694
3101
|
if (activeSessions.length >= config.security.maxConcurrentSessions) {
|
|
2695
|
-
|
|
3102
|
+
deps.sendJson(res, 429, {
|
|
2696
3103
|
error: `Max concurrent sessions (${config.security.maxConcurrentSessions}) reached. Cancel a session first.`
|
|
2697
3104
|
});
|
|
2698
3105
|
return;
|
|
2699
3106
|
}
|
|
2700
|
-
const [adapterId, adapter] =
|
|
3107
|
+
const [adapterId, adapter] = deps.core.adapters.entries().next().value ?? [
|
|
3108
|
+
null,
|
|
3109
|
+
null
|
|
3110
|
+
];
|
|
2701
3111
|
const channelId = adapterId ?? "api";
|
|
2702
3112
|
const resolvedAgent = agent || config.defaultAgent;
|
|
2703
|
-
const resolvedWorkspace =
|
|
3113
|
+
const resolvedWorkspace = deps.core.configManager.resolveWorkspace(
|
|
2704
3114
|
workspace || config.agents[resolvedAgent]?.workingDirectory
|
|
2705
3115
|
);
|
|
2706
|
-
const session = await
|
|
3116
|
+
const session = await deps.core.createSession({
|
|
2707
3117
|
channelId,
|
|
2708
3118
|
agentName: resolvedAgent,
|
|
2709
3119
|
workingDirectory: resolvedWorkspace,
|
|
@@ -2713,54 +3123,138 @@ var ApiServer = class {
|
|
|
2713
3123
|
if (!adapter) {
|
|
2714
3124
|
session.agentInstance.onPermissionRequest = async (request) => {
|
|
2715
3125
|
const allowOption = request.options.find((o) => o.isAllow);
|
|
2716
|
-
|
|
3126
|
+
log8.debug(
|
|
3127
|
+
{
|
|
3128
|
+
sessionId: session.id,
|
|
3129
|
+
permissionId: request.id,
|
|
3130
|
+
option: allowOption?.id
|
|
3131
|
+
},
|
|
3132
|
+
"Auto-approving permission for API session"
|
|
3133
|
+
);
|
|
2717
3134
|
return allowOption?.id ?? request.options[0]?.id ?? "";
|
|
2718
3135
|
};
|
|
2719
3136
|
}
|
|
2720
|
-
session.warmup().catch(
|
|
2721
|
-
|
|
3137
|
+
session.warmup().catch(
|
|
3138
|
+
(err) => log8.warn({ err, sessionId: session.id }, "API session warmup failed")
|
|
3139
|
+
);
|
|
3140
|
+
deps.sendJson(res, 200, {
|
|
2722
3141
|
sessionId: session.id,
|
|
2723
3142
|
agent: session.agentName,
|
|
2724
3143
|
status: session.status,
|
|
2725
3144
|
workspace: session.workingDirectory
|
|
2726
3145
|
});
|
|
2727
|
-
}
|
|
2728
|
-
|
|
2729
|
-
const
|
|
3146
|
+
});
|
|
3147
|
+
router.post("/api/sessions/:sessionId/prompt", async (req, res, params) => {
|
|
3148
|
+
const sessionId = decodeURIComponent(params.sessionId);
|
|
3149
|
+
const session = deps.core.sessionManager.getSession(sessionId);
|
|
2730
3150
|
if (!session) {
|
|
2731
|
-
|
|
3151
|
+
deps.sendJson(res, 404, { error: `Session "${sessionId}" not found` });
|
|
2732
3152
|
return;
|
|
2733
3153
|
}
|
|
2734
3154
|
if (session.status === "cancelled" || session.status === "finished" || session.status === "error") {
|
|
2735
|
-
|
|
3155
|
+
deps.sendJson(res, 400, { error: `Session is ${session.status}` });
|
|
2736
3156
|
return;
|
|
2737
3157
|
}
|
|
2738
|
-
const body = await
|
|
3158
|
+
const body = await deps.readBody(req);
|
|
2739
3159
|
let prompt;
|
|
2740
3160
|
if (body) {
|
|
2741
3161
|
try {
|
|
2742
3162
|
const parsed = JSON.parse(body);
|
|
2743
3163
|
prompt = parsed.prompt;
|
|
2744
3164
|
} catch {
|
|
2745
|
-
|
|
3165
|
+
deps.sendJson(res, 400, { error: "Invalid JSON body" });
|
|
2746
3166
|
return;
|
|
2747
3167
|
}
|
|
2748
3168
|
}
|
|
2749
3169
|
if (!prompt) {
|
|
2750
|
-
|
|
3170
|
+
deps.sendJson(res, 400, { error: "Missing prompt" });
|
|
2751
3171
|
return;
|
|
2752
3172
|
}
|
|
2753
3173
|
session.enqueuePrompt(prompt).catch(() => {
|
|
2754
3174
|
});
|
|
2755
|
-
|
|
2756
|
-
|
|
2757
|
-
|
|
2758
|
-
|
|
3175
|
+
deps.sendJson(res, 200, {
|
|
3176
|
+
ok: true,
|
|
3177
|
+
sessionId,
|
|
3178
|
+
queueDepth: session.queueDepth
|
|
3179
|
+
});
|
|
3180
|
+
});
|
|
3181
|
+
router.post(
|
|
3182
|
+
"/api/sessions/:sessionId/permission",
|
|
3183
|
+
async (req, res, params) => {
|
|
3184
|
+
const sessionId = decodeURIComponent(params.sessionId);
|
|
3185
|
+
const session = deps.core.sessionManager.getSession(sessionId);
|
|
3186
|
+
if (!session) {
|
|
3187
|
+
deps.sendJson(res, 404, { error: `Session "${sessionId}" not found` });
|
|
3188
|
+
return;
|
|
3189
|
+
}
|
|
3190
|
+
const body = await deps.readBody(req);
|
|
3191
|
+
let permissionId;
|
|
3192
|
+
let optionId;
|
|
3193
|
+
if (body) {
|
|
3194
|
+
try {
|
|
3195
|
+
const parsed = JSON.parse(body);
|
|
3196
|
+
permissionId = parsed.permissionId;
|
|
3197
|
+
optionId = parsed.optionId;
|
|
3198
|
+
} catch {
|
|
3199
|
+
deps.sendJson(res, 400, { error: "Invalid JSON body" });
|
|
3200
|
+
return;
|
|
3201
|
+
}
|
|
3202
|
+
}
|
|
3203
|
+
if (!permissionId || !optionId) {
|
|
3204
|
+
deps.sendJson(res, 400, {
|
|
3205
|
+
error: "Missing permissionId or optionId"
|
|
3206
|
+
});
|
|
3207
|
+
return;
|
|
3208
|
+
}
|
|
3209
|
+
if (!session.permissionGate.isPending || session.permissionGate.requestId !== permissionId) {
|
|
3210
|
+
deps.sendJson(res, 400, {
|
|
3211
|
+
error: "No matching pending permission request"
|
|
3212
|
+
});
|
|
3213
|
+
return;
|
|
3214
|
+
}
|
|
3215
|
+
session.permissionGate.resolve(optionId);
|
|
3216
|
+
deps.sendJson(res, 200, { ok: true });
|
|
3217
|
+
}
|
|
3218
|
+
);
|
|
3219
|
+
router.patch(
|
|
3220
|
+
"/api/sessions/:sessionId/dangerous",
|
|
3221
|
+
async (req, res, params) => {
|
|
3222
|
+
const sessionId = decodeURIComponent(params.sessionId);
|
|
3223
|
+
const session = deps.core.sessionManager.getSession(sessionId);
|
|
3224
|
+
if (!session) {
|
|
3225
|
+
deps.sendJson(res, 404, { error: `Session "${sessionId}" not found` });
|
|
3226
|
+
return;
|
|
3227
|
+
}
|
|
3228
|
+
const body = await deps.readBody(req);
|
|
3229
|
+
let enabled;
|
|
3230
|
+
if (body) {
|
|
3231
|
+
try {
|
|
3232
|
+
const parsed = JSON.parse(body);
|
|
3233
|
+
enabled = parsed.enabled;
|
|
3234
|
+
} catch {
|
|
3235
|
+
deps.sendJson(res, 400, { error: "Invalid JSON body" });
|
|
3236
|
+
return;
|
|
3237
|
+
}
|
|
3238
|
+
}
|
|
3239
|
+
if (typeof enabled !== "boolean") {
|
|
3240
|
+
deps.sendJson(res, 400, { error: "Missing enabled boolean" });
|
|
3241
|
+
return;
|
|
3242
|
+
}
|
|
3243
|
+
session.dangerousMode = enabled;
|
|
3244
|
+
await deps.core.sessionManager.patchRecord(sessionId, {
|
|
3245
|
+
dangerousMode: enabled
|
|
3246
|
+
});
|
|
3247
|
+
deps.sendJson(res, 200, { ok: true, dangerousMode: enabled });
|
|
3248
|
+
}
|
|
3249
|
+
);
|
|
3250
|
+
router.get("/api/sessions/:sessionId", async (_req, res, params) => {
|
|
3251
|
+
const sessionId = decodeURIComponent(params.sessionId);
|
|
3252
|
+
const session = deps.core.sessionManager.getSession(sessionId);
|
|
2759
3253
|
if (!session) {
|
|
2760
|
-
|
|
3254
|
+
deps.sendJson(res, 404, { error: `Session "${sessionId}" not found` });
|
|
2761
3255
|
return;
|
|
2762
3256
|
}
|
|
2763
|
-
|
|
3257
|
+
deps.sendJson(res, 200, {
|
|
2764
3258
|
session: {
|
|
2765
3259
|
id: session.id,
|
|
2766
3260
|
agent: session.agentName,
|
|
@@ -2776,60 +3270,77 @@ var ApiServer = class {
|
|
|
2776
3270
|
agentSessionId: session.agentSessionId
|
|
2777
3271
|
}
|
|
2778
3272
|
});
|
|
2779
|
-
}
|
|
2780
|
-
async
|
|
2781
|
-
const
|
|
3273
|
+
});
|
|
3274
|
+
router.post("/api/sessions/:sessionId/archive", async (_req, res, params) => {
|
|
3275
|
+
const sessionId = decodeURIComponent(params.sessionId);
|
|
3276
|
+
const result = await deps.core.archiveSession(sessionId);
|
|
3277
|
+
if (result.ok) {
|
|
3278
|
+
deps.sendJson(res, 200, result);
|
|
3279
|
+
} else {
|
|
3280
|
+
deps.sendJson(res, 400, result);
|
|
3281
|
+
}
|
|
3282
|
+
});
|
|
3283
|
+
router.delete("/api/sessions/:sessionId", async (_req, res, params) => {
|
|
3284
|
+
const sessionId = decodeURIComponent(params.sessionId);
|
|
3285
|
+
const session = deps.core.sessionManager.getSession(sessionId);
|
|
2782
3286
|
if (!session) {
|
|
2783
|
-
|
|
3287
|
+
deps.sendJson(res, 404, { error: `Session "${sessionId}" not found` });
|
|
2784
3288
|
return;
|
|
2785
3289
|
}
|
|
2786
|
-
|
|
2787
|
-
|
|
2788
|
-
|
|
2789
|
-
|
|
2790
|
-
|
|
2791
|
-
|
|
2792
|
-
|
|
2793
|
-
|
|
2794
|
-
|
|
3290
|
+
await deps.core.sessionManager.cancelSession(sessionId);
|
|
3291
|
+
deps.sendJson(res, 200, { ok: true });
|
|
3292
|
+
});
|
|
3293
|
+
router.get("/api/sessions", async (_req, res) => {
|
|
3294
|
+
const sessions = deps.core.sessionManager.listSessions();
|
|
3295
|
+
deps.sendJson(res, 200, {
|
|
3296
|
+
sessions: sessions.map((s) => ({
|
|
3297
|
+
id: s.id,
|
|
3298
|
+
agent: s.agentName,
|
|
3299
|
+
status: s.status,
|
|
3300
|
+
name: s.name ?? null,
|
|
3301
|
+
workspace: s.workingDirectory,
|
|
3302
|
+
createdAt: s.createdAt.toISOString(),
|
|
3303
|
+
dangerousMode: s.dangerousMode,
|
|
3304
|
+
queueDepth: s.queueDepth,
|
|
3305
|
+
promptRunning: s.promptRunning,
|
|
3306
|
+
lastActiveAt: deps.core.sessionManager.getSessionRecord(s.id)?.lastActiveAt ?? null
|
|
3307
|
+
}))
|
|
3308
|
+
});
|
|
3309
|
+
});
|
|
3310
|
+
}
|
|
3311
|
+
|
|
3312
|
+
// src/core/api/routes/config.ts
|
|
3313
|
+
var SENSITIVE_KEYS = [
|
|
3314
|
+
"botToken",
|
|
3315
|
+
"token",
|
|
3316
|
+
"apiKey",
|
|
3317
|
+
"secret",
|
|
3318
|
+
"password",
|
|
3319
|
+
"webhookSecret"
|
|
3320
|
+
];
|
|
3321
|
+
function redactConfig(config) {
|
|
3322
|
+
const redacted = structuredClone(config);
|
|
3323
|
+
redactDeep(redacted);
|
|
3324
|
+
return redacted;
|
|
3325
|
+
}
|
|
3326
|
+
function redactDeep(obj) {
|
|
3327
|
+
for (const [key, value] of Object.entries(obj)) {
|
|
3328
|
+
if (SENSITIVE_KEYS.includes(key) && typeof value === "string") {
|
|
3329
|
+
obj[key] = "***";
|
|
3330
|
+
} else if (Array.isArray(value)) {
|
|
3331
|
+
for (const item of value) {
|
|
3332
|
+
if (item && typeof item === "object")
|
|
3333
|
+
redactDeep(item);
|
|
2795
3334
|
}
|
|
3335
|
+
} else if (value && typeof value === "object") {
|
|
3336
|
+
redactDeep(value);
|
|
2796
3337
|
}
|
|
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
3338
|
}
|
|
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();
|
|
3339
|
+
}
|
|
3340
|
+
function registerConfigRoutes(router, deps) {
|
|
3341
|
+
router.get("/api/config/editable", async (_req, res) => {
|
|
3342
|
+
const { getSafeFields: getSafeFields2, resolveOptions: resolveOptions2, getConfigValue: getConfigValue2 } = await import("./config-registry-7I6GGDOY.js");
|
|
3343
|
+
const config = deps.core.configManager.get();
|
|
2833
3344
|
const safeFields = getSafeFields2();
|
|
2834
3345
|
const fields = safeFields.map((def) => ({
|
|
2835
3346
|
path: def.path,
|
|
@@ -2840,14 +3351,14 @@ var ApiServer = class {
|
|
|
2840
3351
|
value: getConfigValue2(config, def.path),
|
|
2841
3352
|
hotReload: def.hotReload
|
|
2842
3353
|
}));
|
|
2843
|
-
|
|
2844
|
-
}
|
|
2845
|
-
async
|
|
2846
|
-
const config =
|
|
2847
|
-
|
|
2848
|
-
}
|
|
2849
|
-
async
|
|
2850
|
-
const body = await
|
|
3354
|
+
deps.sendJson(res, 200, { fields });
|
|
3355
|
+
});
|
|
3356
|
+
router.get("/api/config", async (_req, res) => {
|
|
3357
|
+
const config = deps.core.configManager.get();
|
|
3358
|
+
deps.sendJson(res, 200, { config: redactConfig(config) });
|
|
3359
|
+
});
|
|
3360
|
+
router.patch("/api/config", async (req, res) => {
|
|
3361
|
+
const body = await deps.readBody(req);
|
|
2851
3362
|
let configPath;
|
|
2852
3363
|
let value;
|
|
2853
3364
|
if (body) {
|
|
@@ -2856,17 +3367,30 @@ var ApiServer = class {
|
|
|
2856
3367
|
configPath = parsed.path;
|
|
2857
3368
|
value = parsed.value;
|
|
2858
3369
|
} catch {
|
|
2859
|
-
|
|
3370
|
+
deps.sendJson(res, 400, { error: "Invalid JSON body" });
|
|
2860
3371
|
return;
|
|
2861
3372
|
}
|
|
2862
3373
|
}
|
|
2863
3374
|
if (!configPath) {
|
|
2864
|
-
|
|
3375
|
+
deps.sendJson(res, 400, { error: "Missing path" });
|
|
2865
3376
|
return;
|
|
2866
3377
|
}
|
|
2867
|
-
const
|
|
2868
|
-
const cloned = structuredClone(currentConfig);
|
|
3378
|
+
const BLOCKED_KEYS = /* @__PURE__ */ new Set(["__proto__", "constructor", "prototype"]);
|
|
2869
3379
|
const parts = configPath.split(".");
|
|
3380
|
+
if (parts.some((p) => BLOCKED_KEYS.has(p))) {
|
|
3381
|
+
deps.sendJson(res, 400, { error: "Invalid config path" });
|
|
3382
|
+
return;
|
|
3383
|
+
}
|
|
3384
|
+
const { getFieldDef: getFieldDef2 } = await import("./config-registry-7I6GGDOY.js");
|
|
3385
|
+
const fieldDef = getFieldDef2(configPath);
|
|
3386
|
+
if (!fieldDef || fieldDef.scope !== "safe") {
|
|
3387
|
+
deps.sendJson(res, 403, {
|
|
3388
|
+
error: "This config field cannot be modified via the API"
|
|
3389
|
+
});
|
|
3390
|
+
return;
|
|
3391
|
+
}
|
|
3392
|
+
const currentConfig = deps.core.configManager.get();
|
|
3393
|
+
const cloned = structuredClone(currentConfig);
|
|
2870
3394
|
let target = cloned;
|
|
2871
3395
|
for (let i = 0; i < parts.length - 1; i++) {
|
|
2872
3396
|
const part = parts[i];
|
|
@@ -2876,18 +3400,21 @@ var ApiServer = class {
|
|
|
2876
3400
|
target[part] = {};
|
|
2877
3401
|
target = target[part];
|
|
2878
3402
|
} else {
|
|
2879
|
-
|
|
3403
|
+
deps.sendJson(res, 400, { error: "Invalid config path" });
|
|
2880
3404
|
return;
|
|
2881
3405
|
}
|
|
2882
3406
|
}
|
|
2883
3407
|
const lastKey = parts[parts.length - 1];
|
|
2884
3408
|
target[lastKey] = value;
|
|
2885
|
-
const { ConfigSchema } = await import("./config-
|
|
3409
|
+
const { ConfigSchema } = await import("./config-4YSJ4NCI.js");
|
|
2886
3410
|
const result = ConfigSchema.safeParse(cloned);
|
|
2887
3411
|
if (!result.success) {
|
|
2888
|
-
|
|
3412
|
+
deps.sendJson(res, 400, {
|
|
2889
3413
|
error: "Validation failed",
|
|
2890
|
-
details: result.error.issues.map((i) => ({
|
|
3414
|
+
details: result.error.issues.map((i) => ({
|
|
3415
|
+
path: i.path.join("."),
|
|
3416
|
+
message: i.message
|
|
3417
|
+
}))
|
|
2891
3418
|
});
|
|
2892
3419
|
return;
|
|
2893
3420
|
}
|
|
@@ -2898,239 +3425,438 @@ var ApiServer = class {
|
|
|
2898
3425
|
updateTarget = updateTarget[parts[i]];
|
|
2899
3426
|
}
|
|
2900
3427
|
updateTarget[lastKey] = value;
|
|
2901
|
-
await
|
|
2902
|
-
const { isHotReloadable: isHotReloadable2 } = await import("./config-registry-
|
|
3428
|
+
await deps.core.configManager.save(updates, configPath);
|
|
3429
|
+
const { isHotReloadable: isHotReloadable2 } = await import("./config-registry-7I6GGDOY.js");
|
|
2903
3430
|
const needsRestart = !isHotReloadable2(configPath);
|
|
2904
|
-
|
|
3431
|
+
deps.sendJson(res, 200, {
|
|
2905
3432
|
ok: true,
|
|
2906
3433
|
needsRestart,
|
|
2907
|
-
config: redactConfig(
|
|
3434
|
+
config: redactConfig(deps.core.configManager.get())
|
|
2908
3435
|
});
|
|
2909
|
-
}
|
|
2910
|
-
|
|
2911
|
-
|
|
2912
|
-
|
|
2913
|
-
|
|
2914
|
-
|
|
2915
|
-
|
|
2916
|
-
|
|
2917
|
-
|
|
2918
|
-
|
|
3436
|
+
});
|
|
3437
|
+
}
|
|
3438
|
+
|
|
3439
|
+
// src/core/api/routes/topics.ts
|
|
3440
|
+
function registerTopicRoutes(router, deps) {
|
|
3441
|
+
router.get("/api/topics", async (req, res) => {
|
|
3442
|
+
if (!deps.topicManager) {
|
|
3443
|
+
deps.sendJson(res, 501, { error: "Topic management not available" });
|
|
3444
|
+
return;
|
|
3445
|
+
}
|
|
3446
|
+
const url = req.url || "";
|
|
3447
|
+
const params = new URL(url, "http://localhost").searchParams;
|
|
3448
|
+
const statusParam = params.get("status");
|
|
3449
|
+
const filter = statusParam ? { statuses: statusParam.split(",") } : void 0;
|
|
3450
|
+
const topics = deps.topicManager.listTopics(filter);
|
|
3451
|
+
deps.sendJson(res, 200, { topics });
|
|
3452
|
+
});
|
|
3453
|
+
router.post("/api/topics/cleanup", async (req, res) => {
|
|
3454
|
+
if (!deps.topicManager) {
|
|
3455
|
+
deps.sendJson(res, 501, { error: "Topic management not available" });
|
|
3456
|
+
return;
|
|
3457
|
+
}
|
|
3458
|
+
const body = await deps.readBody(req);
|
|
3459
|
+
let statuses;
|
|
3460
|
+
if (body) {
|
|
3461
|
+
try {
|
|
3462
|
+
statuses = JSON.parse(body).statuses;
|
|
3463
|
+
} catch {
|
|
3464
|
+
}
|
|
3465
|
+
}
|
|
3466
|
+
const result = await deps.topicManager.cleanup(statuses);
|
|
3467
|
+
deps.sendJson(res, 200, result);
|
|
3468
|
+
});
|
|
3469
|
+
router.delete("/api/topics/:sessionId", async (req, res, params) => {
|
|
3470
|
+
if (!deps.topicManager) {
|
|
3471
|
+
deps.sendJson(res, 501, { error: "Topic management not available" });
|
|
3472
|
+
return;
|
|
3473
|
+
}
|
|
3474
|
+
const sessionId = decodeURIComponent(params.sessionId);
|
|
3475
|
+
const url = req.url || "";
|
|
3476
|
+
const urlParams = new URL(url, "http://localhost").searchParams;
|
|
3477
|
+
const force = urlParams.get("force") === "true";
|
|
3478
|
+
const result = await deps.topicManager.deleteTopic(
|
|
3479
|
+
sessionId,
|
|
3480
|
+
force ? { confirmed: true } : void 0
|
|
3481
|
+
);
|
|
3482
|
+
if (result.ok) {
|
|
3483
|
+
deps.sendJson(res, 200, result);
|
|
3484
|
+
} else if (result.needsConfirmation) {
|
|
3485
|
+
deps.sendJson(res, 409, {
|
|
3486
|
+
error: "Session is active",
|
|
3487
|
+
needsConfirmation: true,
|
|
3488
|
+
session: result.session
|
|
3489
|
+
});
|
|
3490
|
+
} else if (result.error === "Cannot delete system topic") {
|
|
3491
|
+
deps.sendJson(res, 403, { error: result.error });
|
|
3492
|
+
} else {
|
|
3493
|
+
deps.sendJson(res, 404, { error: result.error ?? "Not found" });
|
|
3494
|
+
}
|
|
3495
|
+
});
|
|
3496
|
+
}
|
|
3497
|
+
|
|
3498
|
+
// src/core/api/routes/tunnel.ts
|
|
3499
|
+
function registerTunnelRoutes(router, deps) {
|
|
3500
|
+
router.get("/api/tunnel", async (_req, res) => {
|
|
3501
|
+
const tunnel = deps.core.tunnelService;
|
|
2919
3502
|
if (tunnel) {
|
|
2920
|
-
|
|
3503
|
+
deps.sendJson(res, 200, {
|
|
3504
|
+
enabled: true,
|
|
3505
|
+
url: tunnel.getPublicUrl(),
|
|
3506
|
+
provider: deps.core.configManager.get().tunnel.provider
|
|
3507
|
+
});
|
|
2921
3508
|
} else {
|
|
2922
|
-
|
|
3509
|
+
deps.sendJson(res, 200, { enabled: false });
|
|
2923
3510
|
}
|
|
2924
|
-
}
|
|
2925
|
-
async
|
|
2926
|
-
const tunnel =
|
|
3511
|
+
});
|
|
3512
|
+
router.get("/api/tunnel/list", async (_req, res) => {
|
|
3513
|
+
const tunnel = deps.core.tunnelService;
|
|
2927
3514
|
if (!tunnel) {
|
|
2928
|
-
|
|
3515
|
+
deps.sendJson(res, 200, []);
|
|
2929
3516
|
return;
|
|
2930
3517
|
}
|
|
2931
|
-
|
|
2932
|
-
}
|
|
2933
|
-
async
|
|
2934
|
-
const tunnel =
|
|
3518
|
+
deps.sendJson(res, 200, tunnel.listTunnels());
|
|
3519
|
+
});
|
|
3520
|
+
router.post("/api/tunnel", async (req, res) => {
|
|
3521
|
+
const tunnel = deps.core.tunnelService;
|
|
2935
3522
|
if (!tunnel) {
|
|
2936
|
-
|
|
3523
|
+
deps.sendJson(res, 400, { error: "Tunnel service is not enabled" });
|
|
3524
|
+
return;
|
|
3525
|
+
}
|
|
3526
|
+
const body = await deps.readBody(req);
|
|
3527
|
+
if (body === null) {
|
|
3528
|
+
deps.sendJson(res, 413, { error: "Request body too large" });
|
|
2937
3529
|
return;
|
|
2938
3530
|
}
|
|
2939
|
-
const body = await this.readBody(req);
|
|
2940
3531
|
if (!body) {
|
|
2941
|
-
|
|
3532
|
+
deps.sendJson(res, 400, { error: "Missing request body" });
|
|
2942
3533
|
return;
|
|
2943
3534
|
}
|
|
2944
3535
|
try {
|
|
2945
3536
|
const { port, label, sessionId } = JSON.parse(body);
|
|
2946
3537
|
if (!port || typeof port !== "number") {
|
|
2947
|
-
|
|
3538
|
+
deps.sendJson(res, 400, {
|
|
3539
|
+
error: "port is required and must be a number"
|
|
3540
|
+
});
|
|
2948
3541
|
return;
|
|
2949
3542
|
}
|
|
2950
3543
|
const entry = await tunnel.addTunnel(port, { label, sessionId });
|
|
2951
|
-
|
|
3544
|
+
deps.sendJson(res, 200, entry);
|
|
2952
3545
|
} catch (err) {
|
|
2953
|
-
|
|
3546
|
+
deps.sendJson(res, 400, { error: err.message });
|
|
2954
3547
|
}
|
|
2955
|
-
}
|
|
2956
|
-
async
|
|
2957
|
-
const tunnel =
|
|
3548
|
+
});
|
|
3549
|
+
router.delete("/api/tunnel/:port", async (_req, res, params) => {
|
|
3550
|
+
const tunnel = deps.core.tunnelService;
|
|
2958
3551
|
if (!tunnel) {
|
|
2959
|
-
|
|
3552
|
+
deps.sendJson(res, 400, { error: "Tunnel service is not enabled" });
|
|
2960
3553
|
return;
|
|
2961
3554
|
}
|
|
3555
|
+
const port = parseInt(params.port, 10);
|
|
2962
3556
|
try {
|
|
2963
3557
|
await tunnel.stopTunnel(port);
|
|
2964
|
-
|
|
3558
|
+
deps.sendJson(res, 200, { ok: true });
|
|
2965
3559
|
} catch (err) {
|
|
2966
|
-
|
|
3560
|
+
deps.sendJson(res, 400, { error: err.message });
|
|
2967
3561
|
}
|
|
2968
|
-
}
|
|
2969
|
-
async
|
|
2970
|
-
const tunnel =
|
|
3562
|
+
});
|
|
3563
|
+
router.delete("/api/tunnel", async (_req, res) => {
|
|
3564
|
+
const tunnel = deps.core.tunnelService;
|
|
2971
3565
|
if (!tunnel) {
|
|
2972
|
-
|
|
3566
|
+
deps.sendJson(res, 400, { error: "Tunnel service is not enabled" });
|
|
2973
3567
|
return;
|
|
2974
3568
|
}
|
|
2975
3569
|
const count = tunnel.listTunnels().length;
|
|
2976
3570
|
await tunnel.stopAllUser();
|
|
2977
|
-
|
|
2978
|
-
}
|
|
2979
|
-
|
|
2980
|
-
|
|
3571
|
+
deps.sendJson(res, 200, { ok: true, stopped: count });
|
|
3572
|
+
});
|
|
3573
|
+
}
|
|
3574
|
+
|
|
3575
|
+
// src/core/api/routes/agents.ts
|
|
3576
|
+
function registerAgentRoutes(router, deps) {
|
|
3577
|
+
router.get("/api/agents", async (_req, res) => {
|
|
3578
|
+
const agents = deps.core.agentManager.getAvailableAgents();
|
|
3579
|
+
const defaultAgent = deps.core.configManager.get().defaultAgent;
|
|
3580
|
+
const agentsWithCaps = agents.map((a) => ({
|
|
3581
|
+
...a,
|
|
3582
|
+
capabilities: getAgentCapabilities(a.name)
|
|
3583
|
+
}));
|
|
3584
|
+
deps.sendJson(res, 200, { agents: agentsWithCaps, default: defaultAgent });
|
|
3585
|
+
});
|
|
3586
|
+
}
|
|
3587
|
+
|
|
3588
|
+
// src/core/api/routes/notify.ts
|
|
3589
|
+
function registerNotifyRoutes(router, deps) {
|
|
3590
|
+
router.post("/api/notify", async (req, res) => {
|
|
3591
|
+
const body = await deps.readBody(req);
|
|
2981
3592
|
let message;
|
|
2982
3593
|
if (body) {
|
|
2983
3594
|
try {
|
|
2984
3595
|
const parsed = JSON.parse(body);
|
|
2985
3596
|
message = parsed.message;
|
|
2986
3597
|
} catch {
|
|
2987
|
-
|
|
3598
|
+
deps.sendJson(res, 400, { error: "Invalid JSON body" });
|
|
2988
3599
|
return;
|
|
2989
3600
|
}
|
|
2990
3601
|
}
|
|
2991
3602
|
if (!message) {
|
|
2992
|
-
|
|
3603
|
+
deps.sendJson(res, 400, { error: "Missing message" });
|
|
2993
3604
|
return;
|
|
2994
3605
|
}
|
|
2995
|
-
await
|
|
3606
|
+
await deps.core.notificationManager.notifyAll({
|
|
2996
3607
|
sessionId: "system",
|
|
2997
3608
|
type: "completed",
|
|
2998
3609
|
summary: message
|
|
2999
3610
|
});
|
|
3000
|
-
|
|
3001
|
-
}
|
|
3002
|
-
|
|
3003
|
-
|
|
3004
|
-
|
|
3005
|
-
|
|
3006
|
-
|
|
3007
|
-
|
|
3008
|
-
|
|
3009
|
-
|
|
3010
|
-
|
|
3011
|
-
const
|
|
3012
|
-
|
|
3013
|
-
|
|
3014
|
-
|
|
3015
|
-
|
|
3016
|
-
|
|
3611
|
+
deps.sendJson(res, 200, { ok: true });
|
|
3612
|
+
});
|
|
3613
|
+
}
|
|
3614
|
+
|
|
3615
|
+
// src/core/api/index.ts
|
|
3616
|
+
var log9 = createChildLogger({ module: "api-server" });
|
|
3617
|
+
var DEFAULT_PORT_FILE = path7.join(os2.homedir(), ".openacp", "api.port");
|
|
3618
|
+
var cachedVersion;
|
|
3619
|
+
function getVersion() {
|
|
3620
|
+
if (cachedVersion) return cachedVersion;
|
|
3621
|
+
try {
|
|
3622
|
+
const __filename = fileURLToPath2(import.meta.url);
|
|
3623
|
+
const pkgPath = path7.resolve(
|
|
3624
|
+
path7.dirname(__filename),
|
|
3625
|
+
"../../../package.json"
|
|
3626
|
+
);
|
|
3627
|
+
const pkg = JSON.parse(fs7.readFileSync(pkgPath, "utf-8"));
|
|
3628
|
+
cachedVersion = pkg.version ?? "0.0.0-dev";
|
|
3629
|
+
} catch {
|
|
3630
|
+
cachedVersion = "0.0.0-dev";
|
|
3017
3631
|
}
|
|
3018
|
-
|
|
3019
|
-
|
|
3020
|
-
|
|
3021
|
-
|
|
3022
|
-
|
|
3023
|
-
|
|
3024
|
-
|
|
3025
|
-
this.
|
|
3632
|
+
return cachedVersion;
|
|
3633
|
+
}
|
|
3634
|
+
var ApiServer = class {
|
|
3635
|
+
constructor(core, config, portFilePath, topicManager, secretFilePath, uiDir) {
|
|
3636
|
+
this.core = core;
|
|
3637
|
+
this.config = config;
|
|
3638
|
+
this.topicManager = topicManager;
|
|
3639
|
+
this.portFilePath = portFilePath ?? DEFAULT_PORT_FILE;
|
|
3640
|
+
this.secretFilePath = secretFilePath ?? path7.join(os2.homedir(), ".openacp", "api-secret");
|
|
3641
|
+
this.staticServer = new StaticServer(uiDir);
|
|
3642
|
+
this.sseManager = new SSEManager(
|
|
3643
|
+
core.eventBus,
|
|
3644
|
+
() => {
|
|
3645
|
+
const sessions = this.core.sessionManager.listSessions();
|
|
3646
|
+
return {
|
|
3647
|
+
active: sessions.filter(
|
|
3648
|
+
(s) => s.status === "active" || s.status === "initializing"
|
|
3649
|
+
).length,
|
|
3650
|
+
total: sessions.length
|
|
3651
|
+
};
|
|
3652
|
+
},
|
|
3653
|
+
this.startedAt
|
|
3654
|
+
);
|
|
3655
|
+
this.router = new Router();
|
|
3656
|
+
const deps = {
|
|
3657
|
+
core: this.core,
|
|
3658
|
+
topicManager: this.topicManager,
|
|
3659
|
+
startedAt: this.startedAt,
|
|
3660
|
+
getVersion,
|
|
3661
|
+
sendJson: this.sendJson.bind(this),
|
|
3662
|
+
readBody: this.readBody.bind(this)
|
|
3663
|
+
};
|
|
3664
|
+
registerHealthRoutes(this.router, deps);
|
|
3665
|
+
registerSessionRoutes(this.router, deps);
|
|
3666
|
+
registerConfigRoutes(this.router, deps);
|
|
3667
|
+
registerTopicRoutes(this.router, deps);
|
|
3668
|
+
registerTunnelRoutes(this.router, deps);
|
|
3669
|
+
registerAgentRoutes(this.router, deps);
|
|
3670
|
+
registerNotifyRoutes(this.router, deps);
|
|
3026
3671
|
}
|
|
3027
|
-
|
|
3028
|
-
|
|
3029
|
-
|
|
3030
|
-
|
|
3031
|
-
|
|
3032
|
-
|
|
3033
|
-
|
|
3034
|
-
|
|
3035
|
-
|
|
3036
|
-
|
|
3672
|
+
server = null;
|
|
3673
|
+
actualPort = 0;
|
|
3674
|
+
portFilePath;
|
|
3675
|
+
startedAt = Date.now();
|
|
3676
|
+
secret = "";
|
|
3677
|
+
secretFilePath;
|
|
3678
|
+
sseManager;
|
|
3679
|
+
staticServer;
|
|
3680
|
+
router;
|
|
3681
|
+
async start() {
|
|
3682
|
+
this.loadOrCreateSecret();
|
|
3683
|
+
this.server = http.createServer((req, res) => this.handleRequest(req, res));
|
|
3684
|
+
await new Promise((resolve3, reject) => {
|
|
3685
|
+
this.server.on("error", (err) => {
|
|
3686
|
+
if (err.code === "EADDRINUSE") {
|
|
3687
|
+
log9.warn(
|
|
3688
|
+
{ port: this.config.port },
|
|
3689
|
+
"API port in use, continuing without API server"
|
|
3690
|
+
);
|
|
3691
|
+
this.server = null;
|
|
3692
|
+
resolve3();
|
|
3693
|
+
} else {
|
|
3694
|
+
reject(err);
|
|
3695
|
+
}
|
|
3696
|
+
});
|
|
3697
|
+
this.server.listen(this.config.port, this.config.host, () => {
|
|
3698
|
+
const addr = this.server.address();
|
|
3699
|
+
if (addr && typeof addr === "object") {
|
|
3700
|
+
this.actualPort = addr.port;
|
|
3701
|
+
}
|
|
3702
|
+
this.writePortFile();
|
|
3703
|
+
log9.info(
|
|
3704
|
+
{ host: this.config.host, port: this.actualPort },
|
|
3705
|
+
"API server listening"
|
|
3706
|
+
);
|
|
3707
|
+
this.sseManager.setup();
|
|
3708
|
+
if (this.config.host !== "127.0.0.1" && this.config.host !== "localhost") {
|
|
3709
|
+
log9.warn(
|
|
3710
|
+
"API server binding to non-localhost. Ensure api-secret file is secured."
|
|
3711
|
+
);
|
|
3712
|
+
}
|
|
3713
|
+
resolve3();
|
|
3714
|
+
});
|
|
3037
3715
|
});
|
|
3038
3716
|
}
|
|
3039
|
-
async
|
|
3040
|
-
|
|
3041
|
-
|
|
3042
|
-
|
|
3717
|
+
async stop() {
|
|
3718
|
+
this.sseManager.stop();
|
|
3719
|
+
this.removePortFile();
|
|
3720
|
+
if (this.server) {
|
|
3721
|
+
await new Promise((resolve3) => {
|
|
3722
|
+
this.server.close(() => resolve3());
|
|
3723
|
+
});
|
|
3724
|
+
this.server = null;
|
|
3043
3725
|
}
|
|
3044
|
-
|
|
3726
|
+
}
|
|
3727
|
+
getPort() {
|
|
3728
|
+
return this.actualPort;
|
|
3729
|
+
}
|
|
3730
|
+
getSecret() {
|
|
3731
|
+
return this.secret;
|
|
3732
|
+
}
|
|
3733
|
+
writePortFile() {
|
|
3734
|
+
const dir = path7.dirname(this.portFilePath);
|
|
3735
|
+
fs7.mkdirSync(dir, { recursive: true });
|
|
3736
|
+
fs7.writeFileSync(this.portFilePath, String(this.actualPort));
|
|
3737
|
+
}
|
|
3738
|
+
removePortFile() {
|
|
3045
3739
|
try {
|
|
3046
|
-
|
|
3740
|
+
fs7.unlinkSync(this.portFilePath);
|
|
3047
3741
|
} 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
3742
|
}
|
|
3061
3743
|
}
|
|
3062
|
-
|
|
3063
|
-
const
|
|
3064
|
-
|
|
3065
|
-
|
|
3066
|
-
|
|
3067
|
-
|
|
3068
|
-
|
|
3069
|
-
|
|
3070
|
-
|
|
3071
|
-
|
|
3072
|
-
|
|
3073
|
-
|
|
3074
|
-
|
|
3075
|
-
|
|
3076
|
-
|
|
3077
|
-
|
|
3078
|
-
|
|
3744
|
+
loadOrCreateSecret() {
|
|
3745
|
+
const dir = path7.dirname(this.secretFilePath);
|
|
3746
|
+
fs7.mkdirSync(dir, { recursive: true });
|
|
3747
|
+
try {
|
|
3748
|
+
this.secret = fs7.readFileSync(this.secretFilePath, "utf-8").trim();
|
|
3749
|
+
if (this.secret) {
|
|
3750
|
+
try {
|
|
3751
|
+
const stat = fs7.statSync(this.secretFilePath);
|
|
3752
|
+
const mode = stat.mode & 511;
|
|
3753
|
+
if (mode & 63) {
|
|
3754
|
+
log9.warn(
|
|
3755
|
+
{ path: this.secretFilePath, mode: "0" + mode.toString(8) },
|
|
3756
|
+
"API secret file has insecure permissions (should be 0600). Run: chmod 600 %s",
|
|
3757
|
+
this.secretFilePath
|
|
3758
|
+
);
|
|
3759
|
+
}
|
|
3760
|
+
} catch {
|
|
3761
|
+
}
|
|
3762
|
+
return;
|
|
3763
|
+
}
|
|
3764
|
+
} catch {
|
|
3079
3765
|
}
|
|
3080
|
-
|
|
3081
|
-
|
|
3082
|
-
const filter = statusParam ? { statuses: statusParam.split(",") } : void 0;
|
|
3083
|
-
const topics = this.topicManager.listTopics(filter);
|
|
3084
|
-
this.sendJson(res, 200, { topics });
|
|
3766
|
+
this.secret = crypto.randomBytes(32).toString("hex");
|
|
3767
|
+
fs7.writeFileSync(this.secretFilePath, this.secret, { mode: 384 });
|
|
3085
3768
|
}
|
|
3086
|
-
|
|
3087
|
-
|
|
3088
|
-
|
|
3089
|
-
|
|
3769
|
+
authenticate(req, allowQueryParam = false) {
|
|
3770
|
+
const authHeader = req.headers.authorization;
|
|
3771
|
+
if (authHeader?.startsWith("Bearer ")) {
|
|
3772
|
+
const token = authHeader.slice(7);
|
|
3773
|
+
if (token.length === this.secret.length && crypto.timingSafeEqual(
|
|
3774
|
+
Buffer.from(token, "utf-8"),
|
|
3775
|
+
Buffer.from(this.secret, "utf-8")
|
|
3776
|
+
)) {
|
|
3777
|
+
return true;
|
|
3778
|
+
}
|
|
3090
3779
|
}
|
|
3091
|
-
|
|
3092
|
-
|
|
3093
|
-
|
|
3094
|
-
|
|
3095
|
-
|
|
3096
|
-
|
|
3097
|
-
|
|
3098
|
-
|
|
3099
|
-
|
|
3100
|
-
} else {
|
|
3101
|
-
this.sendJson(res, 404, { error: result.error ?? "Not found" });
|
|
3780
|
+
if (allowQueryParam) {
|
|
3781
|
+
const parsedUrl = new URL(req.url || "", "http://localhost");
|
|
3782
|
+
const qToken = parsedUrl.searchParams.get("token");
|
|
3783
|
+
if (qToken && qToken.length === this.secret.length && crypto.timingSafeEqual(
|
|
3784
|
+
Buffer.from(qToken, "utf-8"),
|
|
3785
|
+
Buffer.from(this.secret, "utf-8")
|
|
3786
|
+
)) {
|
|
3787
|
+
return true;
|
|
3788
|
+
}
|
|
3102
3789
|
}
|
|
3790
|
+
return false;
|
|
3103
3791
|
}
|
|
3104
|
-
async
|
|
3105
|
-
|
|
3106
|
-
|
|
3107
|
-
|
|
3792
|
+
async handleRequest(req, res) {
|
|
3793
|
+
const method = req.method?.toUpperCase();
|
|
3794
|
+
const url = req.url || "";
|
|
3795
|
+
if (url.startsWith("/api/")) {
|
|
3796
|
+
const isExempt = method === "GET" && (url === "/api/health" || url === "/api/version" || url.startsWith("/api/events"));
|
|
3797
|
+
if (!isExempt && !this.authenticate(req)) {
|
|
3798
|
+
this.sendJson(res, 401, { error: "Unauthorized" });
|
|
3799
|
+
return;
|
|
3800
|
+
}
|
|
3108
3801
|
}
|
|
3109
|
-
|
|
3110
|
-
|
|
3111
|
-
|
|
3112
|
-
|
|
3113
|
-
|
|
3114
|
-
|
|
3802
|
+
try {
|
|
3803
|
+
if (method === "GET" && url.startsWith("/api/events")) {
|
|
3804
|
+
if (!this.authenticate(req, true)) {
|
|
3805
|
+
this.sendJson(res, 401, { error: "Unauthorized" });
|
|
3806
|
+
return;
|
|
3807
|
+
}
|
|
3808
|
+
this.sseManager.handleRequest(req, res);
|
|
3809
|
+
return;
|
|
3810
|
+
}
|
|
3811
|
+
if (url.startsWith("/api/")) {
|
|
3812
|
+
const match = this.router.match(method, url);
|
|
3813
|
+
if (match) {
|
|
3814
|
+
await match.handler(req, res, match.params);
|
|
3815
|
+
} else {
|
|
3816
|
+
this.sendJson(res, 404, { error: "Not found" });
|
|
3817
|
+
}
|
|
3818
|
+
return;
|
|
3819
|
+
}
|
|
3820
|
+
if (!this.staticServer.serve(req, res)) {
|
|
3821
|
+
this.sendJson(res, 404, { error: "Not found" });
|
|
3115
3822
|
}
|
|
3823
|
+
} catch (err) {
|
|
3824
|
+
log9.error({ err }, "API request error");
|
|
3825
|
+
this.sendJson(res, 500, { error: "Internal server error" });
|
|
3116
3826
|
}
|
|
3117
|
-
|
|
3118
|
-
|
|
3827
|
+
}
|
|
3828
|
+
sendJson(res, status, data) {
|
|
3829
|
+
res.writeHead(status, { "Content-Type": "application/json" });
|
|
3830
|
+
res.end(JSON.stringify(data));
|
|
3119
3831
|
}
|
|
3120
3832
|
readBody(req) {
|
|
3121
|
-
|
|
3833
|
+
const MAX_BODY_SIZE = 1024 * 1024;
|
|
3834
|
+
return new Promise((resolve3) => {
|
|
3122
3835
|
let data = "";
|
|
3836
|
+
let size = 0;
|
|
3837
|
+
let destroyed = false;
|
|
3123
3838
|
req.on("data", (chunk) => {
|
|
3124
|
-
|
|
3839
|
+
size += chunk.length;
|
|
3840
|
+
if (size > MAX_BODY_SIZE && !destroyed) {
|
|
3841
|
+
destroyed = true;
|
|
3842
|
+
req.destroy();
|
|
3843
|
+
resolve3(null);
|
|
3844
|
+
return;
|
|
3845
|
+
}
|
|
3846
|
+
if (!destroyed) data += chunk;
|
|
3847
|
+
});
|
|
3848
|
+
req.on("end", () => {
|
|
3849
|
+
if (!destroyed) resolve3(data);
|
|
3850
|
+
});
|
|
3851
|
+
req.on("error", () => {
|
|
3852
|
+
if (!destroyed) resolve3("");
|
|
3125
3853
|
});
|
|
3126
|
-
req.on("end", () => resolve2(data));
|
|
3127
|
-
req.on("error", () => resolve2(""));
|
|
3128
3854
|
});
|
|
3129
3855
|
}
|
|
3130
3856
|
};
|
|
3131
3857
|
|
|
3132
3858
|
// src/core/topic-manager.ts
|
|
3133
|
-
var
|
|
3859
|
+
var log10 = createChildLogger({ module: "topic-manager" });
|
|
3134
3860
|
var TopicManager = class {
|
|
3135
3861
|
constructor(sessionManager, adapter, systemTopicIds) {
|
|
3136
3862
|
this.sessionManager = sessionManager;
|
|
@@ -3169,7 +3895,7 @@ var TopicManager = class {
|
|
|
3169
3895
|
try {
|
|
3170
3896
|
await this.adapter.deleteSessionThread(sessionId);
|
|
3171
3897
|
} catch (err) {
|
|
3172
|
-
|
|
3898
|
+
log10.warn({ err, sessionId, topicId }, "Failed to delete platform thread, removing record anyway");
|
|
3173
3899
|
}
|
|
3174
3900
|
}
|
|
3175
3901
|
await this.sessionManager.removeRecord(sessionId);
|
|
@@ -3192,7 +3918,7 @@ var TopicManager = class {
|
|
|
3192
3918
|
try {
|
|
3193
3919
|
await this.adapter.deleteSessionThread(record.sessionId);
|
|
3194
3920
|
} catch (err) {
|
|
3195
|
-
|
|
3921
|
+
log10.warn({ err, sessionId: record.sessionId }, "Failed to delete platform thread during cleanup");
|
|
3196
3922
|
}
|
|
3197
3923
|
}
|
|
3198
3924
|
await this.sessionManager.removeRecord(record.sessionId);
|
|
@@ -3442,7 +4168,7 @@ function splitMessage(text, maxLength = 3800) {
|
|
|
3442
4168
|
|
|
3443
4169
|
// src/adapters/telegram/commands/admin.ts
|
|
3444
4170
|
import { InlineKeyboard } from "grammy";
|
|
3445
|
-
var
|
|
4171
|
+
var log12 = createChildLogger({ module: "telegram-cmd-admin" });
|
|
3446
4172
|
function buildDangerousModeKeyboard(sessionId, enabled) {
|
|
3447
4173
|
return new InlineKeyboard().text(
|
|
3448
4174
|
enabled ? "\u{1F510} Disable Dangerous Mode" : "\u2620\uFE0F Enable Dangerous Mode",
|
|
@@ -3455,7 +4181,7 @@ function setupDangerousModeCallbacks(bot, core) {
|
|
|
3455
4181
|
const session = core.sessionManager.getSession(sessionId);
|
|
3456
4182
|
if (session) {
|
|
3457
4183
|
session.dangerousMode = !session.dangerousMode;
|
|
3458
|
-
|
|
4184
|
+
log12.info({ sessionId, dangerousMode: session.dangerousMode }, "Dangerous mode toggled via button");
|
|
3459
4185
|
core.sessionManager.patchRecord(sessionId, { dangerousMode: session.dangerousMode }).catch(() => {
|
|
3460
4186
|
});
|
|
3461
4187
|
const toastText2 = session.dangerousMode ? "\u2620\uFE0F Dangerous mode enabled \u2014 permissions auto-approved" : "\u{1F510} Dangerous mode disabled \u2014 permissions shown normally";
|
|
@@ -3482,7 +4208,7 @@ function setupDangerousModeCallbacks(bot, core) {
|
|
|
3482
4208
|
const newDangerousMode = !(record.dangerousMode ?? false);
|
|
3483
4209
|
core.sessionManager.patchRecord(sessionId, { dangerousMode: newDangerousMode }).catch(() => {
|
|
3484
4210
|
});
|
|
3485
|
-
|
|
4211
|
+
log12.info({ sessionId, dangerousMode: newDangerousMode }, "Dangerous mode toggled via button (store-only, session not in memory)");
|
|
3486
4212
|
const toastText = newDangerousMode ? "\u2620\uFE0F Dangerous mode enabled \u2014 permissions auto-approved" : "\u{1F510} Dangerous mode disabled \u2014 permissions shown normally";
|
|
3487
4213
|
try {
|
|
3488
4214
|
await ctx.answerCallbackQuery({ text: toastText });
|
|
@@ -3563,6 +4289,65 @@ async function handleDisableDangerous(ctx, core) {
|
|
|
3563
4289
|
}
|
|
3564
4290
|
await ctx.reply("\u{1F510} <b>Dangerous mode disabled</b>\n\nPermission requests will be shown normally.", { parse_mode: "HTML" });
|
|
3565
4291
|
}
|
|
4292
|
+
function buildSessionControlKeyboard(sessionId, dangerousMode, voiceMode) {
|
|
4293
|
+
return new InlineKeyboard().text(
|
|
4294
|
+
dangerousMode ? "\u{1F510} Disable Dangerous Mode" : "\u2620\uFE0F Enable Dangerous Mode",
|
|
4295
|
+
`d:${sessionId}`
|
|
4296
|
+
).text(
|
|
4297
|
+
voiceMode ? "\u{1F50A} Text to Speech" : "\u{1F507} Text to Speech",
|
|
4298
|
+
`v:${sessionId}`
|
|
4299
|
+
);
|
|
4300
|
+
}
|
|
4301
|
+
function setupTTSCallbacks(bot, core) {
|
|
4302
|
+
bot.callbackQuery(/^v:/, async (ctx) => {
|
|
4303
|
+
const sessionId = ctx.callbackQuery.data.slice(2);
|
|
4304
|
+
const session = core.sessionManager.getSession(sessionId);
|
|
4305
|
+
if (!session) {
|
|
4306
|
+
try {
|
|
4307
|
+
await ctx.answerCallbackQuery({ text: "\u26A0\uFE0F Session not found or not active." });
|
|
4308
|
+
} catch {
|
|
4309
|
+
}
|
|
4310
|
+
return;
|
|
4311
|
+
}
|
|
4312
|
+
const newMode = session.voiceMode === "on" ? "off" : "on";
|
|
4313
|
+
session.setVoiceMode(newMode);
|
|
4314
|
+
const toastText = newMode === "on" ? "\u{1F50A} Text to Speech enabled" : "\u{1F507} Text to Speech disabled";
|
|
4315
|
+
try {
|
|
4316
|
+
await ctx.answerCallbackQuery({ text: toastText });
|
|
4317
|
+
} catch {
|
|
4318
|
+
}
|
|
4319
|
+
try {
|
|
4320
|
+
await ctx.editMessageReplyMarkup({
|
|
4321
|
+
reply_markup: buildSessionControlKeyboard(sessionId, session.dangerousMode, newMode === "on")
|
|
4322
|
+
});
|
|
4323
|
+
} catch {
|
|
4324
|
+
}
|
|
4325
|
+
});
|
|
4326
|
+
}
|
|
4327
|
+
async function handleTTS(ctx, core) {
|
|
4328
|
+
const threadId = ctx.message?.message_thread_id;
|
|
4329
|
+
if (!threadId) {
|
|
4330
|
+
await ctx.reply("\u26A0\uFE0F This command only works inside a session topic.", { parse_mode: "HTML" });
|
|
4331
|
+
return;
|
|
4332
|
+
}
|
|
4333
|
+
const session = await core.getOrResumeSession("telegram", String(threadId));
|
|
4334
|
+
if (!session) {
|
|
4335
|
+
await ctx.reply("\u26A0\uFE0F No active session in this topic.", { parse_mode: "HTML" });
|
|
4336
|
+
return;
|
|
4337
|
+
}
|
|
4338
|
+
const args = ctx.message?.text?.split(/\s+/).slice(1) ?? [];
|
|
4339
|
+
const arg = args[0]?.toLowerCase();
|
|
4340
|
+
if (arg === "on") {
|
|
4341
|
+
session.setVoiceMode("on");
|
|
4342
|
+
await ctx.reply("\u{1F50A} Text to Speech enabled for this session.", { parse_mode: "HTML" });
|
|
4343
|
+
} else if (arg === "off") {
|
|
4344
|
+
session.setVoiceMode("off");
|
|
4345
|
+
await ctx.reply("\u{1F507} Text to Speech disabled.", { parse_mode: "HTML" });
|
|
4346
|
+
} else {
|
|
4347
|
+
session.setVoiceMode("next");
|
|
4348
|
+
await ctx.reply("\u{1F50A} Text to Speech enabled for the next message.", { parse_mode: "HTML" });
|
|
4349
|
+
}
|
|
4350
|
+
}
|
|
3566
4351
|
async function handleUpdate(ctx, core) {
|
|
3567
4352
|
if (!core.requestRestart) {
|
|
3568
4353
|
await ctx.reply("\u26A0\uFE0F Update is not available (no restart handler registered).", { parse_mode: "HTML" });
|
|
@@ -3611,7 +4396,7 @@ async function handleRestart(ctx, core) {
|
|
|
3611
4396
|
}
|
|
3612
4397
|
|
|
3613
4398
|
// src/adapters/telegram/commands/new-session.ts
|
|
3614
|
-
var
|
|
4399
|
+
var log13 = createChildLogger({ module: "telegram-cmd-new-session" });
|
|
3615
4400
|
var pendingNewSessions = /* @__PURE__ */ new Map();
|
|
3616
4401
|
var PENDING_TIMEOUT_MS = 5 * 60 * 1e3;
|
|
3617
4402
|
function cleanupPending(userId) {
|
|
@@ -3710,7 +4495,7 @@ async function startConfirmStep(ctx, chatId, userId, agentName, workspace) {
|
|
|
3710
4495
|
});
|
|
3711
4496
|
}
|
|
3712
4497
|
async function createSessionDirect(ctx, core, chatId, agentName, workspace) {
|
|
3713
|
-
|
|
4498
|
+
log13.info({ userId: ctx.from?.id, agentName, workspace }, "New session command (direct)");
|
|
3714
4499
|
let threadId;
|
|
3715
4500
|
try {
|
|
3716
4501
|
const topicName = `\u{1F504} New Session`;
|
|
@@ -3737,13 +4522,13 @@ This is your coding session \u2014 chat here to work with the agent.`,
|
|
|
3737
4522
|
{
|
|
3738
4523
|
message_thread_id: threadId,
|
|
3739
4524
|
parse_mode: "HTML",
|
|
3740
|
-
reply_markup:
|
|
4525
|
+
reply_markup: buildSessionControlKeyboard(session.id, false, false)
|
|
3741
4526
|
}
|
|
3742
4527
|
);
|
|
3743
|
-
session.warmup().catch((err) =>
|
|
4528
|
+
session.warmup().catch((err) => log13.error({ err }, "Warm-up error"));
|
|
3744
4529
|
return threadId ?? null;
|
|
3745
4530
|
} catch (err) {
|
|
3746
|
-
|
|
4531
|
+
log13.error({ err }, "Session creation failed");
|
|
3747
4532
|
if (threadId) {
|
|
3748
4533
|
try {
|
|
3749
4534
|
await ctx.api.deleteForumTopic(chatId, threadId);
|
|
@@ -3818,10 +4603,10 @@ async function handleNewChat(ctx, core, chatId) {
|
|
|
3818
4603
|
{
|
|
3819
4604
|
message_thread_id: newThreadId,
|
|
3820
4605
|
parse_mode: "HTML",
|
|
3821
|
-
reply_markup:
|
|
4606
|
+
reply_markup: buildSessionControlKeyboard(session.id, false, false)
|
|
3822
4607
|
}
|
|
3823
4608
|
);
|
|
3824
|
-
session.warmup().catch((err) =>
|
|
4609
|
+
session.warmup().catch((err) => log13.error({ err }, "Warm-up error"));
|
|
3825
4610
|
} catch (err) {
|
|
3826
4611
|
if (newThreadId) {
|
|
3827
4612
|
try {
|
|
@@ -3852,7 +4637,7 @@ async function executeNewSession(bot, core, chatId, agentName, workspace) {
|
|
|
3852
4637
|
} });
|
|
3853
4638
|
const finalName = `\u{1F504} ${session.agentName} \u2014 New Session`;
|
|
3854
4639
|
await renameSessionTopic(bot, chatId, threadId, finalName);
|
|
3855
|
-
session.warmup().catch((err) =>
|
|
4640
|
+
session.warmup().catch((err) => log13.error({ err }, "Warm-up error"));
|
|
3856
4641
|
return { session, threadId, firstMsgId };
|
|
3857
4642
|
} catch (err) {
|
|
3858
4643
|
try {
|
|
@@ -4000,7 +4785,7 @@ Or just the folder name like <code>my-project</code> (will use ${core.configMana
|
|
|
4000
4785
|
|
|
4001
4786
|
// src/adapters/telegram/commands/session.ts
|
|
4002
4787
|
import { InlineKeyboard as InlineKeyboard3 } from "grammy";
|
|
4003
|
-
var
|
|
4788
|
+
var log14 = createChildLogger({ module: "telegram-cmd-session" });
|
|
4004
4789
|
async function handleCancel(ctx, core, assistant) {
|
|
4005
4790
|
const threadId = ctx.message?.message_thread_id;
|
|
4006
4791
|
if (!threadId) return;
|
|
@@ -4018,14 +4803,14 @@ async function handleCancel(ctx, core, assistant) {
|
|
|
4018
4803
|
String(threadId)
|
|
4019
4804
|
);
|
|
4020
4805
|
if (session) {
|
|
4021
|
-
|
|
4806
|
+
log14.info({ sessionId: session.id }, "Abort prompt command");
|
|
4022
4807
|
await session.abortPrompt();
|
|
4023
4808
|
await ctx.reply("\u26D4 Prompt aborted. Session is still active \u2014 send a new message to continue.", { parse_mode: "HTML" });
|
|
4024
4809
|
return;
|
|
4025
4810
|
}
|
|
4026
4811
|
const record = core.sessionManager.getRecordByThread("telegram", String(threadId));
|
|
4027
4812
|
if (record && record.status !== "error") {
|
|
4028
|
-
|
|
4813
|
+
log14.info({ sessionId: record.sessionId, status: record.status }, "Cancel command \u2014 no active prompt to abort");
|
|
4029
4814
|
await ctx.reply("\u2139\uFE0F No active prompt to cancel. Send a new message to resume the session.", { parse_mode: "HTML" });
|
|
4030
4815
|
}
|
|
4031
4816
|
}
|
|
@@ -4129,7 +4914,7 @@ ${lines.join("\n")}${truncated}`,
|
|
|
4129
4914
|
{ parse_mode: "HTML", reply_markup: keyboard }
|
|
4130
4915
|
);
|
|
4131
4916
|
} catch (err) {
|
|
4132
|
-
|
|
4917
|
+
log14.error({ err }, "handleTopics error");
|
|
4133
4918
|
await ctx.reply("\u274C Failed to list sessions.", { parse_mode: "HTML" }).catch(() => {
|
|
4134
4919
|
});
|
|
4135
4920
|
}
|
|
@@ -4150,13 +4935,13 @@ async function handleCleanup(ctx, core, chatId, statuses) {
|
|
|
4150
4935
|
try {
|
|
4151
4936
|
await ctx.api.deleteForumTopic(chatId, topicId);
|
|
4152
4937
|
} catch (err) {
|
|
4153
|
-
|
|
4938
|
+
log14.warn({ err, sessionId: record.sessionId, topicId }, "Failed to delete forum topic during cleanup");
|
|
4154
4939
|
}
|
|
4155
4940
|
}
|
|
4156
4941
|
await core.sessionManager.removeRecord(record.sessionId);
|
|
4157
4942
|
deleted++;
|
|
4158
4943
|
} catch (err) {
|
|
4159
|
-
|
|
4944
|
+
log14.error({ err, sessionId: record.sessionId }, "Failed to cleanup session");
|
|
4160
4945
|
failed++;
|
|
4161
4946
|
}
|
|
4162
4947
|
}
|
|
@@ -4227,7 +5012,7 @@ async function handleCleanupEverythingConfirmed(ctx, core, chatId, systemTopicId
|
|
|
4227
5012
|
try {
|
|
4228
5013
|
await core.sessionManager.cancelSession(record.sessionId);
|
|
4229
5014
|
} catch (err) {
|
|
4230
|
-
|
|
5015
|
+
log14.warn({ err, sessionId: record.sessionId }, "Failed to cancel session during cleanup");
|
|
4231
5016
|
}
|
|
4232
5017
|
}
|
|
4233
5018
|
const topicId = record.platform?.topicId;
|
|
@@ -4235,13 +5020,13 @@ async function handleCleanupEverythingConfirmed(ctx, core, chatId, systemTopicId
|
|
|
4235
5020
|
try {
|
|
4236
5021
|
await ctx.api.deleteForumTopic(chatId, topicId);
|
|
4237
5022
|
} catch (err) {
|
|
4238
|
-
|
|
5023
|
+
log14.warn({ err, sessionId: record.sessionId, topicId }, "Failed to delete forum topic during cleanup");
|
|
4239
5024
|
}
|
|
4240
5025
|
}
|
|
4241
5026
|
await core.sessionManager.removeRecord(record.sessionId);
|
|
4242
5027
|
deleted++;
|
|
4243
5028
|
} catch (err) {
|
|
4244
|
-
|
|
5029
|
+
log14.error({ err, sessionId: record.sessionId }, "Failed to cleanup session");
|
|
4245
5030
|
failed++;
|
|
4246
5031
|
}
|
|
4247
5032
|
}
|
|
@@ -4566,7 +5351,7 @@ Downloading... ${bar} ${percent}%`, { parse_mode: "HTML" });
|
|
|
4566
5351
|
const { getAgentCapabilities: getAgentCapabilities2 } = await import("./agent-dependencies-QY5QSULV.js");
|
|
4567
5352
|
const caps = getAgentCapabilities2(result.agentKey);
|
|
4568
5353
|
if (caps.integration) {
|
|
4569
|
-
const { installIntegration } = await import("./integrate-
|
|
5354
|
+
const { installIntegration } = await import("./integrate-QTK4PPYQ.js");
|
|
4570
5355
|
const intResult = await installIntegration(result.agentKey, caps.integration);
|
|
4571
5356
|
if (intResult.success) {
|
|
4572
5357
|
try {
|
|
@@ -4636,7 +5421,7 @@ function buildProgressBar(percent) {
|
|
|
4636
5421
|
// src/adapters/telegram/commands/integrate.ts
|
|
4637
5422
|
import { InlineKeyboard as InlineKeyboard5 } from "grammy";
|
|
4638
5423
|
async function handleIntegrate(ctx, _core) {
|
|
4639
|
-
const { listIntegrations } = await import("./integrate-
|
|
5424
|
+
const { listIntegrations } = await import("./integrate-QTK4PPYQ.js");
|
|
4640
5425
|
const agents = listIntegrations();
|
|
4641
5426
|
const keyboard = new InlineKeyboard5();
|
|
4642
5427
|
for (const agent of agents) {
|
|
@@ -4669,7 +5454,7 @@ function setupIntegrateCallbacks(bot, core) {
|
|
|
4669
5454
|
} catch {
|
|
4670
5455
|
}
|
|
4671
5456
|
if (data === "i:back") {
|
|
4672
|
-
const { listIntegrations } = await import("./integrate-
|
|
5457
|
+
const { listIntegrations } = await import("./integrate-QTK4PPYQ.js");
|
|
4673
5458
|
const agents = listIntegrations();
|
|
4674
5459
|
const keyboard2 = new InlineKeyboard5();
|
|
4675
5460
|
for (const agent of agents) {
|
|
@@ -4689,7 +5474,7 @@ Select an agent to manage its integrations.`,
|
|
|
4689
5474
|
const agentMatch = data.match(/^i:agent:(.+)$/);
|
|
4690
5475
|
if (agentMatch) {
|
|
4691
5476
|
const agentName2 = agentMatch[1];
|
|
4692
|
-
const { getIntegration: getIntegration2 } = await import("./integrate-
|
|
5477
|
+
const { getIntegration: getIntegration2 } = await import("./integrate-QTK4PPYQ.js");
|
|
4693
5478
|
const integration2 = getIntegration2(agentName2);
|
|
4694
5479
|
if (!integration2) {
|
|
4695
5480
|
await ctx.reply(`\u274C No integration available for '${escapeHtml(agentName2)}'.`, { parse_mode: "HTML" });
|
|
@@ -4716,7 +5501,7 @@ ${integration2.items.map((i) => `\u2022 <b>${escapeHtml(i.name)}</b> \u2014 ${es
|
|
|
4716
5501
|
const action = actionMatch[1];
|
|
4717
5502
|
const agentName = actionMatch[2];
|
|
4718
5503
|
const itemId = actionMatch[3];
|
|
4719
|
-
const { getIntegration } = await import("./integrate-
|
|
5504
|
+
const { getIntegration } = await import("./integrate-QTK4PPYQ.js");
|
|
4720
5505
|
const integration = getIntegration(agentName);
|
|
4721
5506
|
if (!integration) return;
|
|
4722
5507
|
const item = integration.items.find((i) => i.id === itemId);
|
|
@@ -4753,7 +5538,7 @@ ${resultText}`,
|
|
|
4753
5538
|
|
|
4754
5539
|
// src/adapters/telegram/commands/settings.ts
|
|
4755
5540
|
import { InlineKeyboard as InlineKeyboard6 } from "grammy";
|
|
4756
|
-
var
|
|
5541
|
+
var log15 = createChildLogger({ module: "telegram-settings" });
|
|
4757
5542
|
function buildSettingsKeyboard(core) {
|
|
4758
5543
|
const config = core.configManager.get();
|
|
4759
5544
|
const fields = getSafeFields();
|
|
@@ -4816,7 +5601,7 @@ function setupSettingsCallbacks(bot, core, getAssistantSession) {
|
|
|
4816
5601
|
} catch {
|
|
4817
5602
|
}
|
|
4818
5603
|
} catch (err) {
|
|
4819
|
-
|
|
5604
|
+
log15.error({ err, fieldPath }, "Failed to toggle config");
|
|
4820
5605
|
try {
|
|
4821
5606
|
await ctx.answerCallbackQuery({ text: "\u274C Failed to update" });
|
|
4822
5607
|
} catch {
|
|
@@ -4890,7 +5675,7 @@ Tap to change:`, {
|
|
|
4890
5675
|
} catch {
|
|
4891
5676
|
}
|
|
4892
5677
|
} catch (err) {
|
|
4893
|
-
|
|
5678
|
+
log15.error({ err, fieldPath }, "Failed to set config");
|
|
4894
5679
|
try {
|
|
4895
5680
|
await ctx.answerCallbackQuery({ text: "\u274C Failed to update" });
|
|
4896
5681
|
} catch {
|
|
@@ -4962,7 +5747,7 @@ function buildNestedUpdate(dotPath, value) {
|
|
|
4962
5747
|
|
|
4963
5748
|
// src/adapters/telegram/commands/doctor.ts
|
|
4964
5749
|
import { InlineKeyboard as InlineKeyboard7 } from "grammy";
|
|
4965
|
-
var
|
|
5750
|
+
var log16 = createChildLogger({ module: "telegram-cmd-doctor" });
|
|
4966
5751
|
var pendingFixesStore = /* @__PURE__ */ new Map();
|
|
4967
5752
|
function renderReport(report) {
|
|
4968
5753
|
const icons = { pass: "\u2705", warn: "\u26A0\uFE0F", fail: "\u274C" };
|
|
@@ -5005,7 +5790,7 @@ async function handleDoctor(ctx) {
|
|
|
5005
5790
|
reply_markup: keyboard
|
|
5006
5791
|
});
|
|
5007
5792
|
} catch (err) {
|
|
5008
|
-
|
|
5793
|
+
log16.error({ err }, "Doctor command failed");
|
|
5009
5794
|
await ctx.api.editMessageText(
|
|
5010
5795
|
ctx.chat.id,
|
|
5011
5796
|
statusMsg.message_id,
|
|
@@ -5054,7 +5839,7 @@ function setupDoctorCallbacks(bot) {
|
|
|
5054
5839
|
}
|
|
5055
5840
|
}
|
|
5056
5841
|
} catch (err) {
|
|
5057
|
-
|
|
5842
|
+
log16.error({ err, index }, "Doctor fix callback failed");
|
|
5058
5843
|
}
|
|
5059
5844
|
});
|
|
5060
5845
|
bot.callbackQuery("m:doctor", async (ctx) => {
|
|
@@ -5068,7 +5853,7 @@ function setupDoctorCallbacks(bot) {
|
|
|
5068
5853
|
|
|
5069
5854
|
// src/adapters/telegram/commands/tunnel.ts
|
|
5070
5855
|
import { InlineKeyboard as InlineKeyboard8 } from "grammy";
|
|
5071
|
-
var
|
|
5856
|
+
var log17 = createChildLogger({ module: "telegram-cmd-tunnel" });
|
|
5072
5857
|
async function handleTunnel(ctx, core) {
|
|
5073
5858
|
if (!core.tunnelService) {
|
|
5074
5859
|
await ctx.reply("\u274C Tunnel service is not enabled.", { parse_mode: "HTML" });
|
|
@@ -5244,6 +6029,7 @@ function setupCommands(bot, core, chatId, assistant) {
|
|
|
5244
6029
|
bot.command("tunnel", (ctx) => handleTunnel(ctx, core));
|
|
5245
6030
|
bot.command("tunnels", (ctx) => handleTunnels(ctx, core));
|
|
5246
6031
|
bot.command("archive", (ctx) => handleArchive(ctx, core));
|
|
6032
|
+
bot.command("text_to_speech", (ctx) => handleTTS(ctx, core));
|
|
5247
6033
|
}
|
|
5248
6034
|
function setupAllCallbacks(bot, core, chatId, systemTopicIds, getAssistantSession) {
|
|
5249
6035
|
setupNewSessionCallbacks(bot, core, chatId);
|
|
@@ -5316,13 +6102,14 @@ var STATIC_COMMANDS = [
|
|
|
5316
6102
|
{ command: "usage", description: "View token usage and cost report" },
|
|
5317
6103
|
{ command: "tunnel", description: "Create/stop tunnel for a local port" },
|
|
5318
6104
|
{ command: "tunnels", description: "List active tunnels" },
|
|
5319
|
-
{ command: "archive", description: "Archive session topic (recreate with clean history)" }
|
|
6105
|
+
{ command: "archive", description: "Archive session topic (recreate with clean history)" },
|
|
6106
|
+
{ command: "text_to_speech", description: "Toggle Text to Speech (/text_to_speech on, /text_to_speech off)" }
|
|
5320
6107
|
];
|
|
5321
6108
|
|
|
5322
6109
|
// src/adapters/telegram/permissions.ts
|
|
5323
6110
|
import { InlineKeyboard as InlineKeyboard9 } from "grammy";
|
|
5324
6111
|
import { nanoid as nanoid3 } from "nanoid";
|
|
5325
|
-
var
|
|
6112
|
+
var log18 = createChildLogger({ module: "telegram-permissions" });
|
|
5326
6113
|
var PermissionHandler = class {
|
|
5327
6114
|
constructor(bot, chatId, getSession, sendNotification) {
|
|
5328
6115
|
this.bot = bot;
|
|
@@ -5382,7 +6169,7 @@ ${escapeHtml(request.description)}`,
|
|
|
5382
6169
|
}
|
|
5383
6170
|
const session = this.getSession(pending.sessionId);
|
|
5384
6171
|
const isAllow = pending.options.find((o) => o.id === optionId)?.isAllow ?? false;
|
|
5385
|
-
|
|
6172
|
+
log18.info({ requestId: pending.requestId, optionId, isAllow }, "Permission responded");
|
|
5386
6173
|
if (session?.permissionGate.requestId === pending.requestId) {
|
|
5387
6174
|
session.permissionGate.resolve(optionId);
|
|
5388
6175
|
}
|
|
@@ -5400,10 +6187,10 @@ ${escapeHtml(request.description)}`,
|
|
|
5400
6187
|
};
|
|
5401
6188
|
|
|
5402
6189
|
// src/adapters/telegram/assistant.ts
|
|
5403
|
-
var
|
|
6190
|
+
var log19 = createChildLogger({ module: "telegram-assistant" });
|
|
5404
6191
|
async function spawnAssistant(core, adapter, assistantTopicId) {
|
|
5405
6192
|
const config = core.configManager.get();
|
|
5406
|
-
|
|
6193
|
+
log19.info({ agent: config.defaultAgent }, "Creating assistant session...");
|
|
5407
6194
|
const session = await core.createSession({
|
|
5408
6195
|
channelId: "telegram",
|
|
5409
6196
|
agentName: config.defaultAgent,
|
|
@@ -5412,7 +6199,7 @@ async function spawnAssistant(core, adapter, assistantTopicId) {
|
|
|
5412
6199
|
// Prevent auto-naming from triggering after system prompt
|
|
5413
6200
|
});
|
|
5414
6201
|
session.threadId = String(assistantTopicId);
|
|
5415
|
-
|
|
6202
|
+
log19.info({ sessionId: session.id }, "Assistant agent spawned");
|
|
5416
6203
|
const allRecords = core.sessionManager.listRecords();
|
|
5417
6204
|
const activeCount = allRecords.filter((r) => r.status === "active" || r.status === "initializing").length;
|
|
5418
6205
|
const statusCounts = /* @__PURE__ */ new Map();
|
|
@@ -5433,9 +6220,9 @@ async function spawnAssistant(core, adapter, assistantTopicId) {
|
|
|
5433
6220
|
};
|
|
5434
6221
|
const systemPrompt = buildAssistantSystemPrompt(ctx);
|
|
5435
6222
|
const ready = session.enqueuePrompt(systemPrompt).then(() => {
|
|
5436
|
-
|
|
6223
|
+
log19.info({ sessionId: session.id }, "Assistant system prompt completed");
|
|
5437
6224
|
}).catch((err) => {
|
|
5438
|
-
|
|
6225
|
+
log19.warn({ err }, "Assistant system prompt failed");
|
|
5439
6226
|
});
|
|
5440
6227
|
return { session, ready };
|
|
5441
6228
|
}
|
|
@@ -5601,7 +6388,7 @@ function redirectToAssistant(chatId, assistantTopicId) {
|
|
|
5601
6388
|
}
|
|
5602
6389
|
|
|
5603
6390
|
// src/adapters/telegram/activity.ts
|
|
5604
|
-
var
|
|
6391
|
+
var log20 = createChildLogger({ module: "telegram:activity" });
|
|
5605
6392
|
var THINKING_REFRESH_MS = 15e3;
|
|
5606
6393
|
var THINKING_MAX_MS = 3 * 60 * 1e3;
|
|
5607
6394
|
var ThinkingIndicator = class {
|
|
@@ -5633,7 +6420,7 @@ var ThinkingIndicator = class {
|
|
|
5633
6420
|
this.startRefreshTimer();
|
|
5634
6421
|
}
|
|
5635
6422
|
} catch (err) {
|
|
5636
|
-
|
|
6423
|
+
log20.warn({ err }, "ThinkingIndicator.show() failed");
|
|
5637
6424
|
} finally {
|
|
5638
6425
|
this.sending = false;
|
|
5639
6426
|
}
|
|
@@ -5706,7 +6493,7 @@ var UsageMessage = class {
|
|
|
5706
6493
|
if (result) this.msgId = result.message_id;
|
|
5707
6494
|
}
|
|
5708
6495
|
} catch (err) {
|
|
5709
|
-
|
|
6496
|
+
log20.warn({ err }, "UsageMessage.send() failed");
|
|
5710
6497
|
}
|
|
5711
6498
|
}
|
|
5712
6499
|
getMsgId() {
|
|
@@ -5719,7 +6506,7 @@ var UsageMessage = class {
|
|
|
5719
6506
|
try {
|
|
5720
6507
|
await this.sendQueue.enqueue(() => this.api.deleteMessage(this.chatId, id));
|
|
5721
6508
|
} catch (err) {
|
|
5722
|
-
|
|
6509
|
+
log20.warn({ err }, "UsageMessage.delete() failed");
|
|
5723
6510
|
}
|
|
5724
6511
|
}
|
|
5725
6512
|
};
|
|
@@ -5805,7 +6592,7 @@ var PlanCard = class {
|
|
|
5805
6592
|
if (result) this.msgId = result.message_id;
|
|
5806
6593
|
}
|
|
5807
6594
|
} catch (err) {
|
|
5808
|
-
|
|
6595
|
+
log20.warn({ err }, "PlanCard flush failed");
|
|
5809
6596
|
}
|
|
5810
6597
|
}
|
|
5811
6598
|
};
|
|
@@ -5868,7 +6655,7 @@ var ActivityTracker = class {
|
|
|
5868
6655
|
})
|
|
5869
6656
|
);
|
|
5870
6657
|
} catch (err) {
|
|
5871
|
-
|
|
6658
|
+
log20.warn({ err }, "ActivityTracker.onComplete() Done send failed");
|
|
5872
6659
|
}
|
|
5873
6660
|
}
|
|
5874
6661
|
}
|
|
@@ -5895,19 +6682,19 @@ var TelegramSendQueue = class {
|
|
|
5895
6682
|
enqueue(fn, opts) {
|
|
5896
6683
|
const type = opts?.type ?? "other";
|
|
5897
6684
|
const key = opts?.key;
|
|
5898
|
-
return new Promise((
|
|
6685
|
+
return new Promise((resolve3, reject) => {
|
|
5899
6686
|
if (type === "text" && key) {
|
|
5900
6687
|
const idx = this.items.findIndex(
|
|
5901
6688
|
(item) => item.type === "text" && item.key === key
|
|
5902
6689
|
);
|
|
5903
6690
|
if (idx !== -1) {
|
|
5904
6691
|
this.items[idx].resolve(void 0);
|
|
5905
|
-
this.items[idx] = { fn, type, key, resolve:
|
|
6692
|
+
this.items[idx] = { fn, type, key, resolve: resolve3, reject };
|
|
5906
6693
|
this.scheduleProcess();
|
|
5907
6694
|
return;
|
|
5908
6695
|
}
|
|
5909
6696
|
}
|
|
5910
|
-
this.items.push({ fn, type, key, resolve:
|
|
6697
|
+
this.items.push({ fn, type, key, resolve: resolve3, reject });
|
|
5911
6698
|
this.scheduleProcess();
|
|
5912
6699
|
});
|
|
5913
6700
|
}
|
|
@@ -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 = `https://t.me/c/${numericId}/${usageMsgId ?? 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-IFTYEG5J.js.map
|