@openacp/cli 0.4.5 → 0.4.8

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 (38) hide show
  1. package/README.md +81 -15
  2. package/dist/agent-registry-7HC6D4CH.js +7 -0
  3. package/dist/{chunk-WHKLPZGK.js → chunk-6MJLVZXV.js} +8 -8
  4. package/dist/{chunk-V5P3K4A5.js → chunk-BBPWAWE3.js} +1137 -119
  5. package/dist/chunk-BBPWAWE3.js.map +1 -0
  6. package/dist/{chunk-3QACY5E3.js → chunk-C6YIUTGR.js} +2 -2
  7. package/dist/{chunk-2SY7Y2VB.js → chunk-HZD3CGPK.js} +2 -2
  8. package/dist/{chunk-BLVZFCKN.js → chunk-UAUTLC4E.js} +27 -3
  9. package/dist/{chunk-BLVZFCKN.js.map → chunk-UAUTLC4E.js.map} +1 -1
  10. package/dist/chunk-VA2M52CM.js +15 -0
  11. package/dist/chunk-VA2M52CM.js.map +1 -0
  12. package/dist/{chunk-WF5XDN4D.js → chunk-ZRFBLD3W.js} +6 -2
  13. package/dist/chunk-ZRFBLD3W.js.map +1 -0
  14. package/dist/cli.js +388 -38
  15. package/dist/cli.js.map +1 -1
  16. package/dist/{config-J5YQOMDU.js → config-H2DSEHNW.js} +2 -2
  17. package/dist/config-editor-SKS4LJLT.js +11 -0
  18. package/dist/{daemon-SLGQGRKO.js → daemon-VF6HJQXD.js} +3 -3
  19. package/dist/index.d.ts +111 -10
  20. package/dist/index.js +13 -10
  21. package/dist/integrate-WUPLRJD3.js +145 -0
  22. package/dist/integrate-WUPLRJD3.js.map +1 -0
  23. package/dist/{main-3CDOICYN.js → main-NV7YN3VY.js} +27 -14
  24. package/dist/main-NV7YN3VY.js.map +1 -0
  25. package/dist/{setup-JQZBPXWS.js → setup-FCVL75K6.js} +3 -3
  26. package/dist/setup-FCVL75K6.js.map +1 -0
  27. package/package.json +1 -1
  28. package/dist/chunk-V5P3K4A5.js.map +0 -1
  29. package/dist/chunk-WF5XDN4D.js.map +0 -1
  30. package/dist/config-editor-IXL4BFG3.js +0 -11
  31. package/dist/main-3CDOICYN.js.map +0 -1
  32. /package/dist/{config-J5YQOMDU.js.map → agent-registry-7HC6D4CH.js.map} +0 -0
  33. /package/dist/{chunk-WHKLPZGK.js.map → chunk-6MJLVZXV.js.map} +0 -0
  34. /package/dist/{chunk-3QACY5E3.js.map → chunk-C6YIUTGR.js.map} +0 -0
  35. /package/dist/{chunk-2SY7Y2VB.js.map → chunk-HZD3CGPK.js.map} +0 -0
  36. /package/dist/{config-editor-IXL4BFG3.js.map → config-H2DSEHNW.js.map} +0 -0
  37. /package/dist/{daemon-SLGQGRKO.js.map → config-editor-SKS4LJLT.js.map} +0 -0
  38. /package/dist/{setup-JQZBPXWS.js.map → daemon-VF6HJQXD.js.map} +0 -0
