@openacp/cli 0.6.0 → 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 (54) hide show
  1. package/README.md +11 -2
  2. package/dist/{chunk-MHFCZGRW.js → chunk-2KJC3ILH.js} +13 -3
  3. package/dist/{chunk-MHFCZGRW.js.map → chunk-2KJC3ILH.js.map} +1 -1
  4. package/dist/{chunk-DWQKUECJ.js → chunk-4LFDEW22.js} +42 -4
  5. package/dist/chunk-4LFDEW22.js.map +1 -0
  6. package/dist/{chunk-Z46LGZ7R.js → chunk-4TR5Y3MP.js} +18 -1
  7. package/dist/chunk-4TR5Y3MP.js.map +1 -0
  8. package/dist/{chunk-IURZ4QHG.js → chunk-7QJS2XBD.js} +2 -1
  9. package/dist/chunk-7QJS2XBD.js.map +1 -0
  10. package/dist/{chunk-3WPG7GXA.js → chunk-GINCOFNW.js} +2 -2
  11. package/dist/{chunk-437NLISU.js → chunk-IMILOCR5.js} +2 -2
  12. package/dist/{chunk-5NBWM7P6.js → chunk-LGQYTK55.js} +5 -1
  13. package/dist/chunk-LGQYTK55.js.map +1 -0
  14. package/dist/{chunk-V2V767XI.js → chunk-R3UJUOXI.js} +842 -162
  15. package/dist/chunk-R3UJUOXI.js.map +1 -0
  16. package/dist/{chunk-YYQXWA62.js → chunk-TOZQ3JFN.js} +2 -2
  17. package/dist/{chunk-6Q7PZWCL.js → chunk-UB7XUO7C.js} +3 -3
  18. package/dist/{chunk-SPX7CKWV.js → chunk-ZCHNAM3B.js} +2 -2
  19. package/dist/cli.js +19 -19
  20. package/dist/{config-KF2MQWAP.js → config-AK2W3E67.js} +2 -2
  21. package/dist/{config-editor-OTODXUF7.js → config-editor-VIA7A72X.js} +4 -4
  22. package/dist/{config-registry-SNKA2EH2.js → config-registry-QQOJ2GQP.js} +2 -2
  23. package/dist/{daemon-U6UC7OM4.js → daemon-G27YZUWB.js} +3 -3
  24. package/dist/{discord-SLLKRUP7.js → discord-2DKRH45T.js} +16 -6
  25. package/dist/discord-2DKRH45T.js.map +1 -0
  26. package/dist/doctor-AN6AZ3PF.js +9 -0
  27. package/dist/{doctor-DB5PRQ6D.js → doctor-CHCYUTV5.js} +4 -4
  28. package/dist/index.d.ts +320 -3
  29. package/dist/index.js +18 -10
  30. package/dist/{main-M6RH3SS5.js → main-56SPFYW4.js} +16 -16
  31. package/dist/{menu-J5YVH665.js → menu-XR2GET2B.js} +2 -2
  32. package/dist/{setup-LI5CKYDK.js → setup-IPWJCIJM.js} +3 -3
  33. package/package.json +1 -1
  34. package/dist/chunk-5NBWM7P6.js.map +0 -1
  35. package/dist/chunk-DWQKUECJ.js.map +0 -1
  36. package/dist/chunk-IURZ4QHG.js.map +0 -1
  37. package/dist/chunk-V2V767XI.js.map +0 -1
  38. package/dist/chunk-Z46LGZ7R.js.map +0 -1
  39. package/dist/discord-SLLKRUP7.js.map +0 -1
  40. package/dist/doctor-SYWNJFYK.js +0 -9
  41. /package/dist/{chunk-3WPG7GXA.js.map → chunk-GINCOFNW.js.map} +0 -0
  42. /package/dist/{chunk-437NLISU.js.map → chunk-IMILOCR5.js.map} +0 -0
  43. /package/dist/{chunk-YYQXWA62.js.map → chunk-TOZQ3JFN.js.map} +0 -0
  44. /package/dist/{chunk-6Q7PZWCL.js.map → chunk-UB7XUO7C.js.map} +0 -0
  45. /package/dist/{chunk-SPX7CKWV.js.map → chunk-ZCHNAM3B.js.map} +0 -0
  46. /package/dist/{config-KF2MQWAP.js.map → config-AK2W3E67.js.map} +0 -0
  47. /package/dist/{config-editor-OTODXUF7.js.map → config-editor-VIA7A72X.js.map} +0 -0
  48. /package/dist/{config-registry-SNKA2EH2.js.map → config-registry-QQOJ2GQP.js.map} +0 -0
  49. /package/dist/{daemon-U6UC7OM4.js.map → daemon-G27YZUWB.js.map} +0 -0
  50. /package/dist/{doctor-DB5PRQ6D.js.map → doctor-AN6AZ3PF.js.map} +0 -0
  51. /package/dist/{doctor-SYWNJFYK.js.map → doctor-CHCYUTV5.js.map} +0 -0
  52. /package/dist/{main-M6RH3SS5.js.map → main-56SPFYW4.js.map} +0 -0
  53. /package/dist/{menu-J5YVH665.js.map → menu-XR2GET2B.js.map} +0 -0
  54. /package/dist/{setup-LI5CKYDK.js.map → setup-IPWJCIJM.js.map} +0 -0
@@ -1,17 +1,17 @@
1
1
  import {
2
2
  ChannelAdapter,
3
3
  PRODUCT_GUIDE
4
- } from "./chunk-5NBWM7P6.js";
4
+ } from "./chunk-LGQYTK55.js";
5
5
  import {
6
6
  DoctorEngine
7
- } from "./chunk-SPX7CKWV.js";
7
+ } from "./chunk-ZCHNAM3B.js";
8
8
  import {
9
9
  buildMenuKeyboard,
10
10
  buildSkillMessages,
11
11
  handleClear,
12
12
  handleHelp,
13
13
  handleMenu
14
- } from "./chunk-IURZ4QHG.js";
14
+ } from "./chunk-7QJS2XBD.js";
15
15
  import {
16
16
  AgentCatalog
17
17
  } from "./chunk-J6X5SW6O.js";
