zelari-code 1.30.4 → 1.31.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (42) hide show
  1. package/dist/cli/app.js +5 -0
  2. package/dist/cli/app.js.map +1 -1
  3. package/dist/cli/atMentions.js +77 -28
  4. package/dist/cli/atMentions.js.map +1 -1
  5. package/dist/cli/headless.js.map +1 -1
  6. package/dist/cli/hooks/useChatTurn.js +47 -30
  7. package/dist/cli/hooks/useChatTurn.js.map +1 -1
  8. package/dist/cli/hooks/usePermissionBroker.js +87 -0
  9. package/dist/cli/hooks/usePermissionBroker.js.map +1 -0
  10. package/dist/cli/kraken/executor.js +109 -5
  11. package/dist/cli/kraken/executor.js.map +1 -1
  12. package/dist/cli/kraken/executor.workbench.test.js +134 -0
  13. package/dist/cli/kraken/executor.workbench.test.js.map +1 -0
  14. package/dist/cli/kraken/workbench.js +1 -0
  15. package/dist/cli/kraken/workbench.js.map +1 -1
  16. package/dist/cli/main.bundled.js +1840 -366
  17. package/dist/cli/main.bundled.js.map +4 -4
  18. package/dist/cli/main.js +24 -0
  19. package/dist/cli/main.js.map +1 -1
  20. package/dist/cli/mcp/brokerHandlers.js +123 -0
  21. package/dist/cli/mcp/brokerHandlers.js.map +1 -0
  22. package/dist/cli/mcp/mcpPermissionServer.js +271 -0
  23. package/dist/cli/mcp/mcpPermissionServer.js.map +1 -0
  24. package/dist/cli/mcp/mcpPresets.js +85 -5
  25. package/dist/cli/mcp/mcpPresets.js.map +1 -1
  26. package/dist/cli/mcp/permissionBroker.js +253 -0
  27. package/dist/cli/mcp/permissionBroker.js.map +1 -0
  28. package/dist/cli/mcp/permissionCli.js +46 -0
  29. package/dist/cli/mcp/permissionCli.js.map +1 -0
  30. package/dist/cli/provider/localCli/claudeProvider.js +149 -0
  31. package/dist/cli/provider/localCli/claudeProvider.js.map +1 -0
  32. package/dist/cli/provider/localCli/claudeStreamJson.js +175 -0
  33. package/dist/cli/provider/localCli/claudeStreamJson.js.map +1 -0
  34. package/dist/cli/provider/openai-compatible.js +59 -0
  35. package/dist/cli/provider/openai-compatible.js.map +1 -1
  36. package/dist/cli/runHeadless.js +59 -12
  37. package/dist/cli/runHeadless.js.map +1 -1
  38. package/dist/cli/skillConfigIo.js +1 -0
  39. package/dist/cli/skillConfigIo.js.map +1 -1
  40. package/dist/cli/slashCommands.js +22 -1
  41. package/dist/cli/slashCommands.js.map +1 -1
  42. package/package.json +2 -2
@@ -395,7 +395,7 @@ async function refreshGrokToken(options) {
395
395
  return parseTokenResponseBody(obj, accessToken);
396
396
  }
