@openacp/cli 0.4.11 → 0.5.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (74) hide show
  1. package/README.md +41 -3
  2. package/dist/agent-catalog-LAAVBVLY.js +10 -0
  3. package/dist/agent-dependencies-FCLRGMZM.js +23 -0
  4. package/dist/agent-registry-KZANAFXQ.js +8 -0
  5. package/dist/agent-store-ZBXGOFPH.js +8 -0
  6. package/dist/chunk-5HGXUCMX.js +83 -0
  7. package/dist/chunk-5HGXUCMX.js.map +1 -0
  8. package/dist/chunk-5MH66WUY.js +424 -0
  9. package/dist/chunk-5MH66WUY.js.map +1 -0
  10. package/dist/{chunk-FKOARMAE.js → chunk-776VAU3T.js} +3 -3
  11. package/dist/chunk-GUHCS6X7.js +282 -0
  12. package/dist/chunk-GUHCS6X7.js.map +1 -0
  13. package/dist/{chunk-3DIPXFZJ.js → chunk-IRGYTNLP.js} +2 -2
  14. package/dist/chunk-IURZ4QHG.js +91 -0
  15. package/dist/chunk-IURZ4QHG.js.map +1 -0
  16. package/dist/{chunk-WYZFGHHI.js → chunk-JRF4G4X7.js} +60 -24
  17. package/dist/chunk-JRF4G4X7.js.map +1 -0
  18. package/dist/chunk-NAMYZIS5.js +1 -0
  19. package/dist/{chunk-ZW444AQY.js → chunk-NDR5JCS7.js} +2 -2
  20. package/dist/{chunk-66RVSUAR.js → chunk-PHC67OP4.js} +567 -103
  21. package/dist/chunk-PHC67OP4.js.map +1 -0
  22. package/dist/{chunk-W7QQA6CW.js → chunk-QODDJ4PH.js} +83 -36
  23. package/dist/chunk-QODDJ4PH.js.map +1 -0
  24. package/dist/{chunk-YRJEZD7R.js → chunk-VBEWSWVL.js} +2 -2
  25. package/dist/{chunk-C33LTDZV.js → chunk-Z46LGZ7R.js} +21 -8
  26. package/dist/chunk-Z46LGZ7R.js.map +1 -0
  27. package/dist/cli.js +440 -64
  28. package/dist/cli.js.map +1 -1
  29. package/dist/{config-XURP6B3S.js → config-PCPIBPUA.js} +2 -2
  30. package/dist/config-editor-RGV6VKPZ.js +12 -0
  31. package/dist/{config-registry-OGX4YM2U.js → config-registry-SNKA2EH2.js} +2 -2
  32. package/dist/{daemon-GWJM2S4A.js → daemon-JZLFRUW6.js} +3 -3
  33. package/dist/daemon-JZLFRUW6.js.map +1 -0
  34. package/dist/data/registry-snapshot.json +876 -0
  35. package/dist/doctor-N2HKKUUQ.js +9 -0
  36. package/dist/doctor-N2HKKUUQ.js.map +1 -0
  37. package/dist/index.d.ts +138 -17
  38. package/dist/index.js +24 -15
  39. package/dist/integrate-X7LI6MUO.js +257 -0
  40. package/dist/integrate-X7LI6MUO.js.map +1 -0
  41. package/dist/{main-2QKD2EI2.js → main-DSQBCJHR.js} +18 -15
  42. package/dist/{main-2QKD2EI2.js.map → main-DSQBCJHR.js.map} +1 -1
  43. package/dist/{menu-CARRTW2F.js → menu-J5YVH665.js} +2 -4
  44. package/dist/menu-J5YVH665.js.map +1 -0
  45. package/dist/{setup-TTOL7XAN.js → setup-3A3XDGCM.js} +4 -3
  46. package/dist/setup-3A3XDGCM.js.map +1 -0
  47. package/dist/suggest-RST5VOHB.js +36 -0
  48. package/dist/suggest-RST5VOHB.js.map +1 -0
  49. package/package.json +11 -2
  50. package/dist/agent-registry-7HC6D4CH.js +0 -7
  51. package/dist/chunk-66RVSUAR.js.map +0 -1
  52. package/dist/chunk-BGKQHQB4.js +0 -276
  53. package/dist/chunk-BGKQHQB4.js.map +0 -1
  54. package/dist/chunk-C33LTDZV.js.map +0 -1
  55. package/dist/chunk-VA2M52CM.js +0 -15
  56. package/dist/chunk-VA2M52CM.js.map +0 -1
  57. package/dist/chunk-W7QQA6CW.js.map +0 -1
  58. package/dist/chunk-WYZFGHHI.js.map +0 -1
  59. package/dist/config-editor-AALY3URF.js +0 -11
  60. package/dist/doctor-X477CVZN.js +0 -9
  61. package/dist/integrate-WUPLRJD3.js +0 -145
  62. package/dist/integrate-WUPLRJD3.js.map +0 -1
  63. /package/dist/{agent-registry-7HC6D4CH.js.map → agent-catalog-LAAVBVLY.js.map} +0 -0
  64. /package/dist/{config-XURP6B3S.js.map → agent-dependencies-FCLRGMZM.js.map} +0 -0
  65. /package/dist/{config-editor-AALY3URF.js.map → agent-registry-KZANAFXQ.js.map} +0 -0
  66. /package/dist/{config-registry-OGX4YM2U.js.map → agent-store-ZBXGOFPH.js.map} +0 -0
  67. /package/dist/{chunk-FKOARMAE.js.map → chunk-776VAU3T.js.map} +0 -0
  68. /package/dist/{chunk-3DIPXFZJ.js.map → chunk-IRGYTNLP.js.map} +0 -0
  69. /package/dist/{daemon-GWJM2S4A.js.map → chunk-NAMYZIS5.js.map} +0 -0
  70. /package/dist/{chunk-ZW444AQY.js.map → chunk-NDR5JCS7.js.map} +0 -0
  71. /package/dist/{chunk-YRJEZD7R.js.map → chunk-VBEWSWVL.js.map} +0 -0
  72. /package/dist/{doctor-X477CVZN.js.map → config-PCPIBPUA.js.map} +0 -0
  73. /package/dist/{menu-CARRTW2F.js.map → config-editor-RGV6VKPZ.js.map} +0 -0
  74. /package/dist/{setup-TTOL7XAN.js.map → config-registry-SNKA2EH2.js.map} +0 -0
@@ -1,29 +1,25 @@
1
1
  import {
2
2
  DoctorEngine
3
- } from "./chunk-3DIPXFZJ.js";
4
- import {
5
- getAgentCapabilities
6
- } from "./chunk-VA2M52CM.js";
3
+ } from "./chunk-IRGYTNLP.js";
7
4
  import {
8
5
  buildMenuKeyboard,
9
6
  buildSkillMessages,
10
- escapeHtml,
11
- formatToolCall,
12
- formatToolUpdate,
13
- formatUsage,
14
- handleAgents,
15
7
  handleClear,
16
8
  handleHelp,
17
- handleMenu,
18
- markdownToTelegramHtml,
19
- splitMessage
20
- } from "./chunk-BGKQHQB4.js";
9
+ handleMenu
10
+ } from "./chunk-IURZ4QHG.js";
11
+ import {
12
+ AgentCatalog
13
+ } from "./chunk-5MH66WUY.js";
14
+ import {
15
+ getAgentCapabilities
16
+ } from "./chunk-GUHCS6X7.js";
21
17
  import {
22
18
  getConfigValue,
23
19
  getSafeFields,
24
20
  isHotReloadable,
25
21
  resolveOptions
26
- } from "./chunk-C33LTDZV.js";
22
+ } from "./chunk-Z46LGZ7R.js";
27
23
  import {
28
24
  createChildLogger,
29
25
  createSessionLogger
@@ -72,7 +68,7 @@ var StderrCapture = class {
72
68
  };
73
69
 
74
70
  // src/core/agent-instance.ts
75
- import { spawn, execSync } from "child_process";
71
+ import { spawn, execFileSync } from "child_process";
76
72
  import { Transform } from "stream";
77
73
  import fs from "fs";
78
74
  import path from "path";
@@ -123,7 +119,7 @@ function resolveAgentCommand(cmd) {
123
119
  }
124
120
  }