@@ -23,7 +23,7 @@ import {
23
23
  getSafeFields,
24
24
  isHotReloadable,
25
25
  resolveOptions
26
- } from "./chunk-Z46LGZ7R.js";
26
+ } from "./chunk-4TR5Y3MP.js";
27
27
  import {
28
28
  createChildLogger,
29
29
  createSessionLogger
@@ -761,6 +761,7 @@ var PermissionGate = class {
761
761
 
762
762
  // src/core/session.ts
763
763
  import { nanoid } from "nanoid";
764
+ import * as fs2 from "fs";
764
765
  var moduleLog = createChildLogger({ module: "session" });
765
766
  var VALID_TRANSITIONS = {
766
767
  initializing: /* @__PURE__ */ new Set(["active", "error"]),
@@ -781,9 +782,11 @@ var Session = class extends TypedEmitter {
781
782
  name;
782
783
  createdAt = /* @__PURE__ */ new Date();
783
784
  dangerousMode = false;
785
+ archiving = false;
784
786
  log;
785
787
  permissionGate = new PermissionGate();
786
788
  queue;
789
+ speechService;
787
790
  constructor(opts) {
788
791
  super();
789
792
  this.id = opts.id || nanoid(12);
@@ -791,6 +794,7 @@ var Session = class extends TypedEmitter {
791
794
  this.agentName = opts.agentName;
792
795
  this.workingDirectory = opts.workingDirectory;
793
796
  this.agentInstance = opts.agentInstance;
797
+ this.speechService = opts.speechService;
794
798
  this.log = createSessionLogger(this.id, moduleLog);
795
799
  this.log.info({ agentName: this.agentName }, "Session created");
796
800
  this.queue = new PromptQueue(
@@ -856,7 +860,8 @@ var Session = class extends TypedEmitter {
856
860
  }
857
861
  const promptStart = Date.now();
858
862
  this.log.debug("Prompt execution started");
859
- await this.agentInstance.prompt(text, attachments);
863
+ const processed = await this.maybeTranscribeAudio(text, attachments);
864
+ await this.agentInstance.prompt(processed.text, processed.attachments);
860
865
  this.log.info(
861
866
  { durationMs: Date.now() - promptStart },
862
867
  "Prompt execution completed"
@@ -865,6 +870,51 @@ var Session = class extends TypedEmitter {
865
870
  await this.autoName();
866
871
  }
867
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
+ }
868
918
  // NOTE: This injects a summary prompt into the agent's conversation history.
869
919
  async autoName() {
870
920
  let title = "";
@@ -1042,7 +1092,7 @@ var SessionManager = class {
1042
1092
  };
1043
1093
 
1044
1094
  // src/core/file-service.ts
1045
- import fs2 from "fs";
1095
+ import fs3 from "fs";
1046
1096
  import path2 from "path";
1047
1097
  import { OggOpusDecoder } from "ogg-opus-decoder";
1048
1098
  import wav from "node-wav";
@@ -1089,10 +1139,10 @@ var FileService = class {
1089
1139
  }
1090
1140
  async saveFile(sessionId, fileName, data, mimeType) {
1091
1141
  const sessionDir = path2.join(this.baseDir, sessionId);
1092
- await fs2.promises.mkdir(sessionDir, { recursive: true });
1142
+ await fs3.promises.mkdir(sessionDir, { recursive: true });
1093
1143
  const safeName = `${Date.now()}-${fileName.replace(/[^a-zA-Z0-9._-]/g, "_")}`;
1094
1144
  const filePath = path2.join(sessionDir, safeName);
1095
- await fs2.promises.writeFile(filePath, data);
1145
+ await fs3.promises.writeFile(filePath, data);
1096
1146
  return {
1097
1147
  type: classifyMime(mimeType),
1098
1148
  filePath,
@@ -1103,7 +1153,7 @@ var FileService = class {
1103
1153
  }
1104
1154
  async resolveFile(filePath) {
1105
1155
  try {
1106
- const stat = await fs2.promises.stat(filePath);
1156
+ const stat = await fs3.promises.stat(filePath);
1107
1157
  if (!stat.isFile()) return null;
1108
1158
  const ext = path2.extname(filePath).toLowerCase();
1109
1159
  const mimeType = EXT_TO_MIME[ext] || "application/octet-stream";
@@ -1232,12 +1282,12 @@ var SessionBridge = class {
1232
1282
  break;
1233
1283
  case "image_content": {
1234
1284
  if (this.deps.fileService) {
1235
- const fs5 = this.deps.fileService;
1285
+ const fs7 = this.deps.fileService;
1236
1286
  const sid = this.session.id;
1237
1287
  const { data, mimeType } = event;
1238
1288
  const buffer = Buffer.from(data, "base64");
1239
1289
  const ext = FileService.extensionFromMime(mimeType);
1240
- fs5.saveFile(sid, `agent-image${ext}`, buffer, mimeType).then((att) => {
1290
+ fs7.saveFile(sid, `agent-image${ext}`, buffer, mimeType).then((att) => {
1241
1291
  this.adapter.sendMessage(sid, { type: "attachment", text: "", attachment: att });
1242
1292
  }).catch((err) => log2.error({ err }, "Failed to save agent image"));
1243
1293
  }
@@ -1245,12 +1295,12 @@ var SessionBridge = class {
1245
1295
  }
1246
1296
  case "audio_content": {
1247
1297
  if (this.deps.fileService) {
1248
- const fs5 = this.deps.fileService;
1298
+ const fs7 = this.deps.fileService;
1249
1299
  const sid = this.session.id;
1250
1300
  const { data, mimeType } = event;
1251
1301
  const buffer = Buffer.from(data, "base64");
1252
1302
  const ext = FileService.extensionFromMime(mimeType);
1253
- fs5.saveFile(sid, `agent-audio${ext}`, buffer, mimeType).then((att) => {
1303
+ fs7.saveFile(sid, `agent-audio${ext}`, buffer, mimeType).then((att) => {
1254
1304
  this.adapter.sendMessage(sid, { type: "attachment", text: "", attachment: att });
1255
1305
  }).catch((err) => log2.error({ err }, "Failed to save agent audio"));
1256
1306
  }
@@ -1260,6 +1310,12 @@ var SessionBridge = class {
1260
1310
  log2.debug({ commands: event.commands }, "Commands available");
1261
1311
  this.adapter.sendSkillCommands(this.session.id, event.commands);
1262
1312
  break;
1313
+ case "system_message":
1314
+ this.adapter.sendMessage(
1315
+ this.session.id,
1316
+ this.deps.messageTransformer.transform(event)
1317
+ );
1318
+ break;
1263
1319
  }
1264
1320
  };
1265
1321
  this.session.on("agent_event", this.agentEventHandler);
@@ -1454,6 +1510,8 @@ var MessageTransformer = class {
1454
1510
  return { type: "session_end", text: `Done (${event.reason})` };
1455
1511
  case "error":
1456
1512
  return { type: "error", text: event.message };
1513
+ case "system_message":
1514
+ return { type: "system_message", text: event.message };
1457
1515
  default:
1458
1516
  return { type: "text", text: "" };
1459
1517
  }
@@ -1509,15 +1567,331 @@ var MessageTransformer = class {
1509
1567
  }
1510
1568
  };
1511
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
+
1512
1886
  // src/core/core.ts
1513
- import path4 from "path";
1887
+ import path5 from "path";
1514
1888
  import os from "os";
1515
1889
 
1516
1890
  // src/core/session-store.ts
1517
- import fs3 from "fs";
1518
- import path3 from "path";
1519
- var log4 = createChildLogger({ module: "session-store" });
1520
- 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;
1521
1895
  var JsonFileSessionStore = class {
1522
1896
  records = /* @__PURE__ */ new Map();
1523
1897
  filePath;
@@ -1577,9 +1951,9 @@ var JsonFileSessionStore = class {
1577
1951
  version: 1,
1578
1952
  sessions: Object.fromEntries(this.records)
1579
1953
  };
1580
- const dir = path3.dirname(this.filePath);
1581
- if (!fs3.existsSync(dir)) fs3.mkdirSync(dir, { recursive: true });
1582
- 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));
1583
1957
  }
1584
1958
  destroy() {
1585
1959
  if (this.debounceTimer) clearTimeout(this.debounceTimer);
@@ -1592,13 +1966,13 @@ var JsonFileSessionStore = class {
1592
1966
  }
1593
1967
  }
1594
1968
  load() {
1595
- if (!fs3.existsSync(this.filePath)) return;
1969
+ if (!fs5.existsSync(this.filePath)) return;
1596
1970
  try {
1597
1971
  const raw = JSON.parse(
1598
- fs3.readFileSync(this.filePath, "utf-8")
1972
+ fs5.readFileSync(this.filePath, "utf-8")
1599
1973
  );
1600
1974
  if (raw.version !== 1) {
1601
- log4.warn(
1975
+ log5.warn(
1602
1976
  { version: raw.version },
1603
1977
  "Unknown session store version, skipping load"
1604
1978
  );
@@ -1607,9 +1981,9 @@ var JsonFileSessionStore = class {
1607
1981
  for (const [id, record] of Object.entries(raw.sessions)) {
1608
1982
  this.records.set(id, record);
1609
1983
  }
1610
- log4.info({ count: this.records.size }, "Loaded session records");
1984
+ log5.info({ count: this.records.size }, "Loaded session records");
1611
1985
  } catch (err) {
1612
- log4.error({ err }, "Failed to load session store");
1986
+ log5.error({ err }, "Failed to load session store");
1613
1987
  }
1614
1988
  }
1615
1989
  cleanup() {
@@ -1625,7 +1999,7 @@ var JsonFileSessionStore = class {
1625
1999
  }
1626
2000
  }
1627
2001
  if (removed > 0) {
1628
- log4.info({ removed }, "Cleaned up expired session records");
2002
+ log5.info({ removed }, "Cleaned up expired session records");
1629
2003
  this.scheduleDiskWrite();
1630
2004
  }
1631
2005
  }
@@ -1633,12 +2007,13 @@ var JsonFileSessionStore = class {
1633
2007
  if (this.debounceTimer) clearTimeout(this.debounceTimer);
1634
2008
  this.debounceTimer = setTimeout(() => {
1635
2009
  this.flushSync();
1636
- }, DEBOUNCE_MS);
2010
+ }, DEBOUNCE_MS2);
1637
2011
  }
1638
2012
  };
1639
2013
 
1640
2014
  // src/core/core.ts
1641
- var log5 = createChildLogger({ module: "core" });
2015
+ import { nanoid as nanoid2 } from "nanoid";
2016
+ var log6 = createChildLogger({ module: "core" });
1642
2017
  var OpenACPCore = class {
1643
2018
  configManager;
1644
2019
  agentCatalog;
@@ -1647,32 +2022,57 @@ var OpenACPCore = class {
1647
2022
  notificationManager;
1648
2023
  messageTransformer;
1649
2024
  fileService;
2025
+ speechService;
1650
2026
  adapters = /* @__PURE__ */ new Map();
1651
2027
  /** Set by main.ts — triggers graceful shutdown with restart exit code */
1652
2028
  requestRestart = null;
1653
2029
  _tunnelService;
1654
2030
  sessionStore = null;
1655
2031
  resumeLocks = /* @__PURE__ */ new Map();
2032
+ usageStore = null;
2033
+ usageBudget = null;
1656
2034
  constructor(configManager) {
1657
2035
  this.configManager = configManager;
1658
2036
  const config = configManager.get();
1659
2037
  this.agentCatalog = new AgentCatalog();
1660
2038
  this.agentCatalog.load();
1661
2039
  this.agentManager = new AgentManager(this.agentCatalog);
1662
- const storePath = path4.join(os.homedir(), ".openacp", "sessions.json");
2040
+ const storePath = path5.join(os.homedir(), ".openacp", "sessions.json");
1663
2041
  this.sessionStore = new JsonFileSessionStore(
1664
2042
  storePath,
1665
2043
  config.sessionStore.ttlDays
1666
2044
  );
1667
2045
  this.sessionManager = new SessionManager(this.sessionStore);
1668
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
+ }
1669
2053
  this.messageTransformer = new MessageTransformer();
1670
- 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
+ }
1671
2061
  this.configManager.on("config:changed", async ({ path: configPath, value }) => {
1672
2062
  if (configPath === "logging.level" && typeof value === "string") {
1673
2063
  const { setLogLevel: setLogLevel2 } = await import("./log-SPS2S6FO.js");
1674
2064
  setLogLevel2(value);
1675
- 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");
1676
2076
  }
1677
2077
  });
1678
2078
  }
@@ -1688,7 +2088,7 @@ var OpenACPCore = class {
1688
2088
  }
1689
2089
  async start() {
1690
2090
  this.agentCatalog.refreshRegistryIfStale().catch((err) => {
1691
- log5.warn({ err }, "Background registry refresh failed");
2091
+ log6.warn({ err }, "Background registry refresh failed");
1692
2092
  });
1693
2093
  for (const adapter of this.adapters.values()) {
1694
2094
  await adapter.start();
@@ -1707,11 +2107,30 @@ var OpenACPCore = class {
1707
2107
  for (const adapter of this.adapters.values()) {
1708
2108
  await adapter.stop();
1709
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
+ }
1710
2129
  }
1711
2130
  // --- Message Routing ---
1712
2131
  async handleMessage(message) {
1713
2132
  const config = this.configManager.get();
1714
- log5.debug(
2133
+ log6.debug(
1715
2134
  {
1716
2135
  channelId: message.channelId,
1717
2136
  threadId: message.threadId,
@@ -1722,7 +2141,7 @@ var OpenACPCore = class {
1722
2141
  if (config.security.allowedUserIds.length > 0) {
1723
2142
  const userId = String(message.userId);
1724
2143
  if (!config.security.allowedUserIds.includes(userId)) {
1725
- log5.warn(
2144
+ log6.warn(
1726
2145
  { userId },
1727
2146
  "Rejected message from unauthorized user"
1728
2147
  );
@@ -1731,7 +2150,7 @@ var OpenACPCore = class {
1731
2150
  }
1732
2151
  const activeSessions = this.sessionManager.listSessions().filter((s) => s.status === "active" || s.status === "initializing");
1733
2152
  if (activeSessions.length >= config.security.maxConcurrentSessions) {
1734
- log5.warn(
2153
+ log6.warn(
1735
2154
  {
1736
2155
  userId: message.userId,
1737
2156
  currentCount: activeSessions.length,
@@ -1756,7 +2175,7 @@ var OpenACPCore = class {
1756
2175
  session = await this.lazyResume(message) ?? void 0;
1757
2176
  }
1758
2177
  if (!session) {
1759
- log5.warn(
2178
+ log6.warn(
1760
2179
  { channelId: message.channelId, threadId: message.threadId },
1761
2180
  "No session found for thread (in-memory miss + lazy resume returned null)"
1762
2181
  );
@@ -1780,7 +2199,8 @@ var OpenACPCore = class {
1780
2199
  channelId: params.channelId,
1781
2200
  agentName: params.agentName,
1782
2201
  workingDirectory: params.workingDirectory,
1783
- agentInstance
2202
+ agentInstance,
2203
+ speechService: this.speechService
1784
2204
  });
1785
2205
  session.agentSessionId = agentInstance.sessionId;
1786
2206
  if (params.initialName) {
@@ -1799,6 +2219,32 @@ var OpenACPCore = class {
1799
2219
  const bridge = this.createBridge(session, adapter);
1800
2220
  bridge.connect();
1801
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
+ }
1802
2248
  session.on("status_change", (_from, to) => {
1803
2249
  if ((to === "finished" || to === "cancelled") && this._tunnelService) {
1804
2250
  this._tunnelService.stopBySession(session.id).then((stopped) => {
@@ -1838,7 +2284,7 @@ var OpenACPCore = class {
1838
2284
  name: session.name,
1839
2285
  platform
1840
2286
  });
1841
- log5.info(
2287
+ log6.info(
1842
2288
  { sessionId: session.id, agentName: params.agentName },
1843
2289
  "Session created via pipeline"
1844
2290
  );
@@ -1847,7 +2293,7 @@ var OpenACPCore = class {
1847
2293
  async handleNewSession(channelId, agentName, workspacePath) {
1848
2294
  const config = this.configManager.get();
1849
2295
  const resolvedAgent = agentName || config.defaultAgent;
1850
- log5.info({ channelId, agentName: resolvedAgent }, "New session request");
2296
+ log6.info({ channelId, agentName: resolvedAgent }, "New session request");
1851
2297
  const agentDef = this.agentCatalog.resolve(resolvedAgent);
1852
2298
  const resolvedWorkspace = this.configManager.resolveWorkspace(
1853
2299
  workspacePath || agentDef?.workingDirectory
@@ -1962,20 +2408,20 @@ var OpenACPCore = class {
1962
2408
  (p) => String(p.topicId) === message.threadId
1963
2409
  );
1964
2410
  if (!record) {
1965
- log5.debug(
2411
+ log6.debug(
1966
2412
  { threadId: message.threadId, channelId: message.channelId },
1967
2413
  "No session record found for thread"
1968
2414
  );
1969
2415
  return null;
1970
2416
  }
1971
2417
  if (record.status === "error") {
1972
- log5.debug(
2418
+ log6.debug(
1973
2419
  { threadId: message.threadId, sessionId: record.sessionId, status: record.status },
1974
2420
  "Skipping resume of error session"
1975
2421
  );
1976
2422
  return null;
1977
2423
  }
1978
- log5.info(
2424
+ log6.info(
1979
2425
  { threadId: message.threadId, sessionId: record.sessionId, status: record.status },
1980
2426
  "Lazy resume: found record, attempting resume"
1981
2427
  );
@@ -1992,13 +2438,13 @@ var OpenACPCore = class {
1992
2438
  session.threadId = message.threadId;
1993
2439
  session.activate();
1994
2440
  session.dangerousMode = record.dangerousMode ?? false;
1995
- log5.info(
2441
+ log6.info(
1996
2442
  { sessionId: session.id, threadId: message.threadId },
1997
2443
  "Lazy resume successful"
1998
2444
  );
1999
2445
  return session;
2000
2446
  } catch (err) {
2001
- log5.error({ err, record }, "Lazy resume failed");
2447
+ log6.error({ err, record }, "Lazy resume failed");
2002
2448
  const adapter = this.adapters.get(message.channelId);
2003
2449
  if (adapter) {
2004
2450
  try {
@@ -2031,20 +2477,20 @@ var OpenACPCore = class {
2031
2477
 
2032
2478
  // src/core/api-server.ts
2033
2479
  import * as http from "http";
2034
- import * as fs4 from "fs";
2035
- import * as path5 from "path";
2480
+ import * as fs6 from "fs";
2481
+ import * as path6 from "path";
2036
2482
  import * as os2 from "os";
2037
2483
  import * as crypto from "crypto";
2038
2484
  import { fileURLToPath } from "url";
2039
- var log6 = createChildLogger({ module: "api-server" });
2040
- 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");
2041
2487
  var cachedVersion;
2042
2488
  function getVersion() {
2043
2489
  if (cachedVersion) return cachedVersion;
2044
2490
  try {
2045
2491
  const __filename = fileURLToPath(import.meta.url);
2046
- const pkgPath = path5.resolve(path5.dirname(__filename), "../../package.json");
2047
- 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"));
2048
2494
  cachedVersion = pkg.version ?? "0.0.0-dev";
2049
2495
  } catch {
2050
2496
  cachedVersion = "0.0.0-dev";
@@ -2072,7 +2518,7 @@ var ApiServer = class {
2072
2518
  this.config = config;
2073
2519
  this.topicManager = topicManager;
2074
2520
  this.portFilePath = portFilePath ?? DEFAULT_PORT_FILE;
2075
- this.secretFilePath = secretFilePath ?? path5.join(os2.homedir(), ".openacp", "api-secret");
2521
+ this.secretFilePath = secretFilePath ?? path6.join(os2.homedir(), ".openacp", "api-secret");
2076
2522
  }
2077
2523
  server = null;
2078
2524
  actualPort = 0;
@@ -2086,7 +2532,7 @@ var ApiServer = class {
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,27 +2563,27 @@ 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);
2127
2573
  } catch {
2128
2574
  }
2129
2575
  }
2130
2576
  loadOrCreateSecret() {
2131
- const dir = path5.dirname(this.secretFilePath);
2132
- fs4.mkdirSync(dir, { recursive: true });
2577
+ const dir = path6.dirname(this.secretFilePath);
2578
+ fs6.mkdirSync(dir, { recursive: true });
2133
2579
  try {
2134
- this.secret = fs4.readFileSync(this.secretFilePath, "utf-8").trim();
2580
+ this.secret = fs6.readFileSync(this.secretFilePath, "utf-8").trim();
2135
2581
  if (this.secret) {
2136
2582
  try {
2137
- const stat = fs4.statSync(this.secretFilePath);
2583
+ const stat = fs6.statSync(this.secretFilePath);
2138
2584
  const mode = stat.mode & 511;
2139
2585
  if (mode & 63) {
2140
- log6.warn({ path: this.secretFilePath, mode: "0" + mode.toString(8) }, "API secret file has insecure permissions (should be 0600). Run: chmod 600 %s", this.secretFilePath);
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);
2141
2587
  }
2142
2588
  } catch {
2143
2589
  }
@@ -2146,7 +2592,7 @@ var ApiServer = class {
2146
2592
  } catch {
2147
2593
  }
2148
2594
  this.secret = crypto.randomBytes(32).toString("hex");
2149
- fs4.writeFileSync(this.secretFilePath, this.secret, { mode: 384 });
2595
+ fs6.writeFileSync(this.secretFilePath, this.secret, { mode: 384 });
2150
2596
  }
2151
2597
  authenticate(req) {
2152
2598
  const authHeader = req.headers.authorization;
@@ -2177,6 +2623,9 @@ var ApiServer = class {
2177
2623
  } else if (method === "GET" && url.match(/^\/api\/sessions\/([^/]+)$/)) {
2178
2624
  const sessionId = decodeURIComponent(url.match(/^\/api\/sessions\/([^/]+)$/)[1]);
2179
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);
2180
2629
  } else if (method === "DELETE" && url.match(/^\/api\/sessions\/([^/]+)$/)) {
2181
2630
  const sessionId = decodeURIComponent(url.match(/^\/api\/sessions\/([^/]+)$/)[1]);
2182
2631
  await this.handleCancelSession(sessionId, res);
@@ -2222,7 +2671,7 @@ var ApiServer = class {
2222
2671
  this.sendJson(res, 404, { error: "Not found" });
2223
2672
  }
2224
2673
  } catch (err) {
2225
- log6.error({ err }, "API request error");
2674
+ log7.error({ err }, "API request error");
2226
2675
  this.sendJson(res, 500, { error: "Internal server error" });
2227
2676
  }
2228
2677
  }
@@ -2264,11 +2713,11 @@ var ApiServer = class {
2264
2713
  if (!adapter) {
2265
2714
  session.agentInstance.onPermissionRequest = async (request) => {
2266
2715
  const allowOption = request.options.find((o) => o.isAllow);
2267
- 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");
2268
2717
  return allowOption?.id ?? request.options[0]?.id ?? "";
2269
2718
  };
2270
2719
  }
2271
- 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"));
2272
2721
  this.sendJson(res, 200, {
2273
2722
  sessionId: session.id,
2274
2723
  agent: session.agentName,
@@ -2379,7 +2828,7 @@ var ApiServer = class {
2379
2828
  this.sendJson(res, 200, { version: getVersion() });
2380
2829
  }
2381
2830
  async handleGetEditableConfig(res) {
2382
- 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");
2383
2832
  const config = this.core.configManager.get();
2384
2833
  const safeFields = getSafeFields2();
2385
2834
  const fields = safeFields.map((def) => ({
@@ -2420,20 +2869,20 @@ var ApiServer = class {
2420
2869
  const parts = configPath.split(".");
2421
2870
  let target = cloned;
2422
2871
  for (let i = 0; i < parts.length - 1; i++) {
2423
- if (target[parts[i]] && typeof target[parts[i]] === "object" && !Array.isArray(target[parts[i]])) {
2424
- 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];
2425
2878
  } else {
2426
2879
  this.sendJson(res, 400, { error: "Invalid config path" });
2427
2880
  return;
2428
2881
  }
2429
2882
  }
2430
2883
  const lastKey = parts[parts.length - 1];
2431
- if (!(lastKey in target)) {
2432
- this.sendJson(res, 400, { error: "Invalid config path" });
2433
- return;
2434
- }
2435
2884
  target[lastKey] = value;
2436
- const { ConfigSchema } = await import("./config-KF2MQWAP.js");
2885
+ const { ConfigSchema } = await import("./config-AK2W3E67.js");
2437
2886
  const result = ConfigSchema.safeParse(cloned);
2438
2887
  if (!result.success) {
2439
2888
  this.sendJson(res, 400, {
@@ -2450,7 +2899,7 @@ var ApiServer = class {
2450
2899
  }
2451
2900
  updateTarget[lastKey] = value;
2452
2901
  await this.core.configManager.save(updates, configPath);
2453
- const { isHotReloadable: isHotReloadable2 } = await import("./config-registry-SNKA2EH2.js");
2902
+ const { isHotReloadable: isHotReloadable2 } = await import("./config-registry-QQOJ2GQP.js");
2454
2903
  const needsRestart = !isHotReloadable2(configPath);
2455
2904
  this.sendJson(res, 200, {
2456
2905
  ok: true,
@@ -2558,6 +3007,14 @@ var ApiServer = class {
2558
3007
  this.sendJson(res, 200, { ok: true, message: "Restarting..." });
2559
3008
  setImmediate(() => this.core.requestRestart());
2560
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
+ }
2561
3018
  async handleCancelSession(sessionId, res) {
2562
3019
  const session = this.core.sessionManager.getSession(sessionId);
2563
3020
  if (!session) {
@@ -2673,7 +3130,7 @@ var ApiServer = class {
2673
3130
  };
2674
3131
 
2675
3132
  // src/core/topic-manager.ts
2676
- var log7 = createChildLogger({ module: "topic-manager" });
3133
+ var log8 = createChildLogger({ module: "topic-manager" });
2677
3134
  var TopicManager = class {
2678
3135
  constructor(sessionManager, adapter, systemTopicIds) {
2679
3136
  this.sessionManager = sessionManager;
@@ -2712,7 +3169,7 @@ var TopicManager = class {
2712
3169
  try {
2713
3170
  await this.adapter.deleteSessionThread(sessionId);
2714
3171
  } catch (err) {
2715
- 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");
2716
3173
  }
2717
3174
  }
2718
3175
  await this.sessionManager.removeRecord(sessionId);
@@ -2735,7 +3192,7 @@ var TopicManager = class {
2735
3192
  try {
2736
3193
  await this.adapter.deleteSessionThread(record.sessionId);
2737
3194
  } catch (err) {
2738
- 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");
2739
3196
  }
2740
3197
  }
2741
3198
  await this.sessionManager.removeRecord(record.sessionId);
@@ -2782,6 +3239,9 @@ async function renameSessionTopic(bot, chatId, threadId, name) {
2782
3239
  } catch {
2783
3240
  }
2784
3241
  }
3242
+ async function deleteSessionTopic(bot, chatId, threadId) {
3243
+ await bot.api.deleteForumTopic(chatId, threadId);
3244
+ }
2785
3245
  function buildDeepLink(chatId, messageId) {
2786
3246
  const cleanId = String(chatId).replace("-100", "");
2787
3247
  return `https://t.me/c/${cleanId}/${messageId}`;
@@ -2920,6 +3380,33 @@ function formatUsage(usage) {
2920
3380
  return `${emoji} ${formatTokens(tokensUsed)} / ${formatTokens(contextSize)} tokens
2921
3381
  ${bar} ${pct}%`;
2922
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
+ }
2923
3410
  function splitMessage(text, maxLength = 3800) {
2924
3411
  if (text.length <= maxLength) return [text];
2925
3412
  const chunks = [];
@@ -2955,7 +3442,7 @@ function splitMessage(text, maxLength = 3800) {
2955
3442
 
2956
3443
  // src/adapters/telegram/commands/admin.ts
2957
3444
  import { InlineKeyboard } from "grammy";
2958
- var log9 = createChildLogger({ module: "telegram-cmd-admin" });
3445
+ var log10 = createChildLogger({ module: "telegram-cmd-admin" });
2959
3446
  function buildDangerousModeKeyboard(sessionId, enabled) {
2960
3447
  return new InlineKeyboard().text(
2961
3448
  enabled ? "\u{1F510} Disable Dangerous Mode" : "\u2620\uFE0F Enable Dangerous Mode",
@@ -2968,7 +3455,7 @@ function setupDangerousModeCallbacks(bot, core) {
2968
3455
  const session = core.sessionManager.getSession(sessionId);
2969
3456
  if (session) {
2970
3457
  session.dangerousMode = !session.dangerousMode;
2971
- log9.info({ sessionId, dangerousMode: session.dangerousMode }, "Dangerous mode toggled via button");
3458
+ log10.info({ sessionId, dangerousMode: session.dangerousMode }, "Dangerous mode toggled via button");
2972
3459
  core.sessionManager.patchRecord(sessionId, { dangerousMode: session.dangerousMode }).catch(() => {
2973
3460
  });
2974
3461
  const toastText2 = session.dangerousMode ? "\u2620\uFE0F Dangerous mode enabled \u2014 permissions auto-approved" : "\u{1F510} Dangerous mode disabled \u2014 permissions shown normally";
@@ -2995,7 +3482,7 @@ function setupDangerousModeCallbacks(bot, core) {
2995
3482
  const newDangerousMode = !(record.dangerousMode ?? false);
2996
3483
  core.sessionManager.patchRecord(sessionId, { dangerousMode: newDangerousMode }).catch(() => {
2997
3484
  });
2998
- 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)");
2999
3486
  const toastText = newDangerousMode ? "\u2620\uFE0F Dangerous mode enabled \u2014 permissions auto-approved" : "\u{1F510} Dangerous mode disabled \u2014 permissions shown normally";
3000
3487
  try {
3001
3488
  await ctx.answerCallbackQuery({ text: toastText });
@@ -3124,7 +3611,7 @@ async function handleRestart(ctx, core) {
3124
3611
  }
3125
3612
 
3126
3613
  // src/adapters/telegram/commands/new-session.ts
3127
- var log10 = createChildLogger({ module: "telegram-cmd-new-session" });
3614
+ var log11 = createChildLogger({ module: "telegram-cmd-new-session" });
3128
3615
  var pendingNewSessions = /* @__PURE__ */ new Map();
3129
3616
  var PENDING_TIMEOUT_MS = 5 * 60 * 1e3;
3130
3617
  function cleanupPending(userId) {
@@ -3223,7 +3710,7 @@ async function startConfirmStep(ctx, chatId, userId, agentName, workspace) {
3223
3710
  });
3224
3711
  }
3225
3712
  async function createSessionDirect(ctx, core, chatId, agentName, workspace) {
3226
- 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)");
3227
3714
  let threadId;
3228
3715
  try {
3229
3716
  const topicName = `\u{1F504} New Session`;
@@ -3253,10 +3740,10 @@ This is your coding session \u2014 chat here to work with the agent.`,
3253
3740
  reply_markup: buildDangerousModeKeyboard(session.id, false)
3254
3741
  }
3255
3742
  );
3256
- session.warmup().catch((err) => log10.error({ err }, "Warm-up error"));
3743
+ session.warmup().catch((err) => log11.error({ err }, "Warm-up error"));
3257
3744
  return threadId ?? null;
3258
3745
  } catch (err) {
3259
- log10.error({ err }, "Session creation failed");
3746
+ log11.error({ err }, "Session creation failed");
3260
3747
  if (threadId) {
3261
3748
  try {
3262
3749
  await ctx.api.deleteForumTopic(chatId, threadId);
@@ -3334,7 +3821,7 @@ async function handleNewChat(ctx, core, chatId) {
3334
3821
  reply_markup: buildDangerousModeKeyboard(session.id, false)
3335
3822
  }
3336
3823
  );
3337
- session.warmup().catch((err) => log10.error({ err }, "Warm-up error"));
3824
+ session.warmup().catch((err) => log11.error({ err }, "Warm-up error"));
3338
3825
  } catch (err) {
3339
3826
  if (newThreadId) {
3340
3827
  try {
@@ -3365,7 +3852,7 @@ async function executeNewSession(bot, core, chatId, agentName, workspace) {
3365
3852
  } });
3366
3853
  const finalName = `\u{1F504} ${session.agentName} \u2014 New Session`;
3367
3854
  await renameSessionTopic(bot, chatId, threadId, finalName);
3368
- session.warmup().catch((err) => log10.error({ err }, "Warm-up error"));
3855
+ session.warmup().catch((err) => log11.error({ err }, "Warm-up error"));
3369
3856
  return { session, threadId, firstMsgId };
3370
3857
  } catch (err) {
3371
3858
  try {
@@ -3513,7 +4000,7 @@ Or just the folder name like <code>my-project</code> (will use ${core.configMana
3513
4000
 
3514
4001
  // src/adapters/telegram/commands/session.ts
3515
4002
  import { InlineKeyboard as InlineKeyboard3 } from "grammy";
3516
- var log11 = createChildLogger({ module: "telegram-cmd-session" });
4003
+ var log12 = createChildLogger({ module: "telegram-cmd-session" });
3517
4004
  async function handleCancel(ctx, core, assistant) {
3518
4005
  const threadId = ctx.message?.message_thread_id;
3519
4006
  if (!threadId) return;
@@ -3531,14 +4018,14 @@ async function handleCancel(ctx, core, assistant) {
3531
4018
  String(threadId)
3532
4019
  );
3533
4020
  if (session) {
3534
- log11.info({ sessionId: session.id }, "Abort prompt command");
4021
+ log12.info({ sessionId: session.id }, "Abort prompt command");
3535
4022
  await session.abortPrompt();
3536
4023
  await ctx.reply("\u26D4 Prompt aborted. Session is still active \u2014 send a new message to continue.", { parse_mode: "HTML" });
3537
4024
  return;
3538
4025
  }
3539
4026
  const record = core.sessionManager.getRecordByThread("telegram", String(threadId));
3540
4027
  if (record && record.status !== "error") {
3541
- 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");
3542
4029
  await ctx.reply("\u2139\uFE0F No active prompt to cancel. Send a new message to resume the session.", { parse_mode: "HTML" });
3543
4030
  }
3544
4031
  }
@@ -3642,7 +4129,7 @@ ${lines.join("\n")}${truncated}`,
3642
4129
  { parse_mode: "HTML", reply_markup: keyboard }
3643
4130
  );
3644
4131
  } catch (err) {
3645
- log11.error({ err }, "handleTopics error");
4132
+ log12.error({ err }, "handleTopics error");
3646
4133
  await ctx.reply("\u274C Failed to list sessions.", { parse_mode: "HTML" }).catch(() => {
3647
4134
  });
3648
4135
  }
@@ -3663,13 +4150,13 @@ async function handleCleanup(ctx, core, chatId, statuses) {
3663
4150
  try {
3664
4151
  await ctx.api.deleteForumTopic(chatId, topicId);
3665
4152
  } catch (err) {
3666
- 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");
3667
4154
  }
3668
4155
  }
3669
4156
  await core.sessionManager.removeRecord(record.sessionId);
3670
4157
  deleted++;
3671
4158
  } catch (err) {
3672
- log11.error({ err, sessionId: record.sessionId }, "Failed to cleanup session");
4159
+ log12.error({ err, sessionId: record.sessionId }, "Failed to cleanup session");
3673
4160
  failed++;
3674
4161
  }
3675
4162
  }
@@ -3740,7 +4227,7 @@ async function handleCleanupEverythingConfirmed(ctx, core, chatId, systemTopicId
3740
4227
  try {
3741
4228
  await core.sessionManager.cancelSession(record.sessionId);
3742
4229
  } catch (err) {
3743
- 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");
3744
4231
  }
3745
4232
  }
3746
4233
  const topicId = record.platform?.topicId;
@@ -3748,13 +4235,13 @@ async function handleCleanupEverythingConfirmed(ctx, core, chatId, systemTopicId
3748
4235
  try {
3749
4236
  await ctx.api.deleteForumTopic(chatId, topicId);
3750
4237
  } catch (err) {
3751
- 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");
3752
4239
  }
3753
4240
  }
3754
4241
  await core.sessionManager.removeRecord(record.sessionId);
3755
4242
  deleted++;
3756
4243
  } catch (err) {
3757
- log11.error({ err, sessionId: record.sessionId }, "Failed to cleanup session");
4244
+ log12.error({ err, sessionId: record.sessionId }, "Failed to cleanup session");
3758
4245
  failed++;
3759
4246
  }
3760
4247
  }
@@ -3796,6 +4283,86 @@ function setupSessionCallbacks(bot, core, chatId, systemTopicIds) {
3796
4283
  }
3797
4284
  });
3798
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
+ }
3799
4366
 
3800
4367
  // src/adapters/telegram/commands/agents.ts
3801
4368
  import { InlineKeyboard as InlineKeyboard4 } from "grammy";
@@ -4186,7 +4753,7 @@ ${resultText}`,
4186
4753
 
4187
4754
  // src/adapters/telegram/commands/settings.ts
4188
4755
  import { InlineKeyboard as InlineKeyboard6 } from "grammy";
4189
- var log12 = createChildLogger({ module: "telegram-settings" });
4756
+ var log13 = createChildLogger({ module: "telegram-settings" });
4190
4757
  function buildSettingsKeyboard(core) {
4191
4758
  const config = core.configManager.get();
4192
4759
  const fields = getSafeFields();
@@ -4212,13 +4779,15 @@ function formatFieldLabel(field, value) {
4212
4779
  tunnel: "\u{1F517}",
4213
4780
  security: "\u{1F512}",
4214
4781
  workspace: "\u{1F4C1}",
4215
- storage: "\u{1F4BE}"
4782
+ storage: "\u{1F4BE}",
4783
+ speech: "\u{1F3A4}"
4216
4784
  };
4217
4785
  const icon = icons[field.group] ?? "\u2699\uFE0F";
4218
4786
  if (field.type === "toggle") {
4219
4787
  return `${icon} ${field.displayName}: ${value ? "ON" : "OFF"}`;
4220
4788
  }
4221
- return `${icon} ${field.displayName}: ${String(value)}`;
4789
+ const displayValue = value === null || value === void 0 ? "Not set" : String(value);
4790
+ return `${icon} ${field.displayName}: ${displayValue}`;
4222
4791
  }
4223
4792
  async function handleSettings(ctx, core) {
4224
4793
  const kb = buildSettingsKeyboard(core);
@@ -4247,7 +4816,7 @@ function setupSettingsCallbacks(bot, core, getAssistantSession) {
4247
4816
  } catch {
4248
4817
  }
4249
4818
  } catch (err) {
4250
- log12.error({ err, fieldPath }, "Failed to toggle config");
4819
+ log13.error({ err, fieldPath }, "Failed to toggle config");
4251
4820
  try {
4252
4821
  await ctx.answerCallbackQuery({ text: "\u274C Failed to update" });
4253
4822
  } catch {
@@ -4285,6 +4854,27 @@ Select a value:`, {
4285
4854
  const fieldPath = parts.slice(0, -1).join(":");
4286
4855
  const newValue = parts[parts.length - 1];
4287
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
+ }
4288
4878
  const updates = buildNestedUpdate(fieldPath, newValue);
4289
4879
  await core.configManager.save(updates, fieldPath);
4290
4880
  try {
@@ -4300,7 +4890,7 @@ Tap to change:`, {
4300
4890
  } catch {
4301
4891
  }
4302
4892
  } catch (err) {
4303
- log12.error({ err, fieldPath }, "Failed to set config");
4893
+ log13.error({ err, fieldPath }, "Failed to set config");
4304
4894
  try {
4305
4895
  await ctx.answerCallbackQuery({ text: "\u274C Failed to update" });
4306
4896
  } catch {
@@ -4333,7 +4923,7 @@ Tap to change:`, {
4333
4923
  await ctx.answerCallbackQuery();
4334
4924
  } catch {
4335
4925
  }
4336
- const { buildMenuKeyboard: buildMenuKeyboard3 } = await import("./menu-J5YVH665.js");
4926
+ const { buildMenuKeyboard: buildMenuKeyboard3 } = await import("./menu-XR2GET2B.js");
4337
4927
  try {
4338
4928
  await ctx.editMessageText(`<b>OpenACP Menu</b>
4339
4929
  Choose an action:`, {
@@ -4372,7 +4962,7 @@ function buildNestedUpdate(dotPath, value) {
4372
4962
 
4373
4963
  // src/adapters/telegram/commands/doctor.ts
4374
4964
  import { InlineKeyboard as InlineKeyboard7 } from "grammy";
4375
- var log13 = createChildLogger({ module: "telegram-cmd-doctor" });
4965
+ var log14 = createChildLogger({ module: "telegram-cmd-doctor" });
4376
4966
  var pendingFixesStore = /* @__PURE__ */ new Map();
4377
4967
  function renderReport(report) {
4378
4968
  const icons = { pass: "\u2705", warn: "\u26A0\uFE0F", fail: "\u274C" };
@@ -4415,7 +5005,7 @@ async function handleDoctor(ctx) {
4415
5005
  reply_markup: keyboard
4416
5006
  });
4417
5007
  } catch (err) {
4418
- log13.error({ err }, "Doctor command failed");
5008
+ log14.error({ err }, "Doctor command failed");
4419
5009
  await ctx.api.editMessageText(
4420
5010
  ctx.chat.id,
4421
5011
  statusMsg.message_id,
@@ -4464,7 +5054,7 @@ function setupDoctorCallbacks(bot) {
4464
5054
  }
4465
5055
  }
4466
5056
  } catch (err) {
4467
- log13.error({ err, index }, "Doctor fix callback failed");
5057
+ log14.error({ err, index }, "Doctor fix callback failed");
4468
5058
  }
4469
5059
  });
4470
5060
  bot.callbackQuery("m:doctor", async (ctx) => {
@@ -4478,7 +5068,7 @@ function setupDoctorCallbacks(bot) {
4478
5068
 
4479
5069
  // src/adapters/telegram/commands/tunnel.ts
4480
5070
  import { InlineKeyboard as InlineKeyboard8 } from "grammy";
4481
- var log14 = createChildLogger({ module: "telegram-cmd-tunnel" });
5071
+ var log15 = createChildLogger({ module: "telegram-cmd-tunnel" });
4482
5072
  async function handleTunnel(ctx, core) {
4483
5073
  if (!core.tunnelService) {
4484
5074
  await ctx.reply("\u274C Tunnel service is not enabled.", { parse_mode: "HTML" });
@@ -4543,12 +5133,19 @@ async function handleTunnels(ctx, core) {
4543
5133
  await ctx.reply("\u274C Tunnel service is not enabled.", { parse_mode: "HTML" });
4544
5134
  return;
4545
5135
  }
4546
- 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
+ }
4547
5146
  if (entries.length === 0) {
4548
- await ctx.reply(
4549
- "No active tunnels.\n\nUse <code>/tunnel &lt;port&gt;</code> to create one.",
4550
- { parse_mode: "HTML" }
4551
- );
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" });
4552
5149
  return;
4553
5150
  }
4554
5151
  const lines = entries.map((e) => {
@@ -4643,8 +5240,10 @@ function setupCommands(bot, core, chatId, assistant) {
4643
5240
  bot.command("integrate", (ctx) => handleIntegrate(ctx, core));
4644
5241
  bot.command("clear", (ctx) => handleClear(ctx, assistant));
4645
5242
  bot.command("doctor", (ctx) => handleDoctor(ctx));
5243
+ bot.command("usage", (ctx) => handleUsage(ctx, core));
4646
5244
  bot.command("tunnel", (ctx) => handleTunnel(ctx, core));
4647
5245
  bot.command("tunnels", (ctx) => handleTunnels(ctx, core));
5246
+ bot.command("archive", (ctx) => handleArchive(ctx, core));
4648
5247
  }
4649
5248
  function setupAllCallbacks(bot, core, chatId, systemTopicIds, getAssistantSession) {
4650
5249
  setupNewSessionCallbacks(bot, core, chatId);
@@ -4658,6 +5257,7 @@ function setupAllCallbacks(bot, core, chatId, systemTopicIds, getAssistantSessio
4658
5257
  await ctx.answerCallbackQuery();
4659
5258
  await createSessionDirect(ctx, core, chatId, agentKey, core.configManager.get().workspace.baseDir);
4660
5259
  });
5260
+ bot.callbackQuery(/^ar:/, (ctx) => handleArchiveConfirm(ctx, core, chatId));
4661
5261
  bot.callbackQuery(/^m:/, async (ctx) => {
4662
5262
  const data = ctx.callbackQuery.data;
4663
5263
  try {
@@ -4713,14 +5313,16 @@ var STATIC_COMMANDS = [
4713
5313
  { command: "restart", description: "Restart OpenACP" },
4714
5314
  { command: "update", description: "Update to latest version and restart" },
4715
5315
  { command: "doctor", description: "Run system diagnostics" },
5316
+ { command: "usage", description: "View token usage and cost report" },
4716
5317
  { command: "tunnel", description: "Create/stop tunnel for a local port" },
4717
- { command: "tunnels", description: "List active tunnels" }
5318
+ { command: "tunnels", description: "List active tunnels" },
5319
+ { command: "archive", description: "Archive session topic (recreate with clean history)" }
4718
5320
  ];
4719
5321
 
4720
5322
  // src/adapters/telegram/permissions.ts
4721
5323
  import { InlineKeyboard as InlineKeyboard9 } from "grammy";
4722
- import { nanoid as nanoid2 } from "nanoid";
4723
- var log15 = createChildLogger({ module: "telegram-permissions" });
5324
+ import { nanoid as nanoid3 } from "nanoid";
5325
+ var log16 = createChildLogger({ module: "telegram-permissions" });
4724
5326
  var PermissionHandler = class {
4725
5327
  constructor(bot, chatId, getSession, sendNotification) {
4726
5328
  this.bot = bot;
@@ -4731,7 +5333,7 @@ var PermissionHandler = class {
4731
5333
  pending = /* @__PURE__ */ new Map();
4732
5334
  async sendPermissionRequest(session, request) {
4733
5335
  const threadId = Number(session.threadId);
4734
- const callbackKey = nanoid2(8);
5336
+ const callbackKey = nanoid3(8);
4735
5337
  this.pending.set(callbackKey, {
4736
5338
  sessionId: session.id,
4737
5339
  requestId: request.id,
@@ -4780,7 +5382,7 @@ ${escapeHtml(request.description)}`,
4780
5382
  }
4781
5383
  const session = this.getSession(pending.sessionId);
4782
5384
  const isAllow = pending.options.find((o) => o.id === optionId)?.isAllow ?? false;
4783
- log15.info({ requestId: pending.requestId, optionId, isAllow }, "Permission responded");
5385
+ log16.info({ requestId: pending.requestId, optionId, isAllow }, "Permission responded");
4784
5386
  if (session?.permissionGate.requestId === pending.requestId) {
4785
5387
  session.permissionGate.resolve(optionId);
4786
5388
  }
@@ -4798,10 +5400,10 @@ ${escapeHtml(request.description)}`,
4798
5400
  };
4799
5401
 
4800
5402
  // src/adapters/telegram/assistant.ts
4801
- var log16 = createChildLogger({ module: "telegram-assistant" });
5403
+ var log17 = createChildLogger({ module: "telegram-assistant" });
4802
5404
  async function spawnAssistant(core, adapter, assistantTopicId) {
4803
5405
  const config = core.configManager.get();
4804
- log16.info({ agent: config.defaultAgent }, "Creating assistant session...");
5406
+ log17.info({ agent: config.defaultAgent }, "Creating assistant session...");
4805
5407
  const session = await core.createSession({
4806
5408
  channelId: "telegram",
4807
5409
  agentName: config.defaultAgent,
@@ -4810,7 +5412,7 @@ async function spawnAssistant(core, adapter, assistantTopicId) {
4810
5412
  // Prevent auto-naming from triggering after system prompt
4811
5413
  });
4812
5414
  session.threadId = String(assistantTopicId);
4813
- log16.info({ sessionId: session.id }, "Assistant agent spawned");
5415
+ log17.info({ sessionId: session.id }, "Assistant agent spawned");
4814
5416
  const allRecords = core.sessionManager.listRecords();
4815
5417
  const activeCount = allRecords.filter((r) => r.status === "active" || r.status === "initializing").length;
4816
5418
  const statusCounts = /* @__PURE__ */ new Map();
@@ -4831,9 +5433,9 @@ async function spawnAssistant(core, adapter, assistantTopicId) {
4831
5433
  };
4832
5434
  const systemPrompt = buildAssistantSystemPrompt(ctx);
4833
5435
  const ready = session.enqueuePrompt(systemPrompt).then(() => {
4834
- log16.info({ sessionId: session.id }, "Assistant system prompt completed");
5436
+ log17.info({ sessionId: session.id }, "Assistant system prompt completed");
4835
5437
  }).catch((err) => {
4836
- log16.warn({ err }, "Assistant system prompt failed");
5438
+ log17.warn({ err }, "Assistant system prompt failed");
4837
5439
  });
4838
5440
  return { session, ready };
4839
5441
  }
@@ -4871,6 +5473,7 @@ function buildAssistantSystemPrompt(ctx) {
4871
5473
  - Available in ACP Registry: ${availableAgentCount ?? "28+"} more agents (use /agents to browse)
4872
5474
  - Default agent: ${config.defaultAgent}
4873
5475
  - Workspace base directory: ${config.workspace.baseDir}
5476
+ - STT: ${config.speech?.stt?.provider ? `${config.speech.stt.provider} \u2705` : "Not configured"}
4874
5477
 
4875
5478
  ## Action Playbook
4876
5479
 
@@ -4918,6 +5521,16 @@ function buildAssistantSystemPrompt(ctx) {
4918
5521
  - When user asks about "settings" or "config", use \`openacp config set\` directly
4919
5522
  - When receiving a delegated request from the Settings menu, ask user for the new value, then apply with \`openacp config set <path> <value>\`
4920
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
+
4921
5534
  ### Restart / Update
4922
5535
  - Always ask for confirmation \u2014 these are disruptive actions
4923
5536
  - Guide user: "Tap \u{1F504} Restart button or type /restart"
@@ -4988,7 +5601,7 @@ function redirectToAssistant(chatId, assistantTopicId) {
4988
5601
  }
4989
5602
 
4990
5603
  // src/adapters/telegram/activity.ts
4991
- var log17 = createChildLogger({ module: "telegram:activity" });
5604
+ var log18 = createChildLogger({ module: "telegram:activity" });
4992
5605
  var THINKING_REFRESH_MS = 15e3;
4993
5606
  var THINKING_MAX_MS = 3 * 60 * 1e3;
4994
5607
  var ThinkingIndicator = class {
@@ -5020,7 +5633,7 @@ var ThinkingIndicator = class {
5020
5633
  this.startRefreshTimer();
5021
5634
  }
5022
5635
  } catch (err) {
5023
- log17.warn({ err }, "ThinkingIndicator.show() failed");
5636
+ log18.warn({ err }, "ThinkingIndicator.show() failed");
5024
5637
  } finally {
5025
5638
  this.sending = false;
5026
5639
  }
@@ -5093,7 +5706,7 @@ var UsageMessage = class {
5093
5706
  if (result) this.msgId = result.message_id;
5094
5707
  }
5095
5708
  } catch (err) {
5096
- log17.warn({ err }, "UsageMessage.send() failed");
5709
+ log18.warn({ err }, "UsageMessage.send() failed");
5097
5710
  }
5098
5711
  }
5099
5712
  getMsgId() {
@@ -5106,7 +5719,7 @@ var UsageMessage = class {
5106
5719
  try {
5107
5720
  await this.sendQueue.enqueue(() => this.api.deleteMessage(this.chatId, id));
5108
5721
  } catch (err) {
5109
- log17.warn({ err }, "UsageMessage.delete() failed");
5722
+ log18.warn({ err }, "UsageMessage.delete() failed");
5110
5723
  }
5111
5724
  }
5112
5725
  };
@@ -5192,7 +5805,7 @@ var PlanCard = class {
5192
5805
  if (result) this.msgId = result.message_id;
5193
5806
  }
5194
5807
  } catch (err) {
5195
- log17.warn({ err }, "PlanCard flush failed");
5808
+ log18.warn({ err }, "PlanCard flush failed");
5196
5809
  }
5197
5810
  }
5198
5811
  };
@@ -5255,7 +5868,7 @@ var ActivityTracker = class {
5255
5868
  })
5256
5869
  );
5257
5870
  } catch (err) {
5258
- log17.warn({ err }, "ActivityTracker.onComplete() Done send failed");
5871
+ log18.warn({ err }, "ActivityTracker.onComplete() Done send failed");
5259
5872
  }
5260
5873
  }
5261
5874
  }
@@ -5337,7 +5950,7 @@ var TelegramSendQueue = class {
5337
5950
  };
5338
5951
 
5339
5952
  // src/adapters/telegram/action-detect.ts
5340
- import { nanoid as nanoid3 } from "nanoid";
5953
+ import { nanoid as nanoid4 } from "nanoid";
5341
5954
  import { InlineKeyboard as InlineKeyboard10 } from "grammy";
5342
5955
  var CMD_NEW_RE = /\/new(?:\s+([^\s\u0080-\uFFFF]+)(?:\s+([^\s\u0080-\uFFFF]+))?)?/;
5343
5956
  var CMD_CANCEL_RE = /\/cancel\b/;
@@ -5363,7 +5976,7 @@ function detectAction(text) {
5363
5976
  var ACTION_TTL_MS = 5 * 60 * 1e3;
5364
5977
  var actionMap = /* @__PURE__ */ new Map();
5365
5978
  function storeAction(action) {
5366
- const id = nanoid3(10);
5979
+ const id = nanoid4(10);
5367
5980
  actionMap.set(id, { action, createdAt: Date.now() });
5368
5981
  for (const [key, entry] of actionMap) {
5369
5982
  if (Date.now() - entry.createdAt > ACTION_TTL_MS) {
@@ -5493,7 +6106,7 @@ function setupActionCallbacks(bot, core, chatId, getAssistantSessionId) {
5493
6106
  }
5494
6107
 
5495
6108
  // src/adapters/telegram/tool-call-tracker.ts
5496
- var log18 = createChildLogger({ module: "tool-call-tracker" });
6109
+ var log19 = createChildLogger({ module: "tool-call-tracker" });
5497
6110
  var ToolCallTracker = class {
5498
6111
  constructor(bot, chatId, sendQueue) {
5499
6112
  this.bot = bot;
@@ -5537,7 +6150,7 @@ var ToolCallTracker = class {
5537
6150
  if (!toolState) return;
5538
6151
  if (meta.viewerLinks) {
5539
6152
  toolState.viewerLinks = meta.viewerLinks;
5540
- log18.debug({ toolId: meta.id, viewerLinks: meta.viewerLinks }, "Accumulated viewerLinks");
6153
+ log19.debug({ toolId: meta.id, viewerLinks: meta.viewerLinks }, "Accumulated viewerLinks");
5541
6154
  }
5542
6155
  if (meta.viewerFilePath) toolState.viewerFilePath = meta.viewerFilePath;
5543
6156
  if (meta.name) toolState.name = meta.name;
@@ -5545,7 +6158,7 @@ var ToolCallTracker = class {
5545
6158
  const isTerminal = meta.status === "completed" || meta.status === "failed";
5546
6159
  if (!isTerminal) return;
5547
6160
  await toolState.ready;
5548
- log18.debug(
6161
+ log19.debug(
5549
6162
  {
5550
6163
  toolId: meta.id,
5551
6164
  status: meta.status,
@@ -5574,7 +6187,7 @@ var ToolCallTracker = class {
5574
6187
  )
5575
6188
  );
5576
6189
  } catch (err) {
5577
- log18.warn(
6190
+ log19.warn(
5578
6191
  {
5579
6192
  err,
5580
6193
  msgId: toolState.msgId,
@@ -5824,7 +6437,7 @@ var DraftManager = class {
5824
6437
  };
5825
6438
 
5826
6439
  // src/adapters/telegram/skill-command-manager.ts
5827
- var log19 = createChildLogger({ module: "skill-commands" });
6440
+ var log20 = createChildLogger({ module: "skill-commands" });
5828
6441
  var SkillCommandManager = class {
5829
6442
  // sessionId → pinned msgId
5830
6443
  constructor(bot, chatId, sendQueue, sessionManager) {
@@ -5890,7 +6503,7 @@ var SkillCommandManager = class {
5890
6503
  disable_notification: true
5891
6504
  });
5892
6505
  } catch (err) {
5893
- log19.error({ err, sessionId }, "Failed to send skill commands");
6506
+ log20.error({ err, sessionId }, "Failed to send skill commands");
5894
6507
  }
5895
6508
  }
5896
6509
  async cleanup(sessionId) {
@@ -5916,7 +6529,7 @@ var SkillCommandManager = class {
5916
6529
  };
5917
6530
 
5918
6531
  // src/adapters/telegram/adapter.ts
5919
- var log20 = createChildLogger({ module: "telegram" });
6532
+ var log21 = createChildLogger({ module: "telegram" });
5920
6533
  function patchedFetch(input, init) {
5921
6534
  if (init?.signal && !(init.signal instanceof AbortSignal)) {
5922
6535
  const nativeController = new AbortController();
@@ -5975,7 +6588,7 @@ var TelegramAdapter = class extends ChannelAdapter {
5975
6588
  );
5976
6589
  this.bot.catch((err) => {
5977
6590
  const rootCause = err.error instanceof Error ? err.error : err;
5978
- log20.error({ err: rootCause }, "Telegram bot error");
6591
+ log21.error({ err: rootCause }, "Telegram bot error");
5979
6592
  });
5980
6593
  this.bot.api.config.use(async (prev, method, payload, signal) => {
5981
6594
  const maxRetries = 3;
@@ -5989,7 +6602,7 @@ var TelegramAdapter = class extends ChannelAdapter {
5989
6602
  if (rateLimitedMethods.includes(method)) {
5990
6603
  this.sendQueue.onRateLimited();
5991
6604
  }
5992
- log20.warn(
6605
+ log21.warn(
5993
6606
  { method, retryAfter, attempt: attempt + 1 },
5994
6607
  "Rate limited by Telegram, retrying"
5995
6608
  );
@@ -6121,7 +6734,7 @@ var TelegramAdapter = class extends ChannelAdapter {
6121
6734
  this.setupRoutes();
6122
6735
  this.bot.start({
6123
6736
  allowed_updates: ["message", "callback_query"],
6124
- onStart: () => log20.info(
6737
+ onStart: () => log21.info(
6125
6738
  { chatId: this.telegramConfig.chatId },
6126
6739
  "Telegram bot started"
6127
6740
  )
@@ -6143,10 +6756,10 @@ var TelegramAdapter = class extends ChannelAdapter {
6143
6756
  reply_markup: buildMenuKeyboard()
6144
6757
  });
6145
6758
  } catch (err) {
6146
- log20.warn({ err }, "Failed to send welcome message");
6759
+ log21.warn({ err }, "Failed to send welcome message");
6147
6760
  }
6148
6761
  try {
6149
- log20.info("Spawning assistant session...");
6762
+ log21.info("Spawning assistant session...");
6150
6763
  const { session, ready } = await spawnAssistant(
6151
6764
  this.core,
6152
6765
  this,
@@ -6154,13 +6767,13 @@ var TelegramAdapter = class extends ChannelAdapter {
6154
6767
  );
6155
6768
  this.assistantSession = session;
6156
6769
  this.assistantInitializing = true;
6157
- 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");
6158
6771
  ready.then(() => {
6159
6772
  this.assistantInitializing = false;
6160
- log20.info({ sessionId: session.id }, "Assistant ready for user messages");
6773
+ log21.info({ sessionId: session.id }, "Assistant ready for user messages");
6161
6774
  });
6162
6775
  } catch (err) {
6163
- log20.error({ err }, "Failed to spawn assistant");
6776
+ log21.error({ err }, "Failed to spawn assistant");
6164
6777
  this.bot.api.sendMessage(
6165
6778
  this.telegramConfig.chatId,
6166
6779
  `\u26A0\uFE0F <b>Failed to start assistant session.</b>
@@ -6176,7 +6789,7 @@ var TelegramAdapter = class extends ChannelAdapter {
6176
6789
  await this.assistantSession.destroy();
6177
6790
  }
6178
6791
  await this.bot.stop();
6179
- log20.info("Telegram bot stopped");
6792
+ log21.info("Telegram bot stopped");
6180
6793
  }
6181
6794
  setupRoutes() {
6182
6795
  this.bot.on("message:text", async (ctx) => {
@@ -6204,7 +6817,7 @@ var TelegramAdapter = class extends ChannelAdapter {
6204
6817
  ctx.replyWithChatAction("typing").catch(() => {
6205
6818
  });
6206
6819
  handleAssistantMessage(this.assistantSession, forwardText).catch(
6207
- (err) => log20.error({ err }, "Assistant error")
6820
+ (err) => log21.error({ err }, "Assistant error")
6208
6821
  );
6209
6822
  return;
6210
6823
  }
@@ -6221,7 +6834,7 @@ var TelegramAdapter = class extends ChannelAdapter {
6221
6834
  threadId: String(threadId),
6222
6835
  userId: String(ctx.from.id),
6223
6836
  text: forwardText
6224
- }).catch((err) => log20.error({ err }, "handleMessage error"));
6837
+ }).catch((err) => log21.error({ err }, "handleMessage error"));
6225
6838
  });
6226
6839
  this.bot.on("message:photo", async (ctx) => {
6227
6840
  const threadId = ctx.message.message_thread_id;
@@ -6296,9 +6909,10 @@ var TelegramAdapter = class extends ChannelAdapter {
6296
6909
  if (this.assistantInitializing && sessionId === this.assistantSession?.id) return;
6297
6910
  const session = this.core.sessionManager.getSession(sessionId);
6298
6911
  if (!session) return;
6912
+ if (session.archiving) return;
6299
6913
  const threadId = Number(session.threadId);
6300
6914
  if (!threadId || isNaN(threadId)) {
6301
- 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");
6302
6916
  return;
6303
6917
  }
6304
6918
  switch (content.type) {
@@ -6380,7 +6994,7 @@ Task completed.
6380
6994
  if (!content.attachment) break;
6381
6995
  const { attachment } = content;
6382
6996
  if (attachment.size > 50 * 1024 * 1024) {
6383
- 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)");
6384
6998
  await this.sendQueue.enqueue(
6385
6999
  () => this.bot.api.sendMessage(
6386
7000
  this.telegramConfig.chatId,
@@ -6412,7 +7026,7 @@ Task completed.
6412
7026
  );
6413
7027
  }
6414
7028
  } catch (err) {
6415
- log20.error({ err, sessionId, fileName: attachment.fileName }, "Failed to send attachment");
7029
+ log21.error({ err, sessionId, fileName: attachment.fileName }, "Failed to send attachment");
6416
7030
  }
6417
7031
  break;
6418
7032
  }
@@ -6461,16 +7075,30 @@ Task completed.
6461
7075
  );
6462
7076
  break;
6463
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
+ }
6464
7092
  }
6465
7093
  }
6466
7094
  async sendPermissionRequest(sessionId, request) {
6467
- log20.info({ sessionId, requestId: request.id }, "Permission request sent");
7095
+ log21.info({ sessionId, requestId: request.id }, "Permission request sent");
6468
7096
  const session = this.core.sessionManager.getSession(sessionId);
6469
7097
  if (!session) return;
6470
7098
  if (request.description.includes("openacp")) {
6471
7099
  const allowOption = request.options.find((o) => o.isAllow);
6472
7100
  if (allowOption && session.permissionGate.requestId === request.id) {
6473
- log20.info({ sessionId, requestId: request.id }, "Auto-approving openacp command");
7101
+ log21.info({ sessionId, requestId: request.id }, "Auto-approving openacp command");
6474
7102
  session.permissionGate.resolve(allowOption.id);
6475
7103
  }
6476
7104
  return;
@@ -6478,7 +7106,7 @@ Task completed.
6478
7106
  if (session.dangerousMode) {
6479
7107
  const allowOption = request.options.find((o) => o.isAllow);
6480
7108
  if (allowOption && session.permissionGate.requestId === request.id) {
6481
- 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");
6482
7110
  session.permissionGate.resolve(allowOption.id);
6483
7111
  }
6484
7112
  return;
@@ -6489,7 +7117,7 @@ Task completed.
6489
7117
  }
6490
7118
  async sendNotification(notification) {
6491
7119
  if (notification.sessionId === this.assistantSession?.id) return;
6492
- log20.info(
7120
+ log21.info(
6493
7121
  { sessionId: notification.sessionId, type: notification.type },
6494
7122
  "Notification sent"
6495
7123
  );
@@ -6525,7 +7153,7 @@ Task completed.
6525
7153
  );
6526
7154
  }
6527
7155
  async createSessionThread(sessionId, name) {
6528
- log20.info({ sessionId, name }, "Session topic created");
7156
+ log21.info({ sessionId, name }, "Session topic created");
6529
7157
  return String(
6530
7158
  await createSessionTopic(this.bot, this.telegramConfig.chatId, name)
6531
7159
  );
@@ -6549,7 +7177,7 @@ Task completed.
6549
7177
  try {
6550
7178
  await this.bot.api.deleteForumTopic(this.telegramConfig.chatId, topicId);
6551
7179
  } catch (err) {
6552
- 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)");
6553
7181
  }
6554
7182
  }
6555
7183
  async sendSkillCommands(sessionId, commands) {
@@ -6573,7 +7201,7 @@ Task completed.
6573
7201
  const buffer = Buffer.from(await response.arrayBuffer());
6574
7202
  return { buffer, filePath: file.file_path };
6575
7203
  } catch (err) {
6576
- log20.error({ err }, "Failed to download file from Telegram");
7204
+ log21.error({ err }, "Failed to download file from Telegram");
6577
7205
  return null;
6578
7206
  }
6579
7207
  }
@@ -6581,17 +7209,24 @@ Task completed.
6581
7209
  const downloaded = await this.downloadTelegramFile(fileId);
6582
7210
  if (!downloaded) return;
6583
7211
  let buffer = downloaded.buffer;
7212
+ let originalFilePath;
7213
+ const sessionId = this.resolveSessionId(threadId) || "unknown";
6584
7214
  if (convertOggToWav) {
7215
+ const oggAtt = await this.fileService.saveFile(sessionId, "voice.ogg", downloaded.buffer, "audio/ogg");
7216
+ originalFilePath = oggAtt.filePath;
6585
7217
  try {
6586
7218
  buffer = await this.fileService.convertOggToWav(buffer);
6587
7219
  } catch (err) {
6588
- log20.warn({ err }, "OGG\u2192WAV conversion failed, saving original OGG");
7220
+ log21.warn({ err }, "OGG\u2192WAV conversion failed, saving original OGG");
6589
7221
  fileName = "voice.ogg";
6590
7222
  mimeType = "audio/ogg";
7223
+ originalFilePath = void 0;
6591
7224
  }
6592
7225
  }
6593
- const sessionId = this.resolveSessionId(threadId) || "unknown";
6594
7226
  const att = await this.fileService.saveFile(sessionId, fileName, buffer, mimeType);
7227
+ if (originalFilePath) {
7228
+ att.originalFilePath = originalFilePath;
7229
+ }
6595
7230
  const rawText = caption || `[${att.type === "image" ? "Photo" : att.type === "audio" ? "Audio" : "File"}: ${att.fileName}]`;
6596
7231
  const text = rawText.startsWith("/") ? rawText.slice(1) : rawText;
6597
7232
  if (threadId === this.assistantTopicId) {
@@ -6608,11 +7243,52 @@ Task completed.
6608
7243
  userId: String(userId),
6609
7244
  text,
6610
7245
  attachments: [att]
6611
- }).catch((err) => log20.error({ err }, "handleMessage error"));
7246
+ }).catch((err) => log21.error({ err }, "handleMessage error"));
6612
7247
  }
6613
7248
  async cleanupSkillCommands(sessionId) {
6614
7249
  await this.skillManager.cleanup(sessionId);
6615
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
+ }
6616
7292
  };
6617
7293
 
6618
7294
  export {
@@ -6630,9 +7306,13 @@ export {
6630
7306
  SessionBridge,
6631
7307
  NotificationManager,
6632
7308
  MessageTransformer,
7309
+ UsageStore,
7310
+ UsageBudget,
7311
+ SpeechService,
7312
+ GroqSTT,
6633
7313
  OpenACPCore,
6634
7314
  ApiServer,
6635
7315
  TopicManager,
6636
7316
  TelegramAdapter
6637
7317
  };
6638
- //# sourceMappingURL=chunk-V2V767XI.js.map
7318
+ //# sourceMappingURL=chunk-R3UJUOXI.js.map