397
397
  async function openBrowser(url2) {
398
- const { spawn: spawn12 } = await import("node:child_process");
398
+ const { spawn: spawn13 } = await import("node:child_process");
399
399
  const cmd = (() => {
400
400
  switch (process.platform) {
401
401
  case "darwin":
@@ -408,7 +408,7 @@ async function openBrowser(url2) {
408
408
  })();
409
409
  return new Promise((resolve3, reject) => {
410
410
  try {
411
- const child = spawn12(cmd.bin, cmd.args, {
411
+ const child = spawn13(cmd.bin, cmd.args, {
412
412
  stdio: "ignore",
413
413
  detached: true
414
414
  });
@@ -1829,10 +1829,10 @@ function mergeDefs(...defs) {
1829
1829
  function cloneDef(schema) {
1830
1830
  return mergeDefs(schema._zod.def);
1831
1831
  }
1832
- function getElementAtPath(obj, path47) {
1833
- if (!path47)
1832
+ function getElementAtPath(obj, path48) {
1833
+ if (!path48)
1834
1834
  return obj;
1835
- return path47.reduce((acc, key) => acc?.[key], obj);
1835
+ return path48.reduce((acc, key) => acc?.[key], obj);
1836
1836
  }
1837
1837
  function promiseAllObject(promisesObj) {
1838
1838
  const keys = Object.keys(promisesObj);
@@ -2160,11 +2160,11 @@ function explicitlyAborted(x, startIndex = 0) {
2160
2160
  }
2161
2161
  return false;
2162
2162
  }
2163
- function prefixIssues(path47, issues) {
2163
+ function prefixIssues(path48, issues) {
2164
2164
  return issues.map((iss) => {
2165
2165
  var _a3;
2166
2166
  (_a3 = iss).path ?? (_a3.path = []);
2167
- iss.path.unshift(path47);
2167
+ iss.path.unshift(path48);
2168
2168
  return iss;
2169
2169
  });
2170
2170
  }
@@ -2382,16 +2382,16 @@ function flattenError(error51, mapper = (issue2) => issue2.message) {
2382
2382
  }
2383
2383
  function formatError(error51, mapper = (issue2) => issue2.message) {
2384
2384
  const fieldErrors = { _errors: [] };
2385
- const processError = (error52, path47 = []) => {
2385
+ const processError = (error52, path48 = []) => {
2386
2386
  for (const issue2 of error52.issues) {
2387
2387
  if (issue2.code === "invalid_union" && issue2.errors.length) {
2388
- issue2.errors.map((issues) => processError({ issues }, [...path47, ...issue2.path]));
2388
+ issue2.errors.map((issues) => processError({ issues }, [...path48, ...issue2.path]));
2389
2389
  } else if (issue2.code === "invalid_key") {
2390
- processError({ issues: issue2.issues }, [...path47, ...issue2.path]);
2390
+ processError({ issues: issue2.issues }, [...path48, ...issue2.path]);
2391
2391
  } else if (issue2.code === "invalid_element") {
2392
- processError({ issues: issue2.issues }, [...path47, ...issue2.path]);
2392
+ processError({ issues: issue2.issues }, [...path48, ...issue2.path]);
2393
2393
  } else {
2394
- const fullpath = [...path47, ...issue2.path];
2394
+ const fullpath = [...path48, ...issue2.path];
2395
2395
  if (fullpath.length === 0) {
2396
2396
  fieldErrors._errors.push(mapper(issue2));
2397
2397
  } else {
@@ -2418,17 +2418,17 @@ function formatError(error51, mapper = (issue2) => issue2.message) {
2418
2418
  }
2419
2419
  function treeifyError(error51, mapper = (issue2) => issue2.message) {
2420
2420
  const result = { errors: [] };
2421
- const processError = (error52, path47 = []) => {
2421
+ const processError = (error52, path48 = []) => {
2422
2422
  var _a3, _b;
2423
2423
  for (const issue2 of error52.issues) {
2424
2424
  if (issue2.code === "invalid_union" && issue2.errors.length) {
2425
- issue2.errors.map((issues) => processError({ issues }, [...path47, ...issue2.path]));
2425
+ issue2.errors.map((issues) => processError({ issues }, [...path48, ...issue2.path]));
2426
2426
  } else if (issue2.code === "invalid_key") {
2427
- processError({ issues: issue2.issues }, [...path47, ...issue2.path]);
2427
+ processError({ issues: issue2.issues }, [...path48, ...issue2.path]);
2428
2428
  } else if (issue2.code === "invalid_element") {
2429
- processError({ issues: issue2.issues }, [...path47, ...issue2.path]);
2429
+ processError({ issues: issue2.issues }, [...path48, ...issue2.path]);
2430
2430
  } else {
2431
- const fullpath = [...path47, ...issue2.path];
2431
+ const fullpath = [...path48, ...issue2.path];
2432
2432
  if (fullpath.length === 0) {
2433
2433
  result.errors.push(mapper(issue2));
2434
2434
  continue;
@@ -2460,8 +2460,8 @@ function treeifyError(error51, mapper = (issue2) => issue2.message) {
2460
2460
  }
2461
2461
  function toDotPath(_path) {
2462
2462
  const segs = [];
2463
- const path47 = _path.map((seg) => typeof seg === "object" ? seg.key : seg);
2464
- for (const seg of path47) {
2463
+ const path48 = _path.map((seg) => typeof seg === "object" ? seg.key : seg);
2464
+ for (const seg of path48) {
2465
2465
  if (typeof seg === "number")
2466
2466
  segs.push(`[${seg}]`);
2467
2467
  else if (typeof seg === "symbol")
@@ -15964,13 +15964,13 @@ function resolveRef(ref, ctx) {
15964
15964
  if (!ref.startsWith("#")) {
15965
15965
  throw new Error("External $ref is not supported, only local refs (#/...) are allowed");
15966
15966
  }
15967
- const path47 = ref.slice(1).split("/").filter(Boolean);
15968
- if (path47.length === 0) {
15967
+ const path48 = ref.slice(1).split("/").filter(Boolean);
15968
+ if (path48.length === 0) {
15969
15969
  return ctx.rootSchema;
15970
15970
  }
15971
15971
  const defsKey = ctx.version === "draft-2020-12" ? "$defs" : "definitions";
15972
- if (path47[0] === defsKey) {
15973
- const key = path47[1];
15972
+ if (path48[0] === defsKey) {
15973
+ const key = path48[1];
15974
15974
  if (!key || !ctx.defs[key]) {
15975
15975
  throw new Error(`Reference not found: ${ref}`);
15976
15976
  }
@@ -18134,11 +18134,11 @@ var init_tools = __esm({
18134
18134
  if (!ctx.addDocument)
18135
18135
  return "Knowledge vault tool not available.";
18136
18136
  const title = args["title"] || "New Document";
18137
- const path47 = args["path"] || `notes/${title.toLowerCase().replace(/[^a-z0-9]+/g, "-")}`;
18137
+ const path48 = args["path"] || `notes/${title.toLowerCase().replace(/[^a-z0-9]+/g, "-")}`;
18138
18138
  const content = args["content"] || "";
18139
18139
  const tags = args["tags"] || [];
18140
18140
  ctx.addDocument({
18141
- path: path47,
18141
+ path: path48,
18142
18142
  title,
18143
18143
  content,
18144
18144
  format: "markdown",
@@ -18147,7 +18147,7 @@ var init_tools = __esm({
18147
18147
  workspaceId: ctx.workspaceId
18148
18148
  });
18149
18149
  ctx.addActivity("vault", "created document", title);
18150
- return `Document "${title}" created at "${path47}".`;
18150
+ return `Document "${title}" created at "${path48}".`;
18151
18151
  }
18152
18152
  }
18153
18153
  ];
@@ -21970,11 +21970,11 @@ var init_synthesisAudit = __esm({
21970
21970
  import { existsSync as existsSync6, readFileSync as readFileSync5, writeFileSync as writeFileSync4 } from "node:fs";
21971
21971
  import { join as join2 } from "node:path";
21972
21972
  function loadNfrSpec(zelariRoot) {
21973
- const path47 = join2(zelariRoot, "nfr-spec.json");
21974
- if (!existsSync6(path47))
21973
+ const path48 = join2(zelariRoot, "nfr-spec.json");
21974
+ if (!existsSync6(path48))
21975
21975
  return null;
21976
21976
  try {
21977
- const raw = JSON.parse(readFileSync5(path47, "utf8"));
21977
+ const raw = JSON.parse(readFileSync5(path48, "utf8"));
21978
21978
  if (raw.version !== 1 || !Array.isArray(raw.targets))
21979
21979
  return null;
21980
21980
  return raw;
@@ -24280,9 +24280,9 @@ var init_types3 = __esm({
24280
24280
  import { readFileSync as readFileSync10 } from "node:fs";
24281
24281
  import { join as join8 } from "node:path";
24282
24282
  function readLessonsDeduped(zelariRoot) {
24283
- const path47 = join8(zelariRoot, LESSONS_FILE);
24283
+ const path48 = join8(zelariRoot, LESSONS_FILE);
24284
24284
  try {
24285
- const raw = readFileSync10(path47, "utf8");
24285
+ const raw = readFileSync10(path48, "utf8");
24286
24286
  const byId = /* @__PURE__ */ new Map();
24287
24287
  for (const line of raw.split(/\r?\n/)) {
24288
24288
  if (!line.trim())
@@ -24383,8 +24383,8 @@ function keywordsFrom(check2, signature) {
24383
24383
  return [.../* @__PURE__ */ new Set([...fromId, ...words])].slice(0, 12);
24384
24384
  }
24385
24385
  function writeLesson(zelariRoot, lesson) {
24386
- const path47 = join9(zelariRoot, LESSONS_FILE);
24387
- appendFileSync(path47, `${JSON.stringify(lesson)}
24386
+ const path48 = join9(zelariRoot, LESSONS_FILE);
24387
+ appendFileSync(path48, `${JSON.stringify(lesson)}
24388
24388
  `, "utf8");
24389
24389
  }
24390
24390
  function findSimilar(lessons, signature) {
@@ -24997,9 +24997,9 @@ function findCycle(nodes) {
24997
24997
  if (color.get(start) !== WHITE)
24998
24998
  continue;
24999
24999
  const stack = [[start, 0]];
25000
- const path47 = [];
25000
+ const path48 = [];
25001
25001
  color.set(start, GRAY);
25002
- path47.push(start);
25002
+ path48.push(start);
25003
25003
  while (stack.length > 0) {
25004
25004
  const top = stack[stack.length - 1];
25005
25005
  const [id, idx] = top;
@@ -25012,17 +25012,17 @@ function findCycle(nodes) {
25012
25012
  continue;
25013
25013
  const c = color.get(dep);
25014
25014
  if (c === GRAY) {
25015
- const at = path47.indexOf(dep);
25016
- return [...path47.slice(at), dep];
25015
+ const at = path48.indexOf(dep);
25016
+ return [...path48.slice(at), dep];
25017
25017
  }
25018
25018
  if (c === WHITE) {
25019
25019
  color.set(dep, GRAY);
25020
- path47.push(dep);
25020
+ path48.push(dep);
25021
25021
  stack.push([dep, 0]);
25022
25022
  }
25023
25023
  } else {
25024
25024
  color.set(id, BLACK);
25025
- path47.pop();
25025
+ path48.pop();
25026
25026
  stack.pop();
25027
25027
  }
25028
25028
  }
@@ -25942,8 +25942,8 @@ var init_runner = __esm({
25942
25942
  failed: [...this.tentaclesById.values()].filter((r) => r.status === "error"),
25943
25943
  pending: []
25944
25944
  };
25945
- const path47 = await this.host.saveSnapshot(snapshot, ".zelari/kraken/snapshots");
25946
- this.host.log(`checkpoint: ${label ?? "auto"} \u2192 ${path47}`);
25945
+ const path48 = await this.host.saveSnapshot(snapshot, ".zelari/kraken/snapshots");
25946
+ this.host.log(`checkpoint: ${label ?? "auto"} \u2192 ${path48}`);
25947
25947
  return snapshot;
25948
25948
  }
25949
25949
  callLog(msg, data) {
@@ -26412,6 +26412,8 @@ var init_graphStatus = __esm({
26412
26412
  var openai_compatible_exports = {};
26413
26413
  __export(openai_compatible_exports, {
26414
26414
  PROVIDER_ENDPOINTS: () => PROVIDER_ENDPOINTS,
26415
+ dataUriFromImage: () => dataUriFromImage,
26416
+ modelSupportsVision: () => modelSupportsVision,
26415
26417
  openaiCompatibleProvider: () => openaiCompatibleProvider,
26416
26418
  parseCachedPromptTokens: () => parseCachedPromptTokens,
26417
26419
  providerConfigFor: () => providerConfigFor,
@@ -26481,6 +26483,16 @@ function backoffDelay(attempt, retryAfterHeader) {
26481
26483
  }
26482
26484
  return Math.min(BACKOFF_BASE_MS * 2 ** attempt, BACKOFF_CAP_MS);
26483
26485
  }
26486
+ function modelSupportsVision(model) {
26487
+ const force = process.env.ZELARI_VISION;
26488
+ if (force === "1" || force === "true" || force === "on") return true;
26489
+ if (force === "0" || force === "false" || force === "off") return false;
26490
+ const m = model.toLowerCase();
26491
+ return VISION_MODEL_HINTS.some((hint) => m.includes(hint));
26492
+ }
26493
+ function dataUriFromImage(img) {
26494
+ return `data:${img.mime};base64,${img.dataBase64}`;
26495
+ }
26484
26496
  function resolveActiveProvider() {
26485
26497
  return getProviderConfig().activeProviderId;
26486
26498
  }
@@ -26505,6 +26517,7 @@ function resolveBaseUrl(providerId) {
26505
26517
  }
26506
26518
  function openaiCompatibleProvider(config2) {
26507
26519
  return async function* (params) {
26520
+ const vision = modelSupportsVision(params.model);
26508
26521
  const messages = params.messages.map((m) => {
26509
26522
  if (m.role === "tool") {
26510
26523
  return {
@@ -26538,6 +26551,29 @@ function openaiCompatibleProvider(config2) {
26538
26551
  reasoning_content: m.reasoningContent
26539
26552
  };
26540
26553
  }
26554
+ if (m.role === "user" && m.images && m.images.length > 0) {
26555
+ if (vision) {
26556
+ return {
26557
+ role: "user",
26558
+ content: [
26559
+ { type: "text", text: m.content ?? "" },
26560
+ ...m.images.map((img) => ({
26561
+ type: "image_url",
26562
+ image_url: { url: dataUriFromImage(img) }
26563
+ }))
26564
+ ]
26565
+ };
26566
+ }
26567
+ const notes = m.images.map((img) => `[Immagine allegata: ${img.alt ?? img.mime}]`).join("\n");
26568
+ return {
26569
+ role: "user",
26570
+ content: `${m.content ?? ""}
26571
+
26572
+ ${notes}
26573
+
26574
+ (Il modello attivo non supporta input visivi: i pixel non sono stati inviati.)`
26575
+ };
26576
+ }
26541
26577
  return { role: m.role, content: m.content };
26542
26578
  });
26543
26579
  const body = {
@@ -26818,7 +26854,7 @@ async function providerConfigFor(providerId) {
26818
26854
  providerId
26819
26855
  };
26820
26856
  }
26821
- var RETRYABLE_STATUSES, MAX_RETRIES, BACKOFF_BASE_MS, BACKOFF_CAP_MS, PROVIDER_CONNECT_TIMEOUT_MS, PROVIDER_STREAM_IDLE_MS, PROVIDER_STREAM_MAX_MS, PROVIDER_ENDPOINTS;
26857
+ var RETRYABLE_STATUSES, MAX_RETRIES, BACKOFF_BASE_MS, BACKOFF_CAP_MS, PROVIDER_CONNECT_TIMEOUT_MS, PROVIDER_STREAM_IDLE_MS, PROVIDER_STREAM_MAX_MS, VISION_MODEL_HINTS, PROVIDER_ENDPOINTS;
26822
26858
  var init_openai_compatible = __esm({
26823
26859
  "src/cli/provider/openai-compatible.ts"() {
26824
26860
  "use strict";
@@ -26847,6 +26883,33 @@ var init_openai_compatible = __esm({
26847
26883
  const n = raw ? Number.parseInt(raw, 10) : 18e5;
26848
26884
  return Number.isFinite(n) && n >= 6e4 ? n : 18e5;
26849
26885
  })();
26886
+ VISION_MODEL_HINTS = [
26887
+ "grok-4",
26888
+ "grok-3",
26889
+ "grok-2-vision",
26890
+ "grok-vision",
26891
+ "glm-4v",
26892
+ "glm-4.5v",
26893
+ "glm-4.1v",
26894
+ "glm-5v",
26895
+ "qwen-vl",
26896
+ "qwen2-vl",
26897
+ "qwen2.5-vl",
26898
+ "qwen3-vl",
26899
+ "gpt-4o",
26900
+ "gpt-4.1",
26901
+ "gpt-4.5",
26902
+ "gpt-4-vision",
26903
+ "gpt-5",
26904
+ "claude-3",
26905
+ "claude-4",
26906
+ "gemini-",
26907
+ "gemini/",
26908
+ "minimax-m1",
26909
+ "minimax-m2",
26910
+ "minimax-m3",
26911
+ "deepseek-vl"
26912
+ ];
26850
26913
  PROVIDER_ENDPOINTS = {
26851
26914
  "openai-compatible": "https://api.x.ai/v1",
26852
26915
  "minimax": "https://api.minimax.io/v1",
@@ -26898,9 +26961,9 @@ function spillToolOutput(fullText, meta3) {
26898
26961
  const rnd = randomBytes(3).toString("hex");
26899
26962
  const safeTool = (meta3?.toolName ?? "tool").replace(/[^a-zA-Z0-9._-]+/g, "_").slice(0, 32);
26900
26963
  const file2 = `${stamp}-${safeTool}-${hash3}-${rnd}.txt`;
26901
- const path47 = join11(dir, file2);
26902
- writeFileSync10(path47, fullText, "utf8");
26903
- return path47;
26964
+ const path48 = join11(dir, file2);
26965
+ writeFileSync10(path48, fullText, "utf8");
26966
+ return path48;
26904
26967
  } catch {
26905
26968
  return null;
26906
26969
  }
@@ -26946,10 +27009,10 @@ function truncateToolResult(text, capOrOpts = TOOL_RESULT_LINE_CAP) {
26946
27009
  ${tail}`;
26947
27010
  }
26948
27011
  if (doSpill) {
26949
- const path47 = spillToolOutput(text, { toolName: opts.toolName });
26950
- if (path47) {
27012
+ const path48 = spillToolOutput(text, { toolName: opts.toolName });
27013
+ if (path48) {
26951
27014
  const spillNote = `
26952
- \u2026 [full output spilled to: ${path47} \u2014 re-read with read_file if you need the complete text] \u2026`;
27015
+ \u2026 [full output spilled to: ${path48} \u2014 re-read with read_file if you need the complete text] \u2026`;
26953
27016
  if (preview.includes("] \u2026\n")) {
26954
27017
  preview = preview.replace("] \u2026\n", `] \u2026${spillNote}
26955
27018
  `);
@@ -29890,21 +29953,21 @@ function normalizeAuth(auth) {
29890
29953
  return "agent";
29891
29954
  }
29892
29955
  function readSecrets() {
29893
- const path47 = getSshSecretsPath();
29894
- if (!existsSync17(path47)) return {};
29956
+ const path48 = getSshSecretsPath();
29957
+ if (!existsSync17(path48)) return {};
29895
29958
  try {
29896
- return JSON.parse(readFileSync16(path47, "utf8"));
29959
+ return JSON.parse(readFileSync16(path48, "utf8"));
29897
29960
  } catch {
29898
29961
  return {};
29899
29962
  }
29900
29963
  }
29901
29964
  function writeSecrets(data) {
29902
- const path47 = getSshSecretsPath();
29903
- mkdirSync9(dirname2(path47), { recursive: true });
29904
- writeFileSync11(path47, `${JSON.stringify(data, null, 2)}
29965
+ const path48 = getSshSecretsPath();
29966
+ mkdirSync9(dirname2(path48), { recursive: true });
29967
+ writeFileSync11(path48, `${JSON.stringify(data, null, 2)}
29905
29968
  `, "utf8");
29906
29969
  try {
29907
- chmodSync(path47, 384);
29970
+ chmodSync(path48, 384);
29908
29971
  } catch {
29909
29972
  }
29910
29973
  }
@@ -29933,10 +29996,10 @@ function deleteSshPassword(id) {
29933
29996
  writeSecrets({ passwords });
29934
29997
  }
29935
29998
  function readStore2() {
29936
- const path47 = getSshTargetsPath();
29937
- if (!existsSync17(path47)) return [];
29999
+ const path48 = getSshTargetsPath();
30000
+ if (!existsSync17(path48)) return [];
29938
30001
  try {
29939
- const parsed = JSON.parse(readFileSync16(path47, "utf8"));
30002
+ const parsed = JSON.parse(readFileSync16(path48, "utf8"));
29940
30003
  const list = Array.isArray(parsed.targets) ? parsed.targets : [];
29941
30004
  return list.filter(
29942
30005
  (t) => t && typeof t.id === "string" && typeof t.host === "string" && typeof t.user === "string"
@@ -29951,11 +30014,11 @@ function readStore2() {
29951
30014
  }
29952
30015
  }
29953
30016
  function writeStore2(targets) {
29954
- const path47 = getSshTargetsPath();
29955
- mkdirSync9(dirname2(path47), { recursive: true });
30017
+ const path48 = getSshTargetsPath();
30018
+ mkdirSync9(dirname2(path48), { recursive: true });
29956
30019
  const clean = targets.map(({ hasPassword: _hp, ...t }) => t);
29957
30020
  writeFileSync11(
29958
- path47,
30021
+ path48,
29959
30022
  `${JSON.stringify({ targets: clean }, null, 2)}
29960
30023
  `,
29961
30024
  "utf8"
@@ -30201,11 +30264,11 @@ function formatSshTargetsForPrompt() {
30201
30264
  ];
30202
30265
  for (const t of targets) {
30203
30266
  const tags = t.tags?.length ? ` tags=[${t.tags.join(",")}]` : "";
30204
- const path47 = t.defaultRemotePath ? ` remotePath=${t.defaultRemotePath}` : "";
30267
+ const path48 = t.defaultRemotePath ? ` remotePath=${t.defaultRemotePath}` : "";
30205
30268
  const allow = t.allowedCommands?.length ? ` allowed=${t.allowedCommands.join("|")}` : " allowed=status-only";
30206
30269
  const auth = t.auth === "password" ? " auth=password" : t.auth === "keyPath" ? " auth=key" : " auth=agent";
30207
30270
  lines.push(
30208
- `- id=${t.id} name=${t.name} ${t.user}@${t.host}:${t.port ?? 22}${auth}${path47}${tags}${allow}`
30271
+ `- id=${t.id} name=${t.name} ${t.user}@${t.host}:${t.port ?? 22}${auth}${path48}${tags}${allow}`
30209
30272
  );
30210
30273
  }
30211
30274
  return lines.join("\n");
@@ -32113,6 +32176,268 @@ var init_phase = __esm({
32113
32176
  }
32114
32177
  });
32115
32178
 
32179
+ // src/cli/provider/localCli/claudeStreamJson.ts
32180
+ function textBlock(text) {
32181
+ return { type: "text", text };
32182
+ }
32183
+ function buildClaudeInputLines(messages) {
32184
+ const lines = [];
32185
+ for (const m of messages) {
32186
+ if (m.role === "system") {
32187
+ lines.push(JSON.stringify({ type: "system", content: m.content }));
32188
+ continue;
32189
+ }
32190
+ if (m.role === "user") {
32191
+ lines.push(
32192
+ JSON.stringify({
32193
+ type: "user",
32194
+ message: { role: "user", content: [textBlock(m.content)] }
32195
+ })
32196
+ );
32197
+ continue;
32198
+ }
32199
+ if (m.role === "tool") {
32200
+ lines.push(
32201
+ JSON.stringify({
32202
+ type: "user",
32203
+ message: {
32204
+ role: "user",
32205
+ content: [
32206
+ {
32207
+ type: "tool_result",
32208
+ tool_use_id: m.toolCallId ?? "tool-result",
32209
+ content: m.content
32210
+ }
32211
+ ]
32212
+ }
32213
+ })
32214
+ );
32215
+ continue;
32216
+ }
32217
+ const content = [];
32218
+ if (m.content.length > 0) content.push(textBlock(m.content));
32219
+ for (const tc of m.toolCalls ?? []) {
32220
+ content.push({ type: "tool_use", id: tc.id, name: tc.name, input: tc.args });
32221
+ }
32222
+ if (content.length === 0) content.push(textBlock(""));
32223
+ lines.push(
32224
+ JSON.stringify({
32225
+ type: "assistant",
32226
+ message: { role: "assistant", content }
32227
+ })
32228
+ );
32229
+ }
32230
+ return lines;
32231
+ }
32232
+ function createClaudeStreamParser() {
32233
+ let finished = false;
32234
+ let stopReason = "stop";
32235
+ let totalTextStreamed = 0;
32236
+ const push = (line) => {
32237
+ if (finished) return [];
32238
+ const trimmed = line.trim();
32239
+ if (!trimmed) return [];
32240
+ let msg;
32241
+ try {
32242
+ msg = JSON.parse(trimmed);
32243
+ } catch {
32244
+ return [];
32245
+ }
32246
+ const type = msg.type;
32247
+ if (type === "stream_event") {
32248
+ const event = msg.event;
32249
+ if (event?.type === "content_block_delta" && event.delta?.type === "text_delta" && typeof event.delta.text === "string") {
32250
+ totalTextStreamed += event.delta.text.length;
32251
+ return [{ kind: "text", delta: event.delta.text }];
32252
+ }
32253
+ return [];
32254
+ }
32255
+ if (type === "assistant") {
32256
+ const message = msg.message;
32257
+ if (message?.stop_reason) {
32258
+ stopReason = message.stop_reason === "end_turn" ? "stop" : message.stop_reason;
32259
+ }
32260
+ const deltas = [];
32261
+ for (const block of message?.content ?? []) {
32262
+ if (block.type === "tool_use" && typeof block.name === "string") {
32263
+ deltas.push({ kind: "text", delta: TOOL_USE_TEXT(block.name, block.input) });
32264
+ }
32265
+ }
32266
+ return deltas;
32267
+ }
32268
+ if (type === "result") {
32269
+ finished = true;
32270
+ const deltas = [];
32271
+ if (totalTextStreamed === 0 && typeof msg.result === "string" && msg.result.length > 0) {
32272
+ deltas.push({ kind: "text", delta: msg.result });
32273
+ }
32274
+ const usage = msg.usage;
32275
+ if (usage && typeof usage.input_tokens === "number" && typeof usage.output_tokens === "number") {
32276
+ const promptTokens = usage.input_tokens;
32277
+ const completionTokens = usage.output_tokens;
32278
+ deltas.push({
32279
+ kind: "usage",
32280
+ usage: {
32281
+ promptTokens,
32282
+ completionTokens,
32283
+ totalTokens: promptTokens + completionTokens,
32284
+ ...typeof usage.cache_read_input_tokens === "number" ? { cachedTokens: usage.cache_read_input_tokens } : {}
32285
+ }
32286
+ });
32287
+ }
32288
+ deltas.push({ kind: "finish", reason: stopReason });
32289
+ return deltas;
32290
+ }
32291
+ return [];
32292
+ };
32293
+ return {
32294
+ push,
32295
+ get finished() {
32296
+ return finished;
32297
+ },
32298
+ get stopReason() {
32299
+ return stopReason;
32300
+ }
32301
+ };
32302
+ }
32303
+ var TOOL_USE_TEXT;
32304
+ var init_claudeStreamJson = __esm({
32305
+ "src/cli/provider/localCli/claudeStreamJson.ts"() {
32306
+ "use strict";
32307
+ TOOL_USE_TEXT = (name, input) => `
32308
+ [CLI tool] ${name} ${JSON.stringify(input ?? {})}
32309
+ `;
32310
+ }
32311
+ });
32312
+
32313
+ // src/cli/provider/localCli/claudeProvider.ts
32314
+ var claudeProvider_exports = {};
32315
+ __export(claudeProvider_exports, {
32316
+ createLocalCliProvider: () => createLocalCliProvider
32317
+ });
32318
+ import { spawn as spawn6 } from "node:child_process";
32319
+ function waitForExit(child, timeoutMs = 2e3) {
32320
+ return new Promise((resolve3) => {
32321
+ if (child.exitCode != null) return resolve3(child.exitCode);
32322
+ const timer = setTimeout(() => resolve3(child.exitCode ?? null), timeoutMs);
32323
+ timer.unref?.();
32324
+ child.once("exit", (code) => {
32325
+ clearTimeout(timer);
32326
+ resolve3(code ?? null);
32327
+ });
32328
+ });
32329
+ }
32330
+ function createLocalCliProvider(opts = {}) {
32331
+ return async function* (params) {
32332
+ const cli = opts.cli ?? process.env.ZELARI_LOCAL_CLI ?? "claude";
32333
+ const model = opts.model ?? params.model;
32334
+ const permSocket = opts.permissionSocketPath ?? process.env.ZELARI_PERM_SOCKET ?? "";
32335
+ const args = opts.args ?? [...DEFAULT_ARGS];
32336
+ if (opts.args === void 0) {
32337
+ if (model) {
32338
+ args.push("--model", model);
32339
+ }
32340
+ if (permSocket) {
32341
+ args.push(
32342
+ "--permission-prompt-tool",
32343
+ `zelari-code --permission-mcp ${permSocket}`
32344
+ );
32345
+ }
32346
+ }
32347
+ const spawnFn = opts.spawnFn ?? spawn6;
32348
+ let child;
32349
+ try {
32350
+ child = spawnFn(cli, args, {
32351
+ stdio: ["pipe", "pipe", "pipe"],
32352
+ ...opts.env ? { env: opts.env } : {}
32353
+ });
32354
+ } catch (err) {
32355
+ yield {
32356
+ kind: "error",
32357
+ message: `[local-cli] failed to spawn ${cli}: ${err instanceof Error ? err.message : String(err)}`
32358
+ };
32359
+ return;
32360
+ }
32361
+ const spawnErrorBox = { err: null };
32362
+ child.once("error", (err) => {
32363
+ spawnErrorBox.err = err;
32364
+ });
32365
+ const onAbort = () => {
32366
+ child.kill();
32367
+ };
32368
+ params.signal?.addEventListener("abort", onAbort, { once: true });
32369
+ let stderrTail = "";
32370
+ child.stderr.setEncoding("utf8");
32371
+ child.stderr.on("data", (d) => {
32372
+ stderrTail = (stderrTail + d).slice(-2e3);
32373
+ });
32374
+ for (const line of buildClaudeInputLines(params.messages)) {
32375
+ child.stdin.write(line + "\n");
32376
+ }
32377
+ child.stdin.end();
32378
+ const parser = createClaudeStreamParser();
32379
+ let buf = "";
32380
+ let finished = false;
32381
+ try {
32382
+ for await (const chunk of child.stdout) {
32383
+ buf += chunk.toString("utf8");
32384
+ let nl;
32385
+ while ((nl = buf.indexOf("\n")) >= 0) {
32386
+ const line = buf.slice(0, nl);
32387
+ buf = buf.slice(nl + 1);
32388
+ for (const delta of parser.push(line)) {
32389
+ if (delta.kind === "finish") finished = true;
32390
+ yield delta;
32391
+ }
32392
+ }
32393
+ }
32394
+ if (buf.trim().length > 0) {
32395
+ for (const delta of parser.push(buf)) {
32396
+ if (delta.kind === "finish") finished = true;
32397
+ yield delta;
32398
+ }
32399
+ }
32400
+ if (!finished) {
32401
+ const spawnErr = spawnErrorBox.err;
32402
+ if (spawnErr) {
32403
+ yield {
32404
+ kind: "error",
32405
+ message: `[local-cli] ${cli} failed to start: ${spawnErr.message}`
32406
+ };
32407
+ } else {
32408
+ const exitCode = await waitForExit(child);
32409
+ if (exitCode != null && exitCode !== 0) {
32410
+ yield {
32411
+ kind: "error",
32412
+ message: `[local-cli] ${cli} exited ${exitCode}: ${stderrTail.trim() || "no stderr"}`
32413
+ };
32414
+ } else {
32415
+ yield { kind: "finish", reason: parser.stopReason || "stop" };
32416
+ }
32417
+ }
32418
+ }
32419
+ } finally {
32420
+ params.signal?.removeEventListener("abort", onAbort);
32421
+ if (child.exitCode == null) child.kill();
32422
+ }
32423
+ };
32424
+ }
32425
+ var DEFAULT_ARGS;
32426
+ var init_claudeProvider = __esm({
32427
+ "src/cli/provider/localCli/claudeProvider.ts"() {
32428
+ "use strict";
32429
+ init_claudeStreamJson();
32430
+ DEFAULT_ARGS = [
32431
+ "-p",
32432
+ "--output-format",
32433
+ "stream-json",
32434
+ "--input-format",
32435
+ "stream-json",
32436
+ "--verbose"
32437
+ ];
32438
+ }
32439
+ });
32440
+
32116
32441
  // src/cli/workspace/projectInstructions.ts
32117
32442
  import { existsSync as existsSync19, readFileSync as readFileSync17 } from "node:fs";
32118
32443
  import { join as join16 } from "node:path";
@@ -33064,28 +33389,28 @@ var init_storage = __esm({
33064
33389
  VALID_SCALARS = /^(true|false|null|~)$/i;
33065
33390
  Storage = class {
33066
33391
  /** Read a Markdown file with frontmatter. Throws if not found. */
33067
- read(path47) {
33068
- if (!existsSync25(path47)) {
33069
- throw new Error(`File not found: ${path47}`);
33392
+ read(path48) {
33393
+ if (!existsSync25(path48)) {
33394
+ throw new Error(`File not found: ${path48}`);
33070
33395
  }
33071
- const md = readFileSync21(path47, "utf8");
33396
+ const md = readFileSync21(path48, "utf8");
33072
33397
  return parseFrontmatter(md);
33073
33398
  }
33074
33399
  /** Read a Markdown file; returns null if not found. */
33075
- readIfExists(path47) {
33076
- if (!existsSync25(path47)) return null;
33077
- return this.read(path47);
33400
+ readIfExists(path48) {
33401
+ if (!existsSync25(path48)) return null;
33402
+ return this.read(path48);
33078
33403
  }
33079
33404
  /**
33080
33405
  * Write a Markdown file atomically (tmp + rename). Creates parent dirs.
33081
33406
  * The meta object is serialized as YAML frontmatter; body as Markdown.
33082
33407
  */
33083
- write(path47, meta3, body) {
33084
- mkdirSync11(dirname4(path47), { recursive: true });
33085
- const tmp = path47 + ".tmp-" + process.pid;
33408
+ write(path48, meta3, body) {
33409
+ mkdirSync11(dirname4(path48), { recursive: true });
33410
+ const tmp = path48 + ".tmp-" + process.pid;
33086
33411
  const md = serializeFrontmatter(meta3, body);
33087
33412
  writeFileSync13(tmp, md, "utf8");
33088
- renameSync2(tmp, path47);
33413
+ renameSync2(tmp, path48);
33089
33414
  }
33090
33415
  /** List all .md files in a directory (non-recursive). */
33091
33416
  listMarkdown(dir) {
@@ -33163,8 +33488,8 @@ function readPlan(ctx) {
33163
33488
  } catch {
33164
33489
  }
33165
33490
  }
33166
- const path47 = workspaceFile(ctx.rootDir, "plan");
33167
- const doc = ctx.storage.readIfExists(path47);
33491
+ const path48 = workspaceFile(ctx.rootDir, "plan");
33492
+ const doc = ctx.storage.readIfExists(path48);
33168
33493
  if (!doc) return { phases: [], tasks: [], milestones: [] };
33169
33494
  const meta3 = doc.meta;
33170
33495
  return {
@@ -33329,7 +33654,7 @@ function addMilestoneRecord(ctx, summary, input) {
33329
33654
  dueDate: input.dueDate,
33330
33655
  targetVersion: version2
33331
33656
  });
33332
- const path47 = join23(ctx.rootDir, "milestones", `${id}.md`);
33657
+ const path48 = join23(ctx.rootDir, "milestones", `${id}.md`);
33333
33658
  const meta3 = {
33334
33659
  kind: "milestone",
33335
33660
  id,
@@ -33346,7 +33671,7 @@ function addMilestoneRecord(ctx, summary, input) {
33346
33671
  `Target version: ${version2}`,
33347
33672
  ""
33348
33673
  ].join("\n");
33349
- ctx.storage.write(path47, meta3, body);
33674
+ ctx.storage.write(path48, meta3, body);
33350
33675
  return { id, created: true };
33351
33676
  }
33352
33677
  function readPlanSummary(ctx) {
@@ -33550,7 +33875,7 @@ function addIdeaStub(ctx) {
33550
33875
  const tags = args["tags"] ?? [];
33551
33876
  const category = args["category"] ?? "General";
33552
33877
  const id = `${nextAdrId(ctx)}-${slugify3(title)}`;
33553
- const path47 = workspaceArtifact(ctx.rootDir, "decisions", id);
33878
+ const path48 = workspaceArtifact(ctx.rootDir, "decisions", id);
33554
33879
  const meta3 = {
33555
33880
  kind: "adr",
33556
33881
  status: "proposed",
@@ -33576,7 +33901,7 @@ function addIdeaStub(ctx) {
33576
33901
  ...consequences.map((c) => `- ${c}`),
33577
33902
  ""
33578
33903
  ].join("\n");
33579
- ctx.storage.write(path47, meta3, body);
33904
+ ctx.storage.write(path48, meta3, body);
33580
33905
  return `ADR ${id} created: "${title}". Status: proposed. Promote to accepted via /update ADR or manual edit.`;
33581
33906
  });
33582
33907
  }
@@ -33658,14 +33983,14 @@ function createDocumentStub(ctx) {
33658
33983
  ctx.storage.write(risksPath, riskMeta, content);
33659
33984
  return `Document "${title}" created at risks.md (workspace root).`;
33660
33985
  }
33661
- const path47 = workspaceArtifact(ctx.rootDir, "docs", slug);
33986
+ const path48 = workspaceArtifact(ctx.rootDir, "docs", slug);
33662
33987
  const meta3 = {
33663
33988
  kind: "doc",
33664
33989
  id: slug,
33665
33990
  date: (/* @__PURE__ */ new Date()).toISOString().slice(0, 10),
33666
33991
  tags
33667
33992
  };
33668
- ctx.storage.write(path47, meta3, content);
33993
+ ctx.storage.write(path48, meta3, content);
33669
33994
  return `Document "${title}" created at docs/${slug}.md.`;
33670
33995
  });
33671
33996
  }
@@ -33882,7 +34207,7 @@ __export(updater_exports, {
33882
34207
  resolveBundledNpmCli: () => resolveBundledNpmCli
33883
34208
  });
33884
34209
  import { createRequire as createRequire2 } from "node:module";
33885
- import { spawn as spawn6 } from "node:child_process";
34210
+ import { spawn as spawn7 } from "node:child_process";
33886
34211
  import { existsSync as existsSync27 } from "node:fs";
33887
34212
  import path27 from "node:path";
33888
34213
  import { fileURLToPath } from "node:url";
@@ -33969,7 +34294,7 @@ async function checkForUpdate(fetcher = fetch, registryUrl) {
33969
34294
  updateAvailable: cmp < 0
33970
34295
  };
33971
34296
  }
33972
- async function performUpdate(packageName = "zelari-code", executor = spawn6, resolveNpmCli = resolveBundledNpmCli) {
34297
+ async function performUpdate(packageName = "zelari-code", executor = spawn7, resolveNpmCli = resolveBundledNpmCli) {
33973
34298
  const args = ["install", "-g", `${packageName}@latest`];
33974
34299
  const primary = await runNpm(executor, args, "shim");
33975
34300
  if (primary.ok) return primary;
@@ -34027,7 +34352,7 @@ var init_updater = __esm({
34027
34352
  });
34028
34353
 
34029
34354
  // src/cli/mcp/mcpClient.ts
34030
- import { spawn as spawn7 } from "node:child_process";
34355
+ import { spawn as spawn8 } from "node:child_process";
34031
34356
  var DEFAULT_REQUEST_TIMEOUT_MS, INIT_TIMEOUT_MS, MCP_PROTOCOL_VERSION, McpClient;
34032
34357
  var init_mcpClient = __esm({
34033
34358
  "src/cli/mcp/mcpClient.ts"() {
@@ -34055,10 +34380,10 @@ var init_mcpClient = __esm({
34055
34380
  env: { ...process.env, ...this.config.env ?? {} },
34056
34381
  windowsHide: true
34057
34382
  };
34058
- const child = process.platform === "win32" ? spawn7(buildCmdLine(this.config.command, this.config.args ?? []), {
34383
+ const child = process.platform === "win32" ? spawn8(buildCmdLine(this.config.command, this.config.args ?? []), {
34059
34384
  ...spawnOpts,
34060
34385
  shell: true
34061
- }) : spawn7(this.config.command, this.config.args ?? [], spawnOpts);
34386
+ }) : spawn8(this.config.command, this.config.args ?? [], spawnOpts);
34062
34387
  this.child = child;
34063
34388
  child.stdout.setEncoding("utf8");
34064
34389
  child.stdout.on("data", (chunk) => this.onStdout(chunk));
@@ -34209,10 +34534,10 @@ function getUserMcpPath() {
34209
34534
  function getProjectMcpPath(projectRoot) {
34210
34535
  return join24(projectRoot, ".zelari", "mcp.json");
34211
34536
  }
34212
- function readFile2(path47) {
34213
- if (!existsSync28(path47)) return {};
34537
+ function readFile2(path48) {
34538
+ if (!existsSync28(path48)) return {};
34214
34539
  try {
34215
- const parsed = JSON.parse(readFileSync23(path47, "utf8"));
34540
+ const parsed = JSON.parse(readFileSync23(path48, "utf8"));
34216
34541
  const out = {};
34217
34542
  for (const [name, cfg] of Object.entries(parsed.mcpServers ?? {})) {
34218
34543
  if (!cfg || typeof cfg.command !== "string" || !cfg.command.trim()) continue;
@@ -34228,10 +34553,10 @@ function readFile2(path47) {
34228
34553
  return {};
34229
34554
  }
34230
34555
  }
34231
- function writeFile(path47, servers) {
34232
- mkdirSync13(dirname6(path47), { recursive: true });
34556
+ function writeFile(path48, servers) {
34557
+ mkdirSync13(dirname6(path48), { recursive: true });
34233
34558
  const body = { mcpServers: servers };
34234
- writeFileSync15(path47, `${JSON.stringify(body, null, 2)}
34559
+ writeFileSync15(path48, `${JSON.stringify(body, null, 2)}
34235
34560
  `, "utf8");
34236
34561
  }
34237
34562
  function listMcpServers(projectRoot) {
@@ -34264,9 +34589,9 @@ function upsertMcpServer(opts) {
34264
34589
  if (!opts.config.command?.trim()) {
34265
34590
  return { ok: false, error: "command is required" };
34266
34591
  }
34267
- let path47;
34592
+ let path48;
34268
34593
  if (opts.scope === "user") {
34269
- path47 = getUserMcpPath();
34594
+ path48 = getUserMcpPath();
34270
34595
  } else {
34271
34596
  const root = opts.projectRoot?.trim();
34272
34597
  if (!root) {
@@ -34275,30 +34600,30 @@ function upsertMcpServer(opts) {
34275
34600
  error: "projectRoot required for project scope (Open Folder first)"
34276
34601
  };
34277
34602
  }
34278
- path47 = getProjectMcpPath(root);
34603
+ path48 = getProjectMcpPath(root);
34279
34604
  }
34280
- const current = readFile2(path47);
34605
+ const current = readFile2(path48);
34281
34606
  current[name] = {
34282
34607
  command: opts.config.command.trim(),
34283
34608
  args: opts.config.args,
34284
34609
  env: opts.config.env,
34285
34610
  enabled: opts.config.enabled !== false
34286
34611
  };
34287
- writeFile(path47, current);
34288
- return { ok: true, path: path47 };
34612
+ writeFile(path48, current);
34613
+ return { ok: true, path: path48 };
34289
34614
  }
34290
34615
  function removeMcpServer(opts) {
34291
- const path47 = opts.scope === "user" ? getUserMcpPath() : opts.projectRoot ? getProjectMcpPath(opts.projectRoot) : null;
34292
- if (!path47) {
34616
+ const path48 = opts.scope === "user" ? getUserMcpPath() : opts.projectRoot ? getProjectMcpPath(opts.projectRoot) : null;
34617
+ if (!path48) {
34293
34618
  return { ok: false, error: "projectRoot required for project scope" };
34294
34619
  }
34295
- const current = readFile2(path47);
34620
+ const current = readFile2(path48);
34296
34621
  if (!(opts.name in current)) {
34297
- return { ok: false, error: `Server "${opts.name}" not found in ${path47}` };
34622
+ return { ok: false, error: `Server "${opts.name}" not found in ${path48}` };
34298
34623
  }
34299
34624
  delete current[opts.name];
34300
- writeFile(path47, current);
34301
- return { ok: true, path: path47 };
34625
+ writeFile(path48, current);
34626
+ return { ok: true, path: path48 };
34302
34627
  }
34303
34628
  var init_mcpConfigIo = __esm({
34304
34629
  "src/cli/mcp/mcpConfigIo.ts"() {
@@ -34307,11 +34632,63 @@ var init_mcpConfigIo = __esm({
34307
34632
  });
34308
34633
 
34309
34634
  // src/cli/mcp/mcpPresets.ts
34635
+ function buildComposioPreset() {
34636
+ return {
34637
+ id: "composio",
34638
+ servers: {
34639
+ composio: {
34640
+ command: "npx",
34641
+ args: ["-y", "composio-mcp@latest"],
34642
+ env: process.env.COMPOSIO_API_KEY ? { COMPOSIO_API_KEY: process.env.COMPOSIO_API_KEY } : {},
34643
+ enabled: true
34644
+ }
34645
+ },
34646
+ notes: [
34647
+ "Composio Connect \u2014 500+ app integrations (GitHub, Slack, Notion, ...) as MCP tools.",
34648
+ "Set COMPOSIO_API_KEY before applying: export COMPOSIO_API_KEY=<key>",
34649
+ "Connect apps at https://composio.dev \u2014 each connected account becomes a tool.",
34650
+ "The key is stored in mcp.json env, never in argv.",
34651
+ "Kill switch: disable the composio server in mcp.json or ZELARI_MCP=0"
34652
+ ]
34653
+ };
34654
+ }
34655
+ function buildQwenMmPreset() {
34656
+ const plugin = process.env.QWEN_MM_PLUGIN?.trim() || "qwen-mm-plugins-core";
34657
+ const cap3 = plugin.replace(/^qwen-mm-plugins-/, "");
34658
+ const spec = `qwen-mm-plugins[${cap3}] @ git+https://github.com/QwenLM/Qwen-MM-Plugins.git@${plugin}-v1.0.1`;
34659
+ return {
34660
+ id: "qwen-mm-plugins",
34661
+ servers: {
34662
+ "qwen-mm-plugins": {
34663
+ command: "uvx",
34664
+ args: ["--from", spec, plugin],
34665
+ env: {
34666
+ PYTHONIOENCODING: "utf-8",
34667
+ ...process.env.DASHSCOPE_API_KEY ? { DASHSCOPE_API_KEY: process.env.DASHSCOPE_API_KEY } : {},
34668
+ ...process.env.SERPER_API_KEY ? { SERPER_API_KEY: process.env.SERPER_API_KEY } : {}
34669
+ },
34670
+ enabled: true
34671
+ }
34672
+ },
34673
+ notes: [
34674
+ "Qwen-MM-Plugins \u2014 multimodal capabilities (core / video-memory / video-edit / blender / freecad) as MCP tools.",
34675
+ "Installs via uvx from the GitHub release tag (qwen-mm-plugins-<cap>-v1.0.1); first launch downloads ~70 packages.",
34676
+ "Windows: verified working natively with PYTHONIOENCODING=utf-8 (WSL2 also supported upstream).",
34677
+ "Pick a capability: export QWEN_MM_PLUGIN=qwen-mm-plugins-video-memory (default: qwen-mm-plugins-core).",
34678
+ "API keys read at apply time: DASHSCOPE_API_KEY, SERPER_API_KEY (native reading works without them).",
34679
+ "Blender/FreeCAD variants: start the host app before invoking tools.",
34680
+ "Kill switch: disable the qwen-mm-plugins server in mcp.json or ZELARI_MCP=0"
34681
+ ]
34682
+ };
34683
+ }
34310
34684
  function listMcpPresetIds() {
34311
- return Object.keys(PRESETS).filter((k) => k === "cua");
34685
+ return Object.keys(PRESETS).filter(
34686
+ (k) => k === "cua" || k === "composio" || k === "qwen-mm-plugins"
34687
+ );
34312
34688
  }
34313
34689
  function getMcpPreset(id) {
34314
- return PRESETS[id.trim().toLowerCase()] ?? null;
34690
+ const factory = PRESETS[id.trim().toLowerCase()];
34691
+ return factory ? factory() : null;
34315
34692
  }
34316
34693
  function applyMcpPreset(opts) {
34317
34694
  const preset = getMcpPreset(opts.presetId);
@@ -34371,8 +34748,11 @@ var init_mcpPresets = __esm({
34371
34748
  ]
34372
34749
  };
34373
34750
  PRESETS = {
34374
- cua: CUA_DRIVER_PRESET,
34375
- "cua-driver": CUA_DRIVER_PRESET
34751
+ cua: () => CUA_DRIVER_PRESET,
34752
+ "cua-driver": () => CUA_DRIVER_PRESET,
34753
+ composio: buildComposioPreset,
34754
+ "qwen-mm-plugins": buildQwenMmPreset,
34755
+ "qwen-mm": buildQwenMmPreset
34376
34756
  };
34377
34757
  }
34378
34758
  });
@@ -34635,10 +35015,10 @@ import { createHash as createHash5 } from "node:crypto";
34635
35015
  import { join as join26 } from "node:path";
34636
35016
  import { readFile as readFile3 } from "node:fs/promises";
34637
35017
  async function readPackageJson2(projectRoot) {
34638
- const path47 = join26(projectRoot, "package.json");
34639
- if (!existsSync30(path47)) return null;
35018
+ const path48 = join26(projectRoot, "package.json");
35019
+ if (!existsSync30(path48)) return null;
34640
35020
  try {
34641
- return JSON.parse(await readFile3(path47, "utf8"));
35021
+ return JSON.parse(await readFile3(path48, "utf8"));
34642
35022
  } catch {
34643
35023
  return null;
34644
35024
  }
@@ -34720,9 +35100,9 @@ async function genBuild(ctx) {
34720
35100
  ].join("\n");
34721
35101
  }
34722
35102
  async function genOpenQuestions(ctx) {
34723
- const path47 = join26(ctx.rootDir, "risks.md");
34724
- if (!existsSync30(path47)) return "_No open questions._";
34725
- const content = readFileSync25(path47, "utf8");
35103
+ const path48 = join26(ctx.rootDir, "risks.md");
35104
+ if (!existsSync30(path48)) return "_No open questions._";
35105
+ const content = readFileSync25(path48, "utf8");
34726
35106
  const lines = content.split("\n");
34727
35107
  const questions = [];
34728
35108
  let currentTitle = "";
@@ -34967,7 +35347,7 @@ var init_completeDesign = __esm({
34967
35347
  });
34968
35348
 
34969
35349
  // src/cli/workspace/projectSmoke.ts
34970
- import { spawn as spawn8 } from "node:child_process";
35350
+ import { spawn as spawn9 } from "node:child_process";
34971
35351
  import { existsSync as existsSync31, readFileSync as readFileSync26 } from "node:fs";
34972
35352
  import { join as join27 } from "node:path";
34973
35353
  function pickSmokeScript(scripts) {
@@ -34997,12 +35377,12 @@ async function runProjectSmoke(projectRoot, timeoutMs = DEFAULT_TIMEOUT_MS) {
34997
35377
  return { ran: false, reason: "no typecheck/test/build script (skipped)" };
34998
35378
  }
34999
35379
  return await new Promise((resolveRun) => {
35000
- const child = process.platform === "win32" ? spawn8(buildCmdLine("npm.cmd", ["run", script]), {
35380
+ const child = process.platform === "win32" ? spawn9(buildCmdLine("npm.cmd", ["run", script]), {
35001
35381
  cwd: projectRoot,
35002
35382
  stdio: ["ignore", "pipe", "pipe"],
35003
35383
  env: process.env,
35004
35384
  shell: true
35005
- }) : spawn8("npm", ["run", script], {
35385
+ }) : spawn9("npm", ["run", script], {
35006
35386
  cwd: projectRoot,
35007
35387
  stdio: ["ignore", "pipe", "pipe"],
35008
35388
  env: process.env
@@ -35074,7 +35454,7 @@ __export(postCouncilHook_exports, {
35074
35454
  runImplementationVerificationHook: () => runImplementationVerificationHook,
35075
35455
  runPostCouncilHook: () => runPostCouncilHook
35076
35456
  });
35077
- import { spawn as spawn9 } from "node:child_process";
35457
+ import { spawn as spawn10 } from "node:child_process";
35078
35458
  import { existsSync as existsSync32, readFileSync as readFileSync27 } from "node:fs";
35079
35459
  import { join as join28 } from "node:path";
35080
35460
  async function runCompleteDesignPostProcessor(ctx, options) {
@@ -35123,7 +35503,7 @@ async function runCompleteDesignPostProcessor(ctx, options) {
35123
35503
  }
35124
35504
  }
35125
35505
  return await new Promise((resolveRun) => {
35126
- const child = spawn9(process.execPath, [scriptPath], {
35506
+ const child = spawn10(process.execPath, [scriptPath], {
35127
35507
  cwd: ctx.projectRoot,
35128
35508
  stdio: ["ignore", "pipe", "pipe"],
35129
35509
  env: process.env
@@ -35289,8 +35669,8 @@ async function runPostCouncilHook(ctx, options) {
35289
35669
  sources: scope.sources
35290
35670
  } : void 0
35291
35671
  });
35292
- const path47 = writeCouncilCompletion(ctx.rootDir, completion);
35293
- completionHook = { ran: true, path: path47, completion };
35672
+ const path48 = writeCouncilCompletion(ctx.rootDir, completion);
35673
+ completionHook = { ran: true, path: path48, completion };
35294
35674
  } catch (err) {
35295
35675
  completionHook = {
35296
35676
  ran: true,
@@ -36502,6 +36882,307 @@ var init_missionSlice = __esm({
36502
36882
  }
36503
36883
  });
36504
36884
 
36885
+ // src/cli/mcp/permissionBroker.ts
36886
+ var permissionBroker_exports = {};
36887
+ __export(permissionBroker_exports, {
36888
+ PERMISSION_BROKER_DEFAULT_CONNECT_TIMEOUT_MS: () => PERMISSION_BROKER_DEFAULT_CONNECT_TIMEOUT_MS,
36889
+ PERMISSION_BROKER_DEFAULT_TIMEOUT_MS: () => PERMISSION_BROKER_DEFAULT_TIMEOUT_MS,
36890
+ requestBrokerAsk: () => requestBrokerAsk,
36891
+ startPermissionBroker: () => startPermissionBroker
36892
+ });
36893
+ import { connect, createServer } from "node:net";
36894
+ import { unlink } from "node:fs";
36895
+ function sendJson(socket, obj) {
36896
+ if (socket.destroyed) return;
36897
+ socket.write(JSON.stringify(obj) + "\n");
36898
+ }
36899
+ function safeSocketPath(socketPath) {
36900
+ return socketPath.trim();
36901
+ }
36902
+ function startPermissionBroker(socketPath, handlers, opts) {
36903
+ const path48 = safeSocketPath(socketPath);
36904
+ const requestTimeoutMs = opts?.requestTimeoutMs ?? PERMISSION_BROKER_DEFAULT_TIMEOUT_MS;
36905
+ const sockets = /* @__PURE__ */ new Set();
36906
+ const server = createServer((socket) => {
36907
+ socket.setEncoding("utf8");
36908
+ sockets.add(socket);
36909
+ socket.on("close", () => sockets.delete(socket));
36910
+ socket.on("error", () => {
36911
+ });
36912
+ let buffer = "";
36913
+ socket.on("data", (chunk) => {
36914
+ buffer += chunk;
36915
+ let nl;
36916
+ while ((nl = buffer.indexOf("\n")) >= 0) {
36917
+ const line = buffer.slice(0, nl).trim();
36918
+ buffer = buffer.slice(nl + 1);
36919
+ if (!line) continue;
36920
+ void handleLine(socket, line);
36921
+ }
36922
+ });
36923
+ });
36924
+ server.on("error", () => {
36925
+ });
36926
+ async function handleLine(socket, line) {
36927
+ let msg;
36928
+ try {
36929
+ msg = JSON.parse(line);
36930
+ } catch {
36931
+ return;
36932
+ }
36933
+ if (!msg || msg.t !== "ask" || typeof msg.id !== "string" || !msg.id) return;
36934
+ const id = msg.id;
36935
+ let settled = false;
36936
+ let timer;
36937
+ const reply = (answer) => {
36938
+ if (settled) return;
36939
+ settled = true;
36940
+ if (timer) clearTimeout(timer);
36941
+ sendJson(socket, { t: "answer", id, ...answer });
36942
+ };
36943
+ timer = setTimeout(() => {
36944
+ reply({
36945
+ behavior: "deny",
36946
+ message: `[zelari] permission broker timed out after ${requestTimeoutMs}ms`
36947
+ });
36948
+ }, requestTimeoutMs);
36949
+ if (typeof timer.unref === "function") timer.unref();
36950
+ try {
36951
+ if (msg.kind === "question") {
36952
+ if (!handlers.onQuestion) {
36953
+ reply({
36954
+ behavior: "deny",
36955
+ message: "[zelari] no interactive answer available (question handler missing)"
36956
+ });
36957
+ return;
36958
+ }
36959
+ const answer2 = await handlers.onQuestion({
36960
+ id,
36961
+ question: msg.question ?? "",
36962
+ choices: msg.choices,
36963
+ context: msg.context
36964
+ });
36965
+ if (answer2 == null) {
36966
+ reply({
36967
+ behavior: "deny",
36968
+ message: "[zelari] question cancelled (no answer)"
36969
+ });
36970
+ return;
36971
+ }
36972
+ if (answer2.behavior === "deny") {
36973
+ reply({
36974
+ behavior: "deny",
36975
+ message: answer2.message ?? "[zelari] question denied by handler"
36976
+ });
36977
+ return;
36978
+ }
36979
+ reply({ behavior: "allow", answer: answer2.answer ?? "" });
36980
+ return;
36981
+ }
36982
+ const answer = handlers.onPermission ? await handlers.onPermission({
36983
+ id,
36984
+ tool: msg.tool ?? "unknown",
36985
+ input: msg.input,
36986
+ toolUseId: msg.toolUseId,
36987
+ suggestions: msg.suggestions
36988
+ }) : {
36989
+ behavior: "deny",
36990
+ message: "[zelari] permission broker has no approval handler"
36991
+ };
36992
+ reply(answer);
36993
+ } catch (err) {
36994
+ reply({
36995
+ behavior: "deny",
36996
+ message: `[zelari] permission broker error: ${err instanceof Error ? err.message : String(err)}`
36997
+ });
36998
+ }
36999
+ }
37000
+ return new Promise((resolve3, reject) => {
37001
+ const onError = (err) => reject(err);
37002
+ server.once("error", onError);
37003
+ server.listen(path48, () => {
37004
+ server.removeListener("error", onError);
37005
+ resolve3({
37006
+ socketPath: path48,
37007
+ stop: () => new Promise((res) => {
37008
+ for (const s of sockets) s.destroy();
37009
+ sockets.clear();
37010
+ const closeAll = server.closeAllConnections;
37011
+ if (typeof closeAll === "function") closeAll.call(server);
37012
+ let done = false;
37013
+ const finish = () => {
37014
+ if (done) return;
37015
+ done = true;
37016
+ if (process.platform !== "win32") {
37017
+ unlink(path48, () => res());
37018
+ } else {
37019
+ res();
37020
+ }
37021
+ };
37022
+ server.close(() => finish());
37023
+ setTimeout(finish, 1e3).unref?.();
37024
+ })
37025
+ });
37026
+ });
37027
+ });
37028
+ }
37029
+ function requestBrokerAsk(socketPath, ask, opts) {
37030
+ const path48 = safeSocketPath(socketPath);
37031
+ const requestTimeoutMs = opts?.requestTimeoutMs ?? PERMISSION_BROKER_DEFAULT_TIMEOUT_MS;
37032
+ const connectTimeoutMs = opts?.connectTimeoutMs ?? PERMISSION_BROKER_DEFAULT_CONNECT_TIMEOUT_MS;
37033
+ return new Promise((resolve3, reject) => {
37034
+ const socket = connect(path48);
37035
+ let buffer = "";
37036
+ let settled = false;
37037
+ const settle = (fn) => {
37038
+ if (settled) return;
37039
+ settled = true;
37040
+ clearTimeout(connectTimer);
37041
+ clearTimeout(requestTimer);
37042
+ socket.destroy();
37043
+ fn();
37044
+ };
37045
+ const connectTimer = setTimeout(() => {
37046
+ settle(
37047
+ () => reject(
37048
+ new Error(
37049
+ `permission broker unavailable at "${path48}" (connect timed out after ${connectTimeoutMs}ms)`
37050
+ )
37051
+ )
37052
+ );
37053
+ }, connectTimeoutMs);
37054
+ const requestTimer = setTimeout(() => {
37055
+ settle(
37056
+ () => reject(
37057
+ new Error(
37058
+ `permission broker timed out after ${requestTimeoutMs}ms waiting for an answer`
37059
+ )
37060
+ )
37061
+ );
37062
+ }, requestTimeoutMs);
37063
+ socket.setEncoding("utf8");
37064
+ socket.on("connect", () => clearTimeout(connectTimer));
37065
+ socket.on("error", (err) => settle(() => reject(err)));
37066
+ socket.on("close", () => {
37067
+ settle(
37068
+ () => reject(new Error("permission broker closed the connection without an answer"))
37069
+ );
37070
+ });
37071
+ socket.on("data", (chunk) => {
37072
+ buffer += chunk;
37073
+ let nl;
37074
+ while ((nl = buffer.indexOf("\n")) >= 0) {
37075
+ const line = buffer.slice(0, nl).trim();
37076
+ buffer = buffer.slice(nl + 1);
37077
+ if (!line) continue;
37078
+ let msg;
37079
+ try {
37080
+ msg = JSON.parse(line);
37081
+ } catch {
37082
+ continue;
37083
+ }
37084
+ if (msg?.t === "answer" && msg.id === ask.id) {
37085
+ const { t: _t, id: _id, ...answer } = msg;
37086
+ settle(() => resolve3(answer));
37087
+ return;
37088
+ }
37089
+ }
37090
+ });
37091
+ sendJson(socket, ask);
37092
+ });
37093
+ }
37094
+ var PERMISSION_BROKER_DEFAULT_TIMEOUT_MS, PERMISSION_BROKER_DEFAULT_CONNECT_TIMEOUT_MS;
37095
+ var init_permissionBroker = __esm({
37096
+ "src/cli/mcp/permissionBroker.ts"() {
37097
+ "use strict";
37098
+ PERMISSION_BROKER_DEFAULT_TIMEOUT_MS = 6e4;
37099
+ PERMISSION_BROKER_DEFAULT_CONNECT_TIMEOUT_MS = 2e3;
37100
+ }
37101
+ });
37102
+
37103
+ // src/cli/mcp/brokerHandlers.ts
37104
+ var brokerHandlers_exports = {};
37105
+ __export(brokerHandlers_exports, {
37106
+ createBrokerPermissionHandler: () => createBrokerPermissionHandler,
37107
+ createBrokerQuestionHandler: () => createBrokerQuestionHandler,
37108
+ inferPermissionCategories: () => inferPermissionCategories
37109
+ });
37110
+ function inferPermissionCategories(toolName, input) {
37111
+ const cats = /* @__PURE__ */ new Set();
37112
+ const name = toolName.toLowerCase();
37113
+ const obj = input && typeof input === "object" ? input : {};
37114
+ if (/bash|shell|exec|command|run|npm|npx|yarn|pnpm|git|deno|bun|make/.test(name)) {
37115
+ cats.add("execute");
37116
+ }
37117
+ if (typeof obj.file_path === "string" || typeof obj.path === "string" || typeof obj.file === "string" || typeof obj.destination === "string" || typeof obj.repo === "string" || typeof obj.target === "string") {
37118
+ cats.add("write");
37119
+ }
37120
+ if (typeof obj.url === "string" || typeof obj.endpoint === "string" || typeof obj.host === "string" || /fetch|http|web|network|request/.test(name)) {
37121
+ cats.add("network");
37122
+ }
37123
+ if (cats.size === 0) {
37124
+ cats.add("execute");
37125
+ cats.add("network");
37126
+ }
37127
+ return [...cats];
37128
+ }
37129
+ function createBrokerPermissionHandler(opts) {
37130
+ const policy = opts.policy ?? defaultPermissionPolicy();
37131
+ return async (req) => {
37132
+ const categories = inferPermissionCategories(req.tool, req.input);
37133
+ const decision = resolveToolPermission(req.tool, categories, policy);
37134
+ if (decision.action === "allow") {
37135
+ const granted = isSessionGranted(
37136
+ req.tool,
37137
+ categories.length ? categories : ["execute"]
37138
+ );
37139
+ return granted ? { behavior: "allow", always: true } : { behavior: "allow" };
37140
+ }
37141
+ if (decision.action === "deny") {
37142
+ return { behavior: "deny", message: decision.reason };
37143
+ }
37144
+ if (!opts.onPermissionAsk) {
37145
+ return {
37146
+ behavior: "deny",
37147
+ message: `[zelari] no interactive approval available for "${req.tool}" (set ZELARI_AUTO=1 to auto-allow).`
37148
+ };
37149
+ }
37150
+ const ok = await opts.onPermissionAsk({
37151
+ toolName: req.tool,
37152
+ reason: decision.reason,
37153
+ categories,
37154
+ args: req.input
37155
+ });
37156
+ if (!ok) {
37157
+ return { behavior: "deny", message: "[zelari] denied by user" };
37158
+ }
37159
+ if (isSessionGranted(req.tool, categories.length ? categories : ["execute"])) {
37160
+ return { behavior: "allow", always: true };
37161
+ }
37162
+ return { behavior: "allow" };
37163
+ };
37164
+ }
37165
+ function createBrokerQuestionHandler(opts) {
37166
+ return async (req) => {
37167
+ if (!opts.onQuestion) return null;
37168
+ const answer = await opts.onQuestion({
37169
+ question: req.question,
37170
+ choices: req.choices,
37171
+ context: req.context
37172
+ });
37173
+ if (answer == null || !String(answer).trim()) {
37174
+ return { behavior: "deny", message: "[zelari] question cancelled" };
37175
+ }
37176
+ return { behavior: "allow", answer: String(answer).trim() };
37177
+ };
37178
+ }
37179
+ var init_brokerHandlers = __esm({
37180
+ "src/cli/mcp/brokerHandlers.ts"() {
37181
+ "use strict";
37182
+ init_toolPermissions();
37183
+ }
37184
+ });
37185
+
36505
37186
  // src/cli/kraken/planner.ts
36506
37187
  var planner_exports = {};
36507
37188
  __export(planner_exports, {
@@ -37208,6 +37889,230 @@ var init_tentacle = __esm({
37208
37889
  }
37209
37890
  });
37210
37891
 
37892
+ // src/cli/kraken/workbench.ts
37893
+ import { promises as fs20 } from "node:fs";
37894
+ import path35 from "node:path";
37895
+ function isWorkbenchEnabled(env = process.env) {
37896
+ const v = (env.ZELARI_KRAKEN_WORKBENCH ?? "1").trim().toLowerCase();
37897
+ if (v === "0" || v === "false" || v === "no" || v === "off") return false;
37898
+ return true;
37899
+ }
37900
+ function workbenchPath(cwd, graphId) {
37901
+ return path35.join(cwd, ".zelari", "radio", `workbench-${graphId}.md`);
37902
+ }
37903
+ function countByStatus2(nodes) {
37904
+ const out = { pending: 0, running: 0, done: 0, error: 0, skipped: 0 };
37905
+ for (const n of nodes) out[n.status] += 1;
37906
+ return out;
37907
+ }
37908
+ function statusEmoji(s) {
37909
+ switch (s) {
37910
+ case "pending":
37911
+ return "\u25CB";
37912
+ case "running":
37913
+ return "\u2191";
37914
+ case "done":
37915
+ return "\u2713";
37916
+ case "error":
37917
+ return "\u2717";
37918
+ case "skipped":
37919
+ return "\u2013";
37920
+ }
37921
+ }
37922
+ function formatMs(ms) {
37923
+ if (ms < 1e3) return `${ms}ms`;
37924
+ if (ms < 6e4) return `${(ms / 1e3).toFixed(1)}s`;
37925
+ return `${Math.floor(ms / 6e4)}m ${Math.floor(ms % 6e4 / 1e3)}s`;
37926
+ }
37927
+ function formatElapsed(ms) {
37928
+ const totalSec = Math.max(0, Math.floor(ms / 1e3));
37929
+ const hh = String(Math.floor(totalSec / 3600)).padStart(2, "0");
37930
+ const mm = String(Math.floor(totalSec % 3600 / 60)).padStart(2, "0");
37931
+ const ss = String(totalSec % 60).padStart(2, "0");
37932
+ return `${hh}:${mm}:${ss}`;
37933
+ }
37934
+ var DEBOUNCE_DEFAULT_MS, MAX_EVENTS, MAX_NODES_IN_TABLE, WorkbenchWriter;
37935
+ var init_workbench = __esm({
37936
+ "src/cli/kraken/workbench.ts"() {
37937
+ "use strict";
37938
+ init_dist();
37939
+ DEBOUNCE_DEFAULT_MS = 500;
37940
+ MAX_EVENTS = 30;
37941
+ MAX_NODES_IN_TABLE = 200;
37942
+ WorkbenchWriter = class {
37943
+ cwd;
37944
+ graphId;
37945
+ goal;
37946
+ enabled;
37947
+ debounceMs;
37948
+ startedAt;
37949
+ nodesById = /* @__PURE__ */ new Map();
37950
+ events = [];
37951
+ dirty = true;
37952
+ timer = null;
37953
+ lastWrite = null;
37954
+ constructor(opts) {
37955
+ this.cwd = opts.cwd;
37956
+ this.graphId = opts.graphId;
37957
+ this.goal = opts.goal;
37958
+ this.enabled = opts.enabled ?? isWorkbenchEnabled();
37959
+ this.debounceMs = opts.debounceMs ?? DEBOUNCE_DEFAULT_MS;
37960
+ this.startedAt = (/* @__PURE__ */ new Date()).toISOString();
37961
+ }
37962
+ /** Seed the writer with the planned nodes. Idempotent: later calls
37963
+ * merge — a node's status is not reset to `pending` if it's already
37964
+ * `running` / `done` / `error`. */
37965
+ setNodes(nodes) {
37966
+ if (!this.enabled) return;
37967
+ for (const n of nodes) {
37968
+ const existing = this.nodesById.get(n.id);
37969
+ if (!existing) this.nodesById.set(n.id, { ...n });
37970
+ else if (existing.status !== "pending") {
37971
+ this.nodesById.set(n.id, { ...existing, ...n, status: existing.status });
37972
+ } else {
37973
+ this.nodesById.set(n.id, { ...n });
37974
+ }
37975
+ }
37976
+ this.markDirty();
37977
+ }
37978
+ markStart(id, patch = {}) {
37979
+ if (!this.enabled) return;
37980
+ const n = this.nodesById.get(id) ?? { id, kind: "general", label: id, status: "pending" };
37981
+ this.nodesById.set(id, {
37982
+ ...n,
37983
+ ...patch,
37984
+ id,
37985
+ status: "running",
37986
+ startedAt: patch.startedAt ?? (/* @__PURE__ */ new Date()).toISOString()
37987
+ });
37988
+ this.events.push({ ts: (/* @__PURE__ */ new Date()).toISOString(), text: `node_start ${id} ${patch.kind ?? n.kind}` });
37989
+ this.markDirty();
37990
+ }
37991
+ markEnd(id, patch) {
37992
+ if (!this.enabled) return;
37993
+ const n = this.nodesById.get(id);
37994
+ if (!n) return;
37995
+ let verdict;
37996
+ let weaknessScore;
37997
+ if (patch.findings && patch.findings.length > 0) {
37998
+ const parsed = parsePersonaVerdict(patch.findings);
37999
+ verdict = parsed.verdict;
38000
+ weaknessScore = parsed.weaknessScore;
38001
+ }
38002
+ this.nodesById.set(id, {
38003
+ ...n,
38004
+ status: patch.status,
38005
+ ...patch.durationMs !== void 0 ? { durationMs: patch.durationMs } : {},
38006
+ ...patch.error ? { error: patch.error.slice(0, 200) } : {},
38007
+ ...Array.isArray(patch.deps) ? { deps: patch.deps } : {},
38008
+ ...verdict ? { verdict } : {},
38009
+ ...typeof weaknessScore === "number" ? { weaknessScore } : {},
38010
+ ...patch.model ? { model: patch.model } : {},
38011
+ endedAt: (/* @__PURE__ */ new Date()).toISOString()
38012
+ });
38013
+ this.events.push({
38014
+ ts: (/* @__PURE__ */ new Date()).toISOString(),
38015
+ text: `node_end ${id} ${patch.status}${patch.durationMs !== void 0 ? ` (${patch.durationMs}ms)` : ""}${verdict ? ` \xB7 ${verdict}${typeof weaknessScore === "number" ? ` w=${weaknessScore.toFixed(2)}` : ""}` : ""}${patch.error ? ` \u2014 ${patch.error.slice(0, 80)}` : ""}`
38016
+ });
38017
+ this.markDirty();
38018
+ }
38019
+ logEvent(text) {
38020
+ if (!this.enabled) return;
38021
+ this.events.push({ ts: (/* @__PURE__ */ new Date()).toISOString(), text });
38022
+ this.markDirty();
38023
+ }
38024
+ /** Mark "this set of node ids is running in parallel right now". The
38025
+ * workbench surfaces a "wave" line so a `tail -f` reader can see when
38026
+ * a new parallel wave starts. */
38027
+ markWave(ids) {
38028
+ if (!this.enabled) return;
38029
+ this.events.push({ ts: (/* @__PURE__ */ new Date()).toISOString(), text: `wave: ${ids.join(", ")}` });
38030
+ this.markDirty();
38031
+ }
38032
+ /** Force a synchronous-style flush. Returns the path written, or null
38033
+ * when the writer is disabled. */
38034
+ async flush() {
38035
+ if (!this.enabled) return null;
38036
+ if (this.timer) {
38037
+ clearTimeout(this.timer);
38038
+ this.timer = null;
38039
+ }
38040
+ return this.writeNow();
38041
+ }
38042
+ /** Cancel any pending debounced write. The next `flush()` will write. */
38043
+ close() {
38044
+ if (this.timer) {
38045
+ clearTimeout(this.timer);
38046
+ this.timer = null;
38047
+ }
38048
+ }
38049
+ // --- internals ------------------------------------------------------------
38050
+ markDirty() {
38051
+ if (!this.enabled) return;
38052
+ this.dirty = true;
38053
+ if (this.timer) return;
38054
+ this.timer = setTimeout(() => {
38055
+ this.timer = null;
38056
+ void this.writeNow().catch(() => {
38057
+ });
38058
+ }, this.debounceMs);
38059
+ }
38060
+ async writeNow() {
38061
+ if (!this.enabled) return null;
38062
+ if (!this.dirty && this.lastWrite) return this.lastWrite;
38063
+ const out = workbenchPath(this.cwd, this.graphId);
38064
+ await fs20.mkdir(path35.dirname(out), { recursive: true });
38065
+ const body = this.render();
38066
+ const tmp = `${out}.${process.pid}.${Date.now()}.tmp`;
38067
+ await fs20.writeFile(tmp, body, "utf8");
38068
+ await fs20.rename(tmp, out);
38069
+ this.dirty = false;
38070
+ this.lastWrite = Promise.resolve(out);
38071
+ return out;
38072
+ }
38073
+ render() {
38074
+ const counts = countByStatus2([...this.nodesById.values()]);
38075
+ const elapsed = formatElapsed(Date.now() - Date.parse(this.startedAt));
38076
+ const lines = [
38077
+ `# Kraken workbench`,
38078
+ ``,
38079
+ `**Goal:** ${this.goal}`,
38080
+ `**Graph id:** \`${this.graphId}\``,
38081
+ `**Started:** ${this.startedAt} \xB7 **Elapsed:** ${elapsed}`,
38082
+ ``,
38083
+ `## Progress: ${counts.done + counts.skipped}/${this.nodesById.size} \xB7 ${counts.running}\u2191 \xB7 ${counts.error}\u2717`,
38084
+ ``
38085
+ ];
38086
+ lines.push(`## Wave`);
38087
+ lines.push(``);
38088
+ lines.push(`| id | label | kind | deps | scope | status | verdict | weakness | model | duration |`);
38089
+ lines.push(`|----|-------|------|------|-------|--------|---------|----------|-------|----------|`);
38090
+ const sorted = [...this.nodesById.values()].sort((a, b) => a.id.localeCompare(b.id)).slice(0, MAX_NODES_IN_TABLE);
38091
+ for (const n of sorted) {
38092
+ const status = statusEmoji(n.status);
38093
+ const scope = n.scope ? n.scope.join(", ") : "";
38094
+ const dur = n.durationMs != null ? formatMs(n.durationMs) : "";
38095
+ const verdict = n.verdict ?? "";
38096
+ const weakness = typeof n.weaknessScore === "number" ? n.weaknessScore.toFixed(2) : "";
38097
+ const deps = n.deps && n.deps.length > 0 ? n.deps.join(", ") : "";
38098
+ lines.push(`| ${n.id} | ${n.label} | ${n.kind} | ${deps} | ${scope} | ${status} | ${verdict} | ${weakness} | ${n.model ?? ""} | ${dur} |`);
38099
+ }
38100
+ lines.push(``);
38101
+ lines.push(`## Events (latest ${MAX_EVENTS})`);
38102
+ lines.push(``);
38103
+ const tail = this.events.slice(-MAX_EVENTS);
38104
+ if (tail.length === 0) {
38105
+ lines.push(`(no events yet)`);
38106
+ } else {
38107
+ for (const e of tail) lines.push(`- ${e.ts.slice(11, 19)} ${e.text}`);
38108
+ }
38109
+ lines.push(``);
38110
+ return lines.join("\n");
38111
+ }
38112
+ };
38113
+ }
38114
+ });
38115
+
37211
38116
  // src/cli/kraken/weaknessMeter.ts
37212
38117
  var weaknessMeter_exports = {};
37213
38118
  __export(weaknessMeter_exports, {
@@ -37353,7 +38258,7 @@ __export(executor_exports, {
37353
38258
  thoroughnessForKind: () => thoroughnessForKind
37354
38259
  });
37355
38260
  import { existsSync as existsSync34 } from "node:fs";
37356
- import path35 from "node:path";
38261
+ import path36 from "node:path";
37357
38262
  function resolveMaxParallel(env = process.env) {
37358
38263
  const raw = env.ZELARI_KRAKEN_MAX_PARALLEL;
37359
38264
  if (raw === void 0 || raw === "") return DEFAULT_MAX_PARALLEL;
@@ -37411,7 +38316,7 @@ function isWorldModelGateEnabled(cwd, env = process.env, checksExists = defaultC
37411
38316
  }
37412
38317
  function defaultChecksExists(cwd) {
37413
38318
  try {
37414
- return existsSync34(path35.join(cwd, ".zelari", "world", "checks.json"));
38319
+ return existsSync34(path36.join(cwd, ".zelari", "world", "checks.json"));
37415
38320
  } catch {
37416
38321
  return false;
37417
38322
  }
@@ -37471,6 +38376,7 @@ var init_executor = __esm({
37471
38376
  init_worldModel();
37472
38377
  init_graphStatus();
37473
38378
  init_graphMemory();
38379
+ init_workbench();
37474
38380
  DEFAULT_MAX_PARALLEL = 12;
37475
38381
  DEFAULT_FIX_BUDGET = 3;
37476
38382
  DEFAULT_MAX_REVIEW_ROUNDS = 1;
@@ -37498,6 +38404,8 @@ var init_executor = __esm({
37498
38404
  runTentacleFn;
37499
38405
  mergeFn;
37500
38406
  backtestFn;
38407
+ /** Live workbench writer for this run (created once the graph starts). */
38408
+ wb = null;
37501
38409
  nodeRunState = /* @__PURE__ */ new Map();
37502
38410
  /** fix node id → id of the failed node it was spawned to repair. */
37503
38411
  repairs = /* @__PURE__ */ new Map();
@@ -37546,6 +38454,44 @@ var init_executor = __esm({
37546
38454
  this.mergeFn = opts.mergeFn ?? mergeKrakenWorktree;
37547
38455
  this.backtestFn = opts.backtestFn ?? runBacktest;
37548
38456
  }
38457
+ /** Map a graph node onto the workbench's node shape. */
38458
+ toWorkbenchNode(n) {
38459
+ return {
38460
+ id: n.id,
38461
+ kind: n.kind,
38462
+ label: n.label,
38463
+ status: n.status,
38464
+ ...n.scope && n.scope.length > 0 ? { scope: n.scope } : {},
38465
+ ...n.deps.length > 0 ? { deps: n.deps } : {}
38466
+ };
38467
+ }
38468
+ /**
38469
+ * Create (once) and seed the live workbench writer for this graph. This is
38470
+ * what makes the `.zelari/radio/workbench-<graphId>.md` file exist at all —
38471
+ * the desktop graph/tail tabs read that file, and before this wiring
38472
+ * nothing in the production flow ever constructed the writer, so the tabs
38473
+ * stayed empty no matter how much the graph ran.
38474
+ */
38475
+ initWorkbench(graph) {
38476
+ if (this.wb) return;
38477
+ this.wb = new WorkbenchWriter({
38478
+ cwd: this.parentCwd,
38479
+ graphId: graph.id,
38480
+ goal: this.goal ?? graph.id
38481
+ });
38482
+ this.wb.setNodes([...graph.nodes.values()].map((n) => this.toWorkbenchNode(n)));
38483
+ this.wb.logEvent(`graph_start ${graph.id} (${graph.nodes.size} nodes)`);
38484
+ }
38485
+ /** Mark every skipped node in the workbench. Cheap and idempotent enough
38486
+ * to call after each cascade-skip pass. */
38487
+ syncWorkbenchSkipped(graph) {
38488
+ for (const n of graph.nodes.values()) {
38489
+ if (n.status === "skipped") this.wb?.markEnd(n.id, { status: "skipped" });
38490
+ }
38491
+ }
38492
+ isReviewerKind(kind) {
38493
+ return kind === "verify" || kind === "spec" || kind === "conformance";
38494
+ }
37549
38495
  /** Execute the graph in place (mutates node statuses) until it settles. */
37550
38496
  async execute(graph) {
37551
38497
  if (this.fixBudgetOption === void 0) {
@@ -37581,6 +38527,7 @@ var init_executor = __esm({
37581
38527
  let iterations = 0;
37582
38528
  const inFlight = /* @__PURE__ */ new Map();
37583
38529
  startKrakenGraphLive(graph);
38530
+ this.initWorkbench(graph);
37584
38531
  while (!isSettled(graph)) {
37585
38532
  iterations += 1;
37586
38533
  if (iterations > maxIterations) {
@@ -37596,13 +38543,23 @@ var init_executor = __esm({
37596
38543
  if (admitted.length > 0) {
37597
38544
  for (const node of admitted) node.status = "running";
37598
38545
  updateKrakenGraphLive(graph);
38546
+ for (const node of admitted) {
38547
+ this.wb?.markStart(node.id, {
38548
+ kind: node.kind,
38549
+ label: node.label,
38550
+ ...node.scope && node.scope.length > 0 ? { scope: node.scope } : {},
38551
+ ...node.deps.length > 0 ? { deps: node.deps } : {}
38552
+ });
38553
+ }
38554
+ if (admitted.length > 0) this.wb?.markWave(admitted.map((n) => n.id));
37599
38555
  for (const node of admitted) inFlight.set(node.id, this.runNodeSafely(node, graph));
37600
38556
  }
37601
38557
  if (inFlight.size === 0) {
37602
- if (!this.skipBlockedNodes(graph)) {
37603
- break;
38558
+ if (this.skipBlockedNodes(graph)) {
38559
+ this.syncWorkbenchSkipped(graph);
38560
+ continue;
37604
38561
  }
37605
- continue;
38562
+ break;
37606
38563
  }
37607
38564
  const { id, res } = await Promise.race(inFlight.values());
37608
38565
  inFlight.delete(id);
@@ -37621,6 +38578,7 @@ var init_executor = __esm({
37621
38578
  for (const n of graph.nodes.values()) {
37622
38579
  if (n.status === "pending") n.status = "skipped";
37623
38580
  }
38581
+ this.syncWorkbenchSkipped(graph);
37624
38582
  }
37625
38583
  let backtest;
37626
38584
  const converged = !this.aborted && isConverged(graph);
@@ -37642,6 +38600,11 @@ var init_executor = __esm({
37642
38600
  });
37643
38601
  }
37644
38602
  endKrakenGraphLive(graph, converged);
38603
+ this.wb?.logEvent(
38604
+ converged ? "graph_converged" : `graph_failed: ${this.aborted ? "cancelled" : failedNodeIds(graph).join(", ") || "none"}`
38605
+ );
38606
+ await this.wb?.flush();
38607
+ this.wb?.close();
37645
38608
  await saveGraphSnapshot(
37646
38609
  this.parentCwd,
37647
38610
  toGraphSnapshot(graph, {
@@ -37934,6 +38897,13 @@ ${upstream}` : node.prompt,
37934
38897
  node.status = "done";
37935
38898
  node.result = res.result;
37936
38899
  this.radio("node_end", { description: node.label, agent: node.kind, ok: true });
38900
+ this.wb?.markEnd(node.id, {
38901
+ status: "done",
38902
+ durationMs: this.durationsMs.get(node.id),
38903
+ // Reviewers surface verdict + Bennett weakness; writers don't.
38904
+ ...this.isReviewerKind(node.kind) && typeof node.result === "string" && node.result.length > 0 ? { findings: node.result } : {},
38905
+ ...res.model && res.model !== "n/a" ? { model: res.model } : {}
38906
+ });
37937
38907
  this.reconcileRepairedNode(graph, node);
37938
38908
  if (node.kind === "verify") this.applyVerifyVerdict(graph, node);
37939
38909
  return;
@@ -37947,6 +38917,11 @@ ${upstream}` : node.prompt,
37947
38917
  detail: res.error,
37948
38918
  ok: false
37949
38919
  });
38920
+ this.wb?.markEnd(node.id, {
38921
+ status: "error",
38922
+ error: res.error,
38923
+ durationMs: this.durationsMs.get(node.id)
38924
+ });
37950
38925
  return;
37951
38926
  }
37952
38927
  if (res.cancelled === false) {
@@ -37957,6 +38932,11 @@ ${upstream}` : node.prompt,
37957
38932
  detail: res.error,
37958
38933
  ok: false
37959
38934
  });
38935
+ this.wb?.markEnd(node.id, {
38936
+ status: "error",
38937
+ error: res.error,
38938
+ durationMs: this.durationsMs.get(node.id)
38939
+ });
37960
38940
  return;
37961
38941
  }
37962
38942
  if (node.retryCount < node.maxRetries) {
@@ -37968,6 +38948,7 @@ ${upstream}` : node.prompt,
37968
38948
  detail: `retry ${node.retryCount}/${node.maxRetries}: ${res.error}`,
37969
38949
  ok: false
37970
38950
  });
38951
+ this.wb?.markEnd(node.id, { status: "pending" });
37971
38952
  return;
37972
38953
  }
37973
38954
  if (this.fixBudgetRemaining > 0 && node.kind !== "fix") {
@@ -37980,6 +38961,11 @@ ${upstream}` : node.prompt,
37980
38961
  detail: `spawned to address failure of "${node.label}": ${res.error}`,
37981
38962
  ok: false
37982
38963
  });
38964
+ this.wb?.markEnd(node.id, {
38965
+ status: "error",
38966
+ error: res.error,
38967
+ durationMs: this.durationsMs.get(node.id)
38968
+ });
37983
38969
  node.status = "error";
37984
38970
  return;
37985
38971
  }
@@ -37990,6 +38976,11 @@ ${upstream}` : node.prompt,
37990
38976
  detail: res.error,
37991
38977
  ok: false
37992
38978
  });
38979
+ this.wb?.markEnd(node.id, {
38980
+ status: "error",
38981
+ error: res.error,
38982
+ durationMs: this.durationsMs.get(node.id)
38983
+ });
37993
38984
  }
37994
38985
  /**
37995
38986
  * A `fix` node just completed the work its failed predecessor could not.
@@ -38019,6 +39010,10 @@ ${upstream}` : node.prompt,
38019
39010
  detail: `repaired by ${fixNode.id}`,
38020
39011
  ok: true
38021
39012
  });
39013
+ this.wb?.markEnd(failed.id, {
39014
+ status: "done",
39015
+ durationMs: this.durationsMs.get(failed.id)
39016
+ });
38022
39017
  }
38023
39018
  /**
38024
39019
  * Act on what a completed `verify` node concluded.
@@ -38153,6 +39148,7 @@ ${findings || "(the reviewer reported FAIL without detail)"}`,
38153
39148
  maxRetries: verify.maxRetries
38154
39149
  };
38155
39150
  graph.nodes.set(reVerifyId, reVerifyNode);
39151
+ this.wb?.setNodes([this.toWorkbenchNode(reworkNode), this.toWorkbenchNode(reVerifyNode)]);
38156
39152
  for (const other of graph.nodes.values()) {
38157
39153
  if (other.id === reworkId || other.id === reVerifyId) continue;
38158
39154
  if (other.deps.includes(verify.id)) {
@@ -38192,6 +39188,7 @@ ${failed.error ?? "unknown error"}`,
38192
39188
  // no further retries — one fix attempt per failed node in v1
38193
39189
  };
38194
39190
  graph.nodes.set(fixId, fixNode);
39191
+ this.wb?.setNodes([this.toWorkbenchNode(fixNode)]);
38195
39192
  this.repairs.set(fixId, failed.id);
38196
39193
  for (const other of graph.nodes.values()) {
38197
39194
  if (other.id === fixId) continue;
@@ -38658,10 +39655,10 @@ var init_prereqChecks = __esm({
38658
39655
 
38659
39656
  // src/cli/plugins/prefs.ts
38660
39657
  import { existsSync as existsSync36, readFileSync as readFileSync29, writeFileSync as writeFileSync18, mkdirSync as mkdirSync15 } from "node:fs";
38661
- import path38 from "node:path";
39658
+ import path39 from "node:path";
38662
39659
  import os9 from "node:os";
38663
39660
  function getPluginPrefsPath() {
38664
- return process.env.ZELARI_PLUGINS_PREFS_FILE ?? path38.join(os9.homedir(), ".tmp", "zelari-code", "plugins.json");
39661
+ return process.env.ZELARI_PLUGINS_PREFS_FILE ?? path39.join(os9.homedir(), ".tmp", "zelari-code", "plugins.json");
38665
39662
  }
38666
39663
  function getPluginPrefs() {
38667
39664
  const file2 = getPluginPrefsPath();
@@ -38682,7 +39679,7 @@ function getPluginPrefs() {
38682
39679
  }
38683
39680
  function writePluginPrefs(prefs) {
38684
39681
  const file2 = getPluginPrefsPath();
38685
- mkdirSync15(path38.dirname(file2), { recursive: true });
39682
+ mkdirSync15(path39.dirname(file2), { recursive: true });
38686
39683
  writeFileSync18(file2, JSON.stringify(prefs, null, 2), {
38687
39684
  encoding: "utf-8",
38688
39685
  mode: 384
@@ -38719,7 +39716,7 @@ __export(registry_exports, {
38719
39716
  isBinaryOnPath: () => isBinaryOnPath
38720
39717
  });
38721
39718
  import { existsSync as existsSync37 } from "node:fs";
38722
- import path39 from "node:path";
39719
+ import path40 from "node:path";
38723
39720
  function detectLocalBin(bin) {
38724
39721
  return (cwd) => {
38725
39722
  try {
@@ -38737,7 +39734,7 @@ function isBinaryOnPath(bin, opts = {}) {
38737
39734
  const platform = opts.platform ?? process.platform;
38738
39735
  const exists = opts.exists ?? existsSync37;
38739
39736
  const pathEnv = opts.pathEnv ?? process.env.PATH ?? "";
38740
- const pathMod = platform === "win32" ? path39.win32 : path39.posix;
39737
+ const pathMod = platform === "win32" ? path40.win32 : path40.posix;
38741
39738
  const sep2 = platform === "win32" ? ";" : ":";
38742
39739
  const dirs = pathEnv.split(sep2).filter((d) => d.length > 0);
38743
39740
  const candidates = [bin];
@@ -38875,7 +39872,11 @@ __export(atMentions_exports, {
38875
39872
  hasAtMentions: () => hasAtMentions
38876
39873
  });
38877
39874
  import { existsSync as existsSync40, readFileSync as readFileSync31, statSync as statSync6 } from "node:fs";
38878
- import { isAbsolute as isAbsolute2, relative as relative3, resolve, sep } from "node:path";
39875
+ import { basename as basename3, isAbsolute as isAbsolute2, relative as relative3, resolve, sep } from "node:path";
39876
+ function isImagePath(abs) {
39877
+ const ext = abs.split(".").pop()?.toLowerCase() ?? "";
39878
+ return ext in IMAGE_MIME_BY_EXT;
39879
+ }
38879
39880
  function isProbablyText(name, head) {
38880
39881
  if (/\.(txt|md|markdown|json|jsonc|ts|tsx|js|jsx|mjs|cjs|css|scss|html|htm|xml|yml|yaml|toml|ini|cfg|conf|rs|go|py|java|kt|swift|c|cc|cpp|h|hpp|cs|rb|php|sh|bash|zsh|ps1|sql|graphql|env|gitignore|dockerfile|makefile|cmake|lock|svg)$/i.test(
38881
39882
  name
@@ -38893,32 +39894,29 @@ function underRoot(abs, root) {
38893
39894
  function extractAtMentions(text) {
38894
39895
  const out = [];
38895
39896
  const seen = /* @__PURE__ */ new Set();
38896
- AT_PATH_RE.lastIndex = 0;
38897
- let m;
38898
- while ((m = AT_PATH_RE.exec(text)) !== null) {
38899
- const token = (m[2] ?? "").trim();
38900
- if (!token || token.includes("@")) continue;
38901
- const key = token.replace(/\\/g, "/").toLowerCase();
38902
- if (seen.has(key)) continue;
38903
- seen.add(key);
38904
- out.push(token);
38905
- }
39897
+ const collect = (re) => {
39898
+ re.lastIndex = 0;
39899
+ let m;
39900
+ while ((m = re.exec(text)) !== null) {
39901
+ const token = (m[2] ?? "").trim().replace(/^@/, "");
39902
+ if (!token || token.includes("@")) continue;
39903
+ if (re === AT_PATH_RE && /^[a-zA-Z]$/.test(token) && text.charAt(m.index + m[0].length) === ":") {
39904
+ continue;
39905
+ }
39906
+ const key = token.replace(/\\/g, "/").toLowerCase();
39907
+ if (seen.has(key)) continue;
39908
+ seen.add(key);
39909
+ out.push(token);
39910
+ }
39911
+ };
39912
+ collect(AT_PATH_RE);
39913
+ collect(AT_WIN_ABS_RE);
39914
+ collect(AT_POSIX_ABS_RE);
38906
39915
  return out;
38907
39916
  }
38908
39917
  function resolveMention(token, cwd) {
38909
39918
  const abs = isAbsolute2(token) ? resolve(token) : resolve(cwd, token);
38910
- if (!underRoot(abs, cwd) && !isAbsolute2(token)) {
38911
- if (!underRoot(abs, cwd)) {
38912
- return {
38913
- raw: token,
38914
- path: token,
38915
- absolute: abs,
38916
- isDir: false,
38917
- note: "outside project root \u2014 skipped"
38918
- };
38919
- }
38920
- }
38921
- if (isAbsolute2(token) && !underRoot(abs, cwd)) {
39919
+ if (!underRoot(abs, cwd) && !isImagePath(abs)) {
38922
39920
  return {
38923
39921
  raw: token,
38924
39922
  path: token,
@@ -38967,6 +39965,28 @@ function resolveMention(token, cwd) {
38967
39965
  note: `too large (${Math.round(st.size / 1024)} KB) \u2014 path only`
38968
39966
  };
38969
39967
  }
39968
+ const ext = abs.split(".").pop()?.toLowerCase() ?? "";
39969
+ const mime = IMAGE_MIME_BY_EXT[ext];
39970
+ if (mime) {
39971
+ if (st.size > ATTACH_IMAGE_MAX_BYTES) {
39972
+ return {
39973
+ raw: token,
39974
+ path: rel2,
39975
+ absolute: abs,
39976
+ isDir: false,
39977
+ note: `image too large (${Math.round(st.size / 1024)} KB) \u2014 path only`
39978
+ };
39979
+ }
39980
+ const dataBase64 = readFileSync31(abs).toString("base64");
39981
+ return {
39982
+ raw: token,
39983
+ path: rel2,
39984
+ absolute: abs,
39985
+ isDir: false,
39986
+ text: `[Immagine: ${rel2}]`,
39987
+ image: { mime, dataBase64, alt: basename3(abs) }
39988
+ };
39989
+ }
38970
39990
  try {
38971
39991
  const buf = readFileSync31(abs);
38972
39992
  const head = buf.subarray(0, 800).toString("utf8");
@@ -39012,6 +40032,10 @@ function expandAtMentions(userText, cwd = process.cwd()) {
39012
40032
  }
39013
40033
  const blocks = hits.map((h) => {
39014
40034
  const label = h.path || h.raw;
40035
+ if (h.image) {
40036
+ const kb = Math.round(h.image.dataBase64.length * 3 / 4 / 1024);
40037
+ return `--- Image: ${label} (${h.image.mime}, ~${kb} KB) ---`;
40038
+ }
39015
40039
  if (h.text != null && h.text.length > 0) {
39016
40040
  return `--- File: ${label} ---
39017
40041
  ${h.text}
@@ -39029,13 +40053,28 @@ ${blocks.join("\n\n")}`;
39029
40053
  function hasAtMentions(text) {
39030
40054
  return extractAtMentions(text).length > 0;
39031
40055
  }
39032
- var ATTACH_TEXT_MAX, ATTACH_FILE_MAX_BYTES, AT_PATH_RE;
40056
+ var ATTACH_TEXT_MAX, ATTACH_FILE_MAX_BYTES, ATTACH_IMAGE_MAX_BYTES, IMAGE_MIME_BY_EXT, AT_PATH_RE, BS_CHAR, AT_WIN_ABS_RE, AT_POSIX_ABS_RE;
39033
40057
  var init_atMentions = __esm({
39034
40058
  "src/cli/atMentions.ts"() {
39035
40059
  "use strict";
39036
40060
  ATTACH_TEXT_MAX = 48e3;
39037
40061
  ATTACH_FILE_MAX_BYTES = 512e3;
40062
+ ATTACH_IMAGE_MAX_BYTES = 8 * 1024 * 1024;
40063
+ IMAGE_MIME_BY_EXT = {
40064
+ jpg: "image/jpeg",
40065
+ jpeg: "image/jpeg",
40066
+ png: "image/png",
40067
+ gif: "image/gif",
40068
+ webp: "image/webp",
40069
+ bmp: "image/bmp"
40070
+ };
39038
40071
  AT_PATH_RE = /(^|[\s([{])@((?:\.{1,2}\/)?[A-Za-z0-9_.+-]+(?:[\\/][A-Za-z0-9_.+-]+)*)/g;
40072
+ BS_CHAR = String.fromCharCode(92);
40073
+ AT_WIN_ABS_RE = new RegExp(
40074
+ "(^|[" + BS_CHAR + "s([{])(@[A-Za-z]:[" + BS_CHAR + BS_CHAR + "/][^" + BS_CHAR + "s@]+)",
40075
+ "g"
40076
+ );
40077
+ AT_POSIX_ABS_RE = /(^|[\s([{])@(\/[^\s@]+)/g;
39039
40078
  }
39040
40079
  });
39041
40080
 
@@ -39046,10 +40085,10 @@ __export(triggerLock_exports, {
39046
40085
  lockPath: () => lockPath,
39047
40086
  releaseLock: () => releaseLock
39048
40087
  });
39049
- import { promises as fs27 } from "node:fs";
39050
- import * as path44 from "node:path";
40088
+ import { promises as fs28 } from "node:fs";
40089
+ import * as path45 from "node:path";
39051
40090
  function lockPath(projectRoot) {
39052
- return path44.join(projectRoot, ".zelari", "trigger.lock");
40091
+ return path45.join(projectRoot, ".zelari", "trigger.lock");
39053
40092
  }
39054
40093
  function isPidAlive(pid) {
39055
40094
  try {
@@ -39062,10 +40101,10 @@ function isPidAlive(pid) {
39062
40101
  }
39063
40102
  async function acquireLock(projectRoot, now = () => /* @__PURE__ */ new Date()) {
39064
40103
  const lp = lockPath(projectRoot);
39065
- const dir = path44.dirname(lp);
39066
- await fs27.mkdir(dir, { recursive: true });
40104
+ const dir = path45.dirname(lp);
40105
+ await fs28.mkdir(dir, { recursive: true });
39067
40106
  try {
39068
- const raw = await fs27.readFile(lp, "utf8");
40107
+ const raw = await fs28.readFile(lp, "utf8");
39069
40108
  const existing = JSON.parse(raw);
39070
40109
  if (existing.pid && isPidAlive(existing.pid)) {
39071
40110
  return { acquired: false, heldBy: existing.pid, lockPath: lp };
@@ -39076,13 +40115,13 @@ async function acquireLock(projectRoot, now = () => /* @__PURE__ */ new Date())
39076
40115
  pid: process.pid,
39077
40116
  acquiredAt: now().toISOString()
39078
40117
  };
39079
- await fs27.writeFile(lp, JSON.stringify(payload, null, 2) + "\n", "utf8");
40118
+ await fs28.writeFile(lp, JSON.stringify(payload, null, 2) + "\n", "utf8");
39080
40119
  return { acquired: true, lockPath: lp };
39081
40120
  }
39082
40121
  async function releaseLock(projectRoot) {
39083
40122
  const lp = lockPath(projectRoot);
39084
40123
  try {
39085
- await fs27.unlink(lp);
40124
+ await fs28.unlink(lp);
39086
40125
  } catch {
39087
40126
  }
39088
40127
  }
@@ -39344,6 +40383,272 @@ var init_desktopConfig = __esm({
39344
40383
  }
39345
40384
  });
39346
40385
 
40386
+ // src/cli/mcp/mcpPermissionServer.ts
40387
+ import { createInterface } from "node:readline";
40388
+ import { randomUUID as randomUUID7 } from "node:crypto";
40389
+ function startPermissionMcpServer(opts) {
40390
+ const socketPath = opts.socketPath.trim();
40391
+ const requestTimeoutMs = opts.requestTimeoutMs ?? PERMISSION_BROKER_DEFAULT_TIMEOUT_MS;
40392
+ const input = opts.input ?? process.stdin;
40393
+ const output = opts.output ?? process.stdout;
40394
+ input.on?.("error", () => {
40395
+ });
40396
+ output.on?.("error", () => {
40397
+ });
40398
+ let closedResolve = () => {
40399
+ };
40400
+ const closed = new Promise((resolve3) => {
40401
+ closedResolve = resolve3;
40402
+ });
40403
+ let stopped = false;
40404
+ const send = (obj) => {
40405
+ if (stopped || output.destroyed) return;
40406
+ output.write(JSON.stringify(obj) + "\n");
40407
+ };
40408
+ const rl = createInterface({ input, crlfDelay: Infinity });
40409
+ rl.on("line", (line) => {
40410
+ const text = line.trim();
40411
+ if (!text) return;
40412
+ let msg;
40413
+ try {
40414
+ msg = JSON.parse(text);
40415
+ } catch {
40416
+ return;
40417
+ }
40418
+ if (typeof msg.id === "undefined") return;
40419
+ void handleRpc(msg).then((out) => {
40420
+ send({
40421
+ jsonrpc: "2.0",
40422
+ id: msg.id,
40423
+ ...out ?? {}
40424
+ });
40425
+ }).catch((err) => {
40426
+ send({
40427
+ jsonrpc: "2.0",
40428
+ id: msg.id,
40429
+ error: {
40430
+ code: -32603,
40431
+ message: `internal error: ${err instanceof Error ? err.message : String(err)}`
40432
+ }
40433
+ });
40434
+ });
40435
+ });
40436
+ rl.on("close", () => {
40437
+ closedResolve();
40438
+ });
40439
+ async function handleRpc(msg) {
40440
+ const method = msg.method ?? "";
40441
+ if (typeof msg.id === "undefined") {
40442
+ return { result: null };
40443
+ }
40444
+ if (method === "initialize") {
40445
+ const requested = typeof msg.params?.protocolVersion === "string" ? msg.params.protocolVersion : PERMISSION_MCP_PROTOCOL_VERSION;
40446
+ return {
40447
+ result: {
40448
+ protocolVersion: requested,
40449
+ capabilities: { tools: {} },
40450
+ serverInfo: {
40451
+ name: "zelari-permission-mcp",
40452
+ version: getCurrentVersion()
40453
+ }
40454
+ }
40455
+ };
40456
+ }
40457
+ if (method === "tools/list") {
40458
+ return { result: { tools: PERMISSION_MCP_TOOLS } };
40459
+ }
40460
+ if (method === "tools/call") {
40461
+ const params = msg.params ?? {};
40462
+ const name = typeof params.name === "string" ? params.name : "";
40463
+ const args = params.arguments && typeof params.arguments === "object" ? params.arguments : {};
40464
+ const out = name === "approve" ? await callApprove(args, requestTimeoutMs) : name === "ask_user" ? await callAskUser(args, requestTimeoutMs) : { error: { code: -32602, message: `unknown tool: ${name}` } };
40465
+ if (out && typeof out === "object" && "error" in out && out.error) {
40466
+ return { error: out.error };
40467
+ }
40468
+ return { result: out };
40469
+ }
40470
+ return { error: { code: -32601, message: `method not found: ${method}` } };
40471
+ }
40472
+ async function callApprove(args, timeoutMs) {
40473
+ const toolName = typeof args.tool_name === "string" ? args.tool_name : "";
40474
+ if (!toolName) {
40475
+ return { error: { code: -32602, message: "approve requires a string tool_name" } };
40476
+ }
40477
+ const input2 = args.input ?? {};
40478
+ const suggestions = Array.isArray(args.permission_suggestions) ? args.permission_suggestions : Array.isArray(args.suggestions) ? args.suggestions : void 0;
40479
+ try {
40480
+ const res = await requestBrokerAsk(
40481
+ socketPath,
40482
+ {
40483
+ t: "ask",
40484
+ id: randomUUID7(),
40485
+ kind: "permission",
40486
+ tool: toolName,
40487
+ input: input2,
40488
+ toolUseId: typeof args.tool_use_id === "string" ? args.tool_use_id : void 0,
40489
+ suggestions
40490
+ },
40491
+ { requestTimeoutMs: timeoutMs }
40492
+ );
40493
+ const result = res.behavior === "allow" ? {
40494
+ behavior: "allow",
40495
+ updatedInput: input2,
40496
+ ...res.always === true && suggestions && suggestions.length > 0 ? { updatedPermissions: suggestions } : {}
40497
+ } : {
40498
+ behavior: "deny",
40499
+ message: res.message ?? "[zelari] permission denied (no reason given)"
40500
+ };
40501
+ return textResult(JSON.stringify(result));
40502
+ } catch (err) {
40503
+ return textResult(
40504
+ JSON.stringify({
40505
+ behavior: "deny",
40506
+ message: `[zelari] permission broker unavailable: ${err instanceof Error ? err.message : String(err)}`
40507
+ })
40508
+ );
40509
+ }
40510
+ }
40511
+ async function callAskUser(args, timeoutMs) {
40512
+ const question = typeof args.question === "string" ? args.question : "";
40513
+ if (!question) {
40514
+ return { error: { code: -32602, message: "ask_user requires a string question" } };
40515
+ }
40516
+ const choices = Array.isArray(args.choices) ? args.choices.filter((c) => typeof c === "string") : void 0;
40517
+ const context = typeof args.context === "string" ? args.context : void 0;
40518
+ try {
40519
+ const res = await requestBrokerAsk(
40520
+ socketPath,
40521
+ {
40522
+ t: "ask",
40523
+ id: randomUUID7(),
40524
+ kind: "question",
40525
+ question,
40526
+ choices,
40527
+ context
40528
+ },
40529
+ { requestTimeoutMs: timeoutMs }
40530
+ );
40531
+ const text = res.behavior === "allow" && typeof res.answer === "string" ? res.answer : res.message ?? "No answer was given \u2014 use your best judgment.";
40532
+ return textResult(text);
40533
+ } catch (err) {
40534
+ return textResult(
40535
+ JSON.stringify({
40536
+ behavior: "deny",
40537
+ message: `[zelari] permission broker unavailable: ${err instanceof Error ? err.message : String(err)}`
40538
+ })
40539
+ );
40540
+ }
40541
+ }
40542
+ function textResult(text) {
40543
+ return {
40544
+ content: [{ type: "text", text }]
40545
+ };
40546
+ }
40547
+ return Promise.resolve({
40548
+ closed,
40549
+ stop: async () => {
40550
+ if (stopped) return;
40551
+ stopped = true;
40552
+ rl.close();
40553
+ closedResolve();
40554
+ }
40555
+ });
40556
+ }
40557
+ var PERMISSION_MCP_PROTOCOL_VERSION, APPROVE_TOOL, ASK_USER_TOOL, PERMISSION_MCP_TOOLS;
40558
+ var init_mcpPermissionServer = __esm({
40559
+ "src/cli/mcp/mcpPermissionServer.ts"() {
40560
+ "use strict";
40561
+ init_updater();
40562
+ init_permissionBroker();
40563
+ PERMISSION_MCP_PROTOCOL_VERSION = "2025-03-26";
40564
+ APPROVE_TOOL = {
40565
+ name: "approve",
40566
+ description: "Ask the human to approve or deny a tool call from an external agent. Returns JSON {behavior: 'allow'|'deny', updatedInput?, updatedPermissions?}.",
40567
+ inputSchema: {
40568
+ type: "object",
40569
+ properties: {
40570
+ tool_name: { type: "string", description: "Name of the tool the agent wants to run" },
40571
+ input: { type: "object", description: "Arguments the agent wants to pass" },
40572
+ tool_use_id: { type: "string", description: "Agent-side tool use id (echoed back)" },
40573
+ permission_suggestions: {
40574
+ type: "array",
40575
+ items: { type: "object" },
40576
+ description: "Permission rules the CLI suggests; returned as updatedPermissions only when the user allows permanently"
40577
+ },
40578
+ suggestions: {
40579
+ type: "array",
40580
+ items: { type: "object" },
40581
+ description: "Alias of permission_suggestions (accepted for compat)"
40582
+ }
40583
+ },
40584
+ required: ["tool_name", "input"]
40585
+ }
40586
+ };
40587
+ ASK_USER_TOOL = {
40588
+ name: "ask_user",
40589
+ description: "Ask the human a structured question. Returns JSON {answer: '<text>'} or {behavior: 'deny', message} if cancelled/unavailable.",
40590
+ inputSchema: {
40591
+ type: "object",
40592
+ properties: {
40593
+ question: { type: "string", description: "One focused question" },
40594
+ choices: {
40595
+ type: "array",
40596
+ items: { type: "string" },
40597
+ description: "2\u20136 concrete options; the human may still type a free answer"
40598
+ },
40599
+ context: { type: "string", description: "Why this choice matters (one short line)" }
40600
+ },
40601
+ required: ["question"]
40602
+ }
40603
+ };
40604
+ PERMISSION_MCP_TOOLS = [
40605
+ APPROVE_TOOL,
40606
+ ASK_USER_TOOL
40607
+ ];
40608
+ }
40609
+ });
40610
+
40611
+ // src/cli/mcp/permissionCli.ts
40612
+ var permissionCli_exports = {};
40613
+ __export(permissionCli_exports, {
40614
+ runPermissionCli: () => runPermissionCli
40615
+ });
40616
+ async function runPermissionCli(socketPath) {
40617
+ const server = await startPermissionMcpServer({ socketPath });
40618
+ process.stderr.write(
40619
+ `[zelari-code --permission-mcp] listening on ${socketPath}
40620
+ `
40621
+ );
40622
+ let exiting = false;
40623
+ const shutdown2 = async (code) => {
40624
+ if (exiting) return;
40625
+ exiting = true;
40626
+ try {
40627
+ await server.stop();
40628
+ } catch {
40629
+ }
40630
+ process.exit(code);
40631
+ };
40632
+ process.stdin.on("close", () => {
40633
+ void shutdown2(0);
40634
+ });
40635
+ process.on("SIGINT", () => {
40636
+ void shutdown2(0);
40637
+ });
40638
+ process.on("SIGTERM", () => {
40639
+ void shutdown2(0);
40640
+ });
40641
+ process.stdin.resume?.();
40642
+ await server.closed;
40643
+ await shutdown2(0);
40644
+ }
40645
+ var init_permissionCli = __esm({
40646
+ "src/cli/mcp/permissionCli.ts"() {
40647
+ "use strict";
40648
+ init_mcpPermissionServer();
40649
+ }
40650
+ });
40651
+
39347
40652
  // src/cli/companion/config.ts
39348
40653
  import {
39349
40654
  existsSync as existsSync43,
@@ -39370,12 +40675,12 @@ function ensureHome() {
39370
40675
  }
39371
40676
  }
39372
40677
  function loadCompanionConfig() {
39373
- const path47 = getCompanionConfigPath();
39374
- if (!existsSync43(path47)) {
40678
+ const path48 = getCompanionConfigPath();
40679
+ if (!existsSync43(path48)) {
39375
40680
  return { projects: [] };
39376
40681
  }
39377
40682
  try {
39378
- const raw = JSON.parse(readFileSync34(path47, "utf8"));
40683
+ const raw = JSON.parse(readFileSync34(path48, "utf8"));
39379
40684
  const projects = Array.isArray(raw.projects) ? raw.projects.filter(
39380
40685
  (p3) => p3 && typeof p3.path === "string" && p3.path.trim() && typeof (p3.id ?? p3.name) === "string"
39381
40686
  ).map((p3) => ({
@@ -39413,16 +40718,16 @@ function loadOrCreateToken(explicit) {
39413
40718
  return { token: explicit.trim(), created: false };
39414
40719
  }
39415
40720
  ensureHome();
39416
- const path47 = getCompanionTokenPath();
39417
- if (existsSync43(path47)) {
39418
- const t = readFileSync34(path47, "utf8").trim();
40721
+ const path48 = getCompanionTokenPath();
40722
+ if (existsSync43(path48)) {
40723
+ const t = readFileSync34(path48, "utf8").trim();
39419
40724
  if (t) return { token: t, created: false };
39420
40725
  }
39421
40726
  const token = randomBytes4(24).toString("base64url");
39422
- writeFileSync21(path47, token + "\n", "utf8");
40727
+ writeFileSync21(path48, token + "\n", "utf8");
39423
40728
  try {
39424
- const fs29 = __require("node:fs");
39425
- fs29.chmodSync?.(path47, 384);
40729
+ const fs30 = __require("node:fs");
40730
+ fs30.chmodSync?.(path48, 384);
39426
40731
  } catch {
39427
40732
  }
39428
40733
  return { token, created: true };
@@ -39447,17 +40752,17 @@ function mergeProjects(cfg, extraPaths) {
39447
40752
  byId.set(p3.id, p3);
39448
40753
  }
39449
40754
  for (const raw of extraPaths) {
39450
- const path47 = raw.trim();
39451
- if (!path47) continue;
39452
- let id = slugFromPath(path47);
40755
+ const path48 = raw.trim();
40756
+ if (!path48) continue;
40757
+ let id = slugFromPath(path48);
39453
40758
  let n = 2;
39454
- while (byId.has(id) && byId.get(id).path !== path47) {
39455
- id = `${slugFromPath(path47)}-${n++}`;
40759
+ while (byId.has(id) && byId.get(id).path !== path48) {
40760
+ id = `${slugFromPath(path48)}-${n++}`;
39456
40761
  }
39457
40762
  byId.set(id, {
39458
40763
  id,
39459
- name: slugFromPath(path47),
39460
- path: path47
40764
+ name: slugFromPath(path48),
40765
+ path: path48
39461
40766
  });
39462
40767
  }
39463
40768
  return [...byId.values()];
@@ -39505,9 +40810,9 @@ var init_config = __esm({
39505
40810
  });
39506
40811
 
39507
40812
  // src/cli/companion/runManager.ts
39508
- import { spawn as spawn11 } from "node:child_process";
39509
- import { createInterface } from "node:readline";
39510
- import { randomUUID as randomUUID7 } from "node:crypto";
40813
+ import { spawn as spawn12 } from "node:child_process";
40814
+ import { createInterface as createInterface2 } from "node:readline";
40815
+ import { randomUUID as randomUUID8 } from "node:crypto";
39511
40816
  import { writeFileSync as writeFileSync22, unlinkSync as unlinkSync2 } from "node:fs";
39512
40817
  import { join as join36 } from "node:path";
39513
40818
  import { tmpdir as tmpdir3 } from "node:os";
@@ -39576,7 +40881,7 @@ var init_runManager = __esm({
39576
40881
  }
39577
40882
  const prompt = args.prompt?.trim();
39578
40883
  if (!prompt) return { ok: false, error: "prompt is required" };
39579
- const id = randomUUID7();
40884
+ const id = randomUUID8();
39580
40885
  const mode = (args.mode || "kraken").toLowerCase();
39581
40886
  const phase2 = (args.phase || "build").toLowerCase();
39582
40887
  const run = {
@@ -39621,7 +40926,7 @@ var init_runManager = __esm({
39621
40926
  historyFile = void 0;
39622
40927
  }
39623
40928
  }
39624
- const child = spawn11(process.execPath, argv, {
40929
+ const child = spawn12(process.execPath, argv, {
39625
40930
  cwd: args.cwd,
39626
40931
  env: {
39627
40932
  ...process.env,
@@ -39638,7 +40943,7 @@ var init_runManager = __esm({
39638
40943
  message: `[companion] run ${id} started mode=${mode} phase=${phase2} cwd=${args.cwd}`
39639
40944
  });
39640
40945
  if (child.stdout) {
39641
- const rl = createInterface({ input: child.stdout });
40946
+ const rl = createInterface2({ input: child.stdout });
39642
40947
  rl.on("line", (line) => {
39643
40948
  const t = line.trim();
39644
40949
  if (!t) return;
@@ -39739,7 +41044,7 @@ __export(serve_exports, {
39739
41044
  parseServeFlags: () => parseServeFlags,
39740
41045
  runCompanionServe: () => runCompanionServe
39741
41046
  });
39742
- import { createServer } from "node:http";
41047
+ import { createServer as createServer2 } from "node:http";
39743
41048
  import { existsSync as existsSync44 } from "node:fs";
39744
41049
  import { resolve as resolve2 } from "node:path";
39745
41050
  function readBody(req, max = 2e6) {
@@ -39759,7 +41064,7 @@ function readBody(req, max = 2e6) {
39759
41064
  req.on("error", reject);
39760
41065
  });
39761
41066
  }
39762
- function sendJson(res, status, body, extraHeaders) {
41067
+ function sendJson2(res, status, body, extraHeaders) {
39763
41068
  const data = JSON.stringify(body);
39764
41069
  res.writeHead(status, {
39765
41070
  "content-type": "application/json; charset=utf-8",
@@ -39823,7 +41128,7 @@ async function runCompanionServe(opts = {}) {
39823
41128
  );
39824
41129
  }
39825
41130
  const runs = new RunManager();
39826
- const server = createServer(async (req, res) => {
41131
+ const server = createServer2(async (req, res) => {
39827
41132
  if (req.method === "OPTIONS") {
39828
41133
  res.writeHead(204, {
39829
41134
  "access-control-allow-origin": "*",
@@ -39834,10 +41139,10 @@ async function runCompanionServe(opts = {}) {
39834
41139
  return;
39835
41140
  }
39836
41141
  const url2 = parseUrl(req);
39837
- const path47 = url2.pathname.replace(/\/+$/, "") || "/";
41142
+ const path48 = url2.pathname.replace(/\/+$/, "") || "/";
39838
41143
  try {
39839
- if (req.method === "GET" && (path47 === "/health" || path47 === "/v1/health")) {
39840
- sendJson(res, 200, {
41144
+ if (req.method === "GET" && (path48 === "/health" || path48 === "/v1/health")) {
41145
+ sendJson2(res, 200, {
39841
41146
  ok: true,
39842
41147
  service: "zelari-companion",
39843
41148
  version: getCurrentVersion(),
@@ -39848,19 +41153,19 @@ async function runCompanionServe(opts = {}) {
39848
41153
  });
39849
41154
  return;
39850
41155
  }
39851
- if (path47.startsWith("/v1")) {
41156
+ if (path48.startsWith("/v1")) {
39852
41157
  if (!tokenMatches(token, getBearer(req))) {
39853
- sendJson(res, 401, { ok: false, error: "unauthorized" });
41158
+ sendJson2(res, 401, { ok: false, error: "unauthorized" });
39854
41159
  return;
39855
41160
  }
39856
41161
  }
39857
- if (req.method === "GET" && path47 === "/v1/config") {
41162
+ if (req.method === "GET" && path48 === "/v1/config") {
39858
41163
  const snap = buildDesktopConfigSnapshot();
39859
- sendJson(res, 200, { ok: true, ...snap });
41164
+ sendJson2(res, 200, { ok: true, ...snap });
39860
41165
  return;
39861
41166
  }
39862
- if (req.method === "GET" && path47 === "/v1/projects") {
39863
- sendJson(res, 200, {
41167
+ if (req.method === "GET" && path48 === "/v1/projects") {
41168
+ sendJson2(res, 200, {
39864
41169
  ok: true,
39865
41170
  projects: projects.map((p3) => ({
39866
41171
  id: p3.id,
@@ -39870,8 +41175,8 @@ async function runCompanionServe(opts = {}) {
39870
41175
  });
39871
41176
  return;
39872
41177
  }
39873
- if (req.method === "GET" && path47 === "/v1/runs") {
39874
- sendJson(res, 200, {
41178
+ if (req.method === "GET" && path48 === "/v1/runs") {
41179
+ sendJson2(res, 200, {
39875
41180
  ok: true,
39876
41181
  active: runs.getActive(),
39877
41182
  recent: runs.listRecent().map((r) => ({
@@ -39888,13 +41193,13 @@ async function runCompanionServe(opts = {}) {
39888
41193
  });
39889
41194
  return;
39890
41195
  }
39891
- if (req.method === "POST" && path47 === "/v1/runs") {
41196
+ if (req.method === "POST" && path48 === "/v1/runs") {
39892
41197
  const raw = await readBody(req);
39893
41198
  let body = {};
39894
41199
  try {
39895
41200
  body = raw ? JSON.parse(raw) : {};
39896
41201
  } catch {
39897
- sendJson(res, 400, { ok: false, error: "invalid JSON body" });
41202
+ sendJson2(res, 400, { ok: false, error: "invalid JSON body" });
39898
41203
  return;
39899
41204
  }
39900
41205
  const prompt = String(body.prompt ?? body.task ?? "").trim();
@@ -39903,7 +41208,7 @@ async function runCompanionServe(opts = {}) {
39903
41208
  const cwdArg = body.cwd != null ? String(body.cwd) : body.projectId != null ? String(body.projectId) : null;
39904
41209
  const resolved = resolveProjectPath(projects, cwdArg);
39905
41210
  if (!resolved.ok) {
39906
- sendJson(res, 400, { ok: false, error: resolved.error });
41211
+ sendJson2(res, 400, { ok: false, error: resolved.error });
39907
41212
  return;
39908
41213
  }
39909
41214
  const history2 = Array.isArray(body.history) ? body.history : void 0;
@@ -39917,10 +41222,10 @@ async function runCompanionServe(opts = {}) {
39917
41222
  history: history2
39918
41223
  });
39919
41224
  if (!result.ok) {
39920
- sendJson(res, 409, { ok: false, error: result.error });
41225
+ sendJson2(res, 409, { ok: false, error: result.error });
39921
41226
  return;
39922
41227
  }
39923
- sendJson(res, 201, {
41228
+ sendJson2(res, 201, {
39924
41229
  ok: true,
39925
41230
  run: {
39926
41231
  id: result.run.id,
@@ -39935,12 +41240,12 @@ async function runCompanionServe(opts = {}) {
39935
41240
  });
39936
41241
  return;
39937
41242
  }
39938
- const eventsMatch = /^\/v1\/runs\/([^/]+)\/events$/.exec(path47);
41243
+ const eventsMatch = /^\/v1\/runs\/([^/]+)\/events$/.exec(path48);
39939
41244
  if (req.method === "GET" && eventsMatch) {
39940
41245
  const runId = eventsMatch[1];
39941
41246
  const run = runs.getRun(runId);
39942
41247
  if (!run) {
39943
- sendJson(res, 404, { ok: false, error: "run not found" });
41248
+ sendJson2(res, 404, { ok: false, error: "run not found" });
39944
41249
  return;
39945
41250
  }
39946
41251
  res.writeHead(200, {
@@ -40000,20 +41305,20 @@ async function runCompanionServe(opts = {}) {
40000
41305
  }, 500);
40001
41306
  return;
40002
41307
  }
40003
- const cancelMatch = /^\/v1\/runs\/([^/]+)\/cancel$/.exec(path47);
41308
+ const cancelMatch = /^\/v1\/runs\/([^/]+)\/cancel$/.exec(path48);
40004
41309
  if (req.method === "POST" && cancelMatch) {
40005
41310
  const runId = cancelMatch[1];
40006
41311
  const result = runs.cancel(runId);
40007
41312
  if (!result.ok) {
40008
- sendJson(res, 404, { ok: false, error: result.error });
41313
+ sendJson2(res, 404, { ok: false, error: result.error });
40009
41314
  return;
40010
41315
  }
40011
- sendJson(res, 200, { ok: true, cancelled: runId });
41316
+ sendJson2(res, 200, { ok: true, cancelled: runId });
40012
41317
  return;
40013
41318
  }
40014
- sendJson(res, 404, { ok: false, error: "not found" });
41319
+ sendJson2(res, 404, { ok: false, error: "not found" });
40015
41320
  } catch (err) {
40016
- sendJson(res, 500, {
41321
+ sendJson2(res, 500, {
40017
41322
  ok: false,
40018
41323
  error: err instanceof Error ? err.message : String(err)
40019
41324
  });
@@ -40112,11 +41417,11 @@ import { execSync as execSync2 } from "node:child_process";
40112
41417
  import { existsSync as existsSync45, readFileSync as readFileSync35, readlinkSync, statSync as statSync7 } from "node:fs";
40113
41418
  import { createRequire as createRequire3 } from "node:module";
40114
41419
  import { fileURLToPath as fileURLToPath2 } from "node:url";
40115
- import path46 from "node:path";
41420
+ import path47 from "node:path";
40116
41421
  function findPackageRoot(start) {
40117
41422
  let dir = start;
40118
41423
  for (let i = 0; i < 6; i += 1) {
40119
- const candidate = path46.join(dir, "package.json");
41424
+ const candidate = path47.join(dir, "package.json");
40120
41425
  if (existsSync45(candidate)) {
40121
41426
  try {
40122
41427
  const pkg = JSON.parse(readFileSync35(candidate, "utf8"));
@@ -40124,11 +41429,11 @@ function findPackageRoot(start) {
40124
41429
  } catch {
40125
41430
  }
40126
41431
  }
40127
- const parent = path46.dirname(dir);
41432
+ const parent = path47.dirname(dir);
40128
41433
  if (parent === dir) break;
40129
41434
  dir = parent;
40130
41435
  }
40131
- return path46.resolve(__dirname3, "..", "..", "..");
41436
+ return path47.resolve(__dirname3, "..", "..", "..");
40132
41437
  }
40133
41438
  function tryExec(cmd) {
40134
41439
  try {
@@ -40142,7 +41447,7 @@ function tryExec(cmd) {
40142
41447
  }
40143
41448
  function readPackageJson3() {
40144
41449
  try {
40145
- const pkgPath = path46.join(packageRoot, "package.json");
41450
+ const pkgPath = path47.join(packageRoot, "package.json");
40146
41451
  return JSON.parse(readFileSync35(pkgPath, "utf8"));
40147
41452
  } catch {
40148
41453
  return null;
@@ -40158,7 +41463,7 @@ function checkShim(pkgName) {
40158
41463
  }
40159
41464
  const isWin = process.platform === "win32";
40160
41465
  const shimName = isWin ? "zelari-code.cmd" : "zelari-code";
40161
- const shimPath = path46.join(prefix, shimName);
41466
+ const shimPath = path47.join(prefix, shimName);
40162
41467
  if (!existsSync45(shimPath)) {
40163
41468
  return FAIL(
40164
41469
  `shim not found at ${shimPath}
@@ -40186,8 +41491,8 @@ function checkShim(pkgName) {
40186
41491
  fix: npm install -g ${pkgName}@latest --force`
40187
41492
  );
40188
41493
  }
40189
- const resolved = path46.resolve(path46.dirname(shimPath), target);
40190
- const expected = path46.join(
41494
+ const resolved = path47.resolve(path47.dirname(shimPath), target);
41495
+ const expected = path47.join(
40191
41496
  prefix,
40192
41497
  "node_modules",
40193
41498
  pkgName,
@@ -40226,7 +41531,7 @@ function checkNode(pkg) {
40226
41531
  return OK(`node ${raw}`);
40227
41532
  }
40228
41533
  function checkBundle() {
40229
- const bundle = path46.join(packageRoot, "dist", "cli", "main.bundled.js");
41534
+ const bundle = path47.join(packageRoot, "dist", "cli", "main.bundled.js");
40230
41535
  if (!existsSync45(bundle)) {
40231
41536
  return FAIL(
40232
41537
  `dist/cli/main.bundled.js missing at ${bundle}
@@ -40247,7 +41552,7 @@ function checkRuntimeDeps() {
40247
41552
  const missing = [];
40248
41553
  for (const dep of required2) {
40249
41554
  try {
40250
- const localReq = createRequire3(path46.join(packageRoot, "package.json"));
41555
+ const localReq = createRequire3(path47.join(packageRoot, "package.json"));
40251
41556
  localReq.resolve(dep);
40252
41557
  } catch {
40253
41558
  missing.push(dep);
@@ -40423,7 +41728,7 @@ var init_doctor = __esm({
40423
41728
  "use strict";
40424
41729
  init_prereqChecks();
40425
41730
  require3 = createRequire3(import.meta.url);
40426
- __dirname3 = path46.dirname(fileURLToPath2(import.meta.url));
41731
+ __dirname3 = path47.dirname(fileURLToPath2(import.meta.url));
40427
41732
  packageRoot = findPackageRoot(__dirname3);
40428
41733
  OK = (message) => ({
40429
41734
  ok: true,
@@ -40606,7 +41911,7 @@ import React15 from "react";
40606
41911
  import { render } from "ink";
40607
41912
 
40608
41913
  // src/cli/app.tsx
40609
- import React10, { useState as useState8, useMemo, useCallback as useCallback5, useEffect as useEffect7 } from "react";
41914
+ import React10, { useState as useState8, useMemo, useCallback as useCallback5, useEffect as useEffect8 } from "react";
40610
41915
  import { Box as Box9, Static, useInput as useInput2, useStdin as useStdin2 } from "ink";
40611
41916
 
40612
41917
  // src/cli/components/InputBar.tsx
@@ -44443,15 +45748,22 @@ function useChatTurn(params) {
44443
45748
  historySeedLen = getHistory().length;
44444
45749
  const anchored = maybeAnchorShortAnswer(userText);
44445
45750
  const effectiveUserText = anchored ?? userText;
44446
- envConfig = await providerFromEnv();
44447
- if (!envConfig) {
44448
- const active = resolveActiveProvider();
44449
- const spec = PROVIDERS.find((p3) => p3.id === active);
44450
- appendSystem(
44451
- setMessages,
44452
- `No API key for the active provider "${active}". Set ${spec?.envVar ?? "the provider API key env var"} or run /login ${active}.`
44453
- );
44454
- return;
45751
+ const localCli = (process.env.ZELARI_LOCAL_CLI ?? "").trim();
45752
+ let localCliProvider = null;
45753
+ if (localCli) {
45754
+ const { createLocalCliProvider: createLocalCliProvider2 } = await Promise.resolve().then(() => (init_claudeProvider(), claudeProvider_exports));
45755
+ localCliProvider = createLocalCliProvider2({ cli: localCli });
45756
+ } else {
45757
+ envConfig = await providerFromEnv();
45758
+ if (!envConfig) {
45759
+ const active = resolveActiveProvider();
45760
+ const spec = PROVIDERS.find((p3) => p3.id === active);
45761
+ appendSystem(
45762
+ setMessages,
45763
+ `No API key for the active provider "${active}". Set ${spec?.envVar ?? "the provider API key env var"} or run /login ${active}.`
45764
+ );
45765
+ return;
45766
+ }
44455
45767
  }
44456
45768
  setBusy(true);
44457
45769
  const workPhase = getPhase();
@@ -44499,29 +45811,34 @@ _(${req.context})_` : "") + "\n\u2192 scegli dalla lista (il turno continua dopo
44499
45811
  onPermissionAsk,
44500
45812
  permissionPolicy: defaultPermissionPolicy()
44501
45813
  });
44502
- const baseProviderStream = openaiCompatibleProvider(envConfig);
44503
- const failoverResolution = await resolveFailoverStream({
44504
- failoverEnabled: process.env.ANATHEMA_FAILOVER !== "0",
44505
- envValue: process.env.ANATHEMA_FAILOVER_PROVIDER,
44506
- primaryProviderId: envConfig.providerId,
44507
- primary: baseProviderStream,
44508
- validProviderIds: PROVIDERS.map((p3) => p3.id),
44509
- lookupFallbackConfig: async (id) => providerConfigFor(id),
44510
- buildStream: (config2) => openaiCompatibleProvider(
44511
- config2
44512
- )
44513
- });
44514
- if (failoverResolution.warning) {
44515
- appendSystem(setMessages, `[failover] ${failoverResolution.warning}`);
44516
- }
44517
- const providerStream = failoverResolution.fallbackLabel ? providerFailover({
44518
- primary: baseProviderStream,
44519
- fallback: failoverResolution.fallback,
44520
- fallbackLabel: failoverResolution.fallbackLabel
44521
- }) : providerFailover({
44522
- primary: baseProviderStream,
44523
- fallback: failoverResolution.fallback
44524
- });
45814
+ const baseProviderStream = localCliProvider ?? openaiCompatibleProvider(envConfig);
45815
+ let providerStream;
45816
+ if (localCliProvider) {
45817
+ providerStream = localCliProvider;
45818
+ } else {
45819
+ const failoverResolution = await resolveFailoverStream({
45820
+ failoverEnabled: process.env.ANATHEMA_FAILOVER !== "0",
45821
+ envValue: process.env.ANATHEMA_FAILOVER_PROVIDER,
45822
+ primaryProviderId: envConfig.providerId,
45823
+ primary: baseProviderStream,
45824
+ validProviderIds: PROVIDERS.map((p3) => p3.id),
45825
+ lookupFallbackConfig: async (id) => providerConfigFor(id),
45826
+ buildStream: (config2) => openaiCompatibleProvider(
45827
+ config2
45828
+ )
45829
+ });
45830
+ if (failoverResolution.warning) {
45831
+ appendSystem(setMessages, `[failover] ${failoverResolution.warning}`);
45832
+ }
45833
+ providerStream = failoverResolution.fallbackLabel ? providerFailover({
45834
+ primary: baseProviderStream,
45835
+ fallback: failoverResolution.fallback,
45836
+ fallbackLabel: failoverResolution.fallbackLabel
45837
+ }) : providerFailover({
45838
+ primary: baseProviderStream,
45839
+ fallback: failoverResolution.fallback
45840
+ });
45841
+ }
44525
45842
  const cwd = process.cwd();
44526
45843
  let composedWorkspace = "";
44527
45844
  let composedInstructions = "";
@@ -45795,6 +47112,83 @@ async function runZelariMissionInTui(userMessage, deps, emit) {
45795
47112
  }
45796
47113
  }
45797
47114
 
47115
+ // src/cli/hooks/usePermissionBroker.ts
47116
+ init_permissionBroker();
47117
+ init_brokerHandlers();
47118
+ import { useEffect as useEffect5, useRef as useRef5 } from "react";
47119
+ function usePermissionBroker(opts) {
47120
+ const { setPicker: setPicker2, setMessages } = opts;
47121
+ const handleRef = useRef5(null);
47122
+ useEffect5(() => {
47123
+ const socketPath = process.env.ZELARI_PERM_SOCKET?.trim();
47124
+ if (!socketPath) return;
47125
+ let disposed = false;
47126
+ let handle = null;
47127
+ const onPermissionAsk = createPermissionAskHandler({
47128
+ setPicker: setPicker2,
47129
+ appendSystem: (msg, at) => appendSystem(setMessages, msg, at ?? Date.now())
47130
+ });
47131
+ const onQuestion = (req) => new Promise((resolve3) => {
47132
+ const choices = (req.choices ?? []).map((c) => String(c).trim()).filter((c) => c.length > 0);
47133
+ if (choices.length < 2) {
47134
+ resolve3(null);
47135
+ return;
47136
+ }
47137
+ let settled = false;
47138
+ const finish = (value) => {
47139
+ if (settled) return;
47140
+ settled = true;
47141
+ setPicker2(null);
47142
+ resolve3(value);
47143
+ };
47144
+ appendSystem(
47145
+ setMessages,
47146
+ `[external agent] ${req.question}
47147
+ \u2192 ${choices.join(" \xB7 ")}`,
47148
+ Date.now()
47149
+ );
47150
+ setPicker2({
47151
+ kind: "clarification",
47152
+ title: req.question,
47153
+ items: choices.map((c) => ({ value: c, label: c })),
47154
+ onAnswer: (value) => finish(value),
47155
+ onCancel: () => finish(null)
47156
+ });
47157
+ });
47158
+ void startPermissionBroker(socketPath, {
47159
+ onPermission: createBrokerPermissionHandler({ onPermissionAsk }),
47160
+ onQuestion: createBrokerQuestionHandler({ onQuestion })
47161
+ }).then((h) => {
47162
+ if (disposed) {
47163
+ void h.stop();
47164
+ return;
47165
+ }
47166
+ handle = h;
47167
+ handleRef.current = h;
47168
+ appendSystem(
47169
+ setMessages,
47170
+ `[permission-broker] listening on ${socketPath} \u2014 external agents can request approvals.`,
47171
+ Date.now()
47172
+ );
47173
+ process.stderr.write(
47174
+ `[zelari-code] permission broker listening on ${socketPath}
47175
+ `
47176
+ );
47177
+ }).catch((err) => {
47178
+ process.stderr.write(
47179
+ `[zelari-code] permission broker failed to start: ${err instanceof Error ? err.message : String(err)}
47180
+ `
47181
+ );
47182
+ });
47183
+ return () => {
47184
+ disposed = true;
47185
+ if (handle) void handle.stop();
47186
+ handleRef.current = null;
47187
+ };
47188
+ }, [setPicker2, setMessages]);
47189
+ return { socketPath: process.env.ZELARI_PERM_SOCKET?.trim() || void 0 };
47190
+ }
47191
+
45798
47192
  // src/cli/hooks/useSlashDispatch.ts
45799
47193
  import { useCallback as useCallback3 } from "react";
45800
47194
 
@@ -45901,6 +47295,7 @@ function handleSlashCommand(text, availableSkills) {
45901
47295
  /update [--yes|-y] \u2014 check for zelari-code updates; --yes performs the update (v3-N)
45902
47296
  /plugins \u2014 list optional tool plugins (Playwright, eslint, ruff, LSP servers)
45903
47297
  /plugins install <id> \u2014 install a plugin now (e.g. /plugins install eslint)
47298
+ /integrations \u2014 list MCP integration presets (cua, composio, qwen-mm-plugins) + status
45904
47299
  /steer <text> \u2014 enqueue a follow-up prompt on the active run (Task 18.2)
45905
47300
  /steer --interrupt <text> \u2014 cancel current run + enqueue <text> for next dispatch (Task C.3.2)
45906
47301
  /compact \u2014 compact the session transcript
@@ -46269,6 +47664,27 @@ ${formatSkillList(availableSkills)}`
46269
47664
  message: "Usage: /plugins \u2014 list optional tool plugins (Playwright, eslint, ruff, LSP servers)\n /plugins install <id> \u2014 install a plugin now (e.g. /plugins install eslint)"
46270
47665
  };
46271
47666
  }
47667
+ case "integrations": {
47668
+ const lines = [
47669
+ "/integrations \u2014 MCP integration presets (apply: zelari-code --preset <id> [--scope user|project])",
47670
+ "",
47671
+ " cua \u2014 Cua Driver desktop computer-use (binary on PATH, https://cua.ai/docs)",
47672
+ ` status: ${process.env.ZELARI_CUA === "0" ? "disabled (ZELARI_CUA=0)" : "enabled (kill switch: ZELARI_CUA=0)"}`,
47673
+ " composio \u2014 Composio Connect: 500+ app integrations as MCP tools",
47674
+ ` status: ${process.env.COMPOSIO_API_KEY ? "ready (COMPOSIO_API_KEY set)" : "missing COMPOSIO_API_KEY \u2014 export it first"}`,
47675
+ " qwen-mm-plugins \u2014 Qwen-MM-Plugins multimodal MCP tools (uvx; WSL2 on Windows)",
47676
+ ` status: ${process.env.QWEN_MM_PLUGIN ? "ready (QWEN_MM_PLUGIN=" + process.env.QWEN_MM_PLUGIN + ")" : "ready (default: qwen-mm-plugins-core)"}`,
47677
+ "",
47678
+ "Apply: zelari-code --preset composio (writes ~/.zelari-code/mcp.json)",
47679
+ " zelari-code --preset cua --scope project (writes .zelari/mcp.json)",
47680
+ " zelari-code --preset qwen-mm-plugins (writes ~/.zelari-code/mcp.json)"
47681
+ ];
47682
+ return {
47683
+ handled: true,
47684
+ kind: "integrations_list",
47685
+ message: lines.join("\n")
47686
+ };
47687
+ }
46272
47688
  case "undo": {
46273
47689
  const confirmed = args.includes("--yes") || args.includes("-y");
46274
47690
  if (confirmed) {
@@ -46919,13 +48335,13 @@ ${digest}
46919
48335
  // src/cli/slashHandlers/krakenFanout.ts
46920
48336
  init_auditLogger();
46921
48337
  init_toolRegistry();
46922
- import { promises as fs21 } from "node:fs";
48338
+ import { promises as fs22 } from "node:fs";
46923
48339
 
46924
48340
  // src/cli/tools/krakenCsvFanout.ts
46925
48341
  init_zod();
46926
48342
  init_taskTool();
46927
- import { promises as fs20 } from "node:fs";
46928
- import path36 from "node:path";
48343
+ import { promises as fs21 } from "node:fs";
48344
+ import path37 from "node:path";
46929
48345
  import { randomBytes as randomBytes3 } from "node:crypto";
46930
48346
  var CsvFanoutArgsSchema = external_exports.object({
46931
48347
  csv_path: external_exports.string().min(1),
@@ -46946,7 +48362,7 @@ var CsvFanoutArgsSchema = external_exports.object({
46946
48362
  max_runtime_seconds: external_exports.number().int().positive().optional()
46947
48363
  });
46948
48364
  async function readCsv(filePath) {
46949
- const text = await fs20.readFile(filePath, "utf8");
48365
+ const text = await fs21.readFile(filePath, "utf8");
46950
48366
  return parseCsv(text);
46951
48367
  }
46952
48368
  function parseCsv(text) {
@@ -47019,8 +48435,8 @@ function resolveMaxConcurrency(env = process.env) {
47019
48435
  }
47020
48436
  async function runCsvFanout(args, deps, opts) {
47021
48437
  const start = Date.now();
47022
- const absCsv = path36.isAbsolute(args.csv_path) ? args.csv_path : path36.join(opts.parentCwd, args.csv_path);
47023
- const absOut = path36.isAbsolute(args.output_csv_path) ? args.output_csv_path : path36.join(opts.parentCwd, args.output_csv_path);
48438
+ const absCsv = path37.isAbsolute(args.csv_path) ? args.csv_path : path37.join(opts.parentCwd, args.csv_path);
48439
+ const absOut = path37.isAbsolute(args.output_csv_path) ? args.output_csv_path : path37.join(opts.parentCwd, args.output_csv_path);
47024
48440
  const { headers, rows } = await readCsv(absCsv);
47025
48441
  if (headers.length === 0) {
47026
48442
  throw new Error(`kraken_csv_fanout: ${absCsv} is empty`);
@@ -47076,7 +48492,7 @@ async function runCsvFanout(args, deps, opts) {
47076
48492
  errored += 1;
47077
48493
  errors.push(`${row[args.id_column] ?? i}: ${res.error}`);
47078
48494
  }
47079
- await fs20.mkdir(path36.dirname(absOut), { recursive: true });
48495
+ await fs21.mkdir(path37.dirname(absOut), { recursive: true });
47080
48496
  await queueWrite(serializeCsv(outHeaders, outputRecords));
47081
48497
  }
47082
48498
  }
@@ -47098,8 +48514,8 @@ async function runCsvFanout(args, deps, opts) {
47098
48514
  }
47099
48515
  async function atomicWrite(file2, contents) {
47100
48516
  const tmp = `${file2}.${process.pid}.${Date.now()}.${randomBytes3(6).toString("hex")}.tmp`;
47101
- await fs20.writeFile(tmp, contents, "utf8");
47102
- await fs20.rename(tmp, file2);
48517
+ await fs21.writeFile(tmp, contents, "utf8");
48518
+ await fs21.rename(tmp, file2);
47103
48519
  }
47104
48520
 
47105
48521
  // src/cli/slashHandlers/krakenFanout.ts
@@ -47197,7 +48613,7 @@ async function handleKrakenFanout(ctx, raw) {
47197
48613
  }
47198
48614
  const absCsv = isAbsolute(parsed.args.csv_path) ? parsed.args.csv_path : joinPath(ctx.cwd, parsed.args.csv_path);
47199
48615
  try {
47200
- await fs21.access(absCsv);
48616
+ await fs22.access(absCsv);
47201
48617
  } catch {
47202
48618
  appendSystem(ctx.setMessages, `[kraken fanout] source CSV not found: ${absCsv}`);
47203
48619
  return;
@@ -47269,8 +48685,8 @@ function splitArgs(s) {
47269
48685
  }
47270
48686
 
47271
48687
  // src/cli/slashHandlers/krakenWorkbench.ts
47272
- import { promises as fs22 } from "node:fs";
47273
- import path37 from "node:path";
48688
+ import { promises as fs23 } from "node:fs";
48689
+ import path38 from "node:path";
47274
48690
 
47275
48691
  // src/cli/kraken/workbenchView.ts
47276
48692
  var EMPTY = {
@@ -47387,15 +48803,15 @@ function formatWorkbenchForTerminal(p3) {
47387
48803
 
47388
48804
  // src/cli/slashHandlers/krakenWorkbench.ts
47389
48805
  async function handleKrakenWorkbench(ctx) {
47390
- const dir = path37.join(ctx.cwd, ".zelari", "radio");
48806
+ const dir = path38.join(ctx.cwd, ".zelari", "radio");
47391
48807
  let latest = null;
47392
48808
  let latestMtime = 0;
47393
48809
  try {
47394
- const files = await fs22.readdir(dir);
48810
+ const files = await fs23.readdir(dir);
47395
48811
  for (const f of files) {
47396
48812
  if (!f.startsWith("workbench-") || !f.endsWith(".md")) continue;
47397
- const full = path37.join(dir, f);
47398
- const stat = await fs22.stat(full);
48813
+ const full = path38.join(dir, f);
48814
+ const stat = await fs23.stat(full);
47399
48815
  if (stat.mtimeMs > latestMtime) {
47400
48816
  latestMtime = stat.mtimeMs;
47401
48817
  latest = full;
@@ -47407,14 +48823,14 @@ async function handleKrakenWorkbench(ctx) {
47407
48823
  appendSystem(ctx.setMessages, "[kraken workbench] no workbench file found (.zelari/radio/workbench-*.md)");
47408
48824
  return;
47409
48825
  }
47410
- const content = await fs22.readFile(latest, "utf8");
48826
+ const content = await fs23.readFile(latest, "utf8");
47411
48827
  const parsed = parseWorkbench(content);
47412
48828
  const rendered = formatWorkbenchForTerminal(parsed);
47413
48829
  if (!rendered.trim()) {
47414
- appendSystem(ctx.setMessages, `[kraken workbench] ${path37.basename(latest)}: (no nodes / no events yet)`);
48830
+ appendSystem(ctx.setMessages, `[kraken workbench] ${path38.basename(latest)}: (no nodes / no events yet)`);
47415
48831
  return;
47416
48832
  }
47417
- appendSystem(ctx.setMessages, `[kraken workbench] ${path37.basename(latest)}:
48833
+ appendSystem(ctx.setMessages, `[kraken workbench] ${path38.basename(latest)}:
47418
48834
  ${rendered}`);
47419
48835
  }
47420
48836
 
@@ -47566,8 +48982,8 @@ init_registry3();
47566
48982
  // src/cli/plugins/installer.ts
47567
48983
  init_cmdline();
47568
48984
  init_updater();
47569
- import { spawn as spawn10 } from "node:child_process";
47570
- async function installPlugin(spec, cwd, executor = spawn10) {
48985
+ import { spawn as spawn11 } from "node:child_process";
48986
+ async function installPlugin(spec, cwd, executor = spawn11) {
47571
48987
  const scopeFlag = spec.installScope === "global" ? "-g" : "-D";
47572
48988
  const args = ["install", scopeFlag, spec.npmPackage];
47573
48989
  let result = await runNpm2(executor, args, cwd, "shim");
@@ -47715,17 +49131,17 @@ ${result.output.split("\n").slice(-8).join("\n")}` : "";
47715
49131
  }
47716
49132
 
47717
49133
  // src/cli/slashHandlers/promoteMember.ts
47718
- import { promises as fs23 } from "node:fs";
47719
- import path40 from "node:path";
49134
+ import { promises as fs24 } from "node:fs";
49135
+ import path41 from "node:path";
47720
49136
  import os10 from "node:os";
47721
49137
  async function handlePromoteMember(ctx, memberId) {
47722
49138
  try {
47723
49139
  const { promoteMember: promoteMember2 } = await Promise.resolve().then(() => (init_council(), council_exports));
47724
49140
  const { skill, markdown } = promoteMember2(memberId);
47725
- const skillDir = process.env.ANATHEMA_SKILL_DIR ?? path40.join(os10.homedir(), ".tmp", "zelari-code", "skills");
47726
- await fs23.mkdir(skillDir, { recursive: true });
47727
- const filePath = path40.join(skillDir, `${skill.id}.md`);
47728
- await fs23.writeFile(filePath, markdown, "utf8");
49141
+ const skillDir = process.env.ANATHEMA_SKILL_DIR ?? path41.join(os10.homedir(), ".tmp", "zelari-code", "skills");
49142
+ await fs24.mkdir(skillDir, { recursive: true });
49143
+ const filePath = path41.join(skillDir, `${skill.id}.md`);
49144
+ await fs24.writeFile(filePath, markdown, "utf8");
47729
49145
  appendSystem(
47730
49146
  ctx.setMessages,
47731
49147
  `[promote-member] ${skill.name} (${memberId}) \u2192 ${filePath}
@@ -47741,25 +49157,25 @@ async function handlePromoteMember(ctx, memberId) {
47741
49157
  }
47742
49158
 
47743
49159
  // src/cli/branchManager.ts
47744
- import { promises as fs24, existsSync as existsSync38, readFileSync as readFileSync30, writeFileSync as writeFileSync19, mkdirSync as mkdirSync16, statSync as statSync4, rmSync as rmSync3 } from "node:fs";
47745
- import path41 from "node:path";
49160
+ import { promises as fs25, existsSync as existsSync38, readFileSync as readFileSync30, writeFileSync as writeFileSync19, mkdirSync as mkdirSync16, statSync as statSync4, rmSync as rmSync3 } from "node:fs";
49161
+ import path42 from "node:path";
47746
49162
  import os11 from "node:os";
47747
49163
  var META_FILENAME = "meta.json";
47748
49164
  var SESSIONS_SUBDIR = "sessions";
47749
49165
  function getBranchesBaseDir() {
47750
- return process.env.ANATHEMA_BRANCHES_DIR ?? path41.join(os11.homedir(), ".tmp", "zelari-code", "branches");
49166
+ return process.env.ANATHEMA_BRANCHES_DIR ?? path42.join(os11.homedir(), ".tmp", "zelari-code", "branches");
47751
49167
  }
47752
49168
  function getSessionsBaseDir() {
47753
- return process.env.ANATHEMA_SESSIONS_DIR ?? path41.join(os11.homedir(), ".tmp", "zelari-code", "sessions");
49169
+ return process.env.ANATHEMA_SESSIONS_DIR ?? path42.join(os11.homedir(), ".tmp", "zelari-code", "sessions");
47754
49170
  }
47755
49171
  function branchPathFor(name, baseDir) {
47756
- return path41.join(baseDir, name);
49172
+ return path42.join(baseDir, name);
47757
49173
  }
47758
49174
  function metaPathFor(name, baseDir) {
47759
- return path41.join(baseDir, name, META_FILENAME);
49175
+ return path42.join(baseDir, name, META_FILENAME);
47760
49176
  }
47761
49177
  function sessionsPathFor(name, baseDir) {
47762
- return path41.join(baseDir, name, SESSIONS_SUBDIR);
49178
+ return path42.join(baseDir, name, SESSIONS_SUBDIR);
47763
49179
  }
47764
49180
  function readBranchMeta(name, baseDir) {
47765
49181
  const metaPath = metaPathFor(name, baseDir);
@@ -47784,13 +49200,13 @@ function readBranchMeta(name, baseDir) {
47784
49200
  }
47785
49201
  function writeBranchMeta(name, baseDir, meta3) {
47786
49202
  const metaPath = metaPathFor(name, baseDir);
47787
- mkdirSync16(path41.dirname(metaPath), { recursive: true });
49203
+ mkdirSync16(path42.dirname(metaPath), { recursive: true });
47788
49204
  writeFileSync19(metaPath, JSON.stringify(meta3, null, 2), "utf-8");
47789
49205
  }
47790
49206
  async function countSessions(name, baseDir) {
47791
49207
  const sessionsPath = sessionsPathFor(name, baseDir);
47792
49208
  try {
47793
- const entries = await fs24.readdir(sessionsPath);
49209
+ const entries = await fs25.readdir(sessionsPath);
47794
49210
  return entries.filter((e) => e.endsWith(".jsonl")).length;
47795
49211
  } catch (err) {
47796
49212
  if (err.code === "ENOENT") return 0;
@@ -47835,15 +49251,15 @@ async function createBranch(name, fromSessionId, baseDir = getBranchesBaseDir(),
47835
49251
  if (branchExists(name, baseDir)) {
47836
49252
  throw new BranchAlreadyExistsError(name);
47837
49253
  }
47838
- const sourcePath = path41.join(sessionsBaseDir, `${fromSessionId}.jsonl`);
49254
+ const sourcePath = path42.join(sessionsBaseDir, `${fromSessionId}.jsonl`);
47839
49255
  if (!existsSync38(sourcePath)) {
47840
49256
  throw new SessionNotFoundError(`Source session "${fromSessionId}" not found at ${sourcePath}`);
47841
49257
  }
47842
49258
  const branchPath = branchPathFor(name, baseDir);
47843
49259
  const branchSessionsPath = sessionsPathFor(name, baseDir);
47844
49260
  mkdirSync16(branchSessionsPath, { recursive: true });
47845
- const destPath = path41.join(branchSessionsPath, `${fromSessionId}.jsonl`);
47846
- await fs24.copyFile(sourcePath, destPath);
49261
+ const destPath = path42.join(branchSessionsPath, `${fromSessionId}.jsonl`);
49262
+ await fs25.copyFile(sourcePath, destPath);
47847
49263
  const meta3 = {
47848
49264
  name,
47849
49265
  createdAt: Date.now(),
@@ -47861,7 +49277,7 @@ async function createBranch(name, fromSessionId, baseDir = getBranchesBaseDir(),
47861
49277
  async function listBranches(baseDir = getBranchesBaseDir()) {
47862
49278
  let entries;
47863
49279
  try {
47864
- entries = await fs24.readdir(baseDir);
49280
+ entries = await fs25.readdir(baseDir);
47865
49281
  } catch (err) {
47866
49282
  if (err.code === "ENOENT") return [];
47867
49283
  throw err;
@@ -47942,26 +49358,26 @@ async function handleBranchCheckout(ctx, branchName) {
47942
49358
  }
47943
49359
 
47944
49360
  // src/cli/slashHandlers/workspace.ts
47945
- import { promises as fs25 } from "node:fs";
47946
- import path42 from "node:path";
49361
+ import { promises as fs26 } from "node:fs";
49362
+ import path43 from "node:path";
47947
49363
  async function handleWorkspaceShow(ctx, what) {
47948
49364
  try {
47949
- const zelari = path42.join(process.cwd(), ".zelari");
49365
+ const zelari = path43.join(process.cwd(), ".zelari");
47950
49366
  let content;
47951
49367
  switch (what) {
47952
49368
  case "plan": {
47953
- const planPath = path42.join(zelari, "plan.md");
49369
+ const planPath = path43.join(zelari, "plan.md");
47954
49370
  try {
47955
- content = await fs25.readFile(planPath, "utf-8");
49371
+ content = await fs26.readFile(planPath, "utf-8");
47956
49372
  } catch {
47957
49373
  content = "(no plan.md yet \u2014 run a council session first)";
47958
49374
  }
47959
49375
  break;
47960
49376
  }
47961
49377
  case "decisions": {
47962
- const decisionsDir = path42.join(zelari, "decisions");
49378
+ const decisionsDir = path43.join(zelari, "decisions");
47963
49379
  try {
47964
- const files = (await fs25.readdir(decisionsDir)).filter((f) => f.endsWith(".md")).sort();
49380
+ const files = (await fs26.readdir(decisionsDir)).filter((f) => f.endsWith(".md")).sort();
47965
49381
  if (files.length === 0) {
47966
49382
  content = "(no ADRs yet \u2014 invoke /council to generate some)";
47967
49383
  } else {
@@ -47969,7 +49385,7 @@ async function handleWorkspaceShow(ctx, what) {
47969
49385
  `];
47970
49386
  const { parseFrontmatter: parseFrontmatter2 } = await Promise.resolve().then(() => (init_storage(), storage_exports));
47971
49387
  for (const f of files) {
47972
- const raw = await fs25.readFile(path42.join(decisionsDir, f), "utf-8");
49388
+ const raw = await fs26.readFile(path43.join(decisionsDir, f), "utf-8");
47973
49389
  const { meta: meta3, body } = parseFrontmatter2(raw);
47974
49390
  const title = meta3.title ?? body.split("\n")[0]?.replace(/^#\s*/, "").trim() ?? f;
47975
49391
  lines.push(`- **${f.replace(/\.md$/, "")}** [${meta3.status ?? "unknown"}] ${title}`);
@@ -47982,27 +49398,27 @@ async function handleWorkspaceShow(ctx, what) {
47982
49398
  break;
47983
49399
  }
47984
49400
  case "risks": {
47985
- const risksPath = path42.join(zelari, "risks.md");
49401
+ const risksPath = path43.join(zelari, "risks.md");
47986
49402
  try {
47987
- content = await fs25.readFile(risksPath, "utf-8");
49403
+ content = await fs26.readFile(risksPath, "utf-8");
47988
49404
  } catch {
47989
49405
  content = "(no risks.md yet)";
47990
49406
  }
47991
49407
  break;
47992
49408
  }
47993
49409
  case "agents": {
47994
- const agentsPath = path42.join(process.cwd(), "AGENTS.MD");
49410
+ const agentsPath = path43.join(process.cwd(), "AGENTS.MD");
47995
49411
  try {
47996
- content = await fs25.readFile(agentsPath, "utf-8");
49412
+ content = await fs26.readFile(agentsPath, "utf-8");
47997
49413
  } catch {
47998
49414
  content = "(no AGENTS.MD yet at project root \u2014 run `/workspace sync` after a council session)";
47999
49415
  }
48000
49416
  break;
48001
49417
  }
48002
49418
  case "docs": {
48003
- const docsDir = path42.join(zelari, "docs");
49419
+ const docsDir = path43.join(zelari, "docs");
48004
49420
  try {
48005
- const files = (await fs25.readdir(docsDir)).filter((f) => f.endsWith(".md")).sort();
49421
+ const files = (await fs26.readdir(docsDir)).filter((f) => f.endsWith(".md")).sort();
48006
49422
  content = files.length ? `# Docs (${files.length})
48007
49423
 
48008
49424
  ` + files.map((f) => `- ${f}`).join("\n") : "(no docs drafts yet)";
@@ -48042,8 +49458,8 @@ async function handleWorkspaceReset(ctx, force) {
48042
49458
  return;
48043
49459
  }
48044
49460
  try {
48045
- const target = path42.join(process.cwd(), ".zelari");
48046
- await fs25.rm(target, { recursive: true, force: true });
49461
+ const target = path43.join(process.cwd(), ".zelari");
49462
+ await fs26.rm(target, { recursive: true, force: true });
48047
49463
  appendSystem(ctx.setMessages, "[workspace] .zelari/ removed");
48048
49464
  } catch (err) {
48049
49465
  appendSystem(ctx.setMessages, `[workspace reset error] ${err instanceof Error ? err.message : String(err)}`);
@@ -48402,16 +49818,16 @@ function handleModelsRefresh(ctx) {
48402
49818
  }
48403
49819
 
48404
49820
  // src/cli/slashHandlers/skills.ts
48405
- import path43 from "node:path";
49821
+ import path44 from "node:path";
48406
49822
  import os12 from "node:os";
48407
49823
 
48408
49824
  // src/cli/skillHistory.ts
48409
- import { promises as fs26, existsSync as existsSync39, statSync as statSync5, renameSync as renameSync4, appendFileSync as appendFileSync4, mkdirSync as mkdirSync17 } from "node:fs";
49825
+ import { promises as fs27, existsSync as existsSync39, statSync as statSync5, renameSync as renameSync4, appendFileSync as appendFileSync4, mkdirSync as mkdirSync17 } from "node:fs";
48410
49826
  var SKILL_HISTORY_ROTATE_BYTES = 10 * 1024 * 1024;
48411
49827
  async function readSkillHistory(file2) {
48412
49828
  let raw = "";
48413
49829
  try {
48414
- raw = await fs26.readFile(file2, "utf-8");
49830
+ raw = await fs27.readFile(file2, "utf-8");
48415
49831
  } catch {
48416
49832
  return [];
48417
49833
  }
@@ -48528,7 +49944,7 @@ function handleSkillPicker(ctx, skills, openPicker, fallbackMessage) {
48528
49944
  });
48529
49945
  }
48530
49946
  async function handleSkillStats(ctx, skillId) {
48531
- const historyFile = process.env.ANATHEMA_SKILL_HISTORY_FILE ?? path43.join(os12.homedir(), ".tmp", "zelari-code", "skill-history.jsonl");
49947
+ const historyFile = process.env.ANATHEMA_SKILL_HISTORY_FILE ?? path44.join(os12.homedir(), ".tmp", "zelari-code", "skill-history.jsonl");
48532
49948
  try {
48533
49949
  const records = await readSkillHistory(historyFile);
48534
49950
  const stats = getSkillStats(records, skillId);
@@ -48544,7 +49960,7 @@ async function handleSkillCompare(ctx, ids, fallbackMessage) {
48544
49960
  appendSystem(ctx.setMessages, fallbackMessage ?? "[skill-compare] missing args");
48545
49961
  return;
48546
49962
  }
48547
- const historyFile = process.env.ANATHEMA_SKILL_HISTORY_FILE ?? path43.join(os12.homedir(), ".tmp", "zelari-code", "skill-history.jsonl");
49963
+ const historyFile = process.env.ANATHEMA_SKILL_HISTORY_FILE ?? path44.join(os12.homedir(), ".tmp", "zelari-code", "skill-history.jsonl");
48548
49964
  try {
48549
49965
  const formatted = await compareSkillsFromFile(ids[0], ids[1], historyFile);
48550
49966
  appendSystem(ctx.setMessages, formatted);
@@ -49100,10 +50516,10 @@ function useSlashDispatch(params) {
49100
50516
  }
49101
50517
 
49102
50518
  // src/cli/hooks/useBatchedMessages.ts
49103
- import { useCallback as useCallback4, useEffect as useEffect5, useRef as useRef5 } from "react";
50519
+ import { useCallback as useCallback4, useEffect as useEffect6, useRef as useRef6 } from "react";
49104
50520
  function useBatchedMessages(state3, setState, cadenceMs = 16) {
49105
- const pendingRef = useRef5(null);
49106
- const timerRef = useRef5(null);
50521
+ const pendingRef = useRef6(null);
50522
+ const timerRef = useRef6(null);
49107
50523
  const flush = useCallback4(() => {
49108
50524
  if (timerRef.current !== null) {
49109
50525
  clearTimeout(timerRef.current);
@@ -49141,7 +50557,7 @@ function useBatchedMessages(state3, setState, cadenceMs = 16) {
49141
50557
  },
49142
50558
  [cadenceMs, setState]
49143
50559
  );
49144
- useEffect5(() => {
50560
+ useEffect6(() => {
49145
50561
  return () => {
49146
50562
  if (timerRef.current !== null) {
49147
50563
  clearTimeout(timerRef.current);
@@ -49153,7 +50569,7 @@ function useBatchedMessages(state3, setState, cadenceMs = 16) {
49153
50569
  }
49154
50570
 
49155
50571
  // src/cli/hooks/useTerminalSize.ts
49156
- import { useEffect as useEffect6, useState as useState7 } from "react";
50572
+ import { useEffect as useEffect7, useState as useState7 } from "react";
49157
50573
  import { useStdout as useStdout2 } from "ink";
49158
50574
  var DEFAULT_COALESCE_MS = 120;
49159
50575
  function useTerminalSize(options = {}) {
@@ -49163,7 +50579,7 @@ function useTerminalSize(options = {}) {
49163
50579
  columns: stdout?.columns ?? defaults.columns,
49164
50580
  rows: stdout?.rows ?? defaults.rows
49165
50581
  });
49166
- useEffect6(() => {
50582
+ useEffect7(() => {
49167
50583
  if (!stdout) return;
49168
50584
  const read = () => ({
49169
50585
  columns: stdout.columns ?? defaults.columns,
@@ -49227,6 +50643,7 @@ function App() {
49227
50643
  const activeProviderSpec = getActiveProvider();
49228
50644
  const activeModel = providerConfig.modelByProvider[activeProviderSpec.id];
49229
50645
  const session = useSession();
50646
+ usePermissionBroker({ setPicker: setPicker2, setMessages: session.setMessages });
49230
50647
  const size = useTerminalSize();
49231
50648
  const gitChanges = useGitChanges();
49232
50649
  const cwd = useMemo(() => shortenCwd(process.cwd(), 32), []);
@@ -49240,7 +50657,7 @@ function App() {
49240
50657
  },
49241
50658
  { isActive: isRawModeSupported === true }
49242
50659
  );
49243
- useEffect7(() => {
50660
+ useEffect8(() => {
49244
50661
  const id = activeProviderSpec.id;
49245
50662
  if (!["grok", "glm", "minimax", "deepseek", "openai-compatible"].includes(id)) return;
49246
50663
  if (!isModelsCacheStale(id)) return;
@@ -49330,7 +50747,7 @@ function App() {
49330
50747
  };
49331
50748
  }, []);
49332
50749
  const [sidebarOpen, setSidebarOpen] = useState8(false);
49333
- useEffect7(() => {
50750
+ useEffect8(() => {
49334
50751
  setSidebarOpen((prev2) => sidebarVisibility(size.columns, size.rows, prev2));
49335
50752
  }, [size.columns, size.rows]);
49336
50753
  const staticKey = `${session.sessionId || "pre-bootstrap"}-${clearEpoch}`;
@@ -49390,7 +50807,7 @@ function App() {
49390
50807
  }
49391
50808
 
49392
50809
  // src/cli/components/SplashScreen.tsx
49393
- import React11, { useEffect as useEffect8, useState as useState9 } from "react";
50810
+ import React11, { useEffect as useEffect9, useState as useState9 } from "react";
49394
50811
  import { Box as Box10, Text as Text11, useInput as useInput3, useStdin as useStdin3 } from "ink";
49395
50812
  var SPLASH_DURATION_MS = 2e3;
49396
50813
  var EMBLEM_LARGE = ` =%@*-
@@ -49516,7 +50933,7 @@ function Splash({ onDone, version: version2 }) {
49516
50933
  const columns = process.stdout.columns ?? 80;
49517
50934
  const rows = process.stdout.rows ?? 24;
49518
50935
  const picked = pickSplashArt(columns, rows);
49519
- useEffect8(() => {
50936
+ useEffect9(() => {
49520
50937
  const t = setTimeout(onDone, SPLASH_DURATION_MS);
49521
50938
  return () => clearTimeout(t);
49522
50939
  }, [onDone]);
@@ -49561,7 +50978,7 @@ function SplashGate({
49561
50978
  }
49562
50979
 
49563
50980
  // src/cli/components/PluginGate.tsx
49564
- import React12, { useEffect as useEffect9, useState as useState10, useCallback as useCallback6 } from "react";
50981
+ import React12, { useEffect as useEffect10, useState as useState10, useCallback as useCallback6 } from "react";
49565
50982
  import { Box as Box11, Text as Text12, useStdin as useStdin4 } from "ink";
49566
50983
  init_registry3();
49567
50984
  init_prefs();
@@ -49575,7 +50992,7 @@ function PluginGate({ cwd, children }) {
49575
50992
  const [current, setCurrent] = useState10(null);
49576
50993
  const [result, setResult] = useState10(null);
49577
50994
  const skipGate = process.env.ZELARI_NO_PLUGIN_PROMPT === "1" || isRawModeSupported !== true;
49578
- useEffect9(() => {
50995
+ useEffect10(() => {
49579
50996
  if (skipGate) {
49580
50997
  setPhase2("done");
49581
50998
  return;
@@ -49688,7 +51105,7 @@ function PluginGate({ cwd, children }) {
49688
51105
  }
49689
51106
  function ContinueKey({ onContinue }) {
49690
51107
  const { isRawModeSupported } = useStdin4();
49691
- useEffect9(() => {
51108
+ useEffect10(() => {
49692
51109
  const t = setTimeout(onContinue, 1500);
49693
51110
  return () => clearTimeout(t);
49694
51111
  }, [onContinue]);
@@ -49737,7 +51154,7 @@ function parseWizardFlags(argv) {
49737
51154
  }
49738
51155
 
49739
51156
  // src/cli/wizard/runWizard.tsx
49740
- import React14, { useEffect as useEffect10, useState as useState11 } from "react";
51157
+ import React14, { useEffect as useEffect11, useState as useState11 } from "react";
49741
51158
  import { Box as Box13, Text as Text14 } from "ink";
49742
51159
  import { useInput as useInput4 } from "ink";
49743
51160
  init_keyStore();
@@ -49902,7 +51319,7 @@ function RunWizard(_props) {
49902
51319
  })
49903
51320
  );
49904
51321
  const [, force] = useState11(0);
49905
- useEffect10(() => {
51322
+ useEffect11(() => {
49906
51323
  if (typeof wiz.subscribe === "function") {
49907
51324
  const sub = wiz.subscribe(() => force((n) => n + 1));
49908
51325
  return () => sub();
@@ -49972,7 +51389,7 @@ function RunWizard(_props) {
49972
51389
  }
49973
51390
  function PostCommitBridge() {
49974
51391
  const [showApp, setShowApp] = useState11(false);
49975
- useEffect10(() => {
51392
+ useEffect11(() => {
49976
51393
  const t = setTimeout(() => setShowApp(true), 1200);
49977
51394
  return () => clearTimeout(t);
49978
51395
  }, []);
@@ -50169,6 +51586,7 @@ init_dist();
50169
51586
  init_conversationContext();
50170
51587
  init_council();
50171
51588
  init_toolRegistry();
51589
+ init_claudeProvider();
50172
51590
  init_skills2();
50173
51591
  init_envNumber();
50174
51592
  init_phaseState();
@@ -50203,8 +51621,8 @@ function createStreamScrubber() {
50203
51621
 
50204
51622
  // src/cli/runHeadless.ts
50205
51623
  init_taskTool();
50206
- import { promises as fs28 } from "node:fs";
50207
- import path45 from "node:path";
51624
+ import { promises as fs29 } from "node:fs";
51625
+ import path46 from "node:path";
50208
51626
  import { randomUUID as randomUUID6 } from "node:crypto";
50209
51627
  async function runHeadless(opts) {
50210
51628
  resetTaskSpawnCount();
@@ -50212,10 +51630,14 @@ async function runHeadless(opts) {
50212
51630
  const { expandAtMentions: expandAtMentions2 } = await Promise.resolve().then(() => (init_atMentions(), atMentions_exports));
50213
51631
  const task = typeof opts.task === "string" ? opts.task : "";
50214
51632
  if (task.includes("@")) {
50215
- const { text } = expandAtMentions2(task, process.cwd());
51633
+ const { text, hits } = expandAtMentions2(task, process.cwd());
50216
51634
  if (text !== task) {
50217
51635
  opts = { ...opts, task: text };
50218
51636
  }
51637
+ const images = hits.filter((h) => h.image).map((h) => h.image);
51638
+ if (images.length > 0) {
51639
+ opts = { ...opts, images };
51640
+ }
50219
51641
  }
50220
51642
  } catch {
50221
51643
  }
@@ -50245,19 +51667,51 @@ ${err.stack}` : "";
50245
51667
  process.on("uncaughtException", (err) => handleFatal("uncaughtException", err));
50246
51668
  process.on("unhandledRejection", (err) => handleFatal("unhandledRejection", err));
50247
51669
  setPhase(opts.phase ?? "build");
50248
- const { provider, model } = resolveHeadlessProvider(opts);
50249
- const key = await resolveHeadlessKey(provider);
50250
- if ("error" in key) {
50251
- process.stderr.write(`[zelari-code --headless] ${key.error}
50252
- `);
50253
- return 1;
51670
+ let permBrokerStop = null;
51671
+ const permSocket = process.env.ZELARI_PERM_SOCKET?.trim();
51672
+ if (permSocket) {
51673
+ try {
51674
+ const { startPermissionBroker: startPermissionBroker2 } = await Promise.resolve().then(() => (init_permissionBroker(), permissionBroker_exports));
51675
+ const { createBrokerPermissionHandler: createBrokerPermissionHandler2 } = await Promise.resolve().then(() => (init_brokerHandlers(), brokerHandlers_exports));
51676
+ const h = await startPermissionBroker2(permSocket, {
51677
+ onPermission: createBrokerPermissionHandler2({})
51678
+ });
51679
+ permBrokerStop = h.stop;
51680
+ process.stderr.write(
51681
+ `[zelari-code] permission broker listening on ${permSocket}
51682
+ `
51683
+ );
51684
+ } catch (err) {
51685
+ process.stderr.write(
51686
+ `[zelari-code] permission broker failed to start: ${err instanceof Error ? err.message : String(err)}
51687
+ `
51688
+ );
51689
+ }
50254
51690
  }
50255
- const providerStream = openaiCompatibleProvider({
50256
- providerId: provider,
50257
- apiKey: key.apiKey,
50258
- baseUrl: key.baseUrl,
50259
- model
51691
+ process.once("exit", () => {
51692
+ void permBrokerStop?.();
50260
51693
  });
51694
+ const { provider: resolvedProvider, model } = resolveHeadlessProvider(opts);
51695
+ let provider = resolvedProvider;
51696
+ const localCli = (process.env.ZELARI_LOCAL_CLI ?? "").trim();
51697
+ let providerStream;
51698
+ if (localCli) {
51699
+ provider = "local-cli";
51700
+ providerStream = createLocalCliProvider({ cli: localCli, model });
51701
+ } else {
51702
+ const key = await resolveHeadlessKey(provider);
51703
+ if ("error" in key) {
51704
+ process.stderr.write(`[zelari-code --headless] ${key.error}
51705
+ `);
51706
+ return 1;
51707
+ }
51708
+ providerStream = openaiCompatibleProvider({
51709
+ providerId: provider,
51710
+ apiKey: key.apiKey,
51711
+ baseUrl: key.baseUrl,
51712
+ model
51713
+ });
51714
+ }
50261
51715
  const mode = opts.mode ?? (opts.useCouncil ? "council" : "kraken");
50262
51716
  if (opts.output === "json") {
50263
51717
  emitEvent({
@@ -50316,11 +51770,11 @@ async function runHeadlessKrakenGraph(opts, provider, model) {
50316
51770
  try {
50317
51771
  let preflightGraph;
50318
51772
  if (opts.runPlan && opts.runPlan.trim() !== "") {
50319
- const planPath = path45.join(cwd, ".zelari", "radio", `plan-${opts.runPlan}.json`);
51773
+ const planPath = path46.join(cwd, ".zelari", "radio", `plan-${opts.runPlan}.json`);
50320
51774
  log(`loading pre-flight plan: ${planPath}`);
50321
51775
  let raw;
50322
51776
  try {
50323
- raw = await fs28.readFile(planPath, "utf8");
51777
+ raw = await fs29.readFile(planPath, "utf8");
50324
51778
  } catch (e) {
50325
51779
  log(`plan file not found: ${planPath} (${e.message})`);
50326
51780
  return 1;
@@ -50356,10 +51810,10 @@ async function runHeadlessKrakenGraph(opts, provider, model) {
50356
51810
  log(formatKrakenGraphAscii2(graph));
50357
51811
  if (opts.planOnly) {
50358
51812
  const planId = randomUUID6();
50359
- const planDir = path45.join(cwd, ".zelari", "radio");
50360
- const planPath = path45.join(planDir, `plan-${planId}.json`);
50361
- await fs28.mkdir(planDir, { recursive: true });
50362
- await fs28.writeFile(planPath, JSON.stringify(graph, null, 2), "utf8");
51813
+ const planDir = path46.join(cwd, ".zelari", "radio");
51814
+ const planPath = path46.join(planDir, `plan-${planId}.json`);
51815
+ await fs29.mkdir(planDir, { recursive: true });
51816
+ await fs29.writeFile(planPath, JSON.stringify(graph, null, 2), "utf8");
50363
51817
  log(`plan-only: wrote ${planPath} (${graph.nodes.size} nodes)`);
50364
51818
  log(
50365
51819
  `re-run with ZELARI_KRAKEN_RUN_PLAN=${planId} to execute (or --run-plan <id> when the desktop wiring is in place)`
@@ -50709,7 +52163,11 @@ async function runHeadlessSingle(opts, provider, model, providerStream) {
50709
52163
  const initialMessages = [
50710
52164
  { role: "system", content: systemPrompt },
50711
52165
  ...historySeed,
50712
- { role: "user", content: effectiveTask }
52166
+ {
52167
+ role: "user",
52168
+ content: effectiveTask,
52169
+ ...opts.images && opts.images.length > 0 ? { images: opts.images } : {}
52170
+ }
50713
52171
  ];
50714
52172
  let pass = await runSinglePass(initialMessages, sessionId);
50715
52173
  const wantWrites = expectsDiskImplementation(
@@ -51382,7 +52840,8 @@ var BUILTIN_SKILL_MODULES = [
51382
52840
  "@zelari/core/skills/builtin/review",
51383
52841
  "@zelari/core/skills/builtin/testing",
51384
52842
  "@zelari/core/skills/builtin/schema-loop",
51385
- "@zelari/core/skills/builtin/computer-use-cua"
52843
+ "@zelari/core/skills/builtin/computer-use-cua",
52844
+ "@zelari/core/skills/builtin/qwen-mm-plugins"
51386
52845
  ];
51387
52846
  var builtinsLoaded = false;
51388
52847
  function ensureBuiltinSkillsLoadedSync() {
@@ -51545,7 +53004,7 @@ function upsertSkill(opts) {
51545
53004
  }
51546
53005
  dir = getProjectSkillsDir(root);
51547
53006
  }
51548
- const path47 = skillFilePath(dir, name);
53007
+ const path48 = skillFilePath(dir, name);
51549
53008
  const content = serializeSkillMd({
51550
53009
  name,
51551
53010
  description,
@@ -51554,13 +53013,13 @@ function upsertSkill(opts) {
51554
53013
  tools: opts.tools,
51555
53014
  cost: opts.cost
51556
53015
  });
51557
- const parsed = parseSkillMd(content, path47);
53016
+ const parsed = parseSkillMd(content, path48);
51558
53017
  if (!parsed) {
51559
53018
  return { ok: false, error: "Generated SKILL.md failed validation" };
51560
53019
  }
51561
- mkdirSync18(dirname9(path47), { recursive: true });
51562
- writeFileSync20(path47, content, "utf8");
51563
- return { ok: true, path: path47 };
53020
+ mkdirSync18(dirname9(path48), { recursive: true });
53021
+ writeFileSync20(path48, content, "utf8");
53022
+ return { ok: true, path: path48 };
51564
53023
  }
51565
53024
  function removeSkill(opts) {
51566
53025
  const name = opts.name.trim().toLowerCase();
@@ -51578,8 +53037,8 @@ function removeSkill(opts) {
51578
53037
  dir = getProjectSkillsDir(root);
51579
53038
  }
51580
53039
  const skillDir = join34(dir, name);
51581
- const path47 = skillFilePath(dir, name);
51582
- if (!existsSync42(path47) && !existsSync42(skillDir)) {
53040
+ const path48 = skillFilePath(dir, name);
53041
+ if (!existsSync42(path48) && !existsSync42(skillDir)) {
51583
53042
  return { ok: false, error: `Skill "${name}" not found in ${dir}` };
51584
53043
  }
51585
53044
  try {
@@ -51590,7 +53049,7 @@ function removeSkill(opts) {
51590
53049
  error: err instanceof Error ? err.message : String(err)
51591
53050
  };
51592
53051
  }
51593
- return { ok: true, path: path47 };
53052
+ return { ok: true, path: path48 };
51594
53053
  }
51595
53054
 
51596
53055
  // src/cli/generateSkillFromUrl.ts
@@ -51680,8 +53139,8 @@ function normalizeDraft(raw, sourceUrl, provider, model) {
51680
53139
  let name = String(o.name ?? "").trim().toLowerCase().replace(/[^a-z0-9-]+/g, "-").replace(/^-+|-+$/g, "").slice(0, 64);
51681
53140
  if (!name || !/^[a-z0-9][a-z0-9-]{0,63}$/.test(name)) {
51682
53141
  try {
51683
- const path47 = new URL(sourceUrl).pathname.split("/").filter(Boolean).pop()?.replace(/\.[a-z0-9]+$/i, "").toLowerCase().replace(/[^a-z0-9-]+/g, "-").replace(/^-+|-+$/g, "").slice(0, 48);
51684
- name = path47 && /^[a-z0-9]/.test(path47) ? path47 : "imported-skill";
53142
+ const path48 = new URL(sourceUrl).pathname.split("/").filter(Boolean).pop()?.replace(/\.[a-z0-9]+$/i, "").toLowerCase().replace(/[^a-z0-9-]+/g, "-").replace(/^-+|-+$/g, "").slice(0, 48);
53143
+ name = path48 && /^[a-z0-9]/.test(path48) ? path48 : "imported-skill";
51685
53144
  } catch {
51686
53145
  name = "imported-skill";
51687
53146
  }
@@ -51863,6 +53322,21 @@ function pickRootComponent() {
51863
53322
  void runPluginsInstall(argv).then((code) => process.exit(code));
51864
53323
  return { kind: "done" };
51865
53324
  }
53325
+ if (argv.includes("--permission-mcp")) {
53326
+ const i = argv.indexOf("--permission-mcp");
53327
+ const socketPath = i >= 0 && argv[i + 1] ? argv[i + 1] : void 0;
53328
+ if (!socketPath) {
53329
+ console.error("[zelari-code --permission-mcp] a socket path argument is required");
53330
+ process.exit(1);
53331
+ }
53332
+ void Promise.resolve().then(() => (init_permissionCli(), permissionCli_exports)).then(({ runPermissionCli: runPermissionCli2 }) => runPermissionCli2(socketPath)).catch((err) => {
53333
+ console.error(
53334
+ `[zelari-code --permission-mcp] ${err instanceof Error ? err.message : String(err)}`
53335
+ );
53336
+ process.exit(1);
53337
+ });
53338
+ return { kind: "done" };
53339
+ }
51866
53340
  if (argv.includes("serve") || argv.includes("--serve")) {
51867
53341
  const { parseServeFlags: parseServeFlags2 } = (init_serve(), __toCommonJS(serve_exports));
51868
53342
  return { kind: "serve", serveOpts: parseServeFlags2(argv) ?? {} };
@@ -51943,7 +53417,7 @@ function pickRootComponent() {
51943
53417
  }
51944
53418
  if (argv.includes("--help") || argv.includes("-h")) {
51945
53419
  console.log(
51946
- "zelari-code \u2014 AI Council coding agent CLI.\n\nUsage: zelari-code [options]\n\nOptions:\n --version, -v Print version and exit\n --help, -h Print this help and exit\n --doctor Diagnose install health (shim, bundle, PATH, deps,\n node/git/bash in the agent shell)\n --fix-path Add the npm global prefix to the user PATH\n (Windows only; fixes 'command not found' after install)\n --fix-budget Set recommended ZELARI_MAX_TOOL_LOOP_HARD=180,\n ZELARI_MAX_TOOL_LOOP_ITERATIONS=60, ZELARI_CONTEXT_LIMIT=400000\n at User scope (prevents the agent stopping mid-task)\n --skip-checks Skip the boot-time prerequisite check\n (alias for ZELARI_SKIP_PREFLIGHT=1)\n --no-wizard Skip the first-run wizard\n --reset-config Re-run the wizard (clears provider.json on commit)\n --headless Run a single task without mounting the TUI\n --task <text> Task prompt (required in headless mode)\n --output json|plain Output format (default: json)\n --mode kraken|council|zelari Dispatch mode (default: kraken; agent=alias)\n --council Alias for --mode council\n --phase plan|build Work phase (default: build)\n --provider <id> Provider override (default: active)\n --model <id> Model override (default: provider default)\n serve Companion host for Android/remote clients (Tailscale)\n --bind <ip> Listen address (default: 127.0.0.1; use Tailscale IP)\n --port <n> Port (default: 7421)\n --token <secret> Bearer token (default: ~/.zelari-code/companion.token)\n --project <path> Allowlisted project root (repeatable)\n --save-projects Persist --project list to companion.json\n --print-config Print provider/model config as JSON (no secrets)\n --plugins-status JSON status of optional plugins (Playwright, eslint, \u2026)\n --plugins-install <id> Install plugin (playwright also fetches Chromium)\n --cwd <path> Workspace for -D installs (default: process.cwd())\n --set-config Persist provider/model/endpoint\n --provider <id> Set active provider\n --model <id> Set model for that provider\n --endpoint <url> Custom OpenAI-compatible base URL\n --endpoint-clear Remove custom endpoint override\n --set-key Store an API key (never printed back)\n --provider <id> Provider id (required)\n --key <secret> API key (required)\n --discover-models Refresh model list for a provider\n --provider <id> Provider (default: active)\n --print-mcp Print MCP server config (user + project)\n --cwd <path> Project root for .zelari/mcp.json\n --set-mcp Add/update an MCP server entry\n --name <id> Server name (required)\n --command <bin> Executable (required)\n --args <json> JSON array of args (optional)\n --scope user|project Default: user\n --enabled true|false Default: true\n --cwd <path> Required when scope=project\n --set-mcp-preset Install a named MCP preset (e.g. cua)\n --preset cua Cua Driver desktop computer-use (MCP)\n --scope user|project Default: user\n --cwd <path> Required when scope=project\n --remove-mcp Remove an MCP server entry\n --name <id> --scope user|project [--cwd <path>]\n --print-skills Print skills (builtin + user + project)\n --cwd <path> Project root for .zelari/skills\n --set-skill Add/update a SKILL.md skill\n --name <id> Skill id (required)\n --description <t> One-line description (required)\n --body <text> Markdown instructions (required)\n --category <c> plan|refactor|debug|review|test|docs|ops|git|db|maint\n --tools <csv> Comma-separated required tools (optional)\n --cost <l> low|medium|high (default: medium)\n --scope user|project Default: user\n --cwd <path> Required when scope=project\n --remove-skill Remove a user/project SKILL.md\n --name <id> --scope user|project [--cwd <path>]\n --generate-skill-from-url Fetch URL + draft skill via model (JSON)\n --url <https://...> Required\n --provider <id> Override active provider\n --model <name> Override model for the selected provider\n --print-ssh-targets Print SSH deploy/monitor targets\n --set-ssh-target Upsert target (--json '{...}' or flags)\n --remove-ssh-target --id <id>\n --test-ssh-target --id <id> (BatchMode ssh true)\n --print-ssh-pubkey --path <private-or-.pub> (display public key)\n\nEnvironment:\n ZELARI_NO_WIZARD=1 Skip the first-run wizard\n ZELARI_SKIP_PREFLIGHT=1 Skip the boot prerequisite check\n ZELARI_NO_PLUGIN_PROMPT=1 Skip the boot plugin-install prompt\n ANATHEMA_DEV=1 Disable background update check + preflight\n"
53420
+ "zelari-code \u2014 AI Council coding agent CLI.\n\nUsage: zelari-code [options]\n\nOptions:\n --version, -v Print version and exit\n --help, -h Print this help and exit\n --doctor Diagnose install health (shim, bundle, PATH, deps,\n node/git/bash in the agent shell)\n --fix-path Add the npm global prefix to the user PATH\n (Windows only; fixes 'command not found' after install)\n --fix-budget Set recommended ZELARI_MAX_TOOL_LOOP_HARD=180,\n ZELARI_MAX_TOOL_LOOP_ITERATIONS=60, ZELARI_CONTEXT_LIMIT=400000\n at User scope (prevents the agent stopping mid-task)\n --permission-mcp <socket> MCP stdio server for external agent permission prompts\n (spawned by claude --permission-prompt-tool)\n --skip-checks Skip the boot-time prerequisite check\n (alias for ZELARI_SKIP_PREFLIGHT=1)\n --no-wizard Skip the first-run wizard\n --reset-config Re-run the wizard (clears provider.json on commit)\n --headless Run a single task without mounting the TUI\n --task <text> Task prompt (required in headless mode)\n --output json|plain Output format (default: json)\n --mode kraken|council|zelari Dispatch mode (default: kraken; agent=alias)\n --council Alias for --mode council\n --phase plan|build Work phase (default: build)\n --provider <id> Provider override (default: active)\n --model <id> Model override (default: provider default)\n serve Companion host for Android/remote clients (Tailscale)\n --bind <ip> Listen address (default: 127.0.0.1; use Tailscale IP)\n --port <n> Port (default: 7421)\n --token <secret> Bearer token (default: ~/.zelari-code/companion.token)\n --project <path> Allowlisted project root (repeatable)\n --save-projects Persist --project list to companion.json\n --print-config Print provider/model config as JSON (no secrets)\n --plugins-status JSON status of optional plugins (Playwright, eslint, \u2026)\n --plugins-install <id> Install plugin (playwright also fetches Chromium)\n --cwd <path> Workspace for -D installs (default: process.cwd())\n --set-config Persist provider/model/endpoint\n --provider <id> Set active provider\n --model <id> Set model for that provider\n --endpoint <url> Custom OpenAI-compatible base URL\n --endpoint-clear Remove custom endpoint override\n --set-key Store an API key (never printed back)\n --provider <id> Provider id (required)\n --key <secret> API key (required)\n --discover-models Refresh model list for a provider\n --provider <id> Provider (default: active)\n --print-mcp Print MCP server config (user + project)\n --cwd <path> Project root for .zelari/mcp.json\n --set-mcp Add/update an MCP server entry\n --name <id> Server name (required)\n --command <bin> Executable (required)\n --args <json> JSON array of args (optional)\n --scope user|project Default: user\n --enabled true|false Default: true\n --cwd <path> Required when scope=project\n --set-mcp-preset Install a named MCP preset (e.g. cua)\n --preset cua Cua Driver desktop computer-use (MCP)\n --scope user|project Default: user\n --cwd <path> Required when scope=project\n --remove-mcp Remove an MCP server entry\n --name <id> --scope user|project [--cwd <path>]\n --print-skills Print skills (builtin + user + project)\n --cwd <path> Project root for .zelari/skills\n --set-skill Add/update a SKILL.md skill\n --name <id> Skill id (required)\n --description <t> One-line description (required)\n --body <text> Markdown instructions (required)\n --category <c> plan|refactor|debug|review|test|docs|ops|git|db|maint\n --tools <csv> Comma-separated required tools (optional)\n --cost <l> low|medium|high (default: medium)\n --scope user|project Default: user\n --cwd <path> Required when scope=project\n --remove-skill Remove a user/project SKILL.md\n --name <id> --scope user|project [--cwd <path>]\n --generate-skill-from-url Fetch URL + draft skill via model (JSON)\n --url <https://...> Required\n --provider <id> Override active provider\n --model <name> Override model for the selected provider\n --print-ssh-targets Print SSH deploy/monitor targets\n --set-ssh-target Upsert target (--json '{...}' or flags)\n --remove-ssh-target --id <id>\n --test-ssh-target --id <id> (BatchMode ssh true)\n --print-ssh-pubkey --path <private-or-.pub> (display public key)\n\nEnvironment:\n ZELARI_NO_WIZARD=1 Skip the first-run wizard\n ZELARI_SKIP_PREFLIGHT=1 Skip the boot prerequisite check\n ZELARI_NO_PLUGIN_PROMPT=1 Skip the boot plugin-install prompt\n ANATHEMA_DEV=1 Disable background update check + preflight\n"
51947
53421
  );
51948
53422
  process.exit(0);
51949
53423
  }