125
121
  try {
126
- const fullPath = execSync(`which ${cmd}`, { encoding: "utf-8" }).trim();
122
+ const fullPath = execFileSync("which", [cmd], { encoding: "utf-8" }).trim();
127
123
  if (fullPath) {
128
124
  const content = fs.readFileSync(fullPath, "utf-8");
129
125
  if (content.startsWith("#!/usr/bin/env node")) {
@@ -501,31 +497,29 @@ ${stderr}`
501
497
 
502
498
  // src/core/agent-manager.ts
503
499
  var AgentManager = class {
504
- constructor(config) {
505
- this.config = config;
500
+ constructor(catalog) {
501
+ this.catalog = catalog;
506
502
  }
507
503
  getAvailableAgents() {
508
- return Object.entries(this.config.agents).map(([name, cfg]) => ({
509
- name,
510
- command: cfg.command,
511
- args: cfg.args,
512
- workingDirectory: cfg.workingDirectory,
513
- env: cfg.env
504
+ const installed = this.catalog.getInstalledEntries();
505
+ return Object.entries(installed).map(([key, agent]) => ({
506
+ name: key,
507
+ command: agent.command,
508
+ args: agent.args,
509
+ env: agent.env
514
510
  }));
515
511
  }
516
512
  getAgent(name) {
517
- const cfg = this.config.agents[name];
518
- if (!cfg) return void 0;
519
- return { name, ...cfg };
513
+ return this.catalog.resolve(name);
520
514
  }
521
515
  async spawn(agentName, workingDirectory) {
522
516
  const agentDef = this.getAgent(agentName);
523
- if (!agentDef) throw new Error(`Agent "${agentName}" not found in config`);
517
+ if (!agentDef) throw new Error(`Agent "${agentName}" is not installed. Run "openacp agents install ${agentName}" to add it.`);
524
518
  return AgentInstance.spawn(agentDef, workingDirectory);
525
519
  }
526
520
  async resume(agentName, workingDirectory, agentSessionId) {
527
521
  const agentDef = this.getAgent(agentName);
528
- if (!agentDef) throw new Error(`Agent "${agentName}" not found in config`);
522
+ if (!agentDef) throw new Error(`Agent "${agentName}" is not installed. Run "openacp agents install ${agentName}" to add it.`);
529
523
  return AgentInstance.resume(agentDef, workingDirectory, agentSessionId);
530
524
  }
531
525
  };
@@ -652,28 +646,40 @@ var PromptQueue = class {
652
646
  };
653
647
 
654
648
  // src/core/permission-gate.ts
649
+ var DEFAULT_TIMEOUT_MS = 10 * 60 * 1e3;
655
650
  var PermissionGate = class {
656
651
  request;
657
652
  resolveFn;
658
653
  rejectFn;
659
654
  settled = false;
655
+ timeoutTimer;
656
+ timeoutMs;
657
+ constructor(timeoutMs) {
658
+ this.timeoutMs = timeoutMs ?? DEFAULT_TIMEOUT_MS;
659
+ }
660
660
  setPending(request) {
661
661
  this.request = request;
662
662
  this.settled = false;
663
+ this.clearTimeout();
663
664
  return new Promise((resolve2, reject) => {
664
665
  this.resolveFn = resolve2;
665
666
  this.rejectFn = reject;
667
+ this.timeoutTimer = setTimeout(() => {
668
+ this.reject("Permission request timed out (no response received)");
669
+ }, this.timeoutMs);
666
670
  });
667
671
  }
668
672
  resolve(optionId) {
669
673
  if (this.settled || !this.resolveFn) return;
670
674
  this.settled = true;
675
+ this.clearTimeout();
671
676
  this.resolveFn(optionId);
672
677
  this.cleanup();
673
678
  }
674
679
  reject(reason) {
675
680
  if (this.settled || !this.rejectFn) return;
676
681
  this.settled = true;
682
+ this.clearTimeout();
677
683
  this.rejectFn(new Error(reason ?? "Permission rejected"));
678
684
  this.cleanup();
679
685
  }
@@ -687,6 +693,12 @@ var PermissionGate = class {
687
693
  get requestId() {
688
694
  return this.request?.id;
689
695
  }
696
+ clearTimeout() {
697
+ if (this.timeoutTimer) {
698
+ clearTimeout(this.timeoutTimer);
699
+ this.timeoutTimer = void 0;
700
+ }
701
+ }
690
702
  cleanup() {
691
703
  this.request = void 0;
692
704
  this.resolveFn = void 0;
@@ -1453,6 +1465,7 @@ var JsonFileSessionStore = class {
1453
1465
  var log5 = createChildLogger({ module: "core" });
1454
1466
  var OpenACPCore = class {
1455
1467
  configManager;
1468
+ agentCatalog;
1456
1469
  agentManager;
1457
1470
  sessionManager;
1458
1471
  notificationManager;
@@ -1466,7 +1479,9 @@ var OpenACPCore = class {
1466
1479
  constructor(configManager) {
1467
1480
  this.configManager = configManager;
1468
1481
  const config = configManager.get();
1469
- this.agentManager = new AgentManager(config);
1482
+ this.agentCatalog = new AgentCatalog();
1483
+ this.agentCatalog.load();
1484
+ this.agentManager = new AgentManager(this.agentCatalog);
1470
1485
  const storePath = path3.join(os.homedir(), ".openacp", "sessions.json");
1471
1486
  this.sessionStore = new JsonFileSessionStore(
1472
1487
  storePath,
@@ -1494,6 +1509,9 @@ var OpenACPCore = class {
1494
1509
  this.adapters.set(name, adapter);
1495
1510
  }
1496
1511
  async start() {
1512
+ this.agentCatalog.refreshRegistryIfStale().catch((err) => {
1513
+ log5.warn({ err }, "Background registry refresh failed");
1514
+ });
1497
1515
  for (const adapter of this.adapters.values()) {
1498
1516
  await adapter.start();
1499
1517
  }
@@ -1631,8 +1649,9 @@ var OpenACPCore = class {
1631
1649
  const config = this.configManager.get();
1632
1650
  const resolvedAgent = agentName || config.defaultAgent;
1633
1651
  log5.info({ channelId, agentName: resolvedAgent }, "New session request");
1652
+ const agentDef = this.agentCatalog.resolve(resolvedAgent);
1634
1653
  const resolvedWorkspace = this.configManager.resolveWorkspace(
1635
- workspacePath || config.agents[resolvedAgent]?.workingDirectory
1654
+ workspacePath || agentDef?.workingDirectory
1636
1655
  );
1637
1656
  return this.createSession({
1638
1657
  channelId,
@@ -2128,7 +2147,7 @@ var ApiServer = class {
2128
2147
  this.sendJson(res, 200, { version: getVersion() });
2129
2148
  }
2130
2149
  async handleGetEditableConfig(res) {
2131
- const { getSafeFields: getSafeFields2, resolveOptions: resolveOptions2, getConfigValue: getConfigValue2 } = await import("./config-registry-OGX4YM2U.js");
2150
+ const { getSafeFields: getSafeFields2, resolveOptions: resolveOptions2, getConfigValue: getConfigValue2 } = await import("./config-registry-SNKA2EH2.js");
2132
2151
  const config = this.core.configManager.get();
2133
2152
  const safeFields = getSafeFields2();
2134
2153
  const fields = safeFields.map((def) => ({
@@ -2182,7 +2201,7 @@ var ApiServer = class {
2182
2201
  return;
2183
2202
  }
2184
2203
  target[lastKey] = value;
2185
- const { ConfigSchema } = await import("./config-XURP6B3S.js");
2204
+ const { ConfigSchema } = await import("./config-PCPIBPUA.js");
2186
2205
  const result = ConfigSchema.safeParse(cloned);
2187
2206
  if (!result.success) {
2188
2207
  this.sendJson(res, 400, {
@@ -2199,7 +2218,7 @@ var ApiServer = class {
2199
2218
  }
2200
2219
  updateTarget[lastKey] = value;
2201
2220
  await this.core.configManager.save(updates, configPath);
2202
- const { isHotReloadable: isHotReloadable2 } = await import("./config-registry-OGX4YM2U.js");
2221
+ const { isHotReloadable: isHotReloadable2 } = await import("./config-registry-SNKA2EH2.js");
2203
2222
  const needsRestart = !isHotReloadable2(configPath);
2204
2223
  this.sendJson(res, 200, {
2205
2224
  ok: true,
@@ -2485,6 +2504,169 @@ function buildDeepLink(chatId, messageId) {
2485
2504
  // src/adapters/telegram/commands/new-session.ts
2486
2505
  import { InlineKeyboard as InlineKeyboard2 } from "grammy";
2487
2506
 
2507
+ // src/adapters/telegram/formatting.ts
2508
+ function escapeHtml(text) {
2509
+ if (!text) return "";
2510
+ return text.replace(/&/g, "&amp;").replace(/</g, "&lt;").replace(/>/g, "&gt;");
2511
+ }
2512
+ function markdownToTelegramHtml(md) {
2513
+ const codeBlocks = [];
2514
+ const inlineCodes = [];
2515
+ let text = md.replace(/```(\w*)\n?([\s\S]*?)```/g, (_match, lang, code) => {
2516
+ const index = codeBlocks.length;
2517
+ const escapedCode = escapeHtml(code);
2518
+ const langAttr = lang ? ` class="language-${escapeHtml(lang)}"` : "";
2519
+ codeBlocks.push(`<pre><code${langAttr}>${escapedCode}</code></pre>`);
2520
+ return `\0CODE_BLOCK_${index}\0`;
2521
+ });
2522
+ text = text.replace(/`([^`]+)`/g, (_match, code) => {
2523
+ const index = inlineCodes.length;
2524
+ inlineCodes.push(`<code>${escapeHtml(code)}</code>`);
2525
+ return `\0INLINE_CODE_${index}\0`;
2526
+ });
2527
+ text = escapeHtml(text);
2528
+ text = text.replace(/\*\*(.+?)\*\*/g, "<b>$1</b>");
2529
+ text = text.replace(/(?<!\*)\*(?!\*)(.+?)(?<!\*)\*(?!\*)/g, "<i>$1</i>");
2530
+ text = text.replace(/\[([^\]]+)\]\(([^)]+)\)/g, '<a href="$2">$1</a>');
2531
+ text = text.replace(/\x00CODE_BLOCK_(\d+)\x00/g, (_match, idx) => {
2532
+ return codeBlocks[parseInt(idx, 10)];
2533
+ });
2534
+ text = text.replace(/\x00INLINE_CODE_(\d+)\x00/g, (_match, idx) => {
2535
+ return inlineCodes[parseInt(idx, 10)];
2536
+ });
2537
+ return text;
2538
+ }
2539
+ var STATUS_ICON = {
2540
+ pending: "\u23F3",
2541
+ in_progress: "\u{1F504}",
2542
+ completed: "\u2705",
2543
+ failed: "\u274C"
2544
+ };
2545
+ var KIND_ICON = {
2546
+ read: "\u{1F4D6}",
2547
+ edit: "\u270F\uFE0F",
2548
+ delete: "\u{1F5D1}\uFE0F",
2549
+ execute: "\u25B6\uFE0F",
2550
+ search: "\u{1F50D}",
2551
+ fetch: "\u{1F310}",
2552
+ think: "\u{1F9E0}",
2553
+ move: "\u{1F4E6}",
2554
+ other: "\u{1F6E0}\uFE0F"
2555
+ };
2556
+ function extractContentText(content, depth = 0) {
2557
+ if (!content || depth > 5) return "";
2558
+ if (typeof content === "string") return content;
2559
+ if (Array.isArray(content)) {
2560
+ return content.map((c) => extractContentText(c, depth + 1)).filter(Boolean).join("\n");
2561
+ }
2562
+ if (typeof content === "object" && content !== null) {
2563
+ const c = content;
2564
+ if (c.type === "text" && typeof c.text === "string") return c.text;
2565
+ if (typeof c.text === "string") return c.text;
2566
+ if (typeof c.content === "string") return c.content;
2567
+ if (c.content && typeof c.content === "object") return extractContentText(c.content, depth + 1);
2568
+ if (c.input) return extractContentText(c.input, depth + 1);
2569
+ if (c.output) return extractContentText(c.output, depth + 1);
2570
+ const keys = Object.keys(c).filter((k) => k !== "type");
2571
+ if (keys.length === 0) return "";
2572
+ return JSON.stringify(c, null, 2);
2573
+ }
2574
+ return String(content);
2575
+ }
2576
+ function truncateContent(text, maxLen = 3800) {
2577
+ if (text.length <= maxLen) return text;
2578
+ return text.slice(0, maxLen) + "\n\u2026 (truncated)";
2579
+ }
2580
+ function formatToolCall(tool) {
2581
+ const si = STATUS_ICON[tool.status || ""] || "\u{1F527}";
2582
+ const ki = KIND_ICON[tool.kind || ""] || "\u{1F6E0}\uFE0F";
2583
+ let text = `${si} ${ki} <b>${escapeHtml(tool.name || "Tool")}</b>`;
2584
+ text += formatViewerLinks(tool.viewerLinks, tool.viewerFilePath);
2585
+ if (!tool.viewerLinks) {
2586
+ const details = extractContentText(tool.content);
2587
+ if (details) {
2588
+ text += `
2589
+ <pre>${escapeHtml(truncateContent(details))}</pre>`;
2590
+ }
2591
+ }
2592
+ return text;
2593
+ }
2594
+ function formatToolUpdate(update) {
2595
+ const si = STATUS_ICON[update.status] || "\u{1F527}";
2596
+ const ki = KIND_ICON[update.kind || ""] || "\u{1F6E0}\uFE0F";
2597
+ const name = update.name || "Tool";
2598
+ let text = `${si} ${ki} <b>${escapeHtml(name)}</b>`;
2599
+ text += formatViewerLinks(update.viewerLinks, update.viewerFilePath);
2600
+ if (!update.viewerLinks) {
2601
+ const details = extractContentText(update.content);
2602
+ if (details) {
2603
+ text += `
2604
+ <pre>${escapeHtml(truncateContent(details))}</pre>`;
2605
+ }
2606
+ }
2607
+ return text;
2608
+ }
2609
+ function formatViewerLinks(links, filePath) {
2610
+ if (!links) return "";
2611
+ const fileName = filePath ? filePath.split("/").pop() || filePath : "";
2612
+ let text = "\n";
2613
+ if (links.file) text += `
2614
+ \u{1F4C4} <a href="${escapeHtml(links.file)}">View ${escapeHtml(fileName || "file")}</a>`;
2615
+ if (links.diff) text += `
2616
+ \u{1F4DD} <a href="${escapeHtml(links.diff)}">View diff${fileName ? ` \u2014 ${escapeHtml(fileName)}` : ""}</a>`;
2617
+ return text;
2618
+ }
2619
+ function formatTokens(n) {
2620
+ return n >= 1e3 ? `${Math.round(n / 1e3)}k` : String(n);
2621
+ }
2622
+ function progressBar(ratio) {
2623
+ const filled = Math.round(Math.min(ratio, 1) * 10);
2624
+ return "\u2593".repeat(filled) + "\u2591".repeat(10 - filled);
2625
+ }
2626
+ function formatUsage(usage) {
2627
+ const { tokensUsed, contextSize } = usage;
2628
+ if (tokensUsed == null) return "\u{1F4CA} Usage data unavailable";
2629
+ if (contextSize == null) return `\u{1F4CA} ${formatTokens(tokensUsed)} tokens`;
2630
+ const ratio = tokensUsed / contextSize;
2631
+ const pct = Math.round(ratio * 100);
2632
+ const bar = progressBar(ratio);
2633
+ const emoji = pct >= 85 ? "\u26A0\uFE0F" : "\u{1F4CA}";
2634
+ return `${emoji} ${formatTokens(tokensUsed)} / ${formatTokens(contextSize)} tokens
2635
+ ${bar} ${pct}%`;
2636
+ }
2637
+ function splitMessage(text, maxLength = 3800) {
2638
+ if (text.length <= maxLength) return [text];
2639
+ const chunks = [];
2640
+ let remaining = text;
2641
+ while (remaining.length > 0) {
2642
+ if (remaining.length <= maxLength) {
2643
+ chunks.push(remaining);
2644
+ break;
2645
+ }
2646
+ const wouldLeaveSmall = remaining.length < maxLength * 1.3;
2647
+ const searchLimit = wouldLeaveSmall ? Math.floor(remaining.length / 2) + 300 : maxLength;
2648
+ let splitAt = remaining.lastIndexOf("\n\n", searchLimit);
2649
+ if (splitAt === -1 || splitAt < searchLimit * 0.2) {
2650
+ splitAt = remaining.lastIndexOf("\n", searchLimit);
2651
+ }
2652
+ if (splitAt === -1 || splitAt < searchLimit * 0.2) {
2653
+ splitAt = searchLimit;
2654
+ }
2655
+ const candidate = remaining.slice(0, splitAt);
2656
+ const fences = candidate.match(/```/g);
2657
+ if (fences && fences.length % 2 !== 0) {
2658
+ const closingFence = remaining.indexOf("```", splitAt);
2659
+ if (closingFence !== -1) {
2660
+ const afterFence = remaining.indexOf("\n", closingFence + 3);
2661
+ splitAt = afterFence !== -1 ? afterFence + 1 : closingFence + 3;
2662
+ }
2663
+ }
2664
+ chunks.push(remaining.slice(0, splitAt));
2665
+ remaining = remaining.slice(splitAt).replace(/^\n+/, "");
2666
+ }
2667
+ return chunks;
2668
+ }
2669
+
2488
2670
  // src/adapters/telegram/commands/admin.ts
2489
2671
  import { InlineKeyboard } from "grammy";
2490
2672
  var log9 = createChildLogger({ module: "telegram-cmd-admin" });
@@ -2688,37 +2870,12 @@ async function handleNew(ctx, core, chatId, assistant) {
2688
2870
  return;
2689
2871
  }
2690
2872
  }
