zelari-code 1.30.4 → 1.31.0

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 +1837 -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 +78 -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,60 @@ 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
+ return {
34658
+ id: "qwen-mm-plugins",
34659
+ servers: {
34660
+ "qwen-mm-plugins": {
34661
+ command: "uvx",
34662
+ args: [plugin],
34663
+ env: {
34664
+ ...process.env.DASHSCOPE_API_KEY ? { DASHSCOPE_API_KEY: process.env.DASHSCOPE_API_KEY } : {},
34665
+ ...process.env.SERPER_API_KEY ? { SERPER_API_KEY: process.env.SERPER_API_KEY } : {}
34666
+ },
34667
+ enabled: true
34668
+ }
34669
+ },
34670
+ notes: [
34671
+ "Qwen-MM-Plugins \u2014 multimodal capabilities (core / video-memory / video-edit / blender / freecad) as MCP tools.",
34672
+ "Run the upstream guided installer first: curl -fsSL https://raw.githubusercontent.com/QwenLM/Qwen-MM-Plugins/main/install.sh | bash",
34673
+ "Windows: use WSL2 (Ubuntu); native Windows is not validated upstream.",
34674
+ "Pick a capability: export QWEN_MM_PLUGIN=qwen-mm-plugins-video-memory (default: qwen-mm-plugins-core).",
34675
+ "API keys read at apply time: DASHSCOPE_API_KEY, SERPER_API_KEY (native reading works without them).",
34676
+ "Blender/FreeCAD variants: start the host app before invoking tools.",
34677
+ "Kill switch: disable the qwen-mm-plugins server in mcp.json or ZELARI_MCP=0"
34678
+ ]
34679
+ };
34680
+ }
34310
34681
  function listMcpPresetIds() {
34311
- return Object.keys(PRESETS).filter((k) => k === "cua");
34682
+ return Object.keys(PRESETS).filter(
34683
+ (k) => k === "cua" || k === "composio" || k === "qwen-mm-plugins"
34684
+ );
34312
34685
  }
34313
34686
  function getMcpPreset(id) {
34314
- return PRESETS[id.trim().toLowerCase()] ?? null;
34687
+ const factory = PRESETS[id.trim().toLowerCase()];
34688
+ return factory ? factory() : null;
34315
34689
  }
34316
34690
  function applyMcpPreset(opts) {
34317
34691
  const preset = getMcpPreset(opts.presetId);
@@ -34371,8 +34745,11 @@ var init_mcpPresets = __esm({
34371
34745
  ]
34372
34746
  };
34373
34747
  PRESETS = {
34374
- cua: CUA_DRIVER_PRESET,
34375
- "cua-driver": CUA_DRIVER_PRESET
34748
+ cua: () => CUA_DRIVER_PRESET,
34749
+ "cua-driver": () => CUA_DRIVER_PRESET,
34750
+ composio: buildComposioPreset,
34751
+ "qwen-mm-plugins": buildQwenMmPreset,
34752
+ "qwen-mm": buildQwenMmPreset
34376
34753
  };
34377
34754
  }
34378
34755
  });
@@ -34635,10 +35012,10 @@ import { createHash as createHash5 } from "node:crypto";
34635
35012
  import { join as join26 } from "node:path";
34636
35013
  import { readFile as readFile3 } from "node:fs/promises";
34637
35014
  async function readPackageJson2(projectRoot) {
34638
- const path47 = join26(projectRoot, "package.json");
34639
- if (!existsSync30(path47)) return null;
35015
+ const path48 = join26(projectRoot, "package.json");
35016
+ if (!existsSync30(path48)) return null;
34640
35017
  try {
34641
- return JSON.parse(await readFile3(path47, "utf8"));
35018
+ return JSON.parse(await readFile3(path48, "utf8"));
34642
35019
  } catch {
34643
35020
  return null;
34644
35021
  }
@@ -34720,9 +35097,9 @@ async function genBuild(ctx) {
34720
35097
  ].join("\n");
34721
35098
  }
34722
35099
  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");
35100
+ const path48 = join26(ctx.rootDir, "risks.md");
35101
+ if (!existsSync30(path48)) return "_No open questions._";
35102
+ const content = readFileSync25(path48, "utf8");
34726
35103
  const lines = content.split("\n");
34727
35104
  const questions = [];
34728
35105
  let currentTitle = "";
@@ -34967,7 +35344,7 @@ var init_completeDesign = __esm({
34967
35344
  });
34968
35345
 
34969
35346
  // src/cli/workspace/projectSmoke.ts
34970
- import { spawn as spawn8 } from "node:child_process";
35347
+ import { spawn as spawn9 } from "node:child_process";
34971
35348
  import { existsSync as existsSync31, readFileSync as readFileSync26 } from "node:fs";
34972
35349
  import { join as join27 } from "node:path";