@@ -1,3 +1,6 @@
1
+ import {
2
+ getAgentCapabilities
3
+ } from "./chunk-VA2M52CM.js";
1
4
  import {
2
5
  createChildLogger,
3
6
  createSessionLogger
@@ -7,10 +10,10 @@ import {
7
10
  function nodeToWebWritable(nodeStream) {
8
11
  return new WritableStream({
9
12
  write(chunk) {
10
- return new Promise((resolve, reject) => {
13
+ return new Promise((resolve2, reject) => {
11
14
  nodeStream.write(Buffer.from(chunk), (err) => {
12
15
  if (err) reject(err);
13
- else resolve();
16
+ else resolve2();
14
17
  });
15
18
  });
16
19
  }
@@ -142,7 +145,7 @@ var AgentInstance = class _AgentInstance {
142
145
  env: { ...process.env, ...agentDef.env }
143
146
  }
144
147
  );
145
- await new Promise((resolve, reject) => {
148
+ await new Promise((resolve2, reject) => {
146
149
  instance.child.on("error", (err) => {
147
150
  reject(
148
151
  new Error(
@@ -150,7 +153,7 @@ var AgentInstance = class _AgentInstance {
150
153
  )
151
154
  );
152
155
  });
153
- instance.child.on("spawn", () => resolve());
156
+ instance.child.on("spawn", () => resolve2());
154
157
  });
155
158
  instance.stderrCapture = new StderrCapture(50);
156
159
  instance.child.stderr.on("data", (chunk) => {
@@ -428,9 +431,9 @@ ${stderr}`
428
431
  signal: state.exitStatus.signal
429
432
  };
430
433
  }
431
- return new Promise((resolve) => {
434
+ return new Promise((resolve2) => {
432
435
  state.process.on("exit", (code, signal) => {
433
- resolve({ exitCode: code, signal });
436
+ resolve2({ exitCode: code, signal });
434
437
  });
435
438
  });
436
439
  },
@@ -588,8 +591,8 @@ var PromptQueue = class {
588
591
  processing = false;
589
592
  async enqueue(text) {
590
593
  if (this.processing) {
591
- return new Promise((resolve) => {
592
- this.queue.push({ text, resolve });
594
+ return new Promise((resolve2) => {
595
+ this.queue.push({ text, resolve: resolve2 });
593
596
  });
594
597
  }
595
598
  await this.process(text);
@@ -634,8 +637,8 @@ var PermissionGate = class {
634
637
  setPending(request) {
635
638
  this.request = request;
636
639
  this.settled = false;
637
- return new Promise((resolve, reject) => {
638
- this.resolveFn = resolve;
640
+ return new Promise((resolve2, reject) => {
641
+ this.resolveFn = resolve2;
639
642
  this.rejectFn = reject;
640
643
  });
641
644
  }
@@ -827,6 +830,7 @@ var SessionManager = class {
827
830
  createdAt: session.createdAt.toISOString(),
828
831
  lastActiveAt: (/* @__PURE__ */ new Date()).toISOString(),
829
832
  name: session.name,
833
+ dangerousMode: false,
830
834
  platform: {}
831
835
  });
832
836
  }
@@ -843,6 +847,17 @@ var SessionManager = class {
843
847
  }
844
848
  return void 0;
845
849
  }
850
+ getSessionByAgentSessionId(agentSessionId) {
851
+ for (const session of this.sessions.values()) {
852
+ if (session.agentSessionId === agentSessionId) {
853
+ return session;
854
+ }
855
+ }
856
+ return void 0;
857
+ }
858
+ getRecordByAgentSessionId(agentSessionId) {
859
+ return this.store?.findByAgentSessionId(agentSessionId);
860
+ }
846
861
  getRecordByThread(channelId, threadId) {
847
862
  return this.store?.findByPlatform(
848
863
  channelId,
@@ -910,6 +925,18 @@ var SessionManager = class {
910
925
  if (channelId) return all.filter((s) => s.channelId === channelId);
911
926
  return all;
912
927
  }
928
+ listRecords(filter) {
929
+ if (!this.store) return [];
930
+ let records = this.store.list();
931
+ if (filter?.statuses?.length) {
932
+ records = records.filter((r) => filter.statuses.includes(r.status));
933
+ }
934
+ return records;
935
+ }
936
+ async removeRecord(sessionId) {
937
+ if (!this.store) return;
938
+ await this.store.remove(sessionId);
939
+ }
913
940
  async destroyAll() {
914
941
  if (this.store) {
915
942
  for (const session of this.sessions.values()) {
@@ -1189,6 +1216,11 @@ var JsonFileSessionStore = class {
1189
1216
  }
1190
1217
  return void 0;
1191
1218
  }
1219
+ findByAgentSessionId(agentSessionId) {
1220
+ return [...this.records.values()].find(
1221
+ (r) => r.agentSessionId === agentSessionId || r.originalAgentSessionId === agentSessionId
1222
+ );
1223
+ }
1192
1224
  list(channelId) {
1193
1225
  const all = [...this.records.values()];
1194
1226
  if (channelId) return all.filter((r) => r.channelId === channelId);
@@ -1392,6 +1424,95 @@ var OpenACPCore = class {
1392
1424
  }
1393
1425
  return session;
1394
1426
  }
1427
+ async adoptSession(agentName, agentSessionId, cwd) {
1428
+ const caps = getAgentCapabilities(agentName);
1429
+ if (!caps.supportsResume) {
1430
+ return { ok: false, error: "agent_not_supported", message: `Agent '${agentName}' does not support session resume` };
1431
+ }
1432
+ const agentDef = this.agentManager.getAgent(agentName);
1433
+ if (!agentDef) {
1434
+ return { ok: false, error: "agent_not_supported", message: `Agent '${agentName}' not found` };
1435
+ }
1436
+ const { existsSync } = await import("fs");
1437
+ if (!existsSync(cwd)) {
1438
+ return { ok: false, error: "invalid_cwd", message: `Directory does not exist: ${cwd}` };
1439
+ }
1440
+ const maxSessions = this.configManager.get().security.maxConcurrentSessions;
1441
+ if (this.sessionManager.listSessions().length >= maxSessions) {
1442
+ return { ok: false, error: "session_limit", message: "Maximum concurrent sessions reached" };
1443
+ }
1444
+ const existingRecord = this.sessionManager.getRecordByAgentSessionId(agentSessionId);
1445
+ if (existingRecord) {
1446
+ const platform = existingRecord.platform;
1447
+ if (platform?.topicId) {
1448
+ const adapter2 = this.adapters.values().next().value;
1449
+ if (adapter2) {
1450
+ try {
1451
+ await adapter2.sendMessage(existingRecord.sessionId, {
1452
+ type: "text",
1453
+ text: "Session resumed from CLI."
1454
+ });
1455
+ } catch {
1456
+ }
1457
+ }
1458
+ return {
1459
+ ok: true,
1460
+ sessionId: existingRecord.sessionId,
1461
+ threadId: String(platform.topicId),
1462
+ status: "existing"
1463
+ };
1464
+ }
1465
+ }
1466
+ let agentInstance;
1467
+ try {
1468
+ agentInstance = await this.agentManager.resume(agentName, cwd, agentSessionId);
1469
+ } catch (err) {
1470
+ return {
1471
+ ok: false,
1472
+ error: "resume_failed",
1473
+ message: `Failed to resume session: ${err instanceof Error ? err.message : String(err)}`
1474
+ };
1475
+ }
1476
+ const session = new Session({
1477
+ channelId: "api",
1478
+ agentName,
1479
+ workingDirectory: cwd,
1480
+ agentInstance
1481
+ });
1482
+ session.agentSessionId = agentInstance.sessionId;
1483
+ this.sessionManager.registerSession(session);
1484
+ const firstEntry = this.adapters.entries().next().value;
1485
+ if (!firstEntry) {
1486
+ await session.destroy();
1487
+ return { ok: false, error: "no_adapter", message: "No channel adapter registered" };
1488
+ }
1489
+ const [adapterChannelId, adapter] = firstEntry;
1490
+ const threadId = await adapter.createSessionThread(session.id, session.name ?? "Adopted session");
1491
+ session.channelId = adapterChannelId;
1492
+ session.threadId = threadId;
1493
+ this.wireSessionEvents(session, adapter);
1494
+ if (this.sessionStore) {
1495
+ await this.sessionStore.save({
1496
+ sessionId: session.id,
1497
+ agentSessionId: agentInstance.sessionId,
1498
+ originalAgentSessionId: agentSessionId,
1499
+ agentName,
1500
+ workingDir: cwd,
1501
+ channelId: adapterChannelId,
1502
+ status: "active",
1503
+ createdAt: (/* @__PURE__ */ new Date()).toISOString(),
1504
+ lastActiveAt: (/* @__PURE__ */ new Date()).toISOString(),
1505
+ name: session.name,
1506
+ platform: { topicId: Number(threadId) }
1507
+ });
1508
+ }
1509
+ return {
1510
+ ok: true,
1511
+ sessionId: session.id,
1512
+ threadId,
1513
+ status: "adopted"
1514
+ };
1515
+ }
1395
1516
  async handleNewChat(channelId, currentThreadId) {
1396
1517
  const currentSession = this.sessionManager.getSessionByThread(
1397
1518
  channelId,
@@ -1548,6 +1669,8 @@ var ChannelAdapter = class {
1548
1669
  this.core = core;
1549
1670
  this.config = config;
1550
1671
  }
1672
+ async deleteSessionThread(_sessionId) {
1673
+ }
1551
1674
  // Skill commands — override in adapters that support dynamic commands
1552
1675
  async sendSkillCommands(_sessionId, _commands) {
1553
1676
  }
@@ -1560,25 +1683,56 @@ import * as http from "http";
1560
1683
  import * as fs3 from "fs";
1561
1684
  import * as path4 from "path";
1562
1685
  import * as os2 from "os";
1686
+ import { fileURLToPath } from "url";
1563
1687
  var log5 = createChildLogger({ module: "api-server" });
1564
1688
  var DEFAULT_PORT_FILE = path4.join(os2.homedir(), ".openacp", "api.port");
1689
+ var cachedVersion;
1690
+ function getVersion() {
1691
+ if (cachedVersion) return cachedVersion;
1692
+ try {
1693
+ const __filename = fileURLToPath(import.meta.url);
1694
+ const pkgPath = path4.resolve(path4.dirname(__filename), "../../package.json");
1695
+ const pkg = JSON.parse(fs3.readFileSync(pkgPath, "utf-8"));
1696
+ cachedVersion = pkg.version ?? "0.0.0-dev";
1697
+ } catch {
1698
+ cachedVersion = "0.0.0-dev";
1699
+ }
1700
+ return cachedVersion;
1701
+ }
1702
+ var SENSITIVE_KEYS = ["botToken", "token", "apiKey", "secret", "password", "webhookSecret"];
1703
+ function redactConfig(config) {
1704
+ const redacted = structuredClone(config);
1705
+ redactDeep(redacted);
1706
+ return redacted;
1707
+ }
1708
+ function redactDeep(obj) {
1709
+ for (const [key, value] of Object.entries(obj)) {
1710
+ if (SENSITIVE_KEYS.includes(key) && typeof value === "string") {
1711
+ obj[key] = "***";
1712
+ } else if (value && typeof value === "object" && !Array.isArray(value)) {
1713
+ redactDeep(value);
1714
+ }
1715
+ }
1716
+ }
1565
1717
  var ApiServer = class {
1566
- constructor(core, config, portFilePath) {
1718
+ constructor(core, config, portFilePath, topicManager) {
1567
1719
  this.core = core;
1568
1720
  this.config = config;
1721
+ this.topicManager = topicManager;
1569
1722
  this.portFilePath = portFilePath ?? DEFAULT_PORT_FILE;
1570
1723
  }
1571
1724
  server = null;
1572
1725
  actualPort = 0;
1573
1726
  portFilePath;
1727
+ startedAt = Date.now();
1574
1728
  async start() {
1575
1729
  this.server = http.createServer((req, res) => this.handleRequest(req, res));
1576
- await new Promise((resolve, reject) => {
1730
+ await new Promise((resolve2, reject) => {
1577
1731
  this.server.on("error", (err) => {
1578
1732
  if (err.code === "EADDRINUSE") {
1579
1733
  log5.warn({ port: this.config.port }, "API port in use, continuing without API server");
1580
1734
  this.server = null;
1581
- resolve();
1735
+ resolve2();
1582
1736
  } else {
1583
1737
  reject(err);
1584
1738
  }
@@ -1590,15 +1744,15 @@ var ApiServer = class {
1590
1744
  }
1591
1745
  this.writePortFile();
1592
1746
  log5.info({ host: this.config.host, port: this.actualPort }, "API server listening");
1593
- resolve();
1747
+ resolve2();
1594
1748
  });
1595
1749
  });
1596
1750
  }
1597
1751
  async stop() {
1598
1752
  this.removePortFile();
1599
1753
  if (this.server) {
1600
- await new Promise((resolve) => {
1601
- this.server.close(() => resolve());
1754
+ await new Promise((resolve2) => {
1755
+ this.server.close(() => resolve2());
1602
1756
  });
1603
1757
  this.server = null;
1604
1758
  }
@@ -1621,15 +1775,49 @@ var ApiServer = class {
1621
1775
  const method = req.method?.toUpperCase();
1622
1776
  const url = req.url || "";
1623
1777
  try {
1624
- if (method === "POST" && url === "/api/sessions") {
1778
+ if (method === "POST" && url === "/api/sessions/adopt") {
1779
+ await this.handleAdoptSession(req, res);
1780
+ } else if (method === "POST" && url === "/api/sessions") {
1625
1781
  await this.handleCreateSession(req, res);
1626
- } else if (method === "DELETE" && url.match(/^\/api\/sessions\/(.+)$/)) {
1627
- const sessionId = url.match(/^\/api\/sessions\/(.+)$/)[1];
1782
+ } else if (method === "POST" && url.match(/^\/api\/sessions\/([^/]+)\/prompt$/)) {
1783
+ const sessionId = decodeURIComponent(url.match(/^\/api\/sessions\/([^/]+)\/prompt$/)[1]);
1784
+ await this.handleSendPrompt(sessionId, req, res);
1785
+ } else if (method === "PATCH" && url.match(/^\/api\/sessions\/([^/]+)\/dangerous$/)) {
1786
+ const sessionId = decodeURIComponent(url.match(/^\/api\/sessions\/([^/]+)\/dangerous$/)[1]);
1787
+ await this.handleToggleDangerous(sessionId, req, res);
1788
+ } else if (method === "GET" && url.match(/^\/api\/sessions\/([^/]+)$/)) {
1789
+ const sessionId = decodeURIComponent(url.match(/^\/api\/sessions\/([^/]+)$/)[1]);
1790
+ await this.handleGetSession(sessionId, res);
1791
+ } else if (method === "DELETE" && url.match(/^\/api\/sessions\/([^/]+)$/)) {
1792
+ const sessionId = decodeURIComponent(url.match(/^\/api\/sessions\/([^/]+)$/)[1]);
1628
1793
  await this.handleCancelSession(sessionId, res);
1629
1794
  } else if (method === "GET" && url === "/api/sessions") {
1630
1795
  await this.handleListSessions(res);
1631
1796
  } else if (method === "GET" && url === "/api/agents") {
1632
1797
  await this.handleListAgents(res);
1798
+ } else if (method === "GET" && url === "/api/health") {
1799
+ await this.handleHealth(res);
1800
+ } else if (method === "GET" && url === "/api/version") {
1801
+ await this.handleVersion(res);
1802
+ } else if (method === "GET" && url === "/api/config") {
1803
+ await this.handleGetConfig(res);
1804
+ } else if (method === "PATCH" && url === "/api/config") {
1805
+ await this.handleUpdateConfig(req, res);
1806
+ } else if (method === "GET" && url === "/api/adapters") {
1807
+ await this.handleListAdapters(res);
1808
+ } else if (method === "GET" && url === "/api/tunnel") {
1809
+ await this.handleTunnelStatus(res);
1810
+ } else if (method === "POST" && url === "/api/notify") {
1811
+ await this.handleNotify(req, res);
1812
+ } else if (method === "POST" && url === "/api/restart") {
1813
+ await this.handleRestart(res);
1814
+ } else if (method === "GET" && url.match(/^\/api\/topics(\?.*)?$/)) {
1815
+ await this.handleListTopics(url, res);
1816
+ } else if (method === "POST" && url === "/api/topics/cleanup") {
1817
+ await this.handleCleanupTopics(req, res);
1818
+ } else if (method === "DELETE" && url.match(/^\/api\/topics\/([^/?]+)/)) {
1819
+ const match = url.match(/^\/api\/topics\/([^/?]+)/);
1820
+ await this.handleDeleteTopic(decodeURIComponent(match[1]), url, res);
1633
1821
  } else {
1634
1822
  this.sendJson(res, 404, { error: "Not found" });
1635
1823
  }
@@ -1687,6 +1875,222 @@ var ApiServer = class {
1687
1875
  workspace: session.workingDirectory
1688
1876
  });
1689
1877
  }
1878
+ async handleSendPrompt(sessionId, req, res) {
1879
+ const session = this.core.sessionManager.getSession(sessionId);
1880
+ if (!session) {
1881
+ this.sendJson(res, 404, { error: `Session "${sessionId}" not found` });
1882
+ return;
1883
+ }
1884
+ if (session.status === "cancelled" || session.status === "finished" || session.status === "error") {
1885
+ this.sendJson(res, 400, { error: `Session is ${session.status}` });
1886
+ return;
1887
+ }
1888
+ const body = await this.readBody(req);
1889
+ let prompt;
1890
+ if (body) {
1891
+ try {
1892
+ const parsed = JSON.parse(body);
1893
+ prompt = parsed.prompt;
1894
+ } catch {
1895
+ this.sendJson(res, 400, { error: "Invalid JSON body" });
1896
+ return;
1897
+ }
1898
+ }
1899
+ if (!prompt) {
1900
+ this.sendJson(res, 400, { error: "Missing prompt" });
1901
+ return;
1902
+ }
1903
+ session.enqueuePrompt(prompt).catch(() => {
1904
+ });
1905
+ this.sendJson(res, 200, { ok: true, sessionId, queueDepth: session.queueDepth });
1906
+ }
1907
+ async handleGetSession(sessionId, res) {
1908
+ const session = this.core.sessionManager.getSession(sessionId);
1909
+ if (!session) {
1910
+ this.sendJson(res, 404, { error: `Session "${sessionId}" not found` });
1911
+ return;
1912
+ }
1913
+ this.sendJson(res, 200, {
1914
+ session: {
1915
+ id: session.id,
1916
+ agent: session.agentName,
1917
+ status: session.status,
1918
+ name: session.name ?? null,
1919
+ workspace: session.workingDirectory,
1920
+ createdAt: session.createdAt.toISOString(),
1921
+ dangerousMode: session.dangerousMode,
1922
+ queueDepth: session.queueDepth,
1923
+ promptRunning: session.promptRunning,
1924
+ threadId: session.threadId,
1925
+ channelId: session.channelId,
1926
+ agentSessionId: session.agentSessionId
1927
+ }
1928
+ });
1929
+ }
1930
+ async handleToggleDangerous(sessionId, req, res) {
1931
+ const session = this.core.sessionManager.getSession(sessionId);
1932
+ if (!session) {
1933
+ this.sendJson(res, 404, { error: `Session "${sessionId}" not found` });
1934
+ return;
1935
+ }
1936
+ const body = await this.readBody(req);
1937
+ let enabled;
1938
+ if (body) {
1939
+ try {
1940
+ const parsed = JSON.parse(body);
1941
+ enabled = parsed.enabled;
1942
+ } catch {
1943
+ this.sendJson(res, 400, { error: "Invalid JSON body" });
1944
+ return;
1945
+ }
1946
+ }
1947
+ if (typeof enabled !== "boolean") {
1948
+ this.sendJson(res, 400, { error: "Missing enabled boolean" });
1949
+ return;
1950
+ }
1951
+ session.dangerousMode = enabled;
1952
+ await this.core.sessionManager.updateSessionDangerousMode(sessionId, enabled);
1953
+ this.sendJson(res, 200, { ok: true, dangerousMode: enabled });
1954
+ }
1955
+ async handleHealth(res) {
1956
+ const activeSessions = this.core.sessionManager.listSessions();
1957
+ const allRecords = this.core.sessionManager.listRecords();
1958
+ const mem = process.memoryUsage();
1959
+ const tunnel = this.core.tunnelService;
1960
+ this.sendJson(res, 200, {
1961
+ status: "ok",
1962
+ uptime: Date.now() - this.startedAt,
1963
+ version: getVersion(),
1964
+ memory: {
1965
+ rss: mem.rss,
1966
+ heapUsed: mem.heapUsed,
1967
+ heapTotal: mem.heapTotal
1968
+ },
1969
+ sessions: {
1970
+ active: activeSessions.filter((s) => s.status === "active" || s.status === "initializing").length,
1971
+ total: allRecords.length
1972
+ },
1973
+ adapters: Array.from(this.core.adapters.keys()),
1974
+ tunnel: tunnel ? { enabled: true, url: tunnel.getPublicUrl() } : { enabled: false }
1975
+ });
1976
+ }
1977
+ async handleVersion(res) {
1978
+ this.sendJson(res, 200, { version: getVersion() });
1979
+ }
1980
+ async handleGetConfig(res) {
1981
+ const config = this.core.configManager.get();
1982
+ this.sendJson(res, 200, { config: redactConfig(config) });
1983
+ }
1984
+ async handleUpdateConfig(req, res) {
1985
+ const body = await this.readBody(req);
1986
+ let configPath;
1987
+ let value;
1988
+ if (body) {
1989
+ try {
1990
+ const parsed = JSON.parse(body);
1991
+ configPath = parsed.path;
1992
+ value = parsed.value;
1993
+ } catch {
1994
+ this.sendJson(res, 400, { error: "Invalid JSON body" });
1995
+ return;
1996
+ }
1997
+ }
1998
+ if (!configPath) {
1999
+ this.sendJson(res, 400, { error: "Missing path" });
2000
+ return;
2001
+ }
2002
+ const currentConfig = this.core.configManager.get();
2003
+ const cloned = structuredClone(currentConfig);
2004
+ const parts = configPath.split(".");
2005
+ let target = cloned;
2006
+ for (let i = 0; i < parts.length - 1; i++) {
2007
+ if (target[parts[i]] && typeof target[parts[i]] === "object" && !Array.isArray(target[parts[i]])) {
2008
+ target = target[parts[i]];
2009
+ } else {
2010
+ this.sendJson(res, 400, { error: "Invalid config path" });
2011
+ return;
2012
+ }
2013
+ }
2014
+ const lastKey = parts[parts.length - 1];
2015
+ if (!(lastKey in target)) {
2016
+ this.sendJson(res, 400, { error: "Invalid config path" });
2017
+ return;
2018
+ }
2019
+ target[lastKey] = value;
2020
+ const { ConfigSchema } = await import("./config-H2DSEHNW.js");
2021
+ const result = ConfigSchema.safeParse(cloned);
2022
+ if (!result.success) {
2023
+ this.sendJson(res, 400, {
2024
+ error: "Validation failed",
2025
+ details: result.error.issues.map((i) => ({ path: i.path.join("."), message: i.message }))
2026
+ });
2027
+ return;
2028
+ }
2029
+ const updates = {};
2030
+ let updateTarget = updates;
2031
+ for (let i = 0; i < parts.length - 1; i++) {
2032
+ updateTarget[parts[i]] = {};
2033
+ updateTarget = updateTarget[parts[i]];
2034
+ }
2035
+ updateTarget[lastKey] = value;
2036
+ await this.core.configManager.save(updates);
2037
+ const RESTART_PREFIXES = ["api.port", "api.host", "runMode", "channels.", "tunnel.", "agents."];
2038
+ const needsRestart = RESTART_PREFIXES.some(
2039
+ (prefix) => configPath.startsWith(prefix) || configPath === prefix.replace(/\.$/, "")
2040
+ // exact match for non-wildcard
2041
+ );
2042
+ this.sendJson(res, 200, {
2043
+ ok: true,
2044
+ needsRestart,
2045
+ config: redactConfig(this.core.configManager.get())
2046
+ });
2047
+ }
2048
+ async handleListAdapters(res) {
2049
+ const adapters = Array.from(this.core.adapters.entries()).map(([name]) => ({
2050
+ name,
2051
+ type: "built-in"
2052
+ }));
2053
+ this.sendJson(res, 200, { adapters });
2054
+ }
2055
+ async handleTunnelStatus(res) {
2056
+ const tunnel = this.core.tunnelService;
2057
+ if (tunnel) {
2058
+ this.sendJson(res, 200, { enabled: true, url: tunnel.getPublicUrl(), provider: this.core.configManager.get().tunnel.provider });
2059
+ } else {
2060
+ this.sendJson(res, 200, { enabled: false });
2061
+ }
2062
+ }
2063
+ async handleNotify(req, res) {
2064
+ const body = await this.readBody(req);
2065
+ let message;
2066
+ if (body) {
2067
+ try {
2068
+ const parsed = JSON.parse(body);
2069
+ message = parsed.message;
2070
+ } catch {
2071
+ this.sendJson(res, 400, { error: "Invalid JSON body" });
2072
+ return;
2073
+ }
2074
+ }
2075
+ if (!message) {
2076
+ this.sendJson(res, 400, { error: "Missing message" });
2077
+ return;
2078
+ }
2079
+ await this.core.notificationManager.notifyAll({
2080
+ sessionId: "system",
2081
+ type: "completed",
2082
+ summary: message
2083
+ });
2084
+ this.sendJson(res, 200, { ok: true });
2085
+ }
2086
+ async handleRestart(res) {
2087
+ if (!this.core.requestRestart) {
2088
+ this.sendJson(res, 501, { error: "Restart not available" });
2089
+ return;
2090
+ }
2091
+ this.sendJson(res, 200, { ok: true, message: "Restarting..." });
2092
+ setImmediate(() => this.core.requestRestart());
2093
+ }
1690
2094
  async handleCancelSession(sessionId, res) {
1691
2095
  const session = this.core.sessionManager.getSession(sessionId);
1692
2096
  if (!session) {
@@ -1708,34 +2112,180 @@ var ApiServer = class {
1708
2112
  }))
1709
2113
  });
1710
2114
  }
2115
+ async handleAdoptSession(req, res) {
2116
+ const body = await this.readBody(req);
2117
+ if (!body) {
2118
+ return this.sendJson(res, 400, { error: "bad_request", message: "Empty request body" });
2119
+ }
2120
+ let parsed;
2121
+ try {
2122
+ parsed = JSON.parse(body);
2123
+ } catch {
2124
+ return this.sendJson(res, 400, { error: "bad_request", message: "Invalid JSON" });
2125
+ }
2126
+ const { agent, agentSessionId, cwd } = parsed;
2127
+ if (!agent || !agentSessionId) {
2128
+ return this.sendJson(res, 400, { error: "bad_request", message: "Missing required fields: agent, agentSessionId" });
2129
+ }
2130
+ const result = await this.core.adoptSession(agent, agentSessionId, cwd ?? process.cwd());
2131
+ if (result.ok) {
2132
+ return this.sendJson(res, 200, result);
2133
+ } else {
2134
+ const status = result.error === "session_limit" ? 429 : result.error === "agent_not_supported" ? 400 : 500;
2135
+ return this.sendJson(res, status, result);
2136
+ }
2137
+ }
1711
2138
  async handleListAgents(res) {
1712
2139
  const agents = this.core.agentManager.getAvailableAgents();
1713
2140
  const defaultAgent = this.core.configManager.get().defaultAgent;
1714
- this.sendJson(res, 200, {
1715
- agents: agents.map((a) => ({
1716
- name: a.name,
1717
- command: a.command,
1718
- args: a.args
1719
- })),
1720
- default: defaultAgent
1721
- });
2141
+ const agentsWithCaps = agents.map((a) => ({
2142
+ ...a,
2143
+ capabilities: getAgentCapabilities(a.name)
2144
+ }));
2145
+ this.sendJson(res, 200, { agents: agentsWithCaps, default: defaultAgent });
1722
2146
  }
1723
2147
  sendJson(res, status, data) {
1724
2148
  res.writeHead(status, { "Content-Type": "application/json" });
1725
2149
  res.end(JSON.stringify(data));
1726
2150
  }
2151
+ async handleListTopics(url, res) {
2152
+ if (!this.topicManager) {
2153
+ this.sendJson(res, 501, { error: "Topic management not available" });
2154
+ return;
2155
+ }
2156
+ const params = new URL(url, "http://localhost").searchParams;
2157
+ const statusParam = params.get("status");
2158
+ const filter = statusParam ? { statuses: statusParam.split(",") } : void 0;
2159
+ const topics = this.topicManager.listTopics(filter);
2160
+ this.sendJson(res, 200, { topics });
2161
+ }
2162
+ async handleDeleteTopic(sessionId, url, res) {
2163
+ if (!this.topicManager) {
2164
+ this.sendJson(res, 501, { error: "Topic management not available" });
2165
+ return;
2166
+ }
2167
+ const params = new URL(url, "http://localhost").searchParams;
2168
+ const force = params.get("force") === "true";
2169
+ const result = await this.topicManager.deleteTopic(sessionId, force ? { confirmed: true } : void 0);
2170
+ if (result.ok) {
2171
+ this.sendJson(res, 200, result);
2172
+ } else if (result.needsConfirmation) {
2173
+ this.sendJson(res, 409, { error: "Session is active", needsConfirmation: true, session: result.session });
2174
+ } else if (result.error === "Cannot delete system topic") {
2175
+ this.sendJson(res, 403, { error: result.error });
2176
+ } else {
2177
+ this.sendJson(res, 404, { error: result.error ?? "Not found" });
2178
+ }
2179
+ }
2180
+ async handleCleanupTopics(req, res) {
2181
+ if (!this.topicManager) {
2182
+ this.sendJson(res, 501, { error: "Topic management not available" });
2183
+ return;
2184
+ }
2185
+ const body = await this.readBody(req);
2186
+ let statuses;
2187
+ if (body) {
2188
+ try {
2189
+ statuses = JSON.parse(body).statuses;
2190
+ } catch {
2191
+ }
2192
+ }
2193
+ const result = await this.topicManager.cleanup(statuses);
2194
+ this.sendJson(res, 200, result);
2195
+ }
1727
2196
  readBody(req) {
1728
- return new Promise((resolve) => {
2197
+ return new Promise((resolve2) => {
1729
2198
  let data = "";
1730
2199
  req.on("data", (chunk) => {
1731
2200
  data += chunk;
1732
2201
  });
1733
- req.on("end", () => resolve(data));
1734
- req.on("error", () => resolve(""));
2202
+ req.on("end", () => resolve2(data));
2203
+ req.on("error", () => resolve2(""));
1735
2204
  });
1736
2205
  }
1737
2206
  };
1738
2207
 
2208
+ // src/core/topic-manager.ts
2209
+ var log6 = createChildLogger({ module: "topic-manager" });
2210
+ var TopicManager = class {
2211
+ constructor(sessionManager, adapter, systemTopicIds) {
2212
+ this.sessionManager = sessionManager;
2213
+ this.adapter = adapter;
2214
+ this.systemTopicIds = systemTopicIds;
2215
+ }
2216
+ listTopics(filter) {
2217
+ const records = this.sessionManager.listRecords(filter);
2218
+ return records.filter((r) => !this.isSystemTopic(r)).filter((r) => !filter?.statuses?.length || filter.statuses.includes(r.status)).map((r) => ({
2219
+ sessionId: r.sessionId,
2220
+ topicId: r.platform?.topicId ?? null,
2221
+ name: r.name ?? null,
2222
+ status: r.status,
2223
+ agentName: r.agentName,
2224
+ lastActiveAt: r.lastActiveAt
2225
+ }));
2226
+ }
2227
+ async deleteTopic(sessionId, options) {
2228
+ const records = this.sessionManager.listRecords();
2229
+ const record = records.find((r) => r.sessionId === sessionId);
2230
+ if (!record) return { ok: false, error: "Session not found" };
2231
+ if (this.isSystemTopic(record)) return { ok: false, error: "Cannot delete system topic" };
2232
+ const isActive = record.status === "active" || record.status === "initializing";
2233
+ if (isActive && !options?.confirmed) {
2234
+ return {
2235
+ ok: false,
2236
+ needsConfirmation: true,
2237
+ session: { id: record.sessionId, name: record.name ?? null, status: record.status }
2238
+ };
2239
+ }
2240
+ if (isActive) {
2241
+ await this.sessionManager.cancelSession(sessionId);
2242
+ }
2243
+ const topicId = record.platform?.topicId ?? null;
2244
+ if (this.adapter && topicId) {
2245
+ try {
2246
+ await this.adapter.deleteSessionThread(sessionId);
2247
+ } catch (err) {
2248
+ log6.warn({ err, sessionId, topicId }, "Failed to delete platform thread, removing record anyway");
2249
+ }
2250
+ }
2251
+ await this.sessionManager.removeRecord(sessionId);
2252
+ return { ok: true, topicId };
2253
+ }
2254
+ async cleanup(statuses) {
2255
+ const targetStatuses = statuses?.length ? statuses : ["finished", "error", "cancelled"];
2256
+ const records = this.sessionManager.listRecords({ statuses: targetStatuses });
2257
+ const targets = records.filter((r) => !this.isSystemTopic(r)).filter((r) => targetStatuses.includes(r.status));
2258
+ const deleted = [];
2259
+ const failed = [];
2260
+ for (const record of targets) {
2261
+ try {
2262
+ const isActive = record.status === "active" || record.status === "initializing";
2263
+ if (isActive) {
2264
+ await this.sessionManager.cancelSession(record.sessionId);
2265
+ }
2266
+ const topicId = record.platform?.topicId;
2267
+ if (this.adapter && topicId) {
2268
+ try {
2269
+ await this.adapter.deleteSessionThread(record.sessionId);
2270
+ } catch (err) {
2271
+ log6.warn({ err, sessionId: record.sessionId }, "Failed to delete platform thread during cleanup");
2272
+ }
2273
+ }
2274
+ await this.sessionManager.removeRecord(record.sessionId);
2275
+ deleted.push(record.sessionId);
2276
+ } catch (err) {
2277
+ failed.push({ sessionId: record.sessionId, error: err instanceof Error ? err.message : String(err) });
2278
+ }
2279
+ }
2280
+ return { deleted, failed };
2281
+ }
2282
+ isSystemTopic(record) {
2283
+ const topicId = record.platform?.topicId;
2284
+ if (!topicId) return false;
2285
+ return topicId === this.systemTopicIds.notificationTopicId || topicId === this.systemTopicIds.assistantTopicId;
2286
+ }
2287
+ };
2288
+
1739
2289
  // src/adapters/telegram/adapter.ts
1740
2290
  import { Bot } from "grammy";
1741
2291
 
@@ -1869,7 +2419,7 @@ function formatUsage(usage) {
1869
2419
  return `${emoji} ${formatTokens(tokensUsed)} / ${formatTokens(contextSize)} tokens
1870
2420
  ${bar} ${pct}%`;
1871
2421
  }
1872
- function splitMessage(text, maxLength = 4096) {
2422
+ function splitMessage(text, maxLength = 3800) {
1873
2423
  if (text.length <= maxLength) return [text];
1874
2424
  const chunks = [];
1875
2425
  let remaining = text;
@@ -1879,14 +2429,23 @@ function splitMessage(text, maxLength = 4096) {
1879
2429
  break;
1880
2430
  }
1881
2431
  let splitAt = remaining.lastIndexOf("\n\n", maxLength);
1882
- if (splitAt === -1 || splitAt < maxLength * 0.5) {
2432
+ if (splitAt === -1 || splitAt < maxLength * 0.2) {
1883
2433
  splitAt = remaining.lastIndexOf("\n", maxLength);
1884
2434
  }
1885
- if (splitAt === -1 || splitAt < maxLength * 0.5) {
2435
+ if (splitAt === -1 || splitAt < maxLength * 0.2) {
1886
2436
  splitAt = maxLength;
1887
2437
  }
2438
+ const candidate = remaining.slice(0, splitAt);
2439
+ const fences = candidate.match(/```/g);
2440
+ if (fences && fences.length % 2 !== 0) {
2441
+ const closingFence = remaining.indexOf("```", splitAt);
2442
+ if (closingFence !== -1) {
2443
+ const afterFence = remaining.indexOf("\n", closingFence + 3);
2444
+ splitAt = afterFence !== -1 ? afterFence + 1 : closingFence + 3;
2445
+ }
2446
+ }
1888
2447
  chunks.push(remaining.slice(0, splitAt));
1889
- remaining = remaining.slice(splitAt).trimStart();
2448
+ remaining = remaining.slice(splitAt).replace(/^\n+/, "");
1890
2449
  }
1891
2450
  return chunks;
1892
2451
  }
@@ -1923,14 +2482,22 @@ var MessageDraft = class {
1923
2482
  async flush() {
1924
2483
  if (!this.buffer) return;
1925
2484
  if (this.firstFlushPending) return;
1926
- const html = markdownToTelegramHtml(this.buffer);
1927
- const truncated = html.length > 4096 ? html.slice(0, 4090) + "\n..." : html;
1928
- if (!truncated) return;
2485
+ let displayBuffer = this.buffer;
2486
+ if (displayBuffer.length > 3800) {
2487
+ let cutAt = displayBuffer.lastIndexOf("\n", 3800);
2488
+ if (cutAt < 800) cutAt = 3800;
2489
+ displayBuffer = displayBuffer.slice(0, cutAt) + "\n\u2026";
2490
+ }
2491
+ let html = markdownToTelegramHtml(displayBuffer);
2492
+ if (!html) return;
2493
+ if (html.length > 4096) {
2494
+ html = html.slice(0, 4090) + "\n\u2026";
2495
+ }
1929
2496
  if (!this.messageId) {
1930
2497
  this.firstFlushPending = true;
1931
2498
  try {
1932
2499
  const result = await this.sendQueue.enqueue(
1933
- () => this.bot.api.sendMessage(this.chatId, truncated, {
2500
+ () => this.bot.api.sendMessage(this.chatId, html, {
1934
2501
  message_thread_id: this.threadId,
1935
2502
  parse_mode: "HTML",
1936
2503
  disable_notification: true
@@ -1948,7 +2515,7 @@ var MessageDraft = class {
1948
2515
  } else {
1949
2516
  try {
1950
2517
  await this.sendQueue.enqueue(
1951
- () => this.bot.api.editMessageText(this.chatId, this.messageId, truncated, {
2518
+ () => this.bot.api.editMessageText(this.chatId, this.messageId, html, {
1952
2519
  parse_mode: "HTML"
1953
2520
  }),
1954
2521
  { type: "text", key: this.sessionId }
@@ -1968,21 +2535,20 @@ var MessageDraft = class {
1968
2535
  if (this.messageId && this.buffer === this.lastSentBuffer) {
1969
2536
  return this.messageId;
1970
2537
  }
1971
- const html = markdownToTelegramHtml(this.buffer);
1972
- const chunks = splitMessage(html);
1973
- try {
1974
- for (let i = 0; i < chunks.length; i++) {
1975
- const chunk = chunks[i];
2538
+ const mdChunks = splitMessage(this.buffer);
2539
+ for (let i = 0; i < mdChunks.length; i++) {
2540
+ const html = markdownToTelegramHtml(mdChunks[i]);
2541
+ try {
1976
2542
  if (i === 0 && this.messageId) {
1977
2543
  await this.sendQueue.enqueue(
1978
- () => this.bot.api.editMessageText(this.chatId, this.messageId, chunk, {
2544
+ () => this.bot.api.editMessageText(this.chatId, this.messageId, html, {
1979
2545
  parse_mode: "HTML"
1980
2546
  }),
1981
2547
  { type: "other" }
1982
2548
  );
1983
2549
  } else {
1984
2550
  const msg = await this.sendQueue.enqueue(
1985
- () => this.bot.api.sendMessage(this.chatId, chunk, {
2551
+ () => this.bot.api.sendMessage(this.chatId, html, {
1986
2552
  message_thread_id: this.threadId,
1987
2553
  parse_mode: "HTML",
1988
2554
  disable_notification: true
@@ -1993,17 +2559,25 @@ var MessageDraft = class {
1993
2559
  this.messageId = msg.message_id;
1994
2560
  }
1995
2561
  }
1996
- }
1997
- } catch {
1998
- if (this.buffer !== this.lastSentBuffer) {
2562
+ } catch {
1999
2563
  try {
2000
- await this.sendQueue.enqueue(
2001
- () => this.bot.api.sendMessage(this.chatId, this.buffer.slice(0, 4096), {
2002
- message_thread_id: this.threadId,
2003
- disable_notification: true
2004
- }),
2005
- { type: "other" }
2006
- );
2564
+ if (i === 0 && this.messageId) {
2565
+ await this.sendQueue.enqueue(
2566
+ () => this.bot.api.editMessageText(this.chatId, this.messageId, mdChunks[i].slice(0, 4096)),
2567
+ { type: "other" }
2568
+ );
2569
+ } else {
2570
+ const msg = await this.sendQueue.enqueue(
2571
+ () => this.bot.api.sendMessage(this.chatId, mdChunks[i].slice(0, 4096), {
2572
+ message_thread_id: this.threadId,
2573
+ disable_notification: true
2574
+ }),
2575
+ { type: "other" }
2576
+ );
2577
+ if (msg) {
2578
+ this.messageId = msg.message_id;
2579
+ }
2580
+ }
2007
2581
  } catch {
2008
2582
  }
2009
2583
  }
@@ -2048,12 +2622,13 @@ function buildDeepLink(chatId, messageId) {
2048
2622
 
2049
2623
  // src/adapters/telegram/commands.ts
2050
2624
  import { InlineKeyboard } from "grammy";
2051
- var log7 = createChildLogger({ module: "telegram-commands" });
2625
+ var log8 = createChildLogger({ module: "telegram-commands" });
2052
2626
  function setupCommands(bot, core, chatId, assistant) {
2053
2627
  bot.command("new", (ctx) => handleNew(ctx, core, chatId, assistant));
2054
2628
  bot.command("newchat", (ctx) => handleNewChat(ctx, core, chatId));
2055
2629
  bot.command("cancel", (ctx) => handleCancel(ctx, core, assistant));
2056
2630
  bot.command("status", (ctx) => handleStatus(ctx, core));
2631
+ bot.command("sessions", (ctx) => handleTopics(ctx, core));
2057
2632
  bot.command("agents", (ctx) => handleAgents(ctx, core));
2058
2633
  bot.command("help", (ctx) => handleHelp(ctx));
2059
2634
  bot.command("menu", (ctx) => handleMenu(ctx));
@@ -2061,11 +2636,12 @@ function setupCommands(bot, core, chatId, assistant) {
2061
2636
  bot.command("disable_dangerous", (ctx) => handleDisableDangerous(ctx, core));
2062
2637
  bot.command("restart", (ctx) => handleRestart(ctx, core));
2063
2638
  bot.command("update", (ctx) => handleUpdate(ctx, core));
2639
+ bot.command("integrate", (ctx) => handleIntegrate(ctx, core));
2064
2640
  }
2065
2641
  function buildMenuKeyboard() {
2066
- return new InlineKeyboard().text("\u{1F195} New Session", "m:new").text("\u{1F4AC} New Chat", "m:newchat").row().text("\u26D4 Cancel", "m:cancel").text("\u{1F4CA} Status", "m:status").row().text("\u{1F916} Agents", "m:agents").text("\u2753 Help", "m:help").row().text("\u{1F504} Restart", "m:restart").text("\u2B06\uFE0F Update", "m:update");
2642
+ return new InlineKeyboard().text("\u{1F195} New Session", "m:new").text("\u{1F4AC} New Chat", "m:newchat").row().text("\u26D4 Cancel", "m:cancel").text("\u{1F4CA} Status", "m:status").row().text("\u{1F4CB} Sessions", "m:topics").text("\u{1F916} Agents", "m:agents").row().text("\u{1F517} Integrate", "m:integrate").text("\u2753 Help", "m:help").row().text("\u{1F504} Restart", "m:restart").text("\u2B06\uFE0F Update", "m:update");
2067
2643
  }
2068
- function setupMenuCallbacks(bot, core, chatId) {
2644
+ function setupMenuCallbacks(bot, core, chatId, systemTopicIds) {
2069
2645
  bot.callbackQuery(/^m:/, async (ctx) => {
2070
2646
  const data = ctx.callbackQuery.data;
2071
2647
  try {
@@ -2097,6 +2673,27 @@ function setupMenuCallbacks(bot, core, chatId) {
2097
2673
  case "m:update":
2098
2674
  await handleUpdate(ctx, core);
2099
2675
  break;
2676
+ case "m:integrate":
2677
+ await handleIntegrate(ctx, core);
2678
+ break;
2679
+ case "m:topics":
2680
+ await handleTopics(ctx, core);
2681
+ break;
2682
+ case "m:cleanup:finished":
2683
+ await handleCleanup(ctx, core, chatId, ["finished"]);
2684
+ break;
2685
+ case "m:cleanup:errors":
2686
+ await handleCleanup(ctx, core, chatId, ["error", "cancelled"]);
2687
+ break;
2688
+ case "m:cleanup:all":
2689
+ await handleCleanup(ctx, core, chatId, ["finished", "error", "cancelled"]);
2690
+ break;
2691
+ case "m:cleanup:everything":
2692
+ await handleCleanupEverything(ctx, core, chatId, systemTopicIds);
2693
+ break;
2694
+ case "m:cleanup:everything:confirm":
2695
+ await handleCleanupEverythingConfirmed(ctx, core, chatId, systemTopicIds);
2696
+ break;
2100
2697
  }
2101
2698
  });
2102
2699
  }
@@ -2122,7 +2719,7 @@ async function handleNew(ctx, core, chatId, assistant) {
2122
2719
  return;
2123
2720
  }
2124
2721
  }
2125
- log7.info({ userId: ctx.from?.id, agentName }, "New session command");
2722
+ log8.info({ userId: ctx.from?.id, agentName }, "New session command");
2126
2723
  let threadId;
2127
2724
  try {
2128
2725
  const topicName = `\u{1F504} New Session`;
@@ -2156,9 +2753,9 @@ async function handleNew(ctx, core, chatId, assistant) {
2156
2753
  reply_markup: buildDangerousModeKeyboard(session.id, false)
2157
2754
  }
2158
2755
  );
2159
- session.warmup().catch((err) => log7.error({ err }, "Warm-up error"));
2756
+ session.warmup().catch((err) => log8.error({ err }, "Warm-up error"));
2160
2757
  } catch (err) {
2161
- log7.error({ err }, "Session creation failed");
2758
+ log8.error({ err }, "Session creation failed");
2162
2759
  if (threadId) {
2163
2760
  try {
2164
2761
  await ctx.api.deleteForumTopic(chatId, threadId);
@@ -2235,7 +2832,7 @@ async function handleNewChat(ctx, core, chatId) {
2235
2832
  reply_markup: buildDangerousModeKeyboard(session.id, false)
2236
2833
  }
2237
2834
  );
2238
- session.warmup().catch((err) => log7.error({ err }, "Warm-up error"));
2835
+ session.warmup().catch((err) => log8.error({ err }, "Warm-up error"));
2239
2836
  } catch (err) {
2240
2837
  if (newThreadId) {
2241
2838
  try {
@@ -2264,14 +2861,14 @@ async function handleCancel(ctx, core, assistant) {
2264
2861
  String(threadId)
2265
2862
  );
2266
2863
  if (session) {
2267
- log7.info({ sessionId: session.id }, "Cancel session command");
2864
+ log8.info({ sessionId: session.id }, "Cancel session command");
2268
2865
  await session.cancel();
2269
2866
  await ctx.reply("\u26D4 Session cancelled.", { parse_mode: "HTML" });
2270
2867
  return;
2271
2868
  }
2272
2869
  const record = core.sessionManager.getRecordByThread("telegram", String(threadId));
2273
2870
  if (record && record.status !== "cancelled" && record.status !== "error") {
2274
- log7.info({ sessionId: record.sessionId }, "Cancel session command (from store)");
2871
+ log8.info({ sessionId: record.sessionId }, "Cancel session command (from store)");
2275
2872
  await core.sessionManager.cancelSession(record.sessionId);
2276
2873
  await ctx.reply("\u26D4 Session cancelled.", { parse_mode: "HTML" });
2277
2874
  }
@@ -2321,6 +2918,188 @@ Total sessions: ${sessions.length}`,
2321
2918
  );
2322
2919
  }
2323
2920
  }
2921
+ async function handleTopics(ctx, core) {
2922
+ try {
2923
+ const allRecords = core.sessionManager.listRecords();
2924
+ const records = allRecords.filter((r) => {
2925
+ const platform = r.platform;
2926
+ return !!platform?.topicId;
2927
+ });
2928
+ const headlessCount = allRecords.length - records.length;
2929
+ if (records.length === 0) {
2930
+ const extra = headlessCount > 0 ? ` (${headlessCount} headless hidden)` : "";
2931
+ await ctx.reply(`No sessions with topics found.${extra}`, { parse_mode: "HTML" });
2932
+ return;
2933
+ }
2934
+ const statusEmoji = {
2935
+ active: "\u{1F7E2}",
2936
+ initializing: "\u{1F7E1}",
2937
+ finished: "\u2705",
2938
+ error: "\u274C",
2939
+ cancelled: "\u26D4"
2940
+ };
2941
+ const statusOrder = { active: 0, initializing: 1, error: 2, finished: 3, cancelled: 4 };
2942
+ records.sort((a, b) => (statusOrder[a.status] ?? 5) - (statusOrder[b.status] ?? 5));
2943
+ const MAX_DISPLAY = 30;
2944
+ const displayed = records.slice(0, MAX_DISPLAY);
2945
+ const lines = displayed.map((r) => {
2946
+ const emoji = statusEmoji[r.status] || "\u26AA";
2947
+ const name = r.name?.trim();
2948
+ const label = name ? escapeHtml(name) : `<i>${escapeHtml(r.agentName)} session</i>`;
2949
+ return `${emoji} ${label} <code>[${r.status}]</code>`;
2950
+ });
2951
+ const header = `<b>Sessions: ${records.length}</b>` + (headlessCount > 0 ? ` (${headlessCount} headless hidden)` : "");
2952
+ const truncated = records.length > MAX_DISPLAY ? `
2953
+
2954
+ <i>...and ${records.length - MAX_DISPLAY} more</i>` : "";
2955
+ const finishedCount = records.filter((r) => r.status === "finished").length;
2956
+ const errorCount = records.filter((r) => r.status === "error" || r.status === "cancelled").length;
2957
+ const activeCount = records.filter((r) => r.status === "active" || r.status === "initializing").length;
2958
+ const keyboard = new InlineKeyboard();
2959
+ if (finishedCount > 0) {
2960
+ keyboard.text(`Cleanup finished (${finishedCount})`, "m:cleanup:finished").row();
2961
+ }
2962
+ if (errorCount > 0) {
2963
+ keyboard.text(`Cleanup errors (${errorCount})`, "m:cleanup:errors").row();
2964
+ }
2965
+ if (finishedCount + errorCount > 0) {
2966
+ keyboard.text(`Cleanup all non-active (${finishedCount + errorCount})`, "m:cleanup:all").row();
2967
+ }
2968
+ keyboard.text(`\u26A0\uFE0F Cleanup ALL (${records.length})`, "m:cleanup:everything").row();
2969
+ keyboard.text("Refresh", "m:topics");
2970
+ await ctx.reply(
2971
+ `${header}
2972
+
2973
+ ${lines.join("\n")}${truncated}`,
2974
+ { parse_mode: "HTML", reply_markup: keyboard }
2975
+ );
2976
+ } catch (err) {
2977
+ log8.error({ err }, "handleTopics error");
2978
+ await ctx.reply("\u274C Failed to list sessions.", { parse_mode: "HTML" }).catch(() => {
2979
+ });
2980
+ }
2981
+ }
2982
+ async function handleCleanup(ctx, core, chatId, statuses) {
2983
+ const allRecords = core.sessionManager.listRecords();
2984
+ const cleanable = allRecords.filter((r) => {
2985
+ const platform = r.platform;
2986
+ return !!platform?.topicId && statuses.includes(r.status);
2987
+ });
2988
+ if (cleanable.length === 0) {
2989
+ await ctx.reply("Nothing to clean up.", { parse_mode: "HTML" });
2990
+ return;
2991
+ }
2992
+ let deleted = 0;
2993
+ let failed = 0;
2994
+ for (const record of cleanable) {
2995
+ try {
2996
+ const topicId = record.platform?.topicId;
2997
+ if (topicId) {
2998
+ try {
2999
+ await ctx.api.deleteForumTopic(chatId, topicId);
3000
+ } catch (err) {
3001
+ log8.warn({ err, sessionId: record.sessionId, topicId }, "Failed to delete forum topic during cleanup");
3002
+ }
3003
+ }
3004
+ await core.sessionManager.removeRecord(record.sessionId);
3005
+ deleted++;
3006
+ } catch (err) {
3007
+ log8.error({ err, sessionId: record.sessionId }, "Failed to cleanup session");
3008
+ failed++;
3009
+ }
3010
+ }
3011
+ await ctx.reply(
3012
+ `\u{1F5D1} Cleaned up <b>${deleted}</b> sessions${failed > 0 ? ` (${failed} failed)` : ""}.`,
3013
+ { parse_mode: "HTML" }
3014
+ );
3015
+ }
3016
+ async function handleCleanupEverything(ctx, core, chatId, systemTopicIds) {
3017
+ const allRecords = core.sessionManager.listRecords();
3018
+ const cleanable = allRecords.filter((r) => {
3019
+ const platform = r.platform;
3020
+ if (!platform?.topicId) return false;
3021
+ if (systemTopicIds && (platform.topicId === systemTopicIds.notificationTopicId || platform.topicId === systemTopicIds.assistantTopicId)) return false;
3022
+ return true;
3023
+ });
3024
+ if (cleanable.length === 0) {
3025
+ await ctx.reply("Nothing to clean up.", { parse_mode: "HTML" });
3026
+ return;
3027
+ }
3028
+ const statusCounts = /* @__PURE__ */ new Map();
3029
+ for (const r of cleanable) {
3030
+ statusCounts.set(r.status, (statusCounts.get(r.status) ?? 0) + 1);
3031
+ }
3032
+ const statusEmoji = {
3033
+ active: "\u{1F7E2}",
3034
+ initializing: "\u{1F7E1}",
3035
+ finished: "\u2705",
3036
+ error: "\u274C",
3037
+ cancelled: "\u26D4"
3038
+ };
3039
+ const breakdown = Array.from(statusCounts.entries()).map(([status, count]) => `${statusEmoji[status] ?? "\u26AA"} ${status}: ${count}`).join("\n");
3040
+ const activeCount = (statusCounts.get("active") ?? 0) + (statusCounts.get("initializing") ?? 0);
3041
+ const activeWarning = activeCount > 0 ? `
3042
+
3043
+ \u26A0\uFE0F <b>${activeCount} active session(s) will be cancelled and their agents stopped!</b>` : "";
3044
+ const keyboard = new InlineKeyboard().text("Yes, delete all", "m:cleanup:everything:confirm").text("Cancel", "m:topics");
3045
+ await ctx.reply(
3046
+ `<b>Delete ${cleanable.length} topics?</b>
3047
+
3048
+ This will:
3049
+ \u2022 Delete all session topics from this group
3050
+ \u2022 Cancel any running agent sessions
3051
+ \u2022 Remove all session records
3052
+
3053
+ <b>Breakdown:</b>
3054
+ ${breakdown}${activeWarning}
3055
+
3056
+ <i>Notifications and Assistant topics will NOT be deleted.</i>`,
3057
+ { parse_mode: "HTML", reply_markup: keyboard }
3058
+ );
3059
+ }
3060
+ async function handleCleanupEverythingConfirmed(ctx, core, chatId, systemTopicIds) {
3061
+ const allRecords = core.sessionManager.listRecords();
3062
+ const cleanable = allRecords.filter((r) => {
3063
+ const platform = r.platform;
3064
+ if (!platform?.topicId) return false;
3065
+ if (systemTopicIds && (platform.topicId === systemTopicIds.notificationTopicId || platform.topicId === systemTopicIds.assistantTopicId)) return false;
3066
+ return true;
3067
+ });
3068
+ if (cleanable.length === 0) {
3069
+ await ctx.reply("Nothing to clean up.", { parse_mode: "HTML" });
3070
+ return;
3071
+ }
3072
+ let deleted = 0;
3073
+ let failed = 0;
3074
+ for (const record of cleanable) {
3075
+ try {
3076
+ if (record.status === "active" || record.status === "initializing") {
3077
+ try {
3078
+ await core.sessionManager.cancelSession(record.sessionId);
3079
+ } catch (err) {
3080
+ log8.warn({ err, sessionId: record.sessionId }, "Failed to cancel session during cleanup");
3081
+ }
3082
+ }
3083
+ const topicId = record.platform?.topicId;
3084
+ if (topicId) {
3085
+ try {
3086
+ await ctx.api.deleteForumTopic(chatId, topicId);
3087
+ } catch (err) {
3088
+ log8.warn({ err, sessionId: record.sessionId, topicId }, "Failed to delete forum topic during cleanup");
3089
+ }
3090
+ }
3091
+ await core.sessionManager.removeRecord(record.sessionId);
3092
+ deleted++;
3093
+ } catch (err) {
3094
+ log8.error({ err, sessionId: record.sessionId }, "Failed to cleanup session");
3095
+ failed++;
3096
+ }
3097
+ }
3098
+ await ctx.reply(
3099
+ `\u{1F5D1} Cleaned up <b>${deleted}</b> sessions${failed > 0 ? ` (${failed} failed)` : ""}.`,
3100
+ { parse_mode: "HTML" }
3101
+ );
3102
+ }
2324
3103
  async function handleAgents(ctx, core) {
2325
3104
  const agents = core.agentManager.getAvailableAgents();
2326
3105
  const defaultAgent = core.configManager.get().defaultAgent;
@@ -2365,7 +3144,7 @@ function setupDangerousModeCallbacks(bot, core) {
2365
3144
  const session = core.sessionManager.getSession(sessionId);
2366
3145
  if (session) {
2367
3146
  session.dangerousMode = !session.dangerousMode;
2368
- log7.info({ sessionId, dangerousMode: session.dangerousMode }, "Dangerous mode toggled via button");
3147
+ log8.info({ sessionId, dangerousMode: session.dangerousMode }, "Dangerous mode toggled via button");
2369
3148
  core.sessionManager.updateSessionDangerousMode(sessionId, session.dangerousMode).catch(() => {
2370
3149
  });
2371
3150
  const toastText2 = session.dangerousMode ? "\u2620\uFE0F Dangerous mode enabled \u2014 permissions auto-approved" : "\u{1F510} Dangerous mode disabled \u2014 permissions shown normally";
@@ -2392,7 +3171,7 @@ function setupDangerousModeCallbacks(bot, core) {
2392
3171
  const newDangerousMode = !(record.dangerousMode ?? false);
2393
3172
  core.sessionManager.updateSessionDangerousMode(sessionId, newDangerousMode).catch(() => {
2394
3173
  });
2395
- log7.info({ sessionId, dangerousMode: newDangerousMode }, "Dangerous mode toggled via button (store-only, session not in memory)");
3174
+ log8.info({ sessionId, dangerousMode: newDangerousMode }, "Dangerous mode toggled via button (store-only, session not in memory)");
2396
3175
  const toastText = newDangerousMode ? "\u2620\uFE0F Dangerous mode enabled \u2014 permissions auto-approved" : "\u{1F510} Dangerous mode disabled \u2014 permissions shown normally";
2397
3176
  try {
2398
3177
  await ctx.answerCallbackQuery({ text: toastText });
@@ -2560,7 +3339,7 @@ async function executeNewSession(bot, core, chatId, agentName, workspace) {
2560
3339
  });
2561
3340
  const finalName = `\u{1F504} ${session.agentName} \u2014 New Session`;
2562
3341
  await renameSessionTopic(bot, chatId, threadId, finalName);
2563
- session.warmup().catch((err) => log7.error({ err }, "Warm-up error"));
3342
+ session.warmup().catch((err) => log8.error({ err }, "Warm-up error"));
2564
3343
  return { session, threadId, firstMsgId };
2565
3344
  } catch (err) {
2566
3345
  try {
@@ -2577,16 +3356,134 @@ async function executeCancelSession(core, excludeSessionId) {
2577
3356
  await session.cancel();
2578
3357
  return session;
2579
3358
  }
3359
+ async function handleIntegrate(ctx, _core) {
3360
+ const { listIntegrations } = await import("./integrate-WUPLRJD3.js");
3361
+ const agents = listIntegrations();
3362
+ const keyboard = new InlineKeyboard();
3363
+ for (const agent of agents) {
3364
+ keyboard.text(`\u{1F916} ${agent}`, `i:agent:${agent}`).row();
3365
+ }
3366
+ await ctx.reply(
3367
+ `<b>\u{1F517} Integrations</b>
3368
+
3369
+ Select an agent to manage its integrations.`,
3370
+ { parse_mode: "HTML", reply_markup: keyboard }
3371
+ );
3372
+ }
3373
+ function buildAgentItemsKeyboard(agentName, items) {
3374
+ const keyboard = new InlineKeyboard();
3375
+ for (const item of items) {
3376
+ const installed = item.isInstalled();
3377
+ keyboard.text(
3378
+ installed ? `\u2705 ${item.name} \u2014 Uninstall` : `\u{1F4E6} ${item.name} \u2014 Install`,
3379
+ installed ? `i:uninstall:${agentName}:${item.id}` : `i:install:${agentName}:${item.id}`
3380
+ ).row();
3381
+ }
3382
+ keyboard.text("\u2190 Back", "i:back").row();
3383
+ return keyboard;
3384
+ }
3385
+ function setupIntegrateCallbacks(bot, core) {
3386
+ bot.callbackQuery(/^i:/, async (ctx) => {
3387
+ const data = ctx.callbackQuery.data;
3388
+ try {
3389
+ await ctx.answerCallbackQuery();
3390
+ } catch {
3391
+ }
3392
+ if (data === "i:back") {
3393
+ const { listIntegrations } = await import("./integrate-WUPLRJD3.js");
3394
+ const agents = listIntegrations();
3395
+ const keyboard2 = new InlineKeyboard();
3396
+ for (const agent of agents) {
3397
+ keyboard2.text(`\u{1F916} ${agent}`, `i:agent:${agent}`).row();
3398
+ }
3399
+ try {
3400
+ await ctx.editMessageText(
3401
+ `<b>\u{1F517} Integrations</b>
3402
+
3403
+ Select an agent to manage its integrations.`,
3404
+ { parse_mode: "HTML", reply_markup: keyboard2 }
3405
+ );
3406
+ } catch {
3407
+ }
3408
+ return;
3409
+ }
3410
+ const agentMatch = data.match(/^i:agent:(.+)$/);
3411
+ if (agentMatch) {
3412
+ const agentName2 = agentMatch[1];
3413
+ const { getIntegration: getIntegration2 } = await import("./integrate-WUPLRJD3.js");
3414
+ const integration2 = getIntegration2(agentName2);
3415
+ if (!integration2) {
3416
+ await ctx.reply(`\u274C No integration available for '${escapeHtml(agentName2)}'.`, { parse_mode: "HTML" });
3417
+ return;
3418
+ }
3419
+ const keyboard2 = buildAgentItemsKeyboard(agentName2, integration2.items);
3420
+ try {
3421
+ await ctx.editMessageText(
3422
+ `<b>\u{1F517} ${escapeHtml(agentName2)} Integrations</b>
3423
+
3424
+ ${integration2.items.map((i) => `\u2022 <b>${escapeHtml(i.name)}</b> \u2014 ${escapeHtml(i.description)}`).join("\n")}`,
3425
+ { parse_mode: "HTML", reply_markup: keyboard2 }
3426
+ );
3427
+ } catch {
3428
+ await ctx.reply(
3429
+ `<b>\u{1F517} ${escapeHtml(agentName2)} Integrations</b>`,
3430
+ { parse_mode: "HTML", reply_markup: keyboard2 }
3431
+ );
3432
+ }
3433
+ return;
3434
+ }
3435
+ const actionMatch = data.match(/^i:(install|uninstall):([^:]+):(.+)$/);
3436
+ if (!actionMatch) return;
3437
+ const action = actionMatch[1];
3438
+ const agentName = actionMatch[2];
3439
+ const itemId = actionMatch[3];
3440
+ const { getIntegration } = await import("./integrate-WUPLRJD3.js");
3441
+ const integration = getIntegration(agentName);
3442
+ if (!integration) return;
3443
+ const item = integration.items.find((i) => i.id === itemId);
3444
+ if (!item) return;
3445
+ const result = action === "install" ? await item.install() : await item.uninstall();
3446
+ const installed = action === "install" && result.success;
3447
+ await core.configManager.save({
3448
+ integrations: {
3449
+ [agentName]: {
3450
+ installed,
3451
+ installedAt: installed ? (/* @__PURE__ */ new Date()).toISOString() : void 0
3452
+ }
3453
+ }
3454
+ });
3455
+ const statusEmoji = result.success ? "\u2705" : "\u274C";
3456
+ const actionLabel = action === "install" ? "installed" : "uninstalled";
3457
+ const logsText = result.logs.map((l) => `<code>${escapeHtml(l)}</code>`).join("\n");
3458
+ const resultText = `${statusEmoji} <b>${escapeHtml(item.name)}</b> ${actionLabel}.
3459
+
3460
+ ${logsText}`;
3461
+ const keyboard = buildAgentItemsKeyboard(agentName, integration.items);
3462
+ try {
3463
+ await ctx.editMessageText(
3464
+ `<b>\u{1F517} ${escapeHtml(agentName)} Integrations</b>
3465
+
3466
+ ${resultText}`,
3467
+ { parse_mode: "HTML", reply_markup: keyboard }
3468
+ );
3469
+ } catch {
3470
+ await ctx.reply(resultText, { parse_mode: "HTML" });
3471
+ }
3472
+ });
3473
+ }
2580
3474
  var STATIC_COMMANDS = [
2581
3475
  { command: "new", description: "Create new session" },
2582
3476
  { command: "newchat", description: "New chat, same agent & workspace" },
2583
3477
  { command: "cancel", description: "Cancel current session" },
2584
3478
  { command: "status", description: "Show status" },
3479
+ { command: "sessions", description: "List all sessions" },
2585
3480
  { command: "agents", description: "List available agents" },
2586
3481
  { command: "help", description: "Help" },
2587
3482
  { command: "menu", description: "Show menu" },
2588
3483
  { command: "enable_dangerous", description: "Auto-approve all permission requests (session only)" },
2589
3484
  { command: "disable_dangerous", description: "Restore normal permission prompts (session only)" },
3485
+ { command: "integrate", description: "Manage agent integrations" },
3486
+ { command: "handoff", description: "Continue this session in your terminal" },
2590
3487
  { command: "restart", description: "Restart OpenACP" },
2591
3488
  { command: "update", description: "Update to latest version and restart" }
2592
3489
  ];
@@ -2594,7 +3491,7 @@ var STATIC_COMMANDS = [
2594
3491
  // src/adapters/telegram/permissions.ts
2595
3492
  import { InlineKeyboard as InlineKeyboard2 } from "grammy";
2596
3493
  import { nanoid as nanoid2 } from "nanoid";
2597
- var log8 = createChildLogger({ module: "telegram-permissions" });
3494
+ var log9 = createChildLogger({ module: "telegram-permissions" });
2598
3495
  var PermissionHandler = class {
2599
3496
  constructor(bot, chatId, getSession, sendNotification) {
2600
3497
  this.bot = bot;
@@ -2654,7 +3551,7 @@ ${escapeHtml(request.description)}`,
2654
3551
  }
2655
3552
  const session = this.getSession(pending.sessionId);
2656
3553
  const isAllow = pending.options.find((o) => o.id === optionId)?.isAllow ?? false;
2657
- log8.info({ requestId: pending.requestId, optionId, isAllow }, "Permission responded");
3554
+ log9.info({ requestId: pending.requestId, optionId, isAllow }, "Permission responded");
2658
3555
  if (session?.permissionGate.requestId === pending.requestId) {
2659
3556
  session.permissionGate.resolve(optionId);
2660
3557
  }
@@ -2672,10 +3569,10 @@ ${escapeHtml(request.description)}`,
2672
3569
  };
2673
3570
 
2674
3571
  // src/adapters/telegram/assistant.ts
2675
- var log9 = createChildLogger({ module: "telegram-assistant" });
3572
+ var log10 = createChildLogger({ module: "telegram-assistant" });
2676
3573
  async function spawnAssistant(core, adapter, assistantTopicId) {
2677
3574
  const config = core.configManager.get();
2678
- log9.info({ agent: config.defaultAgent }, "Creating assistant session...");
3575
+ log10.info({ agent: config.defaultAgent }, "Creating assistant session...");
2679
3576
  const session = await core.sessionManager.createSession(
2680
3577
  "telegram",
2681
3578
  config.defaultAgent,
@@ -2684,30 +3581,44 @@ async function spawnAssistant(core, adapter, assistantTopicId) {
2684
3581
  );
2685
3582
  session.threadId = String(assistantTopicId);
2686
3583
  session.name = "Assistant";
2687
- log9.info({ sessionId: session.id }, "Assistant agent spawned");
3584
+ log10.info({ sessionId: session.id }, "Assistant agent spawned");
2688
3585
  core.wireSessionEvents(session, adapter);
2689
- const systemPrompt = buildAssistantSystemPrompt(config);
3586
+ const allRecords = core.sessionManager.listRecords();
3587
+ const activeCount = allRecords.filter((r) => r.status === "active" || r.status === "initializing").length;
3588
+ const statusCounts = /* @__PURE__ */ new Map();
3589
+ for (const r of allRecords) {
3590
+ statusCounts.set(r.status, (statusCounts.get(r.status) ?? 0) + 1);
3591
+ }
3592
+ const topicSummary = Array.from(statusCounts.entries()).map(([status, count]) => ({ status, count }));
3593
+ const ctx = {
3594
+ config,
3595
+ activeSessionCount: activeCount,
3596
+ totalSessionCount: allRecords.length,
3597
+ topicSummary
3598
+ };
3599
+ const systemPrompt = buildAssistantSystemPrompt(ctx);
2690
3600
  const ready = session.enqueuePrompt(systemPrompt).then(() => {
2691
- log9.info({ sessionId: session.id }, "Assistant system prompt completed");
3601
+ log10.info({ sessionId: session.id }, "Assistant system prompt completed");
2692
3602
  }).catch((err) => {
2693
- log9.warn({ err }, "Assistant system prompt failed");
3603
+ log10.warn({ err }, "Assistant system prompt failed");
2694
3604
  });
2695
3605
  return { session, ready };
2696
3606
  }
2697
- function buildAssistantSystemPrompt(config) {
3607
+ function buildAssistantSystemPrompt(ctx) {
3608
+ const { config, activeSessionCount, totalSessionCount, topicSummary } = ctx;
2698
3609
  const agentNames = Object.keys(config.agents).join(", ");
2699
- return `You are the OpenACP Assistant. Help users manage their AI coding sessions.
3610
+ const topicBreakdown = topicSummary.map((s) => `${s.status}: ${s.count}`).join(", ") || "none";
3611
+ return `You are the OpenACP Assistant. Help users manage their AI coding sessions and topics.
2700
3612
 
2701
- Available agents: ${agentNames}
2702
- Default agent: ${config.defaultAgent}
2703
- Workspace base: ${config.workspace.baseDir}
3613
+ ## Current State
3614
+ - Active sessions: ${activeSessionCount} / ${totalSessionCount} total
3615
+ - Topics by status: ${topicBreakdown}
3616
+ - Available agents: ${agentNames}
3617
+ - Default agent: ${config.defaultAgent}
3618
+ - Workspace base: ${config.workspace.baseDir}
2704
3619
 
2705
- When a user wants to create a session, guide them through:
2706
- 1. Which agent to use
2707
- 2. Which workspace/project
2708
- 3. Confirm and create
2709
-
2710
- Commands reference:
3620
+ ## Session Management Commands
3621
+ These are Telegram bot commands (type directly in chat):
2711
3622
  - /new [agent] [workspace] \u2014 Create new session
2712
3623
  - /newchat \u2014 New chat with same agent & workspace
2713
3624
  - /cancel \u2014 Cancel current session
@@ -2715,7 +3626,48 @@ Commands reference:
2715
3626
  - /agents \u2014 List agents
2716
3627
  - /help \u2014 Show help
2717
3628
 
2718
- Be concise and helpful. When the user confirms session creation, tell them you'll create it now.`;
3629
+ ## Management Commands (via CLI)
3630
+ You have access to bash. Use these commands to manage OpenACP:
3631
+
3632
+ ### Session management
3633
+ \`\`\`bash
3634
+ openacp api status # List active sessions
3635
+ openacp api session <id> # Session detail
3636
+ openacp api send <id> "prompt text" # Send prompt to session
3637
+ openacp api cancel <id> # Cancel session
3638
+ openacp api dangerous <id> on|off # Toggle dangerous mode
3639
+ \`\`\`
3640
+
3641
+ ### Topic management
3642
+ \`\`\`bash
3643
+ openacp api topics # List topics
3644
+ openacp api topics --status finished,error
3645
+ openacp api delete-topic <id> # Delete topic
3646
+ openacp api delete-topic <id> --force # Force delete active
3647
+ openacp api cleanup # Cleanup finished topics
3648
+ openacp api cleanup --status finished,error
3649
+ \`\`\`
3650
+
3651
+ ### System
3652
+ \`\`\`bash
3653
+ openacp api health # System health
3654
+ openacp api config # Show config
3655
+ openacp api config set <key> <value> # Update config
3656
+ openacp api adapters # List adapters
3657
+ openacp api tunnel # Tunnel status
3658
+ openacp api notify "message" # Send notification
3659
+ openacp api version # Daemon version
3660
+ openacp api restart # Restart daemon
3661
+ \`\`\`
3662
+
3663
+ ## Guidelines
3664
+ - When a user asks about sessions or topics, run \`openacp api topics\` or \`openacp api status\` to get current data.
3665
+ - When deleting: if the session is active/initializing, warn the user first. Only use --force if they confirm.
3666
+ - Use \`openacp api health\` to check system status.
3667
+ - Use \`openacp api config\` to check configuration, \`openacp api config set\` to update values.
3668
+ - Format responses nicely for Telegram (use bold, code blocks).
3669
+ - Be concise and helpful. Respond in the same language the user uses.
3670
+ - When creating sessions, guide through: agent selection \u2192 workspace \u2192 confirm.`;
2719
3671
  }
2720
3672
  async function handleAssistantMessage(session, text) {
2721
3673
  if (!session) return;
@@ -2728,7 +3680,7 @@ function redirectToAssistant(chatId, assistantTopicId) {
2728
3680
  }
2729
3681
 
2730
3682
  // src/adapters/telegram/activity.ts
2731
- var log10 = createChildLogger({ module: "telegram:activity" });
3683
+ var log11 = createChildLogger({ module: "telegram:activity" });
2732
3684
  var THINKING_REFRESH_MS = 15e3;
2733
3685
  var THINKING_MAX_MS = 3 * 60 * 1e3;
2734
3686
  var ThinkingIndicator = class {
@@ -2760,7 +3712,7 @@ var ThinkingIndicator = class {
2760
3712
  this.startRefreshTimer();
2761
3713
  }
2762
3714
  } catch (err) {
2763
- log10.warn({ err }, "ThinkingIndicator.show() failed");
3715
+ log11.warn({ err }, "ThinkingIndicator.show() failed");
2764
3716
  } finally {
2765
3717
  this.sending = false;
2766
3718
  }
@@ -2833,7 +3785,7 @@ var UsageMessage = class {
2833
3785
  if (result) this.msgId = result.message_id;
2834
3786
  }
2835
3787
  } catch (err) {
2836
- log10.warn({ err }, "UsageMessage.send() failed");
3788
+ log11.warn({ err }, "UsageMessage.send() failed");
2837
3789
  }
2838
3790
  }
2839
3791
  getMsgId() {
@@ -2846,7 +3798,7 @@ var UsageMessage = class {
2846
3798
  try {
2847
3799
  await this.sendQueue.enqueue(() => this.api.deleteMessage(this.chatId, id));
2848
3800
  } catch (err) {
2849
- log10.warn({ err }, "UsageMessage.delete() failed");
3801
+ log11.warn({ err }, "UsageMessage.delete() failed");
2850
3802
  }
2851
3803
  }
2852
3804
  };
@@ -2881,6 +3833,7 @@ var PlanCard = class {
2881
3833
  msgId;
2882
3834
  flushPromise = Promise.resolve();
2883
3835
  latestEntries;
3836
+ lastSentText;
2884
3837
  flushTimer;
2885
3838
  update(entries) {
2886
3839
  this.latestEntries = entries;
@@ -2911,6 +3864,8 @@ var PlanCard = class {
2911
3864
  async _flush() {
2912
3865
  if (!this.latestEntries) return;
2913
3866
  const text = formatPlanCard(this.latestEntries);
3867
+ if (this.msgId && text === this.lastSentText) return;
3868
+ this.lastSentText = text;
2914
3869
  try {
2915
3870
  if (this.msgId) {
2916
3871
  await this.sendQueue.enqueue(
@@ -2929,7 +3884,7 @@ var PlanCard = class {
2929
3884
  if (result) this.msgId = result.message_id;
2930
3885
  }
2931
3886
  } catch (err) {
2932
- log10.warn({ err }, "PlanCard flush failed");
3887
+ log11.warn({ err }, "PlanCard flush failed");
2933
3888
  }
2934
3889
  }
2935
3890
  };
@@ -2992,7 +3947,7 @@ var ActivityTracker = class {
2992
3947
  })
2993
3948
  );
2994
3949
  } catch (err) {
2995
- log10.warn({ err }, "ActivityTracker.onComplete() Done send failed");
3950
+ log11.warn({ err }, "ActivityTracker.onComplete() Done send failed");
2996
3951
  }
2997
3952
  }
2998
3953
  }
@@ -3019,19 +3974,19 @@ var TelegramSendQueue = class {
3019
3974
  enqueue(fn, opts) {
3020
3975
  const type = opts?.type ?? "other";
3021
3976
  const key = opts?.key;
3022
- return new Promise((resolve, reject) => {
3977
+ return new Promise((resolve2, reject) => {
3023
3978
  if (type === "text" && key) {
3024
3979
  const idx = this.items.findIndex(
3025
3980
  (item) => item.type === "text" && item.key === key
3026
3981
  );
3027
3982
  if (idx !== -1) {
3028
3983
  this.items[idx].resolve(void 0);
3029
- this.items[idx] = { fn, type, key, resolve, reject };
3984
+ this.items[idx] = { fn, type, key, resolve: resolve2, reject };
3030
3985
  this.scheduleProcess();
3031
3986
  return;
3032
3987
  }
3033
3988
  }
3034
- this.items.push({ fn, type, key, resolve, reject });
3989
+ this.items.push({ fn, type, key, resolve: resolve2, reject });
3035
3990
  this.scheduleProcess();
3036
3991
  });
3037
3992
  }
@@ -3219,7 +4174,7 @@ function setupActionCallbacks(bot, core, chatId, getAssistantSessionId) {
3219
4174
  }
3220
4175
 
3221
4176
  // src/adapters/telegram/adapter.ts
3222
- var log11 = createChildLogger({ module: "telegram" });
4177
+ var log12 = createChildLogger({ module: "telegram" });
3223
4178
  function patchedFetch(input, init) {
3224
4179
  if (init?.signal && !(init.signal instanceof AbortSignal)) {
3225
4180
  const nativeController = new AbortController();
@@ -3270,7 +4225,7 @@ var TelegramAdapter = class extends ChannelAdapter {
3270
4225
  this.bot = new Bot(this.telegramConfig.botToken, { client: { fetch: patchedFetch } });
3271
4226
  this.bot.catch((err) => {
3272
4227
  const rootCause = err.error instanceof Error ? err.error : err;
3273
- log11.error({ err: rootCause }, "Telegram bot error");
4228
+ log12.error({ err: rootCause }, "Telegram bot error");
3274
4229
  });
3275
4230
  this.bot.api.config.use(async (prev, method, payload, signal) => {
3276
4231
  const maxRetries = 3;
@@ -3284,7 +4239,7 @@ var TelegramAdapter = class extends ChannelAdapter {
3284
4239
  if (rateLimitedMethods.includes(method)) {
3285
4240
  this.sendQueue.onRateLimited();
3286
4241
  }
3287
- log11.warn(
4242
+ log12.warn(
3288
4243
  { method, retryAfter, attempt: attempt + 1 },
3289
4244
  "Rate limited by Telegram, retrying"
3290
4245
  );
@@ -3335,10 +4290,12 @@ var TelegramAdapter = class extends ChannelAdapter {
3335
4290
  this.telegramConfig.chatId,
3336
4291
  () => this.assistantSession?.id
3337
4292
  );
4293
+ setupIntegrateCallbacks(this.bot, this.core);
3338
4294
  setupMenuCallbacks(
3339
4295
  this.bot,
3340
4296
  this.core,
3341
- this.telegramConfig.chatId
4297
+ this.telegramConfig.chatId,
4298
+ { notificationTopicId: this.notificationTopicId, assistantTopicId: this.assistantTopicId }
3342
4299
  );
3343
4300
  setupCommands(
3344
4301
  this.bot,
@@ -3350,10 +4307,48 @@ var TelegramAdapter = class extends ChannelAdapter {
3350
4307
  }
3351
4308
  );
3352
4309
  this.permissionHandler.setupCallbackHandler();
4310
+ this.bot.command("handoff", async (ctx) => {
4311
+ const threadId = ctx.message?.message_thread_id;
4312
+ if (!threadId) return;
4313
+ if (threadId === this.notificationTopicId || threadId === this.assistantTopicId) {
4314
+ await ctx.reply("This command only works in session topics.", {
4315
+ message_thread_id: threadId
4316
+ });
4317
+ return;
4318
+ }
4319
+ const session = this.core.sessionManager.getSessionByThread("telegram", String(threadId));
4320
+ const record = session ? void 0 : this.core.sessionManager.getRecordByThread("telegram", String(threadId));
4321
+ const agentName = session?.agentName ?? record?.agentName;
4322
+ const agentSessionId = session?.agentSessionId ?? record?.agentSessionId;
4323
+ if (!agentName || !agentSessionId) {
4324
+ await ctx.reply("No session found for this topic.", {
4325
+ message_thread_id: threadId
4326
+ });
4327
+ return;
4328
+ }
4329
+ const { getAgentCapabilities: getAgentCapabilities2 } = await import("./agent-registry-7HC6D4CH.js");
4330
+ const caps = getAgentCapabilities2(agentName);
4331
+ if (!caps.supportsResume || !caps.resumeCommand) {
4332
+ await ctx.reply("This agent does not support session transfer.", {
4333
+ message_thread_id: threadId
4334
+ });
4335
+ return;
4336
+ }
4337
+ const command = caps.resumeCommand(agentSessionId);
4338
+ await ctx.reply(
4339
+ `Run this in your terminal to continue the session:
4340
+
4341
+ <code>${command}</code>`,
4342
+ {
4343
+ message_thread_id: threadId,
4344
+ parse_mode: "HTML"
4345
+ }
4346
+ );
4347
+ });
3353
4348
  this.setupRoutes();
3354
4349
  this.bot.start({
3355
4350
  allowed_updates: ["message", "callback_query"],
3356
- onStart: () => log11.info(
4351
+ onStart: () => log12.info(
3357
4352
  { chatId: this.telegramConfig.chatId },
3358
4353
  "Telegram bot started"
3359
4354
  )
@@ -3363,10 +4358,13 @@ var TelegramAdapter = class extends ChannelAdapter {
3363
4358
  const agents = this.core.agentManager.getAvailableAgents();
3364
4359
  const agentList = agents.map((a) => `${escapeHtml(a.name)}${a.name === config.defaultAgent ? " (default)" : ""}`).join(", ");
3365
4360
  const workspace = escapeHtml(config.workspace.baseDir);
4361
+ const allRecords = this.core.sessionManager.listRecords();
4362
+ const activeCount = allRecords.filter((r) => r.status === "active" || r.status === "initializing").length;
3366
4363
  const welcomeText = `\u{1F44B} <b>OpenACP Assistant</b> is online.
3367
4364
 
3368
4365
  Available agents: ${agentList}
3369
4366
  Workspace: <code>${workspace}</code>
4367
+ Sessions: ${activeCount} active / ${allRecords.length} total
3370
4368
 
3371
4369
  <b>Select an action:</b>`;
3372
4370
  await this.bot.api.sendMessage(this.telegramConfig.chatId, welcomeText, {
@@ -3375,10 +4373,10 @@ Workspace: <code>${workspace}</code>
3375
4373
  reply_markup: buildMenuKeyboard()
3376
4374
  });
3377
4375
  } catch (err) {
3378
- log11.warn({ err }, "Failed to send welcome message");
4376
+ log12.warn({ err }, "Failed to send welcome message");
3379
4377
  }
3380
4378
  try {
3381
- log11.info("Spawning assistant session...");
4379
+ log12.info("Spawning assistant session...");
3382
4380
  const { session, ready } = await spawnAssistant(
3383
4381
  this.core,
3384
4382
  this,
@@ -3386,13 +4384,13 @@ Workspace: <code>${workspace}</code>
3386
4384
  );
3387
4385
  this.assistantSession = session;
3388
4386
  this.assistantInitializing = true;
3389
- log11.info({ sessionId: session.id }, "Assistant session ready, system prompt running in background");
4387
+ log12.info({ sessionId: session.id }, "Assistant session ready, system prompt running in background");
3390
4388
  ready.then(() => {
3391
4389
  this.assistantInitializing = false;
3392
- log11.info({ sessionId: session.id }, "Assistant ready for user messages");
4390
+ log12.info({ sessionId: session.id }, "Assistant ready for user messages");
3393
4391
  });
3394
4392
  } catch (err) {
3395
- log11.error({ err }, "Failed to spawn assistant");
4393
+ log12.error({ err }, "Failed to spawn assistant");
3396
4394
  this.bot.api.sendMessage(
3397
4395
  this.telegramConfig.chatId,
3398
4396
  `\u26A0\uFE0F <b>Failed to start assistant session.</b>
@@ -3408,7 +4406,7 @@ Workspace: <code>${workspace}</code>
3408
4406
  await this.assistantSession.destroy();
3409
4407
  }
3410
4408
  await this.bot.stop();
3411
- log11.info("Telegram bot stopped");
4409
+ log12.info("Telegram bot stopped");
3412
4410
  }
3413
4411
  setupRoutes() {
3414
4412
  this.bot.on("message:text", async (ctx) => {
@@ -3431,7 +4429,7 @@ Workspace: <code>${workspace}</code>
3431
4429
  ctx.replyWithChatAction("typing").catch(() => {
3432
4430
  });
3433
4431
  handleAssistantMessage(this.assistantSession, ctx.message.text).catch(
3434
- (err) => log11.error({ err }, "Assistant error")
4432
+ (err) => log12.error({ err }, "Assistant error")
3435
4433
  );
3436
4434
  return;
3437
4435
  }
@@ -3448,7 +4446,7 @@ Workspace: <code>${workspace}</code>
3448
4446
  threadId: String(threadId),
3449
4447
  userId: String(ctx.from.id),
3450
4448
  text: ctx.message.text
3451
- }).catch((err) => log11.error({ err }, "handleMessage error"));
4449
+ }).catch((err) => log12.error({ err }, "handleMessage error"));
3452
4450
  });
3453
4451
  }
3454
4452
  // --- ChannelAdapter implementations ---
@@ -3528,7 +4526,7 @@ Workspace: <code>${workspace}</code>
3528
4526
  if (toolState) {
3529
4527
  if (meta.viewerLinks) {
3530
4528
  toolState.viewerLinks = meta.viewerLinks;
3531
- log11.debug({ toolId: meta.id, viewerLinks: meta.viewerLinks }, "Accumulated viewerLinks");
4529
+ log12.debug({ toolId: meta.id, viewerLinks: meta.viewerLinks }, "Accumulated viewerLinks");
3532
4530
  }
3533
4531
  const viewerFilePath = content.metadata?.viewerFilePath;
3534
4532
  if (viewerFilePath) toolState.viewerFilePath = viewerFilePath;
@@ -3537,7 +4535,7 @@ Workspace: <code>${workspace}</code>
3537
4535
  const isTerminal = meta.status === "completed" || meta.status === "failed";
3538
4536
  if (!isTerminal && !meta.viewerLinks) break;
3539
4537
  await toolState.ready;
3540
- log11.debug(
4538
+ log12.debug(
3541
4539
  { toolId: meta.id, status: meta.status, hasViewerLinks: !!toolState.viewerLinks, viewerLinks: toolState.viewerLinks, name: toolState.name, msgId: toolState.msgId },
3542
4540
  "Tool completed, preparing edit"
3543
4541
  );
@@ -3559,7 +4557,7 @@ Workspace: <code>${workspace}</code>
3559
4557
  )
3560
4558
  );
3561
4559
  } catch (err) {
3562
- log11.warn(
4560
+ log12.warn(
3563
4561
  { err, msgId: toolState.msgId, textLen: formattedText.length, hasViewerLinks: !!merged.viewerLinks },
3564
4562
  "Tool update edit failed"
3565
4563
  );
@@ -3654,15 +4652,23 @@ Task completed.
3654
4652
  }
3655
4653
  }
3656
4654
  async sendPermissionRequest(sessionId, request) {
3657
- log11.info({ sessionId, requestId: request.id }, "Permission request sent");
4655
+ log12.info({ sessionId, requestId: request.id }, "Permission request sent");
3658
4656
  const session = this.core.sessionManager.getSession(
3659
4657
  sessionId
3660
4658
  );
3661
4659
  if (!session) return;
4660
+ if (request.description.includes("openacp")) {
4661
+ const allowOption = request.options.find((o) => o.isAllow);
4662
+ if (allowOption && session.permissionGate.requestId === request.id) {
4663
+ log12.info({ sessionId, requestId: request.id }, "Auto-approving openacp command");
4664
+ session.permissionGate.resolve(allowOption.id);
4665
+ }
4666
+ return;
4667
+ }
3662
4668
  if (session.dangerousMode) {
3663
4669
  const allowOption = request.options.find((o) => o.isAllow);
3664
4670
  if (allowOption && session.permissionGate.requestId === request.id) {
3665
- log11.info({ sessionId, requestId: request.id, optionId: allowOption.id }, "Dangerous mode: auto-approving permission");
4671
+ log12.info({ sessionId, requestId: request.id, optionId: allowOption.id }, "Dangerous mode: auto-approving permission");
3666
4672
  session.permissionGate.resolve(allowOption.id);
3667
4673
  }
3668
4674
  return;
@@ -3673,7 +4679,7 @@ Task completed.
3673
4679
  }
3674
4680
  async sendNotification(notification) {
3675
4681
  if (notification.sessionId === this.assistantSession?.id) return;
3676
- log11.info(
4682
+ log12.info(
3677
4683
  { sessionId: notification.sessionId, type: notification.type },
3678
4684
  "Notification sent"
3679
4685
  );
@@ -3709,7 +4715,7 @@ Task completed.
3709
4715
  );
3710
4716
  }
3711
4717
  async createSessionThread(sessionId, name) {
3712
- log11.info({ sessionId, name }, "Session topic created");
4718
+ log12.info({ sessionId, name }, "Session topic created");
3713
4719
  return String(
3714
4720
  await createSessionTopic(this.bot, this.telegramConfig.chatId, name)
3715
4721
  );
@@ -3730,6 +4736,17 @@ Task completed.
3730
4736
  newName
3731
4737
  );
3732
4738
  }
4739
+ async deleteSessionThread(sessionId) {
4740
+ const record = this.core.sessionManager.getSessionRecord(sessionId);
4741
+ const platform = record?.platform;
4742
+ const topicId = platform?.topicId;
4743
+ if (!topicId) return;
4744
+ try {
4745
+ await this.bot.api.deleteForumTopic(this.telegramConfig.chatId, topicId);
4746
+ } catch (err) {
4747
+ log12.warn({ err, sessionId, topicId }, "Failed to delete forum topic (may already be deleted)");
4748
+ }
4749
+ }
3733
4750
  async sendSkillCommands(sessionId, commands) {
3734
4751
  if (sessionId === this.assistantSession?.id) return;
3735
4752
  const session = this.core.sessionManager.getSession(sessionId);
@@ -3800,7 +4817,7 @@ Task completed.
3800
4817
  { disable_notification: true }
3801
4818
  );
3802
4819
  } catch (err) {
3803
- log11.error({ err, sessionId }, "Failed to send skill commands");
4820
+ log12.error({ err, sessionId }, "Failed to send skill commands");
3804
4821
  }
3805
4822
  }
3806
4823
  async cleanupSkillCommands(sessionId) {
@@ -3868,6 +4885,7 @@ export {
3868
4885
  OpenACPCore,
3869
4886
  ChannelAdapter,
3870
4887
  ApiServer,
4888
+ TopicManager,
3871
4889
  TelegramAdapter
3872
4890
  };
3873
- //# sourceMappingURL=chunk-V5P3K4A5.js.map
4891
+ //# sourceMappingURL=chunk-BBPWAWE3.js.map