2691
- const userId = ctx.from?.id;
2692
- if (!userId) return;
2693
- const agents = core.agentManager.getAvailableAgents();
2694
- const config = core.configManager.get();
2695
- if (agentName || agents.length === 1) {
2696
- const selectedAgent = agentName || config.defaultAgent;
2697
- await startWorkspaceStep(ctx, core, chatId, userId, selectedAgent);
2698
- return;
2699
- }
2700
- const keyboard = new InlineKeyboard2();
2701
- for (const agent of agents) {
2702
- const label = agent.name === config.defaultAgent ? `${agent.name} (default)` : agent.name;
2703
- keyboard.text(label, `m:new:agent:${agent.name}`).row();
2704
- }
2705
- keyboard.text("\u274C Cancel", "m:new:cancel");
2706
- const msg = await ctx.reply(
2707
- `\u{1F916} <b>Choose an agent:</b>`,
2708
- { parse_mode: "HTML", reply_markup: keyboard }
2709
- );
2710
- cleanupPending(userId);
2711
- pendingNewSessions.set(userId, {
2712
- step: "agent",
2713
- messageId: msg.message_id,
2714
- threadId: currentThreadId,
2715
- timer: setTimeout(() => pendingNewSessions.delete(userId), PENDING_TIMEOUT_MS)
2716
- });
2873
+ await showAgentPicker(ctx, core, chatId, agentName);
2717
2874
  }
