@openacp/cli 2026.402.1 → 2026.402.3

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.
package/dist/cli.js CHANGED
@@ -9,6 +9,284 @@ var __export = (target, all) => {
9
9
  __defProp(target, name, { get: all[name], enumerable: true });
10
10
  };
11
11
 
12
+ // src/core/utils/log.ts
13
+ var log_exports = {};
14
+ __export(log_exports, {
15
+ cleanupOldSessionLogs: () => cleanupOldSessionLogs,
16
+ closeSessionLogger: () => closeSessionLogger,
17
+ createChildLogger: () => createChildLogger,
18
+ createSessionLogger: () => createSessionLogger,
19
+ initLogger: () => initLogger,
20
+ log: () => log,
21
+ muteLogger: () => muteLogger,
22
+ setLogLevel: () => setLogLevel,
23
+ shutdownLogger: () => shutdownLogger,
24
+ unmuteLogger: () => unmuteLogger
25
+ });
26
+ import pino from "pino";
27
+ import fs from "fs";
28
+ import path from "path";
29
+ import os from "os";
30
+ function expandHome(p2) {
31
+ return p2.startsWith("~") ? path.join(os.homedir(), p2.slice(1)) : p2;
32
+ }
33
+ function wrapVariadic(logger) {
34
+ return {
35
+ info: (...args2) => {
36
+ if (args2.length === 0) return;
37
+ if (typeof args2[0] === "object" && args2[0] !== null && !(args2[0] instanceof Error)) {
38
+ logger.info(args2[0], args2.slice(1).join(" "));
39
+ } else {
40
+ logger.info(args2.map(String).join(" "));
41
+ }
42
+ },
43
+ warn: (...args2) => {
44
+ if (args2.length === 0) return;
45
+ if (typeof args2[0] === "object" && args2[0] !== null && !(args2[0] instanceof Error)) {
46
+ logger.warn(args2[0], args2.slice(1).join(" "));
47
+ } else {
48
+ logger.warn(args2.map(String).join(" "));
49
+ }
50
+ },
51
+ error: (...args2) => {
52
+ if (args2.length === 0) return;
53
+ if (typeof args2[0] === "object" && args2[0] !== null && !(args2[0] instanceof Error)) {
54
+ logger.error(args2[0], args2.slice(1).join(" "));
55
+ } else {
56
+ logger.error(args2.map(String).join(" "));
57
+ }
58
+ },
59
+ debug: (...args2) => {
60
+ if (args2.length === 0) return;
61
+ if (typeof args2[0] === "object" && args2[0] !== null && !(args2[0] instanceof Error)) {
62
+ logger.debug(args2[0], args2.slice(1).join(" "));
63
+ } else {
64
+ logger.debug(args2.map(String).join(" "));
65
+ }
66
+ },
67
+ fatal: (...args2) => {
68
+ if (args2.length === 0) return;
69
+ if (typeof args2[0] === "object" && args2[0] !== null && !(args2[0] instanceof Error)) {
70
+ logger.fatal(args2[0], args2.slice(1).join(" "));
71
+ } else {
72
+ logger.fatal(args2.map(String).join(" "));
73
+ }
74
+ },
75
+ child: (bindings) => logger.child(bindings)
76
+ };
77
+ }
78
+ function muteLogger() {
79
+ if (muteCount === 0) {
80
+ savedLevel = rootLogger.level;
81
+ rootLogger.level = "silent";
82
+ }
83
+ muteCount++;
84
+ }
85
+ function unmuteLogger() {
86
+ muteCount--;
87
+ if (muteCount <= 0) {
88
+ muteCount = 0;
89
+ rootLogger.level = savedLevel;
90
+ }
91
+ }
92
+ function initLogger(config) {
93
+ if (initialized) return rootLogger;
94
+ const resolvedLogDir = expandHome(config.logDir);
95
+ logDir = resolvedLogDir;
96
+ try {
97
+ fs.mkdirSync(resolvedLogDir, { recursive: true });
98
+ fs.mkdirSync(path.join(resolvedLogDir, "sessions"), { recursive: true });
99
+ } catch (err) {
100
+ console.error(`[WARN] Failed to create log directory ${resolvedLogDir}, falling back to console-only:`, err);
101
+ return rootLogger;
102
+ }
103
+ const transports = pino.transport({
104
+ targets: [
105
+ {
106
+ target: "pino-pretty",
107
+ options: {
108
+ colorize: true,
109
+ translateTime: "SYS:HH:MM:ss",
110
+ ignore: "pid,hostname",
111
+ singleLine: true,
112
+ destination: 2
113
+ },
114
+ level: config.level
115
+ },
116
+ {
117
+ target: "pino-roll",
118
+ options: {
119
+ file: path.join(resolvedLogDir, "openacp.log"),
120
+ size: config.maxFileSize,
121
+ limit: { count: config.maxFiles }
122
+ },
123
+ level: config.level
124
+ }
125
+ ]
126
+ });
127
+ currentTransport = transports;
128
+ rootLogger = pino({ level: config.level }, transports);
129
+ initialized = true;
130
+ Object.assign(log, wrapVariadic(rootLogger));
131
+ return rootLogger;
132
+ }
133
+ function setLogLevel(level) {
134
+ rootLogger.level = level;
135
+ }
136
+ function createChildLogger(context) {
137
+ return new Proxy({}, {
138
+ get(_target, prop, receiver) {
139
+ const child = rootLogger.child(context);
140
+ const value = Reflect.get(child, prop, receiver);
141
+ return typeof value === "function" ? value.bind(child) : value;
142
+ }
143
+ });
144
+ }
145
+ function createSessionLogger(sessionId, parentLogger) {
146
+ const sessionLogDir = logDir ? path.join(logDir, "sessions") : void 0;
147
+ if (!sessionLogDir) {
148
+ return parentLogger.child({ sessionId });
149
+ }
150
+ try {
151
+ const sessionLogPath = path.join(sessionLogDir, `${sessionId}.log`);
152
+ const dest = pino.destination(sessionLogPath);
153
+ const sessionFileLogger = pino({ level: parentLogger.level }, dest).child({ sessionId });
154
+ const combinedChild = parentLogger.child({ sessionId });
155
+ const originalInfo = combinedChild.info.bind(combinedChild);
156
+ const originalWarn = combinedChild.warn.bind(combinedChild);
157
+ const originalError = combinedChild.error.bind(combinedChild);
158
+ const originalDebug = combinedChild.debug.bind(combinedChild);
159
+ const originalFatal = combinedChild.fatal.bind(combinedChild);
160
+ combinedChild.info = ((objOrMsg, ...rest) => {
161
+ sessionFileLogger.info(objOrMsg, ...rest);
162
+ return originalInfo(objOrMsg, ...rest);
163
+ });
164
+ combinedChild.warn = ((objOrMsg, ...rest) => {
165
+ sessionFileLogger.warn(objOrMsg, ...rest);
166
+ return originalWarn(objOrMsg, ...rest);
167
+ });
168
+ combinedChild.error = ((objOrMsg, ...rest) => {
169
+ sessionFileLogger.error(objOrMsg, ...rest);
170
+ return originalError(objOrMsg, ...rest);
171
+ });
172
+ combinedChild.debug = ((objOrMsg, ...rest) => {
173
+ sessionFileLogger.debug(objOrMsg, ...rest);
174
+ return originalDebug(objOrMsg, ...rest);
175
+ });
176
+ combinedChild.fatal = ((objOrMsg, ...rest) => {
177
+ sessionFileLogger.fatal(objOrMsg, ...rest);
178
+ return originalFatal(objOrMsg, ...rest);
179
+ });
180
+ combinedChild.__sessionDest = dest;
181
+ return combinedChild;
182
+ } catch (err) {
183
+ parentLogger.warn({ sessionId, err }, "Failed to create session log file, using combined log only");
184
+ return parentLogger.child({ sessionId });
185
+ }
186
+ }
187
+ function closeSessionLogger(logger) {
188
+ const dest = logger.__sessionDest;
189
+ if (dest && typeof dest.destroy === "function") {
190
+ dest.destroy();
191
+ }
192
+ }
193
+ async function shutdownLogger() {
194
+ if (!initialized) return;
195
+ const transport = currentTransport;
196
+ rootLogger = pino({ level: "debug" });
197
+ Object.assign(log, wrapVariadic(rootLogger));
198
+ currentTransport = void 0;
199
+ logDir = void 0;
200
+ initialized = false;
201
+ if (transport) {
202
+ await new Promise((resolve8) => {
203
+ const timeout = setTimeout(resolve8, 3e3);
204
+ transport.on("close", () => {
205
+ clearTimeout(timeout);
206
+ resolve8();
207
+ });
208
+ transport.end();
209
+ });
210
+ }
211
+ }
212
+ async function cleanupOldSessionLogs(retentionDays) {
213
+ if (!logDir) return;
214
+ const sessionsDir = path.join(logDir, "sessions");
215
+ try {
216
+ const files = await fs.promises.readdir(sessionsDir);
217
+ const cutoff = Date.now() - retentionDays * 24 * 60 * 60 * 1e3;
218
+ for (const file of files) {
219
+ try {
220
+ const filePath = path.join(sessionsDir, file);
221
+ const stat = await fs.promises.stat(filePath);
222
+ if (stat.mtimeMs < cutoff) {
223
+ await fs.promises.unlink(filePath);
224
+ rootLogger.debug({ file }, "Deleted old session log");
225
+ }
226
+ } catch (err) {
227
+ rootLogger.warn({ file, err }, "Failed to delete old session log");
228
+ }
229
+ }
230
+ } catch {
231
+ }
232
+ }
233
+ var rootLogger, initialized, logDir, currentTransport, log, muteCount, savedLevel;
234
+ var init_log = __esm({
235
+ "src/core/utils/log.ts"() {
236
+ "use strict";
237
+ rootLogger = pino({
238
+ level: "debug",
239
+ transport: { target: "pino-pretty", options: { colorize: true, translateTime: "SYS:standard", destination: 2 } }
240
+ });
241
+ initialized = false;
242
+ log = wrapVariadic(rootLogger);
243
+ muteCount = 0;
244
+ savedLevel = "info";
245
+ }
246
+ });
247
+
248
+ // src/cli/output.ts
249
+ function isJsonMode(args2) {
250
+ return args2.includes("--json");
251
+ }
252
+ function jsonSuccess(data) {
253
+ console.log(JSON.stringify({ success: true, data }));
254
+ process.exit(0);
255
+ }
256
+ function jsonError(code, message) {
257
+ console.log(JSON.stringify({ success: false, error: { code, message } }));
258
+ process.exit(1);
259
+ }
260
+ async function muteForJson() {
261
+ try {
262
+ const { muteLogger: muteLogger2 } = await Promise.resolve().then(() => (init_log(), log_exports));
263
+ muteLogger2();
264
+ } catch {
265
+ }
266
+ }
267
+ var ErrorCodes;
268
+ var init_output = __esm({
269
+ "src/cli/output.ts"() {
270
+ "use strict";
271
+ ErrorCodes = {
272
+ DAEMON_NOT_RUNNING: "DAEMON_NOT_RUNNING",
273
+ INSTANCE_NOT_FOUND: "INSTANCE_NOT_FOUND",
274
+ PLUGIN_NOT_FOUND: "PLUGIN_NOT_FOUND",
275
+ AGENT_NOT_FOUND: "AGENT_NOT_FOUND",
276
+ CONFIG_INVALID: "CONFIG_INVALID",
277
+ CONFIG_NOT_FOUND: "CONFIG_NOT_FOUND",
278
+ SETUP_FAILED: "SETUP_FAILED",
279
+ API_ERROR: "API_ERROR",
280
+ TUNNEL_ERROR: "TUNNEL_ERROR",
281
+ INSTALL_FAILED: "INSTALL_FAILED",
282
+ UNINSTALL_FAILED: "UNINSTALL_FAILED",
283
+ MISSING_ARGUMENT: "MISSING_ARGUMENT",
284
+ UNKNOWN_COMMAND: "UNKNOWN_COMMAND",
285
+ UNKNOWN_ERROR: "UNKNOWN_ERROR"
286
+ };
287
+ }
288
+ });
289
+
12
290
  // src/cli/version.ts
13
291
  var version_exports = {};
