@openacp/cli 0.4.4 → 0.4.6

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.
@@ -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
  }
@@ -843,6 +846,17 @@ var SessionManager = class {
843
846
  }
844
847
  return void 0;
845
848
  }
849
+ getSessionByAgentSessionId(agentSessionId) {
850
+ for (const session of this.sessions.values()) {
851
+ if (session.agentSessionId === agentSessionId) {
852
+ return session;
853
+ }
854
+ }
855
+ return void 0;
856
+ }
857
+ getRecordByAgentSessionId(agentSessionId) {
858
+ return this.store?.findByAgentSessionId(agentSessionId);
859
+ }
846
860
  getRecordByThread(channelId, threadId) {
847
861
  return this.store?.findByPlatform(
848
862
  channelId,
@@ -910,6 +924,18 @@ var SessionManager = class {
910
924
  if (channelId) return all.filter((s) => s.channelId === channelId);
911
925
  return all;
912
926
  }
927
+ listRecords(filter) {
928
+ if (!this.store) return [];
929
+ let records = this.store.list();
930
+ if (filter?.statuses?.length) {
931
+ records = records.filter((r) => filter.statuses.includes(r.status));
932
+ }
933
+ return records;
934
+ }
935
+ async removeRecord(sessionId) {
936
+ if (!this.store) return;
937
+ await this.store.remove(sessionId);
938
+ }
913
939
  async destroyAll() {
914
940
  if (this.store) {
915
941
  for (const session of this.sessions.values()) {
@@ -1189,6 +1215,11 @@ var JsonFileSessionStore = class {
1189
1215
  }
1190
1216
  return void 0;
1191
1217
  }
1218
+ findByAgentSessionId(agentSessionId) {
1219
+ return [...this.records.values()].find(
1220
+ (r) => r.agentSessionId === agentSessionId || r.originalAgentSessionId === agentSessionId
1221
+ );
1222
+ }
1192
1223
  list(channelId) {
1193
1224
  const all = [...this.records.values()];
1194
1225
  if (channelId) return all.filter((r) => r.channelId === channelId);
@@ -1392,6 +1423,95 @@ var OpenACPCore = class {
1392
1423
  }
1393
1424
  return session;
1394
1425
  }
1426
+ async adoptSession(agentName, agentSessionId, cwd) {
1427
+ const caps = getAgentCapabilities(agentName);
1428
+ if (!caps.supportsResume) {
1429
+ return { ok: false, error: "agent_not_supported", message: `Agent '${agentName}' does not support session resume` };
1430
+ }
1431
+ const agentDef = this.agentManager.getAgent(agentName);
1432
+ if (!agentDef) {
1433
+ return { ok: false, error: "agent_not_supported", message: `Agent '${agentName}' not found` };
1434
+ }
1435
+ const { existsSync } = await import("fs");
1436
+ if (!existsSync(cwd)) {
1437
+ return { ok: false, error: "invalid_cwd", message: `Directory does not exist: ${cwd}` };
1438
+ }
1439
+ const maxSessions = this.configManager.get().security.maxConcurrentSessions;
1440
+ if (this.sessionManager.listSessions().length >= maxSessions) {
1441
+ return { ok: false, error: "session_limit", message: "Maximum concurrent sessions reached" };
1442
+ }
1443
+ const existingRecord = this.sessionManager.getRecordByAgentSessionId(agentSessionId);
1444
+ if (existingRecord) {
1445
+ const platform = existingRecord.platform;
1446
+ if (platform?.topicId) {
1447
+ const adapter2 = this.adapters.values().next().value;
1448
+ if (adapter2) {
1449
+ try {
1450
+ await adapter2.sendMessage(existingRecord.sessionId, {
1451
+ type: "text",
1452
+ text: "Session resumed from CLI."
1453
+ });
1454
+ } catch {
1455
+ }
1456
+ }
1457
+ return {
1458
+ ok: true,
1459
+ sessionId: existingRecord.sessionId,
1460
+ threadId: String(platform.topicId),
1461
+ status: "existing"
1462
+ };
1463
+ }
1464
+ }
1465
+ let agentInstance;
1466
+ try {
1467
+ agentInstance = await this.agentManager.resume(agentName, cwd, agentSessionId);
1468
+ } catch (err) {
1469
+ return {
1470
+ ok: false,
1471
+ error: "resume_failed",
1472
+ message: `Failed to resume session: ${err instanceof Error ? err.message : String(err)}`
1473
+ };
1474
+ }
1475
+ const session = new Session({
1476
+ channelId: "api",
1477
+ agentName,
1478
+ workingDirectory: cwd,
1479
+ agentInstance
1480
+ });
1481
+ session.agentSessionId = agentInstance.sessionId;
1482
+ this.sessionManager.registerSession(session);
1483
+ const firstEntry = this.adapters.entries().next().value;
1484
+ if (!firstEntry) {
1485
+ await session.destroy();
1486
+ return { ok: false, error: "no_adapter", message: "No channel adapter registered" };
1487
+ }
1488
+ const [adapterChannelId, adapter] = firstEntry;
1489
+ const threadId = await adapter.createSessionThread(session.id, session.name ?? "Adopted session");
1490
+ session.channelId = adapterChannelId;
1491
+ session.threadId = threadId;
1492
+ this.wireSessionEvents(session, adapter);
1493
+ if (this.sessionStore) {
1494
+ await this.sessionStore.save({
1495
+ sessionId: session.id,
1496
+ agentSessionId: agentInstance.sessionId,
1497
+ originalAgentSessionId: agentSessionId,
1498
+ agentName,
1499
+ workingDir: cwd,
1500
+ channelId: adapterChannelId,
1501
+ status: "active",
1502
+ createdAt: (/* @__PURE__ */ new Date()).toISOString(),
1503
+ lastActiveAt: (/* @__PURE__ */ new Date()).toISOString(),
1504
+ name: session.name,
1505
+ platform: { topicId: Number(threadId) }
1506
+ });
1507
+ }
1508
+ return {
1509
+ ok: true,
1510
+ sessionId: session.id,
1511
+ threadId,
1512
+ status: "adopted"
1513
+ };
1514
+ }
1395
1515
  async handleNewChat(channelId, currentThreadId) {
1396
1516
  const currentSession = this.sessionManager.getSessionByThread(
1397
1517
  channelId,
@@ -1548,6 +1668,8 @@ var ChannelAdapter = class {
1548
1668
  this.core = core;
1549
1669
  this.config = config;
1550
1670
  }
1671
+ async deleteSessionThread(_sessionId) {
1672
+ }
1551
1673
  // Skill commands — override in adapters that support dynamic commands
1552
1674
  async sendSkillCommands(_sessionId, _commands) {
1553
1675
  }
@@ -1560,25 +1682,56 @@ import * as http from "http";
1560
1682
  import * as fs3 from "fs";
1561
1683
  import * as path4 from "path";
1562
1684
  import * as os2 from "os";
1685
+ import { fileURLToPath } from "url";
1563
1686
  var log5 = createChildLogger({ module: "api-server" });
1564
1687
  var DEFAULT_PORT_FILE = path4.join(os2.homedir(), ".openacp", "api.port");
1688
+ var cachedVersion;
1689
+ function getVersion() {
1690
+ if (cachedVersion) return cachedVersion;
1691
+ try {
1692
+ const __filename = fileURLToPath(import.meta.url);
1693
+ const pkgPath = path4.resolve(path4.dirname(__filename), "../../package.json");
1694
+ const pkg = JSON.parse(fs3.readFileSync(pkgPath, "utf-8"));
1695
+ cachedVersion = pkg.version ?? "0.0.0-dev";
1696
+ } catch {
1697
+ cachedVersion = "0.0.0-dev";
1698
+ }
1699
+ return cachedVersion;
1700
+ }
1701
+ var SENSITIVE_KEYS = ["botToken", "token", "apiKey", "secret", "password", "webhookSecret"];
1702
+ function redactConfig(config) {
1703
+ const redacted = structuredClone(config);
1704
+ redactDeep(redacted);
1705
+ return redacted;
1706
+ }
1707
+ function redactDeep(obj) {
1708
+ for (const [key, value] of Object.entries(obj)) {
1709
+ if (SENSITIVE_KEYS.includes(key) && typeof value === "string") {
1710
+ obj[key] = "***";
1711
+ } else if (value && typeof value === "object" && !Array.isArray(value)) {
1712
+ redactDeep(value);
1713
+ }
1714
+ }
1715
+ }
1565
1716
  var ApiServer = class {
1566
- constructor(core, config, portFilePath) {
1717
+ constructor(core, config, portFilePath, topicManager) {
1567
1718
  this.core = core;
1568
1719
  this.config = config;
1720
+ this.topicManager = topicManager;
1569
1721
  this.portFilePath = portFilePath ?? DEFAULT_PORT_FILE;
1570
1722
  }
1571
1723
  server = null;
1572
1724
  actualPort = 0;
1573
1725
  portFilePath;
1726
+ startedAt = Date.now();
1574
1727
  async start() {
1575
1728
  this.server = http.createServer((req, res) => this.handleRequest(req, res));
1576
- await new Promise((resolve, reject) => {
1729
+ await new Promise((resolve2, reject) => {
1577
1730
  this.server.on("error", (err) => {
1578
1731
  if (err.code === "EADDRINUSE") {
1579
1732
  log5.warn({ port: this.config.port }, "API port in use, continuing without API server");
1580
1733
  this.server = null;
1581
- resolve();
1734
+ resolve2();
1582
1735
  } else {
1583
1736
  reject(err);
1584
1737
  }
@@ -1590,15 +1743,15 @@ var ApiServer = class {
1590
1743
  }
1591
1744
  this.writePortFile();
1592
1745
  log5.info({ host: this.config.host, port: this.actualPort }, "API server listening");
1593
- resolve();
1746
+ resolve2();
1594
1747
  });
1595
1748
  });
1596
1749
  }
1597
1750
  async stop() {
1598
1751
  this.removePortFile();
1599
1752
  if (this.server) {
1600
- await new Promise((resolve) => {
1601
- this.server.close(() => resolve());
1753
+ await new Promise((resolve2) => {
1754
+ this.server.close(() => resolve2());
1602
1755
  });
1603
1756
  this.server = null;
1604
1757
  }
@@ -1621,15 +1774,49 @@ var ApiServer = class {
1621
1774
  const method = req.method?.toUpperCase();
1622
1775
  const url = req.url || "";
1623
1776
  try {
1624
- if (method === "POST" && url === "/api/sessions") {
1777
+ if (method === "POST" && url === "/api/sessions/adopt") {
1778
+ await this.handleAdoptSession(req, res);
1779
+ } else if (method === "POST" && url === "/api/sessions") {
1625
1780
  await this.handleCreateSession(req, res);
1626
- } else if (method === "DELETE" && url.match(/^\/api\/sessions\/(.+)$/)) {
1627
- const sessionId = url.match(/^\/api\/sessions\/(.+)$/)[1];
1781
+ } else if (method === "POST" && url.match(/^\/api\/sessions\/([^/]+)\/prompt$/)) {
1782
+ const sessionId = decodeURIComponent(url.match(/^\/api\/sessions\/([^/]+)\/prompt$/)[1]);
1783
+ await this.handleSendPrompt(sessionId, req, res);
1784
+ } else if (method === "PATCH" && url.match(/^\/api\/sessions\/([^/]+)\/dangerous$/)) {
1785
+ const sessionId = decodeURIComponent(url.match(/^\/api\/sessions\/([^/]+)\/dangerous$/)[1]);
1786
+ await this.handleToggleDangerous(sessionId, req, res);
1787
+ } else if (method === "GET" && url.match(/^\/api\/sessions\/([^/]+)$/)) {
1788
+ const sessionId = decodeURIComponent(url.match(/^\/api\/sessions\/([^/]+)$/)[1]);
1789
+ await this.handleGetSession(sessionId, res);
1790
+ } else if (method === "DELETE" && url.match(/^\/api\/sessions\/([^/]+)$/)) {
1791
+ const sessionId = decodeURIComponent(url.match(/^\/api\/sessions\/([^/]+)$/)[1]);
1628
1792
  await this.handleCancelSession(sessionId, res);
1629
1793
  } else if (method === "GET" && url === "/api/sessions") {
1630
1794
  await this.handleListSessions(res);
1631
1795
  } else if (method === "GET" && url === "/api/agents") {
1632
1796
  await this.handleListAgents(res);
1797
+ } else if (method === "GET" && url === "/api/health") {
1798
+ await this.handleHealth(res);
1799
+ } else if (method === "GET" && url === "/api/version") {
1800
+ await this.handleVersion(res);
1801
+ } else if (method === "GET" && url === "/api/config") {
1802
+ await this.handleGetConfig(res);
1803
+ } else if (method === "PATCH" && url === "/api/config") {
1804
+ await this.handleUpdateConfig(req, res);
1805
+ } else if (method === "GET" && url === "/api/adapters") {
1806
+ await this.handleListAdapters(res);
1807
+ } else if (method === "GET" && url === "/api/tunnel") {
1808
+ await this.handleTunnelStatus(res);
1809
+ } else if (method === "POST" && url === "/api/notify") {
1810
+ await this.handleNotify(req, res);
1811
+ } else if (method === "POST" && url === "/api/restart") {
1812
+ await this.handleRestart(res);
1813
+ } else if (method === "GET" && url.match(/^\/api\/topics(\?.*)?$/)) {
1814
+ await this.handleListTopics(url, res);
1815
+ } else if (method === "POST" && url === "/api/topics/cleanup") {
1816
+ await this.handleCleanupTopics(req, res);
1817
+ } else if (method === "DELETE" && url.match(/^\/api\/topics\/([^/?]+)/)) {
1818
+ const match = url.match(/^\/api\/topics\/([^/?]+)/);
1819
+ await this.handleDeleteTopic(decodeURIComponent(match[1]), url, res);
1633
1820
  } else {
1634
1821
  this.sendJson(res, 404, { error: "Not found" });
1635
1822
  }
@@ -1687,6 +1874,222 @@ var ApiServer = class {
1687
1874
  workspace: session.workingDirectory
1688
1875
  });
1689
1876
  }
1877
+ async handleSendPrompt(sessionId, req, res) {
1878
+ const session = this.core.sessionManager.getSession(sessionId);
1879
+ if (!session) {
1880
+ this.sendJson(res, 404, { error: `Session "${sessionId}" not found` });
1881
+ return;
1882
+ }
1883
+ if (session.status === "cancelled" || session.status === "finished" || session.status === "error") {
1884
+ this.sendJson(res, 400, { error: `Session is ${session.status}` });
1885
+ return;
1886
+ }
1887
+ const body = await this.readBody(req);
1888
+ let prompt;
1889
+ if (body) {
1890
+ try {
1891
+ const parsed = JSON.parse(body);
1892
+ prompt = parsed.prompt;
1893
+ } catch {
1894
+ this.sendJson(res, 400, { error: "Invalid JSON body" });
1895
+ return;
1896
+ }
1897
+ }
1898
+ if (!prompt) {
1899
+ this.sendJson(res, 400, { error: "Missing prompt" });
1900
+ return;
1901
+ }
1902
+ session.enqueuePrompt(prompt).catch(() => {
1903
+ });
1904
+ this.sendJson(res, 200, { ok: true, sessionId, queueDepth: session.queueDepth });
1905
+ }
1906
+ async handleGetSession(sessionId, res) {
1907
+ const session = this.core.sessionManager.getSession(sessionId);
1908
+ if (!session) {
1909
+ this.sendJson(res, 404, { error: `Session "${sessionId}" not found` });
1910
+ return;
1911
+ }
1912
+ this.sendJson(res, 200, {
1913
+ session: {
1914
+ id: session.id,
1915
+ agent: session.agentName,
1916
+ status: session.status,
1917
+ name: session.name ?? null,
1918
+ workspace: session.workingDirectory,
1919
+ createdAt: session.createdAt.toISOString(),
1920
+ dangerousMode: session.dangerousMode,
1921
+ queueDepth: session.queueDepth,
1922
+ promptRunning: session.promptRunning,
1923
+ threadId: session.threadId,
1924
+ channelId: session.channelId,
1925
+ agentSessionId: session.agentSessionId
1926
+ }
1927
+ });
1928
+ }
1929
+ async handleToggleDangerous(sessionId, req, res) {
1930
+ const session = this.core.sessionManager.getSession(sessionId);
1931
+ if (!session) {
1932
+ this.sendJson(res, 404, { error: `Session "${sessionId}" not found` });
1933
+ return;
1934
+ }
1935
+ const body = await this.readBody(req);
1936
+ let enabled;
1937
+ if (body) {
1938
+ try {
1939
+ const parsed = JSON.parse(body);
1940
+ enabled = parsed.enabled;
1941
+ } catch {
1942
+ this.sendJson(res, 400, { error: "Invalid JSON body" });
1943
+ return;
1944
+ }
1945
+ }
1946
+ if (typeof enabled !== "boolean") {
1947
+ this.sendJson(res, 400, { error: "Missing enabled boolean" });
1948
+ return;
1949
+ }
1950
+ session.dangerousMode = enabled;
1951
+ await this.core.sessionManager.updateSessionDangerousMode(sessionId, enabled);
1952
+ this.sendJson(res, 200, { ok: true, dangerousMode: enabled });
1953
+ }
1954
+ async handleHealth(res) {
1955
+ const activeSessions = this.core.sessionManager.listSessions();
1956
+ const allRecords = this.core.sessionManager.listRecords();
1957
+ const mem = process.memoryUsage();
1958
+ const tunnel = this.core.tunnelService;
1959
+ this.sendJson(res, 200, {
1960
+ status: "ok",
1961
+ uptime: Date.now() - this.startedAt,
1962
+ version: getVersion(),
1963
+ memory: {
1964
+ rss: mem.rss,
1965
+ heapUsed: mem.heapUsed,
1966
+ heapTotal: mem.heapTotal
1967
+ },
1968
+ sessions: {
1969
+ active: activeSessions.filter((s) => s.status === "active" || s.status === "initializing").length,
1970
+ total: allRecords.length
1971
+ },
1972
+ adapters: Array.from(this.core.adapters.keys()),
1973
+ tunnel: tunnel ? { enabled: true, url: tunnel.getPublicUrl() } : { enabled: false }
1974
+ });
1975
+ }
1976
+ async handleVersion(res) {
1977
+ this.sendJson(res, 200, { version: getVersion() });
1978
+ }
1979
+ async handleGetConfig(res) {
1980
+ const config = this.core.configManager.get();
1981
+ this.sendJson(res, 200, { config: redactConfig(config) });
1982
+ }
1983
+ async handleUpdateConfig(req, res) {
1984
+ const body = await this.readBody(req);
1985
+ let configPath;
1986
+ let value;
1987
+ if (body) {
1988
+ try {
1989
+ const parsed = JSON.parse(body);
1990
+ configPath = parsed.path;
1991
+ value = parsed.value;
1992
+ } catch {
1993
+ this.sendJson(res, 400, { error: "Invalid JSON body" });
1994
+ return;
1995
+ }
1996
+ }
1997
+ if (!configPath) {
1998
+ this.sendJson(res, 400, { error: "Missing path" });
1999
+ return;
2000
+ }
2001
+ const currentConfig = this.core.configManager.get();
2002
+ const cloned = structuredClone(currentConfig);
2003
+ const parts = configPath.split(".");
2004
+ let target = cloned;
2005
+ for (let i = 0; i < parts.length - 1; i++) {
2006
+ if (target[parts[i]] && typeof target[parts[i]] === "object" && !Array.isArray(target[parts[i]])) {
2007
+ target = target[parts[i]];
2008
+ } else {
2009
+ this.sendJson(res, 400, { error: "Invalid config path" });
2010
+ return;
2011
+ }
2012
+ }
2013
+ const lastKey = parts[parts.length - 1];
2014
+ if (!(lastKey in target)) {
2015
+ this.sendJson(res, 400, { error: "Invalid config path" });
2016
+ return;
2017
+ }
2018
+ target[lastKey] = value;
2019
+ const { ConfigSchema } = await import("./config-J5YQOMDU.js");
2020
+ const result = ConfigSchema.safeParse(cloned);
2021
+ if (!result.success) {
2022
+ this.sendJson(res, 400, {
2023
+ error: "Validation failed",
2024
+ details: result.error.issues.map((i) => ({ path: i.path.join("."), message: i.message }))
2025
+ });
2026
+ return;
2027
+ }
2028
+ const updates = {};
2029
+ let updateTarget = updates;
2030
+ for (let i = 0; i < parts.length - 1; i++) {
2031
+ updateTarget[parts[i]] = {};
2032
+ updateTarget = updateTarget[parts[i]];
2033
+ }
2034
+ updateTarget[lastKey] = value;
2035
+ await this.core.configManager.save(updates);
2036
+ const RESTART_PREFIXES = ["api.port", "api.host", "runMode", "channels.", "tunnel.", "agents."];
2037
+ const needsRestart = RESTART_PREFIXES.some(
2038
+ (prefix) => configPath.startsWith(prefix) || configPath === prefix.replace(/\.$/, "")
2039
+ // exact match for non-wildcard
2040
+ );
2041
+ this.sendJson(res, 200, {
2042
+ ok: true,
2043
+ needsRestart,
2044
+ config: redactConfig(this.core.configManager.get())
2045
+ });
2046
+ }
2047
+ async handleListAdapters(res) {
2048
+ const adapters = Array.from(this.core.adapters.entries()).map(([name]) => ({
2049
+ name,
2050
+ type: "built-in"
2051
+ }));
2052
+ this.sendJson(res, 200, { adapters });
2053
+ }
2054
+ async handleTunnelStatus(res) {
2055
+ const tunnel = this.core.tunnelService;
2056
+ if (tunnel) {
2057
+ this.sendJson(res, 200, { enabled: true, url: tunnel.getPublicUrl(), provider: this.core.configManager.get().tunnel.provider });
2058
+ } else {
2059
+ this.sendJson(res, 200, { enabled: false });
2060
+ }
2061
+ }
2062
+ async handleNotify(req, res) {
2063
+ const body = await this.readBody(req);
2064
+ let message;
2065
+ if (body) {
2066
+ try {
2067
+ const parsed = JSON.parse(body);
2068
+ message = parsed.message;
2069
+ } catch {
2070
+ this.sendJson(res, 400, { error: "Invalid JSON body" });
2071
+ return;
2072
+ }
2073
+ }
2074
+ if (!message) {
2075
+ this.sendJson(res, 400, { error: "Missing message" });
2076
+ return;
2077
+ }
2078
+ await this.core.notificationManager.notifyAll({
2079
+ sessionId: "system",
2080
+ type: "completed",
2081
+ summary: message
2082
+ });
2083
+ this.sendJson(res, 200, { ok: true });
2084
+ }
2085
+ async handleRestart(res) {
2086
+ if (!this.core.requestRestart) {
2087
+ this.sendJson(res, 501, { error: "Restart not available" });
2088
+ return;
2089
+ }
2090
+ this.sendJson(res, 200, { ok: true, message: "Restarting..." });
2091
+ setImmediate(() => this.core.requestRestart());
2092
+ }
1690
2093
  async handleCancelSession(sessionId, res) {
1691
2094
  const session = this.core.sessionManager.getSession(sessionId);
1692
2095
  if (!session) {
@@ -1708,34 +2111,180 @@ var ApiServer = class {
1708
2111
  }))
1709
2112
  });
1710
2113
  }
2114
+ async handleAdoptSession(req, res) {
2115
+ const body = await this.readBody(req);
2116
+ if (!body) {
2117
+ return this.sendJson(res, 400, { error: "bad_request", message: "Empty request body" });
2118
+ }
2119
+ let parsed;
2120
+ try {
2121
+ parsed = JSON.parse(body);
2122
+ } catch {
2123
+ return this.sendJson(res, 400, { error: "bad_request", message: "Invalid JSON" });
2124
+ }
2125
+ const { agent, agentSessionId, cwd } = parsed;
2126
+ if (!agent || !agentSessionId) {
2127
+ return this.sendJson(res, 400, { error: "bad_request", message: "Missing required fields: agent, agentSessionId" });
2128
+ }
2129
+ const result = await this.core.adoptSession(agent, agentSessionId, cwd ?? process.cwd());
2130
+ if (result.ok) {
2131
+ return this.sendJson(res, 200, result);
2132
+ } else {
2133
+ const status = result.error === "session_limit" ? 429 : result.error === "agent_not_supported" ? 400 : 500;
2134
+ return this.sendJson(res, status, result);
2135
+ }
2136
+ }
1711
2137
  async handleListAgents(res) {
1712
2138
  const agents = this.core.agentManager.getAvailableAgents();
1713
2139
  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
- });
2140
+ const agentsWithCaps = agents.map((a) => ({
2141
+ ...a,
2142
+ capabilities: getAgentCapabilities(a.name)
2143
+ }));
2144
+ this.sendJson(res, 200, { agents: agentsWithCaps, default: defaultAgent });
1722
2145
  }
1723
2146
  sendJson(res, status, data) {
1724
2147
  res.writeHead(status, { "Content-Type": "application/json" });
1725
2148
  res.end(JSON.stringify(data));
1726
2149
  }
2150
+ async handleListTopics(url, res) {
2151
+ if (!this.topicManager) {
2152
+ this.sendJson(res, 501, { error: "Topic management not available" });
2153
+ return;
2154
+ }
2155
+ const params = new URL(url, "http://localhost").searchParams;
2156
+ const statusParam = params.get("status");
2157
+ const filter = statusParam ? { statuses: statusParam.split(",") } : void 0;
2158
+ const topics = this.topicManager.listTopics(filter);
2159
+ this.sendJson(res, 200, { topics });
2160
+ }
2161
+ async handleDeleteTopic(sessionId, url, res) {
2162
+ if (!this.topicManager) {
2163
+ this.sendJson(res, 501, { error: "Topic management not available" });
2164
+ return;
2165
+ }
2166
+ const params = new URL(url, "http://localhost").searchParams;
2167
+ const force = params.get("force") === "true";
2168
+ const result = await this.topicManager.deleteTopic(sessionId, force ? { confirmed: true } : void 0);
2169
+ if (result.ok) {
2170
+ this.sendJson(res, 200, result);
2171
+ } else if (result.needsConfirmation) {
2172
+ this.sendJson(res, 409, { error: "Session is active", needsConfirmation: true, session: result.session });
2173
+ } else if (result.error === "Cannot delete system topic") {
2174
+ this.sendJson(res, 403, { error: result.error });
2175
+ } else {
2176
+ this.sendJson(res, 404, { error: result.error ?? "Not found" });
2177
+ }
2178
+ }
2179
+ async handleCleanupTopics(req, res) {
2180
+ if (!this.topicManager) {
2181
+ this.sendJson(res, 501, { error: "Topic management not available" });
2182
+ return;
2183
+ }
2184
+ const body = await this.readBody(req);
2185
+ let statuses;
2186
+ if (body) {
2187
+ try {
2188
+ statuses = JSON.parse(body).statuses;
2189
+ } catch {
2190
+ }
2191
+ }
2192
+ const result = await this.topicManager.cleanup(statuses);
2193
+ this.sendJson(res, 200, result);
2194
+ }
1727
2195
  readBody(req) {
1728
- return new Promise((resolve) => {
2196
+ return new Promise((resolve2) => {
1729
2197
  let data = "";
1730
2198
  req.on("data", (chunk) => {
1731
2199
  data += chunk;
1732
2200
  });
1733
- req.on("end", () => resolve(data));
1734
- req.on("error", () => resolve(""));
2201
+ req.on("end", () => resolve2(data));
2202
+ req.on("error", () => resolve2(""));
1735
2203
  });
1736
2204
  }
1737
2205
  };
1738
2206
 
2207
+ // src/core/topic-manager.ts
2208
+ var log6 = createChildLogger({ module: "topic-manager" });
2209
+ var TopicManager = class {
2210
+ constructor(sessionManager, adapter, systemTopicIds) {
2211
+ this.sessionManager = sessionManager;
2212
+ this.adapter = adapter;
2213
+ this.systemTopicIds = systemTopicIds;
2214
+ }
2215
+ listTopics(filter) {
2216
+ const records = this.sessionManager.listRecords(filter);
2217
+ return records.filter((r) => !this.isSystemTopic(r)).filter((r) => !filter?.statuses?.length || filter.statuses.includes(r.status)).map((r) => ({
2218
+ sessionId: r.sessionId,
2219
+ topicId: r.platform?.topicId ?? null,
2220
+ name: r.name ?? null,
2221
+ status: r.status,
2222
+ agentName: r.agentName,
2223
+ lastActiveAt: r.lastActiveAt
2224
+ }));
2225
+ }
2226
+ async deleteTopic(sessionId, options) {
2227
+ const records = this.sessionManager.listRecords();
2228
+ const record = records.find((r) => r.sessionId === sessionId);
2229
+ if (!record) return { ok: false, error: "Session not found" };
2230
+ if (this.isSystemTopic(record)) return { ok: false, error: "Cannot delete system topic" };
2231
+ const isActive = record.status === "active" || record.status === "initializing";
2232
+ if (isActive && !options?.confirmed) {
2233
+ return {
2234
+ ok: false,
2235
+ needsConfirmation: true,
2236
+ session: { id: record.sessionId, name: record.name ?? null, status: record.status }
2237
+ };
2238
+ }
2239
+ if (isActive) {
2240
+ await this.sessionManager.cancelSession(sessionId);
2241
+ }
2242
+ const topicId = record.platform?.topicId ?? null;
2243
+ if (this.adapter && topicId) {
2244
+ try {
2245
+ await this.adapter.deleteSessionThread(sessionId);
2246
+ } catch (err) {
2247
+ log6.warn({ err, sessionId, topicId }, "Failed to delete platform thread, removing record anyway");
2248
+ }
2249
+ }
2250
+ await this.sessionManager.removeRecord(sessionId);
2251
+ return { ok: true, topicId };
2252
+ }
2253
+ async cleanup(statuses) {
2254
+ const targetStatuses = statuses?.length ? statuses : ["finished", "error", "cancelled"];
2255
+ const records = this.sessionManager.listRecords({ statuses: targetStatuses });
2256
+ const targets = records.filter((r) => !this.isSystemTopic(r)).filter((r) => targetStatuses.includes(r.status));
2257
+ const deleted = [];
2258
+ const failed = [];
2259
+ for (const record of targets) {
2260
+ try {
2261
+ const isActive = record.status === "active" || record.status === "initializing";
2262
+ if (isActive) {
2263
+ await this.sessionManager.cancelSession(record.sessionId);
2264
+ }
2265
+ const topicId = record.platform?.topicId;
2266
+ if (this.adapter && topicId) {
2267
+ try {
2268
+ await this.adapter.deleteSessionThread(record.sessionId);
2269
+ } catch (err) {
2270
+ log6.warn({ err, sessionId: record.sessionId }, "Failed to delete platform thread during cleanup");
2271
+ }
2272
+ }
2273
+ await this.sessionManager.removeRecord(record.sessionId);
2274
+ deleted.push(record.sessionId);
2275
+ } catch (err) {
2276
+ failed.push({ sessionId: record.sessionId, error: err instanceof Error ? err.message : String(err) });
2277
+ }
2278
+ }
2279
+ return { deleted, failed };
2280
+ }
2281
+ isSystemTopic(record) {
2282
+ const topicId = record.platform?.topicId;
2283
+ if (!topicId) return false;
2284
+ return topicId === this.systemTopicIds.notificationTopicId || topicId === this.systemTopicIds.assistantTopicId;
2285
+ }
2286
+ };
2287
+
1739
2288
  // src/adapters/telegram/adapter.ts
1740
2289
  import { Bot } from "grammy";
1741
2290
 
@@ -2048,7 +2597,7 @@ function buildDeepLink(chatId, messageId) {
2048
2597
 
2049
2598
  // src/adapters/telegram/commands.ts
2050
2599
  import { InlineKeyboard } from "grammy";
2051
- var log7 = createChildLogger({ module: "telegram-commands" });
2600
+ var log8 = createChildLogger({ module: "telegram-commands" });
2052
2601
  function setupCommands(bot, core, chatId, assistant) {
2053
2602
  bot.command("new", (ctx) => handleNew(ctx, core, chatId, assistant));
2054
2603
  bot.command("newchat", (ctx) => handleNewChat(ctx, core, chatId));
@@ -2122,7 +2671,7 @@ async function handleNew(ctx, core, chatId, assistant) {
2122
2671
  return;
2123
2672
  }
2124
2673
  }
2125
- log7.info({ userId: ctx.from?.id, agentName }, "New session command");
2674
+ log8.info({ userId: ctx.from?.id, agentName }, "New session command");
2126
2675
  let threadId;
2127
2676
  try {
2128
2677
  const topicName = `\u{1F504} New Session`;
@@ -2156,9 +2705,9 @@ async function handleNew(ctx, core, chatId, assistant) {
2156
2705
  reply_markup: buildDangerousModeKeyboard(session.id, false)
2157
2706
  }
2158
2707
  );
2159
- session.warmup().catch((err) => log7.error({ err }, "Warm-up error"));
2708
+ session.warmup().catch((err) => log8.error({ err }, "Warm-up error"));
2160
2709
  } catch (err) {
2161
- log7.error({ err }, "Session creation failed");
2710
+ log8.error({ err }, "Session creation failed");
2162
2711
  if (threadId) {
2163
2712
  try {
2164
2713
  await ctx.api.deleteForumTopic(chatId, threadId);
@@ -2235,7 +2784,7 @@ async function handleNewChat(ctx, core, chatId) {
2235
2784
  reply_markup: buildDangerousModeKeyboard(session.id, false)
2236
2785
  }
2237
2786
  );
2238
- session.warmup().catch((err) => log7.error({ err }, "Warm-up error"));
2787
+ session.warmup().catch((err) => log8.error({ err }, "Warm-up error"));
2239
2788
  } catch (err) {
2240
2789
  if (newThreadId) {
2241
2790
  try {
@@ -2264,14 +2813,14 @@ async function handleCancel(ctx, core, assistant) {
2264
2813
  String(threadId)
2265
2814
  );
2266
2815
  if (session) {
2267
- log7.info({ sessionId: session.id }, "Cancel session command");
2816
+ log8.info({ sessionId: session.id }, "Cancel session command");
2268
2817
  await session.cancel();
2269
2818
  await ctx.reply("\u26D4 Session cancelled.", { parse_mode: "HTML" });
2270
2819
  return;
2271
2820
  }
2272
2821
  const record = core.sessionManager.getRecordByThread("telegram", String(threadId));
2273
2822
  if (record && record.status !== "cancelled" && record.status !== "error") {
2274
- log7.info({ sessionId: record.sessionId }, "Cancel session command (from store)");
2823
+ log8.info({ sessionId: record.sessionId }, "Cancel session command (from store)");
2275
2824
  await core.sessionManager.cancelSession(record.sessionId);
2276
2825
  await ctx.reply("\u26D4 Session cancelled.", { parse_mode: "HTML" });
2277
2826
  }
@@ -2365,7 +2914,7 @@ function setupDangerousModeCallbacks(bot, core) {
2365
2914
  const session = core.sessionManager.getSession(sessionId);
2366
2915
  if (session) {
2367
2916
  session.dangerousMode = !session.dangerousMode;
2368
- log7.info({ sessionId, dangerousMode: session.dangerousMode }, "Dangerous mode toggled via button");
2917
+ log8.info({ sessionId, dangerousMode: session.dangerousMode }, "Dangerous mode toggled via button");
2369
2918
  core.sessionManager.updateSessionDangerousMode(sessionId, session.dangerousMode).catch(() => {
2370
2919
  });
2371
2920
  const toastText2 = session.dangerousMode ? "\u2620\uFE0F Dangerous mode enabled \u2014 permissions auto-approved" : "\u{1F510} Dangerous mode disabled \u2014 permissions shown normally";
@@ -2392,7 +2941,7 @@ function setupDangerousModeCallbacks(bot, core) {
2392
2941
  const newDangerousMode = !(record.dangerousMode ?? false);
2393
2942
  core.sessionManager.updateSessionDangerousMode(sessionId, newDangerousMode).catch(() => {
2394
2943
  });
2395
- log7.info({ sessionId, dangerousMode: newDangerousMode }, "Dangerous mode toggled via button (store-only, session not in memory)");
2944
+ log8.info({ sessionId, dangerousMode: newDangerousMode }, "Dangerous mode toggled via button (store-only, session not in memory)");
2396
2945
  const toastText = newDangerousMode ? "\u2620\uFE0F Dangerous mode enabled \u2014 permissions auto-approved" : "\u{1F510} Dangerous mode disabled \u2014 permissions shown normally";
2397
2946
  try {
2398
2947
  await ctx.answerCallbackQuery({ text: toastText });
@@ -2560,7 +3109,7 @@ async function executeNewSession(bot, core, chatId, agentName, workspace) {
2560
3109
  });
2561
3110
  const finalName = `\u{1F504} ${session.agentName} \u2014 New Session`;
2562
3111
  await renameSessionTopic(bot, chatId, threadId, finalName);
2563
- session.warmup().catch((err) => log7.error({ err }, "Warm-up error"));
3112
+ session.warmup().catch((err) => log8.error({ err }, "Warm-up error"));
2564
3113
  return { session, threadId, firstMsgId };
2565
3114
  } catch (err) {
2566
3115
  try {
@@ -2594,7 +3143,7 @@ var STATIC_COMMANDS = [
2594
3143
  // src/adapters/telegram/permissions.ts
2595
3144
  import { InlineKeyboard as InlineKeyboard2 } from "grammy";
2596
3145
  import { nanoid as nanoid2 } from "nanoid";
2597
- var log8 = createChildLogger({ module: "telegram-permissions" });
3146
+ var log9 = createChildLogger({ module: "telegram-permissions" });
2598
3147
  var PermissionHandler = class {
2599
3148
  constructor(bot, chatId, getSession, sendNotification) {
2600
3149
  this.bot = bot;
@@ -2654,7 +3203,7 @@ ${escapeHtml(request.description)}`,
2654
3203
  }
2655
3204
  const session = this.getSession(pending.sessionId);
2656
3205
  const isAllow = pending.options.find((o) => o.id === optionId)?.isAllow ?? false;
2657
- log8.info({ requestId: pending.requestId, optionId, isAllow }, "Permission responded");
3206
+ log9.info({ requestId: pending.requestId, optionId, isAllow }, "Permission responded");
2658
3207
  if (session?.permissionGate.requestId === pending.requestId) {
2659
3208
  session.permissionGate.resolve(optionId);
2660
3209
  }
@@ -2672,10 +3221,10 @@ ${escapeHtml(request.description)}`,
2672
3221
  };
2673
3222
 
2674
3223
  // src/adapters/telegram/assistant.ts
2675
- var log9 = createChildLogger({ module: "telegram-assistant" });
3224
+ var log10 = createChildLogger({ module: "telegram-assistant" });
2676
3225
  async function spawnAssistant(core, adapter, assistantTopicId) {
2677
3226
  const config = core.configManager.get();
2678
- log9.info({ agent: config.defaultAgent }, "Creating assistant session...");
3227
+ log10.info({ agent: config.defaultAgent }, "Creating assistant session...");
2679
3228
  const session = await core.sessionManager.createSession(
2680
3229
  "telegram",
2681
3230
  config.defaultAgent,
@@ -2684,30 +3233,44 @@ async function spawnAssistant(core, adapter, assistantTopicId) {
2684
3233
  );
2685
3234
  session.threadId = String(assistantTopicId);
2686
3235
  session.name = "Assistant";
2687
- log9.info({ sessionId: session.id }, "Assistant agent spawned");
3236
+ log10.info({ sessionId: session.id }, "Assistant agent spawned");
2688
3237
  core.wireSessionEvents(session, adapter);
2689
- const systemPrompt = buildAssistantSystemPrompt(config);
3238
+ const allRecords = core.sessionManager.listRecords();
3239
+ const activeCount = allRecords.filter((r) => r.status === "active" || r.status === "initializing").length;
3240
+ const statusCounts = /* @__PURE__ */ new Map();
3241
+ for (const r of allRecords) {
3242
+ statusCounts.set(r.status, (statusCounts.get(r.status) ?? 0) + 1);
3243
+ }
3244
+ const topicSummary = Array.from(statusCounts.entries()).map(([status, count]) => ({ status, count }));
3245
+ const ctx = {
3246
+ config,
3247
+ activeSessionCount: activeCount,
3248
+ totalSessionCount: allRecords.length,
3249
+ topicSummary
3250
+ };
3251
+ const systemPrompt = buildAssistantSystemPrompt(ctx);
2690
3252
  const ready = session.enqueuePrompt(systemPrompt).then(() => {
2691
- log9.info({ sessionId: session.id }, "Assistant system prompt completed");
3253
+ log10.info({ sessionId: session.id }, "Assistant system prompt completed");
2692
3254
  }).catch((err) => {
2693
- log9.warn({ err }, "Assistant system prompt failed");
3255
+ log10.warn({ err }, "Assistant system prompt failed");
2694
3256
  });
2695
3257
  return { session, ready };
2696
3258
  }
2697
- function buildAssistantSystemPrompt(config) {
3259
+ function buildAssistantSystemPrompt(ctx) {
3260
+ const { config, activeSessionCount, totalSessionCount, topicSummary } = ctx;
2698
3261
  const agentNames = Object.keys(config.agents).join(", ");
2699
- return `You are the OpenACP Assistant. Help users manage their AI coding sessions.
2700
-
2701
- Available agents: ${agentNames}
2702
- Default agent: ${config.defaultAgent}
2703
- Workspace base: ${config.workspace.baseDir}
3262
+ const topicBreakdown = topicSummary.map((s) => `${s.status}: ${s.count}`).join(", ") || "none";
3263
+ return `You are the OpenACP Assistant. Help users manage their AI coding sessions and topics.
2704
3264
 
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
3265
+ ## Current State
3266
+ - Active sessions: ${activeSessionCount} / ${totalSessionCount} total
3267
+ - Topics by status: ${topicBreakdown}
3268
+ - Available agents: ${agentNames}
3269
+ - Default agent: ${config.defaultAgent}
3270
+ - Workspace base: ${config.workspace.baseDir}
2709
3271
 
2710
- Commands reference:
3272
+ ## Session Management Commands
3273
+ These are Telegram bot commands (type directly in chat):
2711
3274
  - /new [agent] [workspace] \u2014 Create new session
2712
3275
  - /newchat \u2014 New chat with same agent & workspace
2713
3276
  - /cancel \u2014 Cancel current session
@@ -2715,7 +3278,48 @@ Commands reference:
2715
3278
  - /agents \u2014 List agents
2716
3279
  - /help \u2014 Show help
2717
3280
 
2718
- Be concise and helpful. When the user confirms session creation, tell them you'll create it now.`;
3281
+ ## Management Commands (via CLI)
3282
+ You have access to bash. Use these commands to manage OpenACP:
3283
+
3284
+ ### Session management
3285
+ \`\`\`bash
3286
+ openacp api status # List active sessions
3287
+ openacp api session <id> # Session detail
3288
+ openacp api send <id> "prompt text" # Send prompt to session
3289
+ openacp api cancel <id> # Cancel session
3290
+ openacp api dangerous <id> on|off # Toggle dangerous mode
3291
+ \`\`\`
3292
+
3293
+ ### Topic management
3294
+ \`\`\`bash
3295
+ openacp api topics # List topics
3296
+ openacp api topics --status finished,error
3297
+ openacp api delete-topic <id> # Delete topic
3298
+ openacp api delete-topic <id> --force # Force delete active
3299
+ openacp api cleanup # Cleanup finished topics
3300
+ openacp api cleanup --status finished,error
3301
+ \`\`\`
3302
+
3303
+ ### System
3304
+ \`\`\`bash
3305
+ openacp api health # System health
3306
+ openacp api config # Show config
3307
+ openacp api config set <key> <value> # Update config
3308
+ openacp api adapters # List adapters
3309
+ openacp api tunnel # Tunnel status
3310
+ openacp api notify "message" # Send notification
3311
+ openacp api version # Daemon version
3312
+ openacp api restart # Restart daemon
3313
+ \`\`\`
3314
+
3315
+ ## Guidelines
3316
+ - When a user asks about sessions or topics, run \`openacp api topics\` or \`openacp api status\` to get current data.
3317
+ - When deleting: if the session is active/initializing, warn the user first. Only use --force if they confirm.
3318
+ - Use \`openacp api health\` to check system status.
3319
+ - Use \`openacp api config\` to check configuration, \`openacp api config set\` to update values.
3320
+ - Format responses nicely for Telegram (use bold, code blocks).
3321
+ - Be concise and helpful. Respond in the same language the user uses.
3322
+ - When creating sessions, guide through: agent selection \u2192 workspace \u2192 confirm.`;
2719
3323
  }
2720
3324
  async function handleAssistantMessage(session, text) {
2721
3325
  if (!session) return;
@@ -2728,7 +3332,7 @@ function redirectToAssistant(chatId, assistantTopicId) {
2728
3332
  }
2729
3333
 
2730
3334
  // src/adapters/telegram/activity.ts
2731
- var log10 = createChildLogger({ module: "telegram:activity" });
3335
+ var log11 = createChildLogger({ module: "telegram:activity" });
2732
3336
  var THINKING_REFRESH_MS = 15e3;
2733
3337
  var THINKING_MAX_MS = 3 * 60 * 1e3;
2734
3338
  var ThinkingIndicator = class {
@@ -2760,7 +3364,7 @@ var ThinkingIndicator = class {
2760
3364
  this.startRefreshTimer();
2761
3365
  }
2762
3366
  } catch (err) {
2763
- log10.warn({ err }, "ThinkingIndicator.show() failed");
3367
+ log11.warn({ err }, "ThinkingIndicator.show() failed");
2764
3368
  } finally {
2765
3369
  this.sending = false;
2766
3370
  }
@@ -2833,7 +3437,7 @@ var UsageMessage = class {
2833
3437
  if (result) this.msgId = result.message_id;
2834
3438
  }
2835
3439
  } catch (err) {
2836
- log10.warn({ err }, "UsageMessage.send() failed");
3440
+ log11.warn({ err }, "UsageMessage.send() failed");
2837
3441
  }
2838
3442
  }
2839
3443
  getMsgId() {
@@ -2846,7 +3450,7 @@ var UsageMessage = class {
2846
3450
  try {
2847
3451
  await this.sendQueue.enqueue(() => this.api.deleteMessage(this.chatId, id));
2848
3452
  } catch (err) {
2849
- log10.warn({ err }, "UsageMessage.delete() failed");
3453
+ log11.warn({ err }, "UsageMessage.delete() failed");
2850
3454
  }
2851
3455
  }
2852
3456
  };
@@ -2881,6 +3485,7 @@ var PlanCard = class {
2881
3485
  msgId;
2882
3486
  flushPromise = Promise.resolve();
2883
3487
  latestEntries;
3488
+ lastSentText;
2884
3489
  flushTimer;
2885
3490
  update(entries) {
2886
3491
  this.latestEntries = entries;
@@ -2911,6 +3516,8 @@ var PlanCard = class {
2911
3516
  async _flush() {
2912
3517
  if (!this.latestEntries) return;
2913
3518
  const text = formatPlanCard(this.latestEntries);
3519
+ if (this.msgId && text === this.lastSentText) return;
3520
+ this.lastSentText = text;
2914
3521
  try {
2915
3522
  if (this.msgId) {
2916
3523
  await this.sendQueue.enqueue(
@@ -2929,7 +3536,7 @@ var PlanCard = class {
2929
3536
  if (result) this.msgId = result.message_id;
2930
3537
  }
2931
3538
  } catch (err) {
2932
- log10.warn({ err }, "PlanCard flush failed");
3539
+ log11.warn({ err }, "PlanCard flush failed");
2933
3540
  }
2934
3541
  }
2935
3542
  };
@@ -2992,7 +3599,7 @@ var ActivityTracker = class {
2992
3599
  })
2993
3600
  );
2994
3601
  } catch (err) {
2995
- log10.warn({ err }, "ActivityTracker.onComplete() Done send failed");
3602
+ log11.warn({ err }, "ActivityTracker.onComplete() Done send failed");
2996
3603
  }
2997
3604
  }
2998
3605
  }
@@ -3019,19 +3626,19 @@ var TelegramSendQueue = class {
3019
3626
  enqueue(fn, opts) {
3020
3627
  const type = opts?.type ?? "other";
3021
3628
  const key = opts?.key;
3022
- return new Promise((resolve, reject) => {
3629
+ return new Promise((resolve2, reject) => {
3023
3630
  if (type === "text" && key) {
3024
3631
  const idx = this.items.findIndex(
3025
3632
  (item) => item.type === "text" && item.key === key
3026
3633
  );
3027
3634
  if (idx !== -1) {
3028
3635
  this.items[idx].resolve(void 0);
3029
- this.items[idx] = { fn, type, key, resolve, reject };
3636
+ this.items[idx] = { fn, type, key, resolve: resolve2, reject };
3030
3637
  this.scheduleProcess();
3031
3638
  return;
3032
3639
  }
3033
3640
  }
3034
- this.items.push({ fn, type, key, resolve, reject });
3641
+ this.items.push({ fn, type, key, resolve: resolve2, reject });
3035
3642
  this.scheduleProcess();
3036
3643
  });
3037
3644
  }
@@ -3219,7 +3826,7 @@ function setupActionCallbacks(bot, core, chatId, getAssistantSessionId) {
3219
3826
  }
3220
3827
 
3221
3828
  // src/adapters/telegram/adapter.ts
3222
- var log11 = createChildLogger({ module: "telegram" });
3829
+ var log12 = createChildLogger({ module: "telegram" });
3223
3830
  function patchedFetch(input, init) {
3224
3831
  if (init?.signal && !(init.signal instanceof AbortSignal)) {
3225
3832
  const nativeController = new AbortController();
@@ -3270,7 +3877,7 @@ var TelegramAdapter = class extends ChannelAdapter {
3270
3877
  this.bot = new Bot(this.telegramConfig.botToken, { client: { fetch: patchedFetch } });
3271
3878
  this.bot.catch((err) => {
3272
3879
  const rootCause = err.error instanceof Error ? err.error : err;
3273
- log11.error({ err: rootCause }, "Telegram bot error");
3880
+ log12.error({ err: rootCause }, "Telegram bot error");
3274
3881
  });
3275
3882
  this.bot.api.config.use(async (prev, method, payload, signal) => {
3276
3883
  const maxRetries = 3;
@@ -3284,7 +3891,7 @@ var TelegramAdapter = class extends ChannelAdapter {
3284
3891
  if (rateLimitedMethods.includes(method)) {
3285
3892
  this.sendQueue.onRateLimited();
3286
3893
  }
3287
- log11.warn(
3894
+ log12.warn(
3288
3895
  { method, retryAfter, attempt: attempt + 1 },
3289
3896
  "Rate limited by Telegram, retrying"
3290
3897
  );
@@ -3350,10 +3957,46 @@ var TelegramAdapter = class extends ChannelAdapter {
3350
3957
  }
3351
3958
  );
3352
3959
  this.permissionHandler.setupCallbackHandler();
3960
+ this.bot.command("handoff", async (ctx) => {
3961
+ const threadId = ctx.message?.message_thread_id;
3962
+ if (!threadId) return;
3963
+ if (threadId === this.notificationTopicId || threadId === this.assistantTopicId) {
3964
+ await ctx.reply("This command only works in session topics.", {
3965
+ message_thread_id: threadId
3966
+ });
3967
+ return;
3968
+ }
3969
+ const session = this.core.sessionManager.getSessionByThread("telegram", String(threadId));
3970
+ if (!session) {
3971
+ await ctx.reply("No active session in this topic.", {
3972
+ message_thread_id: threadId
3973
+ });
3974
+ return;
3975
+ }
3976
+ const { getAgentCapabilities: getAgentCapabilities2 } = await import("./agent-registry-7HC6D4CH.js");
3977
+ const caps = getAgentCapabilities2(session.agentName);
3978
+ if (!caps.supportsResume || !caps.resumeCommand) {
3979
+ await ctx.reply("This agent does not support CLI handoff.", {
3980
+ message_thread_id: threadId
3981
+ });
3982
+ return;
3983
+ }
3984
+ const agentSessionId = session.agentSessionId;
3985
+ const command = caps.resumeCommand(agentSessionId);
3986
+ await ctx.reply(
3987
+ `Resume this session on CLI:
3988
+
3989
+ <code>${command}</code>`,
3990
+ {
3991
+ message_thread_id: threadId,
3992
+ parse_mode: "HTML"
3993
+ }
3994
+ );
3995
+ });
3353
3996
  this.setupRoutes();
3354
3997
  this.bot.start({
3355
3998
  allowed_updates: ["message", "callback_query"],
3356
- onStart: () => log11.info(
3999
+ onStart: () => log12.info(
3357
4000
  { chatId: this.telegramConfig.chatId },
3358
4001
  "Telegram bot started"
3359
4002
  )
@@ -3375,10 +4018,10 @@ Workspace: <code>${workspace}</code>
3375
4018
  reply_markup: buildMenuKeyboard()
3376
4019
  });
3377
4020
  } catch (err) {
3378
- log11.warn({ err }, "Failed to send welcome message");
4021
+ log12.warn({ err }, "Failed to send welcome message");
3379
4022
  }
3380
4023
  try {
3381
- log11.info("Spawning assistant session...");
4024
+ log12.info("Spawning assistant session...");
3382
4025
  const { session, ready } = await spawnAssistant(
3383
4026
  this.core,
3384
4027
  this,
@@ -3386,13 +4029,13 @@ Workspace: <code>${workspace}</code>
3386
4029
  );
3387
4030
  this.assistantSession = session;
3388
4031
  this.assistantInitializing = true;
3389
- log11.info({ sessionId: session.id }, "Assistant session ready, system prompt running in background");
4032
+ log12.info({ sessionId: session.id }, "Assistant session ready, system prompt running in background");
3390
4033
  ready.then(() => {
3391
4034
  this.assistantInitializing = false;
3392
- log11.info({ sessionId: session.id }, "Assistant ready for user messages");
4035
+ log12.info({ sessionId: session.id }, "Assistant ready for user messages");
3393
4036
  });
3394
4037
  } catch (err) {
3395
- log11.error({ err }, "Failed to spawn assistant");
4038
+ log12.error({ err }, "Failed to spawn assistant");
3396
4039
  this.bot.api.sendMessage(
3397
4040
  this.telegramConfig.chatId,
3398
4041
  `\u26A0\uFE0F <b>Failed to start assistant session.</b>
@@ -3408,7 +4051,7 @@ Workspace: <code>${workspace}</code>
3408
4051
  await this.assistantSession.destroy();
3409
4052
  }
3410
4053
  await this.bot.stop();
3411
- log11.info("Telegram bot stopped");
4054
+ log12.info("Telegram bot stopped");
3412
4055
  }
3413
4056
  setupRoutes() {
3414
4057
  this.bot.on("message:text", async (ctx) => {
@@ -3431,7 +4074,7 @@ Workspace: <code>${workspace}</code>
3431
4074
  ctx.replyWithChatAction("typing").catch(() => {
3432
4075
  });
3433
4076
  handleAssistantMessage(this.assistantSession, ctx.message.text).catch(
3434
- (err) => log11.error({ err }, "Assistant error")
4077
+ (err) => log12.error({ err }, "Assistant error")
3435
4078
  );
3436
4079
  return;
3437
4080
  }
@@ -3448,7 +4091,7 @@ Workspace: <code>${workspace}</code>
3448
4091
  threadId: String(threadId),
3449
4092
  userId: String(ctx.from.id),
3450
4093
  text: ctx.message.text
3451
- }).catch((err) => log11.error({ err }, "handleMessage error"));
4094
+ }).catch((err) => log12.error({ err }, "handleMessage error"));
3452
4095
  });
3453
4096
  }
3454
4097
  // --- ChannelAdapter implementations ---
@@ -3528,7 +4171,7 @@ Workspace: <code>${workspace}</code>
3528
4171
  if (toolState) {
3529
4172
  if (meta.viewerLinks) {
3530
4173
  toolState.viewerLinks = meta.viewerLinks;
3531
- log11.debug({ toolId: meta.id, viewerLinks: meta.viewerLinks }, "Accumulated viewerLinks");
4174
+ log12.debug({ toolId: meta.id, viewerLinks: meta.viewerLinks }, "Accumulated viewerLinks");
3532
4175
  }
3533
4176
  const viewerFilePath = content.metadata?.viewerFilePath;
3534
4177
  if (viewerFilePath) toolState.viewerFilePath = viewerFilePath;
@@ -3537,7 +4180,7 @@ Workspace: <code>${workspace}</code>
3537
4180
  const isTerminal = meta.status === "completed" || meta.status === "failed";
3538
4181
  if (!isTerminal && !meta.viewerLinks) break;
3539
4182
  await toolState.ready;
3540
- log11.debug(
4183
+ log12.debug(
3541
4184
  { toolId: meta.id, status: meta.status, hasViewerLinks: !!toolState.viewerLinks, viewerLinks: toolState.viewerLinks, name: toolState.name, msgId: toolState.msgId },
3542
4185
  "Tool completed, preparing edit"
3543
4186
  );
@@ -3559,7 +4202,7 @@ Workspace: <code>${workspace}</code>
3559
4202
  )
3560
4203
  );
3561
4204
  } catch (err) {
3562
- log11.warn(
4205
+ log12.warn(
3563
4206
  { err, msgId: toolState.msgId, textLen: formattedText.length, hasViewerLinks: !!merged.viewerLinks },
3564
4207
  "Tool update edit failed"
3565
4208
  );
@@ -3654,15 +4297,23 @@ Task completed.
3654
4297
  }
3655
4298
  }
3656
4299
  async sendPermissionRequest(sessionId, request) {
3657
- log11.info({ sessionId, requestId: request.id }, "Permission request sent");
4300
+ log12.info({ sessionId, requestId: request.id }, "Permission request sent");
3658
4301
  const session = this.core.sessionManager.getSession(
3659
4302
  sessionId
3660
4303
  );
3661
4304
  if (!session) return;
4305
+ if (request.description.includes("openacp")) {
4306
+ const allowOption = request.options.find((o) => o.isAllow);
4307
+ if (allowOption && session.permissionGate.requestId === request.id) {
4308
+ log12.info({ sessionId, requestId: request.id }, "Auto-approving openacp command");
4309
+ session.permissionGate.resolve(allowOption.id);
4310
+ }
4311
+ return;
4312
+ }
3662
4313
  if (session.dangerousMode) {
3663
4314
  const allowOption = request.options.find((o) => o.isAllow);
3664
4315
  if (allowOption && session.permissionGate.requestId === request.id) {
3665
- log11.info({ sessionId, requestId: request.id, optionId: allowOption.id }, "Dangerous mode: auto-approving permission");
4316
+ log12.info({ sessionId, requestId: request.id, optionId: allowOption.id }, "Dangerous mode: auto-approving permission");
3666
4317
  session.permissionGate.resolve(allowOption.id);
3667
4318
  }
3668
4319
  return;
@@ -3673,7 +4324,7 @@ Task completed.
3673
4324
  }
3674
4325
  async sendNotification(notification) {
3675
4326
  if (notification.sessionId === this.assistantSession?.id) return;
3676
- log11.info(
4327
+ log12.info(
3677
4328
  { sessionId: notification.sessionId, type: notification.type },
3678
4329
  "Notification sent"
3679
4330
  );
@@ -3709,7 +4360,7 @@ Task completed.
3709
4360
  );
3710
4361
  }
3711
4362
  async createSessionThread(sessionId, name) {
3712
- log11.info({ sessionId, name }, "Session topic created");
4363
+ log12.info({ sessionId, name }, "Session topic created");
3713
4364
  return String(
3714
4365
  await createSessionTopic(this.bot, this.telegramConfig.chatId, name)
3715
4366
  );
@@ -3730,6 +4381,17 @@ Task completed.
3730
4381
  newName
3731
4382
  );
3732
4383
  }
4384
+ async deleteSessionThread(sessionId) {
4385
+ const record = this.core.sessionManager.getSessionRecord(sessionId);
4386
+ const platform = record?.platform;
4387
+ const topicId = platform?.topicId;
4388
+ if (!topicId) return;
4389
+ try {
4390
+ await this.bot.api.deleteForumTopic(this.telegramConfig.chatId, topicId);
4391
+ } catch (err) {
4392
+ log12.warn({ err, sessionId, topicId }, "Failed to delete forum topic (may already be deleted)");
4393
+ }
4394
+ }
3733
4395
  async sendSkillCommands(sessionId, commands) {
3734
4396
  if (sessionId === this.assistantSession?.id) return;
3735
4397
  const session = this.core.sessionManager.getSession(sessionId);
@@ -3758,7 +4420,16 @@ Task completed.
3758
4420
  { parse_mode: "HTML" }
3759
4421
  );
3760
4422
  return;
3761
- } catch {
4423
+ } catch (err) {
4424
+ const msg = err instanceof Error ? err.message : "";
4425
+ if (msg.includes("message is not modified")) {
4426
+ return;
4427
+ }
4428
+ try {
4429
+ await this.bot.api.deleteMessage(this.telegramConfig.chatId, existingMsgId);
4430
+ } catch {
4431
+ }
4432
+ this.skillMessages.delete(sessionId);
3762
4433
  }
3763
4434
  }
3764
4435
  try {
@@ -3791,7 +4462,7 @@ Task completed.
3791
4462
  { disable_notification: true }
3792
4463
  );
3793
4464
  } catch (err) {
3794
- log11.error({ err, sessionId }, "Failed to send skill commands");
4465
+ log12.error({ err, sessionId }, "Failed to send skill commands");
3795
4466
  }
3796
4467
  }
3797
4468
  async cleanupSkillCommands(sessionId) {
@@ -3859,6 +4530,7 @@ export {
3859
4530
  OpenACPCore,
3860
4531
  ChannelAdapter,
3861
4532
  ApiServer,
4533
+ TopicManager,
3862
4534
  TelegramAdapter
3863
4535
  };
3864
- //# sourceMappingURL=chunk-VRXFGWZJ.js.map
4536
+ //# sourceMappingURL=chunk-2M4O7AFI.js.map