2718
2875
  async function startWorkspaceStep(ctx, core, chatId, userId, agentName) {
2719
2876
  const config = core.configManager.get();
2720
2877
  const baseDir = config.workspace.baseDir;
2721
- const keyboard = new InlineKeyboard2().text(`\u{1F4C1} Use ${baseDir}`, "m:new:ws:default").row().text("\u270F\uFE0F Enter project path", "m:new:ws:custom").row().text("\u274C Cancel", "m:new:cancel");
2878
+ const keyboard = new InlineKeyboard2().text(`\u{1F4C1} Use ${baseDir}`, "m:new:ws:default").row().text("\u270F\uFE0F Enter project path", "m:new:ws:custom");
2722
2879
  const text = `\u{1F4C1} <b>Where should ${escapeHtml(agentName)} work?</b>
2723
2880
 
2724
2881
  Enter the path to your project folder \u2014 the agent will read, write, and run code there.
@@ -2953,30 +3110,35 @@ async function handlePendingWorkspaceInput(ctx, core, chatId, assistantTopicId)
2953
3110
  return true;
2954
3111
  }
2955
3112
  async function startInteractiveNewSession(ctx, core, chatId, agentName) {
3113
+ await showAgentPicker(ctx, core, chatId, agentName);
3114
+ }
3115
+ async function showAgentPicker(ctx, core, chatId, agentName) {
2956
3116
  const userId = ctx.from?.id;
2957
3117
  if (!userId) return;
2958
- const agents = core.agentManager.getAvailableAgents();
3118
+ const installedEntries = core.agentCatalog.getInstalledEntries();
3119
+ const agentKeys = Object.keys(installedEntries);
2959
3120
  const config = core.configManager.get();
2960
- if (agentName || agents.length === 1) {
3121
+ if (agentName || agentKeys.length === 1) {
2961
3122
  const selectedAgent = agentName || config.defaultAgent;
2962
3123
  await startWorkspaceStep(ctx, core, chatId, userId, selectedAgent);
2963
3124
  return;
2964
3125
  }
2965
3126
  const keyboard = new InlineKeyboard2();
2966
- for (const agent of agents) {
2967
- const label = agent.name === config.defaultAgent ? `${agent.name} (default)` : agent.name;
2968
- keyboard.text(label, `m:new:agent:${agent.name}`).row();
3127
+ for (const key of agentKeys) {
3128
+ const agent = installedEntries[key];
3129
+ const label = key === config.defaultAgent ? `${agent.name} (default)` : agent.name;
3130
+ keyboard.text(label, `m:new:agent:${key}`).row();
2969
3131
  }
2970
- keyboard.text("\u274C Cancel", "m:new:cancel");
2971
3132
  const msg = await ctx.reply(
2972
3133
  `\u{1F916} <b>Choose an agent:</b>`,
2973
3134
  { parse_mode: "HTML", reply_markup: keyboard }
2974
3135
  );
2975
3136
  cleanupPending(userId);
3137
+ const threadId = ctx.message?.message_thread_id ?? ctx.callbackQuery?.message?.message_thread_id;
2976
3138
  pendingNewSessions.set(userId, {
2977
3139
  step: "agent",
2978
3140
  messageId: msg.message_id,
2979
- threadId: ctx.callbackQuery?.message?.message_thread_id,
3141
+ threadId,
2980
3142
  timer: setTimeout(() => pendingNewSessions.delete(userId), PENDING_TIMEOUT_MS)
2981
3143
  });
2982
3144
  }
@@ -3350,12 +3512,250 @@ function setupSessionCallbacks(bot, core, chatId, systemTopicIds) {
3350
3512
  });
3351
3513
  }
3352
3514
 
3353
- // src/adapters/telegram/commands/integrate.ts
3515
+ // src/adapters/telegram/commands/agents.ts
3354
3516
  import { InlineKeyboard as InlineKeyboard4 } from "grammy";
3517
+ var AGENTS_PER_PAGE = 6;
3518
+ async function handleAgents(ctx, core, page = 0) {
3519
+ const catalog = core.agentCatalog;
3520
+ const items = catalog.getAvailable();
3521
+ const installed = items.filter((i) => i.installed);
3522
+ const available = items.filter((i) => !i.installed);
3523
+ let text = "<b>\u{1F916} Agents</b>\n\n";
3524
+ if (installed.length > 0) {
3525
+ text += "<b>Installed:</b>\n";
3526
+ for (const item of installed) {
3527
+ text += `\u2705 <b>${escapeHtml(item.name)}</b>`;
3528
+ if (item.description) {
3529
+ text += ` \u2014 <i>${escapeHtml(truncate(item.description, 50))}</i>`;
3530
+ }
3531
+ text += "\n";
3532
+ }
3533
+ text += "\n";
3534
+ }
3535
+ if (available.length > 0) {
3536
+ const totalPages = Math.ceil(available.length / AGENTS_PER_PAGE);
3537
+ const safePage = Math.max(0, Math.min(page, totalPages - 1));
3538
+ const pageItems = available.slice(safePage * AGENTS_PER_PAGE, (safePage + 1) * AGENTS_PER_PAGE);
3539
+ text += `<b>Available to install:</b>`;
3540
+ if (totalPages > 1) {
3541
+ text += ` (${safePage + 1}/${totalPages})`;
3542
+ }
3543
+ text += "\n";
3544
+ for (const item of pageItems) {
3545
+ if (item.available) {
3546
+ text += `\u2B07\uFE0F <b>${escapeHtml(item.name)}</b>`;
3547
+ } else {
3548
+ const deps = item.missingDeps?.join(", ") ?? "requirements not met";
3549
+ text += `\u26A0\uFE0F <b>${escapeHtml(item.name)}</b> <i>(needs: ${escapeHtml(deps)})</i>`;
3550
+ }
3551
+ if (item.description) {
3552
+ text += `
3553
+ <i>${escapeHtml(truncate(item.description, 60))}</i>`;
3554
+ }
3555
+ text += "\n";
3556
+ }
3557
+ const keyboard = new InlineKeyboard4();
3558
+ const installable = pageItems.filter((i) => i.available);
3559
+ for (let i = 0; i < installable.length; i += 2) {
3560
+ const row = installable.slice(i, i + 2);
3561
+ for (const item of row) {
3562
+ keyboard.text(`\u2B07\uFE0F ${item.name}`, `ag:install:${item.key}`);
3563
+ }
3564
+ keyboard.row();
3565
+ }
3566
+ if (totalPages > 1) {
3567
+ if (safePage > 0) {
3568
+ keyboard.text("\u25C0\uFE0F Prev", `ag:page:${safePage - 1}`);
3569
+ }
3570
+ if (safePage < totalPages - 1) {
3571
+ keyboard.text("Next \u25B6\uFE0F", `ag:page:${safePage + 1}`);
3572
+ }
3573
+ keyboard.row();
3574
+ }
3575
+ if (available.some((i) => !i.available)) {
3576
+ text += "\n\u{1F4A1} <i>Agents marked \u26A0\uFE0F need additional setup. Use</i> <code>openacp agents info &lt;name&gt;</code> <i>for details.</i>\n";
3577
+ }
3578
+ await ctx.reply(text, { parse_mode: "HTML", reply_markup: keyboard });
3579
+ } else {
3580
+ text += "<i>All agents are already installed!</i>";
3581
+ await ctx.reply(text, { parse_mode: "HTML" });
3582
+ }
3583
+ }
3584
+ async function handleInstall(ctx, core) {
3585
+ const text = (ctx.message?.text ?? "").trim();
3586
+ const parts = text.split(/\s+/);
3587
+ const nameOrId = parts[1];
3588
+ if (!nameOrId) {
3589
+ await ctx.reply(
3590
+ "\u{1F4E6} <b>Install an agent</b>\n\nUsage: <code>/install &lt;agent-name&gt;</code>\nExample: <code>/install gemini</code>\n\nUse /agents to browse available agents.",
3591
+ { parse_mode: "HTML" }
3592
+ );
3593
+ return;
3594
+ }
3595
+ await installAgentWithProgress(ctx, core, nameOrId);
3596
+ }
3597
+ async function handleAgentCallback(ctx, core) {
3598
+ const data = ctx.callbackQuery?.data ?? "";
3599
+ await ctx.answerCallbackQuery();
3600
+ if (data.startsWith("ag:install:")) {
3601
+ const nameOrId = data.replace("ag:install:", "");
3602
+ await installAgentWithProgress(ctx, core, nameOrId);
3603
+ return;
3604
+ }
3605
+ if (data.startsWith("ag:page:")) {
3606
+ const page = parseInt(data.replace("ag:page:", ""), 10);
3607
+ try {
3608
+ const catalog = core.agentCatalog;
3609
+ const items = catalog.getAvailable();
3610
+ const installed = items.filter((i) => i.installed);
3611
+ const available = items.filter((i) => !i.installed);
3612
+ let text = "<b>\u{1F916} Agents</b>\n\n";
3613
+ if (installed.length > 0) {
3614
+ text += "<b>Installed:</b>\n";
3615
+ for (const item of installed) {
3616
+ text += `\u2705 <b>${escapeHtml(item.name)}</b>`;
3617
+ if (item.description) {
3618
+ text += ` \u2014 <i>${escapeHtml(truncate(item.description, 50))}</i>`;
3619
+ }
3620
+ text += "\n";
3621
+ }
3622
+ text += "\n";
3623
+ }
3624
+ const totalPages = Math.ceil(available.length / AGENTS_PER_PAGE);
3625
+ const safePage = Math.max(0, Math.min(page, totalPages - 1));
3626
+ const pageItems = available.slice(safePage * AGENTS_PER_PAGE, (safePage + 1) * AGENTS_PER_PAGE);
3627
+ text += `<b>Available to install:</b>`;
3628
+ if (totalPages > 1) {
3629
+ text += ` (${safePage + 1}/${totalPages})`;
3630
+ }
3631
+ text += "\n";
3632
+ for (const item of pageItems) {
3633
+ if (item.available) {
3634
+ text += `\u2B07\uFE0F <b>${escapeHtml(item.name)}</b>`;
3635
+ } else {
3636
+ const deps = item.missingDeps?.join(", ") ?? "requirements not met";
3637
+ text += `\u26A0\uFE0F <b>${escapeHtml(item.name)}</b> <i>(needs: ${escapeHtml(deps)})</i>`;
3638
+ }
3639
+ if (item.description) {
3640
+ text += `
3641
+ <i>${escapeHtml(truncate(item.description, 60))}</i>`;
3642
+ }
3643
+ text += "\n";
3644
+ }
3645
+ const keyboard = new InlineKeyboard4();
3646
+ const installable = pageItems.filter((i) => i.available);
3647
+ for (let i = 0; i < installable.length; i += 2) {
3648
+ const row = installable.slice(i, i + 2);
3649
+ for (const item of row) {
3650
+ keyboard.text(`\u2B07\uFE0F ${item.name}`, `ag:install:${item.key}`);
3651
+ }
3652
+ keyboard.row();
3653
+ }
3654
+ if (totalPages > 1) {
3655
+ if (safePage > 0) {
3656
+ keyboard.text("\u25C0\uFE0F Prev", `ag:page:${safePage - 1}`);
3657
+ }
3658
+ if (safePage < totalPages - 1) {
3659
+ keyboard.text("Next \u25B6\uFE0F", `ag:page:${safePage + 1}`);
3660
+ }
3661
+ keyboard.row();
3662
+ }
3663
+ await ctx.editMessageText(text, { parse_mode: "HTML", reply_markup: keyboard });
3664
+ } catch {
3665
+ }
3666
+ }
3667
+ }
3668
+ async function installAgentWithProgress(ctx, core, nameOrId) {
3669
+ const catalog = core.agentCatalog;
3670
+ const msg = await ctx.reply(`\u23F3 Installing <b>${escapeHtml(nameOrId)}</b>...`, { parse_mode: "HTML" });
3671
+ let lastEdit = 0;
3672
+ const EDIT_THROTTLE_MS = 1500;
3673
+ const progress = {
3674
+ onStart(_id, _name) {
3675
+ },
3676
+ async onStep(step) {
3677
+ const now = Date.now();
3678
+ if (now - lastEdit > EDIT_THROTTLE_MS) {
3679
+ lastEdit = now;
3680
+ try {
3681
+ await ctx.api.editMessageText(msg.chat.id, msg.message_id, `\u23F3 <b>${escapeHtml(nameOrId)}</b>: ${escapeHtml(step)}`, { parse_mode: "HTML" });
3682
+ } catch {
3683
+ }
3684
+ }
3685
+ },
3686
+ async onDownloadProgress(percent) {
3687
+ const now = Date.now();
3688
+ if (now - lastEdit > EDIT_THROTTLE_MS) {
3689
+ lastEdit = now;
3690
+ try {
3691
+ const bar = buildProgressBar(percent);
3692
+ await ctx.api.editMessageText(msg.chat.id, msg.message_id, `\u23F3 <b>${escapeHtml(nameOrId)}</b>
3693
+ Downloading... ${bar} ${percent}%`, { parse_mode: "HTML" });
3694
+ } catch {
3695
+ }
3696
+ }
3697
+ },
3698
+ async onSuccess(name) {
3699
+ try {
3700
+ const keyboard = new InlineKeyboard4().text(`\u{1F680} Start session with ${name}`, `na:${nameOrId}`);
3701
+ await ctx.api.editMessageText(msg.chat.id, msg.message_id, `\u2705 <b>${escapeHtml(name)}</b> installed!`, { parse_mode: "HTML", reply_markup: keyboard });
3702
+ } catch {
3703
+ }
3704
+ },
3705
+ async onError(error) {
3706
+ try {
3707
+ await ctx.api.editMessageText(msg.chat.id, msg.message_id, `\u274C ${escapeHtml(error)}`, { parse_mode: "HTML" });
3708
+ } catch {
3709
+ }
3710
+ }
3711
+ };
3712
+ const result = await catalog.install(nameOrId, progress);
3713
+ if (result.ok) {
3714
+ const { getAgentCapabilities: getAgentCapabilities2 } = await import("./agent-dependencies-FCLRGMZM.js");
3715
+ const caps = getAgentCapabilities2(result.agentKey);
3716
+ if (caps.integration) {
3717
+ const { installIntegration } = await import("./integrate-X7LI6MUO.js");
3718
+ const intResult = await installIntegration(result.agentKey, caps.integration);
3719
+ if (intResult.success) {
3720
+ try {
3721
+ await ctx.reply(`\u{1F517} Handoff integration installed for <b>${escapeHtml(result.agentKey)}</b>`, { parse_mode: "HTML" });
3722
+ } catch {
3723
+ }
3724
+ }
3725
+ }
3726
+ }
3727
+ if (result.ok && result.setupSteps?.length) {
3728
+ let setupText = `\u{1F4CB} <b>Setup for ${escapeHtml(result.agentKey)}:</b>
3729
+
3730
+ `;
3731
+ for (const step of result.setupSteps) {
3732
+ setupText += `\u2192 ${escapeHtml(step)}
3733
+ `;
3734
+ }
3735
+ setupText += `
3736
+ <i>Run in terminal: openacp agents info ${escapeHtml(result.agentKey)}</i>`;
3737
+ try {
3738
+ await ctx.reply(setupText, { parse_mode: "HTML" });
3739
+ } catch {
3740
+ }
3741
+ }
3742
+ }
3743
+ function truncate(text, maxLen) {
3744
+ if (text.length <= maxLen) return text;
3745
+ return text.slice(0, maxLen - 1) + "\u2026";
3746
+ }
3747
+ function buildProgressBar(percent) {
3748
+ const filled = Math.round(percent / 10);
3749
+ const empty = 10 - filled;
3750
+ return "\u2588".repeat(filled) + "\u2591".repeat(empty);
3751
+ }
3752
+
3753
+ // src/adapters/telegram/commands/integrate.ts
3754
+ import { InlineKeyboard as InlineKeyboard5 } from "grammy";
3355
3755
  async function handleIntegrate(ctx, _core) {
3356
- const { listIntegrations } = await import("./integrate-WUPLRJD3.js");
3756
+ const { listIntegrations } = await import("./integrate-X7LI6MUO.js");
3357
3757
  const agents = listIntegrations();
3358
- const keyboard = new InlineKeyboard4();
3758
+ const keyboard = new InlineKeyboard5();
3359
3759
  for (const agent of agents) {
3360
3760
  keyboard.text(`\u{1F916} ${agent}`, `i:agent:${agent}`).row();
3361
3761
  }
@@ -3367,7 +3767,7 @@ Select an agent to manage its integrations.`,
3367
3767
  );