14
292
  __export(version_exports, {
@@ -139,8 +417,8 @@ var plugin_registry_exports = {};
139
417
  __export(plugin_registry_exports, {
140
418
  PluginRegistry: () => PluginRegistry
141
419
  });
142
- import fs3 from "fs";
143
- import path3 from "path";
420
+ import fs4 from "fs";
421
+ import path4 from "path";
144
422
  var PluginRegistry;
145
423
  var init_plugin_registry = __esm({
146
424
  "src/core/plugin/plugin-registry.ts"() {
@@ -183,7 +461,7 @@ var init_plugin_registry = __esm({
183
461
  }
184
462
  async load() {
185
463
  try {
186
- const content = fs3.readFileSync(this.registryPath, "utf-8");
464
+ const content = fs4.readFileSync(this.registryPath, "utf-8");
187
465
  const parsed = JSON.parse(content);
188
466
  if (parsed && typeof parsed.installed === "object") {
189
467
  this.data = parsed;
@@ -193,9 +471,9 @@ var init_plugin_registry = __esm({
193
471
  }
194
472
  }
195
473
  async save() {
196
- const dir = path3.dirname(this.registryPath);
197
- fs3.mkdirSync(dir, { recursive: true });
198
- fs3.writeFileSync(this.registryPath, JSON.stringify(this.data, null, 2));
474
+ const dir = path4.dirname(this.registryPath);
475
+ fs4.mkdirSync(dir, { recursive: true });
476
+ fs4.writeFileSync(this.registryPath, JSON.stringify(this.data, null, 2));
199
477
  }
200
478
  };
201
479
  }
@@ -254,14 +532,31 @@ __export(plugin_search_exports, {
254
532
  cmdPluginSearch: () => cmdPluginSearch
255
533
  });
256
534
  async function cmdPluginSearch(args2) {
257
- const query = args2.join(" ").trim();
535
+ const json = isJsonMode(args2);
536
+ if (json) await muteForJson();
537
+ const query = args2.filter((a) => a !== "--json").join(" ").trim();
258
538
  if (!query) {
539
+ if (json) jsonError(ErrorCodes.MISSING_ARGUMENT, "Search query is required");
259
540
  console.error("Usage: openacp plugin search <query>");
260
541
  process.exit(1);
261
542
  }
262
543
  const client = new RegistryClient();
263
544
  try {
264
545
  const results = await client.search(query);
546
+ if (json) {
547
+ jsonSuccess({
548
+ results: results.map((p2) => ({
549
+ name: p2.name,
550
+ displayName: p2.displayName ?? p2.name,
551
+ version: p2.version,
552
+ description: p2.description,
553
+ npm: p2.npm,
554
+ category: p2.category,
555
+ verified: p2.verified ?? false,
556
+ featured: p2.featured ?? false
557
+ }))
558
+ });
559
+ }
265
560
  if (results.length === 0) {
266
561
  console.log(`No plugins found matching "${query}"`);
267
562
  return;
@@ -279,6 +574,7 @@ Found ${results.length} plugin${results.length > 1 ? "s" : ""} matching "${query
279
574
  console.log();
280
575
  }
281
576
  } catch (err) {
577
+ if (json) jsonError(ErrorCodes.API_ERROR, `Failed to search registry: ${err}`);
282
578
  console.error(`Failed to search registry: ${err}`);
283
579
  process.exit(1);
284
580
  }
@@ -287,6 +583,7 @@ var init_plugin_search = __esm({
287
583
  "src/cli/commands/plugin-search.ts"() {
288
584
  "use strict";
289
585
  init_registry_client();
586
+ init_output();
290
587
  }
291
588
  });
292
589
 
@@ -1234,8 +1531,8 @@ __export(plugin_create_exports, {
1234
1531
  cmdPluginCreate: () => cmdPluginCreate
1235
1532
  });
1236
1533
  import * as p from "@clack/prompts";
1237
- import fs4 from "fs";
1238
- import path4 from "path";
1534
+ import fs5 from "fs";
1535
+ import path5 from "path";
1239
1536
  async function cmdPluginCreate() {
1240
1537
  p.intro("Create a new OpenACP plugin");
1241
1538
  const result = await p.group(
@@ -1278,14 +1575,14 @@ async function cmdPluginCreate() {
1278
1575
  );
1279
1576
  const pluginName = result.name.trim();
1280
1577
  const dirName = pluginName.replace(/^@[^/]+\//, "");
1281
- const targetDir = path4.resolve(process.cwd(), dirName);
1282
- if (fs4.existsSync(targetDir)) {
1578
+ const targetDir = path5.resolve(process.cwd(), dirName);
1579
+ if (fs5.existsSync(targetDir)) {
1283
1580
  p.cancel(`Directory "${dirName}" already exists.`);
1284
1581
  process.exit(1);
1285
1582
  }
1286
1583
  const spinner4 = p.spinner();
1287
1584
  spinner4.start("Scaffolding plugin...");
1288
- fs4.mkdirSync(path4.join(targetDir, "src", "__tests__"), { recursive: true });
1585
+ fs5.mkdirSync(path5.join(targetDir, "src", "__tests__"), { recursive: true });
1289
1586
  const params = {
1290
1587
  pluginName,
1291
1588
  description: result.description || "",
@@ -1302,11 +1599,11 @@ async function cmdPluginCreate() {
1302
1599
  { relativePath: "README.md", content: generateReadme(params) },
1303
1600
  { relativePath: "CLAUDE.md", content: generateClaudeMd(params) },
1304
1601
  { relativePath: "PLUGIN_GUIDE.md", content: generatePluginGuide(params) },
1305
- { relativePath: path4.join("src", "index.ts"), content: generatePluginSource(params) },
1306
- { relativePath: path4.join("src", "__tests__", "index.test.ts"), content: generatePluginTest(params) }
1602
+ { relativePath: path5.join("src", "index.ts"), content: generatePluginSource(params) },
1603
+ { relativePath: path5.join("src", "__tests__", "index.test.ts"), content: generatePluginTest(params) }
1307
1604
  ];
1308
1605
  for (const file of files) {
1309
- fs4.writeFileSync(path4.join(targetDir, file.relativePath), file.content);
1606
+ fs5.writeFileSync(path5.join(targetDir, file.relativePath), file.content);
1310
1607
  }
1311
1608
  spinner4.stop("Plugin scaffolded!");
1312
1609
  p.note(
@@ -1478,9 +1775,9 @@ var read_text_file_exports = {};
1478
1775
  __export(read_text_file_exports, {
1479
1776
  readTextFileWithRange: () => readTextFileWithRange
1480
1777
  });
1481
- import fs5 from "fs";
1778
+ import fs6 from "fs";
1482
1779
  async function readTextFileWithRange(filePath, options) {
1483
- const content = await fs5.promises.readFile(filePath, "utf-8");
1780
+ const content = await fs6.promises.readFile(filePath, "utf-8");
1484
1781
  if (!options?.line && !options?.limit) return content;
1485
1782
  const lines = content.split("\n");
1486
1783
  const start = Math.max(0, (options.line ?? 1) - 1);
@@ -1494,8 +1791,8 @@ var init_read_text_file = __esm({
1494
1791
  });
1495
1792
 
1496
1793
  // src/plugins/file-service/file-service.ts
1497
- import fs6 from "fs";
1498
- import path5 from "path";
1794
+ import fs7 from "fs";
1795
+ import path6 from "path";
1499
1796
  import { OggOpusDecoder } from "ogg-opus-decoder";
1500
1797
  import wav from "node-wav";
1501
1798
  function classifyMime(mimeType) {
@@ -1551,14 +1848,14 @@ var init_file_service = __esm({
1551
1848
  const cutoff = Date.now() - maxAgeDays * 24 * 60 * 60 * 1e3;
1552
1849
  let removed = 0;
1553
1850
  try {
1554
- const entries = await fs6.promises.readdir(this.baseDir, { withFileTypes: true });
1851
+ const entries = await fs7.promises.readdir(this.baseDir, { withFileTypes: true });
1555
1852
  for (const entry of entries) {
1556
1853
  if (!entry.isDirectory()) continue;
1557
- const dirPath = path5.join(this.baseDir, entry.name);
1854
+ const dirPath = path6.join(this.baseDir, entry.name);
1558
1855
  try {
1559
- const stat = await fs6.promises.stat(dirPath);
1856
+ const stat = await fs7.promises.stat(dirPath);
1560
1857
  if (stat.mtimeMs < cutoff) {
1561
- await fs6.promises.rm(dirPath, { recursive: true, force: true });
1858
+ await fs7.promises.rm(dirPath, { recursive: true, force: true });
1562
1859
  removed++;
1563
1860
  }
1564
1861
  } catch {
@@ -1569,11 +1866,11 @@ var init_file_service = __esm({
1569
1866
  return removed;
1570
1867
  }
1571
1868
  async saveFile(sessionId, fileName, data, mimeType) {
1572
- const sessionDir = path5.join(this.baseDir, sessionId);
1573
- await fs6.promises.mkdir(sessionDir, { recursive: true });
1869
+ const sessionDir = path6.join(this.baseDir, sessionId);
1870
+ await fs7.promises.mkdir(sessionDir, { recursive: true });
1574
1871
  const safeName = `${Date.now()}-${fileName.replace(/[^a-zA-Z0-9._-]/g, "_")}`;
1575
- const filePath = path5.join(sessionDir, safeName);
1576
- await fs6.promises.writeFile(filePath, data);
1872
+ const filePath = path6.join(sessionDir, safeName);
1873
+ await fs7.promises.writeFile(filePath, data);
1577
1874
  return {
1578
1875
  type: classifyMime(mimeType),
1579
1876
  filePath,
@@ -1584,14 +1881,14 @@ var init_file_service = __esm({
1584
1881
  }
1585
1882
  async resolveFile(filePath) {
1586
1883
  try {
1587
- const stat = await fs6.promises.stat(filePath);
1884
+ const stat = await fs7.promises.stat(filePath);
1588
1885
  if (!stat.isFile()) return null;
1589
- const ext = path5.extname(filePath).toLowerCase();
1886
+ const ext = path6.extname(filePath).toLowerCase();
1590
1887
  const mimeType = EXT_TO_MIME[ext] || "application/octet-stream";
1591
1888
  return {
1592
1889
  type: classifyMime(mimeType),
1593
1890
  filePath,
1594
- fileName: path5.basename(filePath),
1891
+ fileName: path6.basename(filePath),
1595
1892
  mimeType,
1596
1893
  size: stat.size
1597
1894
  };
@@ -1638,8 +1935,8 @@ var file_service_exports = {};
1638
1935
  __export(file_service_exports, {
1639
1936
  default: () => file_service_default
1640
1937
  });
1641
- import path6 from "path";
1642
- import os3 from "os";
1938
+ import path7 from "path";
1939
+ import os4 from "os";
1643
1940
  function createFileServicePlugin() {
1644
1941
  return {
1645
1942
  name: "@openacp/file-service",
@@ -1649,7 +1946,7 @@ function createFileServicePlugin() {
1649
1946
  permissions: ["services:register"],
1650
1947
  async install(ctx) {
1651
1948
  const { settings, legacyConfig, terminal } = ctx;
1652
- const defaultFilesDir = path6.join(ctx.instanceRoot ?? path6.join(os3.homedir(), ".openacp"), "files");
1949
+ const defaultFilesDir = path7.join(ctx.instanceRoot ?? path7.join(os4.homedir(), ".openacp"), "files");
1653
1950
  if (legacyConfig) {
1654
1951
  const filesCfg = legacyConfig.files;
1655
1952
  if (filesCfg) {
@@ -1668,7 +1965,7 @@ function createFileServicePlugin() {
1668
1965
  async configure(ctx) {
1669
1966
  const { terminal, settings } = ctx;
1670
1967
  const current = await settings.getAll();
1671
- const defaultFilesDir = path6.join(ctx.instanceRoot ?? path6.join(os3.homedir(), ".openacp"), "files");
1968
+ const defaultFilesDir = path7.join(ctx.instanceRoot ?? path7.join(os4.homedir(), ".openacp"), "files");
1672
1969
  const val = await terminal.text({
1673
1970
  message: "File storage directory:",
1674
1971
  defaultValue: current.baseDir ?? defaultFilesDir
@@ -1684,7 +1981,7 @@ function createFileServicePlugin() {
1684
1981
  },
1685
1982
  async setup(ctx) {
1686
1983
  const config = ctx.pluginConfig;
1687
- const baseDir = config.baseDir ?? path6.join(ctx.instanceRoot, "files");
1984
+ const baseDir = config.baseDir ?? path7.join(ctx.instanceRoot, "files");
1688
1985
  const retentionDays = config.retentionDays ?? 30;
1689
1986
  const service = new FileService(baseDir);
1690
1987
  ctx.registerService("file-service", service);
@@ -1706,8 +2003,8 @@ var init_file_service2 = __esm({
1706
2003
  });
1707
2004
 
1708
2005
  // src/plugins/context/context-cache.ts
1709
- import * as fs7 from "fs";
1710
- import * as path7 from "path";
2006
+ import * as fs8 from "fs";
2007
+ import * as path8 from "path";
1711
2008
  import * as crypto from "crypto";
1712
2009
  var DEFAULT_TTL_MS, ContextCache;
1713
2010
  var init_context_cache = __esm({
@@ -1718,37 +2015,37 @@ var init_context_cache = __esm({
1718
2015
  constructor(cacheDir, ttlMs = DEFAULT_TTL_MS) {
1719
2016
  this.cacheDir = cacheDir;
1720
2017
  this.ttlMs = ttlMs;
1721
- fs7.mkdirSync(cacheDir, { recursive: true });
2018
+ fs8.mkdirSync(cacheDir, { recursive: true });
1722
2019
  }
1723
2020
  keyHash(repoPath, queryKey) {
1724
2021
  return crypto.createHash("sha256").update(`${repoPath}:${queryKey}`).digest("hex").slice(0, 16);
1725
2022
  }
1726
2023
  filePath(repoPath, queryKey) {
1727
- return path7.join(this.cacheDir, `${this.keyHash(repoPath, queryKey)}.json`);
2024
+ return path8.join(this.cacheDir, `${this.keyHash(repoPath, queryKey)}.json`);
1728
2025
  }
1729
2026
  get(repoPath, queryKey) {
1730
2027
  const fp = this.filePath(repoPath, queryKey);
1731
2028
  try {
1732
- const stat = fs7.statSync(fp);
2029
+ const stat = fs8.statSync(fp);
1733
2030
  if (Date.now() - stat.mtimeMs > this.ttlMs) {
1734
- fs7.unlinkSync(fp);
2031
+ fs8.unlinkSync(fp);
1735
2032
  return null;
1736
2033
  }
1737
- return JSON.parse(fs7.readFileSync(fp, "utf-8"));
2034
+ return JSON.parse(fs8.readFileSync(fp, "utf-8"));
1738
2035
  } catch {
1739
2036
  return null;
1740
2037
  }
1741
2038
  }
1742
2039
  set(repoPath, queryKey, result) {
1743
- fs7.writeFileSync(this.filePath(repoPath, queryKey), JSON.stringify(result));
2040
+ fs8.writeFileSync(this.filePath(repoPath, queryKey), JSON.stringify(result));
1744
2041
  }
1745
2042
  };
1746
2043
  }
1747
2044
  });
1748
2045
 
1749
2046
  // src/plugins/context/context-manager.ts
1750
- import * as os4 from "os";
1751
- import * as path8 from "path";
2047
+ import * as os5 from "os";
2048
+ import * as path9 from "path";
1752
2049
  var ContextManager;
1753
2050
  var init_context_manager = __esm({
1754
2051
  "src/plugins/context/context-manager.ts"() {
@@ -1759,7 +2056,7 @@ var init_context_manager = __esm({
1759
2056
  cache;
1760
2057
  historyStore;
1761
2058
  constructor(cachePath) {
1762
- this.cache = new ContextCache(cachePath ?? path8.join(os4.homedir(), ".openacp", "cache", "entire"));
2059
+ this.cache = new ContextCache(cachePath ?? path9.join(os5.homedir(), ".openacp", "cache", "entire"));
1763
2060
  }
1764
2061
  setHistoryStore(store) {
1765
2062
  this.historyStore = store;
@@ -3189,8 +3486,8 @@ var init_history_recorder = __esm({
3189
3486
  });
3190
3487
 
3191
3488
  // src/plugins/context/history/history-store.ts
3192
- import fs8 from "fs";
3193
- import path9 from "path";
3489
+ import fs9 from "fs";
3490
+ import path10 from "path";
3194
3491
  var HistoryStore;
3195
3492
  var init_history_store = __esm({
3196
3493
  "src/plugins/context/history/history-store.ts"() {
@@ -3200,14 +3497,14 @@ var init_history_store = __esm({
3200
3497
  this.dir = dir;
3201
3498
  }
3202
3499
  async write(history) {
3203
- await fs8.promises.mkdir(this.dir, { recursive: true });
3500
+ await fs9.promises.mkdir(this.dir, { recursive: true });
3204
3501
  const filePath = this.filePath(history.sessionId);
3205
- await fs8.promises.writeFile(filePath, JSON.stringify(history, null, 2));
3502
+ await fs9.promises.writeFile(filePath, JSON.stringify(history, null, 2));
3206
3503
  }
3207
3504
  async read(sessionId) {
3208
3505
  const filePath = this.filePath(sessionId);
3209
3506
  try {
3210
- const raw = await fs8.promises.readFile(filePath, "utf-8");
3507
+ const raw = await fs9.promises.readFile(filePath, "utf-8");
3211
3508
  return JSON.parse(raw);
3212
3509
  } catch {
3213
3510
  return null;
@@ -3215,7 +3512,7 @@ var init_history_store = __esm({
3215
3512
  }
3216
3513
  async exists(sessionId) {
3217
3514
  try {
3218
- await fs8.promises.access(this.filePath(sessionId));
3515
+ await fs9.promises.access(this.filePath(sessionId));
3219
3516
  return true;
3220
3517
  } catch {
3221
3518
  return false;
@@ -3223,7 +3520,7 @@ var init_history_store = __esm({
3223
3520
  }
3224
3521
  async list() {
3225
3522
  try {
3226
- const files = await fs8.promises.readdir(this.dir);
3523
+ const files = await fs9.promises.readdir(this.dir);
3227
3524
  return files.filter((f) => f.endsWith(".json")).map((f) => f.replace(/\.json$/, ""));
3228
3525
  } catch {
3229
3526
  return [];
@@ -3231,13 +3528,13 @@ var init_history_store = __esm({
3231
3528
  }
3232
3529
  async delete(sessionId) {
3233
3530
  try {
3234
- await fs8.promises.unlink(this.filePath(sessionId));
3531
+ await fs9.promises.unlink(this.filePath(sessionId));
3235
3532
  } catch {
3236
3533
  }
3237
3534
  }
3238
3535
  filePath(sessionId) {
3239
- const basename2 = path9.basename(sessionId);
3240
- const resolved = path9.join(this.dir, `${basename2}.json`);
3536
+ const basename2 = path10.basename(sessionId);
3537
+ const resolved = path10.join(this.dir, `${basename2}.json`);
3241
3538
  if (!resolved.startsWith(this.dir)) {
3242
3539
  throw new Error(`Invalid session ID: ${sessionId}`);
3243
3540
  }
@@ -3252,7 +3549,7 @@ var context_exports = {};
3252
3549
  __export(context_exports, {
3253
3550
  default: () => context_default
3254
3551
  });
3255
- import * as path10 from "path";
3552
+ import * as path11 from "path";
3256
3553
  var contextPlugin, context_default;
3257
3554
  var init_context = __esm({
3258
3555
  "src/plugins/context/index.ts"() {
@@ -3293,12 +3590,12 @@ var init_context = __esm({
3293
3590
  }
3294
3591
  },
3295
3592
  async setup(ctx) {
3296
- const historyDir = path10.join(ctx.instanceRoot, "history");
3593
+ const historyDir = path11.join(ctx.instanceRoot, "history");
3297
3594
  const store = new HistoryStore(historyDir);
3298
3595
  const recorder = new HistoryRecorder(store);
3299
3596
  const sessionManager = ctx.sessions;
3300
3597
  const getRecords = () => sessionManager.listRecords();
3301
- const cachePath = path10.join(ctx.instanceRoot, "cache", "entire");
3598
+ const cachePath = path11.join(ctx.instanceRoot, "cache", "entire");
3302
3599
  const manager = new ContextManager(cachePath);
3303
3600
  manager.register(new HistoryProvider(store, getRecords));
3304
3601
  manager.register(new EntireProvider());
@@ -3499,16 +3796,16 @@ __export(plugin_installer_exports, {
3499
3796
  });
3500
3797
  import { exec } from "child_process";
3501
3798
  import { promisify } from "util";
3502
- import * as fs9 from "fs/promises";
3503
- import * as os5 from "os";
3504
- import * as path11 from "path";
3799
+ import * as fs10 from "fs/promises";
3800
+ import * as os6 from "os";
3801
+ import * as path12 from "path";
3505
3802
  import { pathToFileURL } from "url";
3506
3803
  async function importFromDir(packageName, dir) {
3507
- const pkgDir = path11.join(dir, "node_modules", ...packageName.split("/"));
3508
- const pkgJsonPath = path11.join(pkgDir, "package.json");
3804
+ const pkgDir = path12.join(dir, "node_modules", ...packageName.split("/"));
3805
+ const pkgJsonPath = path12.join(pkgDir, "package.json");
3509
3806
  let pkgJson;
3510
3807
  try {
3511
- pkgJson = JSON.parse(await fs9.readFile(pkgJsonPath, "utf-8"));
3808
+ pkgJson = JSON.parse(await fs10.readFile(pkgJsonPath, "utf-8"));
3512
3809
  } catch (err) {
3513
3810
  throw new Error(`Cannot read package.json for "${packageName}" at ${pkgJsonPath}: ${err.message}`);
3514
3811
  }
@@ -3521,9 +3818,9 @@ async function importFromDir(packageName, dir) {
3521
3818
  } else {
3522
3819
  entry = pkgJson.main ?? "index.js";
3523
3820
  }
3524
- const entryPath = path11.join(pkgDir, entry);
3821
+ const entryPath = path12.join(pkgDir, entry);
3525
3822
  try {
3526
- await fs9.access(entryPath);
3823
+ await fs10.access(entryPath);
3527
3824
  } catch {
3528
3825
  throw new Error(`Entry point "${entry}" not found for "${packageName}" at ${entryPath}`);
3529
3826
  }
@@ -3533,7 +3830,7 @@ async function installNpmPlugin(packageName, pluginsDir) {
3533
3830
  if (!VALID_NPM_NAME.test(packageName)) {
3534
3831
  throw new Error(`Invalid package name: "${packageName}". Must be a valid npm package name.`);
3535
3832
  }
3536
- const dir = pluginsDir ?? path11.join(os5.homedir(), ".openacp", "plugins");
3833
+ const dir = pluginsDir ?? path12.join(os6.homedir(), ".openacp", "plugins");
3537
3834
  try {
3538
3835
  return await importFromDir(packageName, dir);
3539
3836
  } catch {
@@ -3557,7 +3854,7 @@ var speech_exports = {};
3557
3854
  __export(speech_exports, {
3558
3855
  default: () => speech_default
3559
3856
  });
3560
- import path12 from "path";
3857
+ import path13 from "path";
3561
3858
  var EDGE_TTS_PLUGIN, speechPlugin, speech_default;
3562
3859
  var init_speech = __esm({
3563
3860
  "src/plugins/speech/index.ts"() {
@@ -3575,7 +3872,7 @@ var init_speech = __esm({
3575
3872
  inheritableKeys: ["tts"],
3576
3873
  async install(ctx) {
3577
3874
  const { terminal, settings, legacyConfig } = ctx;
3578
- const pluginsDir = ctx.instanceRoot ? path12.join(ctx.instanceRoot, "plugins") : void 0;
3875
+ const pluginsDir = ctx.instanceRoot ? path13.join(ctx.instanceRoot, "plugins") : void 0;
3579
3876
  if (legacyConfig) {
3580
3877
  const speechCfg = legacyConfig.speech;
3581
3878
  if (speechCfg) {
@@ -3678,7 +3975,7 @@ var init_speech = __esm({
3678
3975
  }
3679
3976
  },
3680
3977
  async setup(ctx) {
3681
- const pluginsDir = ctx.instanceRoot ? path12.join(ctx.instanceRoot, "plugins") : void 0;
3978
+ const pluginsDir = ctx.instanceRoot ? path13.join(ctx.instanceRoot, "plugins") : void 0;
3682
3979
  const config = ctx.pluginConfig;
3683
3980
  const groqApiKey = config.groqApiKey;
3684
3981
  const sttProvider = groqApiKey ? "groq" : null;
@@ -3805,301 +4102,65 @@ var init_notification = __esm({
3805
4102
  for (const adapter of this.adapters.values()) {
3806
4103
  try {
3807
4104
  await adapter.sendNotification(notification);
3808
- } catch {
3809
- }
3810
- }
3811
- }
3812
- };
3813
- }
3814
- });
3815
-
3816
- // src/plugins/notifications/index.ts
3817
- var notifications_exports = {};
3818
- __export(notifications_exports, {
3819
- default: () => notifications_default
3820
- });
3821
- function createNotificationsPlugin() {
3822
- return {
3823
- name: "@openacp/notifications",
3824
- version: "1.0.0",
3825
- description: "Cross-session notification routing",
3826
- essential: false,
3827
- pluginDependencies: { "@openacp/security": "^1.0.0" },
3828
- permissions: ["services:register", "kernel:access"],
3829
- async install(ctx) {
3830
- const { settings, terminal } = ctx;
3831
- await settings.setAll({ enabled: true });
3832
- terminal.log.success("Notifications defaults saved");
3833
- },
3834
- async configure(ctx) {
3835
- const { terminal, settings } = ctx;
3836
- const current = await settings.getAll();
3837
- const toggle = await terminal.confirm({
3838
- message: `Notifications are ${current.enabled !== false ? "enabled" : "disabled"}. Toggle?`,
3839
- initialValue: false
3840
- });
3841
- if (toggle) {
3842
- const newState = current.enabled === false;
3843
- await settings.set("enabled", newState);
3844
- terminal.log.success(`Notifications ${newState ? "enabled" : "disabled"}`);
3845
- }
3846
- },
3847
- async uninstall(ctx, opts) {
3848
- if (opts.purge) {
3849
- await ctx.settings.clear();
3850
- ctx.terminal.log.success("Notifications settings cleared");
3851
- }
3852
- },
3853
- async setup(ctx) {
3854
- const core = ctx.core;
3855
- const manager = new NotificationManager(core.adapters);
3856
- ctx.registerService("notifications", manager);
3857
- ctx.log.info("Notifications service ready");
3858
- }
3859
- };
3860
- }
3861
- var notifications_default;
3862
- var init_notifications = __esm({
3863
- "src/plugins/notifications/index.ts"() {
3864
- "use strict";
3865
- init_notification();
3866
- notifications_default = createNotificationsPlugin();
3867
- }
3868
- });
3869
-
3870
- // src/core/utils/log.ts
3871
- var log_exports = {};
3872
- __export(log_exports, {
3873
- cleanupOldSessionLogs: () => cleanupOldSessionLogs,
3874
- closeSessionLogger: () => closeSessionLogger,
3875
- createChildLogger: () => createChildLogger,
3876
- createSessionLogger: () => createSessionLogger,
3877
- initLogger: () => initLogger,
3878
- log: () => log,
3879
- muteLogger: () => muteLogger,
3880
- setLogLevel: () => setLogLevel,
3881
- shutdownLogger: () => shutdownLogger,
3882
- unmuteLogger: () => unmuteLogger
3883
- });
3884
- import pino from "pino";
3885
- import fs10 from "fs";
3886
- import path13 from "path";
3887
- import os6 from "os";
3888
- function expandHome(p2) {
3889
- return p2.startsWith("~") ? path13.join(os6.homedir(), p2.slice(1)) : p2;
3890
- }
3891
- function wrapVariadic(logger) {
3892
- return {
3893
- info: (...args2) => {
3894
- if (args2.length === 0) return;
3895
- if (typeof args2[0] === "object" && args2[0] !== null && !(args2[0] instanceof Error)) {
3896
- logger.info(args2[0], args2.slice(1).join(" "));
3897
- } else {
3898
- logger.info(args2.map(String).join(" "));
3899
- }
3900
- },
3901
- warn: (...args2) => {
3902
- if (args2.length === 0) return;
3903
- if (typeof args2[0] === "object" && args2[0] !== null && !(args2[0] instanceof Error)) {
3904
- logger.warn(args2[0], args2.slice(1).join(" "));
3905
- } else {
3906
- logger.warn(args2.map(String).join(" "));
3907
- }
3908
- },
3909
- error: (...args2) => {
3910
- if (args2.length === 0) return;
3911
- if (typeof args2[0] === "object" && args2[0] !== null && !(args2[0] instanceof Error)) {
3912
- logger.error(args2[0], args2.slice(1).join(" "));
3913
- } else {
3914
- logger.error(args2.map(String).join(" "));
3915
- }
3916
- },
3917
- debug: (...args2) => {
3918
- if (args2.length === 0) return;
3919
- if (typeof args2[0] === "object" && args2[0] !== null && !(args2[0] instanceof Error)) {
3920
- logger.debug(args2[0], args2.slice(1).join(" "));
3921
- } else {
3922
- logger.debug(args2.map(String).join(" "));
3923
- }
3924
- },
3925
- fatal: (...args2) => {
3926
- if (args2.length === 0) return;
3927
- if (typeof args2[0] === "object" && args2[0] !== null && !(args2[0] instanceof Error)) {
3928
- logger.fatal(args2[0], args2.slice(1).join(" "));
3929
- } else {
3930
- logger.fatal(args2.map(String).join(" "));
3931
- }
3932
- },
3933
- child: (bindings) => logger.child(bindings)
3934
- };
3935
- }
3936
- function muteLogger() {
3937
- if (muteCount === 0) {
3938
- savedLevel = rootLogger.level;
3939
- rootLogger.level = "silent";
3940
- }
3941
- muteCount++;
3942
- }
3943
- function unmuteLogger() {
3944
- muteCount--;
3945
- if (muteCount <= 0) {
3946
- muteCount = 0;
3947
- rootLogger.level = savedLevel;
3948
- }
3949
- }
3950
- function initLogger(config) {
3951
- if (initialized) return rootLogger;
3952
- const resolvedLogDir = expandHome(config.logDir);
3953
- logDir = resolvedLogDir;
3954
- try {
3955
- fs10.mkdirSync(resolvedLogDir, { recursive: true });
3956
- fs10.mkdirSync(path13.join(resolvedLogDir, "sessions"), { recursive: true });
3957
- } catch (err) {
3958
- console.error(`[WARN] Failed to create log directory ${resolvedLogDir}, falling back to console-only:`, err);
3959
- return rootLogger;
3960
- }
3961
- const transports = pino.transport({
3962
- targets: [
3963
- {
3964
- target: "pino-pretty",
3965
- options: {
3966
- colorize: true,
3967
- translateTime: "SYS:HH:MM:ss",
3968
- ignore: "pid,hostname",
3969
- singleLine: true,
3970
- destination: 2
3971
- },
3972
- level: config.level
3973
- },
3974
- {
3975
- target: "pino-roll",
3976
- options: {
3977
- file: path13.join(resolvedLogDir, "openacp.log"),
3978
- size: config.maxFileSize,
3979
- limit: { count: config.maxFiles }
3980
- },
3981
- level: config.level
3982
- }
3983
- ]
3984
- });
3985
- currentTransport = transports;
3986
- rootLogger = pino({ level: config.level }, transports);
3987
- initialized = true;
3988
- Object.assign(log, wrapVariadic(rootLogger));
3989
- return rootLogger;
3990
- }
3991
- function setLogLevel(level) {
3992
- rootLogger.level = level;
3993
- }
3994
- function createChildLogger(context) {
3995
- return new Proxy({}, {
3996
- get(_target, prop, receiver) {
3997
- const child = rootLogger.child(context);
3998
- const value = Reflect.get(child, prop, receiver);
3999
- return typeof value === "function" ? value.bind(child) : value;
4000
- }
4001
- });
4002
- }
4003
- function createSessionLogger(sessionId, parentLogger) {
4004
- const sessionLogDir = logDir ? path13.join(logDir, "sessions") : void 0;
4005
- if (!sessionLogDir) {
4006
- return parentLogger.child({ sessionId });
4007
- }
4008
- try {
4009
- const sessionLogPath = path13.join(sessionLogDir, `${sessionId}.log`);
4010
- const dest = pino.destination(sessionLogPath);
4011
- const sessionFileLogger = pino({ level: parentLogger.level }, dest).child({ sessionId });
4012
- const combinedChild = parentLogger.child({ sessionId });
4013
- const originalInfo = combinedChild.info.bind(combinedChild);
4014
- const originalWarn = combinedChild.warn.bind(combinedChild);
4015
- const originalError = combinedChild.error.bind(combinedChild);
4016
- const originalDebug = combinedChild.debug.bind(combinedChild);
4017
- const originalFatal = combinedChild.fatal.bind(combinedChild);
4018
- combinedChild.info = ((objOrMsg, ...rest) => {
4019
- sessionFileLogger.info(objOrMsg, ...rest);
4020
- return originalInfo(objOrMsg, ...rest);
4021
- });
4022
- combinedChild.warn = ((objOrMsg, ...rest) => {
4023
- sessionFileLogger.warn(objOrMsg, ...rest);
4024
- return originalWarn(objOrMsg, ...rest);
4025
- });
4026
- combinedChild.error = ((objOrMsg, ...rest) => {
4027
- sessionFileLogger.error(objOrMsg, ...rest);
4028
- return originalError(objOrMsg, ...rest);
4029
- });
4030
- combinedChild.debug = ((objOrMsg, ...rest) => {
4031
- sessionFileLogger.debug(objOrMsg, ...rest);
4032
- return originalDebug(objOrMsg, ...rest);
4033
- });
4034
- combinedChild.fatal = ((objOrMsg, ...rest) => {
4035
- sessionFileLogger.fatal(objOrMsg, ...rest);
4036
- return originalFatal(objOrMsg, ...rest);
4037
- });
4038
- combinedChild.__sessionDest = dest;
4039
- return combinedChild;
4040
- } catch (err) {
4041
- parentLogger.warn({ sessionId, err }, "Failed to create session log file, using combined log only");
4042
- return parentLogger.child({ sessionId });
4043
- }
4044
- }
4045
- function closeSessionLogger(logger) {
4046
- const dest = logger.__sessionDest;
4047
- if (dest && typeof dest.destroy === "function") {
4048
- dest.destroy();
4105
+ } catch {
4106
+ }
4107
+ }
4108
+ }
4109
+ };
4049
4110
  }
4050
- }
4051
- async function shutdownLogger() {
4052
- if (!initialized) return;
4053
- const transport = currentTransport;
4054
- rootLogger = pino({ level: "debug" });
4055
- Object.assign(log, wrapVariadic(rootLogger));
4056
- currentTransport = void 0;
4057
- logDir = void 0;
4058
- initialized = false;
4059
- if (transport) {
4060
- await new Promise((resolve8) => {
4061
- const timeout = setTimeout(resolve8, 3e3);
4062
- transport.on("close", () => {
4063
- clearTimeout(timeout);
4064
- resolve8();
4111
+ });
4112
+
4113
+ // src/plugins/notifications/index.ts
4114
+ var notifications_exports = {};
4115
+ __export(notifications_exports, {
4116
+ default: () => notifications_default
4117
+ });
4118
+ function createNotificationsPlugin() {
4119
+ return {
4120
+ name: "@openacp/notifications",
4121
+ version: "1.0.0",
4122
+ description: "Cross-session notification routing",
4123
+ essential: false,
4124
+ pluginDependencies: { "@openacp/security": "^1.0.0" },
4125
+ permissions: ["services:register", "kernel:access"],
4126
+ async install(ctx) {
4127
+ const { settings, terminal } = ctx;
4128
+ await settings.setAll({ enabled: true });
4129
+ terminal.log.success("Notifications defaults saved");
4130
+ },
4131
+ async configure(ctx) {
4132
+ const { terminal, settings } = ctx;
4133
+ const current = await settings.getAll();
4134
+ const toggle = await terminal.confirm({
4135
+ message: `Notifications are ${current.enabled !== false ? "enabled" : "disabled"}. Toggle?`,
4136
+ initialValue: false
4065
4137
  });
4066
- transport.end();
4067
- });
4068
- }
4069
- }
4070
- async function cleanupOldSessionLogs(retentionDays) {
4071
- if (!logDir) return;
4072
- const sessionsDir = path13.join(logDir, "sessions");
4073
- try {
4074
- const files = await fs10.promises.readdir(sessionsDir);
4075
- const cutoff = Date.now() - retentionDays * 24 * 60 * 60 * 1e3;
4076
- for (const file of files) {
4077
- try {
4078
- const filePath = path13.join(sessionsDir, file);
4079
- const stat = await fs10.promises.stat(filePath);
4080
- if (stat.mtimeMs < cutoff) {
4081
- await fs10.promises.unlink(filePath);
4082
- rootLogger.debug({ file }, "Deleted old session log");
4083
- }
4084
- } catch (err) {
4085
- rootLogger.warn({ file, err }, "Failed to delete old session log");
4138
+ if (toggle) {
4139
+ const newState = current.enabled === false;
4140
+ await settings.set("enabled", newState);
4141
+ terminal.log.success(`Notifications ${newState ? "enabled" : "disabled"}`);
4142
+ }
4143
+ },
4144
+ async uninstall(ctx, opts) {
4145
+ if (opts.purge) {
4146
+ await ctx.settings.clear();
4147
+ ctx.terminal.log.success("Notifications settings cleared");
4086
4148
  }
4149
+ },
4150
+ async setup(ctx) {
4151
+ const core = ctx.core;
4152
+ const manager = new NotificationManager(core.adapters);
4153
+ ctx.registerService("notifications", manager);
4154
+ ctx.log.info("Notifications service ready");
4087
4155
  }
4088
- } catch {
4089
- }
4156
+ };
4090
4157
  }
4091
- var rootLogger, initialized, logDir, currentTransport, log, muteCount, savedLevel;
4092
- var init_log = __esm({
4093
- "src/core/utils/log.ts"() {
4158
+ var notifications_default;
4159
+ var init_notifications = __esm({
4160
+ "src/plugins/notifications/index.ts"() {
4094
4161
  "use strict";
4095
- rootLogger = pino({
4096
- level: "debug",
4097
- transport: { target: "pino-pretty", options: { colorize: true, translateTime: "SYS:standard", destination: 2 } }
4098
- });
4099
- initialized = false;
4100
- log = wrapVariadic(rootLogger);
4101
- muteCount = 0;
4102
- savedLevel = "info";
4162
+ init_notification();
4163
+ notifications_default = createNotificationsPlugin();
4103
4164
  }
4104
4165
  });
4105
4166
 
@@ -9738,6 +9799,14 @@ var init_validators = __esm({
9738
9799
  });
9739
9800
 
9740
9801
  // src/plugins/telegram/topics.ts
9802
+ var topics_exports2 = {};
9803
+ __export(topics_exports2, {
9804
+ buildDeepLink: () => buildDeepLink,
9805
+ createSessionTopic: () => createSessionTopic,
9806
+ deleteSessionTopic: () => deleteSessionTopic,
9807
+ ensureTopics: () => ensureTopics,
9808
+ renameSessionTopic: () => renameSessionTopic
9809
+ });
9741
9810
  async function ensureTopics(bot, chatId, config, saveConfig) {
9742
9811
  let notificationTopicId = config.notificationTopicId;
9743
9812
  let assistantTopicId = config.assistantTopicId;
@@ -10452,76 +10521,328 @@ function setupVerbosityCallbacks(bot, core) {
10452
10521
  "channels.telegram.outputMode"
10453
10522
  );
10454
10523
  try {
10455
- await ctx.answerCallbackQuery({
10456
- text: `${OUTPUT_MODE_LABELS[level]} Output mode: ${level}`
10457
- });
10524
+ await ctx.answerCallbackQuery({
10525
+ text: `${OUTPUT_MODE_LABELS[level]} Output mode: ${level}`
10526
+ });
10527
+ } catch {
10528
+ }
10529
+ });
10530
+ }
10531
+ async function handleUpdate(ctx, core) {
10532
+ if (!core.requestRestart) {
10533
+ await ctx.reply(
10534
+ "\u26A0\uFE0F Update is not available (no restart handler registered).",
10535
+ { parse_mode: "HTML" }
10536
+ );
10537
+ return;
10538
+ }
10539
+ const { getCurrentVersion: getCurrentVersion2, getLatestVersion: getLatestVersion2, compareVersions: compareVersions2, runUpdate: runUpdate2 } = await Promise.resolve().then(() => (init_version(), version_exports));
10540
+ const current = getCurrentVersion2();
10541
+ const statusMsg = await ctx.reply(
10542
+ `\u{1F50D} Checking for updates... (current: v${escapeHtml4(current)})`,
10543
+ { parse_mode: "HTML" }
10544
+ );
10545
+ const latest = await getLatestVersion2();
10546
+ if (!latest) {
10547
+ await ctx.api.editMessageText(
10548
+ ctx.chat.id,
10549
+ statusMsg.message_id,
10550
+ "\u274C Could not check for updates.",
10551
+ { parse_mode: "HTML" }
10552
+ );
10553
+ return;
10554
+ }
10555
+ if (compareVersions2(current, latest) >= 0) {
10556
+ await ctx.api.editMessageText(
10557
+ ctx.chat.id,
10558
+ statusMsg.message_id,
10559
+ `\u2705 Already up to date (v${escapeHtml4(current)}).`,
10560
+ { parse_mode: "HTML" }
10561
+ );
10562
+ return;
10563
+ }
10564
+ await ctx.api.editMessageText(
10565
+ ctx.chat.id,
10566
+ statusMsg.message_id,
10567
+ `\u2B07\uFE0F Updating v${escapeHtml4(current)} \u2192 v${escapeHtml4(latest)}...`,
10568
+ { parse_mode: "HTML" }
10569
+ );
10570
+ const ok3 = await runUpdate2();
10571
+ if (!ok3) {
10572
+ await ctx.api.editMessageText(
10573
+ ctx.chat.id,
10574
+ statusMsg.message_id,
10575
+ "\u274C Update failed. Try manually: <code>npm install -g @openacp/cli@latest</code>",
10576
+ { parse_mode: "HTML" }
10577
+ );
10578
+ return;
10579
+ }
10580
+ await ctx.api.editMessageText(
10581
+ ctx.chat.id,
10582
+ statusMsg.message_id,
10583
+ `\u2705 Updated to v${escapeHtml4(latest)}. Restarting...`,
10584
+ { parse_mode: "HTML" }
10585
+ );
10586
+ await new Promise((r) => setTimeout(r, 500));
10587
+ await core.requestRestart();
10588
+ }
10589
+ async function handleRestart(ctx, core) {
10590
+ if (!core.requestRestart) {
10591
+ await ctx.reply(
10592
+ "\u26A0\uFE0F Restart is not available (no restart handler registered).",
10593
+ { parse_mode: "HTML" }
10594
+ );
10595
+ return;
10596
+ }
10597
+ await ctx.reply(
10598
+ "\u{1F504} <b>Restarting OpenACP...</b>\nRebuilding and restarting. Be back shortly.",
10599
+ { parse_mode: "HTML" }
10600
+ );
10601
+ await new Promise((r) => setTimeout(r, 500));
10602
+ await core.requestRestart();
10603
+ }
10604
+ var log13, OUTPUT_MODE_LABELS;
10605
+ var init_admin = __esm({
10606
+ "src/plugins/telegram/commands/admin.ts"() {
10607
+ "use strict";
10608
+ init_bypass_detection();
10609
+ init_formatting();
10610
+ init_log();
10611
+ log13 = createChildLogger({ module: "telegram-cmd-admin" });
10612
+ OUTPUT_MODE_LABELS = {
10613
+ low: "\u{1F507} Low",
10614
+ medium: "\u{1F4CA} Medium",
10615
+ high: "\u{1F50D} High"
10616
+ };
10617
+ }
10618
+ });
10619
+
10620
+ // src/plugins/telegram/commands/new-session.ts
10621
+ import { InlineKeyboard as InlineKeyboard2 } from "grammy";
10622
+ function botFromCtx(ctx) {
10623
+ return { api: ctx.api };
10624
+ }
10625
+ async function createSessionDirect(ctx, core, chatId, agentName, workspace, onControlMessage) {
10626
+ log14.info({ userId: ctx.from?.id, agentName, workspace }, "New session command (direct)");
10627
+ let threadId;
10628
+ try {
10629
+ const topicName = `\u{1F504} New Session`;
10630
+ threadId = await createSessionTopic(botFromCtx(ctx), chatId, topicName);
10631
+ await ctx.api.sendMessage(chatId, `\u23F3 Setting up session, please wait...`, {
10632
+ message_thread_id: threadId,
10633
+ parse_mode: "HTML"
10634
+ });
10635
+ const session = await core.handleNewSession("telegram", agentName, workspace);
10636
+ session.threadId = String(threadId);
10637
+ await core.sessionManager.patchRecord(session.id, { platform: { topicId: threadId } });
10638
+ const finalName = `\u{1F504} ${session.agentName} \u2014 New Session`;
10639
+ try {
10640
+ await ctx.api.editForumTopic(chatId, threadId, { name: finalName });
10641
+ } catch {
10642
+ }
10643
+ const controlMsg = await ctx.api.sendMessage(
10644
+ chatId,
10645
+ buildSessionStatusText(session, `\u2705 <b>Session started</b>`),
10646
+ {
10647
+ message_thread_id: threadId,
10648
+ parse_mode: "HTML",
10649
+ reply_markup: buildSessionControlKeyboard(session.id, false, false)
10650
+ }
10651
+ );
10652
+ onControlMessage?.(session.id, controlMsg.message_id);
10653
+ return threadId ?? null;
10654
+ } catch (err) {
10655
+ log14.error({ err }, "Session creation failed");
10656
+ if (threadId) {
10657
+ try {
10658
+ await ctx.api.deleteForumTopic(chatId, threadId);
10659
+ } catch {
10660
+ }
10661
+ }
10662
+ const message = err instanceof Error ? err.message : typeof err === "object" ? JSON.stringify(err) : String(err);
10663
+ await ctx.reply(`\u274C ${escapeHtml4(message)}`, { parse_mode: "HTML" });
10664
+ return null;
10665
+ }
10666
+ }
10667
+ function cacheWorkspace(agentKey, workspace) {
10668
+ const now = Date.now();
10669
+ if (workspaceCache.size > WS_CACHE_MAX) {
10670
+ for (const [id2, entry] of workspaceCache) {
10671
+ if (now - entry.ts > 5 * 6e4 || workspaceCache.size > WS_CACHE_MAX) {
10672
+ workspaceCache.delete(id2);
10673
+ }
10674
+ }
10675
+ }
10676
+ const id = nextWsId++;
10677
+ workspaceCache.set(id, { agentKey, workspace, ts: now });
10678
+ return id;
10679
+ }
10680
+ function shortenPath2(ws) {
10681
+ const home = process.env.HOME || "";
10682
+ return home && ws.startsWith(home) ? "~" + ws.slice(home.length) : ws;
10683
+ }
10684
+ async function showAgentPicker(ctx, core, chatId) {
10685
+ const catalog = core.agentCatalog;
10686
+ const installed = catalog.getAvailable().filter((i) => i.installed);
10687
+ if (installed.length === 0) {
10688
+ await ctx.reply("No agents installed. Use /install to add one.", { parse_mode: "HTML" }).catch(() => {
10689
+ });
10690
+ return;
10691
+ }
10692
+ if (installed.length === 1) {
10693
+ await showWorkspacePicker(ctx, core, chatId, installed[0].key, true);
10694
+ return;
10695
+ }
10696
+ const kb = new InlineKeyboard2();
10697
+ for (let i = 0; i < installed.length; i += 2) {
10698
+ const row = installed.slice(i, i + 2);
10699
+ for (const agent of row) {
10700
+ kb.text(agent.name, `ns:agent:${agent.key}`);
10701
+ }
10702
+ kb.row();
10703
+ }
10704
+ await ctx.reply("<b>\u{1F195} New Session</b>\nSelect an agent:", {
10705
+ parse_mode: "HTML",
10706
+ reply_markup: kb
10707
+ }).catch(() => {
10708
+ });
10709
+ }
10710
+ async function showWorkspacePicker(ctx, core, chatId, agentKey, newMessage = false) {
10711
+ const records = core.sessionManager.listRecords();
10712
+ const recentWorkspaces = [...new Set(records.map((r) => r.workingDir).filter(Boolean))].slice(0, 5);
10713
+ const config = core.configManager.get();
10714
+ const baseDir = config.workspace.baseDir;
10715
+ const resolvedBaseDir = core.configManager.resolveWorkspace(baseDir);
10716
+ const hasBaseDir = recentWorkspaces.some((ws) => ws === baseDir || ws === resolvedBaseDir);
10717
+ const workspaces = hasBaseDir ? recentWorkspaces : [resolvedBaseDir, ...recentWorkspaces].slice(0, 5);
10718
+ const kb = new InlineKeyboard2();
10719
+ for (const ws of workspaces) {
10720
+ const id = cacheWorkspace(agentKey, ws);
10721
+ kb.text(`\u{1F4C1} ${shortenPath2(ws)}`, `ns:ws:${id}`).row();
10722
+ }
10723
+ kb.text("\u{1F4C1} Custom path...", `ns:custom:${agentKey}`).row();
10724
+ const agentLabel = escapeHtml4(agentKey);
10725
+ const text6 = `<b>\u{1F195} New Session</b>
10726
+ Agent: <code>${agentLabel}</code>
10727
+
10728
+ Select workspace:`;
10729
+ const opts = { parse_mode: "HTML", reply_markup: kb };
10730
+ if (newMessage) {
10731
+ await ctx.reply(text6, opts).catch(() => {
10732
+ });
10733
+ } else {
10734
+ try {
10735
+ await ctx.editMessageText(text6, opts);
10736
+ } catch {
10737
+ await ctx.reply(text6, opts).catch(() => {
10738
+ });
10739
+ }
10740
+ }
10741
+ }
10742
+ function setupNewSessionCallbacks(bot, core, chatId, getAssistantSession) {
10743
+ bot.callbackQuery("ns:start", async (ctx) => {
10744
+ try {
10745
+ await ctx.answerCallbackQuery();
10746
+ } catch {
10747
+ }
10748
+ await showAgentPicker(ctx, core, chatId);
10749
+ });
10750
+ bot.callbackQuery(/^ns:agent:/, async (ctx) => {
10751
+ const agentKey = ctx.callbackQuery.data.replace("ns:agent:", "");
10752
+ try {
10753
+ await ctx.answerCallbackQuery();
10754
+ } catch {
10755
+ }
10756
+ await showWorkspacePicker(ctx, core, chatId, agentKey);
10757
+ });
10758
+ bot.callbackQuery(/^ns:ws:/, async (ctx) => {
10759
+ const id = parseInt(ctx.callbackQuery.data.replace("ns:ws:", ""), 10);
10760
+ try {
10761
+ await ctx.answerCallbackQuery();
10762
+ } catch {
10763
+ }
10764
+ const entry = workspaceCache.get(id);
10765
+ if (!entry) {
10766
+ try {
10767
+ await ctx.editMessageText("\u26A0\uFE0F Session expired. Please try again via /menu.");
10768
+ } catch {
10769
+ }
10770
+ return;
10771
+ }
10772
+ workspaceCache.delete(id);
10773
+ try {
10774
+ await ctx.editMessageText(
10775
+ `<b>\u{1F195} New Session</b>
10776
+ Agent: <code>${escapeHtml4(entry.agentKey)}</code>
10777
+ Workspace: <code>${escapeHtml4(shortenPath2(entry.workspace))}</code>
10778
+
10779
+ \u23F3 Creating session...`,
10780
+ { parse_mode: "HTML" }
10781
+ );
10458
10782
  } catch {
10459
10783
  }
10460
- });
10461
- }
10462
- var log13, OUTPUT_MODE_LABELS;
10463
- var init_admin = __esm({
10464
- "src/plugins/telegram/commands/admin.ts"() {
10465
- "use strict";
10466
- init_bypass_detection();
10467
- init_formatting();
10468
- init_log();
10469
- log13 = createChildLogger({ module: "telegram-cmd-admin" });
10470
- OUTPUT_MODE_LABELS = {
10471
- low: "\u{1F507} Low",
10472
- medium: "\u{1F4CA} Medium",
10473
- high: "\u{1F50D} High"
10474
- };
10475
- }
10476
- });
10784
+ const threadId = await createSessionDirect(ctx, core, chatId, entry.agentKey, entry.workspace);
10785
+ if (threadId) {
10786
+ const { buildDeepLink: buildDeepLink2 } = await Promise.resolve().then(() => (init_topics2(), topics_exports2));
10787
+ const link = buildDeepLink2(chatId, threadId);
10788
+ try {
10789
+ await ctx.editMessageText(
10790
+ `<b>\u2705 Session created</b>
10791
+ Agent: <code>${escapeHtml4(entry.agentKey)}</code>
10792
+ Workspace: <code>${escapeHtml4(shortenPath2(entry.workspace))}</code>
10477
10793
 
10478
- // src/plugins/telegram/commands/new-session.ts
10479
- function botFromCtx(ctx) {
10480
- return { api: ctx.api };
10481
- }
10482
- async function createSessionDirect(ctx, core, chatId, agentName, workspace, onControlMessage) {
10483
- log14.info({ userId: ctx.from?.id, agentName, workspace }, "New session command (direct)");
10484
- let threadId;
10485
- try {
10486
- const topicName = `\u{1F504} New Session`;
10487
- threadId = await createSessionTopic(botFromCtx(ctx), chatId, topicName);
10488
- await ctx.api.sendMessage(chatId, `\u23F3 Setting up session, please wait...`, {
10489
- message_thread_id: threadId,
10490
- parse_mode: "HTML"
10491
- });
10492
- const session = await core.handleNewSession("telegram", agentName, workspace);
10493
- session.threadId = String(threadId);
10494
- await core.sessionManager.patchRecord(session.id, { platform: { topicId: threadId } });
10495
- const finalName = `\u{1F504} ${session.agentName} \u2014 New Session`;
10794
+ <a href="${link}">Open session \u2192</a>`,
10795
+ { parse_mode: "HTML" }
10796
+ );
10797
+ } catch {
10798
+ }
10799
+ } else {
10800
+ try {
10801
+ await ctx.editMessageText(
10802
+ `<b>\u274C Session creation failed</b>
10803
+ Agent: <code>${escapeHtml4(entry.agentKey)}</code>
10804
+ Workspace: <code>${escapeHtml4(shortenPath2(entry.workspace))}</code>
10805
+
10806
+ Try again with /new or /menu`,
10807
+ { parse_mode: "HTML" }
10808
+ );
10809
+ } catch {
10810
+ }
10811
+ }
10812
+ });
10813
+ bot.callbackQuery(/^ns:custom:/, async (ctx) => {
10814
+ const agentKey = ctx.callbackQuery.data.replace("ns:custom:", "");
10496
10815
  try {
10497
- await ctx.api.editForumTopic(chatId, threadId, { name: finalName });
10816
+ await ctx.answerCallbackQuery();
10498
10817
  } catch {
10499
10818
  }
10500
- const controlMsg = await ctx.api.sendMessage(
10501
- chatId,
10502
- buildSessionStatusText(session, `\u2705 <b>Session started</b>`),
10503
- {
10504
- message_thread_id: threadId,
10505
- parse_mode: "HTML",
10506
- reply_markup: buildSessionControlKeyboard(session.id, false, false)
10819
+ const assistant = getAssistantSession?.();
10820
+ if (assistant) {
10821
+ try {
10822
+ await ctx.editMessageText(
10823
+ `<b>\u{1F195} New Session</b>
10824
+ Agent: <code>${escapeHtml4(agentKey)}</code>
10825
+
10826
+ \u{1F4AC} Type your workspace path in the chat below.`,
10827
+ { parse_mode: "HTML" }
10828
+ );
10829
+ } catch {
10507
10830
  }
10508
- );
10509
- onControlMessage?.(session.id, controlMsg.message_id);
10510
- return threadId ?? null;
10511
- } catch (err) {
10512
- log14.error({ err }, "Session creation failed");
10513
- if (threadId) {
10831
+ await assistant.enqueuePrompt(
10832
+ `User wants to create a new session with agent "${agentKey}". Ask them for the workspace (project directory) path, then create the session.`
10833
+ );
10834
+ } else {
10514
10835
  try {
10515
- await ctx.api.deleteForumTopic(chatId, threadId);
10836
+ await ctx.editMessageText(
10837
+ `Usage: <code>/new ${escapeHtml4(agentKey)} &lt;workspace-path&gt;</code>`,
10838
+ { parse_mode: "HTML" }
10839
+ );
10516
10840
  } catch {
10517
10841
  }
10518
10842
  }
10519
- const message = err instanceof Error ? err.message : typeof err === "object" ? JSON.stringify(err) : String(err);
10520
- await ctx.reply(`\u274C ${escapeHtml4(message)}`, { parse_mode: "HTML" });
10521
- return null;
10522
- }
10843
+ });
10523
10844
  }
10524
- var log14;
10845
+ var log14, WS_CACHE_MAX, workspaceCache, nextWsId;
10525
10846
  var init_new_session = __esm({
10526
10847
  "src/plugins/telegram/commands/new-session.ts"() {
10527
10848
  "use strict";
@@ -10530,11 +10851,74 @@ var init_new_session = __esm({
10530
10851
  init_log();
10531
10852
  init_admin();
10532
10853
  log14 = createChildLogger({ module: "telegram-cmd-new-session" });
10854
+ WS_CACHE_MAX = 50;
10855
+ workspaceCache = /* @__PURE__ */ new Map();
10856
+ nextWsId = 0;
10533
10857
  }
10534
10858
  });
10535
10859
 
10536
10860
  // src/plugins/telegram/commands/session.ts
10537
- import { InlineKeyboard as InlineKeyboard2 } from "grammy";
10861
+ import { InlineKeyboard as InlineKeyboard3 } from "grammy";
10862
+ async function handleTopics(ctx, core) {
10863
+ try {
10864
+ const allRecords = core.sessionManager.listRecords();
10865
+ const records = allRecords.filter((r) => {
10866
+ const platform2 = r.platform;
10867
+ return !!platform2?.topicId;
10868
+ });
10869
+ const headlessCount = allRecords.length - records.length;
10870
+ if (records.length === 0) {
10871
+ const extra = headlessCount > 0 ? ` (${headlessCount} headless hidden)` : "";
10872
+ await ctx.reply(`No sessions with topics found.${extra}`, { parse_mode: "HTML" });
10873
+ return;
10874
+ }
10875
+ const statusEmoji = {
10876
+ active: "\u{1F7E2}",
10877
+ initializing: "\u{1F7E1}",
10878
+ finished: "\u2705",
10879
+ error: "\u274C",
10880
+ cancelled: "\u26D4"
10881
+ };
10882
+ const statusOrder = { active: 0, initializing: 1, error: 2, finished: 3, cancelled: 4 };
10883
+ records.sort((a, b) => (statusOrder[a.status] ?? 5) - (statusOrder[b.status] ?? 5));
10884
+ const MAX_DISPLAY = 30;
10885
+ const displayed = records.slice(0, MAX_DISPLAY);
10886
+ const lines = displayed.map((r) => {
10887
+ const emoji = statusEmoji[r.status] || "\u26AA";
10888
+ const name = r.name?.trim();
10889
+ const label = name ? escapeHtml4(name) : `<i>${escapeHtml4(r.agentName)} session</i>`;
10890
+ return `${emoji} ${label} <code>[${r.status}]</code>`;
10891
+ });
10892
+ const header2 = `<b>Sessions: ${records.length}</b>` + (headlessCount > 0 ? ` (${headlessCount} headless hidden)` : "");
10893
+ const truncated = records.length > MAX_DISPLAY ? `
10894
+
10895
+ <i>...and ${records.length - MAX_DISPLAY} more</i>` : "";
10896
+ const finishedCount = allRecords.filter((r) => r.status === "finished").length;
10897
+ const errorCount = allRecords.filter((r) => r.status === "error" || r.status === "cancelled").length;
10898
+ const keyboard = new InlineKeyboard3();
10899
+ if (finishedCount > 0) {
10900
+ keyboard.text(`Cleanup finished (${finishedCount})`, "m:cleanup:finished").row();
10901
+ }
10902
+ if (errorCount > 0) {
10903
+ keyboard.text(`Cleanup errors (${errorCount})`, "m:cleanup:errors").row();
10904
+ }
10905
+ if (finishedCount + errorCount > 0) {
10906
+ keyboard.text(`Cleanup all non-active (${finishedCount + errorCount})`, "m:cleanup:all").row();
10907
+ }
10908
+ keyboard.text(`\u26A0\uFE0F Cleanup ALL (${allRecords.length})`, "m:cleanup:everything").row();
10909
+ keyboard.text("Refresh", "m:topics");
10910
+ await ctx.reply(
10911
+ `${header2}
10912
+
10913
+ ${lines.join("\n")}${truncated}`,
10914
+ { parse_mode: "HTML", reply_markup: keyboard }
10915
+ );
10916
+ } catch (err) {
10917
+ log15.error({ err }, "handleTopics error");
10918
+ await ctx.reply("\u274C Failed to list sessions.", { parse_mode: "HTML" }).catch(() => {
10919
+ });
10920
+ }
10921
+ }
10538
10922
  async function handleCleanup(ctx, core, chatId, statuses) {
10539
10923
  const allRecords = core.sessionManager.listRecords();
10540
10924
  const cleanable = allRecords.filter((r) => statuses.includes(r.status));
@@ -10593,7 +10977,7 @@ async function handleCleanupEverything(ctx, core, chatId, systemTopicIds) {
10593
10977
  const activeWarning = activeCount > 0 ? `
10594
10978
 
10595
10979
  \u26A0\uFE0F <b>${activeCount} active session(s) will be cancelled and their agents stopped!</b>` : "";
10596
- const keyboard = new InlineKeyboard2().text("Yes, delete all", "m:cleanup:everything:confirm").text("Cancel", "m:topics");
10980
+ const keyboard = new InlineKeyboard3().text("Yes, delete all", "m:cleanup:everything:confirm").text("Cancel", "m:topics");
10597
10981
  await ctx.reply(
10598
10982
  `<b>Delete ${cleanable.length} topics?</b>
10599
10983
 
@@ -10676,6 +11060,13 @@ function setupSessionCallbacks(bot, core, chatId, systemTopicIds) {
10676
11060
  break;
10677
11061
  }
10678
11062
  });
11063
+ bot.callbackQuery("m:topics", async (ctx) => {
11064
+ try {
11065
+ await ctx.answerCallbackQuery();
11066
+ } catch {
11067
+ }
11068
+ await handleTopics(ctx, core);
11069
+ });
10679
11070
  }
10680
11071
  async function handleArchiveConfirm(ctx, core, chatId) {
10681
11072
  const data = ctx.callbackQuery?.data;
@@ -11087,7 +11478,73 @@ var init_integrate = __esm({
11087
11478
  });
11088
11479
 
11089
11480
  // src/plugins/telegram/commands/agents.ts
11090
- import { InlineKeyboard as InlineKeyboard3 } from "grammy";
11481
+ import { InlineKeyboard as InlineKeyboard4 } from "grammy";
11482
+ async function handleAgents(ctx, core, page = 0) {
11483
+ const catalog = core.agentCatalog;
11484
+ const items = catalog.getAvailable();
11485
+ const installed = items.filter((i) => i.installed);
11486
+ const available = items.filter((i) => !i.installed);
11487
+ let text6 = "<b>\u{1F916} Agents</b>\n\n";
11488
+ if (installed.length > 0) {
11489
+ text6 += "<b>Installed:</b>\n";
11490
+ for (const item of installed) {
11491
+ text6 += `\u2705 <b>${escapeHtml4(item.name)}</b>`;
11492
+ if (item.description) {
11493
+ text6 += ` \u2014 <i>${escapeHtml4(truncate(item.description, 50))}</i>`;
11494
+ }
11495
+ text6 += "\n";
11496
+ }
11497
+ text6 += "\n";
11498
+ }
11499
+ if (available.length > 0) {
11500
+ const totalPages = Math.ceil(available.length / AGENTS_PER_PAGE);
11501
+ const safePage = Math.max(0, Math.min(page, totalPages - 1));
11502
+ const pageItems = available.slice(safePage * AGENTS_PER_PAGE, (safePage + 1) * AGENTS_PER_PAGE);
11503
+ text6 += `<b>Available to install:</b>`;
11504
+ if (totalPages > 1) {
11505
+ text6 += ` (${safePage + 1}/${totalPages})`;
11506
+ }
11507
+ text6 += "\n";
11508
+ for (const item of pageItems) {
11509
+ if (item.available) {
11510
+ text6 += `\u2B07\uFE0F <b>${escapeHtml4(item.name)}</b>`;
11511
+ } else {
11512
+ const deps = item.missingDeps?.join(", ") ?? "requirements not met";
11513
+ text6 += `\u26A0\uFE0F <b>${escapeHtml4(item.name)}</b> <i>(needs: ${escapeHtml4(deps)})</i>`;
11514
+ }
11515
+ if (item.description) {
11516
+ text6 += `
11517
+ <i>${escapeHtml4(truncate(item.description, 60))}</i>`;
11518
+ }
11519
+ text6 += "\n";
11520
+ }
11521
+ const keyboard = new InlineKeyboard4();
11522
+ const installable = pageItems.filter((i) => i.available);
11523
+ for (let i = 0; i < installable.length; i += 2) {
11524
+ const row = installable.slice(i, i + 2);
11525
+ for (const item of row) {
11526
+ keyboard.text(`\u2B07\uFE0F ${item.name}`, `ag:install:${item.key}`);
11527
+ }
11528
+ keyboard.row();
11529
+ }
11530
+ if (totalPages > 1) {
11531
+ if (safePage > 0) {
11532
+ keyboard.text("\u25C0\uFE0F Prev", `ag:page:${safePage - 1}`);
11533
+ }
11534
+ if (safePage < totalPages - 1) {
11535
+ keyboard.text("Next \u25B6\uFE0F", `ag:page:${safePage + 1}`);
11536
+ }
11537
+ keyboard.row();
11538
+ }
11539
+ if (available.some((i) => !i.available)) {
11540
+ text6 += "\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";
11541
+ }
11542
+ await ctx.reply(text6, { parse_mode: "HTML", reply_markup: keyboard });
11543
+ } else {
11544
+ text6 += "<i>All agents are already installed!</i>";
11545
+ await ctx.reply(text6, { parse_mode: "HTML" });
11546
+ }
11547
+ }
11091
11548
  async function handleAgentCallback(ctx, core) {
11092
11549
  const data = ctx.callbackQuery?.data ?? "";
11093
11550
  await ctx.answerCallbackQuery();
@@ -11136,7 +11593,7 @@ async function handleAgentCallback(ctx, core) {
11136
11593
  }
11137
11594
  text6 += "\n";
11138
11595
  }
11139
- const keyboard = new InlineKeyboard3();
11596
+ const keyboard = new InlineKeyboard4();
11140
11597
  const installable = pageItems.filter((i) => i.available);
11141
11598
  for (let i = 0; i < installable.length; i += 2) {
11142
11599
  const row = installable.slice(i, i + 2);
@@ -11191,7 +11648,7 @@ Downloading... ${bar} ${percent}%`, { parse_mode: "HTML" });
11191
11648
  },
11192
11649
  async onSuccess(name) {
11193
11650
  try {
11194
- const keyboard = new InlineKeyboard3().text(`\u{1F680} Start session with ${name}`, `na:${nameOrId}`);
11651
+ const keyboard = new InlineKeyboard4().text(`\u{1F680} Start session with ${name}`, `na:${nameOrId}`);
11195
11652
  await ctx.api.editMessageText(msg.chat.id, msg.message_id, `\u2705 <b>${escapeHtml4(name)}</b> installed!`, { parse_mode: "HTML", reply_markup: keyboard });
11196
11653
  } catch {
11197
11654
  }
@@ -11309,13 +11766,13 @@ __export(menu_exports, {
11309
11766
  handleHelp: () => handleHelp,
11310
11767
  handleMenu: () => handleMenu
11311
11768
  });
11312
- import { InlineKeyboard as InlineKeyboard4 } from "grammy";
11769
+ import { InlineKeyboard as InlineKeyboard5 } from "grammy";
11313
11770
  function buildMenuKeyboard(menuRegistry) {
11314
11771
  if (!menuRegistry) {
11315
- return new InlineKeyboard4().text("\u{1F195} New Session", "m:core:new").text("\u{1F4CB} Sessions", "m:core:sessions").row().text("\u{1F4CA} Status", "m:core:status").text("\u{1F916} Agents", "m:core:agents").row().text("\u2753 Help", "m:core:help");
11772
+ return new InlineKeyboard5().text("\u{1F195} New Session", "m:core:new").text("\u{1F4CB} Sessions", "m:core:sessions").row().text("\u{1F4CA} Status", "m:core:status").text("\u{1F916} Agents", "m:core:agents").row().text("\u2753 Help", "m:core:help");
11316
11773
  }
11317
11774
  const items = menuRegistry.getItems();
11318
- const kb = new InlineKeyboard4();
11775
+ const kb = new InlineKeyboard5();
11319
11776
  let currentGroup;
11320
11777
  let rowCount = 0;
11321
11778
  for (const item of items) {
@@ -11333,11 +11790,11 @@ function buildMenuKeyboard(menuRegistry) {
11333
11790
  }
11334
11791
  return kb;
11335
11792
  }
11336
- async function handleMenu(ctx) {
11793
+ async function handleMenu(ctx, menuRegistry) {
11337
11794
  await ctx.reply(`<b>OpenACP Menu</b>
11338
11795
  Choose an action:`, {
11339
11796
  parse_mode: "HTML",
11340
- reply_markup: buildMenuKeyboard()
11797
+ reply_markup: buildMenuKeyboard(menuRegistry)
11341
11798
  });
11342
11799
  }
11343
11800
  async function handleHelp(ctx) {
@@ -11354,7 +11811,7 @@ Each session gets its own topic \u2014 chat there to work with the agent.
11354
11811
  /status \u2014 Show session or system status
11355
11812
  /sessions \u2014 List all sessions
11356
11813
  /agents \u2014 Browse & install agents
11357
- /install <name> \u2014 Install an agent
11814
+ /install &lt;name&gt; \u2014 Install an agent
11358
11815
 
11359
11816
  \u2699\uFE0F <b>System</b>
11360
11817
  /restart \u2014 Restart OpenACP
@@ -11418,11 +11875,11 @@ var init_menu = __esm({
11418
11875
  });
11419
11876
 
11420
11877
  // src/plugins/telegram/commands/settings.ts
11421
- import { InlineKeyboard as InlineKeyboard5 } from "grammy";
11878
+ import { InlineKeyboard as InlineKeyboard6 } from "grammy";
11422
11879
  function buildSettingsKeyboard(core) {
11423
11880
  const config = core.configManager.get();
11424
11881
  const fields = getSafeFields();
11425
- const kb = new InlineKeyboard5();
11882
+ const kb = new InlineKeyboard6();
11426
11883
  for (const field of fields) {
11427
11884
  const value = getConfigValue(config, field.path);
11428
11885
  const label = formatFieldLabel(field, value);
@@ -11454,6 +11911,14 @@ function formatFieldLabel(field, value) {
11454
11911
  const displayValue = value === null || value === void 0 ? "Not set" : String(value);
11455
11912
  return `${icon} ${field.displayName}: ${displayValue}`;
11456
11913
  }
11914
+ async function handleSettings(ctx, core) {
11915
+ const kb = buildSettingsKeyboard(core);
11916
+ await ctx.reply(`<b>\u2699\uFE0F Settings</b>
11917
+ Tap to change:`, {
11918
+ parse_mode: "HTML",
11919
+ reply_markup: kb
11920
+ });
11921
+ }
11457
11922
  function setupSettingsCallbacks(bot, core, getAssistantSession) {
11458
11923
  bot.callbackQuery(/^s:toggle:/, async (ctx) => {
11459
11924
  const fieldPath = ctx.callbackQuery.data.replace("s:toggle:", "");
@@ -11487,7 +11952,7 @@ function setupSettingsCallbacks(bot, core, getAssistantSession) {
11487
11952
  if (!fieldDef) return;
11488
11953
  const options = resolveOptions(fieldDef, config) ?? [];
11489
11954
  const currentValue = getConfigValue(config, fieldPath);
11490
- const kb = new InlineKeyboard5();
11955
+ const kb = new InlineKeyboard6();
11491
11956
  for (const opt of options) {
11492
11957
  const marker = opt === String(currentValue) ? " \u2713" : "";
11493
11958
  kb.text(`${opt}${marker}`, `s:pick:${fieldPath}:${opt}`).row();
@@ -11581,11 +12046,12 @@ Tap to change:`, {
11581
12046
  } catch {
11582
12047
  }
11583
12048
  const { buildMenuKeyboard: buildMenuKeyboard2 } = await Promise.resolve().then(() => (init_menu(), menu_exports));
12049
+ const menuRegistry = core.lifecycleManager?.serviceRegistry?.get("menu-registry");
11584
12050
  try {
11585
12051
  await ctx.editMessageText(`<b>OpenACP Menu</b>
11586
12052
  Choose an action:`, {
11587
12053
  parse_mode: "HTML",
11588
- reply_markup: buildMenuKeyboard2()
12054
+ reply_markup: buildMenuKeyboard2(menuRegistry)
11589
12055
  });
11590
12056
  } catch {
11591
12057
  }
@@ -12335,7 +12801,7 @@ var init_doctor = __esm({
12335
12801
  });
12336
12802
 
12337
12803
  // src/plugins/telegram/commands/doctor.ts
12338
- import { InlineKeyboard as InlineKeyboard6 } from "grammy";
12804
+ import { InlineKeyboard as InlineKeyboard7 } from "grammy";
12339
12805
  function renderReport(report) {
12340
12806
  const icons = { pass: "\u2705", warn: "\u26A0\uFE0F", fail: "\u274C" };
12341
12807
  const lines = ["\u{1FA7A} <b>OpenACP Doctor</b>\n"];
@@ -12351,7 +12817,7 @@ function renderReport(report) {
12351
12817
  lines.push(`<b>Result:</b> ${passed} passed, ${warnings} warnings, ${failed} failed${fixedStr}`);
12352
12818
  let keyboard;
12353
12819
  if (report.pendingFixes.length > 0) {
12354
- keyboard = new InlineKeyboard6();
12820
+ keyboard = new InlineKeyboard7();
12355
12821
  for (let i = 0; i < report.pendingFixes.length; i++) {
12356
12822
  const label = `\u{1F527} Fix: ${report.pendingFixes[i].message.slice(0, 30)}`;
12357
12823
  keyboard.text(label, `m:doctor:fix:${i}`).row();
@@ -12449,7 +12915,7 @@ var init_doctor2 = __esm({
12449
12915
  });
12450
12916
 
12451
12917
  // src/plugins/telegram/commands/tunnel.ts
12452
- import { InlineKeyboard as InlineKeyboard7 } from "grammy";
12918
+ import { InlineKeyboard as InlineKeyboard8 } from "grammy";
12453
12919
  function setupTunnelCallbacks(bot, core) {
12454
12920
  bot.callbackQuery(/^tn:/, async (ctx) => {
12455
12921
  const data = ctx.callbackQuery.data;
@@ -12476,7 +12942,7 @@ function setupTunnelCallbacks(bot, core) {
12476
12942
  if (remaining2.length === 0) {
12477
12943
  await ctx.editMessageText("\u{1F50C} All tunnels stopped.", { parse_mode: "HTML" });
12478
12944
  } else {
12479
- const kb = new InlineKeyboard7();
12945
+ const kb = new InlineKeyboard8();
12480
12946
  for (const e of remaining2) {
12481
12947
  kb.text(`\u{1F50C} Stop ${e.port}${e.label ? ` (${e.label})` : ""}`, `tn:stop:${e.port}`).row();
12482
12948
  }
@@ -12513,7 +12979,7 @@ var init_tunnel4 = __esm({
12513
12979
  });
12514
12980
 
12515
12981
  // src/plugins/telegram/commands/switch.ts
12516
- import { InlineKeyboard as InlineKeyboard8 } from "grammy";
12982
+ import { InlineKeyboard as InlineKeyboard9 } from "grammy";
12517
12983
  async function executeSwitchAgent(ctx, core, sessionId, agentName) {
12518
12984
  try {
12519
12985
  const { resumed } = await core.switchSessionAgent(sessionId, agentName);
@@ -12540,7 +13006,7 @@ function setupSwitchCallbacks(bot, core) {
12540
13006
  return;
12541
13007
  }
12542
13008
  if (session.promptRunning) {
12543
- const keyboard = new InlineKeyboard8();
13009
+ const keyboard = new InlineKeyboard9();
12544
13010
  keyboard.text("Yes, switch now", `swc:${agentName}`).text("Cancel", "swc:cancel");
12545
13011
  await ctx.reply(
12546
13012
  `A prompt is currently running. Switching will interrupt it.
@@ -12580,10 +13046,35 @@ var init_switch = __esm({
12580
13046
  }
12581
13047
  });
12582
13048
 
13049
+ // src/plugins/telegram/commands/telegram-overrides.ts
13050
+ var TELEGRAM_OVERRIDES;
13051
+ var init_telegram_overrides = __esm({
13052
+ "src/plugins/telegram/commands/telegram-overrides.ts"() {
13053
+ "use strict";
13054
+ init_agents2();
13055
+ init_session();
13056
+ init_doctor2();
13057
+ init_admin();
13058
+ init_menu();
13059
+ TELEGRAM_OVERRIDES = {
13060
+ agents: (ctx, core) => handleAgents(ctx, core),
13061
+ sessions: (ctx, core) => handleTopics(ctx, core),
13062
+ doctor: (ctx) => handleDoctor(ctx),
13063
+ update: (ctx, core) => handleUpdate(ctx, core),
13064
+ restart: (ctx, core) => handleRestart(ctx, core),
13065
+ help: (ctx) => handleHelp(ctx),
13066
+ menu: (ctx, core) => {
13067
+ const menuRegistry = core.lifecycleManager?.serviceRegistry?.get("menu-registry");
13068
+ return handleMenu(ctx, menuRegistry);
13069
+ }
13070
+ };
13071
+ }
13072
+ });
13073
+
12583
13074
  // src/plugins/telegram/commands/integrate.ts
12584
- import { InlineKeyboard as InlineKeyboard9 } from "grammy";
13075
+ import { InlineKeyboard as InlineKeyboard10 } from "grammy";
12585
13076
  function buildAgentItemsKeyboard(agentName, items) {
12586
- const keyboard = new InlineKeyboard9();
13077
+ const keyboard = new InlineKeyboard10();
12587
13078
  for (const item of items) {
12588
13079
  const installed = item.isInstalled();
12589
13080
  keyboard.text(
@@ -12604,7 +13095,7 @@ function setupIntegrateCallbacks(bot, core) {
12604
13095
  if (data === "i:back") {
12605
13096
  const { listIntegrations: listIntegrations2 } = await Promise.resolve().then(() => (init_integrate(), integrate_exports));
12606
13097
  const agents = listIntegrations2();
12607
- const keyboard2 = new InlineKeyboard9();
13098
+ const keyboard2 = new InlineKeyboard10();
12608
13099
  for (const agent of agents) {
12609
13100
  keyboard2.text(`\u{1F916} ${agent}`, `i:agent:${agent}`).row();
12610
13101
  }
@@ -12697,16 +13188,19 @@ __export(assistant_exports, {
12697
13188
  redirectToAssistant: () => redirectToAssistant
12698
13189
  });
12699
13190
  function buildWelcomeMessage(ctx) {
12700
- const { activeCount, errorCount, totalCount, agents, defaultAgent } = ctx;
13191
+ const { activeCount, errorCount, totalCount, agents, defaultAgent, workspace } = ctx;
12701
13192
  const agentList = agents.map((a) => `${a}${a === defaultAgent ? " (default)" : ""}`).join(", ");
12702
13193
  if (totalCount === 0) {
12703
13194
  return `\u{1F44B} <b>OpenACP is ready!</b>
12704
13195
 
13196
+ \u{1F4C2} ${workspace}
13197
+
12705
13198
  No sessions yet. Tap \u{1F195} New Session to start, or ask me anything!`;
12706
13199
  }
12707
13200
  if (errorCount > 0) {
12708
13201
  return `\u{1F44B} <b>OpenACP is ready!</b>
12709
13202
 
13203
+ \u{1F4C2} ${workspace}
12710
13204
  \u{1F4CA} ${activeCount} active, ${errorCount} errors / ${totalCount} total
12711
13205
  \u26A0\uFE0F ${errorCount} session${errorCount > 1 ? "s have" : " has"} errors \u2014 ask me to check if you'd like.
12712
13206
 
@@ -12714,6 +13208,7 @@ Agents: ${agentList}`;
12714
13208
  }
12715
13209
  return `\u{1F44B} <b>OpenACP is ready!</b>
12716
13210
 
13211
+ \u{1F4C2} ${workspace}
12717
13212
  \u{1F4CA} ${activeCount} active / ${totalCount} total
12718
13213
  Agents: ${agentList}`;
12719
13214
  }
@@ -12748,6 +13243,7 @@ function setupAllCallbacks(bot, core, chatId, systemTopicIds, getAssistantSessio
12748
13243
  core.configManager.get().workspace.baseDir
12749
13244
  );
12750
13245
  });
13246
+ setupNewSessionCallbacks(bot, core, chatId, getAssistantSession);
12751
13247
  bot.callbackQuery(/^ar:/, (ctx) => handleArchiveConfirm(ctx, core, chatId));
12752
13248
  bot.callbackQuery(/^m:/, async (ctx) => {
12753
13249
  const itemId = ctx.callbackQuery.data.replace("m:", "");
@@ -12763,6 +13259,17 @@ function setupAllCallbacks(bot, core, chatId, systemTopicIds, getAssistantSessio
12763
13259
  const registry = core.lifecycleManager?.serviceRegistry?.get("command-registry");
12764
13260
  switch (item.action.type) {
12765
13261
  case "command": {
13262
+ const cmdName = item.action.command.replace(/^\//, "").split(" ")[0];
13263
+ const telegramOverride = TELEGRAM_OVERRIDES[cmdName];
13264
+ if (telegramOverride) {
13265
+ try {
13266
+ await telegramOverride(ctx, core);
13267
+ } catch (err) {
13268
+ await ctx.reply(`\u26A0\uFE0F Command failed: ${String(err)}`).catch(() => {
13269
+ });
13270
+ }
13271
+ break;
13272
+ }
12766
13273
  if (!registry) return;
12767
13274
  const response = await registry.execute(item.action.command, {
12768
13275
  raw: "",
@@ -12785,16 +13292,16 @@ function setupAllCallbacks(bot, core, chatId, systemTopicIds, getAssistantSessio
12785
13292
  ${lines}`, { parse_mode: "HTML" }).catch(() => {
12786
13293
  });
12787
13294
  } else if (response.type === "menu") {
12788
- const { InlineKeyboard: InlineKeyboard11 } = await import("grammy");
12789
- const kb = new InlineKeyboard11();
13295
+ const { InlineKeyboard: InlineKeyboard12 } = await import("grammy");
13296
+ const kb = new InlineKeyboard12();
12790
13297
  for (const opt of response.options) {
12791
13298
  kb.text(opt.label, `c/${opt.command}`).row();
12792
13299
  }
12793
13300
  await ctx.reply(response.title, { parse_mode: "HTML", reply_markup: kb }).catch(() => {
12794
13301
  });
12795
13302
  } else if (response.type === "confirm") {
12796
- const { InlineKeyboard: InlineKeyboard11 } = await import("grammy");
12797
- const kb = new InlineKeyboard11().text("\u2705 Yes", `c/${response.onYes}`).text("\u274C No", `c/${response.onNo}`);
13303
+ const { InlineKeyboard: InlineKeyboard12 } = await import("grammy");
13304
+ const kb = new InlineKeyboard12().text("\u2705 Yes", `c/${response.onYes}`).text("\u274C No", `c/${response.onNo}`);
12798
13305
  await ctx.reply(response.question, { reply_markup: kb }).catch(() => {
12799
13306
  });
12800
13307
  }
@@ -12802,6 +13309,10 @@ ${lines}`, { parse_mode: "HTML" }).catch(() => {
12802
13309
  break;
12803
13310
  }
12804
13311
  case "delegate": {
13312
+ if (itemId === "core:new") {
13313
+ await showAgentPicker(ctx, core, chatId);
13314
+ break;
13315
+ }
12805
13316
  const assistant = core.assistantManager?.get("telegram");
12806
13317
  if (assistant) {
12807
13318
  if (topicId && systemTopicIds && topicId !== systemTopicIds.assistantTopicId) {
@@ -12817,8 +13328,13 @@ ${lines}`, { parse_mode: "HTML" }).catch(() => {
12817
13328
  }
12818
13329
  break;
12819
13330
  }
12820
- case "callback":
13331
+ case "callback": {
13332
+ const cbData = item.action.callbackData;
13333
+ if (cbData === "s:settings") {
13334
+ await handleSettings(ctx, core);
13335
+ }
12821
13336
  break;
13337
+ }
12822
13338
  }
12823
13339
  });
12824
13340
  }
@@ -12834,8 +13350,10 @@ var init_commands3 = __esm({
12834
13350
  init_doctor2();
12835
13351
  init_tunnel4();
12836
13352
  init_switch();
13353
+ init_telegram_overrides();
12837
13354
  init_menu();
12838
13355
  init_menu();
13356
+ init_telegram_overrides();
12839
13357
  init_new_session();
12840
13358
  init_session();
12841
13359
  init_admin();
@@ -12877,7 +13395,7 @@ var init_commands3 = __esm({
12877
13395
  });
12878
13396
 
12879
13397
  // src/plugins/telegram/permissions.ts
12880
- import { InlineKeyboard as InlineKeyboard10 } from "grammy";
13398
+ import { InlineKeyboard as InlineKeyboard11 } from "grammy";
12881
13399
  import { nanoid as nanoid2 } from "nanoid";
12882
13400
  var log21, PermissionHandler;
12883
13401
  var init_permissions = __esm({
@@ -12903,7 +13421,7 @@ var init_permissions = __esm({
12903
13421
  requestId: request.id,
12904
13422
  options: request.options.map((o) => ({ id: o.id, isAllow: o.isAllow }))
12905
13423
  });
12906
- const keyboard = new InlineKeyboard10();
13424
+ const keyboard = new InlineKeyboard11();
12907
13425
  for (const option of request.options) {
12908
13426
  const emoji = option.isAllow ? "\u2705" : "\u274C";
12909
13427
  keyboard.text(`${emoji} ${option.label}`, `p:${callbackKey}:${option.id}`);
@@ -14545,6 +15063,7 @@ var init_adapter2 = __esm({
14545
15063
  init_log();
14546
15064
  init_topics2();
14547
15065
  init_commands3();
15066
+ init_telegram_overrides();
14548
15067
  init_admin();
14549
15068
  init_permissions();
14550
15069
  init_assistant();
@@ -14757,6 +15276,15 @@ var init_adapter2 = __esm({
14757
15276
  const commandName = atIdx === -1 ? rawCommand : rawCommand.slice(0, atIdx);
14758
15277
  const def = registry.get(commandName);
14759
15278
  if (!def) return next();
15279
+ const telegramOverride = TELEGRAM_OVERRIDES[commandName];
15280
+ if (telegramOverride) {
15281
+ try {
15282
+ await telegramOverride(ctx, this.core);
15283
+ } catch (err) {
15284
+ await ctx.reply(`\u26A0\uFE0F Command failed: ${String(err)}`);
15285
+ }
15286
+ return;
15287
+ }
14760
15288
  const chatId = ctx.chat.id;
14761
15289
  const topicId = ctx.message.message_thread_id;
14762
15290
  try {
@@ -14862,7 +15390,16 @@ var init_adapter2 = __esm({
14862
15390
  if (!assistant) return void 0;
14863
15391
  return {
14864
15392
  topicId: this.assistantTopicId,
14865
- enqueuePrompt: (p2) => assistant.enqueuePrompt(p2)
15393
+ enqueuePrompt: (p2) => {
15394
+ const pending = this.core.assistantManager?.consumePendingSystemPrompt("telegram");
15395
+ const text6 = pending ? `${pending}
15396
+
15397
+ ---
15398
+
15399
+ User message:
15400
+ ${p2}` : p2;
15401
+ return assistant.enqueuePrompt(text6);
15402
+ }
14866
15403
  };
14867
15404
  },
14868
15405
  (sessionId, msgId) => {
@@ -14923,7 +15460,8 @@ var init_adapter2 = __esm({
14923
15460
  errorCount: allRecords.filter((r) => r.status === "error").length,
14924
15461
  totalCount: allRecords.length,
14925
15462
  agents: agents.map((a) => a.name),
14926
- defaultAgent: config.defaultAgent
15463
+ defaultAgent: config.defaultAgent,
15464
+ workspace: this.core.configManager.resolveWorkspace()
14927
15465
  });
14928
15466
  await this.bot.api.sendMessage(this.telegramConfig.chatId, welcomeText, {
14929
15467
  message_thread_id: this.assistantTopicId,
@@ -16505,8 +17043,10 @@ __export(stop_exports, {
16505
17043
  import path36 from "path";
16506
17044
  import os17 from "os";
16507
17045
  async function cmdStop(args2 = [], instanceRoot) {
17046
+ const json = isJsonMode(args2);
17047
+ if (json) await muteForJson();
16508
17048
  const root = instanceRoot ?? path36.join(os17.homedir(), ".openacp");
16509
- if (wantsHelp(args2)) {
17049
+ if (!json && wantsHelp(args2)) {
16510
17050
  console.log(`
16511
17051
  \x1B[1mopenacp stop\x1B[0m \u2014 Stop the background daemon
16512
17052
 
@@ -16514,14 +17054,20 @@ async function cmdStop(args2 = [], instanceRoot) {
16514
17054
  openacp stop
16515
17055
 
16516
17056
  Sends a stop signal to the running OpenACP daemon process.
17057
+
17058
+ \x1B[1mOptions:\x1B[0m
17059
+ --json Output result as JSON
17060
+ -h, --help Show this help message
16517
17061
  `);
16518
17062
  return;
16519
17063
  }
16520
17064
  const { stopDaemon: stopDaemon2, getPidPath: getPidPath2 } = await Promise.resolve().then(() => (init_daemon2(), daemon_exports));
16521
17065
  const result = await stopDaemon2(getPidPath2(root), root);
16522
17066
  if (result.stopped) {
17067
+ if (json) jsonSuccess({ stopped: true, pid: result.pid });
16523
17068
  console.log(`OpenACP daemon stopped (was PID ${result.pid})`);
16524
17069
  } else {
17070
+ if (json) jsonError(ErrorCodes.DAEMON_NOT_RUNNING, result.error ?? "Daemon is not running.");
16525
17071
  console.error(result.error);
16526
17072
  process.exit(1);
16527
17073
  }
@@ -16530,6 +17076,80 @@ var init_stop = __esm({
16530
17076
  "src/cli/commands/stop.ts"() {
16531
17077
  "use strict";
16532
17078
  init_helpers();
17079
+ init_output();
17080
+ }
17081
+ });
17082
+
17083
+ // src/core/instance/instance-registry.ts
17084
+ var instance_registry_exports = {};
17085
+ __export(instance_registry_exports, {
17086
+ InstanceRegistry: () => InstanceRegistry
17087
+ });
17088
+ import fs34 from "fs";
17089
+ import path37 from "path";
17090
+ var InstanceRegistry;
17091
+ var init_instance_registry = __esm({
17092
+ "src/core/instance/instance-registry.ts"() {
17093
+ "use strict";
17094
+ InstanceRegistry = class {
17095
+ constructor(registryPath) {
17096
+ this.registryPath = registryPath;
17097
+ }
17098
+ data = { version: 1, instances: {} };
17099
+ load() {
17100
+ try {
17101
+ const raw = fs34.readFileSync(this.registryPath, "utf-8");
17102
+ const parsed = JSON.parse(raw);
17103
+ if (parsed.version === 1 && parsed.instances) {
17104
+ this.data = parsed;
17105
+ this.deduplicate();
17106
+ }
17107
+ } catch {
17108
+ }
17109
+ }
17110
+ /** Remove duplicate entries that point to the same root, keeping the first one */
17111
+ deduplicate() {
17112
+ const seen = /* @__PURE__ */ new Set();
17113
+ const toRemove = [];
17114
+ for (const [id, entry] of Object.entries(this.data.instances)) {
17115
+ if (seen.has(entry.root)) {
17116
+ toRemove.push(id);
17117
+ } else {
17118
+ seen.add(entry.root);
17119
+ }
17120
+ }
17121
+ if (toRemove.length > 0) {
17122
+ for (const id of toRemove) delete this.data.instances[id];
17123
+ this.save();
17124
+ }
17125
+ }
17126
+ save() {
17127
+ const dir = path37.dirname(this.registryPath);
17128
+ fs34.mkdirSync(dir, { recursive: true });
17129
+ fs34.writeFileSync(this.registryPath, JSON.stringify(this.data, null, 2));
17130
+ }
17131
+ register(id, root) {
17132
+ this.data.instances[id] = { id, root };
17133
+ }
17134
+ remove(id) {
17135
+ delete this.data.instances[id];
17136
+ }
17137
+ get(id) {
17138
+ return this.data.instances[id];
17139
+ }
17140
+ getByRoot(root) {
17141
+ return Object.values(this.data.instances).find((e) => e.root === root);
17142
+ }
17143
+ list() {
17144
+ return Object.values(this.data.instances);
17145
+ }
17146
+ uniqueId(baseId) {
17147
+ if (!this.data.instances[baseId]) return baseId;
17148
+ let n = 2;
17149
+ while (this.data.instances[`${baseId}-${n}`]) n++;
17150
+ return `${baseId}-${n}`;
17151
+ }
17152
+ };
16533
17153
  }
16534
17154
  });
16535
17155
 
@@ -16839,8 +17459,8 @@ var init_mcp_manager = __esm({
16839
17459
  });
16840
17460
 
16841
17461
  // src/core/utils/debug-tracer.ts
16842
- import fs34 from "fs";
16843
- import path37 from "path";
17462
+ import fs35 from "fs";
17463
+ import path38 from "path";
16844
17464
  function createDebugTracer(sessionId, workingDirectory) {
16845
17465
  if (!DEBUG_ENABLED) return null;
16846
17466
  return new DebugTracer(sessionId, workingDirectory);
@@ -16854,17 +17474,17 @@ var init_debug_tracer = __esm({
16854
17474
  constructor(sessionId, workingDirectory) {
16855
17475
  this.sessionId = sessionId;
16856
17476
  this.workingDirectory = workingDirectory;
16857
- this.logDir = path37.join(workingDirectory, ".log");
17477
+ this.logDir = path38.join(workingDirectory, ".log");
16858
17478
  }
16859
17479
  dirCreated = false;
16860
17480
  logDir;
16861
17481
  log(layer, data) {
16862
17482
  try {
16863
17483
  if (!this.dirCreated) {
16864
- fs34.mkdirSync(this.logDir, { recursive: true });
17484
+ fs35.mkdirSync(this.logDir, { recursive: true });
16865
17485
  this.dirCreated = true;
16866
17486
  }
16867
- const filePath = path37.join(this.logDir, `${this.sessionId}_${layer}.jsonl`);
17487
+ const filePath = path38.join(this.logDir, `${this.sessionId}_${layer}.jsonl`);
16868
17488
  const seen = /* @__PURE__ */ new WeakSet();
16869
17489
  const line = JSON.stringify({ ts: Date.now(), ...data }, (_key, value) => {
16870
17490
  if (typeof value === "object" && value !== null) {
@@ -16873,7 +17493,7 @@ var init_debug_tracer = __esm({
16873
17493
  }
16874
17494
  return value;
16875
17495
  }) + "\n";
16876
- fs34.appendFileSync(filePath, line);
17496
+ fs35.appendFileSync(filePath, line);
16877
17497
  } catch {
16878
17498
  }
16879
17499
  }
@@ -16887,17 +17507,17 @@ var init_debug_tracer = __esm({
16887
17507
  // src/core/agents/agent-instance.ts
16888
17508
  import { spawn as spawn7, execFileSync as execFileSync5 } from "child_process";
16889
17509
  import { Transform } from "stream";
16890
- import fs35 from "fs";
16891
- import path38 from "path";
17510
+ import fs36 from "fs";
17511
+ import path39 from "path";
16892
17512
  import { ClientSideConnection, ndJsonStream } from "@agentclientprotocol/sdk";
16893
17513
  import { PROTOCOL_VERSION } from "@agentclientprotocol/sdk";
16894
17514
  function findPackageRoot(startDir) {
16895
17515
  let dir = startDir;
16896
- while (dir !== path38.dirname(dir)) {
16897
- if (fs35.existsSync(path38.join(dir, "package.json"))) {
17516
+ while (dir !== path39.dirname(dir)) {
17517
+ if (fs36.existsSync(path39.join(dir, "package.json"))) {
16898
17518
  return dir;
16899
17519
  }
16900
- dir = path38.dirname(dir);
17520
+ dir = path39.dirname(dir);
16901
17521
  }
16902
17522
  return startDir;
16903
17523
  }
@@ -16909,26 +17529,26 @@ function resolveAgentCommand(cmd) {
16909
17529
  }
16910
17530
  for (const root of searchRoots) {
16911
17531
  const packageDirs = [
16912
- path38.resolve(root, "node_modules", "@zed-industries", cmd, "dist", "index.js"),
16913
- path38.resolve(root, "node_modules", cmd, "dist", "index.js")
17532
+ path39.resolve(root, "node_modules", "@zed-industries", cmd, "dist", "index.js"),
17533
+ path39.resolve(root, "node_modules", cmd, "dist", "index.js")
16914
17534
  ];
16915
17535
  for (const jsPath of packageDirs) {
16916
- if (fs35.existsSync(jsPath)) {
17536
+ if (fs36.existsSync(jsPath)) {
16917
17537
  return { command: process.execPath, args: [jsPath] };
16918
17538
  }
16919
17539
  }
16920
17540
  }
16921
17541
  for (const root of searchRoots) {
16922
- const localBin = path38.resolve(root, "node_modules", ".bin", cmd);
16923
- if (fs35.existsSync(localBin)) {
16924
- const content = fs35.readFileSync(localBin, "utf-8");
17542
+ const localBin = path39.resolve(root, "node_modules", ".bin", cmd);
17543
+ if (fs36.existsSync(localBin)) {
17544
+ const content = fs36.readFileSync(localBin, "utf-8");
16925
17545
  if (content.startsWith("#!/usr/bin/env node")) {
16926
17546
  return { command: process.execPath, args: [localBin] };
16927
17547
  }
16928
17548
  const match = content.match(/"([^"]+\.js)"/);
16929
17549
  if (match) {
16930
- const target = path38.resolve(path38.dirname(localBin), match[1]);
16931
- if (fs35.existsSync(target)) {
17550
+ const target = path39.resolve(path39.dirname(localBin), match[1]);
17551
+ if (fs36.existsSync(target)) {
16932
17552
  return { command: process.execPath, args: [target] };
16933
17553
  }
16934
17554
  }
@@ -16937,7 +17557,7 @@ function resolveAgentCommand(cmd) {
16937
17557
  try {
16938
17558
  const fullPath = execFileSync5("which", [cmd], { encoding: "utf-8" }).trim();
16939
17559
  if (fullPath) {
16940
- const content = fs35.readFileSync(fullPath, "utf-8");
17560
+ const content = fs36.readFileSync(fullPath, "utf-8");
16941
17561
  if (content.startsWith("#!/usr/bin/env node")) {
16942
17562
  return { command: process.execPath, args: [fullPath] };
16943
17563
  }
@@ -17328,8 +17948,8 @@ ${stderr}`
17328
17948
  writePath = result.path;
17329
17949
  writeContent = result.content;
17330
17950
  }
17331
- await fs35.promises.mkdir(path38.dirname(writePath), { recursive: true });
17332
- await fs35.promises.writeFile(writePath, writeContent, "utf-8");
17951
+ await fs36.promises.mkdir(path39.dirname(writePath), { recursive: true });
17952
+ await fs36.promises.writeFile(writePath, writeContent, "utf-8");
17333
17953
  return {};
17334
17954
  },
17335
17955
  // ── Terminal operations (delegated to TerminalManager) ─────────────
@@ -17404,10 +18024,10 @@ ${stderr}`
17404
18024
  for (const att of attachments ?? []) {
17405
18025
  const tooLarge = att.size > 10 * 1024 * 1024;
17406
18026
  if (att.type === "image" && this.promptCapabilities?.image && !tooLarge && SUPPORTED_IMAGE_MIMES.has(att.mimeType)) {
17407
- const data = await fs35.promises.readFile(att.filePath);
18027
+ const data = await fs36.promises.readFile(att.filePath);
17408
18028
  contentBlocks.push({ type: "image", data: data.toString("base64"), mimeType: att.mimeType });
17409
18029
  } else if (att.type === "audio" && this.promptCapabilities?.audio && !tooLarge) {
17410
- const data = await fs35.promises.readFile(att.filePath);
18030
+ const data = await fs36.promises.readFile(att.filePath);
17411
18031
  contentBlocks.push({ type: "audio", data: data.toString("base64"), mimeType: att.mimeType });
17412
18032
  } else {
17413
18033
  if ((att.type === "image" || att.type === "audio") && !tooLarge) {
@@ -17631,7 +18251,7 @@ var init_permission_gate = __esm({
17631
18251
 
17632
18252
  // src/core/sessions/session.ts
17633
18253
  import { nanoid as nanoid3 } from "nanoid";
17634
- import * as fs36 from "fs";
18254
+ import * as fs37 from "fs";
17635
18255
  var moduleLog, TTS_PROMPT_INSTRUCTION, TTS_BLOCK_REGEX, TTS_MAX_LENGTH, TTS_TIMEOUT_MS, VALID_TRANSITIONS, Session;
17636
18256
  var init_session2 = __esm({
17637
18257
  "src/core/sessions/session.ts"() {
@@ -17862,7 +18482,7 @@ ${text6}`;
17862
18482
  try {
17863
18483
  const audioPath = att.originalFilePath || att.filePath;
17864
18484
  const audioMime = att.originalFilePath ? "audio/ogg" : att.mimeType;
17865
- const audioBuffer = await fs36.promises.readFile(audioPath);
18485
+ const audioBuffer = await fs37.promises.readFile(audioPath);
17866
18486
  const result = await this.speechService.transcribe(audioBuffer, audioMime);
17867
18487
  this.log.info({ provider: "stt", duration: result.duration }, "Voice transcribed");
17868
18488
  this.emit("agent_event", {
@@ -18939,8 +19559,8 @@ var init_message_transformer = __esm({
18939
19559
  });
18940
19560
 
18941
19561
  // src/core/sessions/session-store.ts
18942
- import fs37 from "fs";
18943
- import path39 from "path";
19562
+ import fs38 from "fs";
19563
+ import path40 from "path";
18944
19564
  var log29, DEBOUNCE_MS2, JsonFileSessionStore;
18945
19565
  var init_session_store = __esm({
18946
19566
  "src/core/sessions/session-store.ts"() {
@@ -19016,9 +19636,9 @@ var init_session_store = __esm({
19016
19636
  version: 1,
19017
19637
  sessions: Object.fromEntries(this.records)
19018
19638
  };
19019
- const dir = path39.dirname(this.filePath);
19020
- if (!fs37.existsSync(dir)) fs37.mkdirSync(dir, { recursive: true });
19021
- fs37.writeFileSync(this.filePath, JSON.stringify(data, null, 2));
19639
+ const dir = path40.dirname(this.filePath);
19640
+ if (!fs38.existsSync(dir)) fs38.mkdirSync(dir, { recursive: true });
19641
+ fs38.writeFileSync(this.filePath, JSON.stringify(data, null, 2));
19022
19642
  }
19023
19643
  destroy() {
19024
19644
  if (this.debounceTimer) clearTimeout(this.debounceTimer);
@@ -19031,10 +19651,10 @@ var init_session_store = __esm({
19031
19651
  }
19032
19652
  }
19033
19653
  load() {
19034
- if (!fs37.existsSync(this.filePath)) return;
19654
+ if (!fs38.existsSync(this.filePath)) return;
19035
19655
  try {
19036
19656
  const raw = JSON.parse(
19037
- fs37.readFileSync(this.filePath, "utf-8")
19657
+ fs38.readFileSync(this.filePath, "utf-8")
19038
19658
  );
19039
19659
  if (raw.version !== 1) {
19040
19660
  log29.warn(
@@ -19050,7 +19670,7 @@ var init_session_store = __esm({
19050
19670
  } catch (err) {
19051
19671
  log29.error({ err }, "Failed to load session store, backing up corrupt file");
19052
19672
  try {
19053
- fs37.renameSync(this.filePath, `${this.filePath}.bak`);
19673
+ fs38.renameSync(this.filePath, `${this.filePath}.bak`);
19054
19674
  } catch {
19055
19675
  }
19056
19676
  }
@@ -19590,8 +20210,8 @@ var agent_store_exports = {};
19590
20210
  __export(agent_store_exports, {
19591
20211
  AgentStore: () => AgentStore
19592
20212
  });
19593
- import * as fs38 from "fs";
19594
- import * as path40 from "path";
20213
+ import * as fs39 from "fs";
20214
+ import * as path41 from "path";
19595
20215
  import * as os18 from "os";
19596
20216
  import { z as z7 } from "zod";
19597
20217
  var log32, InstalledAgentSchema, AgentStoreSchema, AgentStore;
@@ -19620,15 +20240,15 @@ var init_agent_store = __esm({
19620
20240
  data = { version: 1, installed: {} };
19621
20241
  filePath;
19622
20242
  constructor(filePath) {
19623
- this.filePath = filePath ?? path40.join(os18.homedir(), ".openacp", "agents.json");
20243
+ this.filePath = filePath ?? path41.join(os18.homedir(), ".openacp", "agents.json");
19624
20244
  }
19625
20245
  load() {
19626
- if (!fs38.existsSync(this.filePath)) {
20246
+ if (!fs39.existsSync(this.filePath)) {
19627
20247
  this.data = { version: 1, installed: {} };
19628
20248
  return;
19629
20249
  }
19630
20250
  try {
19631
- const raw = JSON.parse(fs38.readFileSync(this.filePath, "utf-8"));
20251
+ const raw = JSON.parse(fs39.readFileSync(this.filePath, "utf-8"));
19632
20252
  const result = AgentStoreSchema.safeParse(raw);
19633
20253
  if (result.success) {
19634
20254
  this.data = result.data;
@@ -19642,7 +20262,7 @@ var init_agent_store = __esm({
19642
20262
  }
19643
20263
  }
19644
20264
  exists() {
19645
- return fs38.existsSync(this.filePath);
20265
+ return fs39.existsSync(this.filePath);
19646
20266
  }
19647
20267
  getInstalled() {
19648
20268
  return this.data.installed;
@@ -19662,18 +20282,18 @@ var init_agent_store = __esm({
19662
20282
  return key in this.data.installed;
19663
20283
  }
19664
20284
  save() {
19665
- fs38.mkdirSync(path40.dirname(this.filePath), { recursive: true });
20285
+ fs39.mkdirSync(path41.dirname(this.filePath), { recursive: true });
19666
20286
  const tmpPath = this.filePath + ".tmp";
19667
- fs38.writeFileSync(tmpPath, JSON.stringify(this.data, null, 2));
19668
- fs38.renameSync(tmpPath, this.filePath);
20287
+ fs39.writeFileSync(tmpPath, JSON.stringify(this.data, null, 2));
20288
+ fs39.renameSync(tmpPath, this.filePath);
19669
20289
  }
19670
20290
  };
19671
20291
  }
19672
20292
  });
19673
20293
 
19674
20294
  // src/core/agents/agent-installer.ts
19675
- import * as fs39 from "fs";
19676
- import * as path41 from "path";
20295
+ import * as fs40 from "fs";
20296
+ import * as path42 from "path";
19677
20297
  import * as os19 from "os";
19678
20298
  function getPlatformKey() {
19679
20299
  const platform2 = PLATFORM_MAP[process.platform] ?? process.platform;
@@ -19725,7 +20345,7 @@ function buildInstalledAgent(registryId, name, version, dist, binaryPath) {
19725
20345
  binaryPath: null
19726
20346
  };
19727
20347
  }
19728
- const absCmd = path41.resolve(binaryPath, dist.cmd);
20348
+ const absCmd = path42.resolve(binaryPath, dist.cmd);
19729
20349
  return {
19730
20350
  registryId,
19731
20351
  name,
@@ -19798,8 +20418,8 @@ Install it with: pip install uv`;
19798
20418
  return { ok: true, agentKey, setupSteps: setup?.setupSteps };
19799
20419
  }
19800
20420
  async function downloadAndExtract(agentId, archiveUrl, progress, agentsDir) {
19801
- const destDir = path41.join(agentsDir ?? DEFAULT_AGENTS_DIR, agentId);
19802
- fs39.mkdirSync(destDir, { recursive: true });
20421
+ const destDir = path42.join(agentsDir ?? DEFAULT_AGENTS_DIR, agentId);
20422
+ fs40.mkdirSync(destDir, { recursive: true });
19803
20423
  await progress?.onStep("Downloading...");
19804
20424
  log33.info({ agentId, url: archiveUrl }, "Downloading agent binary");
19805
20425
  const response = await fetch(archiveUrl);
@@ -19837,52 +20457,52 @@ async function readResponseWithProgress(response, contentLength, progress) {
19837
20457
  return Buffer.concat(chunks);
19838
20458
  }
19839
20459
  function validateExtractedPaths(destDir) {
19840
- const realDest = fs39.realpathSync(destDir);
19841
- const entries = fs39.readdirSync(destDir, { recursive: true, withFileTypes: true });
20460
+ const realDest = fs40.realpathSync(destDir);
20461
+ const entries = fs40.readdirSync(destDir, { recursive: true, withFileTypes: true });
19842
20462
  for (const entry of entries) {
19843
20463
  const dirent = entry;
19844
20464
  const parentPath = dirent.parentPath ?? dirent.path ?? destDir;
19845
- const fullPath = path41.join(parentPath, entry.name);
20465
+ const fullPath = path42.join(parentPath, entry.name);
19846
20466
  let realPath;
19847
20467
  try {
19848
- realPath = fs39.realpathSync(fullPath);
20468
+ realPath = fs40.realpathSync(fullPath);
19849
20469
  } catch {
19850
- const linkTarget = fs39.readlinkSync(fullPath);
19851
- realPath = path41.resolve(path41.dirname(fullPath), linkTarget);
20470
+ const linkTarget = fs40.readlinkSync(fullPath);
20471
+ realPath = path42.resolve(path42.dirname(fullPath), linkTarget);
19852
20472
  }
19853
- if (!realPath.startsWith(realDest + path41.sep) && realPath !== realDest) {
19854
- fs39.rmSync(destDir, { recursive: true, force: true });
20473
+ if (!realPath.startsWith(realDest + path42.sep) && realPath !== realDest) {
20474
+ fs40.rmSync(destDir, { recursive: true, force: true });
19855
20475
  throw new Error(`Archive contains unsafe path: ${entry.name}`);
19856
20476
  }
19857
20477
  }
19858
20478
  }
19859
20479
  async function extractTarGz(buffer, destDir) {
19860
20480
  const { execFileSync: execFileSync8 } = await import("child_process");
19861
- const tmpFile = path41.join(destDir, "_archive.tar.gz");
19862
- fs39.writeFileSync(tmpFile, buffer);
20481
+ const tmpFile = path42.join(destDir, "_archive.tar.gz");
20482
+ fs40.writeFileSync(tmpFile, buffer);
19863
20483
  try {
19864
20484
  execFileSync8("tar", ["xzf", tmpFile, "-C", destDir], { stdio: "pipe" });
19865
20485
  } finally {
19866
- fs39.unlinkSync(tmpFile);
20486
+ fs40.unlinkSync(tmpFile);
19867
20487
  }
19868
20488
  validateExtractedPaths(destDir);
19869
20489
  }
19870
20490
  async function extractZip(buffer, destDir) {
19871
20491
  const { execFileSync: execFileSync8 } = await import("child_process");
19872
- const tmpFile = path41.join(destDir, "_archive.zip");
19873
- fs39.writeFileSync(tmpFile, buffer);
20492
+ const tmpFile = path42.join(destDir, "_archive.zip");
20493
+ fs40.writeFileSync(tmpFile, buffer);
19874
20494
  try {
19875
20495
  execFileSync8("unzip", ["-o", tmpFile, "-d", destDir], { stdio: "pipe" });
19876
20496
  } finally {
19877
- fs39.unlinkSync(tmpFile);
20497
+ fs40.unlinkSync(tmpFile);
19878
20498
  }
19879
20499
  validateExtractedPaths(destDir);
19880
20500
  }
19881
20501
  async function uninstallAgent(agentKey, store) {
19882
20502
  const agent = store.getAgent(agentKey);
19883
20503
  if (!agent) return;
19884
- if (agent.binaryPath && fs39.existsSync(agent.binaryPath)) {
19885
- fs39.rmSync(agent.binaryPath, { recursive: true, force: true });
20504
+ if (agent.binaryPath && fs40.existsSync(agent.binaryPath)) {
20505
+ fs40.rmSync(agent.binaryPath, { recursive: true, force: true });
19886
20506
  log33.info({ agentKey, binaryPath: agent.binaryPath }, "Deleted agent binary");
19887
20507
  }
19888
20508
  store.removeAgent(agentKey);
@@ -19894,7 +20514,7 @@ var init_agent_installer = __esm({
19894
20514
  init_log();
19895
20515
  init_agent_dependencies();
19896
20516
  log33 = createChildLogger({ module: "agent-installer" });
19897
- DEFAULT_AGENTS_DIR = path41.join(os19.homedir(), ".openacp", "agents");
20517
+ DEFAULT_AGENTS_DIR = path42.join(os19.homedir(), ".openacp", "agents");
19898
20518
  ARCH_MAP = {
19899
20519
  arm64: "aarch64",
19900
20520
  x64: "x86_64"
@@ -19912,8 +20532,8 @@ var agent_catalog_exports = {};
19912
20532
  __export(agent_catalog_exports, {
19913
20533
  AgentCatalog: () => AgentCatalog
19914
20534
  });
19915
- import * as fs40 from "fs";
19916
- import * as path42 from "path";
20535
+ import * as fs41 from "fs";
20536
+ import * as path43 from "path";
19917
20537
  import * as os20 from "os";
19918
20538
  var log34, REGISTRY_URL2, DEFAULT_TTL_HOURS, AgentCatalog;
19919
20539
  var init_agent_catalog = __esm({
@@ -19933,7 +20553,7 @@ var init_agent_catalog = __esm({
19933
20553
  agentsDir;
19934
20554
  constructor(store, cachePath, agentsDir) {
19935
20555
  this.store = store ?? new AgentStore();
19936
- this.cachePath = cachePath ?? path42.join(os20.homedir(), ".openacp", "registry-cache.json");
20556
+ this.cachePath = cachePath ?? path43.join(os20.homedir(), ".openacp", "registry-cache.json");
19937
20557
  this.agentsDir = agentsDir;
19938
20558
  }
19939
20559
  load() {
@@ -19954,8 +20574,8 @@ var init_agent_catalog = __esm({
19954
20574
  ttlHours: DEFAULT_TTL_HOURS,
19955
20575
  data
19956
20576
  };
19957
- fs40.mkdirSync(path42.dirname(this.cachePath), { recursive: true });
19958
- fs40.writeFileSync(this.cachePath, JSON.stringify(cache, null, 2));
20577
+ fs41.mkdirSync(path43.dirname(this.cachePath), { recursive: true });
20578
+ fs41.writeFileSync(this.cachePath, JSON.stringify(cache, null, 2));
19959
20579
  log34.info({ count: this.registryAgents.length }, "Registry updated");
19960
20580
  } catch (err) {
19961
20581
  log34.warn({ err }, "Failed to fetch registry, using cached data");
@@ -20115,9 +20735,9 @@ var init_agent_catalog = __esm({
20115
20735
  }
20116
20736
  }
20117
20737
  isCacheStale() {
20118
- if (!fs40.existsSync(this.cachePath)) return true;
20738
+ if (!fs41.existsSync(this.cachePath)) return true;
20119
20739
  try {
20120
- const raw = JSON.parse(fs40.readFileSync(this.cachePath, "utf-8"));
20740
+ const raw = JSON.parse(fs41.readFileSync(this.cachePath, "utf-8"));
20121
20741
  const fetchedAt = new Date(raw.fetchedAt).getTime();
20122
20742
  const ttlMs = (raw.ttlHours ?? DEFAULT_TTL_HOURS) * 60 * 60 * 1e3;
20123
20743
  return Date.now() - fetchedAt > ttlMs;
@@ -20126,9 +20746,9 @@ var init_agent_catalog = __esm({
20126
20746
  }
20127
20747
  }
20128
20748
  loadRegistryFromCacheOrSnapshot() {
20129
- if (fs40.existsSync(this.cachePath)) {
20749
+ if (fs41.existsSync(this.cachePath)) {
20130
20750
  try {
20131
- const raw = JSON.parse(fs40.readFileSync(this.cachePath, "utf-8"));
20751
+ const raw = JSON.parse(fs41.readFileSync(this.cachePath, "utf-8"));
20132
20752
  if (raw.data?.agents) {
20133
20753
  this.registryAgents = raw.data.agents;
20134
20754
  log34.debug({ count: this.registryAgents.length }, "Loaded registry from cache");
@@ -20140,13 +20760,13 @@ var init_agent_catalog = __esm({
20140
20760
  }
20141
20761
  try {
20142
20762
  const candidates = [
20143
- path42.join(import.meta.dirname, "data", "registry-snapshot.json"),
20144
- path42.join(import.meta.dirname, "..", "data", "registry-snapshot.json"),
20145
- path42.join(import.meta.dirname, "..", "..", "data", "registry-snapshot.json")
20763
+ path43.join(import.meta.dirname, "data", "registry-snapshot.json"),
20764
+ path43.join(import.meta.dirname, "..", "data", "registry-snapshot.json"),
20765
+ path43.join(import.meta.dirname, "..", "..", "data", "registry-snapshot.json")
20146
20766
  ];
20147
20767
  for (const candidate of candidates) {
20148
- if (fs40.existsSync(candidate)) {
20149
- const raw = JSON.parse(fs40.readFileSync(candidate, "utf-8"));
20768
+ if (fs41.existsSync(candidate)) {
20769
+ const raw = JSON.parse(fs41.readFileSync(candidate, "utf-8"));
20150
20770
  this.registryAgents = raw.agents ?? [];
20151
20771
  log34.debug({ count: this.registryAgents.length }, "Loaded registry from bundled snapshot");
20152
20772
  return;
@@ -20439,8 +21059,8 @@ var init_error_tracker = __esm({
20439
21059
  });
20440
21060
 
20441
21061
  // src/core/plugin/plugin-storage.ts
20442
- import fs41 from "fs";
20443
- import path43 from "path";
21062
+ import fs42 from "fs";
21063
+ import path44 from "path";
20444
21064
  var PluginStorageImpl;
20445
21065
  var init_plugin_storage = __esm({
20446
21066
  "src/core/plugin/plugin-storage.ts"() {
@@ -20450,20 +21070,20 @@ var init_plugin_storage = __esm({
20450
21070
  dataDir;
20451
21071
  writeChain = Promise.resolve();
20452
21072
  constructor(baseDir) {
20453
- this.dataDir = path43.join(baseDir, "data");
20454
- this.kvPath = path43.join(baseDir, "kv.json");
20455
- fs41.mkdirSync(baseDir, { recursive: true });
21073
+ this.dataDir = path44.join(baseDir, "data");
21074
+ this.kvPath = path44.join(baseDir, "kv.json");
21075
+ fs42.mkdirSync(baseDir, { recursive: true });
20456
21076
  }
20457
21077
  readKv() {
20458
21078
  try {
20459
- const raw = fs41.readFileSync(this.kvPath, "utf-8");
21079
+ const raw = fs42.readFileSync(this.kvPath, "utf-8");
20460
21080
  return JSON.parse(raw);
20461
21081
  } catch {
20462
21082
  return {};
20463
21083
  }
20464
21084
  }
20465
21085
  writeKv(data) {
20466
- fs41.writeFileSync(this.kvPath, JSON.stringify(data), "utf-8");
21086
+ fs42.writeFileSync(this.kvPath, JSON.stringify(data), "utf-8");
20467
21087
  }
20468
21088
  async get(key) {
20469
21089
  const data = this.readKv();
@@ -20489,7 +21109,7 @@ var init_plugin_storage = __esm({
20489
21109
  return Object.keys(this.readKv());
20490
21110
  }
20491
21111
  getDataDir() {
20492
- fs41.mkdirSync(this.dataDir, { recursive: true });
21112
+ fs42.mkdirSync(this.dataDir, { recursive: true });
20493
21113
  return this.dataDir;
20494
21114
  }
20495
21115
  };
@@ -20497,7 +21117,7 @@ var init_plugin_storage = __esm({
20497
21117
  });
20498
21118
 
20499
21119
  // src/core/plugin/plugin-context.ts
20500
- import path44 from "path";
21120
+ import path45 from "path";
20501
21121
  import os21 from "os";
20502
21122
  function requirePermission(permissions, required, action) {
20503
21123
  if (!permissions.includes(required)) {
@@ -20517,7 +21137,7 @@ function createPluginContext(opts) {
20517
21137
  config,
20518
21138
  core
20519
21139
  } = opts;
20520
- const instanceRoot = opts.instanceRoot ?? path44.join(os21.homedir(), ".openacp");
21140
+ const instanceRoot = opts.instanceRoot ?? path45.join(os21.homedir(), ".openacp");
20521
21141
  const registeredListeners = [];
20522
21142
  const registeredCommands = [];
20523
21143
  const noopLog = {
@@ -21105,8 +21725,8 @@ var init_assistant_manager = __esm({
21105
21725
  this.registry = registry;
21106
21726
  }
21107
21727
  sessions = /* @__PURE__ */ new Map();
21108
- readyState = /* @__PURE__ */ new Map();
21109
21728
  respawning = /* @__PURE__ */ new Set();
21729
+ pendingSystemPrompts = /* @__PURE__ */ new Map();
21110
21730
  async spawn(channelId, threadId) {
21111
21731
  const session = await this.core.createSession({
21112
21732
  channelId,
@@ -21118,17 +21738,22 @@ var init_assistant_manager = __esm({
21118
21738
  session.threadId = threadId;
21119
21739
  this.sessions.set(channelId, session);
21120
21740
  const systemPrompt = this.registry.buildSystemPrompt(channelId);
21121
- const ready = session.enqueuePrompt(systemPrompt).then(() => {
21122
- log37.info({ sessionId: session.id, channelId }, "Assistant ready");
21123
- }).catch((err) => {
21124
- log37.warn({ err, channelId }, "Assistant system prompt failed");
21125
- });
21126
- this.readyState.set(channelId, ready);
21741
+ this.pendingSystemPrompts.set(channelId, systemPrompt);
21742
+ log37.info({ sessionId: session.id, channelId }, "Assistant spawned (system prompt deferred)");
21127
21743
  return session;
21128
21744
  }
21129
21745
  get(channelId) {
21130
21746
  return this.sessions.get(channelId) ?? null;
21131
21747
  }
21748
+ /**
21749
+ * Consume and return any pending system prompt for a channel.
21750
+ * Should be prepended to the first real user message.
21751
+ */
21752
+ consumePendingSystemPrompt(channelId) {
21753
+ const prompt = this.pendingSystemPrompts.get(channelId);
21754
+ if (prompt) this.pendingSystemPrompts.delete(channelId);
21755
+ return prompt;
21756
+ }
21132
21757
  isAssistant(sessionId) {
21133
21758
  for (const s of this.sessions.values()) {
21134
21759
  if (s.id === sessionId) return true;
@@ -21148,9 +21773,6 @@ var init_assistant_manager = __esm({
21148
21773
  this.respawning.delete(channelId);
21149
21774
  }
21150
21775
  }
21151
- async waitReady(channelId) {
21152
- await this.readyState.get(channelId);
21153
- }
21154
21776
  };
21155
21777
  }
21156
21778
  });
@@ -21358,7 +21980,7 @@ var init_core_items = __esm({
21358
21980
  });
21359
21981
 
21360
21982
  // src/core/core.ts
21361
- import path45 from "path";
21983
+ import path46 from "path";
21362
21984
  import os22 from "os";
21363
21985
  var log38, OpenACPCore;
21364
21986
  var init_core = __esm({
@@ -21438,7 +22060,7 @@ var init_core = __esm({
21438
22060
  );
21439
22061
  this.agentCatalog.load();
21440
22062
  this.agentManager = new AgentManager(this.agentCatalog);
21441
- const storePath = ctx?.paths.sessions ?? path45.join(os22.homedir(), ".openacp", "sessions.json");
22063
+ const storePath = ctx?.paths.sessions ?? path46.join(os22.homedir(), ".openacp", "sessions.json");
21442
22064
  this.sessionStore = new JsonFileSessionStore(
21443
22065
  storePath,
21444
22066
  config.sessionStore.ttlDays
@@ -21462,7 +22084,7 @@ var init_core = __esm({
21462
22084
  sessions: this.sessionManager,
21463
22085
  config: this.configManager,
21464
22086
  core: this,
21465
- storagePath: ctx?.paths.pluginsData ?? path45.join(os22.homedir(), ".openacp", "plugins", "data"),
22087
+ storagePath: ctx?.paths.pluginsData ?? path46.join(os22.homedir(), ".openacp", "plugins", "data"),
21466
22088
  instanceRoot: ctx?.root,
21467
22089
  log: createChildLogger({ module: "plugin" })
21468
22090
  });
@@ -21509,7 +22131,7 @@ var init_core = __esm({
21509
22131
  );
21510
22132
  registerCoreMenuItems(this.menuRegistry);
21511
22133
  if (ctx?.root) {
21512
- this.assistantRegistry.setInstanceRoot(path45.dirname(ctx.root));
22134
+ this.assistantRegistry.setInstanceRoot(path46.dirname(ctx.root));
21513
22135
  }
21514
22136
  this.assistantRegistry.register(createSessionsSection(this));
21515
22137
  this.assistantRegistry.register(createAgentsSection(this));
@@ -21635,7 +22257,19 @@ var init_core = __esm({
21635
22257
  this.sessionManager.patchRecord(session.id, {
21636
22258
  lastActiveAt: (/* @__PURE__ */ new Date()).toISOString()
21637
22259
  });
21638
- await session.enqueuePrompt(message.text, message.attachments);
22260
+ let text6 = message.text;
22261
+ if (this.assistantManager?.isAssistant(session.id)) {
22262
+ const pending = this.assistantManager.consumePendingSystemPrompt(message.channelId);
22263
+ if (pending) {
22264
+ text6 = `${pending}
22265
+
22266
+ ---
22267
+
22268
+ User message:
22269
+ ${text6}`;
22270
+ }
22271
+ }
22272
+ await session.enqueuePrompt(text6, message.attachments);
21639
22273
  }
21640
22274
  // --- Unified Session Creation Pipeline ---
21641
22275
  async createSession(params) {
@@ -22775,79 +23409,6 @@ var init_commands4 = __esm({
22775
23409
  }
22776
23410
  });
22777
23411
 
22778
- // src/core/instance/instance-registry.ts
22779
- var instance_registry_exports = {};
22780
- __export(instance_registry_exports, {
22781
- InstanceRegistry: () => InstanceRegistry
22782
- });
22783
- import fs42 from "fs";
22784
- import path46 from "path";
22785
- var InstanceRegistry;
22786
- var init_instance_registry = __esm({
22787
- "src/core/instance/instance-registry.ts"() {
22788
- "use strict";
22789
- InstanceRegistry = class {
22790
- constructor(registryPath) {
22791
- this.registryPath = registryPath;
22792
- }
22793
- data = { version: 1, instances: {} };
22794
- load() {
22795
- try {
22796
- const raw = fs42.readFileSync(this.registryPath, "utf-8");
22797
- const parsed = JSON.parse(raw);
22798
- if (parsed.version === 1 && parsed.instances) {
22799
- this.data = parsed;
22800
- this.deduplicate();
22801
- }
22802
- } catch {
22803
- }
22804
- }
22805
- /** Remove duplicate entries that point to the same root, keeping the first one */
22806
- deduplicate() {
22807
- const seen = /* @__PURE__ */ new Set();
22808
- const toRemove = [];
22809
- for (const [id, entry] of Object.entries(this.data.instances)) {
22810
- if (seen.has(entry.root)) {
22811
- toRemove.push(id);
22812
- } else {
22813
- seen.add(entry.root);
22814
- }
22815
- }
22816
- if (toRemove.length > 0) {
22817
- for (const id of toRemove) delete this.data.instances[id];
22818
- this.save();
22819
- }
22820
- }
22821
- save() {
22822
- const dir = path46.dirname(this.registryPath);
22823
- fs42.mkdirSync(dir, { recursive: true });
22824
- fs42.writeFileSync(this.registryPath, JSON.stringify(this.data, null, 2));
22825
- }
22826
- register(id, root) {
22827
- this.data.instances[id] = { id, root };
22828
- }
22829
- remove(id) {
22830
- delete this.data.instances[id];
22831
- }
22832
- get(id) {
22833
- return this.data.instances[id];
22834
- }
22835
- getByRoot(root) {
22836
- return Object.values(this.data.instances).find((e) => e.root === root);
22837
- }
22838
- list() {
22839
- return Object.values(this.data.instances);
22840
- }
22841
- uniqueId(baseId) {
22842
- if (!this.data.instances[baseId]) return baseId;
22843
- let n = 2;
22844
- while (this.data.instances[`${baseId}-${n}`]) n++;
22845
- return `${baseId}-${n}`;
22846
- }
22847
- };
22848
- }
22849
- });
22850
-
22851
23412
  // src/core/setup/types.ts
22852
23413
  var ONBOARD_SECTION_OPTIONS, CHANNEL_META;
22853
23414
  var init_types = __esm({
@@ -23701,6 +24262,7 @@ import * as path51 from "path";
23701
24262
  import * as fs46 from "fs";
23702
24263
  import * as os24 from "os";
23703
24264
  import * as clack8 from "@clack/prompts";
24265
+ import { randomUUID as randomUUID2 } from "crypto";
23704
24266
  async function fetchCommunityAdapters() {
23705
24267
  try {
23706
24268
  const client = new RegistryClient();
@@ -24069,7 +24631,7 @@ async function runSetup(configManager, opts) {
24069
24631
  }
24070
24632
  const existingEntry = instanceRegistry.getByRoot(instanceRoot);
24071
24633
  if (!existingEntry) {
24072
- const id = instanceRegistry.uniqueId(generateSlug(instanceName));
24634
+ const id = randomUUID2();
24073
24635
  instanceRegistry.register(id, instanceRoot);
24074
24636
  await instanceRegistry.save();
24075
24637
  }
@@ -24408,13 +24970,17 @@ __export(main_exports, {
24408
24970
  startServer: () => startServer
24409
24971
  });
24410
24972
  import path53 from "path";
24973
+ import { randomUUID as randomUUID3 } from "crypto";
24411
24974
  import fs48 from "fs";
24412
24975
  async function startServer(opts) {
24413
- const ctx = opts?.instanceContext ?? createInstanceContext({
24414
- id: "main",
24415
- root: getGlobalRoot(),
24416
- isGlobal: true
24417
- });
24976
+ const globalRoot = getGlobalRoot();
24977
+ if (!opts?.instanceContext) {
24978
+ const reg = new InstanceRegistry(path53.join(globalRoot, "instances.json"));
24979
+ reg.load();
24980
+ const entry = reg.getByRoot(globalRoot);
24981
+ opts = { ...opts, instanceContext: createInstanceContext({ id: entry?.id ?? randomUUID3(), root: globalRoot, isGlobal: true }) };
24982
+ }
24983
+ const ctx = opts.instanceContext;
24418
24984
  if (process.argv.includes("--daemon-child")) {
24419
24985
  const { writePidFile: writePidFile2, readPidFile: readPidFile2, shouldAutoStart: shouldAutoStart2 } = await Promise.resolve().then(() => (init_daemon2(), daemon_exports));
24420
24986
  console.error(`[startup] Daemon child starting (pid=${process.pid}, root=${ctx.root}, env.OPENACP_INSTANCE_ROOT=${process.env.OPENACP_INSTANCE_ROOT ?? "unset"})`);
@@ -24689,8 +25255,8 @@ async function startServer(opts) {
24689
25255
  });
24690
25256
  await core.start();
24691
25257
  try {
24692
- const globalRoot = getGlobalRoot();
24693
- const registryPath = path53.join(globalRoot, "instances.json");
25258
+ const globalRoot2 = getGlobalRoot();
25259
+ const registryPath = path53.join(globalRoot2, "instances.json");
24694
25260
  const instanceReg = new InstanceRegistry(registryPath);
24695
25261
  await instanceReg.load();
24696
25262
  if (!instanceReg.getByRoot(ctx.root)) {
@@ -24855,9 +25421,12 @@ __export(restart_exports, {
24855
25421
  });
24856
25422
  import path54 from "path";
24857
25423
  import os25 from "os";
25424
+ import { randomUUID as randomUUID4 } from "crypto";
24858
25425
  async function cmdRestart(args2 = [], instanceRoot) {
25426
+ const json = isJsonMode(args2);
25427
+ if (json) await muteForJson();
24859
25428
  const root = instanceRoot ?? path54.join(os25.homedir(), ".openacp");
24860
- if (wantsHelp(args2)) {
25429
+ if (!json && wantsHelp(args2)) {
24861
25430
  console.log(`
24862
25431
  \x1B[1mopenacp restart\x1B[0m \u2014 Restart the background daemon
24863
25432
 
@@ -24868,6 +25437,10 @@ async function cmdRestart(args2 = [], instanceRoot) {
24868
25437
 
24869
25438
  Stops the running daemon (if any) and starts a new one.
24870
25439
 
25440
+ \x1B[1mOptions:\x1B[0m
25441
+ --json Output result as JSON
25442
+ -h, --help Show this help message
25443
+
24871
25444
  \x1B[1mSee also:\x1B[0m
24872
25445
  openacp start Start the daemon
24873
25446
  openacp stop Stop the daemon
@@ -24888,19 +25461,23 @@ Stops the running daemon (if any) and starts a new one.
24888
25461
  }
24889
25462
  const cm = new ConfigManager2();
24890
25463
  if (!await cm.exists()) {
25464
+ if (json) jsonError(ErrorCodes.CONFIG_NOT_FOUND, 'No config found. Run "openacp" first to set up.');
24891
25465
  console.error('No config found. Run "openacp" first to set up.');
24892
25466
  process.exit(1);
24893
25467
  }
24894
25468
  await cm.load();
24895
25469
  const config = cm.get();
24896
- const useForeground = forceForeground || !forceDaemon && config.runMode !== "daemon";
25470
+ const useForeground = json ? false : forceForeground || !forceDaemon && config.runMode !== "daemon";
24897
25471
  if (useForeground) {
24898
25472
  markRunning2(root);
24899
25473
  printInstanceHint(root);
24900
25474
  console.log("Starting in foreground mode...");
24901
25475
  const { startServer: startServer2 } = await Promise.resolve().then(() => (init_main(), main_exports));
25476
+ const reg = new InstanceRegistry(path54.join(getGlobalRoot(), "instances.json"));
25477
+ reg.load();
25478
+ const existingEntry = reg.getByRoot(root);
24902
25479
  const ctx = createInstanceContext({
24903
- id: "default",
25480
+ id: existingEntry?.id ?? randomUUID4(),
24904
25481
  root,
24905
25482
  isGlobal: root === getGlobalRoot()
24906
25483
  });
@@ -24908,9 +25485,11 @@ Stops the running daemon (if any) and starts a new one.
24908
25485
  } else {
24909
25486
  const result = startDaemon2(pidPath, config.logging.logDir, root);
24910
25487
  if ("error" in result) {
25488
+ if (json) jsonError(ErrorCodes.DAEMON_NOT_RUNNING, result.error);
24911
25489
  console.error(result.error);
24912
25490
  process.exit(1);
24913
25491
  }
25492
+ if (json) jsonSuccess({ pid: result.pid, instanceId: path54.basename(root), dir: root });
24914
25493
  printInstanceHint(root);
24915
25494
  console.log(`OpenACP daemon started (PID ${result.pid})`);
24916
25495
  }
@@ -24919,8 +25498,10 @@ var init_restart = __esm({
24919
25498
  "src/cli/commands/restart.ts"() {
24920
25499
  "use strict";
24921
25500
  init_helpers();
25501
+ init_output();
24922
25502
  init_instance_hint();
24923
25503
  init_instance_context();
25504
+ init_instance_registry();
24924
25505
  }
24925
25506
  });
24926
25507
 
@@ -24935,23 +25516,43 @@ import fs49 from "fs";
24935
25516
  import path55 from "path";
24936
25517
  import os26 from "os";
24937
25518
  async function cmdStatus(args2 = [], instanceRoot) {
25519
+ const json = isJsonMode(args2);
25520
+ if (json) await muteForJson();
24938
25521
  if (args2.includes("--all")) {
24939
- await showAllInstances();
25522
+ await showAllInstances(json);
24940
25523
  return;
24941
25524
  }
24942
25525
  const idIdx = args2.indexOf("--id");
24943
25526
  if (idIdx !== -1 && args2[idIdx + 1]) {
24944
- await showInstanceById(args2[idIdx + 1]);
25527
+ await showInstanceById(args2[idIdx + 1], json);
24945
25528
  return;
24946
25529
  }
24947
25530
  const root = instanceRoot ?? getGlobalRoot();
24948
- await showSingleInstance(root);
25531
+ await showSingleInstance(root, json);
24949
25532
  }
24950
- async function showAllInstances() {
25533
+ async function showAllInstances(json = false) {
24951
25534
  const registryPath = path55.join(getGlobalRoot(), "instances.json");
24952
25535
  const registry = new InstanceRegistry(registryPath);
24953
25536
  await registry.load();
24954
25537
  const instances = registry.list();
25538
+ if (json) {
25539
+ jsonSuccess({
25540
+ instances: instances.map((entry) => {
25541
+ const info = readInstanceInfo(entry.root);
25542
+ return {
25543
+ id: entry.id,
25544
+ name: info.name,
25545
+ status: info.pid ? "online" : "offline",
25546
+ pid: info.pid,
25547
+ dir: entry.root,
25548
+ mode: info.runMode,
25549
+ channels: info.channels,
25550
+ apiPort: info.apiPort,
25551
+ tunnelPort: info.tunnelPort
25552
+ };
25553
+ })
25554
+ });
25555
+ }
24955
25556
  if (instances.length === 0) {
24956
25557
  console.log("No workspaces registered.");
24957
25558
  return;
@@ -24972,19 +25573,33 @@ async function showAllInstances() {
24972
25573
  }
24973
25574
  console.log("");
24974
25575
  }
24975
- async function showInstanceById(id) {
25576
+ async function showInstanceById(id, json = false) {
24976
25577
  const registryPath = path55.join(getGlobalRoot(), "instances.json");
24977
25578
  const registry = new InstanceRegistry(registryPath);
24978
25579
  await registry.load();
24979
25580
  const entry = registry.get(id);
24980
25581
  if (!entry) {
25582
+ if (json) jsonError(ErrorCodes.INSTANCE_NOT_FOUND, `Workspace "${id}" not found.`);
24981
25583
  console.error(`Workspace "${id}" not found.`);
24982
25584
  process.exit(1);
24983
25585
  }
24984
- await showSingleInstance(entry.root);
25586
+ await showSingleInstance(entry.root, json);
24985
25587
  }
24986
- async function showSingleInstance(root) {
25588
+ async function showSingleInstance(root, json = false) {
24987
25589
  const info = readInstanceInfo(root);
25590
+ if (json) {
25591
+ jsonSuccess({
25592
+ id: path55.basename(root),
25593
+ name: info.name,
25594
+ status: info.pid ? "online" : "offline",
25595
+ pid: info.pid,
25596
+ dir: root,
25597
+ mode: info.runMode,
25598
+ channels: info.channels,
25599
+ apiPort: info.apiPort,
25600
+ tunnelPort: info.tunnelPort
25601
+ });
25602
+ }
24988
25603
  if (info.pid) {
24989
25604
  console.log(`OpenACP is running (PID ${info.pid})`);
24990
25605
  if (info.name) console.log(` Name: ${info.name}`);
@@ -25062,6 +25677,7 @@ var init_status = __esm({
25062
25677
  "use strict";
25063
25678
  init_instance_registry();
25064
25679
  init_instance_context();
25680
+ init_output();
25065
25681
  }
25066
25682
  });
25067
25683
 
@@ -25936,21 +26552,21 @@ Connect messaging platforms (Telegram, Discord) to 28+ AI coding agents via ACP
25936
26552
 
25937
26553
  \x1B[1mServer:\x1B[0m
25938
26554
  openacp Start (mode from config)
25939
- openacp start Start as background daemon
25940
- openacp stop Stop background daemon
25941
- openacp restart Restart (same mode)
26555
+ openacp start Start as background daemon \x1B[2m[--json]\x1B[0m
26556
+ openacp stop Stop background daemon \x1B[2m[--json]\x1B[0m
26557
+ openacp restart Restart (same mode) \x1B[2m[--json]\x1B[0m
25942
26558
  openacp restart --foreground Restart in foreground mode
25943
26559
  openacp restart --daemon Restart as background daemon
25944
26560
  openacp attach Attach to running daemon
25945
- openacp status Show daemon status
26561
+ openacp status Show daemon status \x1B[2m[--json]\x1B[0m
25946
26562
  openacp logs Tail daemon log file
25947
26563
  openacp --foreground Force foreground mode
25948
26564
 
25949
26565
  \x1B[1mAgent Management:\x1B[0m
25950
- openacp agents Browse all agents (installed + available)
25951
- openacp agents install <name> Install an agent from the ACP Registry
25952
- openacp agents uninstall <name> Remove an installed agent
25953
- openacp agents info <name> Show details, dependencies & setup guide
26566
+ openacp agents Browse all agents (installed + available) \x1B[2m[--json]\x1B[0m
26567
+ openacp agents install <name> Install an agent from the ACP Registry \x1B[2m[--json]\x1B[0m
26568
+ openacp agents uninstall <name> Remove an installed agent \x1B[2m[--json]\x1B[0m
26569
+ openacp agents info <name> Show details, dependencies & setup guide \x1B[2m[--json]\x1B[0m
25954
26570
  openacp agents run <name> [-- args] Run agent CLI directly (login, config...)
25955
26571
  openacp agents refresh Force-refresh agent list from registry
25956
26572
 
@@ -25961,17 +26577,17 @@ Connect messaging platforms (Telegram, Discord) to 28+ AI coding agents via ACP
25961
26577
 
25962
26578
  \x1B[1mConfiguration:\x1B[0m
25963
26579
  openacp config Interactive config editor
25964
- openacp config set <key> <value> Set a config value
26580
+ openacp config set <key> <value> Set a config value \x1B[2m[--json]\x1B[0m
25965
26581
  openacp onboard Re-run onboarding setup wizard
25966
26582
  openacp reset Re-run setup wizard
25967
26583
  openacp update Update to latest version
25968
- openacp doctor Run system diagnostics
26584
+ openacp doctor Run system diagnostics \x1B[2m[--json]\x1B[0m
25969
26585
  openacp doctor --dry-run Check only, don't fix
25970
26586
 
25971
26587
  \x1B[1mPlugins:\x1B[0m
25972
- openacp install <package> Install adapter plugin
25973
- openacp uninstall <package> Remove adapter
25974
- openacp plugins List installed plugins
26588
+ openacp install <package> Install adapter plugin \x1B[2m[--json]\x1B[0m
26589
+ openacp uninstall <package> Remove adapter \x1B[2m[--json]\x1B[0m
26590
+ openacp plugins List installed plugins \x1B[2m[--json]\x1B[0m
25975
26591
  openacp plugin create Scaffold a new plugin project
25976
26592
 
25977
26593
  \x1B[1mDevelopment:\x1B[0m
@@ -25982,25 +26598,25 @@ Connect messaging platforms (Telegram, Discord) to 28+ AI coding agents via ACP
25982
26598
  \x1B[1mSession Transfer:\x1B[0m
25983
26599
  openacp integrate <agent> Install handoff integration
25984
26600
  openacp integrate <agent> --uninstall
25985
- openacp adopt <agent> <id> Adopt an external session
26601
+ openacp adopt <agent> <id> Adopt an external session \x1B[2m[--json]\x1B[0m
25986
26602
 
25987
26603
  \x1B[1mTunnels:\x1B[0m
25988
- openacp tunnel add <port> [--label name] Create tunnel to local port
25989
- openacp tunnel list List active tunnels
25990
- openacp tunnel stop <port> Stop a tunnel
25991
- openacp tunnel stop-all Stop all user tunnels
26604
+ openacp tunnel add <port> [--label name] Create tunnel to local port \x1B[2m[--json]\x1B[0m
26605
+ openacp tunnel list List active tunnels \x1B[2m[--json]\x1B[0m
26606
+ openacp tunnel stop <port> Stop a tunnel \x1B[2m[--json]\x1B[0m
26607
+ openacp tunnel stop-all Stop all user tunnels \x1B[2m[--json]\x1B[0m
25992
26608
 
25993
26609
  \x1B[1mDaemon API:\x1B[0m \x1B[2m(requires running daemon)\x1B[0m
25994
- openacp api status Active sessions
25995
- openacp api session <id> Session details
25996
- openacp api new [agent] [workspace] Create session
25997
- openacp api send <id> <prompt> Send prompt
25998
- openacp api cancel <id> Cancel session
25999
- openacp api bypass <id> on|off Toggle bypass permissions
26000
- openacp api topics [--status ...] List topics
26001
- openacp api cleanup [--status ...] Cleanup old topics
26002
- openacp api health System health check
26003
- openacp api restart Restart daemon
26610
+ openacp api status Active sessions \x1B[2m[--json]\x1B[0m
26611
+ openacp api session <id> Session details \x1B[2m[--json]\x1B[0m
26612
+ openacp api new [agent] [workspace] Create session \x1B[2m[--json]\x1B[0m
26613
+ openacp api send <id> <prompt> Send prompt \x1B[2m[--json]\x1B[0m
26614
+ openacp api cancel <id> Cancel session \x1B[2m[--json]\x1B[0m
26615
+ openacp api bypass <id> on|off Toggle bypass permissions \x1B[2m[--json]\x1B[0m
26616
+ openacp api topics [--status ...] List topics \x1B[2m[--json]\x1B[0m
26617
+ openacp api cleanup [--status ...] Cleanup old topics \x1B[2m[--json]\x1B[0m
26618
+ openacp api health System health check \x1B[2m[--json]\x1B[0m
26619
+ openacp api restart Restart daemon \x1B[2m[--json]\x1B[0m
26004
26620
 
26005
26621
  \x1B[1mWorkspace Flags:\x1B[0m
26006
26622
  --local Use workspace in current directory
@@ -26009,26 +26625,42 @@ Connect messaging platforms (Telegram, Discord) to 28+ AI coding agents via ACP
26009
26625
  --from <path> Copy settings from existing workspace (on create)
26010
26626
  --name <name> Set workspace name (on create)
26011
26627
 
26628
+ \x1B[1mOutput Flags:\x1B[0m
26629
+ --json Output result as JSON (single-line, stdout)
26630
+ Commands marked \x1B[2m[--json]\x1B[0m support machine-readable output.
26631
+ Success: { "success": true, "data": { ... } }
26632
+ Error: { "success": false, "error": { "code": "...", "message": "..." } }
26633
+
26012
26634
  \x1B[2mMore info: https://github.com/Open-ACP/OpenACP\x1B[0m
26013
26635
  `);
26014
26636
  }
26015
26637
 
26016
26638
  // src/cli/commands/version.ts
26017
- async function cmdVersion() {
26639
+ init_output();
26640
+ async function cmdVersion(args2 = []) {
26641
+ const json = isJsonMode(args2);
26642
+ if (json) await muteForJson();
26018
26643
  const { getCurrentVersion: getCurrentVersion2 } = await Promise.resolve().then(() => (init_version(), version_exports));
26019
- console.log(`openacp v${getCurrentVersion2()}`);
26644
+ const version = getCurrentVersion2();
26645
+ if (json) {
26646
+ jsonSuccess({ version });
26647
+ }
26648
+ console.log(`openacp v${version}`);
26020
26649
  }
26021
26650
 
26022
26651
  // src/cli/commands/install.ts
26023
26652
  init_helpers();
26653
+ init_output();
26024
26654
  import { execSync } from "child_process";
26025
- import * as fs from "fs";
26026
- import * as path from "path";
26027
- import * as os from "os";
26655
+ import * as fs2 from "fs";
26656
+ import * as path2 from "path";
26657
+ import * as os2 from "os";
26028
26658
  async function cmdInstall(args2, instanceRoot) {
26029
- const root = instanceRoot ?? path.join(os.homedir(), ".openacp");
26030
- const pluginsDir = path.join(root, "plugins");
26031
- if (wantsHelp(args2)) {
26659
+ const json = isJsonMode(args2);
26660
+ if (json) await muteForJson();
26661
+ const root = instanceRoot ?? path2.join(os2.homedir(), ".openacp");
26662
+ const pluginsDir = path2.join(root, "plugins");
26663
+ if (!json && wantsHelp(args2)) {
26032
26664
  console.log(`
26033
26665
  \x1B[1mopenacp install\x1B[0m \u2014 Install a plugin adapter
26034
26666
 
@@ -26040,36 +26672,50 @@ async function cmdInstall(args2, instanceRoot) {
26040
26672
 
26041
26673
  Installs the plugin to ~/.openacp/plugins/.
26042
26674
 
26675
+ \x1B[1mOptions:\x1B[0m
26676
+ --json Output result as JSON
26677
+ -h, --help Show this help message
26678
+
26043
26679
  \x1B[1mExamples:\x1B[0m
26044
26680
  openacp install @openacp/adapter-discord
26045
26681
  `);
26046
26682
  return;
26047
26683
  }
26048
- const pkg = args2[0];
26684
+ const pkg = args2.filter((a) => a !== "--json")[0];
26049
26685
  if (!pkg) {
26686
+ if (json) jsonError(ErrorCodes.MISSING_ARGUMENT, "Package name is required");
26050
26687
  console.error("Usage: openacp install <package>");
26051
26688
  process.exit(1);
26052
26689
  }
26053
- fs.mkdirSync(pluginsDir, { recursive: true });
26054
- const pkgPath = path.join(pluginsDir, "package.json");
26055
- if (!fs.existsSync(pkgPath)) {
26056
- fs.writeFileSync(pkgPath, JSON.stringify({ name: "openacp-plugins", private: true, dependencies: {} }, null, 2));
26690
+ fs2.mkdirSync(pluginsDir, { recursive: true });
26691
+ const pkgPath = path2.join(pluginsDir, "package.json");
26692
+ if (!fs2.existsSync(pkgPath)) {
26693
+ fs2.writeFileSync(pkgPath, JSON.stringify({ name: "openacp-plugins", private: true, dependencies: {} }, null, 2));
26694
+ }
26695
+ if (!json) console.log(`Installing ${pkg}...`);
26696
+ try {
26697
+ execSync(`npm install ${pkg} --prefix "${pluginsDir}"`, { stdio: json ? "pipe" : "inherit" });
26698
+ } catch (err) {
26699
+ if (json) jsonError(ErrorCodes.INSTALL_FAILED, `Failed to install ${pkg}`);
26700
+ process.exit(1);
26057
26701
  }
26058
- console.log(`Installing ${pkg}...`);
26059
- execSync(`npm install ${pkg} --prefix "${pluginsDir}"`, { stdio: "inherit" });
26702
+ if (json) jsonSuccess({ plugin: pkg, installed: true });
26060
26703
  console.log(`Plugin ${pkg} installed successfully.`);
26061
26704
  }
26062
26705
 
26063
26706
  // src/cli/commands/uninstall.ts
26064
26707
  init_helpers();
26708
+ init_output();
26065
26709
  import { execSync as execSync2 } from "child_process";
26066
- import * as fs2 from "fs";
26067
- import * as path2 from "path";
26068
- import * as os2 from "os";
26710
+ import * as fs3 from "fs";
26711
+ import * as path3 from "path";
26712
+ import * as os3 from "os";
26069
26713
  async function cmdUninstall(args2, instanceRoot) {
26070
- const root = instanceRoot ?? path2.join(os2.homedir(), ".openacp");
26071
- const pluginsDir = path2.join(root, "plugins");
26072
- if (wantsHelp(args2)) {
26714
+ const json = isJsonMode(args2);
26715
+ if (json) await muteForJson();
26716
+ const root = instanceRoot ?? path3.join(os3.homedir(), ".openacp");
26717
+ const pluginsDir = path3.join(root, "plugins");
26718
+ if (!json && wantsHelp(args2)) {
26073
26719
  console.log(`
26074
26720
  \x1B[1mopenacp uninstall\x1B[0m \u2014 Remove a plugin adapter
26075
26721
 
@@ -26079,36 +26725,54 @@ async function cmdUninstall(args2, instanceRoot) {
26079
26725
  \x1B[1mArguments:\x1B[0m
26080
26726
  <package> npm package name to remove
26081
26727
 
26728
+ \x1B[1mOptions:\x1B[0m
26729
+ --json Output result as JSON
26730
+ -h, --help Show this help message
26731
+
26082
26732
  \x1B[1mExamples:\x1B[0m
26083
26733
  openacp uninstall @openacp/adapter-discord
26084
26734
  `);
26085
26735
  return;
26086
26736
  }
26087
- const pkg = args2[0];
26737
+ const pkg = args2.filter((a) => a !== "--json")[0];
26088
26738
  if (!pkg) {
26739
+ if (json) jsonError(ErrorCodes.MISSING_ARGUMENT, "Package name is required");
26089
26740
  console.error("Usage: openacp uninstall <package>");
26090
26741
  process.exit(1);
26091
26742
  }
26092
- fs2.mkdirSync(pluginsDir, { recursive: true });
26093
- const pkgPath = path2.join(pluginsDir, "package.json");
26094
- if (!fs2.existsSync(pkgPath)) {
26095
- fs2.writeFileSync(pkgPath, JSON.stringify({ name: "openacp-plugins", private: true, dependencies: {} }, null, 2));
26743
+ fs3.mkdirSync(pluginsDir, { recursive: true });
26744
+ const pkgPath = path3.join(pluginsDir, "package.json");
26745
+ if (!fs3.existsSync(pkgPath)) {
26746
+ fs3.writeFileSync(pkgPath, JSON.stringify({ name: "openacp-plugins", private: true, dependencies: {} }, null, 2));
26096
26747
  }
26097
- console.log(`Uninstalling ${pkg}...`);
26098
- execSync2(`npm uninstall ${pkg} --prefix "${pluginsDir}"`, { stdio: "inherit" });
26748
+ if (!json) console.log(`Uninstalling ${pkg}...`);
26749
+ try {
26750
+ execSync2(`npm uninstall ${pkg} --prefix "${pluginsDir}"`, { stdio: json ? "pipe" : "inherit" });
26751
+ } catch (err) {
26752
+ if (json) jsonError(ErrorCodes.UNINSTALL_FAILED, `Failed to uninstall ${pkg}`);
26753
+ process.exit(1);
26754
+ }
26755
+ if (json) jsonSuccess({ plugin: pkg, uninstalled: true });
26099
26756
  console.log(`Plugin ${pkg} uninstalled.`);
26100
26757
  }
26101
26758
 
26102
26759
  // src/cli/commands/plugins.ts
26103
26760
  init_helpers();
26761
+ init_output();
26104
26762
  async function cmdPlugins(args2 = [], instanceRoot) {
26105
- if (wantsHelp(args2)) {
26763
+ const json = isJsonMode(args2);
26764
+ if (json) await muteForJson();
26765
+ if (!json && wantsHelp(args2)) {
26106
26766
  console.log(`
26107
26767
  \x1B[1mopenacp plugins\x1B[0m \u2014 List installed plugins
26108
26768
 
26109
26769
  \x1B[1mUsage:\x1B[0m
26110
26770
  openacp plugins
26111
26771
 
26772
+ \x1B[1mOptions:\x1B[0m
26773
+ --json Output result as JSON
26774
+ -h, --help Show this help message
26775
+
26112
26776
  Shows all plugins registered in the plugin registry.
26113
26777
  `);
26114
26778
  return;
@@ -26121,6 +26785,19 @@ Shows all plugins registered in the plugin registry.
26121
26785
  const registry = new PluginRegistry2(registryPath);
26122
26786
  await registry.load();
26123
26787
  const plugins = registry.list();
26788
+ if (json) {
26789
+ const pluginList = [];
26790
+ for (const [name, entry] of plugins) {
26791
+ pluginList.push({
26792
+ name,
26793
+ version: entry.version,
26794
+ enabled: entry.enabled !== false,
26795
+ source: entry.source ?? "unknown",
26796
+ description: entry.description ?? ""
26797
+ });
26798
+ }
26799
+ jsonSuccess({ plugins: pluginList });
26800
+ }
26124
26801
  if (plugins.size === 0) {
26125
26802
  console.log("No plugins installed.");
26126
26803
  } else {
@@ -26150,6 +26827,10 @@ async function cmdPlugin(args2 = [], instanceRoot) {
26150
26827
  openacp plugin configure <name> Run interactive configuration
26151
26828
  openacp plugin create Scaffold a new plugin project
26152
26829
 
26830
+ \x1B[1mOptions:\x1B[0m
26831
+ --json Output result as JSON
26832
+ -h, --help Show this help message
26833
+
26153
26834
  \x1B[1mExamples:\x1B[0m
26154
26835
  openacp plugin list
26155
26836
  openacp plugin search telegram
@@ -26164,7 +26845,7 @@ async function cmdPlugin(args2 = [], instanceRoot) {
26164
26845
  }
26165
26846
  switch (subcommand) {
26166
26847
  case "list":
26167
- return cmdPlugins([], instanceRoot);
26848
+ return cmdPlugins(isJsonMode(args2) ? ["--json"] : [], instanceRoot);
26168
26849
  case "search": {
26169
26850
  const { cmdPluginSearch: cmdPluginSearch2 } = await Promise.resolve().then(() => (init_plugin_search(), plugin_search_exports));
26170
26851
  await cmdPluginSearch2(args2.slice(1));
@@ -26174,39 +26855,43 @@ async function cmdPlugin(args2 = [], instanceRoot) {
26174
26855
  case "install": {
26175
26856
  const pkg = args2[1];
26176
26857
  if (!pkg) {
26858
+ if (isJsonMode(args2)) jsonError(ErrorCodes.MISSING_ARGUMENT, "Package name is required");
26177
26859
  console.error("Error: missing package name. Usage: openacp plugin add <package>");
26178
26860
  process.exit(1);
26179
26861
  }
26180
- await installPlugin(pkg, instanceRoot);
26862
+ await installPlugin(pkg, instanceRoot, isJsonMode(args2));
26181
26863
  return;
26182
26864
  }
26183
26865
  case "remove":
26184
26866
  case "uninstall": {
26185
26867
  const pkg = args2[1];
26186
26868
  if (!pkg) {
26869
+ if (isJsonMode(args2)) jsonError(ErrorCodes.MISSING_ARGUMENT, "Package name is required");
26187
26870
  console.error("Error: missing package name. Usage: openacp plugin remove <package> [--purge]");
26188
26871
  process.exit(1);
26189
26872
  }
26190
26873
  const purge = args2.includes("--purge");
26191
- await uninstallPlugin(pkg, purge, instanceRoot);
26874
+ await uninstallPlugin(pkg, purge, instanceRoot, isJsonMode(args2));
26192
26875
  return;
26193
26876
  }
26194
26877
  case "enable": {
26195
26878
  const name = args2[1];
26196
26879
  if (!name) {
26880
+ if (isJsonMode(args2)) jsonError(ErrorCodes.MISSING_ARGUMENT, "Plugin name is required");
26197
26881
  console.error("Error: missing plugin name. Usage: openacp plugin enable <name>");
26198
26882
  process.exit(1);
26199
26883
  }
26200
- await setPluginEnabled(name, true, instanceRoot);
26884
+ await setPluginEnabled(name, true, instanceRoot, isJsonMode(args2));
26201
26885
  return;
26202
26886
  }
26203
26887
  case "disable": {
26204
26888
  const name = args2[1];
26205
26889
  if (!name) {
26890
+ if (isJsonMode(args2)) jsonError(ErrorCodes.MISSING_ARGUMENT, "Plugin name is required");
26206
26891
  console.error("Error: missing plugin name. Usage: openacp plugin disable <name>");
26207
26892
  process.exit(1);
26208
26893
  }
26209
- await setPluginEnabled(name, false, instanceRoot);
26894
+ await setPluginEnabled(name, false, instanceRoot, isJsonMode(args2));
26210
26895
  return;
26211
26896
  }
26212
26897
  case "configure": {
@@ -26229,7 +26914,8 @@ async function cmdPlugin(args2 = [], instanceRoot) {
26229
26914
  process.exit(1);
26230
26915
  }
26231
26916
  }
26232
- async function setPluginEnabled(name, enabled, instanceRoot) {
26917
+ async function setPluginEnabled(name, enabled, instanceRoot, json = false) {
26918
+ if (json) await muteForJson();
26233
26919
  const os31 = await import("os");
26234
26920
  const path65 = await import("path");
26235
26921
  const { PluginRegistry: PluginRegistry2 } = await Promise.resolve().then(() => (init_plugin_registry(), plugin_registry_exports));
@@ -26239,11 +26925,13 @@ async function setPluginEnabled(name, enabled, instanceRoot) {
26239
26925
  await registry.load();
26240
26926
  const entry = registry.get(name);
26241
26927
  if (!entry) {
26928
+ if (json) jsonError(ErrorCodes.PLUGIN_NOT_FOUND, `Plugin "${name}" not found.`);
26242
26929
  console.error(`Plugin "${name}" not found. Run "openacp plugin list" to see installed plugins.`);
26243
26930
  process.exit(1);
26244
26931
  }
26245
26932
  registry.setEnabled(name, enabled);
26246
26933
  await registry.save();
26934
+ if (json) jsonSuccess({ plugin: name, enabled });
26247
26935
  console.log(`Plugin ${name} ${enabled ? "enabled" : "disabled"}. Restart to apply.`);
26248
26936
  }
26249
26937
  async function configurePlugin(name, instanceRoot) {
@@ -26269,7 +26957,8 @@ async function configurePlugin(name, instanceRoot) {
26269
26957
  console.log(`Plugin ${name} has no configure or install hook.`);
26270
26958
  }
26271
26959
  }
26272
- async function installPlugin(input2, instanceRoot) {
26960
+ async function installPlugin(input2, instanceRoot, json = false) {
26961
+ if (json) await muteForJson();
26273
26962
  const os31 = await import("os");
26274
26963
  const path65 = await import("path");
26275
26964
  const { execFileSync: execFileSync8 } = await import("child_process");
@@ -26310,16 +26999,16 @@ async function installPlugin(input2, instanceRoot) {
26310
26999
  const registry = await client.getRegistry();
26311
27000
  registryPlugin = registry.plugins.find((p2) => p2.name === pkgName || p2.npm === pkgName);
26312
27001
  if (registryPlugin) {
26313
- console.log(`Resolved from registry: ${pkgName} \u2192 ${registryPlugin.npm}`);
27002
+ if (!json) console.log(`Resolved from registry: ${pkgName} \u2192 ${registryPlugin.npm}`);
26314
27003
  pkgName = registryPlugin.npm;
26315
- if (!registryPlugin.verified) {
27004
+ if (!json && !registryPlugin.verified) {
26316
27005
  console.log("\u26A0\uFE0F This plugin is not verified by the OpenACP team.");
26317
27006
  }
26318
27007
  }
26319
27008
  } catch {
26320
27009
  }
26321
27010
  const installSpec = pkgVersion ? `${pkgName}@${pkgVersion}` : pkgName;
26322
- console.log(`Installing ${installSpec}...`);
27011
+ if (!json) console.log(`Installing ${installSpec}...`);
26323
27012
  const { corePlugins: corePlugins2 } = await Promise.resolve().then(() => (init_core_plugins(), core_plugins_exports));
26324
27013
  const builtinPlugin = corePlugins2.find((p2) => p2.name === pkgName);
26325
27014
  const basePath = path65.join(root, "plugins", "data");
@@ -26340,6 +27029,7 @@ async function installPlugin(input2, instanceRoot) {
26340
27029
  description: builtinPlugin.description
26341
27030
  });
26342
27031
  await pluginRegistry.save();
27032
+ if (json) jsonSuccess({ plugin: builtinPlugin.name, version: builtinPlugin.version, installed: true });
26343
27033
  console.log(`\u2713 ${builtinPlugin.name} installed! Restart to activate.`);
26344
27034
  return;
26345
27035
  }
@@ -26347,10 +27037,11 @@ async function installPlugin(input2, instanceRoot) {
26347
27037
  const nodeModulesDir = path65.join(pluginsDir, "node_modules");
26348
27038
  try {
26349
27039
  execFileSync8("npm", ["install", installSpec, "--prefix", pluginsDir, "--save"], {
26350
- stdio: "inherit",
27040
+ stdio: json ? "pipe" : "inherit",
26351
27041
  timeout: 6e4
26352
27042
  });
26353
27043
  } catch {
27044
+ if (json) jsonError(ErrorCodes.INSTALL_FAILED, `Failed to install ${installSpec}`);
26354
27045
  console.error(`Failed to install ${installSpec}. Check the package name and try again.`);
26355
27046
  process.exit(1);
26356
27047
  }
@@ -26365,10 +27056,12 @@ async function installPlugin(input2, instanceRoot) {
26365
27056
  if (minVersion) {
26366
27057
  const { compareVersions: compareVersions2 } = await Promise.resolve().then(() => (init_version(), version_exports));
26367
27058
  if (compareVersions2(cliVersion, minVersion) < 0) {
26368
- console.log(`
27059
+ if (!json) {
27060
+ console.log(`
26369
27061
  \u26A0\uFE0F This plugin requires OpenACP >= ${minVersion}. You have ${cliVersion}.`);
26370
- console.log(` Run 'openacp update' to get the latest version.
27062
+ console.log(` Run 'openacp update' to get the latest version.
26371
27063
  `);
27064
+ }
26372
27065
  }
26373
27066
  }
26374
27067
  const pluginModule = await import(path65.join(pluginRoot, installedPkg.main ?? "dist/index.js"));
@@ -26385,6 +27078,7 @@ async function installPlugin(input2, instanceRoot) {
26385
27078
  description: plugin2?.description ?? installedPkg.description
26386
27079
  });
26387
27080
  await pluginRegistry.save();
27081
+ if (json) jsonSuccess({ plugin: plugin2?.name ?? pkgName, version: installedPkg.version, installed: true });
26388
27082
  console.log(`\u2713 ${plugin2?.name ?? pkgName} installed! Restart to activate.`);
26389
27083
  } catch (err) {
26390
27084
  pluginRegistry.register(pkgName, {
@@ -26394,10 +27088,12 @@ async function installPlugin(input2, instanceRoot) {
26394
27088
  settingsPath: settingsManager.getSettingsPath(pkgName)
26395
27089
  });
26396
27090
  await pluginRegistry.save();
27091
+ if (json) jsonSuccess({ plugin: pkgName, version: pkgVersion ?? "unknown", installed: true });
26397
27092
  console.log(`\u2713 ${pkgName} installed (npm only). Restart to activate.`);
26398
27093
  }
26399
27094
  }
26400
- async function uninstallPlugin(name, purge, instanceRoot) {
27095
+ async function uninstallPlugin(name, purge, instanceRoot, json = false) {
27096
+ if (json) await muteForJson();
26401
27097
  const os31 = await import("os");
26402
27098
  const path65 = await import("path");
26403
27099
  const fs53 = await import("fs");
@@ -26408,10 +27104,12 @@ async function uninstallPlugin(name, purge, instanceRoot) {
26408
27104
  await registry.load();
26409
27105
  const entry = registry.get(name);
26410
27106
  if (!entry) {
27107
+ if (json) jsonError(ErrorCodes.PLUGIN_NOT_FOUND, `Plugin "${name}" not installed.`);
26411
27108
  console.error(`Plugin "${name}" not installed.`);
26412
27109
  process.exit(1);
26413
27110
  }
26414
27111
  if (entry.source === "builtin") {
27112
+ if (json) jsonError(ErrorCodes.UNINSTALL_FAILED, `Cannot uninstall built-in plugin "${name}". Use "openacp plugin disable ${name}" instead.`);
26415
27113
  console.error(`Cannot uninstall built-in plugin. Use "openacp plugin disable ${name}" instead.`);
26416
27114
  process.exit(1);
26417
27115
  }
@@ -26434,12 +27132,14 @@ async function uninstallPlugin(name, purge, instanceRoot) {
26434
27132
  }
26435
27133
  registry.remove(name);
26436
27134
  await registry.save();
27135
+ if (json) jsonSuccess({ plugin: name, uninstalled: true });
26437
27136
  console.log(`Plugin ${name} uninstalled${purge ? " (purged)" : ""}.`);
26438
27137
  }
26439
27138
 
26440
27139
  // src/cli/commands/api.ts
26441
27140
  init_api_client();
26442
27141
  init_helpers();
27142
+ init_output();
26443
27143
  function printApiHelp() {
26444
27144
  console.log(`
26445
27145
  \x1B[1mopenacp api\x1B[0m \u2014 Interact with the running OpenACP daemon
@@ -26481,6 +27181,7 @@ function printApiHelp() {
26481
27181
  openacp api version Show daemon version
26482
27182
 
26483
27183
  \x1B[1mOptions:\x1B[0m
27184
+ --json Output result as JSON
26484
27185
  -h, --help Show this help message
26485
27186
  `);
26486
27187
  }
@@ -26705,8 +27406,11 @@ Shows the version of the currently running daemon process.
26705
27406
  printApiHelp();
26706
27407
  return;
26707
27408
  }
27409
+ const json = isJsonMode(args2);
27410
+ if (json) await muteForJson();
26708
27411
  const port = readApiPort(void 0, instanceRoot);
26709
27412
  if (port === null) {
27413
+ if (json) jsonError(ErrorCodes.DAEMON_NOT_RUNNING, "OpenACP is not running.");
26710
27414
  console.error("OpenACP is not running. Start with `openacp start`");
26711
27415
  process.exit(1);
26712
27416
  }
@@ -26729,9 +27433,11 @@ Shows the version of the currently running daemon process.
26729
27433
  });
26730
27434
  const data = await res.json();
26731
27435
  if (!res.ok) {
27436
+ if (json) jsonError(ErrorCodes.API_ERROR, String(data.error ?? "API request failed"));
26732
27437
  console.error(`Error: ${data.error}`);
26733
27438
  process.exit(1);
26734
27439
  }
27440
+ if (json) jsonSuccess(data);
26735
27441
  console.log("Session created");
26736
27442
  console.log(` ID : ${data.sessionId}`);
26737
27443
  console.log(` Agent : ${data.agent}`);
@@ -26742,6 +27448,7 @@ Shows the version of the currently running daemon process.
26742
27448
  } else if (subCmd === "cancel") {
26743
27449
  const sessionId = args2[1];
26744
27450
  if (!sessionId) {
27451
+ if (json) jsonError(ErrorCodes.MISSING_ARGUMENT, "Session ID is required");
26745
27452
  console.error("Usage: openacp api cancel <session-id>");
26746
27453
  process.exit(1);
26747
27454
  }
@@ -26750,13 +27457,16 @@ Shows the version of the currently running daemon process.
26750
27457
  });
26751
27458
  const data = await res.json();
26752
27459
  if (!res.ok) {
27460
+ if (json) jsonError(ErrorCodes.API_ERROR, String(data.error ?? "API request failed"));
26753
27461
  console.error(`Error: ${data.error}`);
26754
27462
  process.exit(1);
26755
27463
  }
27464
+ if (json) jsonSuccess({ cancelled: true, sessionId });
26756
27465
  console.log(`Session ${sessionId} cancelled`);
26757
27466
  } else if (subCmd === "status") {
26758
27467
  const res = await call("/api/sessions");
26759
27468
  const data = await res.json();
27469
+ if (json) jsonSuccess(data);
26760
27470
  if (data.sessions.length === 0) {
26761
27471
  console.log("No active sessions.");
26762
27472
  } else {
@@ -26770,6 +27480,7 @@ Shows the version of the currently running daemon process.
26770
27480
  } else if (subCmd === "agents") {
26771
27481
  const res = await call("/api/agents");
26772
27482
  const data = await res.json();
27483
+ if (json) jsonSuccess(data);
26773
27484
  console.log("Available agents:");
26774
27485
  for (const a of data.agents) {
26775
27486
  const isDefault = a.name === data.default ? " (default)" : "";
@@ -26781,6 +27492,7 @@ Shows the version of the currently running daemon process.
26781
27492
  const query = statusParam ? `?status=${encodeURIComponent(statusParam)}` : "";
26782
27493
  const res = await call(`/api/topics${query}`);
26783
27494
  const data = await res.json();
27495
+ if (json) jsonSuccess(data);
26784
27496
  if (data.topics.length === 0) {
26785
27497
  console.log("No topics found.");
26786
27498
  } else {
@@ -26795,6 +27507,7 @@ Shows the version of the currently running daemon process.
26795
27507
  } else if (subCmd === "delete-topic") {
26796
27508
  const sessionId = args2[1];
26797
27509
  if (!sessionId) {
27510
+ if (json) jsonError(ErrorCodes.MISSING_ARGUMENT, "Session ID is required");
26798
27511
  console.error("Usage: openacp api delete-topic <session-id> [--force]");
26799
27512
  process.exit(1);
26800
27513
  }
@@ -26804,13 +27517,16 @@ Shows the version of the currently running daemon process.
26804
27517
  const data = await res.json();
26805
27518
  if (res.status === 409) {
26806
27519
  const session = data.session;
27520
+ if (json) jsonError(ErrorCodes.API_ERROR, `Session "${sessionId}" is active (${session?.status}). Use --force to delete.`);
26807
27521
  console.error(`Session "${sessionId}" is active (${session?.status}). Use --force to delete.`);
26808
27522
  process.exit(1);
26809
27523
  }
26810
27524
  if (!res.ok) {
27525
+ if (json) jsonError(ErrorCodes.API_ERROR, String(data.error ?? "API request failed"));
26811
27526
  console.error(`Error: ${data.error}`);
26812
27527
  process.exit(1);
26813
27528
  }
27529
+ if (json) jsonSuccess(data);
26814
27530
  const topicLabel = data.topicId ? `Topic #${data.topicId}` : "headless session";
26815
27531
  console.log(`${topicLabel} deleted (session ${sessionId})`);
26816
27532
  } else if (subCmd === "cleanup") {
@@ -26824,6 +27540,7 @@ Shows the version of the currently running daemon process.
26824
27540
  body: JSON.stringify(body)
26825
27541
  });
26826
27542
  const data = await res.json();
27543
+ if (json) jsonSuccess(data);
26827
27544
  if (data.deleted.length === 0 && data.failed.length === 0) {
26828
27545
  console.log("Nothing to clean up.");
26829
27546
  } else {
@@ -26835,11 +27552,13 @@ Shows the version of the currently running daemon process.
26835
27552
  } else if (subCmd === "send") {
26836
27553
  const sessionId = args2[1];
26837
27554
  if (!sessionId) {
27555
+ if (json) jsonError(ErrorCodes.MISSING_ARGUMENT, "Session ID is required");
26838
27556
  console.error("Usage: openacp api send <session-id> <prompt>");
26839
27557
  process.exit(1);
26840
27558
  }
26841
27559
  const prompt = args2.slice(2).join(" ");
26842
27560
  if (!prompt) {
27561
+ if (json) jsonError(ErrorCodes.MISSING_ARGUMENT, "Prompt is required");
26843
27562
  console.error("Usage: openacp api send <session-id> <prompt>");
26844
27563
  process.exit(1);
26845
27564
  }
@@ -26850,22 +27569,27 @@ Shows the version of the currently running daemon process.
26850
27569
  });
26851
27570
  const data = await res.json();
26852
27571
  if (!res.ok) {
27572
+ if (json) jsonError(ErrorCodes.API_ERROR, String(data.error ?? "API request failed"));
26853
27573
  console.error(`Error: ${data.error}`);
26854
27574
  process.exit(1);
26855
27575
  }
27576
+ if (json) jsonSuccess(data);
26856
27577
  console.log(`Prompt sent to session ${sessionId} (queue depth: ${data.queueDepth})`);
26857
27578
  } else if (subCmd === "session") {
26858
27579
  const sessionId = args2[1];
26859
27580
  if (!sessionId) {
27581
+ if (json) jsonError(ErrorCodes.MISSING_ARGUMENT, "Session ID is required");
26860
27582
  console.error("Usage: openacp api session <session-id>");
26861
27583
  process.exit(1);
26862
27584
  }
26863
27585
  const res = await call(`/api/sessions/${encodeURIComponent(sessionId)}`);
26864
27586
  const data = await res.json();
26865
27587
  if (!res.ok) {
27588
+ if (json) jsonError(ErrorCodes.API_ERROR, String(data.error ?? "API request failed"));
26866
27589
  console.error(`Error: ${data.error}`);
26867
27590
  process.exit(1);
26868
27591
  }
27592
+ if (json) jsonSuccess(data);
26869
27593
  const s = data.session ?? data;
26870
27594
  console.log(`Session details:`);
26871
27595
  console.log(` ID : ${s.id}`);
@@ -26882,11 +27606,13 @@ Shows the version of the currently running daemon process.
26882
27606
  } else if (subCmd === "dangerous" || subCmd === "bypass") {
26883
27607
  const sessionId = args2[1];
26884
27608
  if (!sessionId) {
27609
+ if (json) jsonError(ErrorCodes.MISSING_ARGUMENT, "Session ID is required");
26885
27610
  console.error("Usage: openacp api bypass <session-id> [on|off]");
26886
27611
  process.exit(1);
26887
27612
  }
26888
27613
  const toggle = args2[2];
26889
27614
  if (!toggle || toggle !== "on" && toggle !== "off") {
27615
+ if (json) jsonError(ErrorCodes.MISSING_ARGUMENT, "Toggle value (on|off) is required");
26890
27616
  console.error("Usage: openacp api bypass <session-id> [on|off]");
26891
27617
  process.exit(1);
26892
27618
  }
@@ -26897,18 +27623,22 @@ Shows the version of the currently running daemon process.
26897
27623
  });
26898
27624
  const data = await res.json();
26899
27625
  if (!res.ok) {
27626
+ if (json) jsonError(ErrorCodes.API_ERROR, String(data.error ?? "API request failed"));
26900
27627
  console.error(`Error: ${data.error}`);
26901
27628
  process.exit(1);
26902
27629
  }
27630
+ if (json) jsonSuccess(data);
26903
27631
  const state = toggle === "on" ? "enabled" : "disabled";
26904
27632
  console.log(`Bypass permissions ${state} for session ${sessionId}`);
26905
27633
  } else if (subCmd === "health") {
26906
27634
  const res = await call("/api/health");
26907
27635
  const data = await res.json();
26908
27636
  if (!res.ok) {
27637
+ if (json) jsonError(ErrorCodes.API_ERROR, String(data.error ?? "API request failed"));
26909
27638
  console.error(`Error: ${data.error}`);
26910
27639
  process.exit(1);
26911
27640
  }
27641
+ if (json) jsonSuccess(data);
26912
27642
  const uptimeMs = typeof data.uptime === "number" ? data.uptime : 0;
26913
27643
  const uptimeSeconds = Math.floor(uptimeMs / 1e3);
26914
27644
  const hours = Math.floor(uptimeSeconds / 3600);
@@ -26930,9 +27660,11 @@ Shows the version of the currently running daemon process.
26930
27660
  const res = await call("/api/restart", { method: "POST" });
26931
27661
  const data = await res.json();
26932
27662
  if (!res.ok) {
27663
+ if (json) jsonError(ErrorCodes.API_ERROR, String(data.error ?? "API request failed"));
26933
27664
  console.error(`Error: ${data.error}`);
26934
27665
  process.exit(1);
26935
27666
  }
27667
+ if (json) jsonSuccess({ restarted: true });
26936
27668
  console.log("Restart signal sent. OpenACP is restarting...");
26937
27669
  } else if (subCmd === "config") {
26938
27670
  console.warn('\u26A0\uFE0F Deprecated: use "openacp config" or "openacp config set" instead.');
@@ -26941,14 +27673,17 @@ Shows the version of the currently running daemon process.
26941
27673
  const res = await call("/api/config");
26942
27674
  const data = await res.json();
26943
27675
  if (!res.ok) {
27676
+ if (json) jsonError(ErrorCodes.API_ERROR, String(data.error ?? "API request failed"));
26944
27677
  console.error(`Error: ${data.error}`);
26945
27678
  process.exit(1);
26946
27679
  }
27680
+ if (json) jsonSuccess(data);
26947
27681
  console.log(JSON.stringify(data.config, null, 2));
26948
27682
  } else if (subSubCmd === "set") {
26949
27683
  const configPath = args2[2];
26950
27684
  const configValue = args2[3];
26951
27685
  if (!configPath || configValue === void 0) {
27686
+ if (json) jsonError(ErrorCodes.MISSING_ARGUMENT, "Config path and value are required");
26952
27687
  console.error("Usage: openacp api config set <path> <value>");
26953
27688
  process.exit(1);
26954
27689
  }
@@ -26964,14 +27699,17 @@ Shows the version of the currently running daemon process.
26964
27699
  });
26965
27700
  const data = await res.json();
26966
27701
  if (!res.ok) {
27702
+ if (json) jsonError(ErrorCodes.API_ERROR, String(data.error ?? "API request failed"));
26967
27703
  console.error(`Error: ${data.error}`);
26968
27704
  process.exit(1);
26969
27705
  }
27706
+ if (json) jsonSuccess(data);
26970
27707
  console.log(`Config updated: ${configPath} = ${JSON.stringify(value)}`);
26971
27708
  if (data.needsRestart) {
26972
27709
  console.log("Note: restart required for this change to take effect.");
26973
27710
  }
26974
27711
  } else {
27712
+ if (json) jsonError(ErrorCodes.UNKNOWN_COMMAND, `Unknown config subcommand: ${subSubCmd}`);
26975
27713
  console.error(`Unknown config subcommand: ${subSubCmd}`);
26976
27714
  console.log(" openacp api config Show runtime config");
26977
27715
  console.log(" openacp api config set <key> <value> Update config value");
@@ -26981,9 +27719,11 @@ Shows the version of the currently running daemon process.
26981
27719
  const res = await call("/api/adapters");
26982
27720
  const data = await res.json();
26983
27721
  if (!res.ok) {
27722
+ if (json) jsonError(ErrorCodes.API_ERROR, String(data.error ?? "API request failed"));
26984
27723
  console.error(`Error: ${data.error}`);
26985
27724
  process.exit(1);
26986
27725
  }
27726
+ if (json) jsonSuccess(data);
26987
27727
  console.log("Registered adapters:");
26988
27728
  for (const a of data.adapters) {
26989
27729
  console.log(` ${a.name} (${a.type})`);
@@ -26992,9 +27732,11 @@ Shows the version of the currently running daemon process.
26992
27732
  const res = await call("/api/tunnel");
26993
27733
  const data = await res.json();
26994
27734
  if (!res.ok) {
27735
+ if (json) jsonError(ErrorCodes.API_ERROR, String(data.error ?? "API request failed"));
26995
27736
  console.error(`Error: ${data.error}`);
26996
27737
  process.exit(1);
26997
27738
  }
27739
+ if (json) jsonSuccess(data);
26998
27740
  if (data.enabled) {
26999
27741
  console.log(`Tunnel provider : ${data.provider}`);
27000
27742
  console.log(`Tunnel URL : ${data.url}`);
@@ -27004,6 +27746,7 @@ Shows the version of the currently running daemon process.
27004
27746
  } else if (subCmd === "notify") {
27005
27747
  const message = args2.slice(1).join(" ");
27006
27748
  if (!message) {
27749
+ if (json) jsonError(ErrorCodes.MISSING_ARGUMENT, "Message is required");
27007
27750
  console.error("Usage: openacp api notify <message>");
27008
27751
  process.exit(1);
27009
27752
  }
@@ -27014,21 +27757,26 @@ Shows the version of the currently running daemon process.
27014
27757
  });
27015
27758
  const data = await res.json();
27016
27759
  if (!res.ok) {
27760
+ if (json) jsonError(ErrorCodes.API_ERROR, String(data.error ?? "API request failed"));
27017
27761
  console.error(`Error: ${data.error}`);
27018
27762
  process.exit(1);
27019
27763
  }
27764
+ if (json) jsonSuccess({ sent: true });
27020
27765
  console.log("Notification sent to all channels.");
27021
27766
  } else if (subCmd === "version") {
27022
27767
  const res = await call("/api/version");
27023
27768
  const data = await res.json();
27024
27769
  if (!res.ok) {
27770
+ if (json) jsonError(ErrorCodes.API_ERROR, String(data.error ?? "API request failed"));
27025
27771
  console.error(`Error: ${data.error}`);
27026
27772
  process.exit(1);
27027
27773
  }
27774
+ if (json) jsonSuccess(data);
27028
27775
  console.log(`Daemon version: ${data.version}`);
27029
27776
  } else if (subCmd === "session-config") {
27030
27777
  const sessionId = args2[1];
27031
27778
  if (!sessionId) {
27779
+ if (json) jsonError(ErrorCodes.MISSING_ARGUMENT, "Session ID is required");
27032
27780
  console.error("Usage: openacp api session-config <session-id> [set <opt> <value> | overrides | dangerous [on|off]]");
27033
27781
  process.exit(1);
27034
27782
  }
@@ -27037,9 +27785,11 @@ Shows the version of the currently running daemon process.
27037
27785
  const res = await call(`/api/sessions/${encodeURIComponent(sessionId)}/config`);
27038
27786
  const data = await res.json();
27039
27787
  if (!res.ok) {
27788
+ if (json) jsonError(ErrorCodes.API_ERROR, String(data.error ?? "API request failed"));
27040
27789
  console.error(`Error: ${data.error}`);
27041
27790
  process.exit(1);
27042
27791
  }
27792
+ if (json) jsonSuccess(data);
27043
27793
  const configOptions = data.configOptions;
27044
27794
  const clientOverrides = data.clientOverrides;
27045
27795
  if (!configOptions || configOptions.length === 0) {
@@ -27073,6 +27823,7 @@ Client overrides: ${JSON.stringify(clientOverrides)}`);
27073
27823
  const configId = args2[3];
27074
27824
  const value = args2[4];
27075
27825
  if (!configId || value === void 0) {
27826
+ if (json) jsonError(ErrorCodes.MISSING_ARGUMENT, "Config ID and value are required");
27076
27827
  console.error("Usage: openacp api session-config <session-id> set <config-id> <value>");
27077
27828
  process.exit(1);
27078
27829
  }
@@ -27083,9 +27834,11 @@ Client overrides: ${JSON.stringify(clientOverrides)}`);
27083
27834
  });
27084
27835
  const data = await res.json();
27085
27836
  if (!res.ok) {
27837
+ if (json) jsonError(ErrorCodes.API_ERROR, String(data.error ?? "API request failed"));
27086
27838
  console.error(`Error: ${data.error}`);
27087
27839
  process.exit(1);
27088
27840
  }
27841
+ if (json) jsonSuccess(data);
27089
27842
  console.log(`Config option "${configId}" updated to "${value}"`);
27090
27843
  const configOptions = data.configOptions;
27091
27844
  const updated = configOptions?.find((o) => o.id === configId);
@@ -27096,9 +27849,11 @@ Client overrides: ${JSON.stringify(clientOverrides)}`);
27096
27849
  const res = await call(`/api/sessions/${encodeURIComponent(sessionId)}/config/overrides`);
27097
27850
  const data = await res.json();
27098
27851
  if (!res.ok) {
27852
+ if (json) jsonError(ErrorCodes.API_ERROR, String(data.error ?? "API request failed"));
27099
27853
  console.error(`Error: ${data.error}`);
27100
27854
  process.exit(1);
27101
27855
  }
27856
+ if (json) jsonSuccess(data);
27102
27857
  const overrides = data.clientOverrides;
27103
27858
  if (!overrides || Object.keys(overrides).length === 0) {
27104
27859
  console.log("No client overrides set.");
@@ -27111,6 +27866,7 @@ Client overrides: ${JSON.stringify(clientOverrides)}`);
27111
27866
  } else if (configSubCmd === "dangerous") {
27112
27867
  const toggle = args2[3];
27113
27868
  if (toggle && toggle !== "on" && toggle !== "off") {
27869
+ if (json) jsonError(ErrorCodes.MISSING_ARGUMENT, "Toggle value must be on or off");
27114
27870
  console.error("Usage: openacp api session-config <session-id> dangerous [on|off]");
27115
27871
  process.exit(1);
27116
27872
  }
@@ -27122,23 +27878,28 @@ Client overrides: ${JSON.stringify(clientOverrides)}`);
27122
27878
  });
27123
27879
  const data = await res.json();
27124
27880
  if (!res.ok) {
27881
+ if (json) jsonError(ErrorCodes.API_ERROR, String(data.error ?? "API request failed"));
27125
27882
  console.error(`Error: ${data.error}`);
27126
27883
  process.exit(1);
27127
27884
  }
27885
+ if (json) jsonSuccess(data);
27128
27886
  const state = toggle === "on" ? "enabled" : "disabled";
27129
27887
  console.log(`bypassPermissions ${state} for session ${sessionId}`);
27130
27888
  } else {
27131
27889
  const res = await call(`/api/sessions/${encodeURIComponent(sessionId)}/config/overrides`);
27132
27890
  const data = await res.json();
27133
27891
  if (!res.ok) {
27892
+ if (json) jsonError(ErrorCodes.API_ERROR, String(data.error ?? "API request failed"));
27134
27893
  console.error(`Error: ${data.error}`);
27135
27894
  process.exit(1);
27136
27895
  }
27896
+ if (json) jsonSuccess(data);
27137
27897
  const overrides = data.clientOverrides;
27138
27898
  const bypass = overrides?.bypassPermissions;
27139
27899
  console.log(`bypassPermissions: ${bypass ?? false}`);
27140
27900
  }
27141
27901
  } else {
27902
+ if (json) jsonError(ErrorCodes.UNKNOWN_COMMAND, `Unknown session-config subcommand: ${configSubCmd}`);
27142
27903
  console.error(`Unknown session-config subcommand: ${configSubCmd}`);
27143
27904
  console.log(" openacp api session-config <id> List config options");
27144
27905
  console.log(" openacp api session-config <id> set <opt> <value> Set a config option");
@@ -27170,6 +27931,7 @@ Client overrides: ${JSON.stringify(clientOverrides)}`);
27170
27931
  "session-config"
27171
27932
  ];
27172
27933
  const suggestion = suggestMatch2(subCmd ?? "", apiSubcommands);
27934
+ if (json) jsonError(ErrorCodes.UNKNOWN_COMMAND, `Unknown api command: ${subCmd || "(none)"}`);
27173
27935
  console.error(`Unknown api command: ${subCmd || "(none)"}
27174
27936
  `);
27175
27937
  if (suggestion) console.error(`Did you mean: ${suggestion}?
@@ -27178,11 +27940,14 @@ Client overrides: ${JSON.stringify(clientOverrides)}`);
27178
27940
  process.exit(1);
27179
27941
  }
27180
27942
  } catch (err) {
27943
+ if (err instanceof Error && err.message.startsWith("process.exit")) throw err;
27181
27944
  if (err instanceof TypeError && err.cause?.code === "ECONNREFUSED") {
27945
+ if (json) jsonError(ErrorCodes.API_ERROR, "OpenACP is not running (stale port file)");
27182
27946
  console.error("OpenACP is not running (stale port file)");
27183
27947
  removeStalePortFile(void 0, instanceRoot);
27184
27948
  process.exit(1);
27185
27949
  }
27950
+ if (json) jsonError(ErrorCodes.API_ERROR, err.message);
27186
27951
  throw err;
27187
27952
  }
27188
27953
  }
@@ -27190,12 +27955,15 @@ Client overrides: ${JSON.stringify(clientOverrides)}`);
27190
27955
  // src/cli/commands/start.ts
27191
27956
  init_version();
27192
27957
  init_helpers();
27958
+ init_output();
27193
27959
  init_instance_hint();
27194
27960
  import path35 from "path";
27195
27961
  import os16 from "os";
27196
27962
  async function cmdStart(args2 = [], instanceRoot) {
27963
+ const json = isJsonMode(args2);
27964
+ if (json) await muteForJson();
27197
27965
  const root = instanceRoot ?? path35.join(os16.homedir(), ".openacp");
27198
- if (wantsHelp(args2)) {
27966
+ if (!json && wantsHelp(args2)) {
27199
27967
  console.log(`
27200
27968
  \x1B[1mopenacp start\x1B[0m \u2014 Start OpenACP as a background daemon
27201
27969
 
@@ -27205,6 +27973,10 @@ async function cmdStart(args2 = [], instanceRoot) {
27205
27973
  Starts the server as a background process (daemon mode).
27206
27974
  Requires an existing config \u2014 run 'openacp' first to set up.
27207
27975
 
27976
+ \x1B[1mOptions:\x1B[0m
27977
+ --json Output result as JSON
27978
+ -h, --help Show this help message
27979
+
27208
27980
  \x1B[1mSee also:\x1B[0m
27209
27981
  openacp stop Stop the daemon
27210
27982
  openacp restart Restart the daemon
@@ -27222,12 +27994,15 @@ Requires an existing config \u2014 run 'openacp' first to set up.
27222
27994
  const config = cm.get();
27223
27995
  const result = startDaemon2(getPidPath2(root), config.logging.logDir, root);
27224
27996
  if ("error" in result) {
27997
+ if (json) jsonError(ErrorCodes.DAEMON_NOT_RUNNING, result.error);
27225
27998
  console.error(result.error);
27226
27999
  process.exit(1);
27227
28000
  }
28001
+ if (json) jsonSuccess({ pid: result.pid, instanceId: path35.basename(root), dir: root });
27228
28002
  printInstanceHint(root);
27229
28003
  console.log(`OpenACP daemon started (PID ${result.pid})`);
27230
28004
  } else {
28005
+ if (json) jsonError(ErrorCodes.CONFIG_NOT_FOUND, 'No config found. Run "openacp" first to set up.');
27231
28006
  console.error('No config found. Run "openacp" first to set up.');
27232
28007
  process.exit(1);
27233
28008
  }
@@ -27242,6 +28017,7 @@ init_logs();
27242
28017
  // src/cli/commands/config.ts
27243
28018
  init_api_client();
27244
28019
  init_helpers();
28020
+ init_output();
27245
28021
  import * as pathMod from "path";
27246
28022
  async function cmdConfig(args2 = [], instanceRoot) {
27247
28023
  const subCmd = args2[0];
@@ -27257,6 +28033,7 @@ async function cmdConfig(args2 = [], instanceRoot) {
27257
28033
  <value> New value (JSON-parsed if possible, otherwise string)
27258
28034
 
27259
28035
  \x1B[1mOptions:\x1B[0m
28036
+ --json Output result as JSON
27260
28037
  -h, --help Show this help message
27261
28038
 
27262
28039
  Works with both running and stopped daemon. When running, uses
@@ -27278,6 +28055,7 @@ the API for live updates. When stopped, edits config file directly.
27278
28055
  openacp config set <key> <value> Set a config value directly
27279
28056
 
27280
28057
  \x1B[1mOptions:\x1B[0m
28058
+ --json Output result as JSON
27281
28059
  -h, --help Show this help message
27282
28060
 
27283
28061
  Works with both running and stopped daemon. When running, uses
@@ -27292,9 +28070,12 @@ the API for live updates. When stopped, edits config file directly.
27292
28070
  return;
27293
28071
  }
27294
28072
  if (subCmd === "set") {
28073
+ const json = isJsonMode(args2);
28074
+ if (json) await muteForJson();
27295
28075
  const configPath = args2[1];
27296
28076
  const configValue = args2[2];
27297
28077
  if (!configPath || configValue === void 0) {
28078
+ if (json) jsonError(ErrorCodes.MISSING_ARGUMENT, "Missing required arguments: <path> and <value>");
27298
28079
  console.error("Usage: openacp config set <path> <value>");
27299
28080
  process.exit(1);
27300
28081
  }
@@ -27302,6 +28083,7 @@ the API for live updates. When stopped, edits config file directly.
27302
28083
  const topLevelKey = configPath.split(".")[0];
27303
28084
  const validConfigKeys = Object.keys(ConfigSchema2.shape);
27304
28085
  if (!validConfigKeys.includes(topLevelKey)) {
28086
+ if (json) jsonError(ErrorCodes.CONFIG_INVALID, `Unknown config key: ${topLevelKey}`);
27305
28087
  const { suggestMatch: suggestMatch2 } = await Promise.resolve().then(() => (init_suggest(), suggest_exports));
27306
28088
  const suggestion = suggestMatch2(topLevelKey, validConfigKeys);
27307
28089
  console.error(`Unknown config key: ${topLevelKey}`);
@@ -27322,9 +28104,11 @@ the API for live updates. When stopped, edits config file directly.
27322
28104
  }, instanceRoot);
27323
28105
  const data = await res.json();
27324
28106
  if (!res.ok) {
28107
+ if (json) jsonError(ErrorCodes.API_ERROR, `${data.error}`);
27325
28108
  console.error(`Error: ${data.error}`);
27326
28109
  process.exit(1);
27327
28110
  }
28111
+ if (json) jsonSuccess({ path: configPath, value, needsRestart: data.needsRestart ?? false });
27328
28112
  console.log(`Config updated: ${configPath} = ${JSON.stringify(value)}`);
27329
28113
  if (data.needsRestart) {
27330
28114
  console.log("Note: restart required for this change to take effect.");
@@ -27333,12 +28117,14 @@ the API for live updates. When stopped, edits config file directly.
27333
28117
  const { ConfigManager: ConfigManager3 } = await Promise.resolve().then(() => (init_config2(), config_exports));
27334
28118
  const cm2 = new ConfigManager3(instanceRoot ? pathMod.join(instanceRoot, "config.json") : void 0);
27335
28119
  if (!await cm2.exists()) {
28120
+ if (json) jsonError(ErrorCodes.CONFIG_NOT_FOUND, 'No config found. Run "openacp" first to set up.');
27336
28121
  console.error('No config found. Run "openacp" first to set up.');
27337
28122
  process.exit(1);
27338
28123
  }
27339
28124
  await cm2.load();
27340
28125
  const updates = buildNestedUpdateFromPath(configPath, value);
27341
28126
  await cm2.save(updates);
28127
+ if (json) jsonSuccess({ path: configPath, value, needsRestart: false });
27342
28128
  console.log(`Config updated: ${configPath} = ${JSON.stringify(value)}`);
27343
28129
  }
27344
28130
  return;
@@ -27439,6 +28225,7 @@ installs it globally if an update is available.
27439
28225
  // src/cli/commands/adopt.ts
27440
28226
  init_api_client();
27441
28227
  init_helpers();
28228
+ init_output();
27442
28229
  async function cmdAdopt(args2) {
27443
28230
  if (wantsHelp(args2)) {
27444
28231
  console.log(`
@@ -27454,6 +28241,7 @@ async function cmdAdopt(args2) {
27454
28241
  \x1B[1mOptions:\x1B[0m
27455
28242
  --cwd <path> Working directory for the session (default: current dir)
27456
28243
  --channel <name> Target channel adapter (e.g. telegram, discord). Default: first registered
28244
+ --json Output result as JSON
27457
28245
  -h, --help Show this help message
27458
28246
 
27459
28247
  Transfers an existing agent session into OpenACP so it appears
@@ -27466,9 +28254,23 @@ as a messaging thread. Requires a running daemon.
27466
28254
  `);
27467
28255
  return;
27468
28256
  }
27469
- const agent = args2[0];
27470
- const sessionId = args2[1];
28257
+ const json = isJsonMode(args2);
28258
+ if (json) await muteForJson();
28259
+ const skipFlags = /* @__PURE__ */ new Set(["--json", "-h", "--help"]);
28260
+ const skipWithValue = /* @__PURE__ */ new Set(["--cwd", "--channel"]);
28261
+ const positional = [];
28262
+ for (let i = 0; i < args2.length; i++) {
28263
+ if (skipWithValue.has(args2[i])) {
28264
+ i++;
28265
+ continue;
28266
+ }
28267
+ if (skipFlags.has(args2[i])) continue;
28268
+ positional.push(args2[i]);
28269
+ }
28270
+ const agent = positional[0];
28271
+ const sessionId = positional[1];
27471
28272
  if (!agent || !sessionId) {
28273
+ if (json) jsonError(ErrorCodes.MISSING_ARGUMENT, "Missing required arguments: <agent> and <session_id>");
27472
28274
  console.log("Usage: openacp adopt <agent> <session_id> [--cwd <path>] [--channel <name>]");
27473
28275
  console.log("Example: openacp adopt claude abc123-def456 --cwd /path/to/project");
27474
28276
  process.exit(1);
@@ -27479,6 +28281,7 @@ as a messaging thread. Requires a running daemon.
27479
28281
  const channel = channelIdx !== -1 && args2[channelIdx + 1] ? args2[channelIdx + 1] : void 0;
27480
28282
  const port = readApiPort();
27481
28283
  if (!port) {
28284
+ if (json) jsonError(ErrorCodes.DAEMON_NOT_RUNNING, "OpenACP is not running. Start it with: openacp start");
27482
28285
  console.log("OpenACP is not running. Start it with: openacp start");
27483
28286
  process.exit(1);
27484
28287
  }
@@ -27491,6 +28294,7 @@ as a messaging thread. Requires a running daemon.
27491
28294
  });
27492
28295
  const data = await res.json();
27493
28296
  if (data.ok) {
28297
+ if (json) jsonSuccess({ sessionId: data.sessionId, threadId: data.threadId, agent, status: data.status ?? "new" });
27494
28298
  if (data.status === "existing") {
27495
28299
  console.log(`Session already active. Topic pinged.`);
27496
28300
  } else {
@@ -27499,10 +28303,13 @@ as a messaging thread. Requires a running daemon.
27499
28303
  console.log(` Session ID: ${data.sessionId}`);
27500
28304
  console.log(` Thread ID: ${data.threadId}`);
27501
28305
  } else {
28306
+ if (json) jsonError(ErrorCodes.API_ERROR, `${data.message || data.error || "Unknown error"}`);
27502
28307
  console.log(`Error: ${data.message || data.error}`);
27503
28308
  process.exit(1);
27504
28309
  }
27505
28310
  } catch (err) {
28311
+ if (err instanceof Error && err.message.startsWith("process.exit")) throw err;
28312
+ if (json) jsonError(ErrorCodes.API_ERROR, `Failed to connect to OpenACP: ${err instanceof Error ? err.message : err}`);
27506
28313
  console.log(`Failed to connect to OpenACP: ${err instanceof Error ? err.message : err}`);
27507
28314
  process.exit(1);
27508
28315
  }
@@ -27581,8 +28388,11 @@ a "Handoff" slash command to Claude Code.
27581
28388
 
27582
28389
  // src/cli/commands/doctor.ts
27583
28390
  init_helpers();
28391
+ init_output();
27584
28392
  async function cmdDoctor(args2, instanceRoot) {
27585
- if (wantsHelp(args2)) {
28393
+ const json = isJsonMode(args2);
28394
+ if (json) await muteForJson();
28395
+ if (!json && wantsHelp(args2)) {
27586
28396
  console.log(`
27587
28397
  \x1B[1mopenacp doctor\x1B[0m \u2014 Run system diagnostics
27588
28398
 
@@ -27591,6 +28401,7 @@ async function cmdDoctor(args2, instanceRoot) {
27591
28401
 
27592
28402
  \x1B[1mOptions:\x1B[0m
27593
28403
  --dry-run Check only, don't apply any fixes
28404
+ --json Output result as JSON
27594
28405
  -h, --help Show this help message
27595
28406
 
27596
28407
  Checks your OpenACP installation for common issues including
@@ -27599,11 +28410,11 @@ Fixable issues can be auto-repaired when not using --dry-run.
27599
28410
  `);
27600
28411
  return;
27601
28412
  }
27602
- const knownFlags = ["--dry-run"];
28413
+ const knownFlags = ["--dry-run", "--json"];
27603
28414
  const unknownFlags = args2.filter(
27604
28415
  (a) => a.startsWith("--") && !knownFlags.includes(a)
27605
28416
  );
27606
- if (unknownFlags.length > 0) {
28417
+ if (!json && unknownFlags.length > 0) {
27607
28418
  const { suggestMatch: suggestMatch2 } = await Promise.resolve().then(() => (init_suggest(), suggest_exports));
27608
28419
  for (const flag of unknownFlags) {
27609
28420
  const suggestion = suggestMatch2(flag, knownFlags);
@@ -27612,11 +28423,24 @@ Fixable issues can be auto-repaired when not using --dry-run.
27612
28423
  }
27613
28424
  process.exit(1);
27614
28425
  }
27615
- const dryRun = args2.includes("--dry-run");
28426
+ const dryRun = args2.includes("--dry-run") || json;
27616
28427
  const { DoctorEngine: DoctorEngine2 } = await Promise.resolve().then(() => (init_doctor(), doctor_exports));
27617
28428
  const engine = new DoctorEngine2({ dryRun, dataDir: instanceRoot });
27618
- console.log("\n\u{1FA7A} OpenACP Doctor\n");
28429
+ if (!json) console.log("\n\u{1FA7A} OpenACP Doctor\n");
27619
28430
  const report = await engine.runAll();
28431
+ if (json) {
28432
+ jsonSuccess({
28433
+ categories: report.categories.map((c3) => ({
28434
+ name: c3.name,
28435
+ results: c3.results.map((r) => ({ status: r.status, message: r.message }))
28436
+ })),
28437
+ summary: {
28438
+ passed: report.summary.passed,
28439
+ warnings: report.summary.warnings,
28440
+ failed: report.summary.failed
28441
+ }
28442
+ });
28443
+ }
27620
28444
  const icons = { pass: "\x1B[32m\u2705\x1B[0m", warn: "\x1B[33m\u26A0\uFE0F\x1B[0m", fail: "\x1B[31m\u274C\x1B[0m" };
27621
28445
  for (const category of report.categories) {
27622
28446
  console.log(`\x1B[1m\x1B[36m${category.name}\x1B[0m`);
@@ -27659,6 +28483,7 @@ Fixable issues can be auto-repaired when not using --dry-run.
27659
28483
 
27660
28484
  // src/cli/commands/agents.ts
27661
28485
  init_helpers();
28486
+ init_output();
27662
28487
  async function createCatalog(instanceRoot) {
27663
28488
  const { AgentCatalog: AgentCatalog2 } = await Promise.resolve().then(() => (init_agent_catalog(), agent_catalog_exports));
27664
28489
  if (instanceRoot) {
@@ -27684,6 +28509,7 @@ async function cmdAgents(args2, instanceRoot) {
27684
28509
  openacp agents refresh Force-refresh agent list from registry
27685
28510
 
27686
28511
  \x1B[1mOptions:\x1B[0m
28512
+ --json Output result as JSON
27687
28513
  -h, --help Show this help message
27688
28514
 
27689
28515
  \x1B[1mExamples:\x1B[0m
@@ -27695,11 +28521,12 @@ async function cmdAgents(args2, instanceRoot) {
27695
28521
  `);
27696
28522
  return;
27697
28523
  }
28524
+ const positional = args2.slice(1).find((a) => !a.startsWith("-"));
27698
28525
  switch (subcommand) {
27699
28526
  case "install":
27700
- return agentsInstall(args2[1], args2.includes("--force"), wantsHelp(args2), instanceRoot);
28527
+ return agentsInstall(positional, args2.includes("--force"), wantsHelp(args2), instanceRoot, isJsonMode(args2));
27701
28528
  case "uninstall":
27702
- return agentsUninstall(args2[1], wantsHelp(args2), instanceRoot);
28529
+ return agentsUninstall(positional, wantsHelp(args2), instanceRoot, isJsonMode(args2));
27703
28530
  case "refresh":
27704
28531
  if (wantsHelp(args2)) {
27705
28532
  console.log(`
@@ -27715,7 +28542,7 @@ bypassing the normal staleness check.
27715
28542
  }
27716
28543
  return agentsRefresh(instanceRoot);
27717
28544
  case "info":
27718
- return agentsInfo(args2[1], wantsHelp(args2), instanceRoot);
28545
+ return agentsInfo(positional, wantsHelp(args2), instanceRoot, isJsonMode(args2));
27719
28546
  case "run":
27720
28547
  return agentsRun(args2[1], args2.slice(2), wantsHelp(args2), instanceRoot);
27721
28548
  case "list":
@@ -27734,22 +28561,24 @@ Run 'openacp agents' to see available agents.`);
27734
28561
  }
27735
28562
  }
27736
28563
  async function agentsList(instanceRoot, json = false) {
28564
+ if (json) await muteForJson();
27737
28565
  const catalog = await createCatalog(instanceRoot);
27738
28566
  catalog.load();
27739
28567
  await catalog.refreshRegistryIfStale();
27740
28568
  const items = catalog.getAvailable();
27741
28569
  if (json) {
27742
- console.log(JSON.stringify(items.map((item) => ({
27743
- key: item.key,
27744
- name: item.name,
27745
- version: item.version,
27746
- distribution: item.distribution,
27747
- description: item.description ?? "",
27748
- installed: item.installed,
27749
- available: item.available ?? true,
27750
- missingDeps: item.missingDeps ?? []
27751
- }))));
27752
- return;
28570
+ jsonSuccess({
28571
+ agents: items.map((item) => ({
28572
+ key: item.key,
28573
+ name: item.name,
28574
+ version: item.version,
28575
+ distribution: item.distribution,
28576
+ description: item.description ?? "",
28577
+ installed: item.installed,
28578
+ available: item.available ?? true,
28579
+ missingDeps: item.missingDeps ?? []
28580
+ }))
28581
+ });
27753
28582
  }
27754
28583
  const installed = items.filter((i) => i.installed);
27755
28584
  const available = items.filter((i) => !i.installed);
@@ -27786,8 +28615,9 @@ async function agentsList(instanceRoot, json = false) {
27786
28615
  );
27787
28616
  console.log("");
27788
28617
  }
27789
- async function agentsInstall(nameOrId, force, help = false, instanceRoot) {
27790
- if (help || !nameOrId) {
28618
+ async function agentsInstall(nameOrId, force, help = false, instanceRoot, json = false) {
28619
+ if (json) await muteForJson();
28620
+ if (!json && (help || !nameOrId)) {
27791
28621
  console.log(`
27792
28622
  \x1B[1mopenacp agents install\x1B[0m \u2014 Install an agent from the ACP Registry
27793
28623
 
@@ -27799,6 +28629,7 @@ async function agentsInstall(nameOrId, force, help = false, instanceRoot) {
27799
28629
 
27800
28630
  \x1B[1mOptions:\x1B[0m
27801
28631
  --force Reinstall even if already installed
28632
+ --json Output result as JSON
27802
28633
  -h, --help Show this help message
27803
28634
 
27804
28635
  \x1B[1mExamples:\x1B[0m
@@ -27809,10 +28640,25 @@ Run 'openacp agents' to see available agents.
27809
28640
  `);
27810
28641
  return;
27811
28642
  }
28643
+ if (!nameOrId) {
28644
+ if (json) jsonError(ErrorCodes.MISSING_ARGUMENT, "Agent name is required");
28645
+ return;
28646
+ }
27812
28647
  const catalog = await createCatalog(instanceRoot);
27813
28648
  catalog.load();
27814
28649
  await catalog.refreshRegistryIfStale();
27815
- const progress = {
28650
+ const progress = json ? {
28651
+ onStart() {
28652
+ },
28653
+ onStep() {
28654
+ },
28655
+ onDownloadProgress() {
28656
+ },
28657
+ onSuccess() {
28658
+ },
28659
+ onError() {
28660
+ }
28661
+ } : {
27816
28662
  onStart(_id, name) {
27817
28663
  process.stdout.write(`
27818
28664
  \u23F3 Installing ${name}...
@@ -27842,6 +28688,7 @@ Run 'openacp agents' to see available agents.
27842
28688
  };
27843
28689
  const result = await catalog.install(nameOrId, progress, force);
27844
28690
  if (!result.ok) {
28691
+ if (json) jsonError(ErrorCodes.INSTALL_FAILED, result.error ?? "Installation failed");
27845
28692
  if (result.error?.includes("not found")) {
27846
28693
  const { suggestMatch: suggestMatch2 } = await Promise.resolve().then(() => (init_suggest(), suggest_exports));
27847
28694
  const allKeys = catalog.getAvailable().map((a) => a.key);
@@ -27850,6 +28697,10 @@ Run 'openacp agents' to see available agents.
27850
28697
  }
27851
28698
  process.exit(1);
27852
28699
  }
28700
+ if (json) {
28701
+ const installed = catalog.getInstalledAgent(result.agentKey);
28702
+ jsonSuccess({ key: result.agentKey, version: installed?.version ?? "unknown", installed: true });
28703
+ }
27853
28704
  const { getAgentCapabilities: getAgentCapabilities2 } = await Promise.resolve().then(() => (init_agent_dependencies(), agent_dependencies_exports));
27854
28705
  const caps = getAgentCapabilities2(result.agentKey);
27855
28706
  if (caps.integration) {
@@ -27871,8 +28722,9 @@ Run 'openacp agents' to see available agents.
27871
28722
  `);
27872
28723
  }
27873
28724
  }
27874
- async function agentsUninstall(name, help = false, instanceRoot) {
27875
- if (help || !name) {
28725
+ async function agentsUninstall(name, help = false, instanceRoot, json = false) {
28726
+ if (json) await muteForJson();
28727
+ if (!json && (help || !name)) {
27876
28728
  console.log(`
27877
28729
  \x1B[1mopenacp agents uninstall\x1B[0m \u2014 Remove an installed agent
27878
28730
 
@@ -27882,11 +28734,19 @@ async function agentsUninstall(name, help = false, instanceRoot) {
27882
28734
  \x1B[1mArguments:\x1B[0m
27883
28735
  <name> Agent name to remove
27884
28736
 
28737
+ \x1B[1mOptions:\x1B[0m
28738
+ --json Output result as JSON
28739
+ -h, --help Show this help message
28740
+
27885
28741
  \x1B[1mExamples:\x1B[0m
27886
28742
  openacp agents uninstall gemini
27887
28743
  `);
27888
28744
  return;
27889
28745
  }
28746
+ if (!name) {
28747
+ if (json) jsonError(ErrorCodes.MISSING_ARGUMENT, "Agent name is required");
28748
+ return;
28749
+ }
27890
28750
  const catalog = await createCatalog(instanceRoot);
27891
28751
  catalog.load();
27892
28752
  const result = await catalog.uninstall(name);
@@ -27898,10 +28758,12 @@ async function agentsUninstall(name, help = false, instanceRoot) {
27898
28758
  await uninstallIntegration2(name, caps.integration);
27899
28759
  console.log(` \x1B[32m\u2713\x1B[0m Handoff integration removed for ${name}`);
27900
28760
  }
28761
+ if (json) jsonSuccess({ key: name, uninstalled: true });
27901
28762
  console.log(`
27902
28763
  \x1B[32m\u2713 ${name} removed.\x1B[0m
27903
28764
  `);
27904
28765
  } else {
28766
+ if (json) jsonError(ErrorCodes.UNINSTALL_FAILED, result.error ?? "Uninstall failed");
27905
28767
  console.log(`
27906
28768
  \x1B[31m\u2717 ${result.error}\x1B[0m`);
27907
28769
  if (result.error?.includes("not installed")) {
@@ -27920,8 +28782,9 @@ async function agentsRefresh(instanceRoot) {
27920
28782
  await catalog.fetchRegistry();
27921
28783
  console.log(" \x1B[32m\u2713 Agent list updated.\x1B[0m\n");
27922
28784
  }
27923
- async function agentsInfo(nameOrId, help = false, instanceRoot) {
27924
- if (help || !nameOrId) {
28785
+ async function agentsInfo(nameOrId, help = false, instanceRoot, json = false) {
28786
+ if (json) await muteForJson();
28787
+ if (!json && (help || !nameOrId)) {
27925
28788
  console.log(`
27926
28789
  \x1B[1mopenacp agents info\x1B[0m \u2014 Show agent details, dependencies & setup guide
27927
28790
 
@@ -27934,17 +28797,37 @@ async function agentsInfo(nameOrId, help = false, instanceRoot) {
27934
28797
  Shows version, distribution type, command, setup steps, and
27935
28798
  whether the agent is installed or available from the registry.
27936
28799
 
28800
+ \x1B[1mOptions:\x1B[0m
28801
+ --json Output result as JSON
28802
+ -h, --help Show this help message
28803
+
27937
28804
  \x1B[1mExamples:\x1B[0m
27938
28805
  openacp agents info claude
27939
28806
  openacp agents info cursor
27940
28807
  `);
27941
28808
  return;
27942
28809
  }
28810
+ if (!nameOrId) {
28811
+ if (json) jsonError(ErrorCodes.MISSING_ARGUMENT, "Agent name is required");
28812
+ return;
28813
+ }
27943
28814
  const catalog = await createCatalog(instanceRoot);
27944
28815
  catalog.load();
27945
28816
  const { getAgentSetup: getAgentSetup2 } = await Promise.resolve().then(() => (init_agent_dependencies(), agent_dependencies_exports));
27946
28817
  const installed = catalog.getInstalledAgent(nameOrId);
27947
28818
  if (installed) {
28819
+ if (json) {
28820
+ jsonSuccess({
28821
+ key: installed.registryId ?? nameOrId,
28822
+ name: installed.name,
28823
+ version: installed.version,
28824
+ distribution: installed.distribution,
28825
+ installed: true,
28826
+ command: installed.command,
28827
+ binaryPath: installed.binaryPath ?? null,
28828
+ registryId: installed.registryId ?? null
28829
+ });
28830
+ }
27948
28831
  console.log(`
27949
28832
  \x1B[1m${installed.name}\x1B[0m`);
27950
28833
  console.log(` Version: ${installed.version}`);
@@ -27967,6 +28850,15 @@ whether the agent is installed or available from the registry.
27967
28850
  }
27968
28851
  const regAgent = catalog.findRegistryAgent(nameOrId);
27969
28852
  if (regAgent) {
28853
+ if (json) {
28854
+ jsonSuccess({
28855
+ key: regAgent.id,
28856
+ name: regAgent.name,
28857
+ version: regAgent.version,
28858
+ description: regAgent.description ?? "",
28859
+ installed: false
28860
+ });
28861
+ }
27970
28862
  const availability = catalog.checkAvailability(nameOrId);
27971
28863
  console.log(`
27972
28864
  \x1B[1m${regAgent.name}\x1B[0m \x1B[2m(not installed)\x1B[0m`);
@@ -27989,6 +28881,7 @@ whether the agent is installed or available from the registry.
27989
28881
  `);
27990
28882
  return;
27991
28883
  }
28884
+ if (json) jsonError(ErrorCodes.AGENT_NOT_FOUND, `"${nameOrId}" not found.`);
27992
28885
  const { suggestMatch: suggestMatch2 } = await Promise.resolve().then(() => (init_suggest(), suggest_exports));
27993
28886
  const allKeys = catalog.getAvailable().map((a) => a.key);
27994
28887
  const suggestion = suggestMatch2(nameOrId, allKeys);
@@ -28070,10 +28963,14 @@ ACP-specific flags are automatically stripped.
28070
28963
 
28071
28964
  // src/cli/commands/tunnel.ts
28072
28965
  init_api_client();
28966
+ init_output();
28073
28967
  async function cmdTunnel(args2, instanceRoot) {
28968
+ const json = isJsonMode(args2);
28969
+ if (json) await muteForJson();
28074
28970
  const subCmd = args2[0];
28075
28971
  const port = readApiPort(void 0, instanceRoot);
28076
28972
  if (port === null) {
28973
+ if (json) jsonError(ErrorCodes.DAEMON_NOT_RUNNING, "OpenACP is not running.");
28077
28974
  console.error("OpenACP is not running. Start with `openacp start`");
28078
28975
  process.exit(1);
28079
28976
  }
@@ -28082,6 +28979,7 @@ async function cmdTunnel(args2, instanceRoot) {
28082
28979
  if (subCmd === "add") {
28083
28980
  const tunnelPort = args2[1];
28084
28981
  if (!tunnelPort) {
28982
+ if (json) jsonError(ErrorCodes.MISSING_ARGUMENT, "Port is required");
28085
28983
  console.error("Usage: openacp tunnel add <port> [--label name] [--session id]");
28086
28984
  process.exit(1);
28087
28985
  }
@@ -28099,13 +28997,25 @@ async function cmdTunnel(args2, instanceRoot) {
28099
28997
  });
28100
28998
  const data = await res.json();
28101
28999
  if (!res.ok) {
29000
+ if (json) jsonError(ErrorCodes.TUNNEL_ERROR, String(data.error));
28102
29001
  console.error(`Error: ${data.error}`);
28103
29002
  process.exit(1);
28104
29003
  }
29004
+ if (json) jsonSuccess({ port: data.port, publicUrl: data.publicUrl });
28105
29005
  console.log(`Tunnel active: port ${data.port} \u2192 ${data.publicUrl}`);
28106
29006
  } else if (subCmd === "list") {
28107
29007
  const res = await call("/api/tunnel/list");
28108
29008
  const data = await res.json();
29009
+ if (json) {
29010
+ jsonSuccess({
29011
+ tunnels: data.map((t) => ({
29012
+ port: t.port,
29013
+ label: t.label ?? null,
29014
+ publicUrl: t.publicUrl ?? null,
29015
+ status: t.status ?? "unknown"
29016
+ }))
29017
+ });
29018
+ }
28109
29019
  if (data.length === 0) {
28110
29020
  console.log("No active tunnels.");
28111
29021
  return;
@@ -28120,23 +29030,28 @@ async function cmdTunnel(args2, instanceRoot) {
28120
29030
  } else if (subCmd === "stop") {
28121
29031
  const tunnelPort = args2[1];
28122
29032
  if (!tunnelPort) {
29033
+ if (json) jsonError(ErrorCodes.MISSING_ARGUMENT, "Port is required");
28123
29034
  console.error("Usage: openacp tunnel stop <port>");
28124
29035
  process.exit(1);
28125
29036
  }
28126
29037
  const res = await call(`/api/tunnel/${tunnelPort}`, { method: "DELETE" });
28127
29038
  if (!res.ok) {
28128
29039
  const data = await res.json();
29040
+ if (json) jsonError(ErrorCodes.TUNNEL_ERROR, String(data.error));
28129
29041
  console.error(`Error: ${data.error}`);
28130
29042
  process.exit(1);
28131
29043
  }
29044
+ if (json) jsonSuccess({ port: parseInt(tunnelPort, 10), stopped: true });
28132
29045
  console.log(`Tunnel stopped: port ${tunnelPort}`);
28133
29046
  } else if (subCmd === "stop-all") {
28134
29047
  const res = await call("/api/tunnel", { method: "DELETE" });
28135
29048
  if (!res.ok) {
28136
29049
  const data = await res.json();
29050
+ if (json) jsonError(ErrorCodes.TUNNEL_ERROR, String(data.error));
28137
29051
  console.error(`Error: ${data.error}`);
28138
29052
  process.exit(1);
28139
29053
  }
29054
+ if (json) jsonSuccess({ stopped: true });
28140
29055
  console.log("All user tunnels stopped.");
28141
29056
  } else {
28142
29057
  console.log(`
@@ -28145,10 +29060,16 @@ Tunnel Management:
28145
29060
  openacp tunnel list
28146
29061
  openacp tunnel stop <port>
28147
29062
  openacp tunnel stop-all
29063
+
29064
+ Options:
29065
+ --json Output result as JSON
28148
29066
  `);
28149
29067
  }
28150
29068
  } catch (err) {
28151
- console.error(`Failed to connect to daemon: ${err.message}`);
29069
+ const msg = err.message;
29070
+ if (msg?.startsWith("process.exit")) throw err;
29071
+ if (json) jsonError(ErrorCodes.TUNNEL_ERROR, msg);
29072
+ console.error(`Failed to connect to daemon: ${msg}`);
28152
29073
  process.exit(1);
28153
29074
  }
28154
29075
  }
@@ -28179,9 +29100,11 @@ async function cmdOnboard(instanceRoot) {
28179
29100
  // src/cli/commands/default.ts
28180
29101
  init_version();
28181
29102
  init_instance_context();
29103
+ init_instance_registry();
28182
29104
  init_instance_hint();
28183
29105
  import path58 from "path";
28184
29106
  import os27 from "os";
29107
+ import { randomUUID as randomUUID5 } from "crypto";
28185
29108
  async function cmdDefault(command2, instanceRoot) {
28186
29109
  const root = instanceRoot ?? path58.join(os27.homedir(), ".openacp");
28187
29110
  const pluginsDataDir = path58.join(root, "plugins", "data");
@@ -28251,8 +29174,11 @@ async function cmdDefault(command2, instanceRoot) {
28251
29174
  markRunning2(root);
28252
29175
  printInstanceHint(root);
28253
29176
  const { startServer: startServer2 } = await Promise.resolve().then(() => (init_main(), main_exports));
29177
+ const reg = new InstanceRegistry(path58.join(getGlobalRoot(), "instances.json"));
29178
+ reg.load();
29179
+ const existingEntry = reg.getByRoot(root);
28254
29180
  const ctx = createInstanceContext({
28255
- id: "default",
29181
+ id: existingEntry?.id ?? randomUUID5(),
28256
29182
  root,
28257
29183
  isGlobal: root === getGlobalRoot()
28258
29184
  });
@@ -28450,10 +29376,13 @@ Press Ctrl+C to detach.
28450
29376
  // src/cli/commands/remote.ts
28451
29377
  init_api_client();
28452
29378
  init_instance_registry();
29379
+ init_output();
28453
29380
  import path61 from "path";
28454
29381
  import os29 from "os";
28455
29382
  import qrcode from "qrcode-terminal";
28456
29383
  async function cmdRemote(args2, instanceRoot) {
29384
+ const json = isJsonMode(args2);
29385
+ if (json) await muteForJson();
28457
29386
  const role = extractFlag(args2, "--role") ?? "admin";
28458
29387
  const expire = extractFlag(args2, "--expire") ?? "24h";
28459
29388
  const scopesRaw = extractFlag(args2, "--scopes");
@@ -28469,6 +29398,7 @@ async function cmdRemote(args2, instanceRoot) {
28469
29398
  await registry.load();
28470
29399
  const entry = registry.get(instanceId);
28471
29400
  if (!entry) {
29401
+ if (json) jsonError(ErrorCodes.INSTANCE_NOT_FOUND, `Workspace "${instanceId}" not found. Run "openacp status" to see workspaces.`);
28472
29402
  console.error(`Workspace "${instanceId}" not found. Run "openacp status" to see workspaces.`);
28473
29403
  process.exit(1);
28474
29404
  }
@@ -28476,21 +29406,26 @@ async function cmdRemote(args2, instanceRoot) {
28476
29406
  }
28477
29407
  const port = readApiPort(void 0, resolvedInstanceRoot2);
28478
29408
  if (port === null) {
29409
+ if (json) jsonError(ErrorCodes.DAEMON_NOT_RUNNING, "OpenACP is not running. Start with `openacp start`");
28479
29410
  console.error("OpenACP is not running. Start with `openacp start`");
28480
29411
  process.exit(1);
28481
29412
  }
28482
29413
  try {
28483
29414
  const healthRes = await apiCall(port, "/api/v1/system/health", void 0, resolvedInstanceRoot2);
28484
29415
  if (!healthRes.ok) {
29416
+ if (json) jsonError(ErrorCodes.API_ERROR, "API server is not responding. Try restarting with `openacp restart`");
28485
29417
  console.error("API server is not responding. Try restarting with `openacp restart`");
28486
29418
  process.exit(1);
28487
29419
  }
28488
- } catch {
29420
+ } catch (err) {
29421
+ if (err instanceof Error && err.message.startsWith("process.exit")) throw err;
29422
+ if (json) jsonError(ErrorCodes.API_ERROR, "Cannot connect to API server. Is OpenACP running?");
28489
29423
  console.error("Cannot connect to API server. Is OpenACP running?");
28490
29424
  process.exit(1);
28491
29425
  }
28492
29426
  const secret = readApiSecret(void 0, resolvedInstanceRoot2);
28493
29427
  if (!secret) {
29428
+ if (json) jsonError(ErrorCodes.API_ERROR, "Cannot read API secret. Make sure OpenACP is running with the API server enabled.");
28494
29429
  console.error("Cannot read API secret. Make sure OpenACP is running with the API server enabled.");
28495
29430
  process.exit(1);
28496
29431
  }
@@ -28512,11 +29447,14 @@ async function cmdRemote(args2, instanceRoot) {
28512
29447
  }, resolvedInstanceRoot2);
28513
29448
  if (!res.ok) {
28514
29449
  const err = await res.json();
29450
+ if (json) jsonError(ErrorCodes.API_ERROR, `Failed to generate code: ${err.error ?? err.message ?? "Unknown error"}`);
28515
29451
  console.error(`Failed to generate code: ${err.error ?? err.message ?? "Unknown error"}`);
28516
29452
  process.exit(1);
28517
29453
  }
28518
29454
  codeData = await res.json();
28519
29455
  } catch (err) {
29456
+ if (err instanceof Error && err.message.startsWith("process.exit")) throw err;
29457
+ if (json) jsonError(ErrorCodes.API_ERROR, `Failed to generate code: ${err.message}`);
28520
29458
  console.error(`Failed to generate code: ${err.message}`);
28521
29459
  process.exit(1);
28522
29460
  }
@@ -28538,6 +29476,19 @@ async function cmdRemote(args2, instanceRoot) {
28538
29476
  const tunnelLink = tunnelUrl ? `${tunnelUrl}?code=${code}` : null;
28539
29477
  const appLink = tunnelUrl ? `openacp://connect?host=${new URL(tunnelUrl).host}&code=${code}` : null;
28540
29478
  const expireDisplay = expiresAt;
29479
+ if (json) {
29480
+ jsonSuccess({
29481
+ code,
29482
+ name: tokenName,
29483
+ role,
29484
+ expiresAt,
29485
+ urls: {
29486
+ local: localUrl,
29487
+ tunnel: tunnelLink ?? void 0,
29488
+ app: appLink ?? void 0
29489
+ }
29490
+ });
29491
+ }
28541
29492
  const W = 64;
28542
29493
  const line = "\u2500".repeat(W - 4);
28543
29494
  console.log("");
@@ -28578,6 +29529,7 @@ function extractFlag(args2, flag) {
28578
29529
  }
28579
29530
 
28580
29531
  // src/cli/commands/setup.ts
29532
+ init_output();
28581
29533
  import * as fs51 from "fs";
28582
29534
  import * as path62 from "path";
28583
29535
  function parseFlag(args2, flag) {
@@ -28588,29 +29540,21 @@ async function cmdSetup(args2, instanceRoot) {
28588
29540
  const workspace = parseFlag(args2, "--workspace");
28589
29541
  const agentRaw = parseFlag(args2, "--agent");
28590
29542
  const json = args2.includes("--json");
29543
+ if (json) await muteForJson();
28591
29544
  if (!workspace) {
28592
- if (json) {
28593
- console.log(JSON.stringify({ success: false, error: "--workspace is required" }));
28594
- } else {
28595
- console.error(" Error: --workspace <path> is required");
28596
- }
29545
+ if (json) jsonError(ErrorCodes.MISSING_ARGUMENT, "--workspace is required");
29546
+ console.error(" Error: --workspace <path> is required");
28597
29547
  process.exit(1);
28598
29548
  }
28599
29549
  if (!agentRaw) {
28600
- if (json) {
28601
- console.log(JSON.stringify({ success: false, error: "--agent is required" }));
28602
- } else {
28603
- console.error(" Error: --agent <name> is required");
28604
- }
29550
+ if (json) jsonError(ErrorCodes.MISSING_ARGUMENT, "--agent is required");
29551
+ console.error(" Error: --agent <name> is required");
28605
29552
  process.exit(1);
28606
29553
  }
28607
29554
  const rawRunMode = parseFlag(args2, "--run-mode") ?? "daemon";
28608
29555
  if (rawRunMode !== "daemon" && rawRunMode !== "foreground") {
28609
- if (json) {
28610
- console.log(JSON.stringify({ success: false, error: `--run-mode must be 'daemon' or 'foreground'` }));
28611
- } else {
28612
- console.error(` Error: --run-mode must be 'daemon' or 'foreground'`);
28613
- }
29556
+ if (json) jsonError(ErrorCodes.CONFIG_INVALID, `--run-mode must be 'daemon' or 'foreground'`);
29557
+ console.error(` Error: --run-mode must be 'daemon' or 'foreground'`);
28614
29558
  process.exit(1);
28615
29559
  }
28616
29560
  const runMode = rawRunMode;
@@ -28639,7 +29583,7 @@ async function cmdSetup(args2, instanceRoot) {
28639
29583
  fs51.mkdirSync(instanceRoot, { recursive: true });
28640
29584
  fs51.writeFileSync(configPath, JSON.stringify(config, null, 2));
28641
29585
  if (json) {
28642
- console.log(JSON.stringify({ success: true, configPath }));
29586
+ jsonSuccess({ configPath });
28643
29587
  } else {
28644
29588
  console.log(`
28645
29589
  \x1B[32m\u2713 Setup complete.\x1B[0m Config written to ${configPath}
@@ -28698,8 +29642,8 @@ resolvedInstanceRoot = resolveInstanceRoot({
28698
29642
  var noInstanceCommands = {
28699
29643
  "--help": async () => printHelp(),
28700
29644
  "-h": async () => printHelp(),
28701
- "--version": () => cmdVersion(),
28702
- "-v": () => cmdVersion(),
29645
+ "--version": () => cmdVersion(args),
29646
+ "-v": () => cmdVersion(args),
28703
29647
  "update": () => cmdUpdate(args),
28704
29648
  "adopt": () => cmdAdopt(args),
28705
29649
  "integrate": () => cmdIntegrate(args),
@@ -28725,7 +29669,8 @@ async function main() {
28725
29669
  const registry = new InstanceRegistry2(path64.join(getGlobal(), "instances.json"));
28726
29670
  await registry.load();
28727
29671
  const entry = registry.getByRoot(envRoot);
28728
- const id = entry?.id ?? "unknown";
29672
+ const { randomUUID: randomUUID6 } = await import("crypto");
29673
+ const id = entry?.id ?? randomUUID6();
28729
29674
  const ctx = createInstanceContext2({
28730
29675
  id,
28731
29676
  root: envRoot,