34973
35350
  function pickSmokeScript(scripts) {
@@ -34997,12 +35374,12 @@ async function runProjectSmoke(projectRoot, timeoutMs = DEFAULT_TIMEOUT_MS) {
34997
35374
  return { ran: false, reason: "no typecheck/test/build script (skipped)" };
34998
35375
  }
34999
35376
  return await new Promise((resolveRun) => {
35000
- const child = process.platform === "win32" ? spawn8(buildCmdLine("npm.cmd", ["run", script]), {
35377
+ const child = process.platform === "win32" ? spawn9(buildCmdLine("npm.cmd", ["run", script]), {
35001
35378
  cwd: projectRoot,
35002
35379
  stdio: ["ignore", "pipe", "pipe"],
35003
35380
  env: process.env,
35004
35381
  shell: true
35005
- }) : spawn8("npm", ["run", script], {
35382
+ }) : spawn9("npm", ["run", script], {
35006
35383
  cwd: projectRoot,
35007
35384
  stdio: ["ignore", "pipe", "pipe"],
35008
35385
  env: process.env
@@ -35074,7 +35451,7 @@ __export(postCouncilHook_exports, {
35074
35451
  runImplementationVerificationHook: () => runImplementationVerificationHook,
35075
35452
  runPostCouncilHook: () => runPostCouncilHook
35076
35453
  });
35077
- import { spawn as spawn9 } from "node:child_process";
35454
+ import { spawn as spawn10 } from "node:child_process";
35078
35455
  import { existsSync as existsSync32, readFileSync as readFileSync27 } from "node:fs";
35079
35456
  import { join as join28 } from "node:path";
35080
35457
  async function runCompleteDesignPostProcessor(ctx, options) {
@@ -35123,7 +35500,7 @@ async function runCompleteDesignPostProcessor(ctx, options) {
35123
35500
  }
35124
35501
  }
35125
35502
  return await new Promise((resolveRun) => {
35126
- const child = spawn9(process.execPath, [scriptPath], {
35503
+ const child = spawn10(process.execPath, [scriptPath], {
35127
35504
  cwd: ctx.projectRoot,
35128
35505
  stdio: ["ignore", "pipe", "pipe"],
35129
35506
  env: process.env
@@ -35289,8 +35666,8 @@ async function runPostCouncilHook(ctx, options) {
35289
35666
  sources: scope.sources
35290
35667
  } : void 0
35291
35668
  });
35292
- const path47 = writeCouncilCompletion(ctx.rootDir, completion);
35293
- completionHook = { ran: true, path: path47, completion };
35669
+ const path48 = writeCouncilCompletion(ctx.rootDir, completion);
35670
+ completionHook = { ran: true, path: path48, completion };
35294
35671
  } catch (err) {
35295
35672
  completionHook = {
35296
35673
  ran: true,
@@ -36502,6 +36879,307 @@ var init_missionSlice = __esm({
36502
36879
  }
36503
36880
  });
36504
36881
 
36882
+ // src/cli/mcp/permissionBroker.ts
36883
+ var permissionBroker_exports = {};
36884
+ __export(permissionBroker_exports, {
36885
+ PERMISSION_BROKER_DEFAULT_CONNECT_TIMEOUT_MS: () => PERMISSION_BROKER_DEFAULT_CONNECT_TIMEOUT_MS,
36886
+ PERMISSION_BROKER_DEFAULT_TIMEOUT_MS: () => PERMISSION_BROKER_DEFAULT_TIMEOUT_MS,
36887
+ requestBrokerAsk: () => requestBrokerAsk,
36888
+ startPermissionBroker: () => startPermissionBroker
36889
+ });
36890
+ import { connect, createServer } from "node:net";
36891
+ import { unlink } from "node:fs";
36892
+ function sendJson(socket, obj) {
36893
+ if (socket.destroyed) return;
36894
+ socket.write(JSON.stringify(obj) + "\n");
36895
+ }
36896
+ function safeSocketPath(socketPath) {
36897
+ return socketPath.trim();
36898
+ }
36899
+ function startPermissionBroker(socketPath, handlers, opts) {
36900
+ const path48 = safeSocketPath(socketPath);
36901
+ const requestTimeoutMs = opts?.requestTimeoutMs ?? PERMISSION_BROKER_DEFAULT_TIMEOUT_MS;
36902
+ const sockets = /* @__PURE__ */ new Set();
36903
+ const server = createServer((socket) => {
36904
+ socket.setEncoding("utf8");
36905
+ sockets.add(socket);
36906
+ socket.on("close", () => sockets.delete(socket));
36907
+ socket.on("error", () => {
36908
+ });
36909
+ let buffer = "";
36910
+ socket.on("data", (chunk) => {
36911
+ buffer += chunk;
36912
+ let nl;
36913
+ while ((nl = buffer.indexOf("\n")) >= 0) {
36914
+ const line = buffer.slice(0, nl).trim();
36915
+ buffer = buffer.slice(nl + 1);
36916
+ if (!line) continue;
36917
+ void handleLine(socket, line);
36918
+ }
36919
+ });
36920
+ });
36921
+ server.on("error", () => {
36922
+ });
36923
+ async function handleLine(socket, line) {
36924
+ let msg;
36925
+ try {
36926
+ msg = JSON.parse(line);
36927
+ } catch {
36928
+ return;
36929
+ }
36930
+ if (!msg || msg.t !== "ask" || typeof msg.id !== "string" || !msg.id) return;
36931
+ const id = msg.id;
36932
+ let settled = false;
36933
+ let timer;
36934
+ const reply = (answer) => {
36935
+ if (settled) return;
36936
+ settled = true;
36937
+ if (timer) clearTimeout(timer);
36938
+ sendJson(socket, { t: "answer", id, ...answer });
36939
+ };
36940
+ timer = setTimeout(() => {
36941
+ reply({
36942
+ behavior: "deny",
36943
+ message: `[zelari] permission broker timed out after ${requestTimeoutMs}ms`
36944
+ });
36945
+ }, requestTimeoutMs);
36946
+ if (typeof timer.unref === "function") timer.unref();
36947
+ try {
36948
+ if (msg.kind === "question") {
36949
+ if (!handlers.onQuestion) {
36950
+ reply({
36951
+ behavior: "deny",
36952
+ message: "[zelari] no interactive answer available (question handler missing)"
36953
+ });
36954
+ return;
36955
+ }
36956
+ const answer2 = await handlers.onQuestion({
36957
+ id,
36958
+ question: msg.question ?? "",
36959
+ choices: msg.choices,
36960
+ context: msg.context
36961
+ });
36962
+ if (answer2 == null) {
36963
+ reply({
36964
+ behavior: "deny",
36965
+ message: "[zelari] question cancelled (no answer)"
36966
+ });
36967
+ return;
36968
+ }
36969
+ if (answer2.behavior === "deny") {
36970
+ reply({
36971
+ behavior: "deny",
36972
+ message: answer2.message ?? "[zelari] question denied by handler"
36973
+ });
36974
+ return;
36975
+ }
36976
+ reply({ behavior: "allow", answer: answer2.answer ?? "" });
36977
+ return;
36978
+ }
36979
+ const answer = handlers.onPermission ? await handlers.onPermission({
36980
+ id,
36981
+ tool: msg.tool ?? "unknown",
36982
+ input: msg.input,
36983
+ toolUseId: msg.toolUseId,
36984
+ suggestions: msg.suggestions
36985
+ }) : {
36986
+ behavior: "deny",
36987
+ message: "[zelari] permission broker has no approval handler"
36988
+ };
36989
+ reply(answer);
36990
+ } catch (err) {
36991
+ reply({
36992
+ behavior: "deny",
36993
+ message: `[zelari] permission broker error: ${err instanceof Error ? err.message : String(err)}`
36994
+ });
36995
+ }
36996
+ }
36997
+ return new Promise((resolve3, reject) => {
36998
+ const onError = (err) => reject(err);
36999
+ server.once("error", onError);
37000
+ server.listen(path48, () => {
37001
+ server.removeListener("error", onError);
37002
+ resolve3({
37003
+ socketPath: path48,
37004
+ stop: () => new Promise((res) => {
37005
+ for (const s of sockets) s.destroy();
37006
+ sockets.clear();
37007
+ const closeAll = server.closeAllConnections;
37008
+ if (typeof closeAll === "function") closeAll.call(server);
37009
+ let done = false;
37010
+ const finish = () => {
37011
+ if (done) return;
37012
+ done = true;
37013
+ if (process.platform !== "win32") {
37014
+ unlink(path48, () => res());
37015
+ } else {
37016
+ res();
37017
+ }
37018
+ };
37019
+ server.close(() => finish());
37020
+ setTimeout(finish, 1e3).unref?.();
37021
+ })
37022
+ });
37023
+ });
37024
+ });
37025
+ }
37026
+ function requestBrokerAsk(socketPath, ask, opts) {
37027
+ const path48 = safeSocketPath(socketPath);
37028
+ const requestTimeoutMs = opts?.requestTimeoutMs ?? PERMISSION_BROKER_DEFAULT_TIMEOUT_MS;
37029
+ const connectTimeoutMs = opts?.connectTimeoutMs ?? PERMISSION_BROKER_DEFAULT_CONNECT_TIMEOUT_MS;
37030
+ return new Promise((resolve3, reject) => {
37031
+ const socket = connect(path48);
37032
+ let buffer = "";
37033
+ let settled = false;
37034
+ const settle = (fn) => {
37035
+ if (settled) return;
37036
+ settled = true;
37037
+ clearTimeout(connectTimer);
37038
+ clearTimeout(requestTimer);
37039
+ socket.destroy();
37040
+ fn();
37041
+ };
37042
+ const connectTimer = setTimeout(() => {
37043
+ settle(
37044
+ () => reject(
37045
+ new Error(
37046
+ `permission broker unavailable at "${path48}" (connect timed out after ${connectTimeoutMs}ms)`
37047
+ )
37048
+ )
37049
+ );
37050
+ }, connectTimeoutMs);
37051
+ const requestTimer = setTimeout(() => {
37052
+ settle(
37053
+ () => reject(
37054
+ new Error(
37055
+ `permission broker timed out after ${requestTimeoutMs}ms waiting for an answer`
37056
+ )
37057
+ )
37058
+ );
37059
+ }, requestTimeoutMs);
37060
+ socket.setEncoding("utf8");
37061
+ socket.on("connect", () => clearTimeout(connectTimer));
37062
+ socket.on("error", (err) => settle(() => reject(err)));
37063
+ socket.on("close", () => {
37064
+ settle(
37065
+ () => reject(new Error("permission broker closed the connection without an answer"))
37066
+ );
37067
+ });
37068
+ socket.on("data", (chunk) => {
37069
+ buffer += chunk;
37070
+ let nl;
37071
+ while ((nl = buffer.indexOf("\n")) >= 0) {
37072
+ const line = buffer.slice(0, nl).trim();
37073
+ buffer = buffer.slice(nl + 1);
37074
+ if (!line) continue;
37075
+ let msg;
37076
+ try {
37077
+ msg = JSON.parse(line);
37078
+ } catch {
37079
+ continue;
37080
+ }
37081
+ if (msg?.t === "answer" && msg.id === ask.id) {
37082
+ const { t: _t, id: _id, ...answer } = msg;
37083
+ settle(() => resolve3(answer));
37084
+ return;
37085
+ }
37086
+ }
37087
+ });
37088
+ sendJson(socket, ask);
37089
+ });
37090
+ }
37091
+ var PERMISSION_BROKER_DEFAULT_TIMEOUT_MS, PERMISSION_BROKER_DEFAULT_CONNECT_TIMEOUT_MS;
37092
+ var init_permissionBroker = __esm({
37093
+ "src/cli/mcp/permissionBroker.ts"() {
37094
+ "use strict";
37095
+ PERMISSION_BROKER_DEFAULT_TIMEOUT_MS = 6e4;
37096
+ PERMISSION_BROKER_DEFAULT_CONNECT_TIMEOUT_MS = 2e3;
37097
+ }
37098
+ });
37099
+
37100
+ // src/cli/mcp/brokerHandlers.ts
37101
+ var brokerHandlers_exports = {};
37102
+ __export(brokerHandlers_exports, {
37103
+ createBrokerPermissionHandler: () => createBrokerPermissionHandler,
37104
+ createBrokerQuestionHandler: () => createBrokerQuestionHandler,
37105
+ inferPermissionCategories: () => inferPermissionCategories
37106
+ });
37107
+ function inferPermissionCategories(toolName, input) {
37108
+ const cats = /* @__PURE__ */ new Set();
37109
+ const name = toolName.toLowerCase();
37110
+ const obj = input && typeof input === "object" ? input : {};
37111
+ if (/bash|shell|exec|command|run|npm|npx|yarn|pnpm|git|deno|bun|make/.test(name)) {
37112
+ cats.add("execute");
37113
+ }
37114
+ 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") {
37115
+ cats.add("write");
37116
+ }
37117
+ if (typeof obj.url === "string" || typeof obj.endpoint === "string" || typeof obj.host === "string" || /fetch|http|web|network|request/.test(name)) {
37118
+ cats.add("network");
37119
+ }
37120
+ if (cats.size === 0) {
37121
+ cats.add("execute");
37122
+ cats.add("network");
37123
+ }
37124
+ return [...cats];
37125
+ }
37126
+ function createBrokerPermissionHandler(opts) {
37127
+ const policy = opts.policy ?? defaultPermissionPolicy();
37128
+ return async (req) => {
37129
+ const categories = inferPermissionCategories(req.tool, req.input);
37130
+ const decision = resolveToolPermission(req.tool, categories, policy);
37131
+ if (decision.action === "allow") {
37132
+ const granted = isSessionGranted(
37133
+ req.tool,
37134
+ categories.length ? categories : ["execute"]
37135
+ );
37136
+ return granted ? { behavior: "allow", always: true } : { behavior: "allow" };
37137
+ }
37138
+ if (decision.action === "deny") {
37139
+ return { behavior: "deny", message: decision.reason };
37140
+ }
37141
+ if (!opts.onPermissionAsk) {
37142
+ return {
37143
+ behavior: "deny",
37144
+ message: `[zelari] no interactive approval available for "${req.tool}" (set ZELARI_AUTO=1 to auto-allow).`
37145
+ };
37146
+ }
37147
+ const ok = await opts.onPermissionAsk({
37148
+ toolName: req.tool,
37149
+ reason: decision.reason,
37150
+ categories,
37151
+ args: req.input
37152
+ });
37153
+ if (!ok) {
37154
+ return { behavior: "deny", message: "[zelari] denied by user" };
37155
+ }
37156
+ if (isSessionGranted(req.tool, categories.length ? categories : ["execute"])) {
37157
+ return { behavior: "allow", always: true };
37158
+ }
37159
+ return { behavior: "allow" };
37160
+ };
37161
+ }
37162
+ function createBrokerQuestionHandler(opts) {
37163
+ return async (req) => {
37164
+ if (!opts.onQuestion) return null;
37165
+ const answer = await opts.onQuestion({
37166
+ question: req.question,
37167
+ choices: req.choices,
37168
+ context: req.context
37169
+ });
37170
+ if (answer == null || !String(answer).trim()) {
37171
+ return { behavior: "deny", message: "[zelari] question cancelled" };
37172
+ }
37173
+ return { behavior: "allow", answer: String(answer).trim() };
37174
+ };
37175
+ }
37176
+ var init_brokerHandlers = __esm({
37177
+ "src/cli/mcp/brokerHandlers.ts"() {
37178
+ "use strict";
37179
+ init_toolPermissions();
37180
+ }
37181
+ });
37182
+
36505
37183
  // src/cli/kraken/planner.ts
36506
37184
  var planner_exports = {};
36507
37185
  __export(planner_exports, {
@@ -37208,6 +37886,230 @@ var init_tentacle = __esm({
37208
37886
  }
37209
37887
  });
37210
37888
 
37889
+ // src/cli/kraken/workbench.ts
37890
+ import { promises as fs20 } from "node:fs";
37891
+ import path35 from "node:path";
37892
+ function isWorkbenchEnabled(env = process.env) {
37893
+ const v = (env.ZELARI_KRAKEN_WORKBENCH ?? "1").trim().toLowerCase();
37894
+ if (v === "0" || v === "false" || v === "no" || v === "off") return false;
37895
+ return true;
37896
+ }
37897
+ function workbenchPath(cwd, graphId) {
37898
+ return path35.join(cwd, ".zelari", "radio", `workbench-${graphId}.md`);
37899
+ }
37900
+ function countByStatus2(nodes) {
37901
+ const out = { pending: 0, running: 0, done: 0, error: 0, skipped: 0 };
37902
+ for (const n of nodes) out[n.status] += 1;
37903
+ return out;
37904
+ }
37905
+ function statusEmoji(s) {
37906
+ switch (s) {
37907
+ case "pending":
37908
+ return "\u25CB";
37909
+ case "running":
37910
+ return "\u2191";
37911
+ case "done":
37912
+ return "\u2713";
37913
+ case "error":
37914
+ return "\u2717";
37915
+ case "skipped":
37916
+ return "\u2013";
37917
+ }
37918
+ }
37919
+ function formatMs(ms) {
37920
+ if (ms < 1e3) return `${ms}ms`;
37921
+ if (ms < 6e4) return `${(ms / 1e3).toFixed(1)}s`;
37922
+ return `${Math.floor(ms / 6e4)}m ${Math.floor(ms % 6e4 / 1e3)}s`;
37923
+ }
37924
+ function formatElapsed(ms) {
37925
+ const totalSec = Math.max(0, Math.floor(ms / 1e3));
37926
+ const hh = String(Math.floor(totalSec / 3600)).padStart(2, "0");
37927
+ const mm = String(Math.floor(totalSec % 3600 / 60)).padStart(2, "0");
37928
+ const ss = String(totalSec % 60).padStart(2, "0");
37929
+ return `${hh}:${mm}:${ss}`;
37930
+ }
37931
+ var DEBOUNCE_DEFAULT_MS, MAX_EVENTS, MAX_NODES_IN_TABLE, WorkbenchWriter;
37932
+ var init_workbench = __esm({
37933
+ "src/cli/kraken/workbench.ts"() {
37934
+ "use strict";
37935
+ init_dist();
37936
+ DEBOUNCE_DEFAULT_MS = 500;
37937
+ MAX_EVENTS = 30;
37938
+ MAX_NODES_IN_TABLE = 200;
37939
+ WorkbenchWriter = class {
37940
+ cwd;
37941
+ graphId;
37942
+ goal;
37943
+ enabled;
37944
+ debounceMs;
37945
+ startedAt;
37946
+ nodesById = /* @__PURE__ */ new Map();
37947
+ events = [];
37948
+ dirty = true;
37949
+ timer = null;
37950
+ lastWrite = null;
37951
+ constructor(opts) {
37952
+ this.cwd = opts.cwd;
37953
+ this.graphId = opts.graphId;
37954
+ this.goal = opts.goal;
37955
+ this.enabled = opts.enabled ?? isWorkbenchEnabled();
37956
+ this.debounceMs = opts.debounceMs ?? DEBOUNCE_DEFAULT_MS;
37957
+ this.startedAt = (/* @__PURE__ */ new Date()).toISOString();
37958
+ }
37959
+ /** Seed the writer with the planned nodes. Idempotent: later calls
37960
+ * merge — a node's status is not reset to `pending` if it's already
37961
+ * `running` / `done` / `error`. */
37962
+ setNodes(nodes) {
37963
+ if (!this.enabled) return;
37964
+ for (const n of nodes) {
37965
+ const existing = this.nodesById.get(n.id);
37966
+ if (!existing) this.nodesById.set(n.id, { ...n });
37967
+ else if (existing.status !== "pending") {
37968
+ this.nodesById.set(n.id, { ...existing, ...n, status: existing.status });
37969
+ } else {
37970
+ this.nodesById.set(n.id, { ...n });
37971
+ }
37972
+ }
37973
+ this.markDirty();
37974
+ }
37975
+ markStart(id, patch = {}) {
37976
+ if (!this.enabled) return;
37977
+ const n = this.nodesById.get(id) ?? { id, kind: "general", label: id, status: "pending" };
37978
+ this.nodesById.set(id, {
37979
+ ...n,
37980
+ ...patch,
37981
+ id,
37982
+ status: "running",
37983
+ startedAt: patch.startedAt ?? (/* @__PURE__ */ new Date()).toISOString()
37984
+ });
37985
+ this.events.push({ ts: (/* @__PURE__ */ new Date()).toISOString(), text: `node_start ${id} ${patch.kind ?? n.kind}` });
37986
+ this.markDirty();
37987
+ }
37988
+ markEnd(id, patch) {
37989
+ if (!this.enabled) return;
37990
+ const n = this.nodesById.get(id);
37991
+ if (!n) return;
37992
+ let verdict;
37993
+ let weaknessScore;
37994
+ if (patch.findings && patch.findings.length > 0) {
37995
+ const parsed = parsePersonaVerdict(patch.findings);
37996
+ verdict = parsed.verdict;
37997
+ weaknessScore = parsed.weaknessScore;
37998
+ }
37999
+ this.nodesById.set(id, {
38000
+ ...n,
38001
+ status: patch.status,
38002
+ ...patch.durationMs !== void 0 ? { durationMs: patch.durationMs } : {},
38003
+ ...patch.error ? { error: patch.error.slice(0, 200) } : {},
38004
+ ...Array.isArray(patch.deps) ? { deps: patch.deps } : {},
38005
+ ...verdict ? { verdict } : {},
38006
+ ...typeof weaknessScore === "number" ? { weaknessScore } : {},
38007
+ ...patch.model ? { model: patch.model } : {},
38008
+ endedAt: (/* @__PURE__ */ new Date()).toISOString()
38009
+ });
38010
+ this.events.push({
38011
+ ts: (/* @__PURE__ */ new Date()).toISOString(),
38012
+ 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)}` : ""}`
38013
+ });
38014
+ this.markDirty();
38015
+ }
38016
+ logEvent(text) {
38017
+ if (!this.enabled) return;
38018
+ this.events.push({ ts: (/* @__PURE__ */ new Date()).toISOString(), text });
38019
+ this.markDirty();
38020
+ }
38021
+ /** Mark "this set of node ids is running in parallel right now". The
38022
+ * workbench surfaces a "wave" line so a `tail -f` reader can see when
38023
+ * a new parallel wave starts. */
38024
+ markWave(ids) {
38025
+ if (!this.enabled) return;
38026
+ this.events.push({ ts: (/* @__PURE__ */ new Date()).toISOString(), text: `wave: ${ids.join(", ")}` });
38027
+ this.markDirty();
38028
+ }
38029
+ /** Force a synchronous-style flush. Returns the path written, or null
38030
+ * when the writer is disabled. */
38031
+ async flush() {
38032
+ if (!this.enabled) return null;
38033
+ if (this.timer) {
38034
+ clearTimeout(this.timer);
38035
+ this.timer = null;
38036
+ }
38037
+ return this.writeNow();
38038
+ }
38039
+ /** Cancel any pending debounced write. The next `flush()` will write. */
38040
+ close() {
38041
+ if (this.timer) {
38042
+ clearTimeout(this.timer);
38043
+ this.timer = null;
38044
+ }
38045
+ }
38046
+ // --- internals ------------------------------------------------------------
38047
+ markDirty() {
38048
+ if (!this.enabled) return;
38049
+ this.dirty = true;
38050
+ if (this.timer) return;
38051
+ this.timer = setTimeout(() => {
38052
+ this.timer = null;
38053
+ void this.writeNow().catch(() => {
38054
+ });
38055
+ }, this.debounceMs);
38056
+ }
38057
+ async writeNow() {
38058
+ if (!this.enabled) return null;
38059
+ if (!this.dirty && this.lastWrite) return this.lastWrite;
38060
+ const out = workbenchPath(this.cwd, this.graphId);
38061
+ await fs20.mkdir(path35.dirname(out), { recursive: true });
38062
+ const body = this.render();
38063
+ const tmp = `${out}.${process.pid}.${Date.now()}.tmp`;
38064
+ await fs20.writeFile(tmp, body, "utf8");
38065
+ await fs20.rename(tmp, out);
38066
+ this.dirty = false;
38067
+ this.lastWrite = Promise.resolve(out);
38068
+ return out;
38069
+ }
38070
+ render() {
38071
+ const counts = countByStatus2([...this.nodesById.values()]);
38072
+ const elapsed = formatElapsed(Date.now() - Date.parse(this.startedAt));
38073
+ const lines = [
38074
+ `# Kraken workbench`,
38075
+ ``,
38076
+ `**Goal:** ${this.goal}`,
38077
+ `**Graph id:** \`${this.graphId}\``,
38078
+ `**Started:** ${this.startedAt} \xB7 **Elapsed:** ${elapsed}`,
38079
+ ``,
38080
+ `## Progress: ${counts.done + counts.skipped}/${this.nodesById.size} \xB7 ${counts.running}\u2191 \xB7 ${counts.error}\u2717`,
38081
+ ``
38082
+ ];
38083
+ lines.push(`## Wave`);
38084
+ lines.push(``);
38085
+ lines.push(`| id | label | kind | deps | scope | status | verdict | weakness | model | duration |`);
38086
+ lines.push(`|----|-------|------|------|-------|--------|---------|----------|-------|----------|`);
38087
+ const sorted = [...this.nodesById.values()].sort((a, b) => a.id.localeCompare(b.id)).slice(0, MAX_NODES_IN_TABLE);
38088
+ for (const n of sorted) {
38089
+ const status = statusEmoji(n.status);
38090
+ const scope = n.scope ? n.scope.join(", ") : "";
38091
+ const dur = n.durationMs != null ? formatMs(n.durationMs) : "";
38092
+ const verdict = n.verdict ?? "";
38093
+ const weakness = typeof n.weaknessScore === "number" ? n.weaknessScore.toFixed(2) : "";
38094
+ const deps = n.deps && n.deps.length > 0 ? n.deps.join(", ") : "";
38095
+ lines.push(`| ${n.id} | ${n.label} | ${n.kind} | ${deps} | ${scope} | ${status} | ${verdict} | ${weakness} | ${n.model ?? ""} | ${dur} |`);
38096
+ }
38097
+ lines.push(``);
38098
+ lines.push(`## Events (latest ${MAX_EVENTS})`);
38099
+ lines.push(``);
38100
+ const tail = this.events.slice(-MAX_EVENTS);
38101
+ if (tail.length === 0) {
38102
+ lines.push(`(no events yet)`);
38103
+ } else {
38104
+ for (const e of tail) lines.push(`- ${e.ts.slice(11, 19)} ${e.text}`);
38105
+ }
38106
+ lines.push(``);
38107
+ return lines.join("\n");
38108
+ }
38109
+ };
38110
+ }
38111
+ });
38112
+
37211
38113
  // src/cli/kraken/weaknessMeter.ts
37212
38114
  var weaknessMeter_exports = {};
37213
38115
  __export(weaknessMeter_exports, {
@@ -37353,7 +38255,7 @@ __export(executor_exports, {
37353
38255
  thoroughnessForKind: () => thoroughnessForKind
37354
38256
  });
37355
38257
  import { existsSync as existsSync34 } from "node:fs";
37356
- import path35 from "node:path";
38258
+ import path36 from "node:path";
37357
38259
  function resolveMaxParallel(env = process.env) {
37358
38260
  const raw = env.ZELARI_KRAKEN_MAX_PARALLEL;
37359
38261
  if (raw === void 0 || raw === "") return DEFAULT_MAX_PARALLEL;
@@ -37411,7 +38313,7 @@ function isWorldModelGateEnabled(cwd, env = process.env, checksExists = defaultC
37411
38313
  }
37412
38314
  function defaultChecksExists(cwd) {
37413
38315
  try {
37414
- return existsSync34(path35.join(cwd, ".zelari", "world", "checks.json"));
38316
+ return existsSync34(path36.join(cwd, ".zelari", "world", "checks.json"));
37415
38317
  } catch {
37416
38318
  return false;
37417
38319
  }
@@ -37471,6 +38373,7 @@ var init_executor = __esm({
37471
38373
  init_worldModel();
37472
38374
  init_graphStatus();
37473
38375
  init_graphMemory();
38376
+ init_workbench();
37474
38377
  DEFAULT_MAX_PARALLEL = 12;
37475
38378
  DEFAULT_FIX_BUDGET = 3;
37476
38379
  DEFAULT_MAX_REVIEW_ROUNDS = 1;
@@ -37498,6 +38401,8 @@ var init_executor = __esm({
37498
38401
  runTentacleFn;
37499
38402
  mergeFn;
37500
38403
  backtestFn;
38404
+ /** Live workbench writer for this run (created once the graph starts). */
38405
+ wb = null;
37501
38406
  nodeRunState = /* @__PURE__ */ new Map();
37502
38407
  /** fix node id → id of the failed node it was spawned to repair. */
37503
38408
  repairs = /* @__PURE__ */ new Map();
@@ -37546,6 +38451,44 @@ var init_executor = __esm({
37546
38451
  this.mergeFn = opts.mergeFn ?? mergeKrakenWorktree;
37547
38452
  this.backtestFn = opts.backtestFn ?? runBacktest;
37548
38453
  }
38454
+ /** Map a graph node onto the workbench's node shape. */
38455
+ toWorkbenchNode(n) {
38456
+ return {
38457
+ id: n.id,
38458
+ kind: n.kind,
38459
+ label: n.label,
38460
+ status: n.status,
38461
+ ...n.scope && n.scope.length > 0 ? { scope: n.scope } : {},
38462
+ ...n.deps.length > 0 ? { deps: n.deps } : {}
38463
+ };
38464
+ }
38465
+ /**
38466
+ * Create (once) and seed the live workbench writer for this graph. This is
38467
+ * what makes the `.zelari/radio/workbench-<graphId>.md` file exist at all —
38468
+ * the desktop graph/tail tabs read that file, and before this wiring
38469
+ * nothing in the production flow ever constructed the writer, so the tabs
38470
+ * stayed empty no matter how much the graph ran.
38471
+ */
38472
+ initWorkbench(graph) {
38473
+ if (this.wb) return;
38474
+ this.wb = new WorkbenchWriter({
38475
+ cwd: this.parentCwd,
38476
+ graphId: graph.id,
38477
+ goal: this.goal ?? graph.id
38478
+ });
38479
+ this.wb.setNodes([...graph.nodes.values()].map((n) => this.toWorkbenchNode(n)));
38480
+ this.wb.logEvent(`graph_start ${graph.id} (${graph.nodes.size} nodes)`);
38481
+ }
38482
+ /** Mark every skipped node in the workbench. Cheap and idempotent enough
38483
+ * to call after each cascade-skip pass. */
38484
+ syncWorkbenchSkipped(graph) {
38485
+ for (const n of graph.nodes.values()) {
38486
+ if (n.status === "skipped") this.wb?.markEnd(n.id, { status: "skipped" });
38487
+ }
38488
+ }
38489
+ isReviewerKind(kind) {
38490
+ return kind === "verify" || kind === "spec" || kind === "conformance";
38491
+ }
37549
38492
  /** Execute the graph in place (mutates node statuses) until it settles. */
37550
38493
  async execute(graph) {
37551
38494
  if (this.fixBudgetOption === void 0) {
@@ -37581,6 +38524,7 @@ var init_executor = __esm({
37581
38524
  let iterations = 0;
37582
38525
  const inFlight = /* @__PURE__ */ new Map();
37583
38526
  startKrakenGraphLive(graph);
38527
+ this.initWorkbench(graph);
37584
38528
  while (!isSettled(graph)) {
37585
38529
  iterations += 1;
37586
38530
  if (iterations > maxIterations) {
@@ -37596,13 +38540,23 @@ var init_executor = __esm({
37596
38540
  if (admitted.length > 0) {
37597
38541
  for (const node of admitted) node.status = "running";
37598
38542
  updateKrakenGraphLive(graph);
38543
+ for (const node of admitted) {
38544
+ this.wb?.markStart(node.id, {
38545
+ kind: node.kind,
38546
+ label: node.label,
38547
+ ...node.scope && node.scope.length > 0 ? { scope: node.scope } : {},
38548
+ ...node.deps.length > 0 ? { deps: node.deps } : {}
38549
+ });
38550
+ }
38551
+ if (admitted.length > 0) this.wb?.markWave(admitted.map((n) => n.id));
37599
38552
  for (const node of admitted) inFlight.set(node.id, this.runNodeSafely(node, graph));
37600
38553
  }
37601
38554
  if (inFlight.size === 0) {
37602
- if (!this.skipBlockedNodes(graph)) {
37603
- break;
38555
+ if (this.skipBlockedNodes(graph)) {
38556
+ this.syncWorkbenchSkipped(graph);
38557
+ continue;
37604
38558
  }
37605
- continue;
38559
+ break;
37606
38560
  }
37607
38561
  const { id, res } = await Promise.race(inFlight.values());
37608
38562
  inFlight.delete(id);
@@ -37621,6 +38575,7 @@ var init_executor = __esm({
37621
38575
  for (const n of graph.nodes.values()) {
37622
38576
  if (n.status === "pending") n.status = "skipped";
37623
38577
  }
38578
+ this.syncWorkbenchSkipped(graph);
37624
38579
  }
37625
38580
  let backtest;
37626
38581
  const converged = !this.aborted && isConverged(graph);
@@ -37642,6 +38597,11 @@ var init_executor = __esm({
37642
38597
  });
37643
38598
  }
37644
38599
  endKrakenGraphLive(graph, converged);
38600
+ this.wb?.logEvent(
38601
+ converged ? "graph_converged" : `graph_failed: ${this.aborted ? "cancelled" : failedNodeIds(graph).join(", ") || "none"}`
38602
+ );
38603
+ await this.wb?.flush();
38604
+ this.wb?.close();
37645
38605
  await saveGraphSnapshot(
37646
38606
  this.parentCwd,
37647
38607
  toGraphSnapshot(graph, {
@@ -37934,6 +38894,13 @@ ${upstream}` : node.prompt,
37934
38894
  node.status = "done";
37935
38895
  node.result = res.result;
37936
38896
  this.radio("node_end", { description: node.label, agent: node.kind, ok: true });
38897
+ this.wb?.markEnd(node.id, {
38898
+ status: "done",
38899
+ durationMs: this.durationsMs.get(node.id),
38900
+ // Reviewers surface verdict + Bennett weakness; writers don't.
38901
+ ...this.isReviewerKind(node.kind) && typeof node.result === "string" && node.result.length > 0 ? { findings: node.result } : {},
38902
+ ...res.model && res.model !== "n/a" ? { model: res.model } : {}
38903
+ });
37937
38904
  this.reconcileRepairedNode(graph, node);
37938
38905
  if (node.kind === "verify") this.applyVerifyVerdict(graph, node);
37939
38906
  return;
@@ -37947,6 +38914,11 @@ ${upstream}` : node.prompt,
37947
38914
  detail: res.error,
37948
38915
  ok: false
37949
38916
  });
38917
+ this.wb?.markEnd(node.id, {
38918
+ status: "error",
38919
+ error: res.error,
38920
+ durationMs: this.durationsMs.get(node.id)
38921
+ });
37950
38922
  return;
37951
38923
  }
37952
38924
  if (res.cancelled === false) {
@@ -37957,6 +38929,11 @@ ${upstream}` : node.prompt,
37957
38929
  detail: res.error,
37958
38930
  ok: false
37959
38931
  });
38932
+ this.wb?.markEnd(node.id, {
38933
+ status: "error",
38934
+ error: res.error,
38935
+ durationMs: this.durationsMs.get(node.id)
38936
+ });
37960
38937
  return;
37961
38938
  }
37962
38939
  if (node.retryCount < node.maxRetries) {
@@ -37968,6 +38945,7 @@ ${upstream}` : node.prompt,
37968
38945
  detail: `retry ${node.retryCount}/${node.maxRetries}: ${res.error}`,
37969
38946
  ok: false
37970
38947
  });
38948
+ this.wb?.markEnd(node.id, { status: "pending" });
37971
38949
  return;
37972
38950
  }
37973
38951
  if (this.fixBudgetRemaining > 0 && node.kind !== "fix") {
@@ -37980,6 +38958,11 @@ ${upstream}` : node.prompt,
37980
38958
  detail: `spawned to address failure of "${node.label}": ${res.error}`,
37981
38959
  ok: false
37982
38960
  });
38961
+ this.wb?.markEnd(node.id, {
38962
+ status: "error",
38963
+ error: res.error,
38964
+ durationMs: this.durationsMs.get(node.id)
38965
+ });
37983
38966
  node.status = "error";
37984
38967
  return;
37985
38968
  }
@@ -37990,6 +38973,11 @@ ${upstream}` : node.prompt,
37990
38973
  detail: res.error,
37991
38974
  ok: false
37992
38975
  });
38976
+ this.wb?.markEnd(node.id, {
38977
+ status: "error",
38978
+ error: res.error,
38979
+ durationMs: this.durationsMs.get(node.id)
38980
+ });
37993
38981
  }
37994
38982
  /**
37995
38983
  * A `fix` node just completed the work its failed predecessor could not.
@@ -38019,6 +39007,10 @@ ${upstream}` : node.prompt,
38019
39007
  detail: `repaired by ${fixNode.id}`,
38020
39008
  ok: true
38021
39009
  });
39010
+ this.wb?.markEnd(failed.id, {
39011
+ status: "done",
39012
+ durationMs: this.durationsMs.get(failed.id)
39013
+ });
38022
39014
  }
38023
39015
  /**
38024
39016
  * Act on what a completed `verify` node concluded.
@@ -38153,6 +39145,7 @@ ${findings || "(the reviewer reported FAIL without detail)"}`,
38153
39145
  maxRetries: verify.maxRetries
38154
39146
  };
38155
39147
  graph.nodes.set(reVerifyId, reVerifyNode);
39148
+ this.wb?.setNodes([this.toWorkbenchNode(reworkNode), this.toWorkbenchNode(reVerifyNode)]);
38156
39149
  for (const other of graph.nodes.values()) {
38157
39150
  if (other.id === reworkId || other.id === reVerifyId) continue;
38158
39151
  if (other.deps.includes(verify.id)) {
@@ -38192,6 +39185,7 @@ ${failed.error ?? "unknown error"}`,
38192
39185
  // no further retries — one fix attempt per failed node in v1
38193
39186
  };
38194
39187
  graph.nodes.set(fixId, fixNode);
39188
+ this.wb?.setNodes([this.toWorkbenchNode(fixNode)]);
38195
39189
  this.repairs.set(fixId, failed.id);
38196
39190
  for (const other of graph.nodes.values()) {
38197
39191
  if (other.id === fixId) continue;
@@ -38658,10 +39652,10 @@ var init_prereqChecks = __esm({
38658
39652
 
38659
39653
  // src/cli/plugins/prefs.ts
38660
39654
  import { existsSync as existsSync36, readFileSync as readFileSync29, writeFileSync as writeFileSync18, mkdirSync as mkdirSync15 } from "node:fs";
38661
- import path38 from "node:path";
39655
+ import path39 from "node:path";
38662
39656
  import os9 from "node:os";
38663
39657
  function getPluginPrefsPath() {
38664
- return process.env.ZELARI_PLUGINS_PREFS_FILE ?? path38.join(os9.homedir(), ".tmp", "zelari-code", "plugins.json");
39658
+ return process.env.ZELARI_PLUGINS_PREFS_FILE ?? path39.join(os9.homedir(), ".tmp", "zelari-code", "plugins.json");
38665
39659
  }
38666
39660
  function getPluginPrefs() {
38667
39661
  const file2 = getPluginPrefsPath();
@@ -38682,7 +39676,7 @@ function getPluginPrefs() {
38682
39676
  }
38683
39677
  function writePluginPrefs(prefs) {
38684
39678
  const file2 = getPluginPrefsPath();
38685
- mkdirSync15(path38.dirname(file2), { recursive: true });
39679
+ mkdirSync15(path39.dirname(file2), { recursive: true });
38686
39680
  writeFileSync18(file2, JSON.stringify(prefs, null, 2), {
38687
39681
  encoding: "utf-8",
38688
39682
  mode: 384
@@ -38719,7 +39713,7 @@ __export(registry_exports, {
38719
39713
  isBinaryOnPath: () => isBinaryOnPath
38720
39714
  });
38721
39715
  import { existsSync as existsSync37 } from "node:fs";
38722
- import path39 from "node:path";
39716
+ import path40 from "node:path";
38723
39717
  function detectLocalBin(bin) {
38724
39718
  return (cwd) => {
38725
39719
  try {
@@ -38737,7 +39731,7 @@ function isBinaryOnPath(bin, opts = {}) {
38737
39731
  const platform = opts.platform ?? process.platform;
38738
39732
  const exists = opts.exists ?? existsSync37;
38739
39733
  const pathEnv = opts.pathEnv ?? process.env.PATH ?? "";
38740
- const pathMod = platform === "win32" ? path39.win32 : path39.posix;
39734
+ const pathMod = platform === "win32" ? path40.win32 : path40.posix;
38741
39735
  const sep2 = platform === "win32" ? ";" : ":";
38742
39736
  const dirs = pathEnv.split(sep2).filter((d) => d.length > 0);
38743
39737
  const candidates = [bin];
@@ -38875,7 +39869,11 @@ __export(atMentions_exports, {
38875
39869
  hasAtMentions: () => hasAtMentions
38876
39870
  });
38877
39871
  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";
39872
+ import { basename as basename3, isAbsolute as isAbsolute2, relative as relative3, resolve, sep } from "node:path";
39873
+ function isImagePath(abs) {
39874
+ const ext = abs.split(".").pop()?.toLowerCase() ?? "";
39875
+ return ext in IMAGE_MIME_BY_EXT;
39876
+ }
38879
39877
  function isProbablyText(name, head) {
38880
39878
  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
39879
  name
@@ -38893,32 +39891,29 @@ function underRoot(abs, root) {
38893
39891
  function extractAtMentions(text) {
38894
39892
  const out = [];
38895
39893
  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
- }
39894
+ const collect = (re) => {
39895
+ re.lastIndex = 0;
39896
+ let m;
39897
+ while ((m = re.exec(text)) !== null) {
39898
+ const token = (m[2] ?? "").trim().replace(/^@/, "");
39899
+ if (!token || token.includes("@")) continue;
39900
+ if (re === AT_PATH_RE && /^[a-zA-Z]$/.test(token) && text.charAt(m.index + m[0].length) === ":") {
39901
+ continue;
39902
+ }
39903
+ const key = token.replace(/\\/g, "/").toLowerCase();
39904
+ if (seen.has(key)) continue;
39905
+ seen.add(key);
39906
+ out.push(token);
39907
+ }
39908
+ };
39909
+ collect(AT_PATH_RE);
39910
+ collect(AT_WIN_ABS_RE);
39911
+ collect(AT_POSIX_ABS_RE);
38906
39912
  return out;
38907
39913
  }
38908
39914
  function resolveMention(token, cwd) {
38909
39915
  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)) {
39916
+ if (!underRoot(abs, cwd) && !isImagePath(abs)) {
38922
39917
  return {
38923
39918
  raw: token,
38924
39919
  path: token,
@@ -38967,6 +39962,28 @@ function resolveMention(token, cwd) {
38967
39962
  note: `too large (${Math.round(st.size / 1024)} KB) \u2014 path only`
38968
39963
  };
38969
39964
  }
39965
+ const ext = abs.split(".").pop()?.toLowerCase() ?? "";
39966
+ const mime = IMAGE_MIME_BY_EXT[ext];
39967
+ if (mime) {
39968
+ if (st.size > ATTACH_IMAGE_MAX_BYTES) {
39969
+ return {
39970
+ raw: token,
39971
+ path: rel2,
39972
+ absolute: abs,
39973
+ isDir: false,
39974
+ note: `image too large (${Math.round(st.size / 1024)} KB) \u2014 path only`
39975
+ };
39976
+ }
39977
+ const dataBase64 = readFileSync31(abs).toString("base64");
39978
+ return {
39979
+ raw: token,
39980
+ path: rel2,
39981
+ absolute: abs,
39982
+ isDir: false,
39983
+ text: `[Immagine: ${rel2}]`,
39984
+ image: { mime, dataBase64, alt: basename3(abs) }
39985
+ };
39986
+ }
38970
39987
  try {
38971
39988
  const buf = readFileSync31(abs);
38972
39989
  const head = buf.subarray(0, 800).toString("utf8");
@@ -39012,6 +40029,10 @@ function expandAtMentions(userText, cwd = process.cwd()) {
39012
40029
  }
39013
40030
  const blocks = hits.map((h) => {
39014
40031
  const label = h.path || h.raw;
40032
+ if (h.image) {
40033
+ const kb = Math.round(h.image.dataBase64.length * 3 / 4 / 1024);
40034
+ return `--- Image: ${label} (${h.image.mime}, ~${kb} KB) ---`;
40035
+ }
39015
40036
  if (h.text != null && h.text.length > 0) {
39016
40037
  return `--- File: ${label} ---
39017
40038
  ${h.text}
@@ -39029,13 +40050,28 @@ ${blocks.join("\n\n")}`;
39029
40050
  function hasAtMentions(text) {
39030
40051
  return extractAtMentions(text).length > 0;
39031
40052
  }
39032
- var ATTACH_TEXT_MAX, ATTACH_FILE_MAX_BYTES, AT_PATH_RE;
40053
+ 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
40054
  var init_atMentions = __esm({
39034
40055
  "src/cli/atMentions.ts"() {
39035
40056
  "use strict";
39036
40057
  ATTACH_TEXT_MAX = 48e3;
39037
40058
  ATTACH_FILE_MAX_BYTES = 512e3;
40059
+ ATTACH_IMAGE_MAX_BYTES = 8 * 1024 * 1024;
40060
+ IMAGE_MIME_BY_EXT = {
40061
+ jpg: "image/jpeg",
40062
+ jpeg: "image/jpeg",
40063
+ png: "image/png",
40064
+ gif: "image/gif",
40065
+ webp: "image/webp",
40066
+ bmp: "image/bmp"
40067
+ };
39038
40068
  AT_PATH_RE = /(^|[\s([{])@((?:\.{1,2}\/)?[A-Za-z0-9_.+-]+(?:[\\/][A-Za-z0-9_.+-]+)*)/g;
40069
+ BS_CHAR = String.fromCharCode(92);
40070
+ AT_WIN_ABS_RE = new RegExp(
40071
+ "(^|[" + BS_CHAR + "s([{])(@[A-Za-z]:[" + BS_CHAR + BS_CHAR + "/][^" + BS_CHAR + "s@]+)",
40072
+ "g"
40073
+ );
40074
+ AT_POSIX_ABS_RE = /(^|[\s([{])@(\/[^\s@]+)/g;
39039
40075
  }
39040
40076
  });
39041
40077
 
@@ -39046,10 +40082,10 @@ __export(triggerLock_exports, {
39046
40082
  lockPath: () => lockPath,
39047
40083
  releaseLock: () => releaseLock
39048
40084
  });
39049
- import { promises as fs27 } from "node:fs";
39050
- import * as path44 from "node:path";
40085
+ import { promises as fs28 } from "node:fs";
40086
+ import * as path45 from "node:path";
39051
40087
  function lockPath(projectRoot) {
39052
- return path44.join(projectRoot, ".zelari", "trigger.lock");
40088
+ return path45.join(projectRoot, ".zelari", "trigger.lock");
39053
40089
  }
39054
40090
  function isPidAlive(pid) {
39055
40091
  try {
@@ -39062,10 +40098,10 @@ function isPidAlive(pid) {
39062
40098
  }
39063
40099
  async function acquireLock(projectRoot, now = () => /* @__PURE__ */ new Date()) {
39064
40100
  const lp = lockPath(projectRoot);
39065
- const dir = path44.dirname(lp);
39066
- await fs27.mkdir(dir, { recursive: true });
40101
+ const dir = path45.dirname(lp);
40102
+ await fs28.mkdir(dir, { recursive: true });
39067
40103
  try {
39068
- const raw = await fs27.readFile(lp, "utf8");
40104
+ const raw = await fs28.readFile(lp, "utf8");
39069
40105
  const existing = JSON.parse(raw);
39070
40106
  if (existing.pid && isPidAlive(existing.pid)) {
39071
40107
  return { acquired: false, heldBy: existing.pid, lockPath: lp };
@@ -39076,13 +40112,13 @@ async function acquireLock(projectRoot, now = () => /* @__PURE__ */ new Date())
39076
40112
  pid: process.pid,
39077
40113
  acquiredAt: now().toISOString()
39078
40114
  };
39079
- await fs27.writeFile(lp, JSON.stringify(payload, null, 2) + "\n", "utf8");
40115
+ await fs28.writeFile(lp, JSON.stringify(payload, null, 2) + "\n", "utf8");
39080
40116
  return { acquired: true, lockPath: lp };
39081
40117
  }
39082
40118
  async function releaseLock(projectRoot) {
39083
40119
  const lp = lockPath(projectRoot);
39084
40120
  try {
39085
- await fs27.unlink(lp);
40121
+ await fs28.unlink(lp);
39086
40122
  } catch {
39087
40123
  }
39088
40124
  }
@@ -39344,6 +40380,272 @@ var init_desktopConfig = __esm({
39344
40380
  }
39345
40381
  });
39346
40382
 
40383
+ // src/cli/mcp/mcpPermissionServer.ts
40384
+ import { createInterface } from "node:readline";
40385
+ import { randomUUID as randomUUID7 } from "node:crypto";
40386
+ function startPermissionMcpServer(opts) {
40387
+ const socketPath = opts.socketPath.trim();
40388
+ const requestTimeoutMs = opts.requestTimeoutMs ?? PERMISSION_BROKER_DEFAULT_TIMEOUT_MS;
40389
+ const input = opts.input ?? process.stdin;
40390
+ const output = opts.output ?? process.stdout;
40391
+ input.on?.("error", () => {
40392
+ });
40393
+ output.on?.("error", () => {
40394
+ });
40395
+ let closedResolve = () => {
40396
+ };
40397
+ const closed = new Promise((resolve3) => {
40398
+ closedResolve = resolve3;
40399
+ });
40400
+ let stopped = false;
40401
+ const send = (obj) => {
40402
+ if (stopped || output.destroyed) return;
40403
+ output.write(JSON.stringify(obj) + "\n");
40404
+ };
40405
+ const rl = createInterface({ input, crlfDelay: Infinity });
40406
+ rl.on("line", (line) => {
40407
+ const text = line.trim();
40408
+ if (!text) return;
40409
+ let msg;
40410
+ try {
40411
+ msg = JSON.parse(text);
40412
+ } catch {
40413
+ return;
40414
+ }
40415
+ if (typeof msg.id === "undefined") return;
40416
+ void handleRpc(msg).then((out) => {
40417
+ send({
40418
+ jsonrpc: "2.0",
40419
+ id: msg.id,
40420
+ ...out ?? {}
40421
+ });
40422
+ }).catch((err) => {
40423
+ send({
40424
+ jsonrpc: "2.0",
40425
+ id: msg.id,
40426
+ error: {
40427
+ code: -32603,
40428
+ message: `internal error: ${err instanceof Error ? err.message : String(err)}`
40429
+ }
40430
+ });
40431
+ });
40432
+ });
40433
+ rl.on("close", () => {
40434
+ closedResolve();
40435
+ });
40436
+ async function handleRpc(msg) {
40437
+ const method = msg.method ?? "";
40438
+ if (typeof msg.id === "undefined") {
40439
+ return { result: null };
40440
+ }
40441
+ if (method === "initialize") {
40442
+ const requested = typeof msg.params?.protocolVersion === "string" ? msg.params.protocolVersion : PERMISSION_MCP_PROTOCOL_VERSION;
40443
+ return {
40444
+ result: {
40445
+ protocolVersion: requested,
40446
+ capabilities: { tools: {} },
40447
+ serverInfo: {
40448
+ name: "zelari-permission-mcp",
40449
+ version: getCurrentVersion()
40450
+ }
40451
+ }
40452
+ };
40453
+ }
40454
+ if (method === "tools/list") {
40455
+ return { result: { tools: PERMISSION_MCP_TOOLS } };
40456
+ }
40457
+ if (method === "tools/call") {
40458
+ const params = msg.params ?? {};
40459
+ const name = typeof params.name === "string" ? params.name : "";
40460
+ const args = params.arguments && typeof params.arguments === "object" ? params.arguments : {};
40461
+ const out = name === "approve" ? await callApprove(args, requestTimeoutMs) : name === "ask_user" ? await callAskUser(args, requestTimeoutMs) : { error: { code: -32602, message: `unknown tool: ${name}` } };
40462
+ if (out && typeof out === "object" && "error" in out && out.error) {
40463
+ return { error: out.error };
40464
+ }
40465
+ return { result: out };
40466
+ }
40467
+ return { error: { code: -32601, message: `method not found: ${method}` } };
40468
+ }
40469
+ async function callApprove(args, timeoutMs) {
40470
+ const toolName = typeof args.tool_name === "string" ? args.tool_name : "";
40471
+ if (!toolName) {
40472
+ return { error: { code: -32602, message: "approve requires a string tool_name" } };
40473
+ }
40474
+ const input2 = args.input ?? {};
40475
+ const suggestions = Array.isArray(args.permission_suggestions) ? args.permission_suggestions : Array.isArray(args.suggestions) ? args.suggestions : void 0;
40476
+ try {
40477
+ const res = await requestBrokerAsk(
40478
+ socketPath,
40479
+ {
40480
+ t: "ask",
40481
+ id: randomUUID7(),
40482
+ kind: "permission",
40483
+ tool: toolName,
40484
+ input: input2,
40485
+ toolUseId: typeof args.tool_use_id === "string" ? args.tool_use_id : void 0,
40486
+ suggestions
40487
+ },
40488
+ { requestTimeoutMs: timeoutMs }
40489
+ );
40490
+ const result = res.behavior === "allow" ? {
40491
+ behavior: "allow",
40492
+ updatedInput: input2,
40493
+ ...res.always === true && suggestions && suggestions.length > 0 ? { updatedPermissions: suggestions } : {}
40494
+ } : {
40495
+ behavior: "deny",
40496
+ message: res.message ?? "[zelari] permission denied (no reason given)"
40497
+ };
40498
+ return textResult(JSON.stringify(result));
40499
+ } catch (err) {
40500
+ return textResult(
40501
+ JSON.stringify({
40502
+ behavior: "deny",
40503
+ message: `[zelari] permission broker unavailable: ${err instanceof Error ? err.message : String(err)}`
40504
+ })
40505
+ );
40506
+ }
40507
+ }
40508
+ async function callAskUser(args, timeoutMs) {
40509
+ const question = typeof args.question === "string" ? args.question : "";
40510
+ if (!question) {
40511
+ return { error: { code: -32602, message: "ask_user requires a string question" } };
40512
+ }
40513
+ const choices = Array.isArray(args.choices) ? args.choices.filter((c) => typeof c === "string") : void 0;
40514
+ const context = typeof args.context === "string" ? args.context : void 0;
40515
+ try {
40516
+ const res = await requestBrokerAsk(
40517
+ socketPath,
40518
+ {
40519
+ t: "ask",
40520
+ id: randomUUID7(),
40521
+ kind: "question",
40522
+ question,
40523
+ choices,
40524
+ context
40525
+ },
40526
+ { requestTimeoutMs: timeoutMs }
40527
+ );
40528
+ const text = res.behavior === "allow" && typeof res.answer === "string" ? res.answer : res.message ?? "No answer was given \u2014 use your best judgment.";
40529
+ return textResult(text);
40530
+ } catch (err) {
40531
+ return textResult(
40532
+ JSON.stringify({
40533
+ behavior: "deny",
40534
+ message: `[zelari] permission broker unavailable: ${err instanceof Error ? err.message : String(err)}`
40535
+ })
40536
+ );
40537
+ }
40538
+ }
40539
+ function textResult(text) {
40540
+ return {
40541
+ content: [{ type: "text", text }]
40542
+ };
40543
+ }
40544
+ return Promise.resolve({
40545
+ closed,
40546
+ stop: async () => {
40547
+ if (stopped) return;
40548
+ stopped = true;
40549
+ rl.close();
40550
+ closedResolve();
40551
+ }
40552
+ });
40553
+ }
40554
+ var PERMISSION_MCP_PROTOCOL_VERSION, APPROVE_TOOL, ASK_USER_TOOL, PERMISSION_MCP_TOOLS;
40555
+ var init_mcpPermissionServer = __esm({
40556
+ "src/cli/mcp/mcpPermissionServer.ts"() {
40557
+ "use strict";
40558
+ init_updater();
40559
+ init_permissionBroker();
40560
+ PERMISSION_MCP_PROTOCOL_VERSION = "2025-03-26";
40561
+ APPROVE_TOOL = {
40562
+ name: "approve",
40563
+ description: "Ask the human to approve or deny a tool call from an external agent. Returns JSON {behavior: 'allow'|'deny', updatedInput?, updatedPermissions?}.",
40564
+ inputSchema: {
40565
+ type: "object",
40566
+ properties: {
40567
+ tool_name: { type: "string", description: "Name of the tool the agent wants to run" },
40568
+ input: { type: "object", description: "Arguments the agent wants to pass" },
40569
+ tool_use_id: { type: "string", description: "Agent-side tool use id (echoed back)" },
40570
+ permission_suggestions: {
40571
+ type: "array",
40572
+ items: { type: "object" },
40573
+ description: "Permission rules the CLI suggests; returned as updatedPermissions only when the user allows permanently"
40574
+ },
40575
+ suggestions: {
40576
+ type: "array",
40577
+ items: { type: "object" },
40578
+ description: "Alias of permission_suggestions (accepted for compat)"
40579
+ }
40580
+ },
40581
+ required: ["tool_name", "input"]
40582
+ }
40583
+ };
40584
+ ASK_USER_TOOL = {
40585
+ name: "ask_user",
40586
+ description: "Ask the human a structured question. Returns JSON {answer: '<text>'} or {behavior: 'deny', message} if cancelled/unavailable.",
40587
+ inputSchema: {
40588
+ type: "object",
40589
+ properties: {
40590
+ question: { type: "string", description: "One focused question" },
40591
+ choices: {
40592
+ type: "array",
40593
+ items: { type: "string" },
40594
+ description: "2\u20136 concrete options; the human may still type a free answer"
40595
+ },
40596
+ context: { type: "string", description: "Why this choice matters (one short line)" }
40597
+ },
40598
+ required: ["question"]
40599
+ }
40600
+ };
40601
+ PERMISSION_MCP_TOOLS = [
40602
+ APPROVE_TOOL,
40603
+ ASK_USER_TOOL
40604
+ ];
40605
+ }
40606
+ });
40607
+
40608
+ // src/cli/mcp/permissionCli.ts
40609
+ var permissionCli_exports = {};
40610
+ __export(permissionCli_exports, {
40611
+ runPermissionCli: () => runPermissionCli
40612
+ });
40613
+ async function runPermissionCli(socketPath) {
40614
+ const server = await startPermissionMcpServer({ socketPath });
40615
+ process.stderr.write(
40616
+ `[zelari-code --permission-mcp] listening on ${socketPath}
40617
+ `
40618
+ );
40619
+ let exiting = false;
40620
+ const shutdown2 = async (code) => {
40621
+ if (exiting) return;
40622
+ exiting = true;
40623
+ try {
40624
+ await server.stop();
40625
+ } catch {
40626
+ }
40627
+ process.exit(code);
40628
+ };
40629
+ process.stdin.on("close", () => {
40630
+ void shutdown2(0);
40631
+ });
40632
+ process.on("SIGINT", () => {
40633
+ void shutdown2(0);
40634
+ });
40635
+ process.on("SIGTERM", () => {
40636
+ void shutdown2(0);
40637
+ });
40638
+ process.stdin.resume?.();
40639
+ await server.closed;
40640
+ await shutdown2(0);
40641
+ }
40642
+ var init_permissionCli = __esm({
40643
+ "src/cli/mcp/permissionCli.ts"() {
40644
+ "use strict";
40645
+ init_mcpPermissionServer();
40646
+ }
40647
+ });
40648
+
39347
40649
  // src/cli/companion/config.ts
39348
40650
  import {
39349
40651
  existsSync as existsSync43,
@@ -39370,12 +40672,12 @@ function ensureHome() {
39370
40672
  }
39371
40673
  }
39372
40674
  function loadCompanionConfig() {
39373
- const path47 = getCompanionConfigPath();
39374
- if (!existsSync43(path47)) {
40675
+ const path48 = getCompanionConfigPath();
40676
+ if (!existsSync43(path48)) {
39375
40677
  return { projects: [] };
39376
40678
  }
39377
40679
  try {
39378
- const raw = JSON.parse(readFileSync34(path47, "utf8"));
40680
+ const raw = JSON.parse(readFileSync34(path48, "utf8"));
39379
40681
  const projects = Array.isArray(raw.projects) ? raw.projects.filter(
39380
40682
  (p3) => p3 && typeof p3.path === "string" && p3.path.trim() && typeof (p3.id ?? p3.name) === "string"
39381
40683
  ).map((p3) => ({
@@ -39413,16 +40715,16 @@ function loadOrCreateToken(explicit) {
39413
40715
  return { token: explicit.trim(), created: false };
39414
40716
  }
39415
40717
  ensureHome();
39416
- const path47 = getCompanionTokenPath();
39417
- if (existsSync43(path47)) {
39418
- const t = readFileSync34(path47, "utf8").trim();
40718
+ const path48 = getCompanionTokenPath();
40719
+ if (existsSync43(path48)) {
40720
+ const t = readFileSync34(path48, "utf8").trim();
39419
40721
  if (t) return { token: t, created: false };
39420
40722
  }
39421
40723
  const token = randomBytes4(24).toString("base64url");
39422
- writeFileSync21(path47, token + "\n", "utf8");
40724
+ writeFileSync21(path48, token + "\n", "utf8");
39423
40725
  try {
39424
- const fs29 = __require("node:fs");
39425
- fs29.chmodSync?.(path47, 384);
40726
+ const fs30 = __require("node:fs");
40727
+ fs30.chmodSync?.(path48, 384);
39426
40728
  } catch {
39427
40729
  }
39428
40730
  return { token, created: true };
@@ -39447,17 +40749,17 @@ function mergeProjects(cfg, extraPaths) {
39447
40749
  byId.set(p3.id, p3);
39448
40750
  }
39449
40751
  for (const raw of extraPaths) {
39450
- const path47 = raw.trim();
39451
- if (!path47) continue;
39452
- let id = slugFromPath(path47);
40752
+ const path48 = raw.trim();
40753
+ if (!path48) continue;
40754
+ let id = slugFromPath(path48);
39453
40755
  let n = 2;
39454
- while (byId.has(id) && byId.get(id).path !== path47) {
39455
- id = `${slugFromPath(path47)}-${n++}`;
40756
+ while (byId.has(id) && byId.get(id).path !== path48) {
40757
+ id = `${slugFromPath(path48)}-${n++}`;
39456
40758
  }
39457
40759
  byId.set(id, {
39458
40760
  id,
39459
- name: slugFromPath(path47),
39460
- path: path47
40761
+ name: slugFromPath(path48),
40762
+ path: path48
39461
40763
  });
39462
40764
  }
39463
40765
  return [...byId.values()];
@@ -39505,9 +40807,9 @@ var init_config = __esm({
39505
40807
  });
39506
40808
 
39507
40809
  // 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";
40810
+ import { spawn as spawn12 } from "node:child_process";
40811
+ import { createInterface as createInterface2 } from "node:readline";
40812
+ import { randomUUID as randomUUID8 } from "node:crypto";
39511
40813
  import { writeFileSync as writeFileSync22, unlinkSync as unlinkSync2 } from "node:fs";
39512
40814
  import { join as join36 } from "node:path";
39513
40815
  import { tmpdir as tmpdir3 } from "node:os";
@@ -39576,7 +40878,7 @@ var init_runManager = __esm({
39576
40878
  }
39577
40879
  const prompt = args.prompt?.trim();
39578
40880
  if (!prompt) return { ok: false, error: "prompt is required" };
39579
- const id = randomUUID7();
40881
+ const id = randomUUID8();
39580
40882
  const mode = (args.mode || "kraken").toLowerCase();
39581
40883
  const phase2 = (args.phase || "build").toLowerCase();
39582
40884
  const run = {
@@ -39621,7 +40923,7 @@ var init_runManager = __esm({
39621
40923
  historyFile = void 0;
39622
40924
  }
39623
40925
  }
39624
- const child = spawn11(process.execPath, argv, {
40926
+ const child = spawn12(process.execPath, argv, {
39625
40927
  cwd: args.cwd,
39626
40928
  env: {
39627
40929
  ...process.env,
@@ -39638,7 +40940,7 @@ var init_runManager = __esm({
39638
40940
  message: `[companion] run ${id} started mode=${mode} phase=${phase2} cwd=${args.cwd}`
39639
40941
  });
39640
40942
  if (child.stdout) {
39641
- const rl = createInterface({ input: child.stdout });
40943
+ const rl = createInterface2({ input: child.stdout });
39642
40944
  rl.on("line", (line) => {
39643
40945
  const t = line.trim();
39644
40946
  if (!t) return;
@@ -39739,7 +41041,7 @@ __export(serve_exports, {
39739
41041
  parseServeFlags: () => parseServeFlags,
39740
41042
  runCompanionServe: () => runCompanionServe
39741
41043
  });
39742
- import { createServer } from "node:http";
41044
+ import { createServer as createServer2 } from "node:http";
39743
41045
  import { existsSync as existsSync44 } from "node:fs";
39744
41046
  import { resolve as resolve2 } from "node:path";
39745
41047
  function readBody(req, max = 2e6) {
@@ -39759,7 +41061,7 @@ function readBody(req, max = 2e6) {
39759
41061
  req.on("error", reject);
39760
41062
  });
39761
41063
  }
39762
- function sendJson(res, status, body, extraHeaders) {
41064
+ function sendJson2(res, status, body, extraHeaders) {
39763
41065
  const data = JSON.stringify(body);
39764
41066
  res.writeHead(status, {
39765
41067
  "content-type": "application/json; charset=utf-8",
@@ -39823,7 +41125,7 @@ async function runCompanionServe(opts = {}) {
39823
41125
  );
39824
41126
  }
39825
41127
  const runs = new RunManager();
39826
- const server = createServer(async (req, res) => {
41128
+ const server = createServer2(async (req, res) => {
39827
41129
  if (req.method === "OPTIONS") {
39828
41130
  res.writeHead(204, {
39829
41131
  "access-control-allow-origin": "*",
@@ -39834,10 +41136,10 @@ async function runCompanionServe(opts = {}) {
39834
41136
  return;
39835
41137
  }
39836
41138
  const url2 = parseUrl(req);
39837
- const path47 = url2.pathname.replace(/\/+$/, "") || "/";
41139
+ const path48 = url2.pathname.replace(/\/+$/, "") || "/";
39838
41140
  try {
39839
- if (req.method === "GET" && (path47 === "/health" || path47 === "/v1/health")) {
39840
- sendJson(res, 200, {
41141
+ if (req.method === "GET" && (path48 === "/health" || path48 === "/v1/health")) {
41142
+ sendJson2(res, 200, {
39841
41143
  ok: true,
39842
41144
  service: "zelari-companion",
39843
41145
  version: getCurrentVersion(),
@@ -39848,19 +41150,19 @@ async function runCompanionServe(opts = {}) {
39848
41150
  });
39849
41151
  return;
39850
41152
  }
39851
- if (path47.startsWith("/v1")) {
41153
+ if (path48.startsWith("/v1")) {
39852
41154
  if (!tokenMatches(token, getBearer(req))) {
39853
- sendJson(res, 401, { ok: false, error: "unauthorized" });
41155
+ sendJson2(res, 401, { ok: false, error: "unauthorized" });
39854
41156
  return;
39855
41157
  }
39856
41158
  }
39857
- if (req.method === "GET" && path47 === "/v1/config") {
41159
+ if (req.method === "GET" && path48 === "/v1/config") {
39858
41160
  const snap = buildDesktopConfigSnapshot();
39859
- sendJson(res, 200, { ok: true, ...snap });
41161
+ sendJson2(res, 200, { ok: true, ...snap });
39860
41162
  return;
39861
41163
  }
39862
- if (req.method === "GET" && path47 === "/v1/projects") {
39863
- sendJson(res, 200, {
41164
+ if (req.method === "GET" && path48 === "/v1/projects") {
41165
+ sendJson2(res, 200, {
39864
41166
  ok: true,
39865
41167
  projects: projects.map((p3) => ({
39866
41168
  id: p3.id,
@@ -39870,8 +41172,8 @@ async function runCompanionServe(opts = {}) {
39870
41172
  });
39871
41173
  return;
39872
41174
  }
39873
- if (req.method === "GET" && path47 === "/v1/runs") {
39874
- sendJson(res, 200, {
41175
+ if (req.method === "GET" && path48 === "/v1/runs") {
41176
+ sendJson2(res, 200, {
39875
41177
  ok: true,
39876
41178
  active: runs.getActive(),
39877
41179
  recent: runs.listRecent().map((r) => ({
@@ -39888,13 +41190,13 @@ async function runCompanionServe(opts = {}) {
39888
41190
  });
39889
41191
  return;
39890
41192
  }
39891
- if (req.method === "POST" && path47 === "/v1/runs") {
41193
+ if (req.method === "POST" && path48 === "/v1/runs") {
39892
41194
  const raw = await readBody(req);
39893
41195
  let body = {};
39894
41196
  try {
39895
41197
  body = raw ? JSON.parse(raw) : {};
39896
41198
  } catch {
39897
- sendJson(res, 400, { ok: false, error: "invalid JSON body" });
41199
+ sendJson2(res, 400, { ok: false, error: "invalid JSON body" });
39898
41200
  return;
39899
41201
  }
39900
41202
  const prompt = String(body.prompt ?? body.task ?? "").trim();
@@ -39903,7 +41205,7 @@ async function runCompanionServe(opts = {}) {
39903
41205
  const cwdArg = body.cwd != null ? String(body.cwd) : body.projectId != null ? String(body.projectId) : null;
39904
41206
  const resolved = resolveProjectPath(projects, cwdArg);
39905
41207
  if (!resolved.ok) {
39906
- sendJson(res, 400, { ok: false, error: resolved.error });
41208
+ sendJson2(res, 400, { ok: false, error: resolved.error });
39907
41209
  return;
39908
41210
  }
39909
41211
  const history2 = Array.isArray(body.history) ? body.history : void 0;
@@ -39917,10 +41219,10 @@ async function runCompanionServe(opts = {}) {
39917
41219
  history: history2
39918
41220
  });
39919
41221
  if (!result.ok) {
39920
- sendJson(res, 409, { ok: false, error: result.error });
41222
+ sendJson2(res, 409, { ok: false, error: result.error });
39921
41223
  return;
39922
41224
  }
39923
- sendJson(res, 201, {
41225
+ sendJson2(res, 201, {
39924
41226
  ok: true,
39925
41227
  run: {
39926
41228
  id: result.run.id,
@@ -39935,12 +41237,12 @@ async function runCompanionServe(opts = {}) {
39935
41237
  });
39936
41238
  return;
39937
41239
  }
39938
- const eventsMatch = /^\/v1\/runs\/([^/]+)\/events$/.exec(path47);
41240
+ const eventsMatch = /^\/v1\/runs\/([^/]+)\/events$/.exec(path48);
39939
41241
  if (req.method === "GET" && eventsMatch) {
39940
41242
  const runId = eventsMatch[1];
39941
41243
  const run = runs.getRun(runId);
39942
41244
  if (!run) {
39943
- sendJson(res, 404, { ok: false, error: "run not found" });
41245
+ sendJson2(res, 404, { ok: false, error: "run not found" });
39944
41246
  return;
39945
41247
  }
39946
41248
  res.writeHead(200, {
@@ -40000,20 +41302,20 @@ async function runCompanionServe(opts = {}) {
40000
41302
  }, 500);
40001
41303
  return;
40002
41304
  }
40003
- const cancelMatch = /^\/v1\/runs\/([^/]+)\/cancel$/.exec(path47);
41305
+ const cancelMatch = /^\/v1\/runs\/([^/]+)\/cancel$/.exec(path48);
40004
41306
  if (req.method === "POST" && cancelMatch) {
40005
41307
  const runId = cancelMatch[1];
40006
41308
  const result = runs.cancel(runId);
40007
41309
  if (!result.ok) {
40008
- sendJson(res, 404, { ok: false, error: result.error });
41310
+ sendJson2(res, 404, { ok: false, error: result.error });
40009
41311
  return;
40010
41312
  }
40011
- sendJson(res, 200, { ok: true, cancelled: runId });
41313
+ sendJson2(res, 200, { ok: true, cancelled: runId });
40012
41314
  return;
40013
41315
  }
40014
- sendJson(res, 404, { ok: false, error: "not found" });
41316
+ sendJson2(res, 404, { ok: false, error: "not found" });
40015
41317
  } catch (err) {
40016
- sendJson(res, 500, {
41318
+ sendJson2(res, 500, {
40017
41319
  ok: false,
40018
41320
  error: err instanceof Error ? err.message : String(err)
40019
41321
  });
@@ -40112,11 +41414,11 @@ import { execSync as execSync2 } from "node:child_process";
40112
41414
  import { existsSync as existsSync45, readFileSync as readFileSync35, readlinkSync, statSync as statSync7 } from "node:fs";
40113
41415
  import { createRequire as createRequire3 } from "node:module";
40114
41416
  import { fileURLToPath as fileURLToPath2 } from "node:url";
40115
- import path46 from "node:path";
41417
+ import path47 from "node:path";
40116
41418
  function findPackageRoot(start) {
40117
41419
  let dir = start;
40118
41420
  for (let i = 0; i < 6; i += 1) {
40119
- const candidate = path46.join(dir, "package.json");
41421
+ const candidate = path47.join(dir, "package.json");
40120
41422
  if (existsSync45(candidate)) {
40121
41423
  try {
40122
41424
  const pkg = JSON.parse(readFileSync35(candidate, "utf8"));
@@ -40124,11 +41426,11 @@ function findPackageRoot(start) {
40124
41426
  } catch {
40125
41427
  }
40126
41428
  }
40127
- const parent = path46.dirname(dir);
41429
+ const parent = path47.dirname(dir);
40128
41430
  if (parent === dir) break;
40129
41431
  dir = parent;
40130
41432
  }
40131
- return path46.resolve(__dirname3, "..", "..", "..");
41433
+ return path47.resolve(__dirname3, "..", "..", "..");
40132
41434
  }
40133
41435
  function tryExec(cmd) {
40134
41436
  try {
@@ -40142,7 +41444,7 @@ function tryExec(cmd) {
40142
41444
  }
40143
41445
  function readPackageJson3() {
40144
41446
  try {
40145
- const pkgPath = path46.join(packageRoot, "package.json");
41447
+ const pkgPath = path47.join(packageRoot, "package.json");
40146
41448
  return JSON.parse(readFileSync35(pkgPath, "utf8"));
40147
41449
  } catch {
40148
41450
  return null;
@@ -40158,7 +41460,7 @@ function checkShim(pkgName) {
40158
41460
  }
40159
41461
  const isWin = process.platform === "win32";
40160
41462
  const shimName = isWin ? "zelari-code.cmd" : "zelari-code";
40161
- const shimPath = path46.join(prefix, shimName);
41463
+ const shimPath = path47.join(prefix, shimName);
40162
41464
  if (!existsSync45(shimPath)) {
40163
41465
  return FAIL(
40164
41466
  `shim not found at ${shimPath}
@@ -40186,8 +41488,8 @@ function checkShim(pkgName) {
40186
41488
  fix: npm install -g ${pkgName}@latest --force`
40187
41489
  );
40188
41490
  }
40189
- const resolved = path46.resolve(path46.dirname(shimPath), target);
40190
- const expected = path46.join(
41491
+ const resolved = path47.resolve(path47.dirname(shimPath), target);
41492
+ const expected = path47.join(
40191
41493
  prefix,
40192
41494
  "node_modules",
40193
41495
  pkgName,
@@ -40226,7 +41528,7 @@ function checkNode(pkg) {
40226
41528
  return OK(`node ${raw}`);
40227
41529
  }
40228
41530
  function checkBundle() {
40229
- const bundle = path46.join(packageRoot, "dist", "cli", "main.bundled.js");
41531
+ const bundle = path47.join(packageRoot, "dist", "cli", "main.bundled.js");
40230
41532
  if (!existsSync45(bundle)) {
40231
41533
  return FAIL(
40232
41534
  `dist/cli/main.bundled.js missing at ${bundle}
@@ -40247,7 +41549,7 @@ function checkRuntimeDeps() {
40247
41549
  const missing = [];
40248
41550
  for (const dep of required2) {
40249
41551
  try {
40250
- const localReq = createRequire3(path46.join(packageRoot, "package.json"));
41552
+ const localReq = createRequire3(path47.join(packageRoot, "package.json"));
40251
41553
  localReq.resolve(dep);
40252
41554
  } catch {
40253
41555
  missing.push(dep);
@@ -40423,7 +41725,7 @@ var init_doctor = __esm({
40423
41725
  "use strict";
40424
41726
  init_prereqChecks();
40425
41727
  require3 = createRequire3(import.meta.url);
40426
- __dirname3 = path46.dirname(fileURLToPath2(import.meta.url));
41728
+ __dirname3 = path47.dirname(fileURLToPath2(import.meta.url));
40427
41729
  packageRoot = findPackageRoot(__dirname3);
40428
41730
  OK = (message) => ({
40429
41731
  ok: true,
@@ -40606,7 +41908,7 @@ import React15 from "react";
40606
41908
  import { render } from "ink";
40607
41909
 
40608
41910
  // src/cli/app.tsx
40609
- import React10, { useState as useState8, useMemo, useCallback as useCallback5, useEffect as useEffect7 } from "react";
41911
+ import React10, { useState as useState8, useMemo, useCallback as useCallback5, useEffect as useEffect8 } from "react";
40610
41912
  import { Box as Box9, Static, useInput as useInput2, useStdin as useStdin2 } from "ink";
40611
41913
 
40612
41914
  // src/cli/components/InputBar.tsx
@@ -44443,15 +45745,22 @@ function useChatTurn(params) {
44443
45745
  historySeedLen = getHistory().length;
44444
45746
  const anchored = maybeAnchorShortAnswer(userText);
44445
45747
  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;
45748
+ const localCli = (process.env.ZELARI_LOCAL_CLI ?? "").trim();
45749
+ let localCliProvider = null;
45750
+ if (localCli) {
45751
+ const { createLocalCliProvider: createLocalCliProvider2 } = await Promise.resolve().then(() => (init_claudeProvider(), claudeProvider_exports));
45752
+ localCliProvider = createLocalCliProvider2({ cli: localCli });
45753
+ } else {
45754
+ envConfig = await providerFromEnv();
45755
+ if (!envConfig) {
45756
+ const active = resolveActiveProvider();
45757
+ const spec = PROVIDERS.find((p3) => p3.id === active);
45758
+ appendSystem(
45759
+ setMessages,
45760
+ `No API key for the active provider "${active}". Set ${spec?.envVar ?? "the provider API key env var"} or run /login ${active}.`
45761
+ );
45762
+ return;
45763
+ }
44455
45764
  }
44456
45765
  setBusy(true);
44457
45766
  const workPhase = getPhase();
@@ -44499,29 +45808,34 @@ _(${req.context})_` : "") + "\n\u2192 scegli dalla lista (il turno continua dopo
44499
45808
  onPermissionAsk,
44500
45809
  permissionPolicy: defaultPermissionPolicy()
44501
45810
  });
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
- });
45811
+ const baseProviderStream = localCliProvider ?? openaiCompatibleProvider(envConfig);
45812
+ let providerStream;
45813
+ if (localCliProvider) {
45814
+ providerStream = localCliProvider;
45815
+ } else {
45816
+ const failoverResolution = await resolveFailoverStream({
45817
+ failoverEnabled: process.env.ANATHEMA_FAILOVER !== "0",
45818
+ envValue: process.env.ANATHEMA_FAILOVER_PROVIDER,
45819
+ primaryProviderId: envConfig.providerId,
45820
+ primary: baseProviderStream,
45821
+ validProviderIds: PROVIDERS.map((p3) => p3.id),
45822
+ lookupFallbackConfig: async (id) => providerConfigFor(id),
45823
+ buildStream: (config2) => openaiCompatibleProvider(
45824
+ config2
45825
+ )
45826
+ });
45827
+ if (failoverResolution.warning) {
45828
+ appendSystem(setMessages, `[failover] ${failoverResolution.warning}`);
45829
+ }
45830
+ providerStream = failoverResolution.fallbackLabel ? providerFailover({
45831
+ primary: baseProviderStream,
45832
+ fallback: failoverResolution.fallback,
45833
+ fallbackLabel: failoverResolution.fallbackLabel
45834
+ }) : providerFailover({
45835
+ primary: baseProviderStream,
45836
+ fallback: failoverResolution.fallback
45837
+ });
45838
+ }
44525
45839
  const cwd = process.cwd();
44526
45840
  let composedWorkspace = "";
44527
45841
  let composedInstructions = "";
@@ -45795,6 +47109,83 @@ async function runZelariMissionInTui(userMessage, deps, emit) {
45795
47109
  }
45796
47110
  }
45797
47111
 
47112
+ // src/cli/hooks/usePermissionBroker.ts
47113
+ init_permissionBroker();
47114
+ init_brokerHandlers();
47115
+ import { useEffect as useEffect5, useRef as useRef5 } from "react";
47116
+ function usePermissionBroker(opts) {
47117
+ const { setPicker: setPicker2, setMessages } = opts;
47118
+ const handleRef = useRef5(null);
47119
+ useEffect5(() => {
47120
+ const socketPath = process.env.ZELARI_PERM_SOCKET?.trim();
47121
+ if (!socketPath) return;
47122
+ let disposed = false;
47123
+ let handle = null;
47124
+ const onPermissionAsk = createPermissionAskHandler({
47125
+ setPicker: setPicker2,
47126
+ appendSystem: (msg, at) => appendSystem(setMessages, msg, at ?? Date.now())
47127
+ });
47128
+ const onQuestion = (req) => new Promise((resolve3) => {
47129
+ const choices = (req.choices ?? []).map((c) => String(c).trim()).filter((c) => c.length > 0);
47130
+ if (choices.length < 2) {
47131
+ resolve3(null);
47132
+ return;
47133
+ }
47134
+ let settled = false;
47135
+ const finish = (value) => {
47136
+ if (settled) return;
47137
+ settled = true;
47138
+ setPicker2(null);
47139
+ resolve3(value);
47140
+ };
47141
+ appendSystem(
47142
+ setMessages,
47143
+ `[external agent] ${req.question}
47144
+ \u2192 ${choices.join(" \xB7 ")}`,
47145
+ Date.now()
47146
+ );
47147
+ setPicker2({
47148
+ kind: "clarification",
47149
+ title: req.question,
47150
+ items: choices.map((c) => ({ value: c, label: c })),
47151
+ onAnswer: (value) => finish(value),
47152
+ onCancel: () => finish(null)
47153
+ });
47154
+ });
47155
+ void startPermissionBroker(socketPath, {
47156
+ onPermission: createBrokerPermissionHandler({ onPermissionAsk }),
47157
+ onQuestion: createBrokerQuestionHandler({ onQuestion })
47158
+ }).then((h) => {
47159
+ if (disposed) {
47160
+ void h.stop();
47161
+ return;
47162
+ }
47163
+ handle = h;
47164
+ handleRef.current = h;
47165
+ appendSystem(
47166
+ setMessages,
47167
+ `[permission-broker] listening on ${socketPath} \u2014 external agents can request approvals.`,
47168
+ Date.now()
47169
+ );
47170
+ process.stderr.write(
47171
+ `[zelari-code] permission broker listening on ${socketPath}
47172
+ `
47173
+ );
47174
+ }).catch((err) => {
47175
+ process.stderr.write(
47176
+ `[zelari-code] permission broker failed to start: ${err instanceof Error ? err.message : String(err)}
47177
+ `
47178
+ );
47179
+ });
47180
+ return () => {
47181
+ disposed = true;
47182
+ if (handle) void handle.stop();
47183
+ handleRef.current = null;
47184
+ };
47185
+ }, [setPicker2, setMessages]);
47186
+ return { socketPath: process.env.ZELARI_PERM_SOCKET?.trim() || void 0 };
47187
+ }
47188
+
45798
47189
  // src/cli/hooks/useSlashDispatch.ts
45799
47190
  import { useCallback as useCallback3 } from "react";
45800
47191
 
@@ -45901,6 +47292,7 @@ function handleSlashCommand(text, availableSkills) {
45901
47292
  /update [--yes|-y] \u2014 check for zelari-code updates; --yes performs the update (v3-N)
45902
47293
  /plugins \u2014 list optional tool plugins (Playwright, eslint, ruff, LSP servers)
45903
47294
  /plugins install <id> \u2014 install a plugin now (e.g. /plugins install eslint)
47295
+ /integrations \u2014 list MCP integration presets (cua, composio, qwen-mm-plugins) + status
45904
47296
  /steer <text> \u2014 enqueue a follow-up prompt on the active run (Task 18.2)
45905
47297
  /steer --interrupt <text> \u2014 cancel current run + enqueue <text> for next dispatch (Task C.3.2)
45906
47298
  /compact \u2014 compact the session transcript
@@ -46269,6 +47661,27 @@ ${formatSkillList(availableSkills)}`
46269
47661
  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
47662
  };
46271
47663
  }
47664
+ case "integrations": {
47665
+ const lines = [
47666
+ "/integrations \u2014 MCP integration presets (apply: zelari-code --preset <id> [--scope user|project])",
47667
+ "",
47668
+ " cua \u2014 Cua Driver desktop computer-use (binary on PATH, https://cua.ai/docs)",
47669
+ ` status: ${process.env.ZELARI_CUA === "0" ? "disabled (ZELARI_CUA=0)" : "enabled (kill switch: ZELARI_CUA=0)"}`,
47670
+ " composio \u2014 Composio Connect: 500+ app integrations as MCP tools",
47671
+ ` status: ${process.env.COMPOSIO_API_KEY ? "ready (COMPOSIO_API_KEY set)" : "missing COMPOSIO_API_KEY \u2014 export it first"}`,
47672
+ " qwen-mm-plugins \u2014 Qwen-MM-Plugins multimodal MCP tools (uvx; WSL2 on Windows)",
47673
+ ` status: ${process.env.QWEN_MM_PLUGIN ? "ready (QWEN_MM_PLUGIN=" + process.env.QWEN_MM_PLUGIN + ")" : "ready (default: qwen-mm-plugins-core)"}`,
47674
+ "",
47675
+ "Apply: zelari-code --preset composio (writes ~/.zelari-code/mcp.json)",
47676
+ " zelari-code --preset cua --scope project (writes .zelari/mcp.json)",
47677
+ " zelari-code --preset qwen-mm-plugins (writes ~/.zelari-code/mcp.json)"
47678
+ ];
47679
+ return {
47680
+ handled: true,
47681
+ kind: "integrations_list",
47682
+ message: lines.join("\n")
47683
+ };
47684
+ }
46272
47685
  case "undo": {
46273
47686
  const confirmed = args.includes("--yes") || args.includes("-y");
46274
47687
  if (confirmed) {
@@ -46919,13 +48332,13 @@ ${digest}
46919
48332
  // src/cli/slashHandlers/krakenFanout.ts
46920
48333
  init_auditLogger();
46921
48334
  init_toolRegistry();
46922
- import { promises as fs21 } from "node:fs";
48335
+ import { promises as fs22 } from "node:fs";
46923
48336
 
46924
48337
  // src/cli/tools/krakenCsvFanout.ts
46925
48338
  init_zod();
46926
48339
  init_taskTool();
46927
- import { promises as fs20 } from "node:fs";
46928
- import path36 from "node:path";
48340
+ import { promises as fs21 } from "node:fs";
48341
+ import path37 from "node:path";
46929
48342
  import { randomBytes as randomBytes3 } from "node:crypto";
46930
48343
  var CsvFanoutArgsSchema = external_exports.object({
46931
48344
  csv_path: external_exports.string().min(1),
@@ -46946,7 +48359,7 @@ var CsvFanoutArgsSchema = external_exports.object({
46946
48359
  max_runtime_seconds: external_exports.number().int().positive().optional()
46947
48360
  });
46948
48361
  async function readCsv(filePath) {
46949
- const text = await fs20.readFile(filePath, "utf8");
48362
+ const text = await fs21.readFile(filePath, "utf8");
46950
48363
  return parseCsv(text);
46951
48364
  }
46952
48365
  function parseCsv(text) {
@@ -47019,8 +48432,8 @@ function resolveMaxConcurrency(env = process.env) {
47019
48432
  }
47020
48433
  async function runCsvFanout(args, deps, opts) {
47021
48434
  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);
48435
+ const absCsv = path37.isAbsolute(args.csv_path) ? args.csv_path : path37.join(opts.parentCwd, args.csv_path);
48436
+ const absOut = path37.isAbsolute(args.output_csv_path) ? args.output_csv_path : path37.join(opts.parentCwd, args.output_csv_path);
47024
48437
  const { headers, rows } = await readCsv(absCsv);
47025
48438
  if (headers.length === 0) {
47026
48439
  throw new Error(`kraken_csv_fanout: ${absCsv} is empty`);
@@ -47076,7 +48489,7 @@ async function runCsvFanout(args, deps, opts) {
47076
48489
  errored += 1;
47077
48490
  errors.push(`${row[args.id_column] ?? i}: ${res.error}`);
47078
48491
  }
47079
- await fs20.mkdir(path36.dirname(absOut), { recursive: true });
48492
+ await fs21.mkdir(path37.dirname(absOut), { recursive: true });
47080
48493
  await queueWrite(serializeCsv(outHeaders, outputRecords));
47081
48494
  }
47082
48495
  }
@@ -47098,8 +48511,8 @@ async function runCsvFanout(args, deps, opts) {
47098
48511
  }
47099
48512
  async function atomicWrite(file2, contents) {
47100
48513
  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);
48514
+ await fs21.writeFile(tmp, contents, "utf8");
48515
+ await fs21.rename(tmp, file2);
47103
48516
  }
47104
48517
 
47105
48518
  // src/cli/slashHandlers/krakenFanout.ts
@@ -47197,7 +48610,7 @@ async function handleKrakenFanout(ctx, raw) {
47197
48610
  }
47198
48611
  const absCsv = isAbsolute(parsed.args.csv_path) ? parsed.args.csv_path : joinPath(ctx.cwd, parsed.args.csv_path);
47199
48612
  try {
47200
- await fs21.access(absCsv);
48613
+ await fs22.access(absCsv);
47201
48614
  } catch {
47202
48615
  appendSystem(ctx.setMessages, `[kraken fanout] source CSV not found: ${absCsv}`);
47203
48616
  return;
@@ -47269,8 +48682,8 @@ function splitArgs(s) {
47269
48682
  }
47270
48683
 
47271
48684
  // src/cli/slashHandlers/krakenWorkbench.ts
47272
- import { promises as fs22 } from "node:fs";
47273
- import path37 from "node:path";
48685
+ import { promises as fs23 } from "node:fs";
48686
+ import path38 from "node:path";
47274
48687
 
47275
48688
  // src/cli/kraken/workbenchView.ts
47276
48689
  var EMPTY = {
@@ -47387,15 +48800,15 @@ function formatWorkbenchForTerminal(p3) {
47387
48800
 
47388
48801
  // src/cli/slashHandlers/krakenWorkbench.ts
47389
48802
  async function handleKrakenWorkbench(ctx) {
47390
- const dir = path37.join(ctx.cwd, ".zelari", "radio");
48803
+ const dir = path38.join(ctx.cwd, ".zelari", "radio");
47391
48804
  let latest = null;
47392
48805
  let latestMtime = 0;
47393
48806
  try {
47394
- const files = await fs22.readdir(dir);
48807
+ const files = await fs23.readdir(dir);
47395
48808
  for (const f of files) {
47396
48809
  if (!f.startsWith("workbench-") || !f.endsWith(".md")) continue;
47397
- const full = path37.join(dir, f);
47398
- const stat = await fs22.stat(full);
48810
+ const full = path38.join(dir, f);
48811
+ const stat = await fs23.stat(full);
47399
48812
  if (stat.mtimeMs > latestMtime) {
47400
48813
  latestMtime = stat.mtimeMs;
47401
48814
  latest = full;
@@ -47407,14 +48820,14 @@ async function handleKrakenWorkbench(ctx) {
47407
48820
  appendSystem(ctx.setMessages, "[kraken workbench] no workbench file found (.zelari/radio/workbench-*.md)");
47408
48821
  return;
47409
48822
  }
47410
- const content = await fs22.readFile(latest, "utf8");
48823
+ const content = await fs23.readFile(latest, "utf8");
47411
48824
  const parsed = parseWorkbench(content);
47412
48825
  const rendered = formatWorkbenchForTerminal(parsed);
47413
48826
  if (!rendered.trim()) {
47414
- appendSystem(ctx.setMessages, `[kraken workbench] ${path37.basename(latest)}: (no nodes / no events yet)`);
48827
+ appendSystem(ctx.setMessages, `[kraken workbench] ${path38.basename(latest)}: (no nodes / no events yet)`);
47415
48828
  return;
47416
48829
  }
47417
- appendSystem(ctx.setMessages, `[kraken workbench] ${path37.basename(latest)}:
48830
+ appendSystem(ctx.setMessages, `[kraken workbench] ${path38.basename(latest)}:
47418
48831
  ${rendered}`);
47419
48832
  }
47420
48833
 
@@ -47566,8 +48979,8 @@ init_registry3();
47566
48979
  // src/cli/plugins/installer.ts
47567
48980
  init_cmdline();
47568
48981
  init_updater();
47569
- import { spawn as spawn10 } from "node:child_process";
47570
- async function installPlugin(spec, cwd, executor = spawn10) {
48982
+ import { spawn as spawn11 } from "node:child_process";
48983
+ async function installPlugin(spec, cwd, executor = spawn11) {
47571
48984
  const scopeFlag = spec.installScope === "global" ? "-g" : "-D";
47572
48985
  const args = ["install", scopeFlag, spec.npmPackage];
47573
48986
  let result = await runNpm2(executor, args, cwd, "shim");
@@ -47715,17 +49128,17 @@ ${result.output.split("\n").slice(-8).join("\n")}` : "";
47715
49128
  }
47716
49129
 
47717
49130
  // src/cli/slashHandlers/promoteMember.ts
47718
- import { promises as fs23 } from "node:fs";
47719
- import path40 from "node:path";
49131
+ import { promises as fs24 } from "node:fs";
49132
+ import path41 from "node:path";
47720
49133
  import os10 from "node:os";
47721
49134
  async function handlePromoteMember(ctx, memberId) {
47722
49135
  try {
47723
49136
  const { promoteMember: promoteMember2 } = await Promise.resolve().then(() => (init_council(), council_exports));
47724
49137
  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");
49138
+ const skillDir = process.env.ANATHEMA_SKILL_DIR ?? path41.join(os10.homedir(), ".tmp", "zelari-code", "skills");
49139
+ await fs24.mkdir(skillDir, { recursive: true });
49140
+ const filePath = path41.join(skillDir, `${skill.id}.md`);
49141
+ await fs24.writeFile(filePath, markdown, "utf8");
47729
49142
  appendSystem(
47730
49143
  ctx.setMessages,
47731
49144
  `[promote-member] ${skill.name} (${memberId}) \u2192 ${filePath}
@@ -47741,25 +49154,25 @@ async function handlePromoteMember(ctx, memberId) {
47741
49154
  }
47742
49155
 
47743
49156
  // 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";
49157
+ 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";
49158
+ import path42 from "node:path";
47746
49159
  import os11 from "node:os";
47747
49160
  var META_FILENAME = "meta.json";
47748
49161
  var SESSIONS_SUBDIR = "sessions";
47749
49162
  function getBranchesBaseDir() {
47750
- return process.env.ANATHEMA_BRANCHES_DIR ?? path41.join(os11.homedir(), ".tmp", "zelari-code", "branches");
49163
+ return process.env.ANATHEMA_BRANCHES_DIR ?? path42.join(os11.homedir(), ".tmp", "zelari-code", "branches");
47751
49164
  }
47752
49165
  function getSessionsBaseDir() {
47753
- return process.env.ANATHEMA_SESSIONS_DIR ?? path41.join(os11.homedir(), ".tmp", "zelari-code", "sessions");
49166
+ return process.env.ANATHEMA_SESSIONS_DIR ?? path42.join(os11.homedir(), ".tmp", "zelari-code", "sessions");
47754
49167
  }
47755
49168
  function branchPathFor(name, baseDir) {
47756
- return path41.join(baseDir, name);
49169
+ return path42.join(baseDir, name);
47757
49170
  }
47758
49171
  function metaPathFor(name, baseDir) {
47759
- return path41.join(baseDir, name, META_FILENAME);
49172
+ return path42.join(baseDir, name, META_FILENAME);
47760
49173
  }
47761
49174
  function sessionsPathFor(name, baseDir) {
47762
- return path41.join(baseDir, name, SESSIONS_SUBDIR);
49175
+ return path42.join(baseDir, name, SESSIONS_SUBDIR);
47763
49176
  }
47764
49177
  function readBranchMeta(name, baseDir) {
47765
49178
  const metaPath = metaPathFor(name, baseDir);
@@ -47784,13 +49197,13 @@ function readBranchMeta(name, baseDir) {
47784
49197
  }
47785
49198
  function writeBranchMeta(name, baseDir, meta3) {
47786
49199
  const metaPath = metaPathFor(name, baseDir);
47787
- mkdirSync16(path41.dirname(metaPath), { recursive: true });
49200
+ mkdirSync16(path42.dirname(metaPath), { recursive: true });
47788
49201
  writeFileSync19(metaPath, JSON.stringify(meta3, null, 2), "utf-8");
47789
49202
  }
47790
49203
  async function countSessions(name, baseDir) {
47791
49204
  const sessionsPath = sessionsPathFor(name, baseDir);
47792
49205
  try {
47793
- const entries = await fs24.readdir(sessionsPath);
49206
+ const entries = await fs25.readdir(sessionsPath);
47794
49207
  return entries.filter((e) => e.endsWith(".jsonl")).length;
47795
49208
  } catch (err) {
47796
49209
  if (err.code === "ENOENT") return 0;
@@ -47835,15 +49248,15 @@ async function createBranch(name, fromSessionId, baseDir = getBranchesBaseDir(),
47835
49248
  if (branchExists(name, baseDir)) {
47836
49249
  throw new BranchAlreadyExistsError(name);
47837
49250
  }
47838
- const sourcePath = path41.join(sessionsBaseDir, `${fromSessionId}.jsonl`);
49251
+ const sourcePath = path42.join(sessionsBaseDir, `${fromSessionId}.jsonl`);
47839
49252
  if (!existsSync38(sourcePath)) {
47840
49253
  throw new SessionNotFoundError(`Source session "${fromSessionId}" not found at ${sourcePath}`);
47841
49254
  }
47842
49255
  const branchPath = branchPathFor(name, baseDir);
47843
49256
  const branchSessionsPath = sessionsPathFor(name, baseDir);
47844
49257
  mkdirSync16(branchSessionsPath, { recursive: true });
47845
- const destPath = path41.join(branchSessionsPath, `${fromSessionId}.jsonl`);
47846
- await fs24.copyFile(sourcePath, destPath);
49258
+ const destPath = path42.join(branchSessionsPath, `${fromSessionId}.jsonl`);
49259
+ await fs25.copyFile(sourcePath, destPath);
47847
49260
  const meta3 = {
47848
49261
  name,
47849
49262
  createdAt: Date.now(),
@@ -47861,7 +49274,7 @@ async function createBranch(name, fromSessionId, baseDir = getBranchesBaseDir(),
47861
49274
  async function listBranches(baseDir = getBranchesBaseDir()) {
47862
49275
  let entries;
47863
49276
  try {
47864
- entries = await fs24.readdir(baseDir);
49277
+ entries = await fs25.readdir(baseDir);
47865
49278
  } catch (err) {
47866
49279
  if (err.code === "ENOENT") return [];
47867
49280
  throw err;
@@ -47942,26 +49355,26 @@ async function handleBranchCheckout(ctx, branchName) {
47942
49355
  }
47943
49356
 
47944
49357
  // src/cli/slashHandlers/workspace.ts
47945
- import { promises as fs25 } from "node:fs";
47946
- import path42 from "node:path";
49358
+ import { promises as fs26 } from "node:fs";
49359
+ import path43 from "node:path";
47947
49360
  async function handleWorkspaceShow(ctx, what) {
47948
49361
  try {
47949
- const zelari = path42.join(process.cwd(), ".zelari");
49362
+ const zelari = path43.join(process.cwd(), ".zelari");
47950
49363
  let content;
47951
49364
  switch (what) {
47952
49365
  case "plan": {
47953
- const planPath = path42.join(zelari, "plan.md");
49366
+ const planPath = path43.join(zelari, "plan.md");
47954
49367
  try {
47955
- content = await fs25.readFile(planPath, "utf-8");
49368
+ content = await fs26.readFile(planPath, "utf-8");
47956
49369
  } catch {
47957
49370
  content = "(no plan.md yet \u2014 run a council session first)";
47958
49371
  }
47959
49372
  break;
47960
49373
  }
47961
49374
  case "decisions": {
47962
- const decisionsDir = path42.join(zelari, "decisions");
49375
+ const decisionsDir = path43.join(zelari, "decisions");
47963
49376
  try {
47964
- const files = (await fs25.readdir(decisionsDir)).filter((f) => f.endsWith(".md")).sort();
49377
+ const files = (await fs26.readdir(decisionsDir)).filter((f) => f.endsWith(".md")).sort();
47965
49378
  if (files.length === 0) {
47966
49379
  content = "(no ADRs yet \u2014 invoke /council to generate some)";
47967
49380
  } else {
@@ -47969,7 +49382,7 @@ async function handleWorkspaceShow(ctx, what) {
47969
49382
  `];
47970
49383
  const { parseFrontmatter: parseFrontmatter2 } = await Promise.resolve().then(() => (init_storage(), storage_exports));
47971
49384
  for (const f of files) {
47972
- const raw = await fs25.readFile(path42.join(decisionsDir, f), "utf-8");
49385
+ const raw = await fs26.readFile(path43.join(decisionsDir, f), "utf-8");
47973
49386
  const { meta: meta3, body } = parseFrontmatter2(raw);
47974
49387
  const title = meta3.title ?? body.split("\n")[0]?.replace(/^#\s*/, "").trim() ?? f;
47975
49388
  lines.push(`- **${f.replace(/\.md$/, "")}** [${meta3.status ?? "unknown"}] ${title}`);
@@ -47982,27 +49395,27 @@ async function handleWorkspaceShow(ctx, what) {
47982
49395
  break;
47983
49396
  }
47984
49397
  case "risks": {
47985
- const risksPath = path42.join(zelari, "risks.md");
49398
+ const risksPath = path43.join(zelari, "risks.md");
47986
49399
  try {
47987
- content = await fs25.readFile(risksPath, "utf-8");
49400
+ content = await fs26.readFile(risksPath, "utf-8");
47988
49401
  } catch {
47989
49402
  content = "(no risks.md yet)";
47990
49403
  }
47991
49404
  break;
47992
49405
  }
47993
49406
  case "agents": {
47994
- const agentsPath = path42.join(process.cwd(), "AGENTS.MD");
49407
+ const agentsPath = path43.join(process.cwd(), "AGENTS.MD");
47995
49408
  try {
47996
- content = await fs25.readFile(agentsPath, "utf-8");
49409
+ content = await fs26.readFile(agentsPath, "utf-8");
47997
49410
  } catch {
47998
49411
  content = "(no AGENTS.MD yet at project root \u2014 run `/workspace sync` after a council session)";
47999
49412
  }
48000
49413
  break;
48001
49414
  }
48002
49415
  case "docs": {
48003
- const docsDir = path42.join(zelari, "docs");
49416
+ const docsDir = path43.join(zelari, "docs");
48004
49417
  try {
48005
- const files = (await fs25.readdir(docsDir)).filter((f) => f.endsWith(".md")).sort();
49418
+ const files = (await fs26.readdir(docsDir)).filter((f) => f.endsWith(".md")).sort();
48006
49419
  content = files.length ? `# Docs (${files.length})
48007
49420
 
48008
49421
  ` + files.map((f) => `- ${f}`).join("\n") : "(no docs drafts yet)";
@@ -48042,8 +49455,8 @@ async function handleWorkspaceReset(ctx, force) {
48042
49455
  return;
48043
49456
  }
48044
49457
  try {
48045
- const target = path42.join(process.cwd(), ".zelari");
48046
- await fs25.rm(target, { recursive: true, force: true });
49458
+ const target = path43.join(process.cwd(), ".zelari");
49459
+ await fs26.rm(target, { recursive: true, force: true });
48047
49460
  appendSystem(ctx.setMessages, "[workspace] .zelari/ removed");
48048
49461
  } catch (err) {
48049
49462
  appendSystem(ctx.setMessages, `[workspace reset error] ${err instanceof Error ? err.message : String(err)}`);
@@ -48402,16 +49815,16 @@ function handleModelsRefresh(ctx) {
48402
49815
  }
48403
49816
 
48404
49817
  // src/cli/slashHandlers/skills.ts
48405
- import path43 from "node:path";
49818
+ import path44 from "node:path";
48406
49819
  import os12 from "node:os";
48407
49820
 
48408
49821
  // 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";
49822
+ import { promises as fs27, existsSync as existsSync39, statSync as statSync5, renameSync as renameSync4, appendFileSync as appendFileSync4, mkdirSync as mkdirSync17 } from "node:fs";
48410
49823
  var SKILL_HISTORY_ROTATE_BYTES = 10 * 1024 * 1024;
48411
49824
  async function readSkillHistory(file2) {
48412
49825
  let raw = "";
48413
49826
  try {
48414
- raw = await fs26.readFile(file2, "utf-8");
49827
+ raw = await fs27.readFile(file2, "utf-8");
48415
49828
  } catch {
48416
49829
  return [];
48417
49830
  }
@@ -48528,7 +49941,7 @@ function handleSkillPicker(ctx, skills, openPicker, fallbackMessage) {
48528
49941
  });
48529
49942
  }
48530
49943
  async function handleSkillStats(ctx, skillId) {
48531
- const historyFile = process.env.ANATHEMA_SKILL_HISTORY_FILE ?? path43.join(os12.homedir(), ".tmp", "zelari-code", "skill-history.jsonl");
49944
+ const historyFile = process.env.ANATHEMA_SKILL_HISTORY_FILE ?? path44.join(os12.homedir(), ".tmp", "zelari-code", "skill-history.jsonl");
48532
49945
  try {
48533
49946
  const records = await readSkillHistory(historyFile);
48534
49947
  const stats = getSkillStats(records, skillId);
@@ -48544,7 +49957,7 @@ async function handleSkillCompare(ctx, ids, fallbackMessage) {
48544
49957
  appendSystem(ctx.setMessages, fallbackMessage ?? "[skill-compare] missing args");
48545
49958
  return;
48546
49959
  }
48547
- const historyFile = process.env.ANATHEMA_SKILL_HISTORY_FILE ?? path43.join(os12.homedir(), ".tmp", "zelari-code", "skill-history.jsonl");
49960
+ const historyFile = process.env.ANATHEMA_SKILL_HISTORY_FILE ?? path44.join(os12.homedir(), ".tmp", "zelari-code", "skill-history.jsonl");
48548
49961
  try {
48549
49962
  const formatted = await compareSkillsFromFile(ids[0], ids[1], historyFile);
48550
49963
  appendSystem(ctx.setMessages, formatted);
@@ -49100,10 +50513,10 @@ function useSlashDispatch(params) {
49100
50513
  }
49101
50514
 
49102
50515
  // src/cli/hooks/useBatchedMessages.ts
49103
- import { useCallback as useCallback4, useEffect as useEffect5, useRef as useRef5 } from "react";
50516
+ import { useCallback as useCallback4, useEffect as useEffect6, useRef as useRef6 } from "react";
49104
50517
  function useBatchedMessages(state3, setState, cadenceMs = 16) {
49105
- const pendingRef = useRef5(null);
49106
- const timerRef = useRef5(null);
50518
+ const pendingRef = useRef6(null);
50519
+ const timerRef = useRef6(null);
49107
50520
  const flush = useCallback4(() => {
49108
50521
  if (timerRef.current !== null) {
49109
50522
  clearTimeout(timerRef.current);
@@ -49141,7 +50554,7 @@ function useBatchedMessages(state3, setState, cadenceMs = 16) {
49141
50554
  },
49142
50555
  [cadenceMs, setState]
49143
50556
  );
49144
- useEffect5(() => {
50557
+ useEffect6(() => {
49145
50558
  return () => {
49146
50559
  if (timerRef.current !== null) {
49147
50560
  clearTimeout(timerRef.current);
@@ -49153,7 +50566,7 @@ function useBatchedMessages(state3, setState, cadenceMs = 16) {
49153
50566
  }
49154
50567
 
49155
50568
  // src/cli/hooks/useTerminalSize.ts
49156
- import { useEffect as useEffect6, useState as useState7 } from "react";
50569
+ import { useEffect as useEffect7, useState as useState7 } from "react";
49157
50570
  import { useStdout as useStdout2 } from "ink";
49158
50571
  var DEFAULT_COALESCE_MS = 120;
49159
50572
  function useTerminalSize(options = {}) {
@@ -49163,7 +50576,7 @@ function useTerminalSize(options = {}) {
49163
50576
  columns: stdout?.columns ?? defaults.columns,
49164
50577
  rows: stdout?.rows ?? defaults.rows
49165
50578
  });
49166
- useEffect6(() => {
50579
+ useEffect7(() => {
49167
50580
  if (!stdout) return;
49168
50581
  const read = () => ({
49169
50582
  columns: stdout.columns ?? defaults.columns,
@@ -49227,6 +50640,7 @@ function App() {
49227
50640
  const activeProviderSpec = getActiveProvider();
49228
50641
  const activeModel = providerConfig.modelByProvider[activeProviderSpec.id];
49229
50642
  const session = useSession();
50643
+ usePermissionBroker({ setPicker: setPicker2, setMessages: session.setMessages });
49230
50644
  const size = useTerminalSize();
49231
50645
  const gitChanges = useGitChanges();
49232
50646
  const cwd = useMemo(() => shortenCwd(process.cwd(), 32), []);
@@ -49240,7 +50654,7 @@ function App() {
49240
50654
  },
49241
50655
  { isActive: isRawModeSupported === true }
49242
50656
  );
49243
- useEffect7(() => {
50657
+ useEffect8(() => {
49244
50658
  const id = activeProviderSpec.id;
49245
50659
  if (!["grok", "glm", "minimax", "deepseek", "openai-compatible"].includes(id)) return;
49246
50660
  if (!isModelsCacheStale(id)) return;
@@ -49330,7 +50744,7 @@ function App() {
49330
50744
  };
49331
50745
  }, []);
49332
50746
  const [sidebarOpen, setSidebarOpen] = useState8(false);
49333
- useEffect7(() => {
50747
+ useEffect8(() => {
49334
50748
  setSidebarOpen((prev2) => sidebarVisibility(size.columns, size.rows, prev2));
49335
50749
  }, [size.columns, size.rows]);
49336
50750
  const staticKey = `${session.sessionId || "pre-bootstrap"}-${clearEpoch}`;
@@ -49390,7 +50804,7 @@ function App() {
49390
50804
  }
49391
50805
 
49392
50806
  // src/cli/components/SplashScreen.tsx
49393
- import React11, { useEffect as useEffect8, useState as useState9 } from "react";
50807
+ import React11, { useEffect as useEffect9, useState as useState9 } from "react";
49394
50808
  import { Box as Box10, Text as Text11, useInput as useInput3, useStdin as useStdin3 } from "ink";
49395
50809
  var SPLASH_DURATION_MS = 2e3;
49396
50810
  var EMBLEM_LARGE = ` =%@*-
@@ -49516,7 +50930,7 @@ function Splash({ onDone, version: version2 }) {
49516
50930
  const columns = process.stdout.columns ?? 80;
49517
50931
  const rows = process.stdout.rows ?? 24;
49518
50932
  const picked = pickSplashArt(columns, rows);
49519
- useEffect8(() => {
50933
+ useEffect9(() => {
49520
50934
  const t = setTimeout(onDone, SPLASH_DURATION_MS);
49521
50935
  return () => clearTimeout(t);
49522
50936
  }, [onDone]);
@@ -49561,7 +50975,7 @@ function SplashGate({
49561
50975
  }
49562
50976
 
49563
50977
  // src/cli/components/PluginGate.tsx
49564
- import React12, { useEffect as useEffect9, useState as useState10, useCallback as useCallback6 } from "react";
50978
+ import React12, { useEffect as useEffect10, useState as useState10, useCallback as useCallback6 } from "react";
49565
50979
  import { Box as Box11, Text as Text12, useStdin as useStdin4 } from "ink";
49566
50980
  init_registry3();
49567
50981
  init_prefs();
@@ -49575,7 +50989,7 @@ function PluginGate({ cwd, children }) {
49575
50989
  const [current, setCurrent] = useState10(null);
49576
50990
  const [result, setResult] = useState10(null);
49577
50991
  const skipGate = process.env.ZELARI_NO_PLUGIN_PROMPT === "1" || isRawModeSupported !== true;
49578
- useEffect9(() => {
50992
+ useEffect10(() => {
49579
50993
  if (skipGate) {
49580
50994
  setPhase2("done");
49581
50995
  return;
@@ -49688,7 +51102,7 @@ function PluginGate({ cwd, children }) {
49688
51102
  }
49689
51103
  function ContinueKey({ onContinue }) {
49690
51104
  const { isRawModeSupported } = useStdin4();
49691
- useEffect9(() => {
51105
+ useEffect10(() => {
49692
51106
  const t = setTimeout(onContinue, 1500);
49693
51107
  return () => clearTimeout(t);
49694
51108
  }, [onContinue]);
@@ -49737,7 +51151,7 @@ function parseWizardFlags(argv) {
49737
51151
  }
49738
51152
 
49739
51153
  // src/cli/wizard/runWizard.tsx
49740
- import React14, { useEffect as useEffect10, useState as useState11 } from "react";
51154
+ import React14, { useEffect as useEffect11, useState as useState11 } from "react";
49741
51155
  import { Box as Box13, Text as Text14 } from "ink";
49742
51156
  import { useInput as useInput4 } from "ink";
49743
51157
  init_keyStore();
@@ -49902,7 +51316,7 @@ function RunWizard(_props) {
49902
51316
  })
49903
51317
  );
49904
51318
  const [, force] = useState11(0);
49905
- useEffect10(() => {
51319
+ useEffect11(() => {
49906
51320
  if (typeof wiz.subscribe === "function") {
49907
51321
  const sub = wiz.subscribe(() => force((n) => n + 1));
49908
51322
  return () => sub();
@@ -49972,7 +51386,7 @@ function RunWizard(_props) {
49972
51386
  }
49973
51387
  function PostCommitBridge() {
49974
51388
  const [showApp, setShowApp] = useState11(false);
49975
- useEffect10(() => {
51389
+ useEffect11(() => {
49976
51390
  const t = setTimeout(() => setShowApp(true), 1200);
49977
51391
  return () => clearTimeout(t);
49978
51392
  }, []);
@@ -50169,6 +51583,7 @@ init_dist();
50169
51583
  init_conversationContext();
50170
51584
  init_council();
50171
51585
  init_toolRegistry();
51586
+ init_claudeProvider();
50172
51587
  init_skills2();
50173
51588
  init_envNumber();
50174
51589
  init_phaseState();
@@ -50203,8 +51618,8 @@ function createStreamScrubber() {
50203
51618
 
50204
51619
  // src/cli/runHeadless.ts
50205
51620
  init_taskTool();
50206
- import { promises as fs28 } from "node:fs";
50207
- import path45 from "node:path";
51621
+ import { promises as fs29 } from "node:fs";
51622
+ import path46 from "node:path";
50208
51623
  import { randomUUID as randomUUID6 } from "node:crypto";
50209
51624
  async function runHeadless(opts) {
50210
51625
  resetTaskSpawnCount();
@@ -50212,10 +51627,14 @@ async function runHeadless(opts) {
50212
51627
  const { expandAtMentions: expandAtMentions2 } = await Promise.resolve().then(() => (init_atMentions(), atMentions_exports));
50213
51628
  const task = typeof opts.task === "string" ? opts.task : "";
50214
51629
  if (task.includes("@")) {
50215
- const { text } = expandAtMentions2(task, process.cwd());
51630
+ const { text, hits } = expandAtMentions2(task, process.cwd());
50216
51631
  if (text !== task) {
50217
51632
  opts = { ...opts, task: text };
50218
51633
  }
51634
+ const images = hits.filter((h) => h.image).map((h) => h.image);
51635
+ if (images.length > 0) {
51636
+ opts = { ...opts, images };
51637
+ }
50219
51638
  }
50220
51639
  } catch {
50221
51640
  }
@@ -50245,19 +51664,51 @@ ${err.stack}` : "";
50245
51664
  process.on("uncaughtException", (err) => handleFatal("uncaughtException", err));
50246
51665
  process.on("unhandledRejection", (err) => handleFatal("unhandledRejection", err));
50247
51666
  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;
51667
+ let permBrokerStop = null;
51668
+ const permSocket = process.env.ZELARI_PERM_SOCKET?.trim();
51669
+ if (permSocket) {
51670
+ try {
51671
+ const { startPermissionBroker: startPermissionBroker2 } = await Promise.resolve().then(() => (init_permissionBroker(), permissionBroker_exports));
51672
+ const { createBrokerPermissionHandler: createBrokerPermissionHandler2 } = await Promise.resolve().then(() => (init_brokerHandlers(), brokerHandlers_exports));
51673
+ const h = await startPermissionBroker2(permSocket, {
51674
+ onPermission: createBrokerPermissionHandler2({})
51675
+ });
51676
+ permBrokerStop = h.stop;
51677
+ process.stderr.write(
51678
+ `[zelari-code] permission broker listening on ${permSocket}
51679
+ `
51680
+ );
51681
+ } catch (err) {
51682
+ process.stderr.write(
51683
+ `[zelari-code] permission broker failed to start: ${err instanceof Error ? err.message : String(err)}
51684
+ `
51685
+ );
51686
+ }
50254
51687
  }
50255
- const providerStream = openaiCompatibleProvider({
50256
- providerId: provider,
50257
- apiKey: key.apiKey,
50258
- baseUrl: key.baseUrl,
50259
- model
51688
+ process.once("exit", () => {
51689
+ void permBrokerStop?.();
50260
51690
  });
51691
+ const { provider: resolvedProvider, model } = resolveHeadlessProvider(opts);
51692
+ let provider = resolvedProvider;
51693
+ const localCli = (process.env.ZELARI_LOCAL_CLI ?? "").trim();
51694
+ let providerStream;
51695
+ if (localCli) {
51696
+ provider = "local-cli";
51697
+ providerStream = createLocalCliProvider({ cli: localCli, model });
51698
+ } else {
51699
+ const key = await resolveHeadlessKey(provider);
51700
+ if ("error" in key) {
51701
+ process.stderr.write(`[zelari-code --headless] ${key.error}
51702
+ `);
51703
+ return 1;
51704
+ }
51705
+ providerStream = openaiCompatibleProvider({
51706
+ providerId: provider,
51707
+ apiKey: key.apiKey,
51708
+ baseUrl: key.baseUrl,
51709
+ model
51710
+ });
51711
+ }
50261
51712
  const mode = opts.mode ?? (opts.useCouncil ? "council" : "kraken");
50262
51713
  if (opts.output === "json") {
50263
51714
  emitEvent({
@@ -50316,11 +51767,11 @@ async function runHeadlessKrakenGraph(opts, provider, model) {
50316
51767
  try {
50317
51768
  let preflightGraph;
50318
51769
  if (opts.runPlan && opts.runPlan.trim() !== "") {
50319
- const planPath = path45.join(cwd, ".zelari", "radio", `plan-${opts.runPlan}.json`);
51770
+ const planPath = path46.join(cwd, ".zelari", "radio", `plan-${opts.runPlan}.json`);
50320
51771
  log(`loading pre-flight plan: ${planPath}`);
50321
51772
  let raw;
50322
51773
  try {
50323
- raw = await fs28.readFile(planPath, "utf8");
51774
+ raw = await fs29.readFile(planPath, "utf8");
50324
51775
  } catch (e) {
50325
51776
  log(`plan file not found: ${planPath} (${e.message})`);
50326
51777
  return 1;
@@ -50356,10 +51807,10 @@ async function runHeadlessKrakenGraph(opts, provider, model) {
50356
51807
  log(formatKrakenGraphAscii2(graph));
50357
51808
  if (opts.planOnly) {
50358
51809
  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");
51810
+ const planDir = path46.join(cwd, ".zelari", "radio");
51811
+ const planPath = path46.join(planDir, `plan-${planId}.json`);
51812
+ await fs29.mkdir(planDir, { recursive: true });
51813
+ await fs29.writeFile(planPath, JSON.stringify(graph, null, 2), "utf8");
50363
51814
  log(`plan-only: wrote ${planPath} (${graph.nodes.size} nodes)`);
50364
51815
  log(
50365
51816
  `re-run with ZELARI_KRAKEN_RUN_PLAN=${planId} to execute (or --run-plan <id> when the desktop wiring is in place)`
@@ -50709,7 +52160,11 @@ async function runHeadlessSingle(opts, provider, model, providerStream) {
50709
52160
  const initialMessages = [
50710
52161
  { role: "system", content: systemPrompt },
50711
52162
  ...historySeed,
50712
- { role: "user", content: effectiveTask }
52163
+ {
52164
+ role: "user",
52165
+ content: effectiveTask,
52166
+ ...opts.images && opts.images.length > 0 ? { images: opts.images } : {}
52167
+ }
50713
52168
  ];
50714
52169
  let pass = await runSinglePass(initialMessages, sessionId);
50715
52170
  const wantWrites = expectsDiskImplementation(
@@ -51382,7 +52837,8 @@ var BUILTIN_SKILL_MODULES = [
51382
52837
  "@zelari/core/skills/builtin/review",
51383
52838
  "@zelari/core/skills/builtin/testing",
51384
52839
  "@zelari/core/skills/builtin/schema-loop",
51385
- "@zelari/core/skills/builtin/computer-use-cua"
52840
+ "@zelari/core/skills/builtin/computer-use-cua",
52841
+ "@zelari/core/skills/builtin/qwen-mm-plugins"
51386
52842
  ];
51387
52843
  var builtinsLoaded = false;
51388
52844
  function ensureBuiltinSkillsLoadedSync() {
@@ -51545,7 +53001,7 @@ function upsertSkill(opts) {
51545
53001
  }
51546
53002
  dir = getProjectSkillsDir(root);
51547
53003
  }
51548
- const path47 = skillFilePath(dir, name);
53004
+ const path48 = skillFilePath(dir, name);
51549
53005
  const content = serializeSkillMd({
51550
53006
  name,
51551
53007
  description,
@@ -51554,13 +53010,13 @@ function upsertSkill(opts) {
51554
53010
  tools: opts.tools,
51555
53011
  cost: opts.cost
51556
53012
  });
51557
- const parsed = parseSkillMd(content, path47);
53013
+ const parsed = parseSkillMd(content, path48);
51558
53014
  if (!parsed) {
51559
53015
  return { ok: false, error: "Generated SKILL.md failed validation" };
51560
53016
  }
51561
- mkdirSync18(dirname9(path47), { recursive: true });
51562
- writeFileSync20(path47, content, "utf8");
51563
- return { ok: true, path: path47 };
53017
+ mkdirSync18(dirname9(path48), { recursive: true });
53018
+ writeFileSync20(path48, content, "utf8");
53019
+ return { ok: true, path: path48 };
51564
53020
  }
51565
53021
  function removeSkill(opts) {
51566
53022
  const name = opts.name.trim().toLowerCase();
@@ -51578,8 +53034,8 @@ function removeSkill(opts) {
51578
53034
  dir = getProjectSkillsDir(root);
51579
53035
  }
51580
53036
  const skillDir = join34(dir, name);
51581
- const path47 = skillFilePath(dir, name);
51582
- if (!existsSync42(path47) && !existsSync42(skillDir)) {
53037
+ const path48 = skillFilePath(dir, name);
53038
+ if (!existsSync42(path48) && !existsSync42(skillDir)) {
51583
53039
  return { ok: false, error: `Skill "${name}" not found in ${dir}` };
51584
53040
  }
51585
53041
  try {
@@ -51590,7 +53046,7 @@ function removeSkill(opts) {
51590
53046
  error: err instanceof Error ? err.message : String(err)
51591
53047
  };
51592
53048
  }
51593
- return { ok: true, path: path47 };
53049
+ return { ok: true, path: path48 };
51594
53050
  }
51595
53051
 
51596
53052
  // src/cli/generateSkillFromUrl.ts
@@ -51680,8 +53136,8 @@ function normalizeDraft(raw, sourceUrl, provider, model) {
51680
53136
  let name = String(o.name ?? "").trim().toLowerCase().replace(/[^a-z0-9-]+/g, "-").replace(/^-+|-+$/g, "").slice(0, 64);
51681
53137
  if (!name || !/^[a-z0-9][a-z0-9-]{0,63}$/.test(name)) {
51682
53138
  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";
53139
+ 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);
53140
+ name = path48 && /^[a-z0-9]/.test(path48) ? path48 : "imported-skill";
51685
53141
  } catch {
51686
53142
  name = "imported-skill";
51687
53143
  }
@@ -51863,6 +53319,21 @@ function pickRootComponent() {
51863
53319
  void runPluginsInstall(argv).then((code) => process.exit(code));
51864
53320
  return { kind: "done" };
51865
53321
  }
53322
+ if (argv.includes("--permission-mcp")) {
53323
+ const i = argv.indexOf("--permission-mcp");
53324
+ const socketPath = i >= 0 && argv[i + 1] ? argv[i + 1] : void 0;
53325
+ if (!socketPath) {
53326
+ console.error("[zelari-code --permission-mcp] a socket path argument is required");
53327
+ process.exit(1);
53328
+ }
53329
+ void Promise.resolve().then(() => (init_permissionCli(), permissionCli_exports)).then(({ runPermissionCli: runPermissionCli2 }) => runPermissionCli2(socketPath)).catch((err) => {
53330
+ console.error(
53331
+ `[zelari-code --permission-mcp] ${err instanceof Error ? err.message : String(err)}`
53332
+ );
53333
+ process.exit(1);
53334
+ });
53335
+ return { kind: "done" };
53336
+ }
51866
53337
  if (argv.includes("serve") || argv.includes("--serve")) {
51867
53338
  const { parseServeFlags: parseServeFlags2 } = (init_serve(), __toCommonJS(serve_exports));
51868
53339
  return { kind: "serve", serveOpts: parseServeFlags2(argv) ?? {} };
@@ -51943,7 +53414,7 @@ function pickRootComponent() {
51943
53414
  }
51944
53415
  if (argv.includes("--help") || argv.includes("-h")) {
51945
53416
  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"
53417
+ "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
53418
  );
51948
53419
  process.exit(0);
51949
53420
  }