3368
3768
  }
3369
3769
  function buildAgentItemsKeyboard(agentName, items) {
3370
- const keyboard = new InlineKeyboard4();
3770
+ const keyboard = new InlineKeyboard5();
3371
3771
  for (const item of items) {
3372
3772
  const installed = item.isInstalled();
3373
3773
  keyboard.text(
@@ -3386,9 +3786,9 @@ function setupIntegrateCallbacks(bot, core) {
3386
3786
  } catch {
3387
3787
  }
3388
3788
  if (data === "i:back") {
3389
- const { listIntegrations } = await import("./integrate-WUPLRJD3.js");
3789
+ const { listIntegrations } = await import("./integrate-X7LI6MUO.js");
3390
3790
  const agents = listIntegrations();
3391
- const keyboard2 = new InlineKeyboard4();
3791
+ const keyboard2 = new InlineKeyboard5();
3392
3792
  for (const agent of agents) {
3393
3793
  keyboard2.text(`\u{1F916} ${agent}`, `i:agent:${agent}`).row();
3394
3794
  }
@@ -3406,7 +3806,7 @@ Select an agent to manage its integrations.`,
3406
3806
  const agentMatch = data.match(/^i:agent:(.+)$/);
3407
3807
  if (agentMatch) {
3408
3808
  const agentName2 = agentMatch[1];
3409
- const { getIntegration: getIntegration2 } = await import("./integrate-WUPLRJD3.js");
3809
+ const { getIntegration: getIntegration2 } = await import("./integrate-X7LI6MUO.js");
3410
3810
  const integration2 = getIntegration2(agentName2);
3411
3811
  if (!integration2) {
3412
3812
  await ctx.reply(`\u274C No integration available for '${escapeHtml(agentName2)}'.`, { parse_mode: "HTML" });
@@ -3433,7 +3833,7 @@ ${integration2.items.map((i) => `\u2022 <b>${escapeHtml(i.name)}</b> \u2014 ${es
3433
3833
  const action = actionMatch[1];
3434
3834
  const agentName = actionMatch[2];
3435
3835
  const itemId = actionMatch[3];
3436
- const { getIntegration } = await import("./integrate-WUPLRJD3.js");
3836
+ const { getIntegration } = await import("./integrate-X7LI6MUO.js");
3437
3837
  const integration = getIntegration(agentName);
3438
3838
  if (!integration) return;
3439
3839
  const item = integration.items.find((i) => i.id === itemId);
@@ -3469,12 +3869,12 @@ ${resultText}`,
3469
3869
  }
3470
3870
 
3471
3871
  // src/adapters/telegram/commands/settings.ts
3472
- import { InlineKeyboard as InlineKeyboard5 } from "grammy";
3872
+ import { InlineKeyboard as InlineKeyboard6 } from "grammy";
3473
3873
  var log12 = createChildLogger({ module: "telegram-settings" });
3474
3874
  function buildSettingsKeyboard(core) {
3475
3875
  const config = core.configManager.get();
3476
3876
  const fields = getSafeFields();
3477
- const kb = new InlineKeyboard5();
3877
+ const kb = new InlineKeyboard6();
3478
3878
  for (const field of fields) {
3479
3879
  const value = getConfigValue(config, field.path);
3480
3880
  const label = formatFieldLabel(field, value);
@@ -3545,7 +3945,7 @@ function setupSettingsCallbacks(bot, core, getAssistantSession) {
3545
3945
  if (!fieldDef) return;
3546
3946
  const options = resolveOptions(fieldDef, config) ?? [];
3547
3947
  const currentValue = getConfigValue(config, fieldPath);
3548
- const kb = new InlineKeyboard5();
3948
+ const kb = new InlineKeyboard6();
3549
3949
  for (const opt of options) {
3550
3950
  const marker = opt === String(currentValue) ? " \u2713" : "";
3551
3951
  kb.text(`${opt}${marker}`, `s:pick:${fieldPath}:${opt}`).row();
@@ -3617,7 +4017,7 @@ Tap to change:`, {
3617
4017
  await ctx.answerCallbackQuery();
3618
4018
  } catch {
3619
4019
  }
3620
- const { buildMenuKeyboard: buildMenuKeyboard3 } = await import("./menu-CARRTW2F.js");
4020
+ const { buildMenuKeyboard: buildMenuKeyboard3 } = await import("./menu-J5YVH665.js");
3621
4021
  try {
3622
4022
  await ctx.editMessageText(`<b>OpenACP Menu</b>
3623
4023
  Choose an action:`, {
@@ -3655,7 +4055,7 @@ function buildNestedUpdate(dotPath, value) {
3655
4055
  }
3656
4056
 
3657
4057
  // src/adapters/telegram/commands/doctor.ts
3658
- import { InlineKeyboard as InlineKeyboard6 } from "grammy";
4058
+ import { InlineKeyboard as InlineKeyboard7 } from "grammy";
3659
4059
  var log13 = createChildLogger({ module: "telegram-cmd-doctor" });
3660
4060
  var pendingFixesStore = /* @__PURE__ */ new Map();
3661
4061
  function renderReport(report) {
@@ -3673,7 +4073,7 @@ function renderReport(report) {
3673
4073
  lines.push(`<b>Result:</b> ${passed} passed, ${warnings} warnings, ${failed} failed${fixedStr}`);
3674
4074
  let keyboard;
3675
4075
  if (report.pendingFixes.length > 0) {
3676
- keyboard = new InlineKeyboard6();
4076
+ keyboard = new InlineKeyboard7();
3677
4077
  for (let i = 0; i < report.pendingFixes.length; i++) {
3678
4078
  const label = `\u{1F527} Fix: ${report.pendingFixes[i].message.slice(0, 30)}`;
3679
4079
  keyboard.text(label, `m:doctor:fix:${i}`).row();
@@ -3768,6 +4168,7 @@ function setupCommands(bot, core, chatId, assistant) {
3768
4168
  bot.command("status", (ctx) => handleStatus(ctx, core));
3769
4169
  bot.command("sessions", (ctx) => handleTopics(ctx, core));
3770
4170
  bot.command("agents", (ctx) => handleAgents(ctx, core));
4171
+ bot.command("install", (ctx) => handleInstall(ctx, core));
3771
4172
  bot.command("help", (ctx) => handleHelp(ctx));
3772
4173
  bot.command("menu", (ctx) => handleMenu(ctx));
3773
4174
  bot.command("enable_dangerous", (ctx) => handleEnableDangerous(ctx, core));
@@ -3783,6 +4184,12 @@ function setupAllCallbacks(bot, core, chatId, systemTopicIds, getAssistantSessio
3783
4184
  setupSessionCallbacks(bot, core, chatId, systemTopicIds);
3784
4185
  setupSettingsCallbacks(bot, core, getAssistantSession ?? (() => void 0));
3785
4186
  setupDoctorCallbacks(bot);
4187
+ bot.callbackQuery(/^ag:/, (ctx) => handleAgentCallback(ctx, core));
4188
+ bot.callbackQuery(/^na:/, async (ctx) => {
4189
+ const agentKey = ctx.callbackQuery.data.replace("na:", "");
4190
+ await ctx.answerCallbackQuery();
4191
+ await createSessionDirect(ctx, core, chatId, agentKey, core.configManager.get().workspace.baseDir);
4192
+ });
3786
4193
  bot.callbackQuery(/^m:/, async (ctx) => {
3787
4194
  const data = ctx.callbackQuery.data;
3788
4195
  try {
@@ -3827,6 +4234,7 @@ var STATIC_COMMANDS = [
3827
4234
  { command: "status", description: "Show status" },
3828
4235
  { command: "sessions", description: "List all sessions" },
3829
4236
  { command: "agents", description: "List available agents" },
4237
+ { command: "install", description: "Install a new agent" },
3830
4238
  { command: "help", description: "Help" },
3831
4239
  { command: "menu", description: "Show menu" },
3832
4240
  { command: "enable_dangerous", description: "Auto-approve all permission requests (session only)" },
@@ -3840,7 +4248,7 @@ var STATIC_COMMANDS = [
3840
4248
  ];
3841
4249
 
3842
4250
  // src/adapters/telegram/permissions.ts
3843
- import { InlineKeyboard as InlineKeyboard7 } from "grammy";
4251
+ import { InlineKeyboard as InlineKeyboard8 } from "grammy";
3844
4252
  import { nanoid as nanoid2 } from "nanoid";
3845
4253
  var log14 = createChildLogger({ module: "telegram-permissions" });
3846
4254
  var PermissionHandler = class {
@@ -3859,7 +4267,7 @@ var PermissionHandler = class {
3859
4267
  requestId: request.id,
3860
4268
  options: request.options.map((o) => ({ id: o.id, isAllow: o.isAllow }))
3861
4269
  });
3862
- const keyboard = new InlineKeyboard7();
4270
+ const keyboard = new InlineKeyboard8();
3863
4271
  for (const option of request.options) {
3864
4272
  const emoji = option.isAllow ? "\u2705" : "\u274C";
3865
4273
  keyboard.text(`${emoji} ${option.label}`, `p:${callbackKey}:${option.id}`);
@@ -3945,9 +4353,29 @@ A session = one conversation with one AI agent working in one project folder.
3945
4353
  Each session gets its own Telegram topic. Chat there to give instructions to the agent.
3946
4354
 
3947
4355
  ### Agents
3948
- An agent is an AI coding tool (e.g., Claude Code). You can configure multiple agents.
4356
+ An agent is an AI coding tool (e.g., Claude Code, Gemini, Cursor, Codex, etc.).
4357
+ OpenACP supports 28+ agents from the official ACP Registry (agentclientprotocol.com).
4358
+ You can install multiple agents and choose which one to use per session.
3949
4359
  The default agent is used when you don't specify one.
3950
4360
 
4361
+ ### Agent Management
4362
+ - Browse agents: \`/agents\` in Telegram or \`openacp agents\` in CLI
4363
+ - Install: tap the install button in /agents, or \`openacp agents install <name>\`
4364
+ - Uninstall: \`openacp agents uninstall <name>\`
4365
+ - Setup/login: \`openacp agents run <name> -- <args>\` (e.g., \`openacp agents run gemini -- auth login\`)
4366
+ - Details: \`openacp agents info <name>\` shows version, dependencies, and setup steps
4367
+
4368
+ Some agents need additional setup before they can be used:
4369
+ - Claude: requires \`claude login\`
4370
+ - Gemini: requires \`openacp agents run gemini -- auth login\`
4371
+ - Codex: requires setting \`OPENAI_API_KEY\` environment variable
4372
+ - GitHub Copilot: requires \`openacp agents run copilot -- auth login\`
4373
+
4374
+ Agents are installed in three ways depending on the agent:
4375
+ - **npx** \u2014 Node.js agents, downloaded automatically on first use
4376
+ - **uvx** \u2014 Python agents, downloaded automatically on first use
4377
+ - **binary** \u2014 Platform-specific binaries, downloaded to \`~/.openacp/agents/\`
4378
+
3951
4379
  ### Project Folder (Workspace)
3952
4380
  The directory where the agent reads, writes, and runs code.
3953
4381
  When creating a session, you choose which folder the agent works in.
@@ -4080,7 +4508,8 @@ Just chat naturally: "How do I create a session?", "What's the status?", "Someth
4080
4508
  | \`/cancel\` | Session topic | Cancel current session |
4081
4509
  | \`/status\` | Anywhere | Show status |
4082
4510
  | \`/sessions\` | Anywhere | List all sessions |
4083
- | \`/agents\` | Anywhere | List available agents |
4511
+ | \`/agents\` | Anywhere | Browse & install agents from ACP Registry |
4512
+ | \`/install <name>\` | Anywhere | Install an agent |
4084
4513
  | \`/enable_dangerous\` | Session topic | Auto-approve all permissions |
4085
4514
  | \`/disable_dangerous\` | Session topic | Restore permission prompts |
4086
4515
  | \`/handoff\` | Session topic | Transfer session to terminal |
@@ -4127,6 +4556,14 @@ Just chat naturally: "How do I create a session?", "What's the status?", "Someth
4127
4556
  - \`openacp config\` \u2014 Interactive config editor
4128
4557
  - \`openacp reset\` \u2014 Delete all data and start fresh
4129
4558
 
4559
+ ### Agent Management (CLI)
4560
+ - \`openacp agents\` \u2014 List all agents (installed + available from ACP Registry)
4561
+ - \`openacp agents install <name>\` \u2014 Install an agent
4562
+ - \`openacp agents uninstall <name>\` \u2014 Remove an agent
4563
+ - \`openacp agents info <name>\` \u2014 Show details, dependencies, and setup guide
4564
+ - \`openacp agents run <name> [-- args]\` \u2014 Run agent CLI directly (for login, config, etc.)
4565
+ - \`openacp agents refresh\` \u2014 Force-refresh registry cache
4566
+
4130
4567
  ### Plugins
4131
4568
  - \`openacp install <package>\` \u2014 Install adapter plugin (e.g., \`@openacp/adapter-discord\`)
4132
4569
  - \`openacp uninstall <package>\` \u2014 Remove adapter plugin
@@ -4185,10 +4622,10 @@ Config file: \`~/.openacp/config.json\`
4185
4622
  - **telegram.chatId** \u2014 Your Telegram supergroup ID
4186
4623
 
4187
4624
  ### Agents
4188
- - **agents.<name>.command** \u2014 Agent executable path (e.g., \`claude\`, \`codex\`)
4189
- - **agents.<name>.args** \u2014 Arguments to pass to the agent command
4190
- - **agents.<name>.env** \u2014 Custom environment variables for the agent subprocess
4191
4625
  - **defaultAgent** \u2014 Which agent to use by default
4626
+ - Agents are managed via \`/agents\` (Telegram) or \`openacp agents\` (CLI)
4627
+ - Installed agents are stored in \`~/.openacp/agents.json\`
4628
+ - Agent list is fetched from the ACP Registry CDN and cached locally (24h)
4192
4629
 
4193
4630
  ### Workspace
4194
4631
  - **workspace.baseDir** \u2014 Base directory for project folders (default: \`~/openacp-workspace\`)
@@ -4240,9 +4677,10 @@ Override config with env vars:
4240
4677
  - Check system health: Assistant can run health check
4241
4678
 
4242
4679
  ### Agent not found
4243
- - Check available agents: \`/agents\`
4244
- - Verify agent command is installed and in PATH
4245
- - Check config: agent command + args must be correct
4680
+ - Check available agents: \`/agents\` or \`openacp agents\`
4681
+ - Install missing agent: \`openacp agents install <name>\`
4682
+ - Some agents need login first: \`openacp agents info <name>\` to see setup steps
4683
+ - Run agent CLI for setup: \`openacp agents run <name> -- <args>\`
4246
4684
 
4247
4685
  ### Permission request not showing
4248
4686
  - Check Notifications topic for the alert
@@ -4273,6 +4711,9 @@ Override config with env vars:
4273
4711
 
4274
4712
  All data is stored in \`~/.openacp/\`:
4275
4713
  - \`config.json\` \u2014 Configuration
4714
+ - \`agents.json\` \u2014 Installed agents (managed by AgentCatalog)
4715
+ - \`registry-cache.json\` \u2014 Cached ACP Registry data (refreshes every 24h)
4716
+ - \`agents/\` \u2014 Downloaded binary agents
4276
4717
  - \`sessions/\` \u2014 Session records and state
4277
4718
  - \`topics/\` \u2014 Topic-to-session mappings
4278
4719
  - \`logs/\` \u2014 System and session logs
@@ -4304,11 +4745,16 @@ async function spawnAssistant(core, adapter, assistantTopicId) {
4304
4745
  statusCounts.set(r.status, (statusCounts.get(r.status) ?? 0) + 1);
4305
4746
  }
4306
4747
  const topicSummary = Array.from(statusCounts.entries()).map(([status, count]) => ({ status, count }));
4748
+ const installedAgents = Object.keys(core.agentCatalog.getInstalledEntries());
4749
+ const availableItems = core.agentCatalog.getAvailable();
4750
+ const availableAgentCount = availableItems.filter((i) => !i.installed).length;
4307
4751
  const ctx = {
4308
4752
  config,
4309
4753
  activeSessionCount: activeCount,
4310
4754
  totalSessionCount: allRecords.length,
4311
- topicSummary
4755
+ topicSummary,
4756
+ installedAgents,
4757
+ availableAgentCount
4312
4758
  };
4313
4759
  const systemPrompt = buildAssistantSystemPrompt(ctx);
4314
4760
  const ready = session.enqueuePrompt(systemPrompt).then(() => {
@@ -4340,15 +4786,16 @@ Agents: ${agentList}`;
4340
4786
  Agents: ${agentList}`;
4341
4787
  }
4342
4788
  function buildAssistantSystemPrompt(ctx) {
4343
- const { config, activeSessionCount, totalSessionCount, topicSummary } = ctx;
4344
- const agentNames = Object.keys(config.agents).join(", ");
4789
+ const { config, activeSessionCount, totalSessionCount, topicSummary, installedAgents, availableAgentCount } = ctx;
4790
+ const agentNames = installedAgents?.length ? installedAgents.join(", ") : Object.keys(config.agents).join(", ");
4345
4791
  const topicBreakdown = topicSummary.map((s) => `${s.status}: ${s.count}`).join(", ") || "none";
4346
4792
  return `You are the OpenACP Assistant \u2014 a helpful guide for managing AI coding sessions.
4347
4793
 
4348
4794
  ## Current State
4349
4795
  - Active sessions: ${activeSessionCount} / ${totalSessionCount} total
4350
4796
  - Topics by status: ${topicBreakdown}
4351
- - Available agents: ${agentNames}
4797
+ - Installed agents: ${agentNames}
4798
+ - Available in ACP Registry: ${availableAgentCount ?? "28+"} more agents (use /agents to browse)
4352
4799
  - Default agent: ${config.defaultAgent}
4353
4800
  - Workspace base directory: ${config.workspace.baseDir}
4354
4801
 
@@ -4356,11 +4803,22 @@ function buildAssistantSystemPrompt(ctx) {
4356
4803
 
4357
4804
  ### Create Session
4358
4805
  - The workspace is the project directory where the agent will work (read, write, execute code). It is NOT the base directory \u2014 it should be a specific project folder like \`~/code/my-project\` or \`${config.workspace.baseDir}/my-app\`.
4359
- - Ask which agent to use (if multiple are configured). Show available: ${agentNames}
4806
+ - Ask which agent to use (if multiple are installed). Show installed: ${agentNames}
4360
4807
  - Ask which project directory to use as workspace. Suggest \`${config.workspace.baseDir}\` as the base, but explain the user can provide any path.
4361
4808
  - Confirm before creating: show agent name + full workspace path.
4362
4809
  - Create via: \`openacp api new <agent> <workspace>\`
4363
4810
 
4811
+ ### Browse & Install Agents
4812
+ - Guide users to /agents command to see all available agents (installed + from ACP Registry)
4813
+ - The /agents list is paginated with install buttons \u2014 users can tap to install directly
4814
+ - For CLI users: \`openacp agents install <name>\`
4815
+ - Some agents need login/setup after install \u2014 guide users to \`openacp agents info <name>\` for setup steps
4816
+ - To run agent CLI for login: \`openacp agents run <name> -- <args>\`
4817
+ - Common setup examples:
4818
+ - Gemini: \`openacp agents run gemini -- auth login\`
4819
+ - GitHub Copilot: \`openacp agents run copilot -- auth login\`
4820
+ - Codex: needs OPENAI_API_KEY environment variable
4821
+
4364
4822
  ### Check Status / List Sessions
4365
4823
  - Run \`openacp api status\` for active sessions overview
4366
4824
  - Run \`openacp api topics\` for full list with statuses
@@ -4413,12 +4871,18 @@ openacp api delete-topic <id> --force # Force delete active
4413
4871
  openacp api cleanup # Cleanup finished topics
4414
4872
  openacp api cleanup --status finished,error
4415
4873
 
4874
+ # Agent management (user-facing CLI commands)
4875
+ openacp agents # List installed + available agents
4876
+ openacp agents install <name> # Install agent from ACP Registry
4877
+ openacp agents uninstall <name> # Remove agent
4878
+ openacp agents info <name> # Show details & setup guide
4879
+ openacp agents run <name> -- <args> # Run agent CLI (for login, etc.)
4880
+ openacp agents refresh # Force-refresh registry
4881
+
4416
4882
  # System
4417
4883
  openacp api health # System health
4418
4884
  openacp config # Edit config (interactive)
4419
4885
  openacp config set <key> <value> # Update config value
4420
- openacp api config # Show config (deprecated)
4421
- openacp api config set <key> <value> # Update config (deprecated)
4422
4886
  openacp api adapters # List adapters
4423
4887
  openacp api tunnel # Tunnel status
4424
4888
  openacp api notify "message" # Send notification
@@ -4801,7 +5265,7 @@ var TelegramSendQueue = class {
4801
5265
 
4802
5266
  // src/adapters/telegram/action-detect.ts
4803
5267
  import { nanoid as nanoid3 } from "nanoid";
4804
- import { InlineKeyboard as InlineKeyboard8 } from "grammy";
5268
+ import { InlineKeyboard as InlineKeyboard9 } from "grammy";
4805
5269
  var CMD_NEW_RE = /\/new(?:\s+([^\s\u0080-\uFFFF]+)(?:\s+([^\s\u0080-\uFFFF]+))?)?/;
4806
5270
  var CMD_CANCEL_RE = /\/cancel\b/;
4807
5271
  var KW_NEW_RE = /(?:create|new)\s+session/i;
@@ -4848,7 +5312,7 @@ function removeAction(id) {
4848
5312
  actionMap.delete(id);
4849
5313
  }
4850
5314
  function buildActionKeyboard(actionId, action) {
4851
- const keyboard = new InlineKeyboard8();
5315
+ const keyboard = new InlineKeyboard9();
4852
5316
  if (action.action === "new_session") {
4853
5317
  keyboard.text("\u2705 Create session", `a:${actionId}`);
4854
5318
  keyboard.text("\u274C Cancel", `a:dismiss:${actionId}`);
@@ -5577,7 +6041,7 @@ var TelegramAdapter = class extends ChannelAdapter {
5577
6041
  });
5578
6042
  return;
5579
6043
  }
5580
- const { getAgentCapabilities: getAgentCapabilities2 } = await import("./agent-registry-7HC6D4CH.js");
6044
+ const { getAgentCapabilities: getAgentCapabilities2 } = await import("./agent-registry-KZANAFXQ.js");
5581
6045
  const caps = getAgentCapabilities2(agentName);
5582
6046
  if (!caps.supportsResume || !caps.resumeCommand) {
5583
6047
  await ctx.reply("This agent does not support session transfer.", {
@@ -5953,4 +6417,4 @@ export {
5953
6417
  TopicManager,
5954
6418
  TelegramAdapter
5955
6419
  };
5956
- //# sourceMappingURL=chunk-66RVSUAR.js.map
6420
+ //# sourceMappingURL=chunk-PHC67OP4.js.map