@openacp/cli 0.5.3 → 0.6.1
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/README.md +51 -16
- package/dist/action-detect-6M5GCGAU.js +15 -0
- package/dist/admin-IKPS5PFC.js +16 -0
- package/dist/agents-55NX3DHM.js +14 -0
- package/dist/{api-client-UN7BXQOQ.js → api-client-BH2JFHQW.js} +4 -2
- package/dist/{autostart-K73RQZVV.js → autostart-A7JRU4WJ.js} +6 -2
- package/dist/{chunk-ECBD5I5R.js → chunk-2KJC3ILH.js} +123 -16
- package/dist/chunk-2KJC3ILH.js.map +1 -0
- package/dist/{chunk-2Z2XPUD5.js → chunk-4LFDEW22.js} +53 -5
- package/dist/chunk-4LFDEW22.js.map +1 -0
- package/dist/{chunk-Z46LGZ7R.js → chunk-4TR5Y3MP.js} +18 -1
- package/dist/chunk-4TR5Y3MP.js.map +1 -0
- package/dist/chunk-7G5QKLLF.js +105 -0
- package/dist/chunk-7G5QKLLF.js.map +1 -0
- package/dist/{chunk-IURZ4QHG.js → chunk-7QJS2XBD.js} +2 -1
- package/dist/chunk-7QJS2XBD.js.map +1 -0
- package/dist/chunk-AKIU4JBF.js +145 -0
- package/dist/chunk-AKIU4JBF.js.map +1 -0
- package/dist/{chunk-KSIQZC3J.js → chunk-EVFJW45N.js} +1 -1
- package/dist/chunk-EVFJW45N.js.map +1 -0
- package/dist/chunk-GINCOFNW.js +134 -0
- package/dist/chunk-GINCOFNW.js.map +1 -0
- package/dist/chunk-H7ZMPBZC.js +203 -0
- package/dist/chunk-H7ZMPBZC.js.map +1 -0
- package/dist/chunk-I7WC6E5S.js +71 -0
- package/dist/chunk-I7WC6E5S.js.map +1 -0
- package/dist/{chunk-6DAZSKE5.js → chunk-IMILOCR5.js} +2 -2
- package/dist/chunk-LGQYTK55.js +442 -0
- package/dist/chunk-LGQYTK55.js.map +1 -0
- package/dist/{chunk-X6LLG7XN.js → chunk-PMGNLNSH.js} +15 -6
- package/dist/chunk-PMGNLNSH.js.map +1 -0
- package/dist/{chunk-LCJIPE5S.js → chunk-R3UJUOXI.js} +889 -591
- package/dist/chunk-R3UJUOXI.js.map +1 -0
- package/dist/chunk-SM3G6UAX.js +122 -0
- package/dist/chunk-SM3G6UAX.js.map +1 -0
- package/dist/chunk-T22OLSET.js +265 -0
- package/dist/chunk-T22OLSET.js.map +1 -0
- package/dist/chunk-THBR6OXH.js +62 -0
- package/dist/chunk-THBR6OXH.js.map +1 -0
- package/dist/{chunk-5KYLXEG3.js → chunk-TOZQ3JFN.js} +52 -9
- package/dist/chunk-TOZQ3JFN.js.map +1 -0
- package/dist/{chunk-IQIPQTQT.js → chunk-UB7XUO7C.js} +171 -26
- package/dist/chunk-UB7XUO7C.js.map +1 -0
- package/dist/{chunk-OORPX73T.js → chunk-W3EYKZNQ.js} +17 -2
- package/dist/chunk-W3EYKZNQ.js.map +1 -0
- package/dist/{chunk-K53OZH5Y.js → chunk-ZCHNAM3B.js} +76 -2
- package/dist/chunk-ZCHNAM3B.js.map +1 -0
- package/dist/cli.js +30 -29
- package/dist/cli.js.map +1 -1
- package/dist/{config-OH26EIWN.js → config-AK2W3E67.js} +2 -2
- package/dist/config-editor-VIA7A72X.js +12 -0
- package/dist/{config-registry-SNKA2EH2.js → config-registry-QQOJ2GQP.js} +2 -2
- package/dist/{daemon-VKCONJUY.js → daemon-G27YZUWB.js} +3 -3
- package/dist/discord-2DKRH45T.js +2044 -0
- package/dist/discord-2DKRH45T.js.map +1 -0
- package/dist/doctor-AN6AZ3PF.js +9 -0
- package/dist/doctor-CHCYUTV5.js +14 -0
- package/dist/doctor-CHCYUTV5.js.map +1 -0
- package/dist/index.d.ts +331 -6
- package/dist/index.js +21 -11
- package/dist/{main-NEYPQHB4.js → main-56SPFYW4.js} +32 -24
- package/dist/main-56SPFYW4.js.map +1 -0
- package/dist/{menu-J5YVH665.js → menu-XR2GET2B.js} +2 -2
- package/dist/menu-XR2GET2B.js.map +1 -0
- package/dist/new-session-DRRP2J7E.js +16 -0
- package/dist/new-session-DRRP2J7E.js.map +1 -0
- package/dist/session-FVFLBREJ.js +19 -0
- package/dist/session-FVFLBREJ.js.map +1 -0
- package/dist/settings-LPOLJ6SA.js +12 -0
- package/dist/settings-LPOLJ6SA.js.map +1 -0
- package/dist/{setup-ZCWGOEAH.js → setup-IPWJCIJM.js} +9 -5
- package/dist/setup-IPWJCIJM.js.map +1 -0
- package/dist/{version-VC5CPXBX.js → version-ALWGGVKM.js} +2 -2
- package/dist/version-ALWGGVKM.js.map +1 -0
- package/package.json +2 -1
- package/dist/chunk-2Z2XPUD5.js.map +0 -1
- package/dist/chunk-5KYLXEG3.js.map +0 -1
- package/dist/chunk-ECBD5I5R.js.map +0 -1
- package/dist/chunk-IQIPQTQT.js.map +0 -1
- package/dist/chunk-IURZ4QHG.js.map +0 -1
- package/dist/chunk-K53OZH5Y.js.map +0 -1
- package/dist/chunk-KSIQZC3J.js.map +0 -1
- package/dist/chunk-LCJIPE5S.js.map +0 -1
- package/dist/chunk-OORPX73T.js.map +0 -1
- package/dist/chunk-X6LLG7XN.js.map +0 -1
- package/dist/chunk-Z46LGZ7R.js.map +0 -1
- package/dist/config-editor-5TICUK3K.js +0 -12
- package/dist/doctor-X6UCE7GQ.js +0 -9
- package/dist/main-NEYPQHB4.js.map +0 -1
- /package/dist/{api-client-UN7BXQOQ.js.map → action-detect-6M5GCGAU.js.map} +0 -0
- /package/dist/{autostart-K73RQZVV.js.map → admin-IKPS5PFC.js.map} +0 -0
- /package/dist/{config-OH26EIWN.js.map → agents-55NX3DHM.js.map} +0 -0
- /package/dist/{config-editor-5TICUK3K.js.map → api-client-BH2JFHQW.js.map} +0 -0
- /package/dist/{config-registry-SNKA2EH2.js.map → autostart-A7JRU4WJ.js.map} +0 -0
- /package/dist/{chunk-6DAZSKE5.js.map → chunk-IMILOCR5.js.map} +0 -0
- /package/dist/{daemon-VKCONJUY.js.map → config-AK2W3E67.js.map} +0 -0
- /package/dist/{doctor-X6UCE7GQ.js.map → config-editor-VIA7A72X.js.map} +0 -0
- /package/dist/{menu-J5YVH665.js.map → config-registry-QQOJ2GQP.js.map} +0 -0
- /package/dist/{setup-ZCWGOEAH.js.map → daemon-G27YZUWB.js.map} +0 -0
- /package/dist/{version-VC5CPXBX.js.map → doctor-AN6AZ3PF.js.map} +0 -0
|
@@ -1,13 +1,17 @@
|
|
|
1
|
+
import {
|
|
2
|
+
ChannelAdapter,
|
|
3
|
+
PRODUCT_GUIDE
|
|
4
|
+
} from "./chunk-LGQYTK55.js";
|
|
1
5
|
import {
|
|
2
6
|
DoctorEngine
|
|
3
|
-
} from "./chunk-
|
|
7
|
+
} from "./chunk-ZCHNAM3B.js";
|
|
4
8
|
import {
|
|
5
9
|
buildMenuKeyboard,
|
|
6
10
|
buildSkillMessages,
|
|
7
11
|
handleClear,
|
|
8
12
|
handleHelp,
|
|
9
13
|
handleMenu
|
|
10
|
-
} from "./chunk-
|
|
14
|
+
} from "./chunk-7QJS2XBD.js";
|
|
11
15
|
import {
|
|
12
16
|
AgentCatalog
|
|
13
17
|
} from "./chunk-J6X5SW6O.js";
|
|
@@ -19,7 +23,7 @@ import {
|
|
|
19
23
|
getSafeFields,
|
|
20
24
|
isHotReloadable,
|
|
21
25
|
resolveOptions
|
|
22
|
-
} from "./chunk-
|
|
26
|
+
} from "./chunk-4TR5Y3MP.js";
|
|
23
27
|
import {
|
|
24
28
|
createChildLogger,
|
|
25
29
|
createSessionLogger
|
|
@@ -488,9 +492,10 @@ ${stderr}`
|
|
|
488
492
|
}
|
|
489
493
|
async prompt(text, attachments) {
|
|
490
494
|
const contentBlocks = [{ type: "text", text }];
|
|
495
|
+
const SUPPORTED_IMAGE_MIMES = /* @__PURE__ */ new Set(["image/jpeg", "image/png", "image/gif", "image/webp"]);
|
|
491
496
|
for (const att of attachments ?? []) {
|
|
492
497
|
const tooLarge = att.size > 10 * 1024 * 1024;
|
|
493
|
-
if (att.type === "image" && this.promptCapabilities?.image && !tooLarge) {
|
|
498
|
+
if (att.type === "image" && this.promptCapabilities?.image && !tooLarge && SUPPORTED_IMAGE_MIMES.has(att.mimeType)) {
|
|
494
499
|
const data = await fs.promises.readFile(att.filePath);
|
|
495
500
|
contentBlocks.push({ type: "image", data: data.toString("base64"), mimeType: att.mimeType });
|
|
496
501
|
} else if (att.type === "audio" && this.promptCapabilities?.audio && !tooLarge) {
|
|
@@ -756,6 +761,7 @@ var PermissionGate = class {
|
|
|
756
761
|
|
|
757
762
|
// src/core/session.ts
|
|
758
763
|
import { nanoid } from "nanoid";
|
|
764
|
+
import * as fs2 from "fs";
|
|
759
765
|
var moduleLog = createChildLogger({ module: "session" });
|
|
760
766
|
var VALID_TRANSITIONS = {
|
|
761
767
|
initializing: /* @__PURE__ */ new Set(["active", "error"]),
|
|
@@ -776,9 +782,11 @@ var Session = class extends TypedEmitter {
|
|
|
776
782
|
name;
|
|
777
783
|
createdAt = /* @__PURE__ */ new Date();
|
|
778
784
|
dangerousMode = false;
|
|
785
|
+
archiving = false;
|
|
779
786
|
log;
|
|
780
787
|
permissionGate = new PermissionGate();
|
|
781
788
|
queue;
|
|
789
|
+
speechService;
|
|
782
790
|
constructor(opts) {
|
|
783
791
|
super();
|
|
784
792
|
this.id = opts.id || nanoid(12);
|
|
@@ -786,6 +794,7 @@ var Session = class extends TypedEmitter {
|
|
|
786
794
|
this.agentName = opts.agentName;
|
|
787
795
|
this.workingDirectory = opts.workingDirectory;
|
|
788
796
|
this.agentInstance = opts.agentInstance;
|
|
797
|
+
this.speechService = opts.speechService;
|
|
789
798
|
this.log = createSessionLogger(this.id, moduleLog);
|
|
790
799
|
this.log.info({ agentName: this.agentName }, "Session created");
|
|
791
800
|
this.queue = new PromptQueue(
|
|
@@ -851,7 +860,8 @@ var Session = class extends TypedEmitter {
|
|
|
851
860
|
}
|
|
852
861
|
const promptStart = Date.now();
|
|
853
862
|
this.log.debug("Prompt execution started");
|
|
854
|
-
await this.
|
|
863
|
+
const processed = await this.maybeTranscribeAudio(text, attachments);
|
|
864
|
+
await this.agentInstance.prompt(processed.text, processed.attachments);
|
|
855
865
|
this.log.info(
|
|
856
866
|
{ durationMs: Date.now() - promptStart },
|
|
857
867
|
"Prompt execution completed"
|
|
@@ -860,6 +870,51 @@ var Session = class extends TypedEmitter {
|
|
|
860
870
|
await this.autoName();
|
|
861
871
|
}
|
|
862
872
|
}
|
|
873
|
+
async maybeTranscribeAudio(text, attachments) {
|
|
874
|
+
if (!attachments?.length || !this.speechService) {
|
|
875
|
+
return { text, attachments };
|
|
876
|
+
}
|
|
877
|
+
const hasAudioCapability = this.agentInstance.promptCapabilities?.audio === true;
|
|
878
|
+
if (hasAudioCapability) {
|
|
879
|
+
return { text, attachments };
|
|
880
|
+
}
|
|
881
|
+
if (!this.speechService.isSTTAvailable()) {
|
|
882
|
+
return { text, attachments };
|
|
883
|
+
}
|
|
884
|
+
let transcribedText = text;
|
|
885
|
+
const remainingAttachments = [];
|
|
886
|
+
for (const att of attachments) {
|
|
887
|
+
if (att.type !== "audio") {
|
|
888
|
+
remainingAttachments.push(att);
|
|
889
|
+
continue;
|
|
890
|
+
}
|
|
891
|
+
try {
|
|
892
|
+
const audioPath = att.originalFilePath || att.filePath;
|
|
893
|
+
const audioMime = att.originalFilePath ? "audio/ogg" : att.mimeType;
|
|
894
|
+
const audioBuffer = await fs2.promises.readFile(audioPath);
|
|
895
|
+
const result = await this.speechService.transcribe(audioBuffer, audioMime);
|
|
896
|
+
this.log.info({ provider: "stt", duration: result.duration }, "Voice transcribed");
|
|
897
|
+
this.emit("agent_event", {
|
|
898
|
+
type: "system_message",
|
|
899
|
+
message: `\u{1F3A4} You said: ${result.text}`
|
|
900
|
+
});
|
|
901
|
+
transcribedText = transcribedText.replace(/\[Audio:\s*[^\]]*\]\s*/g, "").trim();
|
|
902
|
+
transcribedText = transcribedText ? `${transcribedText}
|
|
903
|
+
${result.text}` : result.text;
|
|
904
|
+
} catch (err) {
|
|
905
|
+
this.log.warn({ err }, "STT transcription failed, keeping audio attachment");
|
|
906
|
+
this.emit("agent_event", {
|
|
907
|
+
type: "error",
|
|
908
|
+
message: `Voice transcription failed: ${err.message}`
|
|
909
|
+
});
|
|
910
|
+
remainingAttachments.push(att);
|
|
911
|
+
}
|
|
912
|
+
}
|
|
913
|
+
return {
|
|
914
|
+
text: transcribedText,
|
|
915
|
+
attachments: remainingAttachments.length > 0 ? remainingAttachments : void 0
|
|
916
|
+
};
|
|
917
|
+
}
|
|
863
918
|
// NOTE: This injects a summary prompt into the agent's conversation history.
|
|
864
919
|
async autoName() {
|
|
865
920
|
let title = "";
|
|
@@ -972,7 +1027,7 @@ var SessionManager = class {
|
|
|
972
1027
|
getRecordByThread(channelId, threadId) {
|
|
973
1028
|
return this.store?.findByPlatform(
|
|
974
1029
|
channelId,
|
|
975
|
-
(p) => String(p.topicId) === threadId
|
|
1030
|
+
(p) => String(p.topicId) === threadId || p.threadId === threadId
|
|
976
1031
|
);
|
|
977
1032
|
}
|
|
978
1033
|
registerSession(session) {
|
|
@@ -1037,7 +1092,7 @@ var SessionManager = class {
|
|
|
1037
1092
|
};
|
|
1038
1093
|
|
|
1039
1094
|
// src/core/file-service.ts
|
|
1040
|
-
import
|
|
1095
|
+
import fs3 from "fs";
|
|
1041
1096
|
import path2 from "path";
|
|
1042
1097
|
import { OggOpusDecoder } from "ogg-opus-decoder";
|
|
1043
1098
|
import wav from "node-wav";
|
|
@@ -1084,10 +1139,10 @@ var FileService = class {
|
|
|
1084
1139
|
}
|
|
1085
1140
|
async saveFile(sessionId, fileName, data, mimeType) {
|
|
1086
1141
|
const sessionDir = path2.join(this.baseDir, sessionId);
|
|
1087
|
-
await
|
|
1142
|
+
await fs3.promises.mkdir(sessionDir, { recursive: true });
|
|
1088
1143
|
const safeName = `${Date.now()}-${fileName.replace(/[^a-zA-Z0-9._-]/g, "_")}`;
|
|
1089
1144
|
const filePath = path2.join(sessionDir, safeName);
|
|
1090
|
-
await
|
|
1145
|
+
await fs3.promises.writeFile(filePath, data);
|
|
1091
1146
|
return {
|
|
1092
1147
|
type: classifyMime(mimeType),
|
|
1093
1148
|
filePath,
|
|
@@ -1098,7 +1153,7 @@ var FileService = class {
|
|
|
1098
1153
|
}
|
|
1099
1154
|
async resolveFile(filePath) {
|
|
1100
1155
|
try {
|
|
1101
|
-
const stat = await
|
|
1156
|
+
const stat = await fs3.promises.stat(filePath);
|
|
1102
1157
|
if (!stat.isFile()) return null;
|
|
1103
1158
|
const ext = path2.extname(filePath).toLowerCase();
|
|
1104
1159
|
const mimeType = EXT_TO_MIME[ext] || "application/octet-stream";
|
|
@@ -1227,12 +1282,12 @@ var SessionBridge = class {
|
|
|
1227
1282
|
break;
|
|
1228
1283
|
case "image_content": {
|
|
1229
1284
|
if (this.deps.fileService) {
|
|
1230
|
-
const
|
|
1285
|
+
const fs7 = this.deps.fileService;
|
|
1231
1286
|
const sid = this.session.id;
|
|
1232
1287
|
const { data, mimeType } = event;
|
|
1233
1288
|
const buffer = Buffer.from(data, "base64");
|
|
1234
1289
|
const ext = FileService.extensionFromMime(mimeType);
|
|
1235
|
-
|
|
1290
|
+
fs7.saveFile(sid, `agent-image${ext}`, buffer, mimeType).then((att) => {
|
|
1236
1291
|
this.adapter.sendMessage(sid, { type: "attachment", text: "", attachment: att });
|
|
1237
1292
|
}).catch((err) => log2.error({ err }, "Failed to save agent image"));
|
|
1238
1293
|
}
|
|
@@ -1240,12 +1295,12 @@ var SessionBridge = class {
|
|
|
1240
1295
|
}
|
|
1241
1296
|
case "audio_content": {
|
|
1242
1297
|
if (this.deps.fileService) {
|
|
1243
|
-
const
|
|
1298
|
+
const fs7 = this.deps.fileService;
|
|
1244
1299
|
const sid = this.session.id;
|
|
1245
1300
|
const { data, mimeType } = event;
|
|
1246
1301
|
const buffer = Buffer.from(data, "base64");
|
|
1247
1302
|
const ext = FileService.extensionFromMime(mimeType);
|
|
1248
|
-
|
|
1303
|
+
fs7.saveFile(sid, `agent-audio${ext}`, buffer, mimeType).then((att) => {
|
|
1249
1304
|
this.adapter.sendMessage(sid, { type: "attachment", text: "", attachment: att });
|
|
1250
1305
|
}).catch((err) => log2.error({ err }, "Failed to save agent audio"));
|
|
1251
1306
|
}
|
|
@@ -1255,6 +1310,12 @@ var SessionBridge = class {
|
|
|
1255
1310
|
log2.debug({ commands: event.commands }, "Commands available");
|
|
1256
1311
|
this.adapter.sendSkillCommands(this.session.id, event.commands);
|
|
1257
1312
|
break;
|
|
1313
|
+
case "system_message":
|
|
1314
|
+
this.adapter.sendMessage(
|
|
1315
|
+
this.session.id,
|
|
1316
|
+
this.deps.messageTransformer.transform(event)
|
|
1317
|
+
);
|
|
1318
|
+
break;
|
|
1258
1319
|
}
|
|
1259
1320
|
};
|
|
1260
1321
|
this.session.on("agent_event", this.agentEventHandler);
|
|
@@ -1449,6 +1510,8 @@ var MessageTransformer = class {
|
|
|
1449
1510
|
return { type: "session_end", text: `Done (${event.reason})` };
|
|
1450
1511
|
case "error":
|
|
1451
1512
|
return { type: "error", text: event.message };
|
|
1513
|
+
case "system_message":
|
|
1514
|
+
return { type: "system_message", text: event.message };
|
|
1452
1515
|
default:
|
|
1453
1516
|
return { type: "text", text: "" };
|
|
1454
1517
|
}
|
|
@@ -1504,15 +1567,331 @@ var MessageTransformer = class {
|
|
|
1504
1567
|
}
|
|
1505
1568
|
};
|
|
1506
1569
|
|
|
1570
|
+
// src/core/usage-store.ts
|
|
1571
|
+
import fs4 from "fs";
|
|
1572
|
+
import path3 from "path";
|
|
1573
|
+
var log4 = createChildLogger({ module: "usage-store" });
|
|
1574
|
+
var DEBOUNCE_MS = 2e3;
|
|
1575
|
+
var UsageStore = class {
|
|
1576
|
+
constructor(filePath, retentionDays) {
|
|
1577
|
+
this.filePath = filePath;
|
|
1578
|
+
this.retentionDays = retentionDays;
|
|
1579
|
+
this.load();
|
|
1580
|
+
this.cleanup();
|
|
1581
|
+
this.cleanupInterval = setInterval(
|
|
1582
|
+
() => this.cleanup(),
|
|
1583
|
+
24 * 60 * 60 * 1e3
|
|
1584
|
+
);
|
|
1585
|
+
this.flushHandler = () => {
|
|
1586
|
+
try {
|
|
1587
|
+
this.flushSync();
|
|
1588
|
+
} catch {
|
|
1589
|
+
}
|
|
1590
|
+
};
|
|
1591
|
+
process.on("SIGTERM", this.flushHandler);
|
|
1592
|
+
process.on("SIGINT", this.flushHandler);
|
|
1593
|
+
process.on("exit", this.flushHandler);
|
|
1594
|
+
}
|
|
1595
|
+
records = [];
|
|
1596
|
+
debounceTimer = null;
|
|
1597
|
+
cleanupInterval = null;
|
|
1598
|
+
flushHandler = null;
|
|
1599
|
+
append(record) {
|
|
1600
|
+
this.records.push(record);
|
|
1601
|
+
this.scheduleDiskWrite();
|
|
1602
|
+
}
|
|
1603
|
+
query(period) {
|
|
1604
|
+
const cutoff = this.getCutoff(period);
|
|
1605
|
+
const filtered = cutoff ? this.records.filter((r) => new Date(r.timestamp).getTime() >= cutoff) : this.records;
|
|
1606
|
+
const totalTokens = filtered.reduce((sum, r) => sum + r.tokensUsed, 0);
|
|
1607
|
+
const totalCost = filtered.reduce(
|
|
1608
|
+
(sum, r) => sum + (r.cost?.amount ?? 0),
|
|
1609
|
+
0
|
|
1610
|
+
);
|
|
1611
|
+
const sessionIds = new Set(filtered.map((r) => r.sessionId));
|
|
1612
|
+
const currency = filtered.find((r) => r.cost?.currency)?.cost?.currency ?? "USD";
|
|
1613
|
+
return {
|
|
1614
|
+
period,
|
|
1615
|
+
totalTokens,
|
|
1616
|
+
totalCost,
|
|
1617
|
+
currency,
|
|
1618
|
+
sessionCount: sessionIds.size,
|
|
1619
|
+
recordCount: filtered.length
|
|
1620
|
+
};
|
|
1621
|
+
}
|
|
1622
|
+
getMonthlyTotal() {
|
|
1623
|
+
const now = /* @__PURE__ */ new Date();
|
|
1624
|
+
const startOfMonth = new Date(now.getFullYear(), now.getMonth(), 1);
|
|
1625
|
+
const cutoff = startOfMonth.getTime();
|
|
1626
|
+
const filtered = this.records.filter(
|
|
1627
|
+
(r) => new Date(r.timestamp).getTime() >= cutoff
|
|
1628
|
+
);
|
|
1629
|
+
const totalCost = filtered.reduce(
|
|
1630
|
+
(sum, r) => sum + (r.cost?.amount ?? 0),
|
|
1631
|
+
0
|
|
1632
|
+
);
|
|
1633
|
+
const currency = filtered.find((r) => r.cost?.currency)?.cost?.currency ?? "USD";
|
|
1634
|
+
return { totalCost, currency };
|
|
1635
|
+
}
|
|
1636
|
+
cleanup() {
|
|
1637
|
+
const cutoff = Date.now() - this.retentionDays * 24 * 60 * 60 * 1e3;
|
|
1638
|
+
const before = this.records.length;
|
|
1639
|
+
this.records = this.records.filter(
|
|
1640
|
+
(r) => new Date(r.timestamp).getTime() >= cutoff
|
|
1641
|
+
);
|
|
1642
|
+
const removed = before - this.records.length;
|
|
1643
|
+
if (removed > 0) {
|
|
1644
|
+
log4.info({ removed }, "Cleaned up expired usage records");
|
|
1645
|
+
this.scheduleDiskWrite();
|
|
1646
|
+
}
|
|
1647
|
+
}
|
|
1648
|
+
flushSync() {
|
|
1649
|
+
if (this.debounceTimer) {
|
|
1650
|
+
clearTimeout(this.debounceTimer);
|
|
1651
|
+
this.debounceTimer = null;
|
|
1652
|
+
}
|
|
1653
|
+
const data = { version: 1, records: this.records };
|
|
1654
|
+
const dir = path3.dirname(this.filePath);
|
|
1655
|
+
if (!fs4.existsSync(dir)) fs4.mkdirSync(dir, { recursive: true });
|
|
1656
|
+
fs4.writeFileSync(this.filePath, JSON.stringify(data, null, 2));
|
|
1657
|
+
}
|
|
1658
|
+
destroy() {
|
|
1659
|
+
if (this.debounceTimer) this.flushSync();
|
|
1660
|
+
if (this.cleanupInterval) clearInterval(this.cleanupInterval);
|
|
1661
|
+
if (this.flushHandler) {
|
|
1662
|
+
process.removeListener("SIGTERM", this.flushHandler);
|
|
1663
|
+
process.removeListener("SIGINT", this.flushHandler);
|
|
1664
|
+
process.removeListener("exit", this.flushHandler);
|
|
1665
|
+
this.flushHandler = null;
|
|
1666
|
+
}
|
|
1667
|
+
}
|
|
1668
|
+
load() {
|
|
1669
|
+
if (!fs4.existsSync(this.filePath)) return;
|
|
1670
|
+
try {
|
|
1671
|
+
const raw = JSON.parse(
|
|
1672
|
+
fs4.readFileSync(this.filePath, "utf-8")
|
|
1673
|
+
);
|
|
1674
|
+
if (raw.version !== 1) {
|
|
1675
|
+
log4.warn(
|
|
1676
|
+
{ version: raw.version },
|
|
1677
|
+
"Unknown usage store version, skipping load"
|
|
1678
|
+
);
|
|
1679
|
+
return;
|
|
1680
|
+
}
|
|
1681
|
+
this.records = raw.records || [];
|
|
1682
|
+
log4.info({ count: this.records.length }, "Loaded usage records");
|
|
1683
|
+
} catch (err) {
|
|
1684
|
+
log4.error({ err }, "Failed to load usage store, backing up corrupt file");
|
|
1685
|
+
try {
|
|
1686
|
+
fs4.copyFileSync(this.filePath, this.filePath + ".bak");
|
|
1687
|
+
} catch {
|
|
1688
|
+
}
|
|
1689
|
+
this.records = [];
|
|
1690
|
+
}
|
|
1691
|
+
}
|
|
1692
|
+
getCutoff(period) {
|
|
1693
|
+
const now = /* @__PURE__ */ new Date();
|
|
1694
|
+
switch (period) {
|
|
1695
|
+
case "today": {
|
|
1696
|
+
const start = new Date(now);
|
|
1697
|
+
start.setHours(0, 0, 0, 0);
|
|
1698
|
+
return start.getTime();
|
|
1699
|
+
}
|
|
1700
|
+
case "week":
|
|
1701
|
+
return Date.now() - 7 * 24 * 60 * 60 * 1e3;
|
|
1702
|
+
case "month": {
|
|
1703
|
+
const startOfMonth = new Date(now.getFullYear(), now.getMonth(), 1);
|
|
1704
|
+
return startOfMonth.getTime();
|
|
1705
|
+
}
|
|
1706
|
+
case "all":
|
|
1707
|
+
return null;
|
|
1708
|
+
}
|
|
1709
|
+
}
|
|
1710
|
+
scheduleDiskWrite() {
|
|
1711
|
+
if (this.debounceTimer) clearTimeout(this.debounceTimer);
|
|
1712
|
+
this.debounceTimer = setTimeout(() => {
|
|
1713
|
+
this.flushSync();
|
|
1714
|
+
}, DEBOUNCE_MS);
|
|
1715
|
+
}
|
|
1716
|
+
};
|
|
1717
|
+
|
|
1718
|
+
// src/core/usage-budget.ts
|
|
1719
|
+
var UsageBudget = class {
|
|
1720
|
+
constructor(store, config, now = () => /* @__PURE__ */ new Date()) {
|
|
1721
|
+
this.store = store;
|
|
1722
|
+
this.config = config;
|
|
1723
|
+
this.now = now;
|
|
1724
|
+
this.lastNotifiedMonth = this.now().getMonth();
|
|
1725
|
+
}
|
|
1726
|
+
lastNotifiedStatus = "ok";
|
|
1727
|
+
lastNotifiedMonth;
|
|
1728
|
+
check() {
|
|
1729
|
+
if (!this.config.monthlyBudget) {
|
|
1730
|
+
return { status: "ok" };
|
|
1731
|
+
}
|
|
1732
|
+
const currentMonth = this.now().getMonth();
|
|
1733
|
+
if (currentMonth !== this.lastNotifiedMonth) {
|
|
1734
|
+
this.lastNotifiedStatus = "ok";
|
|
1735
|
+
this.lastNotifiedMonth = currentMonth;
|
|
1736
|
+
}
|
|
1737
|
+
const { totalCost } = this.store.getMonthlyTotal();
|
|
1738
|
+
const budget = this.config.monthlyBudget;
|
|
1739
|
+
const threshold = this.config.warningThreshold;
|
|
1740
|
+
let status;
|
|
1741
|
+
if (totalCost >= budget) {
|
|
1742
|
+
status = "exceeded";
|
|
1743
|
+
} else if (totalCost >= threshold * budget) {
|
|
1744
|
+
status = "warning";
|
|
1745
|
+
} else {
|
|
1746
|
+
status = "ok";
|
|
1747
|
+
}
|
|
1748
|
+
let message;
|
|
1749
|
+
if (status !== "ok" && status !== this.lastNotifiedStatus) {
|
|
1750
|
+
const pct = Math.round(totalCost / budget * 100);
|
|
1751
|
+
const filled = Math.round(Math.min(totalCost / budget, 1) * 10);
|
|
1752
|
+
const bar = "\u2593".repeat(filled) + "\u2591".repeat(10 - filled);
|
|
1753
|
+
if (status === "warning") {
|
|
1754
|
+
message = `\u26A0\uFE0F <b>Budget Warning</b>
|
|
1755
|
+
Monthly usage: $${totalCost.toFixed(2)} / $${budget.toFixed(2)} (${pct}%)
|
|
1756
|
+
${bar} ${pct}%`;
|
|
1757
|
+
} else {
|
|
1758
|
+
message = `\u{1F6A8} <b>Budget Exceeded</b>
|
|
1759
|
+
Monthly usage: $${totalCost.toFixed(2)} / $${budget.toFixed(2)} (${pct}%)
|
|
1760
|
+
${bar} ${pct}%
|
|
1761
|
+
Sessions are NOT blocked \u2014 this is a warning only.`;
|
|
1762
|
+
}
|
|
1763
|
+
}
|
|
1764
|
+
this.lastNotifiedStatus = status;
|
|
1765
|
+
return { status, message };
|
|
1766
|
+
}
|
|
1767
|
+
getStatus() {
|
|
1768
|
+
const { totalCost } = this.store.getMonthlyTotal();
|
|
1769
|
+
const budget = this.config.monthlyBudget ?? 0;
|
|
1770
|
+
let status = "ok";
|
|
1771
|
+
if (budget > 0) {
|
|
1772
|
+
if (totalCost >= budget) {
|
|
1773
|
+
status = "exceeded";
|
|
1774
|
+
} else if (totalCost >= this.config.warningThreshold * budget) {
|
|
1775
|
+
status = "warning";
|
|
1776
|
+
}
|
|
1777
|
+
}
|
|
1778
|
+
const percent = budget > 0 ? Math.round(totalCost / budget * 100) : 0;
|
|
1779
|
+
return { status, used: totalCost, budget, percent };
|
|
1780
|
+
}
|
|
1781
|
+
};
|
|
1782
|
+
|
|
1783
|
+
// src/core/speech/speech-service.ts
|
|
1784
|
+
var SpeechService = class {
|
|
1785
|
+
constructor(config) {
|
|
1786
|
+
this.config = config;
|
|
1787
|
+
}
|
|
1788
|
+
sttProviders = /* @__PURE__ */ new Map();
|
|
1789
|
+
ttsProviders = /* @__PURE__ */ new Map();
|
|
1790
|
+
registerSTTProvider(name, provider) {
|
|
1791
|
+
this.sttProviders.set(name, provider);
|
|
1792
|
+
}
|
|
1793
|
+
registerTTSProvider(name, provider) {
|
|
1794
|
+
this.ttsProviders.set(name, provider);
|
|
1795
|
+
}
|
|
1796
|
+
isSTTAvailable() {
|
|
1797
|
+
const { provider, providers } = this.config.stt;
|
|
1798
|
+
return provider !== null && providers[provider]?.apiKey !== void 0;
|
|
1799
|
+
}
|
|
1800
|
+
isTTSAvailable() {
|
|
1801
|
+
const { provider, providers } = this.config.tts;
|
|
1802
|
+
return provider !== null && providers[provider]?.apiKey !== void 0;
|
|
1803
|
+
}
|
|
1804
|
+
async transcribe(audioBuffer, mimeType, options) {
|
|
1805
|
+
const providerName = this.config.stt.provider;
|
|
1806
|
+
if (!providerName || !this.config.stt.providers[providerName]?.apiKey) {
|
|
1807
|
+
throw new Error("STT not configured. Set speech.stt.provider and API key in config.");
|
|
1808
|
+
}
|
|
1809
|
+
const provider = this.sttProviders.get(providerName);
|
|
1810
|
+
if (!provider) {
|
|
1811
|
+
throw new Error(`STT provider "${providerName}" not registered. Available: ${[...this.sttProviders.keys()].join(", ") || "none"}`);
|
|
1812
|
+
}
|
|
1813
|
+
return provider.transcribe(audioBuffer, mimeType, options);
|
|
1814
|
+
}
|
|
1815
|
+
async synthesize(text, options) {
|
|
1816
|
+
const providerName = this.config.tts.provider;
|
|
1817
|
+
if (!providerName || !this.config.tts.providers[providerName]?.apiKey) {
|
|
1818
|
+
throw new Error("TTS not configured. Set speech.tts.provider and API key in config.");
|
|
1819
|
+
}
|
|
1820
|
+
const provider = this.ttsProviders.get(providerName);
|
|
1821
|
+
if (!provider) {
|
|
1822
|
+
throw new Error(`TTS provider "${providerName}" not registered. Available: ${[...this.ttsProviders.keys()].join(", ") || "none"}`);
|
|
1823
|
+
}
|
|
1824
|
+
return provider.synthesize(text, options);
|
|
1825
|
+
}
|
|
1826
|
+
updateConfig(config) {
|
|
1827
|
+
this.config = config;
|
|
1828
|
+
}
|
|
1829
|
+
};
|
|
1830
|
+
|
|
1831
|
+
// src/core/speech/providers/groq.ts
|
|
1832
|
+
var GROQ_API_URL = "https://api.groq.com/openai/v1/audio/transcriptions";
|
|
1833
|
+
var GroqSTT = class {
|
|
1834
|
+
constructor(apiKey, defaultModel = "whisper-large-v3-turbo") {
|
|
1835
|
+
this.apiKey = apiKey;
|
|
1836
|
+
this.defaultModel = defaultModel;
|
|
1837
|
+
}
|
|
1838
|
+
name = "groq";
|
|
1839
|
+
async transcribe(audioBuffer, mimeType, options) {
|
|
1840
|
+
const ext = mimeToExt(mimeType);
|
|
1841
|
+
const form = new FormData();
|
|
1842
|
+
form.append("file", new Blob([new Uint8Array(audioBuffer)], { type: mimeType }), `audio${ext}`);
|
|
1843
|
+
form.append("model", options?.model || this.defaultModel);
|
|
1844
|
+
form.append("response_format", "verbose_json");
|
|
1845
|
+
if (options?.language) {
|
|
1846
|
+
form.append("language", options.language);
|
|
1847
|
+
}
|
|
1848
|
+
const resp = await fetch(GROQ_API_URL, {
|
|
1849
|
+
method: "POST",
|
|
1850
|
+
headers: { Authorization: `Bearer ${this.apiKey}` },
|
|
1851
|
+
body: form
|
|
1852
|
+
});
|
|
1853
|
+
if (!resp.ok) {
|
|
1854
|
+
const body = await resp.text();
|
|
1855
|
+
if (resp.status === 401) {
|
|
1856
|
+
throw new Error("Invalid Groq API key. Check your key at console.groq.com.");
|
|
1857
|
+
}
|
|
1858
|
+
if (resp.status === 413) {
|
|
1859
|
+
throw new Error("Audio file too large for Groq API (max 25MB).");
|
|
1860
|
+
}
|
|
1861
|
+
if (resp.status === 429) {
|
|
1862
|
+
throw new Error("Groq rate limit exceeded. Free tier: 28,800 seconds/day. Try again later.");
|
|
1863
|
+
}
|
|
1864
|
+
throw new Error(`Groq STT error (${resp.status}): ${body}`);
|
|
1865
|
+
}
|
|
1866
|
+
const data = await resp.json();
|
|
1867
|
+
return {
|
|
1868
|
+
text: data.text,
|
|
1869
|
+
language: data.language,
|
|
1870
|
+
duration: data.duration
|
|
1871
|
+
};
|
|
1872
|
+
}
|
|
1873
|
+
};
|
|
1874
|
+
function mimeToExt(mimeType) {
|
|
1875
|
+
const map = {
|
|
1876
|
+
"audio/ogg": ".ogg",
|
|
1877
|
+
"audio/wav": ".wav",
|
|
1878
|
+
"audio/mpeg": ".mp3",
|
|
1879
|
+
"audio/mp4": ".m4a",
|
|
1880
|
+
"audio/webm": ".webm",
|
|
1881
|
+
"audio/flac": ".flac"
|
|
1882
|
+
};
|
|
1883
|
+
return map[mimeType] || ".bin";
|
|
1884
|
+
}
|
|
1885
|
+
|
|
1507
1886
|
// src/core/core.ts
|
|
1508
|
-
import
|
|
1887
|
+
import path5 from "path";
|
|
1509
1888
|
import os from "os";
|
|
1510
1889
|
|
|
1511
1890
|
// src/core/session-store.ts
|
|
1512
|
-
import
|
|
1513
|
-
import
|
|
1514
|
-
var
|
|
1515
|
-
var
|
|
1891
|
+
import fs5 from "fs";
|
|
1892
|
+
import path4 from "path";
|
|
1893
|
+
var log5 = createChildLogger({ module: "session-store" });
|
|
1894
|
+
var DEBOUNCE_MS2 = 2e3;
|
|
1516
1895
|
var JsonFileSessionStore = class {
|
|
1517
1896
|
records = /* @__PURE__ */ new Map();
|
|
1518
1897
|
filePath;
|
|
@@ -1572,9 +1951,9 @@ var JsonFileSessionStore = class {
|
|
|
1572
1951
|
version: 1,
|
|
1573
1952
|
sessions: Object.fromEntries(this.records)
|
|
1574
1953
|
};
|
|
1575
|
-
const dir =
|
|
1576
|
-
if (!
|
|
1577
|
-
|
|
1954
|
+
const dir = path4.dirname(this.filePath);
|
|
1955
|
+
if (!fs5.existsSync(dir)) fs5.mkdirSync(dir, { recursive: true });
|
|
1956
|
+
fs5.writeFileSync(this.filePath, JSON.stringify(data, null, 2));
|
|
1578
1957
|
}
|
|
1579
1958
|
destroy() {
|
|
1580
1959
|
if (this.debounceTimer) clearTimeout(this.debounceTimer);
|
|
@@ -1587,13 +1966,13 @@ var JsonFileSessionStore = class {
|
|
|
1587
1966
|
}
|
|
1588
1967
|
}
|
|
1589
1968
|
load() {
|
|
1590
|
-
if (!
|
|
1969
|
+
if (!fs5.existsSync(this.filePath)) return;
|
|
1591
1970
|
try {
|
|
1592
1971
|
const raw = JSON.parse(
|
|
1593
|
-
|
|
1972
|
+
fs5.readFileSync(this.filePath, "utf-8")
|
|
1594
1973
|
);
|
|
1595
1974
|
if (raw.version !== 1) {
|
|
1596
|
-
|
|
1975
|
+
log5.warn(
|
|
1597
1976
|
{ version: raw.version },
|
|
1598
1977
|
"Unknown session store version, skipping load"
|
|
1599
1978
|
);
|
|
@@ -1602,9 +1981,9 @@ var JsonFileSessionStore = class {
|
|
|
1602
1981
|
for (const [id, record] of Object.entries(raw.sessions)) {
|
|
1603
1982
|
this.records.set(id, record);
|
|
1604
1983
|
}
|
|
1605
|
-
|
|
1984
|
+
log5.info({ count: this.records.size }, "Loaded session records");
|
|
1606
1985
|
} catch (err) {
|
|
1607
|
-
|
|
1986
|
+
log5.error({ err }, "Failed to load session store");
|
|
1608
1987
|
}
|
|
1609
1988
|
}
|
|
1610
1989
|
cleanup() {
|
|
@@ -1620,7 +1999,7 @@ var JsonFileSessionStore = class {
|
|
|
1620
1999
|
}
|
|
1621
2000
|
}
|
|
1622
2001
|
if (removed > 0) {
|
|
1623
|
-
|
|
2002
|
+
log5.info({ removed }, "Cleaned up expired session records");
|
|
1624
2003
|
this.scheduleDiskWrite();
|
|
1625
2004
|
}
|
|
1626
2005
|
}
|
|
@@ -1628,12 +2007,13 @@ var JsonFileSessionStore = class {
|
|
|
1628
2007
|
if (this.debounceTimer) clearTimeout(this.debounceTimer);
|
|
1629
2008
|
this.debounceTimer = setTimeout(() => {
|
|
1630
2009
|
this.flushSync();
|
|
1631
|
-
},
|
|
2010
|
+
}, DEBOUNCE_MS2);
|
|
1632
2011
|
}
|
|
1633
2012
|
};
|
|
1634
2013
|
|
|
1635
2014
|
// src/core/core.ts
|
|
1636
|
-
|
|
2015
|
+
import { nanoid as nanoid2 } from "nanoid";
|
|
2016
|
+
var log6 = createChildLogger({ module: "core" });
|
|
1637
2017
|
var OpenACPCore = class {
|
|
1638
2018
|
configManager;
|
|
1639
2019
|
agentCatalog;
|
|
@@ -1642,32 +2022,57 @@ var OpenACPCore = class {
|
|
|
1642
2022
|
notificationManager;
|
|
1643
2023
|
messageTransformer;
|
|
1644
2024
|
fileService;
|
|
2025
|
+
speechService;
|
|
1645
2026
|
adapters = /* @__PURE__ */ new Map();
|
|
1646
2027
|
/** Set by main.ts — triggers graceful shutdown with restart exit code */
|
|
1647
2028
|
requestRestart = null;
|
|
1648
2029
|
_tunnelService;
|
|
1649
2030
|
sessionStore = null;
|
|
1650
2031
|
resumeLocks = /* @__PURE__ */ new Map();
|
|
2032
|
+
usageStore = null;
|
|
2033
|
+
usageBudget = null;
|
|
1651
2034
|
constructor(configManager) {
|
|
1652
2035
|
this.configManager = configManager;
|
|
1653
2036
|
const config = configManager.get();
|
|
1654
2037
|
this.agentCatalog = new AgentCatalog();
|
|
1655
2038
|
this.agentCatalog.load();
|
|
1656
2039
|
this.agentManager = new AgentManager(this.agentCatalog);
|
|
1657
|
-
const storePath =
|
|
2040
|
+
const storePath = path5.join(os.homedir(), ".openacp", "sessions.json");
|
|
1658
2041
|
this.sessionStore = new JsonFileSessionStore(
|
|
1659
2042
|
storePath,
|
|
1660
2043
|
config.sessionStore.ttlDays
|
|
1661
2044
|
);
|
|
1662
2045
|
this.sessionManager = new SessionManager(this.sessionStore);
|
|
1663
2046
|
this.notificationManager = new NotificationManager(this.adapters);
|
|
2047
|
+
const usageConfig = config.usage;
|
|
2048
|
+
if (usageConfig.enabled) {
|
|
2049
|
+
const usagePath = path5.join(os.homedir(), ".openacp", "usage.json");
|
|
2050
|
+
this.usageStore = new UsageStore(usagePath, usageConfig.retentionDays);
|
|
2051
|
+
this.usageBudget = new UsageBudget(this.usageStore, usageConfig);
|
|
2052
|
+
}
|
|
1664
2053
|
this.messageTransformer = new MessageTransformer();
|
|
1665
|
-
this.fileService = new FileService(
|
|
2054
|
+
this.fileService = new FileService(path5.join(os.homedir(), ".openacp", "files"));
|
|
2055
|
+
const speechConfig = config.speech ?? { stt: { provider: null, providers: {} }, tts: { provider: null, providers: {} } };
|
|
2056
|
+
this.speechService = new SpeechService(speechConfig);
|
|
2057
|
+
const groqConfig = speechConfig.stt?.providers?.groq;
|
|
2058
|
+
if (groqConfig?.apiKey) {
|
|
2059
|
+
this.speechService.registerSTTProvider("groq", new GroqSTT(groqConfig.apiKey, groqConfig.model));
|
|
2060
|
+
}
|
|
1666
2061
|
this.configManager.on("config:changed", async ({ path: configPath, value }) => {
|
|
1667
2062
|
if (configPath === "logging.level" && typeof value === "string") {
|
|
1668
2063
|
const { setLogLevel: setLogLevel2 } = await import("./log-SPS2S6FO.js");
|
|
1669
2064
|
setLogLevel2(value);
|
|
1670
|
-
|
|
2065
|
+
log6.info({ level: value }, "Log level changed at runtime");
|
|
2066
|
+
}
|
|
2067
|
+
if (configPath.startsWith("speech.")) {
|
|
2068
|
+
const newConfig = this.configManager.get();
|
|
2069
|
+
const newSpeechConfig = newConfig.speech ?? { stt: { provider: null, providers: {} }, tts: { provider: null, providers: {} } };
|
|
2070
|
+
this.speechService.updateConfig(newSpeechConfig);
|
|
2071
|
+
const groqCfg = newSpeechConfig.stt?.providers?.groq;
|
|
2072
|
+
if (groqCfg?.apiKey) {
|
|
2073
|
+
this.speechService.registerSTTProvider("groq", new GroqSTT(groqCfg.apiKey, groqCfg.model));
|
|
2074
|
+
}
|
|
2075
|
+
log6.info("Speech service config updated at runtime");
|
|
1671
2076
|
}
|
|
1672
2077
|
});
|
|
1673
2078
|
}
|
|
@@ -1683,7 +2088,7 @@ var OpenACPCore = class {
|
|
|
1683
2088
|
}
|
|
1684
2089
|
async start() {
|
|
1685
2090
|
this.agentCatalog.refreshRegistryIfStale().catch((err) => {
|
|
1686
|
-
|
|
2091
|
+
log6.warn({ err }, "Background registry refresh failed");
|
|
1687
2092
|
});
|
|
1688
2093
|
for (const adapter of this.adapters.values()) {
|
|
1689
2094
|
await adapter.start();
|
|
@@ -1702,11 +2107,30 @@ var OpenACPCore = class {
|
|
|
1702
2107
|
for (const adapter of this.adapters.values()) {
|
|
1703
2108
|
await adapter.stop();
|
|
1704
2109
|
}
|
|
2110
|
+
if (this.usageStore) {
|
|
2111
|
+
this.usageStore.destroy();
|
|
2112
|
+
}
|
|
2113
|
+
}
|
|
2114
|
+
// --- Archive ---
|
|
2115
|
+
async archiveSession(sessionId) {
|
|
2116
|
+
const session = this.sessionManager.getSession(sessionId);
|
|
2117
|
+
if (!session) return { ok: false, error: "Session not found" };
|
|
2118
|
+
if (session.status === "initializing") return { ok: false, error: "Session is still initializing" };
|
|
2119
|
+
if (session.status !== "active") return { ok: false, error: `Session is ${session.status}` };
|
|
2120
|
+
const adapter = this.adapters.get(session.channelId);
|
|
2121
|
+
if (!adapter) return { ok: false, error: "Adapter not found for session" };
|
|
2122
|
+
try {
|
|
2123
|
+
const result = await adapter.archiveSessionTopic(session.id);
|
|
2124
|
+
if (!result) return { ok: false, error: "Adapter does not support archiving" };
|
|
2125
|
+
return { ok: true, newThreadId: result.newThreadId };
|
|
2126
|
+
} catch (err) {
|
|
2127
|
+
return { ok: false, error: err.message };
|
|
2128
|
+
}
|
|
1705
2129
|
}
|
|
1706
2130
|
// --- Message Routing ---
|
|
1707
2131
|
async handleMessage(message) {
|
|
1708
2132
|
const config = this.configManager.get();
|
|
1709
|
-
|
|
2133
|
+
log6.debug(
|
|
1710
2134
|
{
|
|
1711
2135
|
channelId: message.channelId,
|
|
1712
2136
|
threadId: message.threadId,
|
|
@@ -1715,9 +2139,10 @@ var OpenACPCore = class {
|
|
|
1715
2139
|
"Incoming message"
|
|
1716
2140
|
);
|
|
1717
2141
|
if (config.security.allowedUserIds.length > 0) {
|
|
1718
|
-
|
|
1719
|
-
|
|
1720
|
-
|
|
2142
|
+
const userId = String(message.userId);
|
|
2143
|
+
if (!config.security.allowedUserIds.includes(userId)) {
|
|
2144
|
+
log6.warn(
|
|
2145
|
+
{ userId },
|
|
1721
2146
|
"Rejected message from unauthorized user"
|
|
1722
2147
|
);
|
|
1723
2148
|
return;
|
|
@@ -1725,7 +2150,7 @@ var OpenACPCore = class {
|
|
|
1725
2150
|
}
|
|
1726
2151
|
const activeSessions = this.sessionManager.listSessions().filter((s) => s.status === "active" || s.status === "initializing");
|
|
1727
2152
|
if (activeSessions.length >= config.security.maxConcurrentSessions) {
|
|
1728
|
-
|
|
2153
|
+
log6.warn(
|
|
1729
2154
|
{
|
|
1730
2155
|
userId: message.userId,
|
|
1731
2156
|
currentCount: activeSessions.length,
|
|
@@ -1750,7 +2175,7 @@ var OpenACPCore = class {
|
|
|
1750
2175
|
session = await this.lazyResume(message) ?? void 0;
|
|
1751
2176
|
}
|
|
1752
2177
|
if (!session) {
|
|
1753
|
-
|
|
2178
|
+
log6.warn(
|
|
1754
2179
|
{ channelId: message.channelId, threadId: message.threadId },
|
|
1755
2180
|
"No session found for thread (in-memory miss + lazy resume returned null)"
|
|
1756
2181
|
);
|
|
@@ -1774,7 +2199,8 @@ var OpenACPCore = class {
|
|
|
1774
2199
|
channelId: params.channelId,
|
|
1775
2200
|
agentName: params.agentName,
|
|
1776
2201
|
workingDirectory: params.workingDirectory,
|
|
1777
|
-
agentInstance
|
|
2202
|
+
agentInstance,
|
|
2203
|
+
speechService: this.speechService
|
|
1778
2204
|
});
|
|
1779
2205
|
session.agentSessionId = agentInstance.sessionId;
|
|
1780
2206
|
if (params.initialName) {
|
|
@@ -1793,6 +2219,32 @@ var OpenACPCore = class {
|
|
|
1793
2219
|
const bridge = this.createBridge(session, adapter);
|
|
1794
2220
|
bridge.connect();
|
|
1795
2221
|
}
|
|
2222
|
+
if (this.usageStore) {
|
|
2223
|
+
session.on("agent_event", (event) => {
|
|
2224
|
+
if (event.type !== "usage") return;
|
|
2225
|
+
const record = {
|
|
2226
|
+
id: nanoid2(),
|
|
2227
|
+
sessionId: session.id,
|
|
2228
|
+
agentName: session.agentName,
|
|
2229
|
+
tokensUsed: event.tokensUsed ?? 0,
|
|
2230
|
+
contextSize: event.contextSize ?? 0,
|
|
2231
|
+
cost: event.cost,
|
|
2232
|
+
timestamp: (/* @__PURE__ */ new Date()).toISOString()
|
|
2233
|
+
};
|
|
2234
|
+
this.usageStore.append(record);
|
|
2235
|
+
if (this.usageBudget) {
|
|
2236
|
+
const result = this.usageBudget.check();
|
|
2237
|
+
if (result.message) {
|
|
2238
|
+
this.notificationManager.notifyAll({
|
|
2239
|
+
sessionId: session.id,
|
|
2240
|
+
sessionName: session.name,
|
|
2241
|
+
type: "budget_warning",
|
|
2242
|
+
summary: result.message
|
|
2243
|
+
});
|
|
2244
|
+
}
|
|
2245
|
+
}
|
|
2246
|
+
});
|
|
2247
|
+
}
|
|
1796
2248
|
session.on("status_change", (_from, to) => {
|
|
1797
2249
|
if ((to === "finished" || to === "cancelled") && this._tunnelService) {
|
|
1798
2250
|
this._tunnelService.stopBySession(session.id).then((stopped) => {
|
|
@@ -1814,7 +2266,11 @@ var OpenACPCore = class {
|
|
|
1814
2266
|
...existingRecord?.platform ?? {}
|
|
1815
2267
|
};
|
|
1816
2268
|
if (session.threadId) {
|
|
1817
|
-
|
|
2269
|
+
if (params.channelId === "telegram") {
|
|
2270
|
+
platform.topicId = Number(session.threadId);
|
|
2271
|
+
} else {
|
|
2272
|
+
platform.threadId = session.threadId;
|
|
2273
|
+
}
|
|
1818
2274
|
}
|
|
1819
2275
|
await this.sessionManager.patchRecord(session.id, {
|
|
1820
2276
|
sessionId: session.id,
|
|
@@ -1828,7 +2284,7 @@ var OpenACPCore = class {
|
|
|
1828
2284
|
name: session.name,
|
|
1829
2285
|
platform
|
|
1830
2286
|
});
|
|
1831
|
-
|
|
2287
|
+
log6.info(
|
|
1832
2288
|
{ sessionId: session.id, agentName: params.agentName },
|
|
1833
2289
|
"Session created via pipeline"
|
|
1834
2290
|
);
|
|
@@ -1837,7 +2293,7 @@ var OpenACPCore = class {
|
|
|
1837
2293
|
async handleNewSession(channelId, agentName, workspacePath) {
|
|
1838
2294
|
const config = this.configManager.get();
|
|
1839
2295
|
const resolvedAgent = agentName || config.defaultAgent;
|
|
1840
|
-
|
|
2296
|
+
log6.info({ channelId, agentName: resolvedAgent }, "New session request");
|
|
1841
2297
|
const agentDef = this.agentCatalog.resolve(resolvedAgent);
|
|
1842
2298
|
const resolvedWorkspace = this.configManager.resolveWorkspace(
|
|
1843
2299
|
workspacePath || agentDef?.workingDirectory
|
|
@@ -1952,20 +2408,20 @@ var OpenACPCore = class {
|
|
|
1952
2408
|
(p) => String(p.topicId) === message.threadId
|
|
1953
2409
|
);
|
|
1954
2410
|
if (!record) {
|
|
1955
|
-
|
|
2411
|
+
log6.debug(
|
|
1956
2412
|
{ threadId: message.threadId, channelId: message.channelId },
|
|
1957
2413
|
"No session record found for thread"
|
|
1958
2414
|
);
|
|
1959
2415
|
return null;
|
|
1960
2416
|
}
|
|
1961
2417
|
if (record.status === "error") {
|
|
1962
|
-
|
|
2418
|
+
log6.debug(
|
|
1963
2419
|
{ threadId: message.threadId, sessionId: record.sessionId, status: record.status },
|
|
1964
2420
|
"Skipping resume of error session"
|
|
1965
2421
|
);
|
|
1966
2422
|
return null;
|
|
1967
2423
|
}
|
|
1968
|
-
|
|
2424
|
+
log6.info(
|
|
1969
2425
|
{ threadId: message.threadId, sessionId: record.sessionId, status: record.status },
|
|
1970
2426
|
"Lazy resume: found record, attempting resume"
|
|
1971
2427
|
);
|
|
@@ -1982,13 +2438,13 @@ var OpenACPCore = class {
|
|
|
1982
2438
|
session.threadId = message.threadId;
|
|
1983
2439
|
session.activate();
|
|
1984
2440
|
session.dangerousMode = record.dangerousMode ?? false;
|
|
1985
|
-
|
|
2441
|
+
log6.info(
|
|
1986
2442
|
{ sessionId: session.id, threadId: message.threadId },
|
|
1987
2443
|
"Lazy resume successful"
|
|
1988
2444
|
);
|
|
1989
2445
|
return session;
|
|
1990
2446
|
} catch (err) {
|
|
1991
|
-
|
|
2447
|
+
log6.error({ err, record }, "Lazy resume failed");
|
|
1992
2448
|
const adapter = this.adapters.get(message.channelId);
|
|
1993
2449
|
if (adapter) {
|
|
1994
2450
|
try {
|
|
@@ -2019,36 +2475,22 @@ var OpenACPCore = class {
|
|
|
2019
2475
|
}
|
|
2020
2476
|
};
|
|
2021
2477
|
|
|
2022
|
-
// src/core/channel.ts
|
|
2023
|
-
var ChannelAdapter = class {
|
|
2024
|
-
constructor(core, config) {
|
|
2025
|
-
this.core = core;
|
|
2026
|
-
this.config = config;
|
|
2027
|
-
}
|
|
2028
|
-
async deleteSessionThread(_sessionId) {
|
|
2029
|
-
}
|
|
2030
|
-
// Skill commands — override in adapters that support dynamic commands
|
|
2031
|
-
async sendSkillCommands(_sessionId, _commands) {
|
|
2032
|
-
}
|
|
2033
|
-
async cleanupSkillCommands(_sessionId) {
|
|
2034
|
-
}
|
|
2035
|
-
};
|
|
2036
|
-
|
|
2037
2478
|
// src/core/api-server.ts
|
|
2038
2479
|
import * as http from "http";
|
|
2039
|
-
import * as
|
|
2040
|
-
import * as
|
|
2480
|
+
import * as fs6 from "fs";
|
|
2481
|
+
import * as path6 from "path";
|
|
2041
2482
|
import * as os2 from "os";
|
|
2483
|
+
import * as crypto from "crypto";
|
|
2042
2484
|
import { fileURLToPath } from "url";
|
|
2043
|
-
var
|
|
2044
|
-
var DEFAULT_PORT_FILE =
|
|
2485
|
+
var log7 = createChildLogger({ module: "api-server" });
|
|
2486
|
+
var DEFAULT_PORT_FILE = path6.join(os2.homedir(), ".openacp", "api.port");
|
|
2045
2487
|
var cachedVersion;
|
|
2046
2488
|
function getVersion() {
|
|
2047
2489
|
if (cachedVersion) return cachedVersion;
|
|
2048
2490
|
try {
|
|
2049
2491
|
const __filename = fileURLToPath(import.meta.url);
|
|
2050
|
-
const pkgPath =
|
|
2051
|
-
const pkg = JSON.parse(
|
|
2492
|
+
const pkgPath = path6.resolve(path6.dirname(__filename), "../../package.json");
|
|
2493
|
+
const pkg = JSON.parse(fs6.readFileSync(pkgPath, "utf-8"));
|
|
2052
2494
|
cachedVersion = pkg.version ?? "0.0.0-dev";
|
|
2053
2495
|
} catch {
|
|
2054
2496
|
cachedVersion = "0.0.0-dev";
|
|
@@ -2071,22 +2513,26 @@ function redactDeep(obj) {
|
|
|
2071
2513
|
}
|
|
2072
2514
|
}
|
|
2073
2515
|
var ApiServer = class {
|
|
2074
|
-
constructor(core, config, portFilePath, topicManager) {
|
|
2516
|
+
constructor(core, config, portFilePath, topicManager, secretFilePath) {
|
|
2075
2517
|
this.core = core;
|
|
2076
2518
|
this.config = config;
|
|
2077
2519
|
this.topicManager = topicManager;
|
|
2078
2520
|
this.portFilePath = portFilePath ?? DEFAULT_PORT_FILE;
|
|
2521
|
+
this.secretFilePath = secretFilePath ?? path6.join(os2.homedir(), ".openacp", "api-secret");
|
|
2079
2522
|
}
|
|
2080
2523
|
server = null;
|
|
2081
2524
|
actualPort = 0;
|
|
2082
2525
|
portFilePath;
|
|
2083
2526
|
startedAt = Date.now();
|
|
2527
|
+
secret = "";
|
|
2528
|
+
secretFilePath;
|
|
2084
2529
|
async start() {
|
|
2530
|
+
this.loadOrCreateSecret();
|
|
2085
2531
|
this.server = http.createServer((req, res) => this.handleRequest(req, res));
|
|
2086
2532
|
await new Promise((resolve2, reject) => {
|
|
2087
2533
|
this.server.on("error", (err) => {
|
|
2088
2534
|
if (err.code === "EADDRINUSE") {
|
|
2089
|
-
|
|
2535
|
+
log7.warn({ port: this.config.port }, "API port in use, continuing without API server");
|
|
2090
2536
|
this.server = null;
|
|
2091
2537
|
resolve2();
|
|
2092
2538
|
} else {
|
|
@@ -2099,7 +2545,7 @@ var ApiServer = class {
|
|
|
2099
2545
|
this.actualPort = addr.port;
|
|
2100
2546
|
}
|
|
2101
2547
|
this.writePortFile();
|
|
2102
|
-
|
|
2548
|
+
log7.info({ host: this.config.host, port: this.actualPort }, "API server listening");
|
|
2103
2549
|
resolve2();
|
|
2104
2550
|
});
|
|
2105
2551
|
});
|
|
@@ -2117,19 +2563,52 @@ var ApiServer = class {
|
|
|
2117
2563
|
return this.actualPort;
|
|
2118
2564
|
}
|
|
2119
2565
|
writePortFile() {
|
|
2120
|
-
const dir =
|
|
2121
|
-
|
|
2122
|
-
|
|
2566
|
+
const dir = path6.dirname(this.portFilePath);
|
|
2567
|
+
fs6.mkdirSync(dir, { recursive: true });
|
|
2568
|
+
fs6.writeFileSync(this.portFilePath, String(this.actualPort));
|
|
2123
2569
|
}
|
|
2124
2570
|
removePortFile() {
|
|
2125
2571
|
try {
|
|
2126
|
-
|
|
2572
|
+
fs6.unlinkSync(this.portFilePath);
|
|
2573
|
+
} catch {
|
|
2574
|
+
}
|
|
2575
|
+
}
|
|
2576
|
+
loadOrCreateSecret() {
|
|
2577
|
+
const dir = path6.dirname(this.secretFilePath);
|
|
2578
|
+
fs6.mkdirSync(dir, { recursive: true });
|
|
2579
|
+
try {
|
|
2580
|
+
this.secret = fs6.readFileSync(this.secretFilePath, "utf-8").trim();
|
|
2581
|
+
if (this.secret) {
|
|
2582
|
+
try {
|
|
2583
|
+
const stat = fs6.statSync(this.secretFilePath);
|
|
2584
|
+
const mode = stat.mode & 511;
|
|
2585
|
+
if (mode & 63) {
|
|
2586
|
+
log7.warn({ path: this.secretFilePath, mode: "0" + mode.toString(8) }, "API secret file has insecure permissions (should be 0600). Run: chmod 600 %s", this.secretFilePath);
|
|
2587
|
+
}
|
|
2588
|
+
} catch {
|
|
2589
|
+
}
|
|
2590
|
+
return;
|
|
2591
|
+
}
|
|
2127
2592
|
} catch {
|
|
2128
2593
|
}
|
|
2594
|
+
this.secret = crypto.randomBytes(32).toString("hex");
|
|
2595
|
+
fs6.writeFileSync(this.secretFilePath, this.secret, { mode: 384 });
|
|
2596
|
+
}
|
|
2597
|
+
authenticate(req) {
|
|
2598
|
+
const authHeader = req.headers.authorization;
|
|
2599
|
+
if (!authHeader?.startsWith("Bearer ")) return false;
|
|
2600
|
+
const token = authHeader.slice(7);
|
|
2601
|
+
if (token.length !== this.secret.length) return false;
|
|
2602
|
+
return crypto.timingSafeEqual(Buffer.from(token, "utf-8"), Buffer.from(this.secret, "utf-8"));
|
|
2129
2603
|
}
|
|
2130
2604
|
async handleRequest(req, res) {
|
|
2131
2605
|
const method = req.method?.toUpperCase();
|
|
2132
2606
|
const url = req.url || "";
|
|
2607
|
+
const isExempt = method === "GET" && (url === "/api/health" || url === "/api/version");
|
|
2608
|
+
if (!isExempt && !this.authenticate(req)) {
|
|
2609
|
+
this.sendJson(res, 401, { error: "Unauthorized" });
|
|
2610
|
+
return;
|
|
2611
|
+
}
|
|
2133
2612
|
try {
|
|
2134
2613
|
if (method === "POST" && url === "/api/sessions/adopt") {
|
|
2135
2614
|
await this.handleAdoptSession(req, res);
|
|
@@ -2144,6 +2623,9 @@ var ApiServer = class {
|
|
|
2144
2623
|
} else if (method === "GET" && url.match(/^\/api\/sessions\/([^/]+)$/)) {
|
|
2145
2624
|
const sessionId = decodeURIComponent(url.match(/^\/api\/sessions\/([^/]+)$/)[1]);
|
|
2146
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);
|
|
2147
2629
|
} else if (method === "DELETE" && url.match(/^\/api\/sessions\/([^/]+)$/)) {
|
|
2148
2630
|
const sessionId = decodeURIComponent(url.match(/^\/api\/sessions\/([^/]+)$/)[1]);
|
|
2149
2631
|
await this.handleCancelSession(sessionId, res);
|
|
@@ -2189,7 +2671,7 @@ var ApiServer = class {
|
|
|
2189
2671
|
this.sendJson(res, 404, { error: "Not found" });
|
|
2190
2672
|
}
|
|
2191
2673
|
} catch (err) {
|
|
2192
|
-
|
|
2674
|
+
log7.error({ err }, "API request error");
|
|
2193
2675
|
this.sendJson(res, 500, { error: "Internal server error" });
|
|
2194
2676
|
}
|
|
2195
2677
|
}
|
|
@@ -2231,11 +2713,11 @@ var ApiServer = class {
|
|
|
2231
2713
|
if (!adapter) {
|
|
2232
2714
|
session.agentInstance.onPermissionRequest = async (request) => {
|
|
2233
2715
|
const allowOption = request.options.find((o) => o.isAllow);
|
|
2234
|
-
|
|
2716
|
+
log7.debug({ sessionId: session.id, permissionId: request.id, option: allowOption?.id }, "Auto-approving permission for API session");
|
|
2235
2717
|
return allowOption?.id ?? request.options[0]?.id ?? "";
|
|
2236
2718
|
};
|
|
2237
2719
|
}
|
|
2238
|
-
session.warmup().catch((err) =>
|
|
2720
|
+
session.warmup().catch((err) => log7.warn({ err, sessionId: session.id }, "API session warmup failed"));
|
|
2239
2721
|
this.sendJson(res, 200, {
|
|
2240
2722
|
sessionId: session.id,
|
|
2241
2723
|
agent: session.agentName,
|
|
@@ -2346,7 +2828,7 @@ var ApiServer = class {
|
|
|
2346
2828
|
this.sendJson(res, 200, { version: getVersion() });
|
|
2347
2829
|
}
|
|
2348
2830
|
async handleGetEditableConfig(res) {
|
|
2349
|
-
const { getSafeFields: getSafeFields2, resolveOptions: resolveOptions2, getConfigValue: getConfigValue2 } = await import("./config-registry-
|
|
2831
|
+
const { getSafeFields: getSafeFields2, resolveOptions: resolveOptions2, getConfigValue: getConfigValue2 } = await import("./config-registry-QQOJ2GQP.js");
|
|
2350
2832
|
const config = this.core.configManager.get();
|
|
2351
2833
|
const safeFields = getSafeFields2();
|
|
2352
2834
|
const fields = safeFields.map((def) => ({
|
|
@@ -2387,20 +2869,20 @@ var ApiServer = class {
|
|
|
2387
2869
|
const parts = configPath.split(".");
|
|
2388
2870
|
let target = cloned;
|
|
2389
2871
|
for (let i = 0; i < parts.length - 1; i++) {
|
|
2390
|
-
|
|
2391
|
-
|
|
2872
|
+
const part = parts[i];
|
|
2873
|
+
if (target[part] && typeof target[part] === "object" && !Array.isArray(target[part])) {
|
|
2874
|
+
target = target[part];
|
|
2875
|
+
} else if (target[part] === void 0 || target[part] === null) {
|
|
2876
|
+
target[part] = {};
|
|
2877
|
+
target = target[part];
|
|
2392
2878
|
} else {
|
|
2393
2879
|
this.sendJson(res, 400, { error: "Invalid config path" });
|
|
2394
2880
|
return;
|
|
2395
2881
|
}
|
|
2396
2882
|
}
|
|
2397
2883
|
const lastKey = parts[parts.length - 1];
|
|
2398
|
-
if (!(lastKey in target)) {
|
|
2399
|
-
this.sendJson(res, 400, { error: "Invalid config path" });
|
|
2400
|
-
return;
|
|
2401
|
-
}
|
|
2402
2884
|
target[lastKey] = value;
|
|
2403
|
-
const { ConfigSchema } = await import("./config-
|
|
2885
|
+
const { ConfigSchema } = await import("./config-AK2W3E67.js");
|
|
2404
2886
|
const result = ConfigSchema.safeParse(cloned);
|
|
2405
2887
|
if (!result.success) {
|
|
2406
2888
|
this.sendJson(res, 400, {
|
|
@@ -2417,7 +2899,7 @@ var ApiServer = class {
|
|
|
2417
2899
|
}
|
|
2418
2900
|
updateTarget[lastKey] = value;
|
|
2419
2901
|
await this.core.configManager.save(updates, configPath);
|
|
2420
|
-
const { isHotReloadable: isHotReloadable2 } = await import("./config-registry-
|
|
2902
|
+
const { isHotReloadable: isHotReloadable2 } = await import("./config-registry-QQOJ2GQP.js");
|
|
2421
2903
|
const needsRestart = !isHotReloadable2(configPath);
|
|
2422
2904
|
this.sendJson(res, 200, {
|
|
2423
2905
|
ok: true,
|
|
@@ -2525,6 +3007,14 @@ var ApiServer = class {
|
|
|
2525
3007
|
this.sendJson(res, 200, { ok: true, message: "Restarting..." });
|
|
2526
3008
|
setImmediate(() => this.core.requestRestart());
|
|
2527
3009
|
}
|
|
3010
|
+
async handleArchiveSession(sessionId, res) {
|
|
3011
|
+
const result = await this.core.archiveSession(sessionId);
|
|
3012
|
+
if (result.ok) {
|
|
3013
|
+
this.sendJson(res, 200, result);
|
|
3014
|
+
} else {
|
|
3015
|
+
this.sendJson(res, 400, result);
|
|
3016
|
+
}
|
|
3017
|
+
}
|
|
2528
3018
|
async handleCancelSession(sessionId, res) {
|
|
2529
3019
|
const session = this.core.sessionManager.getSession(sessionId);
|
|
2530
3020
|
if (!session) {
|
|
@@ -2640,7 +3130,7 @@ var ApiServer = class {
|
|
|
2640
3130
|
};
|
|
2641
3131
|
|
|
2642
3132
|
// src/core/topic-manager.ts
|
|
2643
|
-
var
|
|
3133
|
+
var log8 = createChildLogger({ module: "topic-manager" });
|
|
2644
3134
|
var TopicManager = class {
|
|
2645
3135
|
constructor(sessionManager, adapter, systemTopicIds) {
|
|
2646
3136
|
this.sessionManager = sessionManager;
|
|
@@ -2679,7 +3169,7 @@ var TopicManager = class {
|
|
|
2679
3169
|
try {
|
|
2680
3170
|
await this.adapter.deleteSessionThread(sessionId);
|
|
2681
3171
|
} catch (err) {
|
|
2682
|
-
|
|
3172
|
+
log8.warn({ err, sessionId, topicId }, "Failed to delete platform thread, removing record anyway");
|
|
2683
3173
|
}
|
|
2684
3174
|
}
|
|
2685
3175
|
await this.sessionManager.removeRecord(sessionId);
|
|
@@ -2702,7 +3192,7 @@ var TopicManager = class {
|
|
|
2702
3192
|
try {
|
|
2703
3193
|
await this.adapter.deleteSessionThread(record.sessionId);
|
|
2704
3194
|
} catch (err) {
|
|
2705
|
-
|
|
3195
|
+
log8.warn({ err, sessionId: record.sessionId }, "Failed to delete platform thread during cleanup");
|
|
2706
3196
|
}
|
|
2707
3197
|
}
|
|
2708
3198
|
await this.sessionManager.removeRecord(record.sessionId);
|
|
@@ -2749,6 +3239,9 @@ async function renameSessionTopic(bot, chatId, threadId, name) {
|
|
|
2749
3239
|
} catch {
|
|
2750
3240
|
}
|
|
2751
3241
|
}
|
|
3242
|
+
async function deleteSessionTopic(bot, chatId, threadId) {
|
|
3243
|
+
await bot.api.deleteForumTopic(chatId, threadId);
|
|
3244
|
+
}
|
|
2752
3245
|
function buildDeepLink(chatId, messageId) {
|
|
2753
3246
|
const cleanId = String(chatId).replace("-100", "");
|
|
2754
3247
|
return `https://t.me/c/${cleanId}/${messageId}`;
|
|
@@ -2887,6 +3380,33 @@ function formatUsage(usage) {
|
|
|
2887
3380
|
return `${emoji} ${formatTokens(tokensUsed)} / ${formatTokens(contextSize)} tokens
|
|
2888
3381
|
${bar} ${pct}%`;
|
|
2889
3382
|
}
|
|
3383
|
+
var PERIOD_LABEL = {
|
|
3384
|
+
today: "Today",
|
|
3385
|
+
week: "This Week",
|
|
3386
|
+
month: "This Month",
|
|
3387
|
+
all: "All Time"
|
|
3388
|
+
};
|
|
3389
|
+
function formatUsageReport(summaries, budgetStatus) {
|
|
3390
|
+
const hasData = summaries.some((s) => s.recordCount > 0);
|
|
3391
|
+
if (!hasData) {
|
|
3392
|
+
return "\u{1F4CA} <b>Usage Report</b>\n\nNo usage data yet.";
|
|
3393
|
+
}
|
|
3394
|
+
const formatCost = (n) => `$${n.toFixed(2)}`;
|
|
3395
|
+
const lines = ["\u{1F4CA} <b>Usage Report</b>"];
|
|
3396
|
+
for (const summary of summaries) {
|
|
3397
|
+
lines.push("");
|
|
3398
|
+
lines.push(`\u2500\u2500 <b>${PERIOD_LABEL[summary.period] ?? summary.period}</b> \u2500\u2500`);
|
|
3399
|
+
lines.push(
|
|
3400
|
+
`\u{1F4B0} ${formatCost(summary.totalCost)} \xB7 \u{1F524} ${formatTokens(summary.totalTokens)} tokens \xB7 \u{1F4CB} ${summary.sessionCount} sessions`
|
|
3401
|
+
);
|
|
3402
|
+
if (summary.period === "month" && budgetStatus.budget > 0) {
|
|
3403
|
+
const bar = progressBar(budgetStatus.used / budgetStatus.budget);
|
|
3404
|
+
lines.push(`Budget: ${formatCost(budgetStatus.used)} / ${formatCost(budgetStatus.budget)} (${budgetStatus.percent}%)`);
|
|
3405
|
+
lines.push(`${bar} ${budgetStatus.percent}%`);
|
|
3406
|
+
}
|
|
3407
|
+
}
|
|
3408
|
+
return lines.join("\n");
|
|
3409
|
+
}
|
|
2890
3410
|
function splitMessage(text, maxLength = 3800) {
|
|
2891
3411
|
if (text.length <= maxLength) return [text];
|
|
2892
3412
|
const chunks = [];
|
|
@@ -2922,7 +3442,7 @@ function splitMessage(text, maxLength = 3800) {
|
|
|
2922
3442
|
|
|
2923
3443
|
// src/adapters/telegram/commands/admin.ts
|
|
2924
3444
|
import { InlineKeyboard } from "grammy";
|
|
2925
|
-
var
|
|
3445
|
+
var log10 = createChildLogger({ module: "telegram-cmd-admin" });
|
|
2926
3446
|
function buildDangerousModeKeyboard(sessionId, enabled) {
|
|
2927
3447
|
return new InlineKeyboard().text(
|
|
2928
3448
|
enabled ? "\u{1F510} Disable Dangerous Mode" : "\u2620\uFE0F Enable Dangerous Mode",
|
|
@@ -2935,7 +3455,7 @@ function setupDangerousModeCallbacks(bot, core) {
|
|
|
2935
3455
|
const session = core.sessionManager.getSession(sessionId);
|
|
2936
3456
|
if (session) {
|
|
2937
3457
|
session.dangerousMode = !session.dangerousMode;
|
|
2938
|
-
|
|
3458
|
+
log10.info({ sessionId, dangerousMode: session.dangerousMode }, "Dangerous mode toggled via button");
|
|
2939
3459
|
core.sessionManager.patchRecord(sessionId, { dangerousMode: session.dangerousMode }).catch(() => {
|
|
2940
3460
|
});
|
|
2941
3461
|
const toastText2 = session.dangerousMode ? "\u2620\uFE0F Dangerous mode enabled \u2014 permissions auto-approved" : "\u{1F510} Dangerous mode disabled \u2014 permissions shown normally";
|
|
@@ -2962,7 +3482,7 @@ function setupDangerousModeCallbacks(bot, core) {
|
|
|
2962
3482
|
const newDangerousMode = !(record.dangerousMode ?? false);
|
|
2963
3483
|
core.sessionManager.patchRecord(sessionId, { dangerousMode: newDangerousMode }).catch(() => {
|
|
2964
3484
|
});
|
|
2965
|
-
|
|
3485
|
+
log10.info({ sessionId, dangerousMode: newDangerousMode }, "Dangerous mode toggled via button (store-only, session not in memory)");
|
|
2966
3486
|
const toastText = newDangerousMode ? "\u2620\uFE0F Dangerous mode enabled \u2014 permissions auto-approved" : "\u{1F510} Dangerous mode disabled \u2014 permissions shown normally";
|
|
2967
3487
|
try {
|
|
2968
3488
|
await ctx.answerCallbackQuery({ text: toastText });
|
|
@@ -3048,7 +3568,7 @@ async function handleUpdate(ctx, core) {
|
|
|
3048
3568
|
await ctx.reply("\u26A0\uFE0F Update is not available (no restart handler registered).", { parse_mode: "HTML" });
|
|
3049
3569
|
return;
|
|
3050
3570
|
}
|
|
3051
|
-
const { getCurrentVersion, getLatestVersion, compareVersions, runUpdate } = await import("./version-
|
|
3571
|
+
const { getCurrentVersion, getLatestVersion, compareVersions, runUpdate } = await import("./version-ALWGGVKM.js");
|
|
3052
3572
|
const current = getCurrentVersion();
|
|
3053
3573
|
const statusMsg = await ctx.reply(`\u{1F50D} Checking for updates... (current: v${escapeHtml(current)})`, { parse_mode: "HTML" });
|
|
3054
3574
|
const latest = await getLatestVersion();
|
|
@@ -3091,7 +3611,7 @@ async function handleRestart(ctx, core) {
|
|
|
3091
3611
|
}
|
|
3092
3612
|
|
|
3093
3613
|
// src/adapters/telegram/commands/new-session.ts
|
|
3094
|
-
var
|
|
3614
|
+
var log11 = createChildLogger({ module: "telegram-cmd-new-session" });
|
|
3095
3615
|
var pendingNewSessions = /* @__PURE__ */ new Map();
|
|
3096
3616
|
var PENDING_TIMEOUT_MS = 5 * 60 * 1e3;
|
|
3097
3617
|
function cleanupPending(userId) {
|
|
@@ -3190,7 +3710,7 @@ async function startConfirmStep(ctx, chatId, userId, agentName, workspace) {
|
|
|
3190
3710
|
});
|
|
3191
3711
|
}
|
|
3192
3712
|
async function createSessionDirect(ctx, core, chatId, agentName, workspace) {
|
|
3193
|
-
|
|
3713
|
+
log11.info({ userId: ctx.from?.id, agentName, workspace }, "New session command (direct)");
|
|
3194
3714
|
let threadId;
|
|
3195
3715
|
try {
|
|
3196
3716
|
const topicName = `\u{1F504} New Session`;
|
|
@@ -3220,10 +3740,10 @@ This is your coding session \u2014 chat here to work with the agent.`,
|
|
|
3220
3740
|
reply_markup: buildDangerousModeKeyboard(session.id, false)
|
|
3221
3741
|
}
|
|
3222
3742
|
);
|
|
3223
|
-
session.warmup().catch((err) =>
|
|
3743
|
+
session.warmup().catch((err) => log11.error({ err }, "Warm-up error"));
|
|
3224
3744
|
return threadId ?? null;
|
|
3225
3745
|
} catch (err) {
|
|
3226
|
-
|
|
3746
|
+
log11.error({ err }, "Session creation failed");
|
|
3227
3747
|
if (threadId) {
|
|
3228
3748
|
try {
|
|
3229
3749
|
await ctx.api.deleteForumTopic(chatId, threadId);
|
|
@@ -3301,7 +3821,7 @@ async function handleNewChat(ctx, core, chatId) {
|
|
|
3301
3821
|
reply_markup: buildDangerousModeKeyboard(session.id, false)
|
|
3302
3822
|
}
|
|
3303
3823
|
);
|
|
3304
|
-
session.warmup().catch((err) =>
|
|
3824
|
+
session.warmup().catch((err) => log11.error({ err }, "Warm-up error"));
|
|
3305
3825
|
} catch (err) {
|
|
3306
3826
|
if (newThreadId) {
|
|
3307
3827
|
try {
|
|
@@ -3332,7 +3852,7 @@ async function executeNewSession(bot, core, chatId, agentName, workspace) {
|
|
|
3332
3852
|
} });
|
|
3333
3853
|
const finalName = `\u{1F504} ${session.agentName} \u2014 New Session`;
|
|
3334
3854
|
await renameSessionTopic(bot, chatId, threadId, finalName);
|
|
3335
|
-
session.warmup().catch((err) =>
|
|
3855
|
+
session.warmup().catch((err) => log11.error({ err }, "Warm-up error"));
|
|
3336
3856
|
return { session, threadId, firstMsgId };
|
|
3337
3857
|
} catch (err) {
|
|
3338
3858
|
try {
|
|
@@ -3480,7 +4000,7 @@ Or just the folder name like <code>my-project</code> (will use ${core.configMana
|
|
|
3480
4000
|
|
|
3481
4001
|
// src/adapters/telegram/commands/session.ts
|
|
3482
4002
|
import { InlineKeyboard as InlineKeyboard3 } from "grammy";
|
|
3483
|
-
var
|
|
4003
|
+
var log12 = createChildLogger({ module: "telegram-cmd-session" });
|
|
3484
4004
|
async function handleCancel(ctx, core, assistant) {
|
|
3485
4005
|
const threadId = ctx.message?.message_thread_id;
|
|
3486
4006
|
if (!threadId) return;
|
|
@@ -3498,14 +4018,14 @@ async function handleCancel(ctx, core, assistant) {
|
|
|
3498
4018
|
String(threadId)
|
|
3499
4019
|
);
|
|
3500
4020
|
if (session) {
|
|
3501
|
-
|
|
4021
|
+
log12.info({ sessionId: session.id }, "Abort prompt command");
|
|
3502
4022
|
await session.abortPrompt();
|
|
3503
4023
|
await ctx.reply("\u26D4 Prompt aborted. Session is still active \u2014 send a new message to continue.", { parse_mode: "HTML" });
|
|
3504
4024
|
return;
|
|
3505
4025
|
}
|
|
3506
4026
|
const record = core.sessionManager.getRecordByThread("telegram", String(threadId));
|
|
3507
4027
|
if (record && record.status !== "error") {
|
|
3508
|
-
|
|
4028
|
+
log12.info({ sessionId: record.sessionId, status: record.status }, "Cancel command \u2014 no active prompt to abort");
|
|
3509
4029
|
await ctx.reply("\u2139\uFE0F No active prompt to cancel. Send a new message to resume the session.", { parse_mode: "HTML" });
|
|
3510
4030
|
}
|
|
3511
4031
|
}
|
|
@@ -3609,7 +4129,7 @@ ${lines.join("\n")}${truncated}`,
|
|
|
3609
4129
|
{ parse_mode: "HTML", reply_markup: keyboard }
|
|
3610
4130
|
);
|
|
3611
4131
|
} catch (err) {
|
|
3612
|
-
|
|
4132
|
+
log12.error({ err }, "handleTopics error");
|
|
3613
4133
|
await ctx.reply("\u274C Failed to list sessions.", { parse_mode: "HTML" }).catch(() => {
|
|
3614
4134
|
});
|
|
3615
4135
|
}
|
|
@@ -3630,13 +4150,13 @@ async function handleCleanup(ctx, core, chatId, statuses) {
|
|
|
3630
4150
|
try {
|
|
3631
4151
|
await ctx.api.deleteForumTopic(chatId, topicId);
|
|
3632
4152
|
} catch (err) {
|
|
3633
|
-
|
|
4153
|
+
log12.warn({ err, sessionId: record.sessionId, topicId }, "Failed to delete forum topic during cleanup");
|
|
3634
4154
|
}
|
|
3635
4155
|
}
|
|
3636
4156
|
await core.sessionManager.removeRecord(record.sessionId);
|
|
3637
4157
|
deleted++;
|
|
3638
4158
|
} catch (err) {
|
|
3639
|
-
|
|
4159
|
+
log12.error({ err, sessionId: record.sessionId }, "Failed to cleanup session");
|
|
3640
4160
|
failed++;
|
|
3641
4161
|
}
|
|
3642
4162
|
}
|
|
@@ -3707,7 +4227,7 @@ async function handleCleanupEverythingConfirmed(ctx, core, chatId, systemTopicId
|
|
|
3707
4227
|
try {
|
|
3708
4228
|
await core.sessionManager.cancelSession(record.sessionId);
|
|
3709
4229
|
} catch (err) {
|
|
3710
|
-
|
|
4230
|
+
log12.warn({ err, sessionId: record.sessionId }, "Failed to cancel session during cleanup");
|
|
3711
4231
|
}
|
|
3712
4232
|
}
|
|
3713
4233
|
const topicId = record.platform?.topicId;
|
|
@@ -3715,13 +4235,13 @@ async function handleCleanupEverythingConfirmed(ctx, core, chatId, systemTopicId
|
|
|
3715
4235
|
try {
|
|
3716
4236
|
await ctx.api.deleteForumTopic(chatId, topicId);
|
|
3717
4237
|
} catch (err) {
|
|
3718
|
-
|
|
4238
|
+
log12.warn({ err, sessionId: record.sessionId, topicId }, "Failed to delete forum topic during cleanup");
|
|
3719
4239
|
}
|
|
3720
4240
|
}
|
|
3721
4241
|
await core.sessionManager.removeRecord(record.sessionId);
|
|
3722
4242
|
deleted++;
|
|
3723
4243
|
} catch (err) {
|
|
3724
|
-
|
|
4244
|
+
log12.error({ err, sessionId: record.sessionId }, "Failed to cleanup session");
|
|
3725
4245
|
failed++;
|
|
3726
4246
|
}
|
|
3727
4247
|
}
|
|
@@ -3763,6 +4283,86 @@ function setupSessionCallbacks(bot, core, chatId, systemTopicIds) {
|
|
|
3763
4283
|
}
|
|
3764
4284
|
});
|
|
3765
4285
|
}
|
|
4286
|
+
async function handleUsage(ctx, core) {
|
|
4287
|
+
if (!core.usageStore) {
|
|
4288
|
+
await ctx.reply("\u{1F4CA} Usage tracking is disabled.", { parse_mode: "HTML" });
|
|
4289
|
+
return;
|
|
4290
|
+
}
|
|
4291
|
+
const rawMatch = ctx.match;
|
|
4292
|
+
const period = typeof rawMatch === "string" ? rawMatch.trim().toLowerCase() : "";
|
|
4293
|
+
let summaries;
|
|
4294
|
+
if (period === "today" || period === "week" || period === "month") {
|
|
4295
|
+
summaries = [core.usageStore.query(period)];
|
|
4296
|
+
} else {
|
|
4297
|
+
summaries = [
|
|
4298
|
+
core.usageStore.query("month"),
|
|
4299
|
+
core.usageStore.query("week"),
|
|
4300
|
+
core.usageStore.query("today")
|
|
4301
|
+
];
|
|
4302
|
+
}
|
|
4303
|
+
const budgetStatus = core.usageBudget ? core.usageBudget.getStatus() : { status: "ok", used: 0, budget: 0, percent: 0 };
|
|
4304
|
+
const text = formatUsageReport(summaries, budgetStatus);
|
|
4305
|
+
await ctx.reply(text, { parse_mode: "HTML" });
|
|
4306
|
+
}
|
|
4307
|
+
async function handleArchive(ctx, core) {
|
|
4308
|
+
const threadId = ctx.message?.message_thread_id;
|
|
4309
|
+
if (!threadId) return;
|
|
4310
|
+
const session = core.sessionManager.getSessionByThread("telegram", String(threadId));
|
|
4311
|
+
if (!session) {
|
|
4312
|
+
await ctx.reply(
|
|
4313
|
+
"\u2139\uFE0F <b>/archive</b> works in session topics \u2014 it recreates the topic with a clean chat view while keeping your agent session alive.\n\nGo to the session topic you want to archive and type /archive there.",
|
|
4314
|
+
{ parse_mode: "HTML" }
|
|
4315
|
+
);
|
|
4316
|
+
return;
|
|
4317
|
+
}
|
|
4318
|
+
if (session.status === "initializing") {
|
|
4319
|
+
await ctx.reply("\u23F3 Please wait for session to be ready.", { parse_mode: "HTML" });
|
|
4320
|
+
return;
|
|
4321
|
+
}
|
|
4322
|
+
if (session.status !== "active") {
|
|
4323
|
+
await ctx.reply(`\u26A0\uFE0F Cannot archive \u2014 session is ${session.status}.`, { parse_mode: "HTML" });
|
|
4324
|
+
return;
|
|
4325
|
+
}
|
|
4326
|
+
await ctx.reply(
|
|
4327
|
+
"\u26A0\uFE0F <b>Archive this session topic?</b>\n\nThis will permanently delete all messages in this topic and create a fresh one.\nYour agent session will continue \u2014 only the chat view is reset.\n\n<i>Note: links to messages in this topic will stop working.</i>",
|
|
4328
|
+
{
|
|
4329
|
+
parse_mode: "HTML",
|
|
4330
|
+
reply_markup: new InlineKeyboard3().text("\u{1F5D1} Yes, archive", `ar:yes:${session.id}`).text("\u274C Cancel", `ar:no:${session.id}`)
|
|
4331
|
+
}
|
|
4332
|
+
);
|
|
4333
|
+
}
|
|
4334
|
+
async function handleArchiveConfirm(ctx, core, chatId) {
|
|
4335
|
+
const data = ctx.callbackQuery?.data;
|
|
4336
|
+
if (!data) return;
|
|
4337
|
+
try {
|
|
4338
|
+
await ctx.answerCallbackQuery();
|
|
4339
|
+
} catch {
|
|
4340
|
+
}
|
|
4341
|
+
const [, action, sessionId] = data.split(":");
|
|
4342
|
+
if (action === "no") {
|
|
4343
|
+
await ctx.editMessageText("Archive cancelled.", { parse_mode: "HTML" });
|
|
4344
|
+
return;
|
|
4345
|
+
}
|
|
4346
|
+
await ctx.editMessageText("\u{1F504} Archiving topic...", { parse_mode: "HTML" });
|
|
4347
|
+
const result = await core.archiveSession(sessionId);
|
|
4348
|
+
if (result.ok) {
|
|
4349
|
+
const newTopicId = Number(result.newThreadId);
|
|
4350
|
+
await ctx.api.sendMessage(chatId, "\u2705 Topic archived. Session continues.", {
|
|
4351
|
+
message_thread_id: newTopicId,
|
|
4352
|
+
parse_mode: "HTML"
|
|
4353
|
+
});
|
|
4354
|
+
} else {
|
|
4355
|
+
try {
|
|
4356
|
+
await ctx.editMessageText(`\u274C Failed to archive: <code>${escapeHtml(result.error)}</code>`, { parse_mode: "HTML" });
|
|
4357
|
+
} catch {
|
|
4358
|
+
core.notificationManager.notifyAll({
|
|
4359
|
+
sessionId,
|
|
4360
|
+
type: "error",
|
|
4361
|
+
summary: `Failed to recreate topic for session "${sessionId}": ${result.error}`
|
|
4362
|
+
});
|
|
4363
|
+
}
|
|
4364
|
+
}
|
|
4365
|
+
}
|
|
3766
4366
|
|
|
3767
4367
|
// src/adapters/telegram/commands/agents.ts
|
|
3768
4368
|
import { InlineKeyboard as InlineKeyboard4 } from "grammy";
|
|
@@ -4153,7 +4753,7 @@ ${resultText}`,
|
|
|
4153
4753
|
|
|
4154
4754
|
// src/adapters/telegram/commands/settings.ts
|
|
4155
4755
|
import { InlineKeyboard as InlineKeyboard6 } from "grammy";
|
|
4156
|
-
var
|
|
4756
|
+
var log13 = createChildLogger({ module: "telegram-settings" });
|
|
4157
4757
|
function buildSettingsKeyboard(core) {
|
|
4158
4758
|
const config = core.configManager.get();
|
|
4159
4759
|
const fields = getSafeFields();
|
|
@@ -4179,13 +4779,15 @@ function formatFieldLabel(field, value) {
|
|
|
4179
4779
|
tunnel: "\u{1F517}",
|
|
4180
4780
|
security: "\u{1F512}",
|
|
4181
4781
|
workspace: "\u{1F4C1}",
|
|
4182
|
-
storage: "\u{1F4BE}"
|
|
4782
|
+
storage: "\u{1F4BE}",
|
|
4783
|
+
speech: "\u{1F3A4}"
|
|
4183
4784
|
};
|
|
4184
4785
|
const icon = icons[field.group] ?? "\u2699\uFE0F";
|
|
4185
4786
|
if (field.type === "toggle") {
|
|
4186
4787
|
return `${icon} ${field.displayName}: ${value ? "ON" : "OFF"}`;
|
|
4187
4788
|
}
|
|
4188
|
-
|
|
4789
|
+
const displayValue = value === null || value === void 0 ? "Not set" : String(value);
|
|
4790
|
+
return `${icon} ${field.displayName}: ${displayValue}`;
|
|
4189
4791
|
}
|
|
4190
4792
|
async function handleSettings(ctx, core) {
|
|
4191
4793
|
const kb = buildSettingsKeyboard(core);
|
|
@@ -4214,7 +4816,7 @@ function setupSettingsCallbacks(bot, core, getAssistantSession) {
|
|
|
4214
4816
|
} catch {
|
|
4215
4817
|
}
|
|
4216
4818
|
} catch (err) {
|
|
4217
|
-
|
|
4819
|
+
log13.error({ err, fieldPath }, "Failed to toggle config");
|
|
4218
4820
|
try {
|
|
4219
4821
|
await ctx.answerCallbackQuery({ text: "\u274C Failed to update" });
|
|
4220
4822
|
} catch {
|
|
@@ -4252,6 +4854,27 @@ Select a value:`, {
|
|
|
4252
4854
|
const fieldPath = parts.slice(0, -1).join(":");
|
|
4253
4855
|
const newValue = parts[parts.length - 1];
|
|
4254
4856
|
try {
|
|
4857
|
+
if (fieldPath === "speech.stt.provider") {
|
|
4858
|
+
const config = core.configManager.get();
|
|
4859
|
+
const providerConfig = config.speech?.stt?.providers?.[newValue];
|
|
4860
|
+
if (!providerConfig?.apiKey) {
|
|
4861
|
+
const assistant = getAssistantSession();
|
|
4862
|
+
if (assistant) {
|
|
4863
|
+
try {
|
|
4864
|
+
await ctx.answerCallbackQuery({ text: `\u{1F511} API key needed \u2014 check Assistant topic` });
|
|
4865
|
+
} catch {
|
|
4866
|
+
}
|
|
4867
|
+
const prompt = `User wants to enable ${newValue} as Speech-to-Text provider, but no API key is configured yet. Guide them to get a ${newValue} API key and set it up. After they provide the key, run both commands: \`openacp config set speech.stt.providers.${newValue}.apiKey <key>\` and \`openacp config set speech.stt.provider ${newValue}\``;
|
|
4868
|
+
await assistant.enqueuePrompt(prompt);
|
|
4869
|
+
return;
|
|
4870
|
+
}
|
|
4871
|
+
try {
|
|
4872
|
+
await ctx.answerCallbackQuery({ text: `\u26A0\uFE0F Set API key first: openacp config set speech.stt.providers.${newValue}.apiKey <key>` });
|
|
4873
|
+
} catch {
|
|
4874
|
+
}
|
|
4875
|
+
return;
|
|
4876
|
+
}
|
|
4877
|
+
}
|
|
4255
4878
|
const updates = buildNestedUpdate(fieldPath, newValue);
|
|
4256
4879
|
await core.configManager.save(updates, fieldPath);
|
|
4257
4880
|
try {
|
|
@@ -4267,7 +4890,7 @@ Tap to change:`, {
|
|
|
4267
4890
|
} catch {
|
|
4268
4891
|
}
|
|
4269
4892
|
} catch (err) {
|
|
4270
|
-
|
|
4893
|
+
log13.error({ err, fieldPath }, "Failed to set config");
|
|
4271
4894
|
try {
|
|
4272
4895
|
await ctx.answerCallbackQuery({ text: "\u274C Failed to update" });
|
|
4273
4896
|
} catch {
|
|
@@ -4300,7 +4923,7 @@ Tap to change:`, {
|
|
|
4300
4923
|
await ctx.answerCallbackQuery();
|
|
4301
4924
|
} catch {
|
|
4302
4925
|
}
|
|
4303
|
-
const { buildMenuKeyboard: buildMenuKeyboard3 } = await import("./menu-
|
|
4926
|
+
const { buildMenuKeyboard: buildMenuKeyboard3 } = await import("./menu-XR2GET2B.js");
|
|
4304
4927
|
try {
|
|
4305
4928
|
await ctx.editMessageText(`<b>OpenACP Menu</b>
|
|
4306
4929
|
Choose an action:`, {
|
|
@@ -4339,7 +4962,7 @@ function buildNestedUpdate(dotPath, value) {
|
|
|
4339
4962
|
|
|
4340
4963
|
// src/adapters/telegram/commands/doctor.ts
|
|
4341
4964
|
import { InlineKeyboard as InlineKeyboard7 } from "grammy";
|
|
4342
|
-
var
|
|
4965
|
+
var log14 = createChildLogger({ module: "telegram-cmd-doctor" });
|
|
4343
4966
|
var pendingFixesStore = /* @__PURE__ */ new Map();
|
|
4344
4967
|
function renderReport(report) {
|
|
4345
4968
|
const icons = { pass: "\u2705", warn: "\u26A0\uFE0F", fail: "\u274C" };
|
|
@@ -4382,7 +5005,7 @@ async function handleDoctor(ctx) {
|
|
|
4382
5005
|
reply_markup: keyboard
|
|
4383
5006
|
});
|
|
4384
5007
|
} catch (err) {
|
|
4385
|
-
|
|
5008
|
+
log14.error({ err }, "Doctor command failed");
|
|
4386
5009
|
await ctx.api.editMessageText(
|
|
4387
5010
|
ctx.chat.id,
|
|
4388
5011
|
statusMsg.message_id,
|
|
@@ -4431,7 +5054,7 @@ function setupDoctorCallbacks(bot) {
|
|
|
4431
5054
|
}
|
|
4432
5055
|
}
|
|
4433
5056
|
} catch (err) {
|
|
4434
|
-
|
|
5057
|
+
log14.error({ err, index }, "Doctor fix callback failed");
|
|
4435
5058
|
}
|
|
4436
5059
|
});
|
|
4437
5060
|
bot.callbackQuery("m:doctor", async (ctx) => {
|
|
@@ -4445,7 +5068,7 @@ function setupDoctorCallbacks(bot) {
|
|
|
4445
5068
|
|
|
4446
5069
|
// src/adapters/telegram/commands/tunnel.ts
|
|
4447
5070
|
import { InlineKeyboard as InlineKeyboard8 } from "grammy";
|
|
4448
|
-
var
|
|
5071
|
+
var log15 = createChildLogger({ module: "telegram-cmd-tunnel" });
|
|
4449
5072
|
async function handleTunnel(ctx, core) {
|
|
4450
5073
|
if (!core.tunnelService) {
|
|
4451
5074
|
await ctx.reply("\u274C Tunnel service is not enabled.", { parse_mode: "HTML" });
|
|
@@ -4510,12 +5133,19 @@ async function handleTunnels(ctx, core) {
|
|
|
4510
5133
|
await ctx.reply("\u274C Tunnel service is not enabled.", { parse_mode: "HTML" });
|
|
4511
5134
|
return;
|
|
4512
5135
|
}
|
|
4513
|
-
const
|
|
5136
|
+
const threadId = ctx.message?.message_thread_id;
|
|
5137
|
+
let entries = core.tunnelService.listTunnels();
|
|
5138
|
+
let sessionScoped = false;
|
|
5139
|
+
if (threadId) {
|
|
5140
|
+
const session = core.sessionManager.getSessionByThread("telegram", String(threadId));
|
|
5141
|
+
if (session) {
|
|
5142
|
+
entries = entries.filter((e) => e.sessionId === session.id);
|
|
5143
|
+
sessionScoped = true;
|
|
5144
|
+
}
|
|
5145
|
+
}
|
|
4514
5146
|
if (entries.length === 0) {
|
|
4515
|
-
|
|
4516
|
-
|
|
4517
|
-
{ parse_mode: "HTML" }
|
|
4518
|
-
);
|
|
5147
|
+
const hint = sessionScoped ? "No tunnels for this session.\n\nUse <code>/tunnel <port></code> to create one." : "No active tunnels.\n\nUse <code>/tunnel <port></code> to create one.";
|
|
5148
|
+
await ctx.reply(hint, { parse_mode: "HTML" });
|
|
4519
5149
|
return;
|
|
4520
5150
|
}
|
|
4521
5151
|
const lines = entries.map((e) => {
|
|
@@ -4610,8 +5240,10 @@ function setupCommands(bot, core, chatId, assistant) {
|
|
|
4610
5240
|
bot.command("integrate", (ctx) => handleIntegrate(ctx, core));
|
|
4611
5241
|
bot.command("clear", (ctx) => handleClear(ctx, assistant));
|
|
4612
5242
|
bot.command("doctor", (ctx) => handleDoctor(ctx));
|
|
5243
|
+
bot.command("usage", (ctx) => handleUsage(ctx, core));
|
|
4613
5244
|
bot.command("tunnel", (ctx) => handleTunnel(ctx, core));
|
|
4614
5245
|
bot.command("tunnels", (ctx) => handleTunnels(ctx, core));
|
|
5246
|
+
bot.command("archive", (ctx) => handleArchive(ctx, core));
|
|
4615
5247
|
}
|
|
4616
5248
|
function setupAllCallbacks(bot, core, chatId, systemTopicIds, getAssistantSession) {
|
|
4617
5249
|
setupNewSessionCallbacks(bot, core, chatId);
|
|
@@ -4625,6 +5257,7 @@ function setupAllCallbacks(bot, core, chatId, systemTopicIds, getAssistantSessio
|
|
|
4625
5257
|
await ctx.answerCallbackQuery();
|
|
4626
5258
|
await createSessionDirect(ctx, core, chatId, agentKey, core.configManager.get().workspace.baseDir);
|
|
4627
5259
|
});
|
|
5260
|
+
bot.callbackQuery(/^ar:/, (ctx) => handleArchiveConfirm(ctx, core, chatId));
|
|
4628
5261
|
bot.callbackQuery(/^m:/, async (ctx) => {
|
|
4629
5262
|
const data = ctx.callbackQuery.data;
|
|
4630
5263
|
try {
|
|
@@ -4680,14 +5313,16 @@ var STATIC_COMMANDS = [
|
|
|
4680
5313
|
{ command: "restart", description: "Restart OpenACP" },
|
|
4681
5314
|
{ command: "update", description: "Update to latest version and restart" },
|
|
4682
5315
|
{ command: "doctor", description: "Run system diagnostics" },
|
|
5316
|
+
{ command: "usage", description: "View token usage and cost report" },
|
|
4683
5317
|
{ command: "tunnel", description: "Create/stop tunnel for a local port" },
|
|
4684
|
-
{ command: "tunnels", description: "List active tunnels" }
|
|
5318
|
+
{ command: "tunnels", description: "List active tunnels" },
|
|
5319
|
+
{ command: "archive", description: "Archive session topic (recreate with clean history)" }
|
|
4685
5320
|
];
|
|
4686
5321
|
|
|
4687
5322
|
// src/adapters/telegram/permissions.ts
|
|
4688
5323
|
import { InlineKeyboard as InlineKeyboard9 } from "grammy";
|
|
4689
|
-
import { nanoid as
|
|
4690
|
-
var
|
|
5324
|
+
import { nanoid as nanoid3 } from "nanoid";
|
|
5325
|
+
var log16 = createChildLogger({ module: "telegram-permissions" });
|
|
4691
5326
|
var PermissionHandler = class {
|
|
4692
5327
|
constructor(bot, chatId, getSession, sendNotification) {
|
|
4693
5328
|
this.bot = bot;
|
|
@@ -4698,7 +5333,7 @@ var PermissionHandler = class {
|
|
|
4698
5333
|
pending = /* @__PURE__ */ new Map();
|
|
4699
5334
|
async sendPermissionRequest(session, request) {
|
|
4700
5335
|
const threadId = Number(session.threadId);
|
|
4701
|
-
const callbackKey =
|
|
5336
|
+
const callbackKey = nanoid3(8);
|
|
4702
5337
|
this.pending.set(callbackKey, {
|
|
4703
5338
|
sessionId: session.id,
|
|
4704
5339
|
requestId: request.id,
|
|
@@ -4747,7 +5382,7 @@ ${escapeHtml(request.description)}`,
|
|
|
4747
5382
|
}
|
|
4748
5383
|
const session = this.getSession(pending.sessionId);
|
|
4749
5384
|
const isAllow = pending.options.find((o) => o.id === optionId)?.isAllow ?? false;
|
|
4750
|
-
|
|
5385
|
+
log16.info({ requestId: pending.requestId, optionId, isAllow }, "Permission responded");
|
|
4751
5386
|
if (session?.permissionGate.requestId === pending.requestId) {
|
|
4752
5387
|
session.permissionGate.resolve(optionId);
|
|
4753
5388
|
}
|
|
@@ -4764,425 +5399,11 @@ ${escapeHtml(request.description)}`,
|
|
|
4764
5399
|
}
|
|
4765
5400
|
};
|
|
4766
5401
|
|
|
4767
|
-
// src/product-guide.ts
|
|
4768
|
-
var PRODUCT_GUIDE = `
|
|
4769
|
-
# OpenACP \u2014 Product Guide
|
|
4770
|
-
|
|
4771
|
-
OpenACP lets you chat with AI coding agents (like Claude Code) through Telegram.
|
|
4772
|
-
You type messages in Telegram, the agent reads/writes/runs code in your project folder, and results stream back in real time.
|
|
4773
|
-
|
|
4774
|
-
---
|
|
4775
|
-
|
|
4776
|
-
## Quick Start
|
|
4777
|
-
|
|
4778
|
-
1. Start OpenACP: \`openacp\` (or \`openacp start\` for background daemon)
|
|
4779
|
-
2. Open your Telegram group \u2014 you'll see the Assistant topic
|
|
4780
|
-
3. Tap \u{1F195} New Session or type /new
|
|
4781
|
-
4. Pick an agent and a project folder
|
|
4782
|
-
5. Chat in the session topic \u2014 the agent works on your code
|
|
4783
|
-
|
|
4784
|
-
---
|
|
4785
|
-
|
|
4786
|
-
## Core Concepts
|
|
4787
|
-
|
|
4788
|
-
### Sessions
|
|
4789
|
-
A session = one conversation with one AI agent working in one project folder.
|
|
4790
|
-
Each session gets its own Telegram topic. Chat there to give instructions to the agent.
|
|
4791
|
-
|
|
4792
|
-
### Agents
|
|
4793
|
-
An agent is an AI coding tool (e.g., Claude Code, Gemini, Cursor, Codex, etc.).
|
|
4794
|
-
OpenACP supports 28+ agents from the official ACP Registry (agentclientprotocol.com).
|
|
4795
|
-
You can install multiple agents and choose which one to use per session.
|
|
4796
|
-
The default agent is used when you don't specify one.
|
|
4797
|
-
|
|
4798
|
-
### Agent Management
|
|
4799
|
-
- Browse agents: \`/agents\` in Telegram or \`openacp agents\` in CLI
|
|
4800
|
-
- Install: tap the install button in /agents, or \`openacp agents install <name>\`
|
|
4801
|
-
- Uninstall: \`openacp agents uninstall <name>\`
|
|
4802
|
-
- Setup/login: \`openacp agents run <name> -- <args>\` (e.g., \`openacp agents run gemini -- auth login\`)
|
|
4803
|
-
- Details: \`openacp agents info <name>\` shows version, dependencies, and setup steps
|
|
4804
|
-
|
|
4805
|
-
Some agents need additional setup before they can be used:
|
|
4806
|
-
- Claude: requires \`claude login\`
|
|
4807
|
-
- Gemini: requires \`openacp agents run gemini -- auth login\`
|
|
4808
|
-
- Codex: requires setting \`OPENAI_API_KEY\` environment variable
|
|
4809
|
-
- GitHub Copilot: requires \`openacp agents run copilot -- auth login\`
|
|
4810
|
-
|
|
4811
|
-
Agents are installed in three ways depending on the agent:
|
|
4812
|
-
- **npx** \u2014 Node.js agents, downloaded automatically on first use
|
|
4813
|
-
- **uvx** \u2014 Python agents, downloaded automatically on first use
|
|
4814
|
-
- **binary** \u2014 Platform-specific binaries, downloaded to \`~/.openacp/agents/\`
|
|
4815
|
-
|
|
4816
|
-
### Project Folder (Workspace)
|
|
4817
|
-
The directory where the agent reads, writes, and runs code.
|
|
4818
|
-
When creating a session, you choose which folder the agent works in.
|
|
4819
|
-
You can type a full path like \`~/code/my-project\` or just a name like \`my-project\` (it becomes \`<base-dir>/my-project\`).
|
|
4820
|
-
|
|
4821
|
-
### System Topics
|
|
4822
|
-
- **Assistant** \u2014 Always-on helper that can answer questions, create sessions, check status, troubleshoot
|
|
4823
|
-
- **Notifications** \u2014 System alerts (permission requests, session errors, completions)
|
|
4824
|
-
|
|
4825
|
-
---
|
|
4826
|
-
|
|
4827
|
-
## Creating Sessions
|
|
4828
|
-
|
|
4829
|
-
### From menu
|
|
4830
|
-
Tap \u{1F195} New Session \u2192 choose agent (if multiple) \u2192 choose project folder \u2192 confirm
|
|
4831
|
-
|
|
4832
|
-
### From command
|
|
4833
|
-
- \`/new\` \u2014 Interactive flow (asks agent + folder)
|
|
4834
|
-
- \`/new claude ~/code/my-project\` \u2014 Create directly with specific agent and folder
|
|
4835
|
-
|
|
4836
|
-
### From Assistant topic
|
|
4837
|
-
Just ask: "Create a session for my-project with claude" \u2014 the assistant handles it
|
|
4838
|
-
|
|
4839
|
-
### Quick new chat
|
|
4840
|
-
\`/newchat\` in a session topic \u2014 creates new session with same agent and folder as current one
|
|
4841
|
-
|
|
4842
|
-
---
|
|
4843
|
-
|
|
4844
|
-
## Working with Sessions
|
|
4845
|
-
|
|
4846
|
-
### Chat
|
|
4847
|
-
Type messages in the session topic. The agent responds with code changes, explanations, tool outputs.
|
|
4848
|
-
|
|
4849
|
-
### What you see while the agent works
|
|
4850
|
-
- **\u{1F4AD} Thinking indicator** \u2014 Shows when the agent is reasoning, with elapsed time
|
|
4851
|
-
- **Text responses** \u2014 Streamed in real time, updated every few seconds
|
|
4852
|
-
- **Tool calls** \u2014 When the agent runs commands or edits files, you see tool name, input, status, and output
|
|
4853
|
-
- **\u{1F4CB} Plan card** \u2014 Visual task progress with completed/in-progress/pending items and progress bar
|
|
4854
|
-
- **"View File" / "View Diff" buttons** \u2014 Opens in browser with Monaco editor (requires tunnel)
|
|
4855
|
-
|
|
4856
|
-
### Session lifecycle
|
|
4857
|
-
1. **Creating** \u2014 Topic created, agent spawning
|
|
4858
|
-
2. **Warming up** \u2014 Agent primes its cache (happens automatically, invisible to you)
|
|
4859
|
-
3. **Active** \u2014 Ready for your messages
|
|
4860
|
-
4. **Auto-naming** \u2014 After your first message, the session gets a descriptive name (agent summarizes in ~5 words). The topic title updates automatically.
|
|
4861
|
-
5. **Finished/Error** \u2014 Session completed or hit an error
|
|
4862
|
-
|
|
4863
|
-
### Agent skills
|
|
4864
|
-
Some agents provide slash commands (e.g., /compact, /review). Available skills are pinned in the session topic.
|
|
4865
|
-
|
|
4866
|
-
### Permission requests
|
|
4867
|
-
When the agent wants to run a command, it asks for permission.
|
|
4868
|
-
You see buttons: \u2705 Allow, \u274C Reject (and sometimes "Always Allow").
|
|
4869
|
-
A notification also appears in the Notifications topic with a link to the request.
|
|
4870
|
-
|
|
4871
|
-
### Dangerous mode
|
|
4872
|
-
Auto-approves ALL permission requests \u2014 the agent runs any command without asking.
|
|
4873
|
-
- Enable: \`/enable_dangerous\` or tap the \u2620\uFE0F button in the session
|
|
4874
|
-
- Disable: \`/disable_dangerous\` or tap the \u{1F510} button
|
|
4875
|
-
- \u26A0\uFE0F Use with caution \u2014 the agent can execute anything
|
|
4876
|
-
|
|
4877
|
-
### Session timeout
|
|
4878
|
-
Idle sessions are automatically cancelled after a configurable timeout (default: 60 minutes).
|
|
4879
|
-
Configure via \`security.sessionTimeoutMinutes\` in config.
|
|
4880
|
-
|
|
4881
|
-
---
|
|
4882
|
-
|
|
4883
|
-
## Session Transfer (Handoff)
|
|
4884
|
-
|
|
4885
|
-
### Telegram \u2192 Terminal
|
|
4886
|
-
1. Type \`/handoff\` in a session topic
|
|
4887
|
-
2. You get a command like \`claude --resume <SESSION_ID>\`
|
|
4888
|
-
3. Copy and run it in your terminal \u2014 the session continues there with full conversation history
|
|
4889
|
-
|
|
4890
|
-
### Terminal \u2192 Telegram
|
|
4891
|
-
1. First time: run \`openacp integrate claude\` to install the handoff skill (one-time setup)
|
|
4892
|
-
2. In Claude Code, use the /openacp:handoff slash command
|
|
4893
|
-
3. The session appears as a new topic in Telegram and you can continue chatting there
|
|
4894
|
-
|
|
4895
|
-
### How it works
|
|
4896
|
-
- The agent session ID is shared between platforms
|
|
4897
|
-
- Conversation history is preserved \u2014 pick up where you left off
|
|
4898
|
-
- The agent that supports resume (e.g., Claude with \`--resume\`) handles the actual transfer
|
|
4899
|
-
|
|
4900
|
-
---
|
|
4901
|
-
|
|
4902
|
-
## Managing Sessions
|
|
4903
|
-
|
|
4904
|
-
### Status
|
|
4905
|
-
- \`/status\` \u2014 Shows active sessions count and details
|
|
4906
|
-
- Ask the Assistant: "What sessions are running?"
|
|
4907
|
-
|
|
4908
|
-
### List all sessions
|
|
4909
|
-
- \`/sessions\` \u2014 Shows all sessions with status (active, finished, error)
|
|
4910
|
-
|
|
4911
|
-
### Cancel
|
|
4912
|
-
- \`/cancel\` in a session topic \u2014 cancels that session
|
|
4913
|
-
- Ask the Assistant: "Cancel the stuck session"
|
|
4914
|
-
|
|
4915
|
-
### Cleanup
|
|
4916
|
-
- From \`/sessions\` \u2192 tap cleanup buttons (finished, errors, all)
|
|
4917
|
-
- Ask the Assistant: "Clean up old sessions"
|
|
4918
|
-
|
|
4919
|
-
---
|
|
4920
|
-
|
|
4921
|
-
## Assistant Topic
|
|
4922
|
-
|
|
4923
|
-
The Assistant is an always-on AI helper in its own topic. It can:
|
|
4924
|
-
- Answer questions about OpenACP
|
|
4925
|
-
- Create sessions for you
|
|
4926
|
-
- Check status and health
|
|
4927
|
-
- Cancel sessions
|
|
4928
|
-
- Clean up old sessions
|
|
4929
|
-
- Troubleshoot issues
|
|
4930
|
-
- Manage configuration
|
|
4931
|
-
|
|
4932
|
-
Just chat naturally: "How do I create a session?", "What's the status?", "Something is stuck"
|
|
4933
|
-
|
|
4934
|
-
### Clear history
|
|
4935
|
-
\`/clear\` in the Assistant topic \u2014 resets the conversation
|
|
4936
|
-
|
|
4937
|
-
---
|
|
4938
|
-
|
|
4939
|
-
## System Commands
|
|
4940
|
-
|
|
4941
|
-
| Command | Where | What it does |
|
|
4942
|
-
|---------|-------|-------------|
|
|
4943
|
-
| \`/new [agent] [path]\` | Anywhere | Create new session |
|
|
4944
|
-
| \`/newchat\` | Session topic | New session, same agent + folder |
|
|
4945
|
-
| \`/cancel\` | Session topic | Cancel current session |
|
|
4946
|
-
| \`/status\` | Anywhere | Show status |
|
|
4947
|
-
| \`/sessions\` | Anywhere | List all sessions |
|
|
4948
|
-
| \`/agents\` | Anywhere | Browse & install agents from ACP Registry |
|
|
4949
|
-
| \`/install <name>\` | Anywhere | Install an agent |
|
|
4950
|
-
| \`/enable_dangerous\` | Session topic | Auto-approve all permissions |
|
|
4951
|
-
| \`/disable_dangerous\` | Session topic | Restore permission prompts |
|
|
4952
|
-
| \`/handoff\` | Session topic | Transfer session to terminal |
|
|
4953
|
-
| \`/clear\` | Assistant topic | Clear assistant history |
|
|
4954
|
-
| \`/menu\` | Anywhere | Show action menu |
|
|
4955
|
-
| \`/help\` | Anywhere | Show help |
|
|
4956
|
-
| \`/restart\` | Anywhere | Restart OpenACP |
|
|
4957
|
-
| \`/update\` | Anywhere | Update to latest version |
|
|
4958
|
-
| \`/integrate\` | Anywhere | Manage agent integrations |
|
|
4959
|
-
|
|
4960
|
-
---
|
|
4961
|
-
|
|
4962
|
-
## Menu Buttons
|
|
4963
|
-
|
|
4964
|
-
| Button | Action |
|
|
4965
|
-
|--------|--------|
|
|
4966
|
-
| \u{1F195} New Session | Create new session (interactive) |
|
|
4967
|
-
| \u{1F4CB} Sessions | List all sessions with cleanup options |
|
|
4968
|
-
| \u{1F4CA} Status | Show active/total session count |
|
|
4969
|
-
| \u{1F916} Agents | List available agents |
|
|
4970
|
-
| \u{1F517} Integrate | Manage agent integrations |
|
|
4971
|
-
| \u2753 Help | Show help text |
|
|
4972
|
-
| \u{1F504} Restart | Restart OpenACP |
|
|
4973
|
-
| \u2B06\uFE0F Update | Check and install updates |
|
|
4974
|
-
|
|
4975
|
-
---
|
|
4976
|
-
|
|
4977
|
-
## CLI Commands
|
|
4978
|
-
|
|
4979
|
-
### Server
|
|
4980
|
-
- \`openacp\` \u2014 Start (uses configured mode: foreground or daemon)
|
|
4981
|
-
- \`openacp start\` \u2014 Start as background daemon
|
|
4982
|
-
- \`openacp stop\` \u2014 Stop daemon
|
|
4983
|
-
- \`openacp status\` \u2014 Show daemon status
|
|
4984
|
-
- \`openacp logs\` \u2014 Tail daemon logs
|
|
4985
|
-
- \`openacp --foreground\` \u2014 Force foreground mode (useful for debugging or containers)
|
|
4986
|
-
|
|
4987
|
-
### Auto-start (run on boot)
|
|
4988
|
-
- macOS: installs a LaunchAgent in \`~/Library/LaunchAgents/\`
|
|
4989
|
-
- Linux: installs a systemd user service in \`~/.config/systemd/user/\`
|
|
4990
|
-
- Enabled automatically when you start the daemon. Remove with \`openacp stop\`.
|
|
4991
|
-
|
|
4992
|
-
### Configuration
|
|
4993
|
-
- \`openacp config\` \u2014 Interactive config editor
|
|
4994
|
-
- \`openacp reset\` \u2014 Delete all data and start fresh
|
|
4995
|
-
|
|
4996
|
-
### Agent Management (CLI)
|
|
4997
|
-
- \`openacp agents\` \u2014 List all agents (installed + available from ACP Registry)
|
|
4998
|
-
- \`openacp agents install <name>\` \u2014 Install an agent
|
|
4999
|
-
- \`openacp agents uninstall <name>\` \u2014 Remove an agent
|
|
5000
|
-
- \`openacp agents info <name>\` \u2014 Show details, dependencies, and setup guide
|
|
5001
|
-
- \`openacp agents run <name> [-- args]\` \u2014 Run agent CLI directly (for login, config, etc.)
|
|
5002
|
-
- \`openacp agents refresh\` \u2014 Force-refresh registry cache
|
|
5003
|
-
|
|
5004
|
-
### Plugins
|
|
5005
|
-
- \`openacp install <package>\` \u2014 Install adapter plugin (e.g., \`@openacp/adapter-discord\`)
|
|
5006
|
-
- \`openacp uninstall <package>\` \u2014 Remove adapter plugin
|
|
5007
|
-
- \`openacp plugins\` \u2014 List installed plugins
|
|
5008
|
-
|
|
5009
|
-
### Integration
|
|
5010
|
-
- \`openacp integrate <agent>\` \u2014 Install agent integration (e.g., Claude handoff skill)
|
|
5011
|
-
- \`openacp integrate <agent> --uninstall\` \u2014 Remove integration
|
|
5012
|
-
|
|
5013
|
-
### API (requires running daemon)
|
|
5014
|
-
\`openacp api <command>\` \u2014 Interact with running daemon:
|
|
5015
|
-
|
|
5016
|
-
| Command | Description |
|
|
5017
|
-
|---------|-------------|
|
|
5018
|
-
| \`status\` | List active sessions |
|
|
5019
|
-
| \`session <id>\` | Session details |
|
|
5020
|
-
| \`new <agent> <path>\` | Create session |
|
|
5021
|
-
| \`send <id> "text"\` | Send prompt |
|
|
5022
|
-
| \`cancel <id>\` | Cancel session |
|
|
5023
|
-
| \`dangerous <id> on/off\` | Toggle dangerous mode |
|
|
5024
|
-
| \`topics [--status x,y]\` | List topics |
|
|
5025
|
-
| \`delete-topic <id> [--force]\` | Delete topic |
|
|
5026
|
-
| \`cleanup [--status x,y]\` | Cleanup old topics |
|
|
5027
|
-
| \`agents\` | List agents |
|
|
5028
|
-
| \`health\` | System health |
|
|
5029
|
-
| \`config\` | Show config |
|
|
5030
|
-
| \`config set <key> <value>\` | Update config |
|
|
5031
|
-
| \`adapters\` | List adapters |
|
|
5032
|
-
| \`tunnel\` | Tunnel status |
|
|
5033
|
-
| \`notify "message"\` | Send notification |
|
|
5034
|
-
| \`version\` | Daemon version |
|
|
5035
|
-
| \`restart\` | Restart daemon |
|
|
5036
|
-
|
|
5037
|
-
---
|
|
5038
|
-
|
|
5039
|
-
## File Viewer (Tunnel)
|
|
5040
|
-
|
|
5041
|
-
When tunnel is enabled, file edits and diffs get "View" buttons that open in your browser:
|
|
5042
|
-
- **Monaco Editor** \u2014 Full VS Code editor with syntax highlighting
|
|
5043
|
-
- **Diff viewer** \u2014 Side-by-side or inline comparison
|
|
5044
|
-
- **Line highlighting** \u2014 Click lines to highlight
|
|
5045
|
-
- Dark/light theme toggle
|
|
5046
|
-
|
|
5047
|
-
### Setup
|
|
5048
|
-
Enable in config: set \`tunnel.enabled\` to \`true\`.
|
|
5049
|
-
Providers: Cloudflare (default, free), ngrok, bore, Tailscale Funnel.
|
|
5050
|
-
|
|
5051
|
-
### Port Tunneling
|
|
5052
|
-
|
|
5053
|
-
Expose any local port (dev servers, APIs, etc.) to the internet:
|
|
5054
|
-
|
|
5055
|
-
**CLI commands** (agent can call these directly):
|
|
5056
|
-
- \`openacp tunnel add <port> --label <name>\` \u2014 Create tunnel to a local port
|
|
5057
|
-
- \`openacp tunnel list\` \u2014 List active tunnels
|
|
5058
|
-
- \`openacp tunnel stop <port>\` \u2014 Stop a tunnel
|
|
5059
|
-
- \`openacp tunnel stop-all\` \u2014 Stop all user tunnels
|
|
5060
|
-
|
|
5061
|
-
**Telegram commands**:
|
|
5062
|
-
- \`/tunnel <port> [label]\` \u2014 Create tunnel
|
|
5063
|
-
- \`/tunnels\` \u2014 List active tunnels
|
|
5064
|
-
- \`/tunnel stop <port>\` \u2014 Stop tunnel
|
|
5065
|
-
|
|
5066
|
-
Example: after starting a dev server on port 3000, run \`openacp tunnel add 3000 --label my-app\` to get a public URL.
|
|
5067
|
-
|
|
5068
|
-
---
|
|
5069
|
-
|
|
5070
|
-
## Configuration
|
|
5071
|
-
|
|
5072
|
-
Config file: \`~/.openacp/config.json\`
|
|
5073
|
-
|
|
5074
|
-
### Telegram
|
|
5075
|
-
- **telegram.botToken** \u2014 Your Telegram bot token
|
|
5076
|
-
- **telegram.chatId** \u2014 Your Telegram supergroup ID
|
|
5077
|
-
|
|
5078
|
-
### Agents
|
|
5079
|
-
- **defaultAgent** \u2014 Which agent to use by default
|
|
5080
|
-
- Agents are managed via \`/agents\` (Telegram) or \`openacp agents\` (CLI)
|
|
5081
|
-
- Installed agents are stored in \`~/.openacp/agents.json\`
|
|
5082
|
-
- Agent list is fetched from the ACP Registry CDN and cached locally (24h)
|
|
5083
|
-
|
|
5084
|
-
### Workspace
|
|
5085
|
-
- **workspace.baseDir** \u2014 Base directory for project folders (default: \`~/openacp-workspace\`)
|
|
5086
|
-
|
|
5087
|
-
### Security
|
|
5088
|
-
- **security.allowedUserIds** \u2014 Restrict who can use the bot (empty = everyone)
|
|
5089
|
-
- **security.maxConcurrentSessions** \u2014 Max parallel sessions (default: 5)
|
|
5090
|
-
- **security.sessionTimeoutMinutes** \u2014 Auto-cancel idle sessions (default: 60)
|
|
5091
|
-
|
|
5092
|
-
### Tunnel / File Viewer
|
|
5093
|
-
- **tunnel.enabled** \u2014 Enable file viewer tunnel
|
|
5094
|
-
- **tunnel.provider** \u2014 Tunnel provider: cloudflare (default, free), ngrok, bore, tailscale
|
|
5095
|
-
- **tunnel.port** \u2014 Local port for tunnel server (default: 3100)
|
|
5096
|
-
- **tunnel.auth.enabled** \u2014 Enable authentication for tunnel URLs
|
|
5097
|
-
- **tunnel.auth.token** \u2014 Auth token for tunnel access
|
|
5098
|
-
- **tunnel.storeTtlMinutes** \u2014 How long viewer links stay cached (default: 60)
|
|
5099
|
-
|
|
5100
|
-
### Logging
|
|
5101
|
-
- **logging.level** \u2014 Log level: silent, debug, info, warn, error, fatal (default: info)
|
|
5102
|
-
- **logging.logDir** \u2014 Log directory (default: \`~/.openacp/logs\`)
|
|
5103
|
-
- **logging.maxFileSize** \u2014 Max log file size before rotation
|
|
5104
|
-
- **logging.maxFiles** \u2014 Max number of rotated log files
|
|
5105
|
-
- **logging.sessionLogRetentionDays** \u2014 Auto-delete old session logs (default: 30)
|
|
5106
|
-
|
|
5107
|
-
### Data Retention
|
|
5108
|
-
- **sessionStore.ttlDays** \u2014 How long session records persist (default: 30). Old records are cleaned up automatically.
|
|
5109
|
-
|
|
5110
|
-
### Environment variables
|
|
5111
|
-
Override config with env vars:
|
|
5112
|
-
- \`OPENACP_TELEGRAM_BOT_TOKEN\`
|
|
5113
|
-
- \`OPENACP_TELEGRAM_CHAT_ID\`
|
|
5114
|
-
- \`OPENACP_DEFAULT_AGENT\`
|
|
5115
|
-
- \`OPENACP_RUN_MODE\` \u2014 foreground or daemon
|
|
5116
|
-
- \`OPENACP_API_PORT\` \u2014 API server port (default: 21420)
|
|
5117
|
-
- \`OPENACP_TUNNEL_ENABLED\`
|
|
5118
|
-
- \`OPENACP_TUNNEL_PORT\`
|
|
5119
|
-
- \`OPENACP_TUNNEL_PROVIDER\`
|
|
5120
|
-
- \`OPENACP_LOG_LEVEL\`
|
|
5121
|
-
- \`OPENACP_LOG_DIR\`
|
|
5122
|
-
- \`OPENACP_DEBUG\` \u2014 Sets log level to debug
|
|
5123
|
-
|
|
5124
|
-
---
|
|
5125
|
-
|
|
5126
|
-
## Troubleshooting
|
|
5127
|
-
|
|
5128
|
-
### Session stuck / not responding
|
|
5129
|
-
- Check status: ask Assistant "Is anything stuck?"
|
|
5130
|
-
- Cancel and create new: \`/cancel\` then \`/new\`
|
|
5131
|
-
- Check system health: Assistant can run health check
|
|
5132
|
-
|
|
5133
|
-
### Agent not found
|
|
5134
|
-
- Check available agents: \`/agents\` or \`openacp agents\`
|
|
5135
|
-
- Install missing agent: \`openacp agents install <name>\`
|
|
5136
|
-
- Some agents need login first: \`openacp agents info <name>\` to see setup steps
|
|
5137
|
-
- Run agent CLI for setup: \`openacp agents run <name> -- <args>\`
|
|
5138
|
-
|
|
5139
|
-
### Permission request not showing
|
|
5140
|
-
- Check Notifications topic for the alert
|
|
5141
|
-
- Try \`/enable_dangerous\` to auto-approve (if you trust the agent)
|
|
5142
|
-
|
|
5143
|
-
### Session disappeared after restart
|
|
5144
|
-
- Sessions persist across restarts
|
|
5145
|
-
- Send a message in the old topic \u2014 it auto-resumes
|
|
5146
|
-
- If topic was deleted, the session record may still exist in status
|
|
5147
|
-
|
|
5148
|
-
### Bot not responding at all
|
|
5149
|
-
- Check daemon: \`openacp status\`
|
|
5150
|
-
- Check logs: \`openacp logs\`
|
|
5151
|
-
- Restart: \`openacp start\` or \`/restart\`
|
|
5152
|
-
|
|
5153
|
-
### Messages going to wrong topic
|
|
5154
|
-
- Each session is bound to a specific Telegram topic
|
|
5155
|
-
- If you see messages appearing in the Assistant topic instead of the session topic, try creating a new session
|
|
5156
|
-
|
|
5157
|
-
### Viewing logs
|
|
5158
|
-
- Session-specific logs: \`~/.openacp/logs/sessions/\`
|
|
5159
|
-
- System logs: \`openacp logs\` to tail live
|
|
5160
|
-
- Set \`OPENACP_DEBUG=true\` for verbose output
|
|
5161
|
-
|
|
5162
|
-
---
|
|
5163
|
-
|
|
5164
|
-
## Data & Storage
|
|
5165
|
-
|
|
5166
|
-
All data is stored in \`~/.openacp/\`:
|
|
5167
|
-
- \`config.json\` \u2014 Configuration
|
|
5168
|
-
- \`agents.json\` \u2014 Installed agents (managed by AgentCatalog)
|
|
5169
|
-
- \`registry-cache.json\` \u2014 Cached ACP Registry data (refreshes every 24h)
|
|
5170
|
-
- \`agents/\` \u2014 Downloaded binary agents
|
|
5171
|
-
- \`sessions/\` \u2014 Session records and state
|
|
5172
|
-
- \`topics/\` \u2014 Topic-to-session mappings
|
|
5173
|
-
- \`logs/\` \u2014 System and session logs
|
|
5174
|
-
- \`plugins/\` \u2014 Installed adapter plugins
|
|
5175
|
-
- \`openacp.pid\` \u2014 Daemon PID file
|
|
5176
|
-
|
|
5177
|
-
Session records auto-cleanup: 30 days (configurable via \`sessionStore.ttlDays\`).
|
|
5178
|
-
Session logs auto-cleanup: 30 days (configurable via \`logging.sessionLogRetentionDays\`).
|
|
5179
|
-
`;
|
|
5180
|
-
|
|
5181
5402
|
// src/adapters/telegram/assistant.ts
|
|
5182
|
-
var
|
|
5403
|
+
var log17 = createChildLogger({ module: "telegram-assistant" });
|
|
5183
5404
|
async function spawnAssistant(core, adapter, assistantTopicId) {
|
|
5184
5405
|
const config = core.configManager.get();
|
|
5185
|
-
|
|
5406
|
+
log17.info({ agent: config.defaultAgent }, "Creating assistant session...");
|
|
5186
5407
|
const session = await core.createSession({
|
|
5187
5408
|
channelId: "telegram",
|
|
5188
5409
|
agentName: config.defaultAgent,
|
|
@@ -5191,7 +5412,7 @@ async function spawnAssistant(core, adapter, assistantTopicId) {
|
|
|
5191
5412
|
// Prevent auto-naming from triggering after system prompt
|
|
5192
5413
|
});
|
|
5193
5414
|
session.threadId = String(assistantTopicId);
|
|
5194
|
-
|
|
5415
|
+
log17.info({ sessionId: session.id }, "Assistant agent spawned");
|
|
5195
5416
|
const allRecords = core.sessionManager.listRecords();
|
|
5196
5417
|
const activeCount = allRecords.filter((r) => r.status === "active" || r.status === "initializing").length;
|
|
5197
5418
|
const statusCounts = /* @__PURE__ */ new Map();
|
|
@@ -5212,9 +5433,9 @@ async function spawnAssistant(core, adapter, assistantTopicId) {
|
|
|
5212
5433
|
};
|
|
5213
5434
|
const systemPrompt = buildAssistantSystemPrompt(ctx);
|
|
5214
5435
|
const ready = session.enqueuePrompt(systemPrompt).then(() => {
|
|
5215
|
-
|
|
5436
|
+
log17.info({ sessionId: session.id }, "Assistant system prompt completed");
|
|
5216
5437
|
}).catch((err) => {
|
|
5217
|
-
|
|
5438
|
+
log17.warn({ err }, "Assistant system prompt failed");
|
|
5218
5439
|
});
|
|
5219
5440
|
return { session, ready };
|
|
5220
5441
|
}
|
|
@@ -5252,6 +5473,7 @@ function buildAssistantSystemPrompt(ctx) {
|
|
|
5252
5473
|
- Available in ACP Registry: ${availableAgentCount ?? "28+"} more agents (use /agents to browse)
|
|
5253
5474
|
- Default agent: ${config.defaultAgent}
|
|
5254
5475
|
- Workspace base directory: ${config.workspace.baseDir}
|
|
5476
|
+
- STT: ${config.speech?.stt?.provider ? `${config.speech.stt.provider} \u2705` : "Not configured"}
|
|
5255
5477
|
|
|
5256
5478
|
## Action Playbook
|
|
5257
5479
|
|
|
@@ -5299,6 +5521,16 @@ function buildAssistantSystemPrompt(ctx) {
|
|
|
5299
5521
|
- When user asks about "settings" or "config", use \`openacp config set\` directly
|
|
5300
5522
|
- When receiving a delegated request from the Settings menu, ask user for the new value, then apply with \`openacp config set <path> <value>\`
|
|
5301
5523
|
|
|
5524
|
+
### Voice / Speech-to-Text
|
|
5525
|
+
- OpenACP can transcribe voice messages to text using STT providers (Groq Whisper, OpenAI Whisper)
|
|
5526
|
+
- Current STT provider: ${config.speech?.stt?.provider ?? "Not configured"}
|
|
5527
|
+
- To enable: user needs an API key from the STT provider
|
|
5528
|
+
- Groq (recommended, free tier ~8h/day): Get key at console.groq.com \u2192 API Keys
|
|
5529
|
+
- Set via: \`openacp config set speech.stt.provider groq\` then \`openacp config set speech.stt.providers.groq.apiKey <key>\`
|
|
5530
|
+
- When STT is configured, voice messages are automatically transcribed before sending to agents that don't support audio
|
|
5531
|
+
- Agents with audio capability receive the audio directly (no transcription needed)
|
|
5532
|
+
- User can also configure via /settings \u2192 STT Provider
|
|
5533
|
+
|
|
5302
5534
|
### Restart / Update
|
|
5303
5535
|
- Always ask for confirmation \u2014 these are disruptive actions
|
|
5304
5536
|
- Guide user: "Tap \u{1F504} Restart button or type /restart"
|
|
@@ -5369,7 +5601,7 @@ function redirectToAssistant(chatId, assistantTopicId) {
|
|
|
5369
5601
|
}
|
|
5370
5602
|
|
|
5371
5603
|
// src/adapters/telegram/activity.ts
|
|
5372
|
-
var
|
|
5604
|
+
var log18 = createChildLogger({ module: "telegram:activity" });
|
|
5373
5605
|
var THINKING_REFRESH_MS = 15e3;
|
|
5374
5606
|
var THINKING_MAX_MS = 3 * 60 * 1e3;
|
|
5375
5607
|
var ThinkingIndicator = class {
|
|
@@ -5401,7 +5633,7 @@ var ThinkingIndicator = class {
|
|
|
5401
5633
|
this.startRefreshTimer();
|
|
5402
5634
|
}
|
|
5403
5635
|
} catch (err) {
|
|
5404
|
-
|
|
5636
|
+
log18.warn({ err }, "ThinkingIndicator.show() failed");
|
|
5405
5637
|
} finally {
|
|
5406
5638
|
this.sending = false;
|
|
5407
5639
|
}
|
|
@@ -5474,7 +5706,7 @@ var UsageMessage = class {
|
|
|
5474
5706
|
if (result) this.msgId = result.message_id;
|
|
5475
5707
|
}
|
|
5476
5708
|
} catch (err) {
|
|
5477
|
-
|
|
5709
|
+
log18.warn({ err }, "UsageMessage.send() failed");
|
|
5478
5710
|
}
|
|
5479
5711
|
}
|
|
5480
5712
|
getMsgId() {
|
|
@@ -5487,7 +5719,7 @@ var UsageMessage = class {
|
|
|
5487
5719
|
try {
|
|
5488
5720
|
await this.sendQueue.enqueue(() => this.api.deleteMessage(this.chatId, id));
|
|
5489
5721
|
} catch (err) {
|
|
5490
|
-
|
|
5722
|
+
log18.warn({ err }, "UsageMessage.delete() failed");
|
|
5491
5723
|
}
|
|
5492
5724
|
}
|
|
5493
5725
|
};
|
|
@@ -5573,7 +5805,7 @@ var PlanCard = class {
|
|
|
5573
5805
|
if (result) this.msgId = result.message_id;
|
|
5574
5806
|
}
|
|
5575
5807
|
} catch (err) {
|
|
5576
|
-
|
|
5808
|
+
log18.warn({ err }, "PlanCard flush failed");
|
|
5577
5809
|
}
|
|
5578
5810
|
}
|
|
5579
5811
|
};
|
|
@@ -5636,7 +5868,7 @@ var ActivityTracker = class {
|
|
|
5636
5868
|
})
|
|
5637
5869
|
);
|
|
5638
5870
|
} catch (err) {
|
|
5639
|
-
|
|
5871
|
+
log18.warn({ err }, "ActivityTracker.onComplete() Done send failed");
|
|
5640
5872
|
}
|
|
5641
5873
|
}
|
|
5642
5874
|
}
|
|
@@ -5718,7 +5950,7 @@ var TelegramSendQueue = class {
|
|
|
5718
5950
|
};
|
|
5719
5951
|
|
|
5720
5952
|
// src/adapters/telegram/action-detect.ts
|
|
5721
|
-
import { nanoid as
|
|
5953
|
+
import { nanoid as nanoid4 } from "nanoid";
|
|
5722
5954
|
import { InlineKeyboard as InlineKeyboard10 } from "grammy";
|
|
5723
5955
|
var CMD_NEW_RE = /\/new(?:\s+([^\s\u0080-\uFFFF]+)(?:\s+([^\s\u0080-\uFFFF]+))?)?/;
|
|
5724
5956
|
var CMD_CANCEL_RE = /\/cancel\b/;
|
|
@@ -5744,7 +5976,7 @@ function detectAction(text) {
|
|
|
5744
5976
|
var ACTION_TTL_MS = 5 * 60 * 1e3;
|
|
5745
5977
|
var actionMap = /* @__PURE__ */ new Map();
|
|
5746
5978
|
function storeAction(action) {
|
|
5747
|
-
const id =
|
|
5979
|
+
const id = nanoid4(10);
|
|
5748
5980
|
actionMap.set(id, { action, createdAt: Date.now() });
|
|
5749
5981
|
for (const [key, entry] of actionMap) {
|
|
5750
5982
|
if (Date.now() - entry.createdAt > ACTION_TTL_MS) {
|
|
@@ -5874,7 +6106,7 @@ function setupActionCallbacks(bot, core, chatId, getAssistantSessionId) {
|
|
|
5874
6106
|
}
|
|
5875
6107
|
|
|
5876
6108
|
// src/adapters/telegram/tool-call-tracker.ts
|
|
5877
|
-
var
|
|
6109
|
+
var log19 = createChildLogger({ module: "tool-call-tracker" });
|
|
5878
6110
|
var ToolCallTracker = class {
|
|
5879
6111
|
constructor(bot, chatId, sendQueue) {
|
|
5880
6112
|
this.bot = bot;
|
|
@@ -5918,7 +6150,7 @@ var ToolCallTracker = class {
|
|
|
5918
6150
|
if (!toolState) return;
|
|
5919
6151
|
if (meta.viewerLinks) {
|
|
5920
6152
|
toolState.viewerLinks = meta.viewerLinks;
|
|
5921
|
-
|
|
6153
|
+
log19.debug({ toolId: meta.id, viewerLinks: meta.viewerLinks }, "Accumulated viewerLinks");
|
|
5922
6154
|
}
|
|
5923
6155
|
if (meta.viewerFilePath) toolState.viewerFilePath = meta.viewerFilePath;
|
|
5924
6156
|
if (meta.name) toolState.name = meta.name;
|
|
@@ -5926,7 +6158,7 @@ var ToolCallTracker = class {
|
|
|
5926
6158
|
const isTerminal = meta.status === "completed" || meta.status === "failed";
|
|
5927
6159
|
if (!isTerminal) return;
|
|
5928
6160
|
await toolState.ready;
|
|
5929
|
-
|
|
6161
|
+
log19.debug(
|
|
5930
6162
|
{
|
|
5931
6163
|
toolId: meta.id,
|
|
5932
6164
|
status: meta.status,
|
|
@@ -5955,7 +6187,7 @@ var ToolCallTracker = class {
|
|
|
5955
6187
|
)
|
|
5956
6188
|
);
|
|
5957
6189
|
} catch (err) {
|
|
5958
|
-
|
|
6190
|
+
log19.warn(
|
|
5959
6191
|
{
|
|
5960
6192
|
err,
|
|
5961
6193
|
msgId: toolState.msgId,
|
|
@@ -6205,7 +6437,7 @@ var DraftManager = class {
|
|
|
6205
6437
|
};
|
|
6206
6438
|
|
|
6207
6439
|
// src/adapters/telegram/skill-command-manager.ts
|
|
6208
|
-
var
|
|
6440
|
+
var log20 = createChildLogger({ module: "skill-commands" });
|
|
6209
6441
|
var SkillCommandManager = class {
|
|
6210
6442
|
// sessionId → pinned msgId
|
|
6211
6443
|
constructor(bot, chatId, sendQueue, sessionManager) {
|
|
@@ -6271,7 +6503,7 @@ var SkillCommandManager = class {
|
|
|
6271
6503
|
disable_notification: true
|
|
6272
6504
|
});
|
|
6273
6505
|
} catch (err) {
|
|
6274
|
-
|
|
6506
|
+
log20.error({ err, sessionId }, "Failed to send skill commands");
|
|
6275
6507
|
}
|
|
6276
6508
|
}
|
|
6277
6509
|
async cleanup(sessionId) {
|
|
@@ -6297,7 +6529,7 @@ var SkillCommandManager = class {
|
|
|
6297
6529
|
};
|
|
6298
6530
|
|
|
6299
6531
|
// src/adapters/telegram/adapter.ts
|
|
6300
|
-
var
|
|
6532
|
+
var log21 = createChildLogger({ module: "telegram" });
|
|
6301
6533
|
function patchedFetch(input, init) {
|
|
6302
6534
|
if (init?.signal && !(init.signal instanceof AbortSignal)) {
|
|
6303
6535
|
const nativeController = new AbortController();
|
|
@@ -6356,7 +6588,7 @@ var TelegramAdapter = class extends ChannelAdapter {
|
|
|
6356
6588
|
);
|
|
6357
6589
|
this.bot.catch((err) => {
|
|
6358
6590
|
const rootCause = err.error instanceof Error ? err.error : err;
|
|
6359
|
-
|
|
6591
|
+
log21.error({ err: rootCause }, "Telegram bot error");
|
|
6360
6592
|
});
|
|
6361
6593
|
this.bot.api.config.use(async (prev, method, payload, signal) => {
|
|
6362
6594
|
const maxRetries = 3;
|
|
@@ -6370,7 +6602,7 @@ var TelegramAdapter = class extends ChannelAdapter {
|
|
|
6370
6602
|
if (rateLimitedMethods.includes(method)) {
|
|
6371
6603
|
this.sendQueue.onRateLimited();
|
|
6372
6604
|
}
|
|
6373
|
-
|
|
6605
|
+
log21.warn(
|
|
6374
6606
|
{ method, retryAfter, attempt: attempt + 1 },
|
|
6375
6607
|
"Rate limited by Telegram, retrying"
|
|
6376
6608
|
);
|
|
@@ -6502,7 +6734,7 @@ var TelegramAdapter = class extends ChannelAdapter {
|
|
|
6502
6734
|
this.setupRoutes();
|
|
6503
6735
|
this.bot.start({
|
|
6504
6736
|
allowed_updates: ["message", "callback_query"],
|
|
6505
|
-
onStart: () =>
|
|
6737
|
+
onStart: () => log21.info(
|
|
6506
6738
|
{ chatId: this.telegramConfig.chatId },
|
|
6507
6739
|
"Telegram bot started"
|
|
6508
6740
|
)
|
|
@@ -6524,10 +6756,10 @@ var TelegramAdapter = class extends ChannelAdapter {
|
|
|
6524
6756
|
reply_markup: buildMenuKeyboard()
|
|
6525
6757
|
});
|
|
6526
6758
|
} catch (err) {
|
|
6527
|
-
|
|
6759
|
+
log21.warn({ err }, "Failed to send welcome message");
|
|
6528
6760
|
}
|
|
6529
6761
|
try {
|
|
6530
|
-
|
|
6762
|
+
log21.info("Spawning assistant session...");
|
|
6531
6763
|
const { session, ready } = await spawnAssistant(
|
|
6532
6764
|
this.core,
|
|
6533
6765
|
this,
|
|
@@ -6535,13 +6767,13 @@ var TelegramAdapter = class extends ChannelAdapter {
|
|
|
6535
6767
|
);
|
|
6536
6768
|
this.assistantSession = session;
|
|
6537
6769
|
this.assistantInitializing = true;
|
|
6538
|
-
|
|
6770
|
+
log21.info({ sessionId: session.id }, "Assistant session ready, system prompt running in background");
|
|
6539
6771
|
ready.then(() => {
|
|
6540
6772
|
this.assistantInitializing = false;
|
|
6541
|
-
|
|
6773
|
+
log21.info({ sessionId: session.id }, "Assistant ready for user messages");
|
|
6542
6774
|
});
|
|
6543
6775
|
} catch (err) {
|
|
6544
|
-
|
|
6776
|
+
log21.error({ err }, "Failed to spawn assistant");
|
|
6545
6777
|
this.bot.api.sendMessage(
|
|
6546
6778
|
this.telegramConfig.chatId,
|
|
6547
6779
|
`\u26A0\uFE0F <b>Failed to start assistant session.</b>
|
|
@@ -6557,7 +6789,7 @@ var TelegramAdapter = class extends ChannelAdapter {
|
|
|
6557
6789
|
await this.assistantSession.destroy();
|
|
6558
6790
|
}
|
|
6559
6791
|
await this.bot.stop();
|
|
6560
|
-
|
|
6792
|
+
log21.info("Telegram bot stopped");
|
|
6561
6793
|
}
|
|
6562
6794
|
setupRoutes() {
|
|
6563
6795
|
this.bot.on("message:text", async (ctx) => {
|
|
@@ -6585,7 +6817,7 @@ var TelegramAdapter = class extends ChannelAdapter {
|
|
|
6585
6817
|
ctx.replyWithChatAction("typing").catch(() => {
|
|
6586
6818
|
});
|
|
6587
6819
|
handleAssistantMessage(this.assistantSession, forwardText).catch(
|
|
6588
|
-
(err) =>
|
|
6820
|
+
(err) => log21.error({ err }, "Assistant error")
|
|
6589
6821
|
);
|
|
6590
6822
|
return;
|
|
6591
6823
|
}
|
|
@@ -6602,7 +6834,7 @@ var TelegramAdapter = class extends ChannelAdapter {
|
|
|
6602
6834
|
threadId: String(threadId),
|
|
6603
6835
|
userId: String(ctx.from.id),
|
|
6604
6836
|
text: forwardText
|
|
6605
|
-
}).catch((err) =>
|
|
6837
|
+
}).catch((err) => log21.error({ err }, "handleMessage error"));
|
|
6606
6838
|
});
|
|
6607
6839
|
this.bot.on("message:photo", async (ctx) => {
|
|
6608
6840
|
const threadId = ctx.message.message_thread_id;
|
|
@@ -6677,9 +6909,10 @@ var TelegramAdapter = class extends ChannelAdapter {
|
|
|
6677
6909
|
if (this.assistantInitializing && sessionId === this.assistantSession?.id) return;
|
|
6678
6910
|
const session = this.core.sessionManager.getSession(sessionId);
|
|
6679
6911
|
if (!session) return;
|
|
6912
|
+
if (session.archiving) return;
|
|
6680
6913
|
const threadId = Number(session.threadId);
|
|
6681
6914
|
if (!threadId || isNaN(threadId)) {
|
|
6682
|
-
|
|
6915
|
+
log21.warn({ sessionId, threadId: session.threadId }, "Session has no valid threadId, skipping message");
|
|
6683
6916
|
return;
|
|
6684
6917
|
}
|
|
6685
6918
|
switch (content.type) {
|
|
@@ -6761,7 +6994,7 @@ Task completed.
|
|
|
6761
6994
|
if (!content.attachment) break;
|
|
6762
6995
|
const { attachment } = content;
|
|
6763
6996
|
if (attachment.size > 50 * 1024 * 1024) {
|
|
6764
|
-
|
|
6997
|
+
log21.warn({ sessionId, fileName: attachment.fileName, size: attachment.size }, "File too large for Telegram (>50MB)");
|
|
6765
6998
|
await this.sendQueue.enqueue(
|
|
6766
6999
|
() => this.bot.api.sendMessage(
|
|
6767
7000
|
this.telegramConfig.chatId,
|
|
@@ -6793,7 +7026,7 @@ Task completed.
|
|
|
6793
7026
|
);
|
|
6794
7027
|
}
|
|
6795
7028
|
} catch (err) {
|
|
6796
|
-
|
|
7029
|
+
log21.error({ err, sessionId, fileName: attachment.fileName }, "Failed to send attachment");
|
|
6797
7030
|
}
|
|
6798
7031
|
break;
|
|
6799
7032
|
}
|
|
@@ -6842,16 +7075,30 @@ Task completed.
|
|
|
6842
7075
|
);
|
|
6843
7076
|
break;
|
|
6844
7077
|
}
|
|
7078
|
+
case "system_message": {
|
|
7079
|
+
await this.sendQueue.enqueue(
|
|
7080
|
+
() => this.bot.api.sendMessage(
|
|
7081
|
+
this.telegramConfig.chatId,
|
|
7082
|
+
escapeHtml(content.text),
|
|
7083
|
+
{
|
|
7084
|
+
message_thread_id: threadId,
|
|
7085
|
+
parse_mode: "HTML",
|
|
7086
|
+
disable_notification: true
|
|
7087
|
+
}
|
|
7088
|
+
)
|
|
7089
|
+
);
|
|
7090
|
+
break;
|
|
7091
|
+
}
|
|
6845
7092
|
}
|
|
6846
7093
|
}
|
|
6847
7094
|
async sendPermissionRequest(sessionId, request) {
|
|
6848
|
-
|
|
7095
|
+
log21.info({ sessionId, requestId: request.id }, "Permission request sent");
|
|
6849
7096
|
const session = this.core.sessionManager.getSession(sessionId);
|
|
6850
7097
|
if (!session) return;
|
|
6851
7098
|
if (request.description.includes("openacp")) {
|
|
6852
7099
|
const allowOption = request.options.find((o) => o.isAllow);
|
|
6853
7100
|
if (allowOption && session.permissionGate.requestId === request.id) {
|
|
6854
|
-
|
|
7101
|
+
log21.info({ sessionId, requestId: request.id }, "Auto-approving openacp command");
|
|
6855
7102
|
session.permissionGate.resolve(allowOption.id);
|
|
6856
7103
|
}
|
|
6857
7104
|
return;
|
|
@@ -6859,7 +7106,7 @@ Task completed.
|
|
|
6859
7106
|
if (session.dangerousMode) {
|
|
6860
7107
|
const allowOption = request.options.find((o) => o.isAllow);
|
|
6861
7108
|
if (allowOption && session.permissionGate.requestId === request.id) {
|
|
6862
|
-
|
|
7109
|
+
log21.info({ sessionId, requestId: request.id, optionId: allowOption.id }, "Dangerous mode: auto-approving permission");
|
|
6863
7110
|
session.permissionGate.resolve(allowOption.id);
|
|
6864
7111
|
}
|
|
6865
7112
|
return;
|
|
@@ -6870,7 +7117,7 @@ Task completed.
|
|
|
6870
7117
|
}
|
|
6871
7118
|
async sendNotification(notification) {
|
|
6872
7119
|
if (notification.sessionId === this.assistantSession?.id) return;
|
|
6873
|
-
|
|
7120
|
+
log21.info(
|
|
6874
7121
|
{ sessionId: notification.sessionId, type: notification.type },
|
|
6875
7122
|
"Notification sent"
|
|
6876
7123
|
);
|
|
@@ -6906,7 +7153,7 @@ Task completed.
|
|
|
6906
7153
|
);
|
|
6907
7154
|
}
|
|
6908
7155
|
async createSessionThread(sessionId, name) {
|
|
6909
|
-
|
|
7156
|
+
log21.info({ sessionId, name }, "Session topic created");
|
|
6910
7157
|
return String(
|
|
6911
7158
|
await createSessionTopic(this.bot, this.telegramConfig.chatId, name)
|
|
6912
7159
|
);
|
|
@@ -6930,7 +7177,7 @@ Task completed.
|
|
|
6930
7177
|
try {
|
|
6931
7178
|
await this.bot.api.deleteForumTopic(this.telegramConfig.chatId, topicId);
|
|
6932
7179
|
} catch (err) {
|
|
6933
|
-
|
|
7180
|
+
log21.warn({ err, sessionId, topicId }, "Failed to delete forum topic (may already be deleted)");
|
|
6934
7181
|
}
|
|
6935
7182
|
}
|
|
6936
7183
|
async sendSkillCommands(sessionId, commands) {
|
|
@@ -6954,7 +7201,7 @@ Task completed.
|
|
|
6954
7201
|
const buffer = Buffer.from(await response.arrayBuffer());
|
|
6955
7202
|
return { buffer, filePath: file.file_path };
|
|
6956
7203
|
} catch (err) {
|
|
6957
|
-
|
|
7204
|
+
log21.error({ err }, "Failed to download file from Telegram");
|
|
6958
7205
|
return null;
|
|
6959
7206
|
}
|
|
6960
7207
|
}
|
|
@@ -6962,17 +7209,24 @@ Task completed.
|
|
|
6962
7209
|
const downloaded = await this.downloadTelegramFile(fileId);
|
|
6963
7210
|
if (!downloaded) return;
|
|
6964
7211
|
let buffer = downloaded.buffer;
|
|
7212
|
+
let originalFilePath;
|
|
7213
|
+
const sessionId = this.resolveSessionId(threadId) || "unknown";
|
|
6965
7214
|
if (convertOggToWav) {
|
|
7215
|
+
const oggAtt = await this.fileService.saveFile(sessionId, "voice.ogg", downloaded.buffer, "audio/ogg");
|
|
7216
|
+
originalFilePath = oggAtt.filePath;
|
|
6966
7217
|
try {
|
|
6967
7218
|
buffer = await this.fileService.convertOggToWav(buffer);
|
|
6968
7219
|
} catch (err) {
|
|
6969
|
-
|
|
7220
|
+
log21.warn({ err }, "OGG\u2192WAV conversion failed, saving original OGG");
|
|
6970
7221
|
fileName = "voice.ogg";
|
|
6971
7222
|
mimeType = "audio/ogg";
|
|
7223
|
+
originalFilePath = void 0;
|
|
6972
7224
|
}
|
|
6973
7225
|
}
|
|
6974
|
-
const sessionId = this.resolveSessionId(threadId) || "unknown";
|
|
6975
7226
|
const att = await this.fileService.saveFile(sessionId, fileName, buffer, mimeType);
|
|
7227
|
+
if (originalFilePath) {
|
|
7228
|
+
att.originalFilePath = originalFilePath;
|
|
7229
|
+
}
|
|
6976
7230
|
const rawText = caption || `[${att.type === "image" ? "Photo" : att.type === "audio" ? "Audio" : "File"}: ${att.fileName}]`;
|
|
6977
7231
|
const text = rawText.startsWith("/") ? rawText.slice(1) : rawText;
|
|
6978
7232
|
if (threadId === this.assistantTopicId) {
|
|
@@ -6989,11 +7243,52 @@ Task completed.
|
|
|
6989
7243
|
userId: String(userId),
|
|
6990
7244
|
text,
|
|
6991
7245
|
attachments: [att]
|
|
6992
|
-
}).catch((err) =>
|
|
7246
|
+
}).catch((err) => log21.error({ err }, "handleMessage error"));
|
|
6993
7247
|
}
|
|
6994
7248
|
async cleanupSkillCommands(sessionId) {
|
|
6995
7249
|
await this.skillManager.cleanup(sessionId);
|
|
6996
7250
|
}
|
|
7251
|
+
async archiveSessionTopic(sessionId) {
|
|
7252
|
+
const core = this.core;
|
|
7253
|
+
const session = core.sessionManager.getSession(sessionId);
|
|
7254
|
+
if (!session) return null;
|
|
7255
|
+
const chatId = this.telegramConfig.chatId;
|
|
7256
|
+
const oldTopicId = Number(session.threadId);
|
|
7257
|
+
const rawName = (session.name || `Session ${session.id.slice(0, 6)}`).replace(/^🔄\s*/, "");
|
|
7258
|
+
session.archiving = true;
|
|
7259
|
+
await this.draftManager.finalize(session.id, this.assistantSession?.id);
|
|
7260
|
+
this.draftManager.cleanup(session.id);
|
|
7261
|
+
this.toolTracker.cleanup(session.id);
|
|
7262
|
+
await this.skillManager.cleanup(session.id);
|
|
7263
|
+
const tracker = this.sessionTrackers.get(session.id);
|
|
7264
|
+
if (tracker) {
|
|
7265
|
+
tracker.destroy();
|
|
7266
|
+
this.sessionTrackers.delete(session.id);
|
|
7267
|
+
}
|
|
7268
|
+
await deleteSessionTopic(this.bot, chatId, oldTopicId);
|
|
7269
|
+
let newTopicId;
|
|
7270
|
+
try {
|
|
7271
|
+
newTopicId = await createSessionTopic(this.bot, chatId, `\u{1F504} ${rawName}`);
|
|
7272
|
+
} catch (createErr) {
|
|
7273
|
+
session.archiving = false;
|
|
7274
|
+
core.notificationManager.notifyAll({
|
|
7275
|
+
sessionId: session.id,
|
|
7276
|
+
sessionName: session.name,
|
|
7277
|
+
type: "error",
|
|
7278
|
+
summary: `Topic recreation failed for session "${rawName}". Session is orphaned. Error: ${createErr.message}`
|
|
7279
|
+
});
|
|
7280
|
+
throw createErr;
|
|
7281
|
+
}
|
|
7282
|
+
session.threadId = String(newTopicId);
|
|
7283
|
+
const existingRecord = core.sessionManager.getSessionRecord(session.id);
|
|
7284
|
+
const existingPlatform = { ...existingRecord?.platform ?? {} };
|
|
7285
|
+
delete existingPlatform.skillMsgId;
|
|
7286
|
+
await core.sessionManager.patchRecord(session.id, {
|
|
7287
|
+
platform: { ...existingPlatform, topicId: newTopicId }
|
|
7288
|
+
});
|
|
7289
|
+
session.archiving = false;
|
|
7290
|
+
return { newThreadId: String(newTopicId) };
|
|
7291
|
+
}
|
|
6997
7292
|
};
|
|
6998
7293
|
|
|
6999
7294
|
export {
|
|
@@ -7011,10 +7306,13 @@ export {
|
|
|
7011
7306
|
SessionBridge,
|
|
7012
7307
|
NotificationManager,
|
|
7013
7308
|
MessageTransformer,
|
|
7309
|
+
UsageStore,
|
|
7310
|
+
UsageBudget,
|
|
7311
|
+
SpeechService,
|
|
7312
|
+
GroqSTT,
|
|
7014
7313
|
OpenACPCore,
|
|
7015
|
-
ChannelAdapter,
|
|
7016
7314
|
ApiServer,
|
|
7017
7315
|
TopicManager,
|
|
7018
7316
|
TelegramAdapter
|
|
7019
7317
|
};
|
|
7020
|
-
//# sourceMappingURL=chunk-
|
|
7318
|
+
//# sourceMappingURL=chunk-R3UJUOXI.js.map
|