@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.
Files changed (100) hide show
  1. package/README.md +51 -16
  2. package/dist/action-detect-6M5GCGAU.js +15 -0
  3. package/dist/admin-IKPS5PFC.js +16 -0
  4. package/dist/agents-55NX3DHM.js +14 -0
  5. package/dist/{api-client-UN7BXQOQ.js → api-client-BH2JFHQW.js} +4 -2
  6. package/dist/{autostart-K73RQZVV.js → autostart-A7JRU4WJ.js} +6 -2
  7. package/dist/{chunk-ECBD5I5R.js → chunk-2KJC3ILH.js} +123 -16
  8. package/dist/chunk-2KJC3ILH.js.map +1 -0
  9. package/dist/{chunk-2Z2XPUD5.js → chunk-4LFDEW22.js} +53 -5
  10. package/dist/chunk-4LFDEW22.js.map +1 -0
  11. package/dist/{chunk-Z46LGZ7R.js → chunk-4TR5Y3MP.js} +18 -1
  12. package/dist/chunk-4TR5Y3MP.js.map +1 -0
  13. package/dist/chunk-7G5QKLLF.js +105 -0
  14. package/dist/chunk-7G5QKLLF.js.map +1 -0
  15. package/dist/{chunk-IURZ4QHG.js → chunk-7QJS2XBD.js} +2 -1
  16. package/dist/chunk-7QJS2XBD.js.map +1 -0
  17. package/dist/chunk-AKIU4JBF.js +145 -0
  18. package/dist/chunk-AKIU4JBF.js.map +1 -0
  19. package/dist/{chunk-KSIQZC3J.js → chunk-EVFJW45N.js} +1 -1
  20. package/dist/chunk-EVFJW45N.js.map +1 -0
  21. package/dist/chunk-GINCOFNW.js +134 -0
  22. package/dist/chunk-GINCOFNW.js.map +1 -0
  23. package/dist/chunk-H7ZMPBZC.js +203 -0
  24. package/dist/chunk-H7ZMPBZC.js.map +1 -0
  25. package/dist/chunk-I7WC6E5S.js +71 -0
  26. package/dist/chunk-I7WC6E5S.js.map +1 -0
  27. package/dist/{chunk-6DAZSKE5.js → chunk-IMILOCR5.js} +2 -2
  28. package/dist/chunk-LGQYTK55.js +442 -0
  29. package/dist/chunk-LGQYTK55.js.map +1 -0
  30. package/dist/{chunk-X6LLG7XN.js → chunk-PMGNLNSH.js} +15 -6
  31. package/dist/chunk-PMGNLNSH.js.map +1 -0
  32. package/dist/{chunk-LCJIPE5S.js → chunk-R3UJUOXI.js} +889 -591
  33. package/dist/chunk-R3UJUOXI.js.map +1 -0
  34. package/dist/chunk-SM3G6UAX.js +122 -0
  35. package/dist/chunk-SM3G6UAX.js.map +1 -0
  36. package/dist/chunk-T22OLSET.js +265 -0
  37. package/dist/chunk-T22OLSET.js.map +1 -0
  38. package/dist/chunk-THBR6OXH.js +62 -0
  39. package/dist/chunk-THBR6OXH.js.map +1 -0
  40. package/dist/{chunk-5KYLXEG3.js → chunk-TOZQ3JFN.js} +52 -9
  41. package/dist/chunk-TOZQ3JFN.js.map +1 -0
  42. package/dist/{chunk-IQIPQTQT.js → chunk-UB7XUO7C.js} +171 -26
  43. package/dist/chunk-UB7XUO7C.js.map +1 -0
  44. package/dist/{chunk-OORPX73T.js → chunk-W3EYKZNQ.js} +17 -2
  45. package/dist/chunk-W3EYKZNQ.js.map +1 -0
  46. package/dist/{chunk-K53OZH5Y.js → chunk-ZCHNAM3B.js} +76 -2
  47. package/dist/chunk-ZCHNAM3B.js.map +1 -0
  48. package/dist/cli.js +30 -29
  49. package/dist/cli.js.map +1 -1
  50. package/dist/{config-OH26EIWN.js → config-AK2W3E67.js} +2 -2
  51. package/dist/config-editor-VIA7A72X.js +12 -0
  52. package/dist/{config-registry-SNKA2EH2.js → config-registry-QQOJ2GQP.js} +2 -2
  53. package/dist/{daemon-VKCONJUY.js → daemon-G27YZUWB.js} +3 -3
  54. package/dist/discord-2DKRH45T.js +2044 -0
  55. package/dist/discord-2DKRH45T.js.map +1 -0
  56. package/dist/doctor-AN6AZ3PF.js +9 -0
  57. package/dist/doctor-CHCYUTV5.js +14 -0
  58. package/dist/doctor-CHCYUTV5.js.map +1 -0
  59. package/dist/index.d.ts +331 -6
  60. package/dist/index.js +21 -11
  61. package/dist/{main-NEYPQHB4.js → main-56SPFYW4.js} +32 -24
  62. package/dist/main-56SPFYW4.js.map +1 -0
  63. package/dist/{menu-J5YVH665.js → menu-XR2GET2B.js} +2 -2
  64. package/dist/menu-XR2GET2B.js.map +1 -0
  65. package/dist/new-session-DRRP2J7E.js +16 -0
  66. package/dist/new-session-DRRP2J7E.js.map +1 -0
  67. package/dist/session-FVFLBREJ.js +19 -0
  68. package/dist/session-FVFLBREJ.js.map +1 -0
  69. package/dist/settings-LPOLJ6SA.js +12 -0
  70. package/dist/settings-LPOLJ6SA.js.map +1 -0
  71. package/dist/{setup-ZCWGOEAH.js → setup-IPWJCIJM.js} +9 -5
  72. package/dist/setup-IPWJCIJM.js.map +1 -0
  73. package/dist/{version-VC5CPXBX.js → version-ALWGGVKM.js} +2 -2
  74. package/dist/version-ALWGGVKM.js.map +1 -0
  75. package/package.json +2 -1
  76. package/dist/chunk-2Z2XPUD5.js.map +0 -1
  77. package/dist/chunk-5KYLXEG3.js.map +0 -1
  78. package/dist/chunk-ECBD5I5R.js.map +0 -1
  79. package/dist/chunk-IQIPQTQT.js.map +0 -1
  80. package/dist/chunk-IURZ4QHG.js.map +0 -1
  81. package/dist/chunk-K53OZH5Y.js.map +0 -1
  82. package/dist/chunk-KSIQZC3J.js.map +0 -1
  83. package/dist/chunk-LCJIPE5S.js.map +0 -1
  84. package/dist/chunk-OORPX73T.js.map +0 -1
  85. package/dist/chunk-X6LLG7XN.js.map +0 -1
  86. package/dist/chunk-Z46LGZ7R.js.map +0 -1
  87. package/dist/config-editor-5TICUK3K.js +0 -12
  88. package/dist/doctor-X6UCE7GQ.js +0 -9
  89. package/dist/main-NEYPQHB4.js.map +0 -1
  90. /package/dist/{api-client-UN7BXQOQ.js.map → action-detect-6M5GCGAU.js.map} +0 -0
  91. /package/dist/{autostart-K73RQZVV.js.map → admin-IKPS5PFC.js.map} +0 -0
  92. /package/dist/{config-OH26EIWN.js.map → agents-55NX3DHM.js.map} +0 -0
  93. /package/dist/{config-editor-5TICUK3K.js.map → api-client-BH2JFHQW.js.map} +0 -0
  94. /package/dist/{config-registry-SNKA2EH2.js.map → autostart-A7JRU4WJ.js.map} +0 -0
  95. /package/dist/{chunk-6DAZSKE5.js.map → chunk-IMILOCR5.js.map} +0 -0
  96. /package/dist/{daemon-VKCONJUY.js.map → config-AK2W3E67.js.map} +0 -0
  97. /package/dist/{doctor-X6UCE7GQ.js.map → config-editor-VIA7A72X.js.map} +0 -0
  98. /package/dist/{menu-J5YVH665.js.map → config-registry-QQOJ2GQP.js.map} +0 -0
  99. /package/dist/{setup-ZCWGOEAH.js.map → daemon-G27YZUWB.js.map} +0 -0
  100. /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-K53OZH5Y.js";
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-IURZ4QHG.js";
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-Z46LGZ7R.js";
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.agentInstance.prompt(text, attachments);
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 fs2 from "fs";
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 fs2.promises.mkdir(sessionDir, { recursive: true });
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 fs2.promises.writeFile(filePath, data);
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 fs2.promises.stat(filePath);
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 fs5 = this.deps.fileService;
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
- fs5.saveFile(sid, `agent-image${ext}`, buffer, mimeType).then((att) => {
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 fs5 = this.deps.fileService;
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
- fs5.saveFile(sid, `agent-audio${ext}`, buffer, mimeType).then((att) => {
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 path4 from "path";
1887
+ import path5 from "path";
1509
1888
  import os from "os";
1510
1889
 
1511
1890
  // src/core/session-store.ts
1512
- import fs3 from "fs";
1513
- import path3 from "path";
1514
- var log4 = createChildLogger({ module: "session-store" });
1515
- var DEBOUNCE_MS = 2e3;
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 = path3.dirname(this.filePath);
1576
- if (!fs3.existsSync(dir)) fs3.mkdirSync(dir, { recursive: true });
1577
- fs3.writeFileSync(this.filePath, JSON.stringify(data, null, 2));
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 (!fs3.existsSync(this.filePath)) return;
1969
+ if (!fs5.existsSync(this.filePath)) return;
1591
1970
  try {
1592
1971
  const raw = JSON.parse(
1593
- fs3.readFileSync(this.filePath, "utf-8")
1972
+ fs5.readFileSync(this.filePath, "utf-8")
1594
1973
  );
1595
1974
  if (raw.version !== 1) {
1596
- log4.warn(
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
- log4.info({ count: this.records.size }, "Loaded session records");
1984
+ log5.info({ count: this.records.size }, "Loaded session records");
1606
1985
  } catch (err) {
1607
- log4.error({ err }, "Failed to load session store");
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
- log4.info({ removed }, "Cleaned up expired session records");
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
- }, DEBOUNCE_MS);
2010
+ }, DEBOUNCE_MS2);
1632
2011
  }
1633
2012
  };
1634
2013
 
1635
2014
  // src/core/core.ts
1636
- var log5 = createChildLogger({ module: "core" });
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 = path4.join(os.homedir(), ".openacp", "sessions.json");
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(path4.join(os.homedir(), ".openacp", "files"));
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
- log5.info({ level: value }, "Log level changed at runtime");
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
- log5.warn({ err }, "Background registry refresh failed");
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
- log5.debug(
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
- if (!config.security.allowedUserIds.includes(message.userId)) {
1719
- log5.warn(
1720
- { userId: message.userId },
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
- log5.warn(
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
- log5.warn(
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
- platform.topicId = Number(session.threadId);
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
- log5.info(
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
- log5.info({ channelId, agentName: resolvedAgent }, "New session request");
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
- log5.debug(
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
- log5.debug(
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
- log5.info(
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
- log5.info(
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
- log5.error({ err, record }, "Lazy resume failed");
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 fs4 from "fs";
2040
- import * as path5 from "path";
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 log6 = createChildLogger({ module: "api-server" });
2044
- var DEFAULT_PORT_FILE = path5.join(os2.homedir(), ".openacp", "api.port");
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 = path5.resolve(path5.dirname(__filename), "../../package.json");
2051
- const pkg = JSON.parse(fs4.readFileSync(pkgPath, "utf-8"));
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
- log6.warn({ port: this.config.port }, "API port in use, continuing without API server");
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
- log6.info({ host: this.config.host, port: this.actualPort }, "API server listening");
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 = path5.dirname(this.portFilePath);
2121
- fs4.mkdirSync(dir, { recursive: true });
2122
- fs4.writeFileSync(this.portFilePath, String(this.actualPort));
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
- fs4.unlinkSync(this.portFilePath);
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
- log6.error({ err }, "API request error");
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
- log6.debug({ sessionId: session.id, permissionId: request.id, option: allowOption?.id }, "Auto-approving permission for API session");
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) => log6.warn({ err, sessionId: session.id }, "API session warmup failed"));
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-SNKA2EH2.js");
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
- if (target[parts[i]] && typeof target[parts[i]] === "object" && !Array.isArray(target[parts[i]])) {
2391
- target = target[parts[i]];
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-OH26EIWN.js");
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-SNKA2EH2.js");
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 log7 = createChildLogger({ module: "topic-manager" });
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
- log7.warn({ err, sessionId, topicId }, "Failed to delete platform thread, removing record anyway");
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
- log7.warn({ err, sessionId: record.sessionId }, "Failed to delete platform thread during cleanup");
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 log9 = createChildLogger({ module: "telegram-cmd-admin" });
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
- log9.info({ sessionId, dangerousMode: session.dangerousMode }, "Dangerous mode toggled via button");
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
- log9.info({ sessionId, dangerousMode: newDangerousMode }, "Dangerous mode toggled via button (store-only, session not in memory)");
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-VC5CPXBX.js");
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 log10 = createChildLogger({ module: "telegram-cmd-new-session" });
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
- log10.info({ userId: ctx.from?.id, agentName, workspace }, "New session command (direct)");
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) => log10.error({ err }, "Warm-up error"));
3743
+ session.warmup().catch((err) => log11.error({ err }, "Warm-up error"));
3224
3744
  return threadId ?? null;
3225
3745
  } catch (err) {
3226
- log10.error({ err }, "Session creation failed");
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) => log10.error({ err }, "Warm-up error"));
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) => log10.error({ err }, "Warm-up error"));
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 log11 = createChildLogger({ module: "telegram-cmd-session" });
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
- log11.info({ sessionId: session.id }, "Abort prompt command");
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
- log11.info({ sessionId: record.sessionId, status: record.status }, "Cancel command \u2014 no active prompt to abort");
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
- log11.error({ err }, "handleTopics error");
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
- log11.warn({ err, sessionId: record.sessionId, topicId }, "Failed to delete forum topic during cleanup");
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
- log11.error({ err, sessionId: record.sessionId }, "Failed to cleanup session");
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
- log11.warn({ err, sessionId: record.sessionId }, "Failed to cancel session during cleanup");
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
- log11.warn({ err, sessionId: record.sessionId, topicId }, "Failed to delete forum topic during cleanup");
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
- log11.error({ err, sessionId: record.sessionId }, "Failed to cleanup session");
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 log12 = createChildLogger({ module: "telegram-settings" });
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
- return `${icon} ${field.displayName}: ${String(value)}`;
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
- log12.error({ err, fieldPath }, "Failed to toggle config");
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
- log12.error({ err, fieldPath }, "Failed to set config");
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-J5YVH665.js");
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 log13 = createChildLogger({ module: "telegram-cmd-doctor" });
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
- log13.error({ err }, "Doctor command failed");
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
- log13.error({ err, index }, "Doctor fix callback failed");
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 log14 = createChildLogger({ module: "telegram-cmd-tunnel" });
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 entries = core.tunnelService.listTunnels();
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
- await ctx.reply(
4516
- "No active tunnels.\n\nUse <code>/tunnel &lt;port&gt;</code> to create one.",
4517
- { parse_mode: "HTML" }
4518
- );
5147
+ const hint = sessionScoped ? "No tunnels for this session.\n\nUse <code>/tunnel &lt;port&gt;</code> to create one." : "No active tunnels.\n\nUse <code>/tunnel &lt;port&gt;</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 nanoid2 } from "nanoid";
4690
- var log15 = createChildLogger({ module: "telegram-permissions" });
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 = nanoid2(8);
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
- log15.info({ requestId: pending.requestId, optionId, isAllow }, "Permission responded");
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 log16 = createChildLogger({ module: "telegram-assistant" });
5403
+ var log17 = createChildLogger({ module: "telegram-assistant" });
5183
5404
  async function spawnAssistant(core, adapter, assistantTopicId) {
5184
5405
  const config = core.configManager.get();
5185
- log16.info({ agent: config.defaultAgent }, "Creating assistant session...");
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
- log16.info({ sessionId: session.id }, "Assistant agent spawned");
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
- log16.info({ sessionId: session.id }, "Assistant system prompt completed");
5436
+ log17.info({ sessionId: session.id }, "Assistant system prompt completed");
5216
5437
  }).catch((err) => {
5217
- log16.warn({ err }, "Assistant system prompt failed");
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 log17 = createChildLogger({ module: "telegram:activity" });
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
- log17.warn({ err }, "ThinkingIndicator.show() failed");
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
- log17.warn({ err }, "UsageMessage.send() failed");
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
- log17.warn({ err }, "UsageMessage.delete() failed");
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
- log17.warn({ err }, "PlanCard flush failed");
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
- log17.warn({ err }, "ActivityTracker.onComplete() Done send failed");
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 nanoid3 } from "nanoid";
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 = nanoid3(10);
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 log18 = createChildLogger({ module: "tool-call-tracker" });
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
- log18.debug({ toolId: meta.id, viewerLinks: meta.viewerLinks }, "Accumulated viewerLinks");
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
- log18.debug(
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
- log18.warn(
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 log19 = createChildLogger({ module: "skill-commands" });
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
- log19.error({ err, sessionId }, "Failed to send skill commands");
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 log20 = createChildLogger({ module: "telegram" });
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
- log20.error({ err: rootCause }, "Telegram bot error");
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
- log20.warn(
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: () => log20.info(
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
- log20.warn({ err }, "Failed to send welcome message");
6759
+ log21.warn({ err }, "Failed to send welcome message");
6528
6760
  }
6529
6761
  try {
6530
- log20.info("Spawning assistant session...");
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
- log20.info({ sessionId: session.id }, "Assistant session ready, system prompt running in background");
6770
+ log21.info({ sessionId: session.id }, "Assistant session ready, system prompt running in background");
6539
6771
  ready.then(() => {
6540
6772
  this.assistantInitializing = false;
6541
- log20.info({ sessionId: session.id }, "Assistant ready for user messages");
6773
+ log21.info({ sessionId: session.id }, "Assistant ready for user messages");
6542
6774
  });
6543
6775
  } catch (err) {
6544
- log20.error({ err }, "Failed to spawn assistant");
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
- log20.info("Telegram bot stopped");
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) => log20.error({ err }, "Assistant error")
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) => log20.error({ err }, "handleMessage error"));
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
- log20.warn({ sessionId, threadId: session.threadId }, "Session has no valid threadId, skipping message");
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
- log20.warn({ sessionId, fileName: attachment.fileName, size: attachment.size }, "File too large for Telegram (>50MB)");
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
- log20.error({ err, sessionId, fileName: attachment.fileName }, "Failed to send attachment");
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
- log20.info({ sessionId, requestId: request.id }, "Permission request sent");
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
- log20.info({ sessionId, requestId: request.id }, "Auto-approving openacp command");
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
- log20.info({ sessionId, requestId: request.id, optionId: allowOption.id }, "Dangerous mode: auto-approving permission");
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
- log20.info(
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
- log20.info({ sessionId, name }, "Session topic created");
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
- log20.warn({ err, sessionId, topicId }, "Failed to delete forum topic (may already be deleted)");
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
- log20.error({ err }, "Failed to download file from Telegram");
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
- log20.warn({ err }, "OGG\u2192WAV conversion failed, saving original OGG");
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) => log20.error({ err }, "handleMessage error"));
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-LCJIPE5S.js.map
7318
+ //# sourceMappingURL=chunk-R3UJUOXI.js.map