zelari-code 1.23.0 → 1.25.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 (46) hide show
  1. package/README.md +3 -0
  2. package/bin/zelari-code.js +44 -12
  3. package/dist/cli/atMentions.js +179 -0
  4. package/dist/cli/atMentions.js.map +1 -0
  5. package/dist/cli/companion/config.js +172 -0
  6. package/dist/cli/companion/config.js.map +1 -0
  7. package/dist/cli/companion/runManager.js +250 -0
  8. package/dist/cli/companion/runManager.js.map +1 -0
  9. package/dist/cli/companion/serve.js +376 -0
  10. package/dist/cli/companion/serve.js.map +1 -0
  11. package/dist/cli/components/InputBar.js +1 -1
  12. package/dist/cli/components/InputBar.js.map +1 -1
  13. package/dist/cli/generateSkillFromUrl.js +236 -0
  14. package/dist/cli/generateSkillFromUrl.js.map +1 -0
  15. package/dist/cli/headless.js +6 -0
  16. package/dist/cli/headless.js.map +1 -1
  17. package/dist/cli/hooks/useSlashDispatch.js +19 -6
  18. package/dist/cli/hooks/useSlashDispatch.js.map +1 -1
  19. package/dist/cli/main.bundled.js +2617 -769
  20. package/dist/cli/main.bundled.js.map +4 -4
  21. package/dist/cli/main.js +155 -0
  22. package/dist/cli/main.js.map +1 -1
  23. package/dist/cli/missionSlice.js +19 -0
  24. package/dist/cli/missionSlice.js.map +1 -1
  25. package/dist/cli/provider/openai-compatible.js +164 -41
  26. package/dist/cli/provider/openai-compatible.js.map +1 -1
  27. package/dist/cli/runHeadless.js +33 -0
  28. package/dist/cli/runHeadless.js.map +1 -1
  29. package/dist/cli/skillCategories.js +15 -0
  30. package/dist/cli/skillCategories.js.map +1 -0
  31. package/dist/cli/skillConfigIo.js +285 -0
  32. package/dist/cli/skillConfigIo.js.map +1 -0
  33. package/dist/cli/slashCommands.js +14 -5
  34. package/dist/cli/slashCommands.js.map +1 -1
  35. package/dist/cli/slashHandlers/skills.js +30 -0
  36. package/dist/cli/slashHandlers/skills.js.map +1 -1
  37. package/dist/cli/traceStore.js +84 -0
  38. package/dist/cli/traceStore.js.map +1 -0
  39. package/dist/cli/triggerLock.js +79 -0
  40. package/dist/cli/triggerLock.js.map +1 -0
  41. package/dist/cli/zelariMission.js +77 -0
  42. package/dist/cli/zelariMission.js.map +1 -1
  43. package/dist/cli/zelariMission.test.js +44 -0
  44. package/dist/cli/zelariMission.test.js.map +1 -0
  45. package/package.json +4 -3
  46. package/scripts/start-companion-serve.ps1 +43 -0
@@ -4,6 +4,12 @@ var __defProp = Object.defineProperty;
4
4
  var __getOwnPropDesc = Object.getOwnPropertyDescriptor;
5
5
  var __getOwnPropNames = Object.getOwnPropertyNames;
6
6
  var __hasOwnProp = Object.prototype.hasOwnProperty;
7
+ var __require = /* @__PURE__ */ ((x) => typeof require !== "undefined" ? require : typeof Proxy !== "undefined" ? new Proxy(x, {
8
+ get: (a, b) => (typeof require !== "undefined" ? require : a)[b]
9
+ }) : x)(function(x) {
10
+ if (typeof require !== "undefined") return require.apply(this, arguments);
11
+ throw Error('Dynamic require of "' + x + '" is not supported');
12
+ });
7
13
  var __esm = (fn, res) => function __init() {
8
14
  return fn && (res = (0, fn[__getOwnPropNames(fn)[0]])(fn = 0)), res;
9
15
  };
@@ -21,6 +27,87 @@ var __copyProps = (to, from, except, desc) => {
21
27
  };
22
28
  var __toCommonJS = (mod) => __copyProps(__defProp({}, "__esModule", { value: true }), mod);
23
29
 
30
+ // src/cli/modelPricing.ts
31
+ function envOverride(model) {
32
+ const key = `ANATHEMA_PRICE_${model.toUpperCase().replace(/[^A-Z0-9]/g, "_")}`;
33
+ const raw = process.env[key] ?? process.env.ANATHEMA_PRICE_DEFAULT;
34
+ return parseRateOverride(raw);
35
+ }
36
+ function parseRateOverride(raw) {
37
+ if (!raw) return null;
38
+ const [inStr, outStr] = raw.split("/");
39
+ const input = Number.parseFloat(inStr);
40
+ const output = outStr !== void 0 ? Number.parseFloat(outStr) : input;
41
+ if (!Number.isFinite(input) || !Number.isFinite(output)) return null;
42
+ return { input, output };
43
+ }
44
+ function getModelRate(model) {
45
+ if (!model) return DEFAULT_RATE;
46
+ return envOverride(model) ?? PRICES_PER_MILLION[model] ?? DEFAULT_RATE;
47
+ }
48
+ function calculateCost(model, promptTokens, completionTokens, cachedPromptTokens = 0) {
49
+ if (!Number.isFinite(promptTokens) || promptTokens < 0) return 0;
50
+ if (!Number.isFinite(completionTokens) || completionTokens < 0) return 0;
51
+ const rate = getModelRate(model);
52
+ const cached2 = Number.isFinite(cachedPromptTokens) && cachedPromptTokens > 0 ? Math.min(cachedPromptTokens, promptTokens) : 0;
53
+ const uncachedPrompt = promptTokens - cached2;
54
+ const cachedRate = rate.cachedInput ?? rate.input * DEFAULT_CACHE_DISCOUNT;
55
+ const inputCost = uncachedPrompt / 1e6 * rate.input + cached2 / 1e6 * cachedRate;
56
+ const outputCost = completionTokens / 1e6 * rate.output;
57
+ return Number((inputCost + outputCost).toFixed(6));
58
+ }
59
+ function formatCost(usd) {
60
+ if (!Number.isFinite(usd) || usd < 0) return "$0.0000";
61
+ if (usd === 0) return "$0.0000";
62
+ if (usd < 1e-4) return "<$0.0001";
63
+ return `$${usd.toFixed(4)}`;
64
+ }
65
+ function formatTokens(tokens) {
66
+ if (!Number.isFinite(tokens) || tokens < 0) return "0";
67
+ if (tokens < 1e3) return `${tokens}`;
68
+ if (tokens < 1e6) return `${(tokens / 1e3).toFixed(1)}k`;
69
+ if (tokens < 1e9) return `${(tokens / 1e6).toFixed(2)}M`;
70
+ return `${(tokens / 1e9).toFixed(2)}B`;
71
+ }
72
+ var PRICES_PER_MILLION, DEFAULT_RATE, DEFAULT_CACHE_DISCOUNT;
73
+ var init_modelPricing = __esm({
74
+ "src/cli/modelPricing.ts"() {
75
+ "use strict";
76
+ PRICES_PER_MILLION = {
77
+ // xAI Grok (list prices; grok-4.5 default reasoning_effort is "high")
78
+ "grok-4.5": { input: 2, output: 6, cachedInput: 0.5 },
79
+ "grok-4.3": { input: 1.25, output: 2.5, cachedInput: 0.2 },
80
+ "grok-4": { input: 3, output: 15 },
81
+ "grok-4-fast": { input: 0.2, output: 0.5 },
82
+ "grok-3": { input: 3, output: 15 },
83
+ "grok-3-mini": { input: 0.3, output: 0.5 },
84
+ "grok-2-vision": { input: 2, output: 10 },
85
+ // GLM / Z.AI
86
+ "glm-4.6": { input: 0.6, output: 2.2 },
87
+ "glm-4.5": { input: 0.5, output: 2 },
88
+ "glm-4.5-air": { input: 0.1, output: 0.6 },
89
+ "glm-z1": { input: 0.5, output: 2 },
90
+ // MiniMax
91
+ "MiniMax-M2.5": { input: 0.2, output: 1.1 },
92
+ "MiniMax-M2": { input: 0.2, output: 1.1 },
93
+ "MiniMax-M2-her": { input: 0.3, output: 1.2 },
94
+ // DeepSeek (global platform) — estimated list prices; override via
95
+ // ANATHEMA_PRICE_DEEPSEEK_V4_FLASH / ANATHEMA_PRICE_DEEPSEEK_V4_PRO.
96
+ // DeepSeek prompt-cache hits are ~10× cheaper than a cache miss.
97
+ "deepseek-v4-flash": { input: 0.14, output: 0.28, cachedInput: 0.014 },
98
+ "deepseek-v4-pro": { input: 0.55, output: 2.19, cachedInput: 0.055 },
99
+ // OpenAI (for openai-compatible fallback)
100
+ "gpt-4o": { input: 2.5, output: 10 },
101
+ "gpt-4o-mini": { input: 0.15, output: 0.6 },
102
+ "gpt-4-turbo": { input: 10, output: 30 },
103
+ "o1-preview": { input: 15, output: 60 },
104
+ "o1-mini": { input: 3, output: 12 }
105
+ };
106
+ DEFAULT_RATE = { input: 1, output: 3 };
107
+ DEFAULT_CACHE_DISCOUNT = 0.25;
108
+ }
109
+ });
110
+
24
111
  // src/cli/grokOAuth.ts
25
112
  var grokOAuth_exports = {};
26
113
  __export(grokOAuth_exports, {
@@ -308,7 +395,7 @@ async function refreshGrokToken(options) {
308
395
  return parseTokenResponseBody(obj, accessToken);
309
396
  }
310
397
  async function openBrowser(url2) {
311
- const { spawn: spawn11 } = await import("node:child_process");
398
+ const { spawn: spawn12 } = await import("node:child_process");
312
399
  const cmd = (() => {
313
400
  switch (process.platform) {
314
401
  case "darwin":
@@ -319,18 +406,18 @@ async function openBrowser(url2) {
319
406
  return { bin: "xdg-open", args: [url2] };
320
407
  }
321
408
  })();
322
- return new Promise((resolve, reject) => {
409
+ return new Promise((resolve3, reject) => {
323
410
  try {
324
- const child = spawn11(cmd.bin, cmd.args, {
411
+ const child = spawn12(cmd.bin, cmd.args, {
325
412
  stdio: "ignore",
326
413
  detached: true
327
414
  });
328
415
  child.on("error", (err) => reject(err));
329
416
  child.on("spawn", () => {
330
417
  child.unref();
331
- resolve();
418
+ resolve3();
332
419
  });
333
- setTimeout(() => resolve(), 100);
420
+ setTimeout(() => resolve3(), 100);
334
421
  } catch (err) {
335
422
  reject(err);
336
423
  }
@@ -754,6 +841,180 @@ var init_providerConfig = __esm({
754
841
  }
755
842
  });
756
843
 
844
+ // src/cli/modelDiscovery.ts
845
+ import { promises as fs3, existsSync as existsSync3, readFileSync as readFileSync3 } from "node:fs";
846
+ import { homedir } from "node:os";
847
+ import path4 from "node:path";
848
+ async function resolveDiscoveryBaseUrl(provider, options) {
849
+ if (options.baseUrl) return options.baseUrl;
850
+ const { getCustomEndpoint: getCustomEndpoint2 } = await Promise.resolve().then(() => (init_providerConfig(), providerConfig_exports));
851
+ const custom2 = getCustomEndpoint2(provider);
852
+ if (custom2) return custom2;
853
+ if (provider === "openai-compatible") {
854
+ const envBase = process.env.OPENAI_BASE_URL;
855
+ if (envBase && envBase.trim().length > 0) return envBase;
856
+ }
857
+ return PROVIDER_BASE_URLS[provider];
858
+ }
859
+ function defaultModelsFilePath() {
860
+ return process.env.ANATHEMA_MODELS_FILE ?? path4.join(homedir(), ".tmp", "zelari-code", "models.json");
861
+ }
862
+ function getModelsFilePath() {
863
+ return defaultModelsFilePath();
864
+ }
865
+ function loadModelsRegistry(file2 = getModelsFilePath()) {
866
+ if (!existsSync3(file2)) return {};
867
+ try {
868
+ const raw = readFileSync3(file2, "utf-8");
869
+ const parsed = JSON.parse(raw);
870
+ if (!parsed || typeof parsed !== "object") return {};
871
+ return parsed;
872
+ } catch {
873
+ return {};
874
+ }
875
+ }
876
+ function getCachedModels(provider, file2 = getModelsFilePath()) {
877
+ const registry3 = loadModelsRegistry(file2);
878
+ return registry3[provider];
879
+ }
880
+ function isModelsCacheStale(provider, maxAgeMs = 6 * 60 * 60 * 1e3, file2 = getModelsFilePath(), now = Date.now()) {
881
+ const entry = getCachedModels(provider, file2);
882
+ if (!entry) return true;
883
+ return now - entry.fetchedAt > maxAgeMs;
884
+ }
885
+ async function withRegistryLock(fn) {
886
+ const next = writeChain.then(async () => fn());
887
+ writeChain = next.catch(() => void 0);
888
+ return next;
889
+ }
890
+ async function readModifyWriteRegistry(mutator, file2 = getModelsFilePath()) {
891
+ return withRegistryLock(async () => {
892
+ const onDisk = loadModelsRegistry(file2);
893
+ const merged = mutator(onDisk);
894
+ await fs3.mkdir(path4.dirname(file2), { recursive: true });
895
+ const tmp = `${file2}.tmp-${process.pid}-${Date.now()}`;
896
+ await fs3.writeFile(tmp, JSON.stringify(merged, null, 2), "utf-8");
897
+ await fs3.rename(tmp, file2);
898
+ return merged;
899
+ });
900
+ }
901
+ async function resolveAuthToken(provider, options) {
902
+ if (options.authToken) return options.authToken;
903
+ const { resolveApiKeyWithMeta: resolveApiKeyWithMeta2, getOAuthToken: getOAuthToken2 } = await Promise.resolve().then(() => (init_keyStore(), keyStore_exports));
904
+ if (provider === "grok") {
905
+ const oauth = getOAuthToken2("grok");
906
+ if (oauth?.apiKey) return oauth.apiKey;
907
+ }
908
+ const resolved = await resolveApiKeyWithMeta2(provider);
909
+ return resolved?.apiKey;
910
+ }
911
+ function parseOpenAIModelsResponse(json2, baseUrl) {
912
+ if (!json2 || typeof json2 !== "object") return [];
913
+ const obj = json2;
914
+ if (!Array.isArray(obj.data)) return [];
915
+ const models = [];
916
+ for (const m of obj.data) {
917
+ if (!m || typeof m.id !== "string") continue;
918
+ const out = { id: m.id };
919
+ if (typeof m.created === "number") out.created = m.created;
920
+ if (typeof m.owned_by === "string") out.ownedBy = m.owned_by;
921
+ const ctx = m.context_length;
922
+ if (typeof ctx === "number") out.contextLength = ctx;
923
+ models.push(out);
924
+ }
925
+ models.sort((a, b) => a.id.localeCompare(b.id));
926
+ return models;
927
+ }
928
+ async function discoverModelsForProvider(provider, options = {}) {
929
+ const baseUrl = await resolveDiscoveryBaseUrl(provider, options);
930
+ if (!baseUrl || baseUrl.trim().length === 0) {
931
+ throw new ModelDiscoveryError(
932
+ `No base URL for provider "${provider}" \u2014 set one with /provider custom <url> before discovering models`,
933
+ "no_base_url"
934
+ );
935
+ }
936
+ const url2 = `${baseUrl.replace(/\/$/, "")}/models`;
937
+ const authToken = await resolveAuthToken(provider, options);
938
+ const fetchImpl = options.fetchImpl ?? fetch;
939
+ let response;
940
+ try {
941
+ const headers = { Accept: "application/json" };
942
+ if (authToken) headers.Authorization = `Bearer ${authToken}`;
943
+ response = await fetchImpl(url2, { method: "GET", headers });
944
+ } catch (err) {
945
+ throw new ModelDiscoveryError(
946
+ `Network error contacting ${url2}: ${err instanceof Error ? err.message : String(err)}`,
947
+ "network_error"
948
+ );
949
+ }
950
+ if (!response.ok) {
951
+ const body = await response.text().catch(() => "");
952
+ throw new ModelDiscoveryError(
953
+ `HTTP ${response.status} from ${url2}: ${body.slice(0, 200)}`,
954
+ `http_${response.status}`
955
+ );
956
+ }
957
+ let json2;
958
+ try {
959
+ json2 = await response.json();
960
+ } catch (err) {
961
+ throw new ModelDiscoveryError(
962
+ `Invalid JSON from ${url2}: ${err instanceof Error ? err.message : String(err)}`,
963
+ "invalid_json"
964
+ );
965
+ }
966
+ const models = parseOpenAIModelsResponse(json2, baseUrl);
967
+ if (models.length === 0) {
968
+ throw new ModelDiscoveryError(
969
+ `Provider ${provider} returned 0 models \u2014 refusing to overwrite cache`,
970
+ "empty_response"
971
+ );
972
+ }
973
+ const entry = {
974
+ models,
975
+ fetchedAt: Date.now(),
976
+ baseUrl
977
+ };
978
+ if (!options.skipCacheWrite) {
979
+ const file2 = getModelsFilePath();
980
+ await readModifyWriteRegistry((current) => {
981
+ current[provider] = entry;
982
+ return current;
983
+ }, file2);
984
+ }
985
+ return entry;
986
+ }
987
+ function discoverModelsInBackground(provider, options = {}) {
988
+ discoverModelsForProvider(provider, options).catch((err) => {
989
+ if (err instanceof ModelDiscoveryError && options.onError) {
990
+ options.onError(err);
991
+ }
992
+ });
993
+ }
994
+ var PROVIDER_BASE_URLS, writeChain, ModelDiscoveryError;
995
+ var init_modelDiscovery = __esm({
996
+ "src/cli/modelDiscovery.ts"() {
997
+ "use strict";
998
+ PROVIDER_BASE_URLS = {
999
+ "grok": "https://api.x.ai/v1",
1000
+ // Must match PROVIDER_ENDPOINTS in provider/openai-compatible.ts (chat host).
1001
+ "glm": "https://api.z.ai/api/coding/paas/v4",
1002
+ "minimax": "https://api.minimax.io/v1",
1003
+ // Must match PROVIDER_ENDPOINTS in provider/openai-compatible.ts (chat host).
1004
+ "deepseek": "https://api.deepseek.com",
1005
+ "openai-compatible": "https://api.x.ai/v1"
1006
+ };
1007
+ writeChain = Promise.resolve();
1008
+ ModelDiscoveryError = class extends Error {
1009
+ constructor(message, code) {
1010
+ super(message);
1011
+ this.code = code;
1012
+ this.name = "ModelDiscoveryError";
1013
+ }
1014
+ };
1015
+ }
1016
+ });
1017
+
757
1018
  // packages/core/dist/agents/skills.js
758
1019
  function getSkillsByCategory(cat) {
759
1020
  return SKILL_CATALOG.filter((s) => s.category === cat);
@@ -1543,10 +1804,10 @@ function mergeDefs(...defs) {
1543
1804
  function cloneDef(schema) {
1544
1805
  return mergeDefs(schema._zod.def);
1545
1806
  }
1546
- function getElementAtPath(obj, path38) {
1547
- if (!path38)
1807
+ function getElementAtPath(obj, path40) {
1808
+ if (!path40)
1548
1809
  return obj;
1549
- return path38.reduce((acc, key) => acc?.[key], obj);
1810
+ return path40.reduce((acc, key) => acc?.[key], obj);
1550
1811
  }
1551
1812
  function promiseAllObject(promisesObj) {
1552
1813
  const keys = Object.keys(promisesObj);
@@ -1874,11 +2135,11 @@ function explicitlyAborted(x, startIndex = 0) {
1874
2135
  }
1875
2136
  return false;
1876
2137
  }
1877
- function prefixIssues(path38, issues) {
2138
+ function prefixIssues(path40, issues) {
1878
2139
  return issues.map((iss) => {
1879
2140
  var _a3;
1880
2141
  (_a3 = iss).path ?? (_a3.path = []);
1881
- iss.path.unshift(path38);
2142
+ iss.path.unshift(path40);
1882
2143
  return iss;
1883
2144
  });
1884
2145
  }
@@ -2096,16 +2357,16 @@ function flattenError(error51, mapper = (issue2) => issue2.message) {
2096
2357
  }
2097
2358
  function formatError(error51, mapper = (issue2) => issue2.message) {
2098
2359
  const fieldErrors = { _errors: [] };
2099
- const processError = (error52, path38 = []) => {
2360
+ const processError = (error52, path40 = []) => {
2100
2361
  for (const issue2 of error52.issues) {
2101
2362
  if (issue2.code === "invalid_union" && issue2.errors.length) {
2102
- issue2.errors.map((issues) => processError({ issues }, [...path38, ...issue2.path]));
2363
+ issue2.errors.map((issues) => processError({ issues }, [...path40, ...issue2.path]));
2103
2364
  } else if (issue2.code === "invalid_key") {
2104
- processError({ issues: issue2.issues }, [...path38, ...issue2.path]);
2365
+ processError({ issues: issue2.issues }, [...path40, ...issue2.path]);
2105
2366
  } else if (issue2.code === "invalid_element") {
2106
- processError({ issues: issue2.issues }, [...path38, ...issue2.path]);
2367
+ processError({ issues: issue2.issues }, [...path40, ...issue2.path]);
2107
2368
  } else {
2108
- const fullpath = [...path38, ...issue2.path];
2369
+ const fullpath = [...path40, ...issue2.path];
2109
2370
  if (fullpath.length === 0) {
2110
2371
  fieldErrors._errors.push(mapper(issue2));
2111
2372
  } else {
@@ -2132,17 +2393,17 @@ function formatError(error51, mapper = (issue2) => issue2.message) {
2132
2393
  }
2133
2394
  function treeifyError(error51, mapper = (issue2) => issue2.message) {
2134
2395
  const result = { errors: [] };
2135
- const processError = (error52, path38 = []) => {
2396
+ const processError = (error52, path40 = []) => {
2136
2397
  var _a3, _b;
2137
2398
  for (const issue2 of error52.issues) {
2138
2399
  if (issue2.code === "invalid_union" && issue2.errors.length) {
2139
- issue2.errors.map((issues) => processError({ issues }, [...path38, ...issue2.path]));
2400
+ issue2.errors.map((issues) => processError({ issues }, [...path40, ...issue2.path]));
2140
2401
  } else if (issue2.code === "invalid_key") {
2141
- processError({ issues: issue2.issues }, [...path38, ...issue2.path]);
2402
+ processError({ issues: issue2.issues }, [...path40, ...issue2.path]);
2142
2403
  } else if (issue2.code === "invalid_element") {
2143
- processError({ issues: issue2.issues }, [...path38, ...issue2.path]);
2404
+ processError({ issues: issue2.issues }, [...path40, ...issue2.path]);
2144
2405
  } else {
2145
- const fullpath = [...path38, ...issue2.path];
2406
+ const fullpath = [...path40, ...issue2.path];
2146
2407
  if (fullpath.length === 0) {
2147
2408
  result.errors.push(mapper(issue2));
2148
2409
  continue;
@@ -2174,8 +2435,8 @@ function treeifyError(error51, mapper = (issue2) => issue2.message) {
2174
2435
  }
2175
2436
  function toDotPath(_path) {
2176
2437
  const segs = [];
2177
- const path38 = _path.map((seg) => typeof seg === "object" ? seg.key : seg);
2178
- for (const seg of path38) {
2438
+ const path40 = _path.map((seg) => typeof seg === "object" ? seg.key : seg);
2439
+ for (const seg of path40) {
2179
2440
  if (typeof seg === "number")
2180
2441
  segs.push(`[${seg}]`);
2181
2442
  else if (typeof seg === "symbol")
@@ -15678,13 +15939,13 @@ function resolveRef(ref, ctx) {
15678
15939
  if (!ref.startsWith("#")) {
15679
15940
  throw new Error("External $ref is not supported, only local refs (#/...) are allowed");
15680
15941
  }
15681
- const path38 = ref.slice(1).split("/").filter(Boolean);
15682
- if (path38.length === 0) {
15942
+ const path40 = ref.slice(1).split("/").filter(Boolean);
15943
+ if (path40.length === 0) {
15683
15944
  return ctx.rootSchema;
15684
15945
  }
15685
15946
  const defsKey = ctx.version === "draft-2020-12" ? "$defs" : "definitions";
15686
- if (path38[0] === defsKey) {
15687
- const key = path38[1];
15947
+ if (path40[0] === defsKey) {
15948
+ const key = path40[1];
15688
15949
  if (!key || !ctx.defs[key]) {
15689
15950
  throw new Error(`Reference not found: ${ref}`);
15690
15951
  }
@@ -16737,12 +16998,12 @@ function withNodeDirOnPath(base) {
16737
16998
  const nodeDir = dirname(process.execPath);
16738
16999
  if (!nodeDir)
16739
17000
  return env;
16740
- const sep = process.platform === "win32" ? ";" : ":";
17001
+ const sep2 = process.platform === "win32" ? ";" : ":";
16741
17002
  const current = env.PATH ?? env.Path ?? "";
16742
- const parts = current.split(sep).filter((p3) => p3.length > 0);
17003
+ const parts = current.split(sep2).filter((p3) => p3.length > 0);
16743
17004
  const has = parts.some((p3) => p3.toLowerCase() === nodeDir.toLowerCase());
16744
17005
  if (!has) {
16745
- env.PATH = `${nodeDir}${sep}${current}`;
17006
+ env.PATH = `${nodeDir}${sep2}${current}`;
16746
17007
  }
16747
17008
  } catch {
16748
17009
  }
@@ -16769,7 +17030,7 @@ var init_shell = __esm({
16769
17030
  timeoutMs: 6e4,
16770
17031
  inputSchema: BashArgsSchema,
16771
17032
  execute: async (args, ctx) => {
16772
- return new Promise((resolve) => {
17033
+ return new Promise((resolve3) => {
16773
17034
  const start = Date.now();
16774
17035
  const cwd = args.cwd ?? ctx.cwd;
16775
17036
  const resolved = resolveShell();
@@ -16817,14 +17078,14 @@ var init_shell = __esm({
16817
17078
  });
16818
17079
  const timer = setTimeout(() => {
16819
17080
  child.kill("SIGTERM");
16820
- resolve(typedErr(`Bash command timed out after ${args.timeoutMs}ms. If it was waiting for interactive input: stdin is closed \u2014 pass non-interactive flags or create the files directly instead of retrying.`));
17081
+ resolve3(typedErr(`Bash command timed out after ${args.timeoutMs}ms. If it was waiting for interactive input: stdin is closed \u2014 pass non-interactive flags or create the files directly instead of retrying.`));
16821
17082
  }, args.timeoutMs);
16822
17083
  child.on("close", (code) => {
16823
17084
  clearTimeout(timer);
16824
17085
  const cappedStdout = stdout.slice(0, maxBuffer);
16825
17086
  const cappedStderr = stderr.slice(0, maxBuffer);
16826
17087
  const interactive = INTERACTIVE_CANCEL_RE.test(cappedStdout) || INTERACTIVE_CANCEL_RE.test(cappedStderr);
16827
- resolve(typedOk({
17088
+ resolve3(typedOk({
16828
17089
  stdout: cappedStdout,
16829
17090
  stderr: cappedStderr,
16830
17091
  exitCode: code ?? -1,
@@ -16835,7 +17096,7 @@ var init_shell = __esm({
16835
17096
  });
16836
17097
  child.on("error", (err) => {
16837
17098
  clearTimeout(timer);
16838
- resolve(typedErr(err.message));
17099
+ resolve3(typedErr(err.message));
16839
17100
  });
16840
17101
  });
16841
17102
  }
@@ -17848,11 +18109,11 @@ var init_tools = __esm({
17848
18109
  if (!ctx.addDocument)
17849
18110
  return "Knowledge vault tool not available.";
17850
18111
  const title = args["title"] || "New Document";
17851
- const path38 = args["path"] || `notes/${title.toLowerCase().replace(/[^a-z0-9]+/g, "-")}`;
18112
+ const path40 = args["path"] || `notes/${title.toLowerCase().replace(/[^a-z0-9]+/g, "-")}`;
17852
18113
  const content = args["content"] || "";
17853
18114
  const tags = args["tags"] || [];
17854
18115
  ctx.addDocument({
17855
- path: path38,
18116
+ path: path40,
17856
18117
  title,
17857
18118
  content,
17858
18119
  format: "markdown",
@@ -17861,7 +18122,7 @@ var init_tools = __esm({
17861
18122
  workspaceId: ctx.workspaceId
17862
18123
  });
17863
18124
  ctx.addActivity("vault", "created document", title);
17864
- return `Document "${title}" created at "${path38}".`;
18125
+ return `Document "${title}" created at "${path40}".`;
17865
18126
  }
17866
18127
  }
17867
18128
  ];
@@ -20548,8 +20809,8 @@ function wrapLegacyStream(legacyStream, params, _messages, _tools) {
20548
20809
  }
20549
20810
  if (done)
20550
20811
  break;
20551
- await new Promise((resolve) => {
20552
- resolveNext = resolve;
20812
+ await new Promise((resolve3) => {
20813
+ resolveNext = resolve3;
20553
20814
  });
20554
20815
  }
20555
20816
  await promise2;
@@ -20668,19 +20929,60 @@ __export(openai_compatible_exports, {
20668
20929
  resolveBaseUrl: () => resolveBaseUrl
20669
20930
  });
20670
20931
  function abortableSleep(ms, signal) {
20671
- return new Promise((resolve) => {
20672
- if (signal?.aborted) return resolve();
20673
- const t = setTimeout(resolve, ms);
20932
+ return new Promise((resolve3) => {
20933
+ if (signal?.aborted) return resolve3();
20934
+ const t = setTimeout(resolve3, ms);
20674
20935
  signal?.addEventListener(
20675
20936
  "abort",
20676
20937
  () => {
20677
20938
  clearTimeout(t);
20678
- resolve();
20939
+ resolve3();
20679
20940
  },
20680
20941
  { once: true }
20681
20942
  );
20682
20943
  });
20683
20944
  }
20945
+ function isTimeoutAbortMessage(msg) {
20946
+ const m = msg.toLowerCase();
20947
+ return m.includes("aborted due to timeout") || m.includes("timeout") || m.includes("the operation was aborted");
20948
+ }
20949
+ async function readChunkWithTimeout(reader, opts) {
20950
+ if (opts.signal?.aborted) {
20951
+ throw new Error("aborted");
20952
+ }
20953
+ const remaining = opts.deadlineMs - Date.now();
20954
+ if (remaining <= 0) {
20955
+ throw new Error(
20956
+ `Provider stream exceeded max duration (${Math.round(PROVIDER_STREAM_MAX_MS / 1e3)}s). Raise ZELARI_PROVIDER_STREAM_MAX_MS if needed.`
20957
+ );
20958
+ }
20959
+ const waitMs = Math.min(opts.idleMs, remaining);
20960
+ let idleTimer;
20961
+ let onAbort;
20962
+ try {
20963
+ return await Promise.race([
20964
+ reader.read(),
20965
+ new Promise((_, reject) => {
20966
+ idleTimer = setTimeout(() => {
20967
+ reject(
20968
+ new Error(
20969
+ `Provider stream idle for ${Math.round(waitMs / 1e3)}s (no tokens). The model/gateway stalled \u2014 try again or switch model. Override with ZELARI_PROVIDER_STREAM_IDLE_MS.`
20970
+ )
20971
+ );
20972
+ }, waitMs);
20973
+ if (opts.signal) {
20974
+ onAbort = () => reject(new Error("aborted"));
20975
+ opts.signal.addEventListener("abort", onAbort, { once: true });
20976
+ }
20977
+ })
20978
+ ]);
20979
+ } finally {
20980
+ if (idleTimer) clearTimeout(idleTimer);
20981
+ if (onAbort && opts.signal) {
20982
+ opts.signal.removeEventListener("abort", onAbort);
20983
+ }
20984
+ }
20985
+ }
20684
20986
  function backoffDelay(attempt, retryAfterHeader) {
20685
20987
  if (retryAfterHeader) {
20686
20988
  const seconds = Number.parseFloat(retryAfterHeader);
@@ -20782,31 +21084,46 @@ function openaiCompatibleProvider(config2) {
20782
21084
  return;
20783
21085
  }
20784
21086
  try {
20785
- const timeoutSignal = AbortSignal.timeout(PROVIDER_TIMEOUT_MS);
20786
- const fetchSignal = params.signal ? AbortSignal.any([params.signal, timeoutSignal]) : timeoutSignal;
20787
- response = await fetch(`${config2.baseUrl}/chat/completions`, {
20788
- method: "POST",
20789
- headers: {
20790
- "Content-Type": "application/json",
20791
- Authorization: `Bearer ${config2.apiKey}`
20792
- },
20793
- body: JSON.stringify(body),
20794
- // Use `params.signal` (per-call AbortSignal from AgentHarness
20795
- // controller) so `.cancel()` actually aborts the HTTP request.
20796
- // `config.signal` is the factory-level signal, typically undefined.
20797
- // v0.6.0 audit HIGH-2.
20798
- // v1.10.0: also apply PROVIDER_TIMEOUT_MS so a stalled connection
20799
- // can't hang the harness forever.
20800
- signal: fetchSignal
20801
- });
21087
+ const connectController = new AbortController();
21088
+ const connectTimer = setTimeout(() => {
21089
+ connectController.abort(
21090
+ new Error(
21091
+ `Provider connect timeout after ${Math.round(PROVIDER_CONNECT_TIMEOUT_MS / 1e3)}s (no response headers). Override ZELARI_PROVIDER_CONNECT_TIMEOUT_MS.`
21092
+ )
21093
+ );
21094
+ }, PROVIDER_CONNECT_TIMEOUT_MS);
21095
+ const signals = [connectController.signal];
21096
+ if (params.signal) signals.push(params.signal);
21097
+ const fetchSignal = signals.length === 1 ? signals[0] : AbortSignal.any(signals);
21098
+ try {
21099
+ response = await fetch(`${config2.baseUrl}/chat/completions`, {
21100
+ method: "POST",
21101
+ headers: {
21102
+ "Content-Type": "application/json",
21103
+ Authorization: `Bearer ${config2.apiKey}`
21104
+ },
21105
+ body: JSON.stringify(body),
21106
+ // Cancel aborts the HTTP request; stream idle is enforced below
21107
+ // per-chunk so active multi-minute streams are not killed.
21108
+ signal: fetchSignal
21109
+ });
21110
+ } finally {
21111
+ clearTimeout(connectTimer);
21112
+ }
20802
21113
  } catch (err) {
20803
21114
  lastStatus = 0;
20804
- lastErrText = err instanceof Error ? err.message : String(err);
21115
+ const asErr = err instanceof Error ? err : null;
21116
+ const causeMsg = asErr?.cause instanceof Error ? asErr.cause.message : typeof asErr?.cause === "string" ? String(asErr.cause) : "";
21117
+ lastErrText = (causeMsg && causeMsg.includes("Provider connect") ? causeMsg : null) || asErr?.message || String(err);
21118
+ if (isTimeoutAbortMessage(lastErrText) && !lastErrText.includes("Provider connect")) {
21119
+ lastErrText = `Provider connect timeout after ${Math.round(PROVIDER_CONNECT_TIMEOUT_MS / 1e3)}s (no response headers). Last error: ${lastErrText}`;
21120
+ }
20805
21121
  if (params.signal?.aborted) {
20806
21122
  yield { kind: "error", message: "aborted" };
20807
21123
  return;
20808
21124
  }
20809
- if (attempt < MAX_RETRIES) {
21125
+ const maxAttempts = isTimeoutAbortMessage(lastErrText) ? Math.min(1, MAX_RETRIES) : MAX_RETRIES;
21126
+ if (attempt < maxAttempts) {
20810
21127
  await abortableSleep(backoffDelay(attempt, null), params.signal);
20811
21128
  continue;
20812
21129
  }
@@ -20861,9 +21178,30 @@ function openaiCompatibleProvider(config2) {
20861
21178
  }
20862
21179
  toolCallAccumulator.clear();
20863
21180
  };
21181
+ const streamDeadline = Date.now() + PROVIDER_STREAM_MAX_MS;
20864
21182
  try {
20865
21183
  while (true) {
20866
- const { value, done } = await reader.read();
21184
+ let chunk;
21185
+ try {
21186
+ chunk = await readChunkWithTimeout(reader, {
21187
+ idleMs: PROVIDER_STREAM_IDLE_MS,
21188
+ deadlineMs: streamDeadline,
21189
+ signal: params.signal
21190
+ });
21191
+ } catch (err) {
21192
+ const msg = err instanceof Error ? err.message : String(err);
21193
+ if (msg === "aborted" || params.signal?.aborted) {
21194
+ yield { kind: "error", message: "aborted" };
21195
+ return;
21196
+ }
21197
+ try {
21198
+ await reader.cancel(msg);
21199
+ } catch {
21200
+ }
21201
+ yield { kind: "error", message: msg };
21202
+ return;
21203
+ }
21204
+ const { value, done } = chunk;
20867
21205
  if (done) break;
20868
21206
  buffer += decoder.decode(value, { stream: true });
20869
21207
  const lines = buffer.split("\n");
@@ -20989,7 +21327,7 @@ async function providerConfigFor(providerId) {
20989
21327
  providerId
20990
21328
  };
20991
21329
  }
20992
- var RETRYABLE_STATUSES, MAX_RETRIES, BACKOFF_BASE_MS, BACKOFF_CAP_MS, PROVIDER_TIMEOUT_MS, PROVIDER_ENDPOINTS;
21330
+ 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;
20993
21331
  var init_openai_compatible = __esm({
20994
21332
  "src/cli/provider/openai-compatible.ts"() {
20995
21333
  "use strict";
@@ -21003,10 +21341,20 @@ var init_openai_compatible = __esm({
21003
21341
  })();
21004
21342
  BACKOFF_BASE_MS = 500;
21005
21343
  BACKOFF_CAP_MS = 8e3;
21006
- PROVIDER_TIMEOUT_MS = (() => {
21007
- const raw = process.env.ZELARI_PROVIDER_TIMEOUT_MS;
21344
+ PROVIDER_CONNECT_TIMEOUT_MS = (() => {
21345
+ const raw = process.env.ZELARI_PROVIDER_CONNECT_TIMEOUT_MS;
21346
+ const n = raw ? Number.parseInt(raw, 10) : 9e4;
21347
+ return Number.isFinite(n) && n >= 5e3 ? n : 9e4;
21348
+ })();
21349
+ PROVIDER_STREAM_IDLE_MS = (() => {
21350
+ const raw = process.env.ZELARI_PROVIDER_STREAM_IDLE_MS ?? process.env.ZELARI_PROVIDER_TIMEOUT_MS;
21008
21351
  const n = raw ? Number.parseInt(raw, 10) : 3e5;
21009
- return Number.isFinite(n) && n >= 1e4 ? n : 3e5;
21352
+ return Number.isFinite(n) && n >= 15e3 ? n : 3e5;
21353
+ })();
21354
+ PROVIDER_STREAM_MAX_MS = (() => {
21355
+ const raw = process.env.ZELARI_PROVIDER_STREAM_MAX_MS;
21356
+ const n = raw ? Number.parseInt(raw, 10) : 18e5;
21357
+ return Number.isFinite(n) && n >= 6e4 ? n : 18e5;
21010
21358
  })();
21011
21359
  PROVIDER_ENDPOINTS = {
21012
21360
  "openai-compatible": "https://api.x.ai/v1",
@@ -21059,9 +21407,9 @@ function spillToolOutput(fullText, meta3) {
21059
21407
  const rnd = randomBytes(3).toString("hex");
21060
21408
  const safeTool = (meta3?.toolName ?? "tool").replace(/[^a-zA-Z0-9._-]+/g, "_").slice(0, 32);
21061
21409
  const file2 = `${stamp}-${safeTool}-${hash3}-${rnd}.txt`;
21062
- const path38 = join(dir, file2);
21063
- writeFileSync5(path38, fullText, "utf8");
21064
- return path38;
21410
+ const path40 = join(dir, file2);
21411
+ writeFileSync5(path40, fullText, "utf8");
21412
+ return path40;
21065
21413
  } catch {
21066
21414
  return null;
21067
21415
  }
@@ -21107,10 +21455,10 @@ function truncateToolResult(text, capOrOpts = TOOL_RESULT_LINE_CAP) {
21107
21455
  ${tail}`;
21108
21456
  }
21109
21457
  if (doSpill) {
21110
- const path38 = spillToolOutput(text, { toolName: opts.toolName });
21111
- if (path38) {
21458
+ const path40 = spillToolOutput(text, { toolName: opts.toolName });
21459
+ if (path40) {
21112
21460
  const spillNote = `
21113
- \u2026 [full output spilled to: ${path38} \u2014 re-read with read_file if you need the complete text] \u2026`;
21461
+ \u2026 [full output spilled to: ${path40} \u2014 re-read with read_file if you need the complete text] \u2026`;
21114
21462
  if (preview.includes("] \u2026\n")) {
21115
21463
  preview = preview.replace("] \u2026\n", `] \u2026${spillNote}
21116
21464
  `);
@@ -21581,14 +21929,14 @@ var init_engine = __esm({
21581
21929
  parse: parseRuffJson
21582
21930
  }
21583
21931
  ];
21584
- defaultRunner = (cmd, args, opts) => new Promise((resolve) => {
21932
+ defaultRunner = (cmd, args, opts) => new Promise((resolve3) => {
21585
21933
  let stdout = "";
21586
21934
  let stderr = "";
21587
21935
  let settled = false;
21588
21936
  const done = (r) => {
21589
21937
  if (settled) return;
21590
21938
  settled = true;
21591
- resolve(r);
21939
+ resolve3(r);
21592
21940
  };
21593
21941
  let child;
21594
21942
  try {
@@ -22257,12 +22605,12 @@ var init_client = __esm({
22257
22605
  if (this.closed) return Promise.reject(new Error("LSP transport is closed"));
22258
22606
  const id = this.nextId++;
22259
22607
  const msg = { jsonrpc: "2.0", id, method, ...params !== void 0 ? { params } : {} };
22260
- return new Promise((resolve, reject) => {
22608
+ return new Promise((resolve3, reject) => {
22261
22609
  const timer = setTimeout(() => {
22262
22610
  this.pending.delete(id);
22263
22611
  reject(new Error(`LSP request "${method}" timed out after ${this.timeoutMs}ms`));
22264
22612
  }, this.timeoutMs);
22265
- this.pending.set(id, { resolve, reject, timer });
22613
+ this.pending.set(id, { resolve: resolve3, reject, timer });
22266
22614
  this.transport.send(encodeMessage(msg));
22267
22615
  });
22268
22616
  }
@@ -22528,8 +22876,8 @@ var init_manager = __esm({
22528
22876
  if (cached2 !== void 0) return cached2;
22529
22877
  let resolveInit;
22530
22878
  let rejectInit;
22531
- const initialized = new Promise((resolve, reject) => {
22532
- resolveInit = resolve;
22879
+ const initialized = new Promise((resolve3, reject) => {
22880
+ resolveInit = resolve3;
22533
22881
  rejectInit = reject;
22534
22882
  });
22535
22883
  let entry = null;
@@ -23248,8 +23596,8 @@ async function runBrowserCheck(options, loader) {
23248
23596
  const failedRequests = [];
23249
23597
  const evaluateResults = [];
23250
23598
  const base = { ok: false, consoleErrors, pageErrors, failedRequests };
23251
- const resolve = loader ?? (() => loadPlaywright(options.cwd ?? process.cwd()));
23252
- const pw = await resolve();
23599
+ const resolve3 = loader ?? (() => loadPlaywright(options.cwd ?? process.cwd()));
23600
+ const pw = await resolve3();
23253
23601
  if (!pw) {
23254
23602
  return {
23255
23603
  ...base,
@@ -23542,21 +23890,21 @@ function normalizeAuth(auth) {
23542
23890
  return "agent";
23543
23891
  }
23544
23892
  function readSecrets() {
23545
- const path38 = getSshSecretsPath();
23546
- if (!existsSync11(path38)) return {};
23893
+ const path40 = getSshSecretsPath();
23894
+ if (!existsSync11(path40)) return {};
23547
23895
  try {
23548
- return JSON.parse(readFileSync8(path38, "utf8"));
23896
+ return JSON.parse(readFileSync8(path40, "utf8"));
23549
23897
  } catch {
23550
23898
  return {};
23551
23899
  }
23552
23900
  }
23553
23901
  function writeSecrets(data) {
23554
- const path38 = getSshSecretsPath();
23555
- mkdirSync7(dirname2(path38), { recursive: true });
23556
- writeFileSync6(path38, `${JSON.stringify(data, null, 2)}
23902
+ const path40 = getSshSecretsPath();
23903
+ mkdirSync7(dirname2(path40), { recursive: true });
23904
+ writeFileSync6(path40, `${JSON.stringify(data, null, 2)}
23557
23905
  `, "utf8");
23558
23906
  try {
23559
- chmodSync(path38, 384);
23907
+ chmodSync(path40, 384);
23560
23908
  } catch {
23561
23909
  }
23562
23910
  }
@@ -23585,10 +23933,10 @@ function deleteSshPassword(id) {
23585
23933
  writeSecrets({ passwords });
23586
23934
  }
23587
23935
  function readStore2() {
23588
- const path38 = getSshTargetsPath();
23589
- if (!existsSync11(path38)) return [];
23936
+ const path40 = getSshTargetsPath();
23937
+ if (!existsSync11(path40)) return [];
23590
23938
  try {
23591
- const parsed = JSON.parse(readFileSync8(path38, "utf8"));
23939
+ const parsed = JSON.parse(readFileSync8(path40, "utf8"));
23592
23940
  const list = Array.isArray(parsed.targets) ? parsed.targets : [];
23593
23941
  return list.filter(
23594
23942
  (t) => t && typeof t.id === "string" && typeof t.host === "string" && typeof t.user === "string"
@@ -23603,11 +23951,11 @@ function readStore2() {
23603
23951
  }
23604
23952
  }
23605
23953
  function writeStore2(targets) {
23606
- const path38 = getSshTargetsPath();
23607
- mkdirSync7(dirname2(path38), { recursive: true });
23954
+ const path40 = getSshTargetsPath();
23955
+ mkdirSync7(dirname2(path40), { recursive: true });
23608
23956
  const clean = targets.map(({ hasPassword: _hp, ...t }) => t);
23609
23957
  writeFileSync6(
23610
- path38,
23958
+ path40,
23611
23959
  `${JSON.stringify({ targets: clean }, null, 2)}
23612
23960
  `,
23613
23961
  "utf8"
@@ -23741,9 +24089,9 @@ exec node "$(dirname "$0")/askpass.cjs"
23741
24089
  return sh;
23742
24090
  }
23743
24091
  function runSsh(target, remoteCommand, timeoutMs = 6e4) {
23744
- return new Promise((resolve) => {
24092
+ return new Promise((resolve3) => {
23745
24093
  if (target.auth === "password" && !getSshPassword(target.id)) {
23746
- resolve({
24094
+ resolve3({
23747
24095
  code: 1,
23748
24096
  stdout: "",
23749
24097
  stderr: "No password stored for this target \u2014 edit target and set password"
@@ -23775,7 +24123,7 @@ function runSsh(target, remoteCommand, timeoutMs = 6e4) {
23775
24123
  });
23776
24124
  const timer = setTimeout(() => {
23777
24125
  child.kill("SIGTERM");
23778
- resolve({
24126
+ resolve3({
23779
24127
  code: 124,
23780
24128
  stdout,
23781
24129
  stderr: stderr + "\n[ssh] timeout"
@@ -23783,7 +24131,7 @@ function runSsh(target, remoteCommand, timeoutMs = 6e4) {
23783
24131
  }, timeoutMs);
23784
24132
  child.on("error", (err) => {
23785
24133
  clearTimeout(timer);
23786
- resolve({
24134
+ resolve3({
23787
24135
  code: 127,
23788
24136
  stdout,
23789
24137
  stderr: err.message.includes("ENOENT") ? "ssh not found on PATH \u2014 install OpenSSH client" : err.message
@@ -23791,7 +24139,7 @@ function runSsh(target, remoteCommand, timeoutMs = 6e4) {
23791
24139
  });
23792
24140
  child.on("close", (code) => {
23793
24141
  clearTimeout(timer);
23794
- resolve({ code: code ?? 1, stdout, stderr });
24142
+ resolve3({ code: code ?? 1, stdout, stderr });
23795
24143
  });
23796
24144
  });
23797
24145
  }
@@ -23853,11 +24201,11 @@ function formatSshTargetsForPrompt() {
23853
24201
  ];
23854
24202
  for (const t of targets) {
23855
24203
  const tags = t.tags?.length ? ` tags=[${t.tags.join(",")}]` : "";
23856
- const path38 = t.defaultRemotePath ? ` remotePath=${t.defaultRemotePath}` : "";
24204
+ const path40 = t.defaultRemotePath ? ` remotePath=${t.defaultRemotePath}` : "";
23857
24205
  const allow = t.allowedCommands?.length ? ` allowed=${t.allowedCommands.join("|")}` : " allowed=status-only";
23858
24206
  const auth = t.auth === "password" ? " auth=password" : t.auth === "keyPath" ? " auth=key" : " auth=agent";
23859
24207
  lines.push(
23860
- `- id=${t.id} name=${t.name} ${t.user}@${t.host}:${t.port ?? 22}${auth}${path38}${tags}${allow}`
24208
+ `- id=${t.id} name=${t.name} ${t.user}@${t.host}:${t.port ?? 22}${auth}${path40}${tags}${allow}`
23861
24209
  );
23862
24210
  }
23863
24211
  return lines.join("\n");
@@ -24010,7 +24358,7 @@ async function readChecks(cwd) {
24010
24358
  }
24011
24359
  }
24012
24360
  function runShell(command, cwd, timeoutMs, signal) {
24013
- return new Promise((resolve) => {
24361
+ return new Promise((resolve3) => {
24014
24362
  const isWin = process.platform === "win32";
24015
24363
  const child = spawn5(isWin ? "cmd.exe" : "/bin/sh", isWin ? ["/c", command] : ["-c", command], {
24016
24364
  cwd,
@@ -24024,7 +24372,7 @@ function runShell(command, cwd, timeoutMs, signal) {
24024
24372
  const finish = (exitCode) => {
24025
24373
  if (settled) return;
24026
24374
  settled = true;
24027
- resolve({ exitCode, stdout, stderr });
24375
+ resolve3({ exitCode, stdout, stderr });
24028
24376
  };
24029
24377
  const timer = setTimeout(() => {
24030
24378
  try {
@@ -25857,11 +26205,11 @@ var init_synthesisAudit = __esm({
25857
26205
  import { existsSync as existsSync13, readFileSync as readFileSync10, writeFileSync as writeFileSync7 } from "node:fs";
25858
26206
  import { join as join6 } from "node:path";
25859
26207
  function loadNfrSpec(zelariRoot) {
25860
- const path38 = join6(zelariRoot, "nfr-spec.json");
25861
- if (!existsSync13(path38))
26208
+ const path40 = join6(zelariRoot, "nfr-spec.json");
26209
+ if (!existsSync13(path40))
25862
26210
  return null;
25863
26211
  try {
25864
- const raw = JSON.parse(readFileSync10(path38, "utf8"));
26212
+ const raw = JSON.parse(readFileSync10(path40, "utf8"));
25865
26213
  if (raw.version !== 1 || !Array.isArray(raw.targets))
25866
26214
  return null;
25867
26215
  return raw;
@@ -28167,9 +28515,9 @@ var init_types3 = __esm({
28167
28515
  import { readFileSync as readFileSync15 } from "node:fs";
28168
28516
  import { join as join12 } from "node:path";
28169
28517
  function readLessonsDeduped(zelariRoot) {
28170
- const path38 = join12(zelariRoot, LESSONS_FILE);
28518
+ const path40 = join12(zelariRoot, LESSONS_FILE);
28171
28519
  try {
28172
- const raw = readFileSync15(path38, "utf8");
28520
+ const raw = readFileSync15(path40, "utf8");
28173
28521
  const byId = /* @__PURE__ */ new Map();
28174
28522
  for (const line of raw.split(/\r?\n/)) {
28175
28523
  if (!line.trim())
@@ -28270,8 +28618,8 @@ function keywordsFrom(check2, signature) {
28270
28618
  return [.../* @__PURE__ */ new Set([...fromId, ...words])].slice(0, 12);
28271
28619
  }
28272
28620
  function writeLesson(zelariRoot, lesson) {
28273
- const path38 = join13(zelariRoot, LESSONS_FILE);
28274
- appendFileSync2(path38, `${JSON.stringify(lesson)}
28621
+ const path40 = join13(zelariRoot, LESSONS_FILE);
28622
+ appendFileSync2(path40, `${JSON.stringify(lesson)}
28275
28623
  `, "utf8");
28276
28624
  }
28277
28625
  function findSimilar(lessons, signature) {
@@ -30428,28 +30776,28 @@ var init_storage = __esm({
30428
30776
  VALID_SCALARS = /^(true|false|null|~)$/i;
30429
30777
  Storage = class {
30430
30778
  /** Read a Markdown file with frontmatter. Throws if not found. */
30431
- read(path38) {
30432
- if (!existsSync23(path38)) {
30433
- throw new Error(`File not found: ${path38}`);
30779
+ read(path40) {
30780
+ if (!existsSync23(path40)) {
30781
+ throw new Error(`File not found: ${path40}`);
30434
30782
  }
30435
- const md = readFileSync20(path38, "utf8");
30783
+ const md = readFileSync20(path40, "utf8");
30436
30784
  return parseFrontmatter(md);
30437
30785
  }
30438
30786
  /** Read a Markdown file; returns null if not found. */
30439
- readIfExists(path38) {
30440
- if (!existsSync23(path38)) return null;
30441
- return this.read(path38);
30787
+ readIfExists(path40) {
30788
+ if (!existsSync23(path40)) return null;
30789
+ return this.read(path40);
30442
30790
  }
30443
30791
  /**
30444
30792
  * Write a Markdown file atomically (tmp + rename). Creates parent dirs.
30445
30793
  * The meta object is serialized as YAML frontmatter; body as Markdown.
30446
30794
  */
30447
- write(path38, meta3, body) {
30448
- mkdirSync9(dirname4(path38), { recursive: true });
30449
- const tmp = path38 + ".tmp-" + process.pid;
30795
+ write(path40, meta3, body) {
30796
+ mkdirSync9(dirname4(path40), { recursive: true });
30797
+ const tmp = path40 + ".tmp-" + process.pid;
30450
30798
  const md = serializeFrontmatter(meta3, body);
30451
30799
  writeFileSync13(tmp, md, "utf8");
30452
- renameSync2(tmp, path38);
30800
+ renameSync2(tmp, path40);
30453
30801
  }
30454
30802
  /** List all .md files in a directory (non-recursive). */
30455
30803
  listMarkdown(dir) {
@@ -30463,8 +30811,8 @@ var init_storage = __esm({
30463
30811
  const prev2 = this.chains.get(key) ?? Promise.resolve();
30464
30812
  let release = () => {
30465
30813
  };
30466
- const next = new Promise((resolve) => {
30467
- release = resolve;
30814
+ const next = new Promise((resolve3) => {
30815
+ release = resolve3;
30468
30816
  });
30469
30817
  const chained = prev2.then(() => next);
30470
30818
  this.chains.set(key, chained);
@@ -30527,8 +30875,8 @@ function readPlan(ctx) {
30527
30875
  } catch {
30528
30876
  }
30529
30877
  }
30530
- const path38 = workspaceFile(ctx.rootDir, "plan");
30531
- const doc = ctx.storage.readIfExists(path38);
30878
+ const path40 = workspaceFile(ctx.rootDir, "plan");
30879
+ const doc = ctx.storage.readIfExists(path40);
30532
30880
  if (!doc) return { phases: [], tasks: [], milestones: [] };
30533
30881
  const meta3 = doc.meta;
30534
30882
  return {
@@ -30693,7 +31041,7 @@ function addMilestoneRecord(ctx, summary, input) {
30693
31041
  dueDate: input.dueDate,
30694
31042
  targetVersion: version2
30695
31043
  });
30696
- const path38 = join23(ctx.rootDir, "milestones", `${id}.md`);
31044
+ const path40 = join23(ctx.rootDir, "milestones", `${id}.md`);
30697
31045
  const meta3 = {
30698
31046
  kind: "milestone",
30699
31047
  id,
@@ -30710,7 +31058,7 @@ function addMilestoneRecord(ctx, summary, input) {
30710
31058
  `Target version: ${version2}`,
30711
31059
  ""
30712
31060
  ].join("\n");
30713
- ctx.storage.write(path38, meta3, body);
31061
+ ctx.storage.write(path40, meta3, body);
30714
31062
  return { id, created: true };
30715
31063
  }
30716
31064
  function readPlanSummary(ctx) {
@@ -30914,7 +31262,7 @@ function addIdeaStub(ctx) {
30914
31262
  const tags = args["tags"] ?? [];
30915
31263
  const category = args["category"] ?? "General";
30916
31264
  const id = `${nextAdrId(ctx)}-${slugify3(title)}`;
30917
- const path38 = workspaceArtifact(ctx.rootDir, "decisions", id);
31265
+ const path40 = workspaceArtifact(ctx.rootDir, "decisions", id);
30918
31266
  const meta3 = {
30919
31267
  kind: "adr",
30920
31268
  status: "proposed",
@@ -30940,7 +31288,7 @@ function addIdeaStub(ctx) {
30940
31288
  ...consequences.map((c) => `- ${c}`),
30941
31289
  ""
30942
31290
  ].join("\n");
30943
- ctx.storage.write(path38, meta3, body);
31291
+ ctx.storage.write(path40, meta3, body);
30944
31292
  return `ADR ${id} created: "${title}". Status: proposed. Promote to accepted via /update ADR or manual edit.`;
30945
31293
  });
30946
31294
  }
@@ -31022,14 +31370,14 @@ function createDocumentStub(ctx) {
31022
31370
  ctx.storage.write(risksPath, riskMeta, content);
31023
31371
  return `Document "${title}" created at risks.md (workspace root).`;
31024
31372
  }
31025
- const path38 = workspaceArtifact(ctx.rootDir, "docs", slug);
31373
+ const path40 = workspaceArtifact(ctx.rootDir, "docs", slug);
31026
31374
  const meta3 = {
31027
31375
  kind: "doc",
31028
31376
  id: slug,
31029
31377
  date: (/* @__PURE__ */ new Date()).toISOString().slice(0, 10),
31030
31378
  tags
31031
31379
  };
31032
- ctx.storage.write(path38, meta3, content);
31380
+ ctx.storage.write(path40, meta3, content);
31033
31381
  return `Document "${title}" created at docs/${slug}.md.`;
31034
31382
  });
31035
31383
  }
@@ -31349,7 +31697,7 @@ ${fallback.output}`
31349
31697
  return primary;
31350
31698
  }
31351
31699
  function runNpm(executor, args, mode, npmCliPath) {
31352
- return new Promise((resolve) => {
31700
+ return new Promise((resolve3) => {
31353
31701
  let stdout = "";
31354
31702
  let stderr = "";
31355
31703
  const stdio = ["ignore", "pipe", "pipe"];
@@ -31361,7 +31709,7 @@ function runNpm(executor, args, mode, npmCliPath) {
31361
31709
  stderr += chunk.toString();
31362
31710
  });
31363
31711
  child.on("error", (err) => {
31364
- resolve({
31712
+ resolve3({
31365
31713
  ok: false,
31366
31714
  output: stdout + stderr,
31367
31715
  error: err.message,
@@ -31370,7 +31718,7 @@ function runNpm(executor, args, mode, npmCliPath) {
31370
31718
  });
31371
31719
  child.on("close", (code) => {
31372
31720
  const ok = code === 0;
31373
- resolve({
31721
+ resolve3({
31374
31722
  ok,
31375
31723
  output: stdout + stderr,
31376
31724
  error: ok ? void 0 : `npm exited with code ${code}`,
@@ -31494,7 +31842,7 @@ var init_mcpClient = __esm({
31494
31842
  return Promise.reject(new Error(`[mcp:${this.serverName}] not started`));
31495
31843
  const id = this.nextId++;
31496
31844
  const payload = JSON.stringify({ jsonrpc: "2.0", id, method, params });
31497
- return new Promise((resolve, reject) => {
31845
+ return new Promise((resolve3, reject) => {
31498
31846
  const timer = setTimeout(() => {
31499
31847
  this.pending.delete(id);
31500
31848
  reject(
@@ -31503,7 +31851,7 @@ var init_mcpClient = __esm({
31503
31851
  )
31504
31852
  );
31505
31853
  }, timeoutMs);
31506
- this.pending.set(id, { resolve, reject, timer });
31854
+ this.pending.set(id, { resolve: resolve3, reject, timer });
31507
31855
  child.stdin.write(payload + "\n", (err) => {
31508
31856
  if (err) {
31509
31857
  clearTimeout(timer);
@@ -31573,10 +31921,10 @@ function getUserMcpPath() {
31573
31921
  function getProjectMcpPath(projectRoot) {
31574
31922
  return join24(projectRoot, ".zelari", "mcp.json");
31575
31923
  }
31576
- function readFile2(path38) {
31577
- if (!existsSync26(path38)) return {};
31924
+ function readFile2(path40) {
31925
+ if (!existsSync26(path40)) return {};
31578
31926
  try {
31579
- const parsed = JSON.parse(readFileSync22(path38, "utf8"));
31927
+ const parsed = JSON.parse(readFileSync22(path40, "utf8"));
31580
31928
  const out = {};
31581
31929
  for (const [name, cfg] of Object.entries(parsed.mcpServers ?? {})) {
31582
31930
  if (!cfg || typeof cfg.command !== "string" || !cfg.command.trim()) continue;
@@ -31592,10 +31940,10 @@ function readFile2(path38) {
31592
31940
  return {};
31593
31941
  }
31594
31942
  }
31595
- function writeFile(path38, servers) {
31596
- mkdirSync11(dirname6(path38), { recursive: true });
31943
+ function writeFile(path40, servers) {
31944
+ mkdirSync11(dirname6(path40), { recursive: true });
31597
31945
  const body = { mcpServers: servers };
31598
- writeFileSync15(path38, `${JSON.stringify(body, null, 2)}
31946
+ writeFileSync15(path40, `${JSON.stringify(body, null, 2)}
31599
31947
  `, "utf8");
31600
31948
  }
31601
31949
  function listMcpServers(projectRoot) {
@@ -31628,9 +31976,9 @@ function upsertMcpServer(opts) {
31628
31976
  if (!opts.config.command?.trim()) {
31629
31977
  return { ok: false, error: "command is required" };
31630
31978
  }
31631
- let path38;
31979
+ let path40;
31632
31980
  if (opts.scope === "user") {
31633
- path38 = getUserMcpPath();
31981
+ path40 = getUserMcpPath();
31634
31982
  } else {
31635
31983
  const root = opts.projectRoot?.trim();
31636
31984
  if (!root) {
@@ -31639,30 +31987,30 @@ function upsertMcpServer(opts) {
31639
31987
  error: "projectRoot required for project scope (Open Folder first)"
31640
31988
  };
31641
31989
  }
31642
- path38 = getProjectMcpPath(root);
31990
+ path40 = getProjectMcpPath(root);
31643
31991
  }
31644
- const current = readFile2(path38);
31992
+ const current = readFile2(path40);
31645
31993
  current[name] = {
31646
31994
  command: opts.config.command.trim(),
31647
31995
  args: opts.config.args,
31648
31996
  env: opts.config.env,
31649
31997
  enabled: opts.config.enabled !== false
31650
31998
  };
31651
- writeFile(path38, current);
31652
- return { ok: true, path: path38 };
31999
+ writeFile(path40, current);
32000
+ return { ok: true, path: path40 };
31653
32001
  }
31654
32002
  function removeMcpServer(opts) {
31655
- const path38 = opts.scope === "user" ? getUserMcpPath() : opts.projectRoot ? getProjectMcpPath(opts.projectRoot) : null;
31656
- if (!path38) {
32003
+ const path40 = opts.scope === "user" ? getUserMcpPath() : opts.projectRoot ? getProjectMcpPath(opts.projectRoot) : null;
32004
+ if (!path40) {
31657
32005
  return { ok: false, error: "projectRoot required for project scope" };
31658
32006
  }
31659
- const current = readFile2(path38);
32007
+ const current = readFile2(path40);
31660
32008
  if (!(opts.name in current)) {
31661
- return { ok: false, error: `Server "${opts.name}" not found in ${path38}` };
32009
+ return { ok: false, error: `Server "${opts.name}" not found in ${path40}` };
31662
32010
  }
31663
32011
  delete current[opts.name];
31664
- writeFile(path38, current);
31665
- return { ok: true, path: path38 };
32012
+ writeFile(path40, current);
32013
+ return { ok: true, path: path40 };
31666
32014
  }
31667
32015
  var init_mcpConfigIo = __esm({
31668
32016
  "src/cli/mcp/mcpConfigIo.ts"() {
@@ -31999,10 +32347,10 @@ import { createHash as createHash5 } from "node:crypto";
31999
32347
  import { join as join26 } from "node:path";
32000
32348
  import { readFile as readFile3 } from "node:fs/promises";
32001
32349
  async function readPackageJson2(projectRoot) {
32002
- const path38 = join26(projectRoot, "package.json");
32003
- if (!existsSync28(path38)) return null;
32350
+ const path40 = join26(projectRoot, "package.json");
32351
+ if (!existsSync28(path40)) return null;
32004
32352
  try {
32005
- return JSON.parse(await readFile3(path38, "utf8"));
32353
+ return JSON.parse(await readFile3(path40, "utf8"));
32006
32354
  } catch {
32007
32355
  return null;
32008
32356
  }
@@ -32084,9 +32432,9 @@ async function genBuild(ctx) {
32084
32432
  ].join("\n");
32085
32433
  }
32086
32434
  async function genOpenQuestions(ctx) {
32087
- const path38 = join26(ctx.rootDir, "risks.md");
32088
- if (!existsSync28(path38)) return "_No open questions._";
32089
- const content = readFileSync24(path38, "utf8");
32435
+ const path40 = join26(ctx.rootDir, "risks.md");
32436
+ if (!existsSync28(path40)) return "_No open questions._";
32437
+ const content = readFileSync24(path40, "utf8");
32090
32438
  const lines = content.split("\n");
32091
32439
  const questions = [];
32092
32440
  let currentTitle = "";
@@ -32653,8 +33001,8 @@ async function runPostCouncilHook(ctx, options) {
32653
33001
  sources: scope.sources
32654
33002
  } : void 0
32655
33003
  });
32656
- const path38 = writeCouncilCompletion(ctx.rootDir, completion);
32657
- completionHook = { ran: true, path: path38, completion };
33004
+ const path40 = writeCouncilCompletion(ctx.rootDir, completion);
33005
+ completionHook = { ran: true, path: path40, completion };
32658
33006
  } catch (err) {
32659
33007
  completionHook = {
32660
33008
  ran: true,
@@ -33211,18 +33559,49 @@ var init_fileBackend = __esm({
33211
33559
  }
33212
33560
  });
33213
33561
 
33562
+ // src/cli/traceStore.ts
33563
+ import { promises as fs17 } from "node:fs";
33564
+ import * as path29 from "node:path";
33565
+ function traceDir(projectRoot) {
33566
+ return path29.join(projectRoot, ".zelari", "trace");
33567
+ }
33568
+ function tracePath(projectRoot, missionId) {
33569
+ return path29.join(traceDir(projectRoot), `${missionId}.json`);
33570
+ }
33571
+ async function saveTrace(projectRoot, missionId, entries) {
33572
+ const dir = traceDir(projectRoot);
33573
+ await fs17.mkdir(dir, { recursive: true });
33574
+ const payload = {
33575
+ missionId,
33576
+ ts: Date.now(),
33577
+ entries
33578
+ };
33579
+ await fs17.writeFile(
33580
+ tracePath(projectRoot, missionId),
33581
+ JSON.stringify(payload, null, 2) + "\n",
33582
+ "utf8"
33583
+ );
33584
+ }
33585
+ var init_traceStore = __esm({
33586
+ "src/cli/traceStore.ts"() {
33587
+ "use strict";
33588
+ }
33589
+ });
33590
+
33214
33591
  // src/cli/zelariMission.ts
33215
33592
  var zelariMission_exports = {};
33216
33593
  __export(zelariMission_exports, {
33217
33594
  formatBriefForChat: () => formatBriefForChat,
33218
33595
  isMissionAutoStart: () => isMissionAutoStart,
33596
+ resolveMaxCost: () => resolveMaxCost,
33219
33597
  resolveMaxIterations: () => resolveMaxIterations,
33220
33598
  resolveMaxStall: () => resolveMaxStall,
33599
+ resolveMaxTokens: () => resolveMaxTokens,
33221
33600
  runZelariMission: () => runZelariMission
33222
33601
  });
33223
33602
  import { randomUUID as randomUUID5 } from "node:crypto";
33224
- import { promises as fs17 } from "node:fs";
33225
- import * as path29 from "node:path";
33603
+ import { promises as fs18 } from "node:fs";
33604
+ import * as path30 from "node:path";
33226
33605
  function resolveMaxIterations(env = process.env) {
33227
33606
  const raw = env.ZELARI_MISSION_MAX_ITER;
33228
33607
  const n = raw ? Number.parseInt(raw, 10) : DEFAULT_MAX_ITER;
@@ -33234,17 +33613,35 @@ function resolveMaxStall(env = process.env) {
33234
33613
  const n = Number.parseInt(raw, 10);
33235
33614
  return Number.isFinite(n) && n >= 0 ? n : DEFAULT_MAX_STALL;
33236
33615
  }
33616
+ function resolveMaxCost(env = process.env) {
33617
+ const raw = env.ZELARI_MISSION_MAX_COST;
33618
+ if (!raw) return void 0;
33619
+ const n = Number.parseFloat(raw);
33620
+ return Number.isFinite(n) && n > 0 ? n : void 0;
33621
+ }
33622
+ function resolveMaxTokens(env = process.env) {
33623
+ const raw = env.ZELARI_MISSION_MAX_TOKENS;
33624
+ if (!raw) return void 0;
33625
+ const n = Number.parseInt(raw, 10);
33626
+ return Number.isFinite(n) && n > 0 ? n : void 0;
33627
+ }
33237
33628
  function isMissionAutoStart(env = process.env) {
33238
33629
  return env.ZELARI_MISSION_AUTO === "1";
33239
33630
  }
33240
33631
  async function writeMissionState(projectRoot, state2) {
33241
- const dir = path29.join(projectRoot, ".zelari");
33242
- await fs17.mkdir(dir, { recursive: true });
33243
- await fs17.writeFile(
33244
- path29.join(dir, "mission-state.json"),
33632
+ const dir = path30.join(projectRoot, ".zelari");
33633
+ await fs18.mkdir(dir, { recursive: true });
33634
+ await fs18.writeFile(
33635
+ path30.join(dir, "mission-state.json"),
33245
33636
  JSON.stringify(state2, null, 2) + "\n",
33246
33637
  "utf8"
33247
33638
  );
33639
+ if (state2.trace?.length) {
33640
+ try {
33641
+ await saveTrace(projectRoot, state2.missionId, state2.trace);
33642
+ } catch {
33643
+ }
33644
+ }
33248
33645
  }
33249
33646
  function buildSlicePrompt(brief, userMessage, runMode, iteration) {
33250
33647
  if (runMode === "design-phase") {
@@ -33282,6 +33679,8 @@ async function runZelariMission(userMessage, brief, deps) {
33282
33679
  const now = deps.now ?? (() => /* @__PURE__ */ new Date());
33283
33680
  const maxIter = deps.maxIterations ?? resolveMaxIterations(deps.env);
33284
33681
  const maxStall = resolveMaxStall(deps.env);
33682
+ const maxCost = resolveMaxCost(deps.env);
33683
+ const maxTokens = resolveMaxTokens(deps.env);
33285
33684
  const missionId = deps.missionId ?? `m_${randomUUID5().slice(0, 8)}`;
33286
33685
  const startedAt = now().toISOString();
33287
33686
  const state2 = {
@@ -33317,6 +33716,8 @@ async function runZelariMission(userMessage, brief, deps) {
33317
33716
  let step = 0;
33318
33717
  let implStep = 0;
33319
33718
  let pendingDesign = designFirst;
33719
+ let cumulativeCostUsd = 0;
33720
+ let cumulativeTokens = 0;
33320
33721
  while (true) {
33321
33722
  const runMode = pendingDesign ? "design-phase" : "implementation";
33322
33723
  if (runMode === "implementation") {
@@ -33333,6 +33734,8 @@ async function runZelariMission(userMessage, brief, deps) {
33333
33734
  const promptIter = runMode === "implementation" ? implStep : 1;
33334
33735
  const slicePrompt = buildSlicePrompt(brief, userMessage, runMode, promptIter);
33335
33736
  const implementerRetry = runMode === "implementation" && implStep > 1;
33737
+ const sliceStartedAt = now().toISOString();
33738
+ const sliceStartMs = now().getTime();
33336
33739
  if (runMode === "design-phase") {
33337
33740
  deps.emit(
33338
33741
  `[zelari] design-phase (fuori budget) \xB7 step ${step} \xB7 slice ${brief.sliceMvp.id}`
@@ -33369,6 +33772,8 @@ async function runZelariMission(userMessage, brief, deps) {
33369
33772
  );
33370
33773
  return state2;
33371
33774
  }
33775
+ if (typeof result.costUsd === "number") cumulativeCostUsd += result.costUsd;
33776
+ if (typeof result.costTokens === "number") cumulativeTokens += result.costTokens;
33372
33777
  await deps.memory.add(
33373
33778
  JSON.stringify({
33374
33779
  iteration: step,
@@ -33394,6 +33799,20 @@ async function runZelariMission(userMessage, brief, deps) {
33394
33799
  }
33395
33800
  state2.lastCompletionOk = completionOk;
33396
33801
  state2.updatedAt = now().toISOString();
33802
+ state2.cumulativeCostUsd = cumulativeCostUsd;
33803
+ state2.cumulativeTokens = cumulativeTokens;
33804
+ if (!state2.trace) state2.trace = [];
33805
+ state2.trace.push({
33806
+ sliceId: brief.sliceMvp.id,
33807
+ iteration: step,
33808
+ runMode,
33809
+ completionOk,
33810
+ degraded: result.degraded,
33811
+ costTokens: typeof result.costTokens === "number" ? result.costTokens : void 0,
33812
+ costUsd: typeof result.costUsd === "number" ? result.costUsd : void 0,
33813
+ startedAt: sliceStartedAt,
33814
+ durationMs: now().getTime() - sliceStartMs
33815
+ });
33397
33816
  if (runMode === "design-phase") {
33398
33817
  pendingDesign = false;
33399
33818
  await writeMissionState(deps.projectRoot, state2);
@@ -33457,6 +33876,16 @@ async function runZelariMission(userMessage, brief, deps) {
33457
33876
  return state2;
33458
33877
  }
33459
33878
  }
33879
+ if (maxCost !== void 0 && cumulativeCostUsd >= maxCost || maxTokens !== void 0 && cumulativeTokens >= maxTokens) {
33880
+ state2.status = "stopped";
33881
+ state2.updatedAt = now().toISOString();
33882
+ await writeMissionState(deps.projectRoot, state2);
33883
+ const reason = maxCost !== void 0 && cumulativeCostUsd >= maxCost ? `budget USD ${formatCost(cumulativeCostUsd)} \u2265 ${formatCost(maxCost)}` : `token ${formatTokens(cumulativeTokens)} \u2265 ${formatTokens(maxTokens)}`;
33884
+ deps.emit(
33885
+ `[zelari] fermata: ${reason}. Imposta ZELARI_MISSION_MAX_COST / ZELARI_MISSION_MAX_TOKENS pi\xF9 alto, o usa un modello pi\xF9 economico. Stato salvato in .zelari/mission-state.json`
33886
+ );
33887
+ return state2;
33888
+ }
33460
33889
  await writeMissionState(deps.projectRoot, state2);
33461
33890
  }
33462
33891
  state2.status = "stopped";
@@ -33475,6 +33904,8 @@ var init_zelariMission = __esm({
33475
33904
  init_checkpointManager();
33476
33905
  init_fileStateStore();
33477
33906
  init_commitHelpers();
33907
+ init_modelPricing();
33908
+ init_traceStore();
33478
33909
  DEFAULT_MAX_ITER = 6;
33479
33910
  DEFAULT_MAX_STALL = 2;
33480
33911
  }
@@ -33632,6 +34063,8 @@ async function runAgentMissionSlice(deps) {
33632
34063
  });
33633
34064
  let text = "";
33634
34065
  let errored2 = false;
34066
+ let promptTokens = 0;
34067
+ let completionTokens = 0;
33635
34068
  try {
33636
34069
  for await (const event of harness.run()) {
33637
34070
  counter.onEvent(event);
@@ -33642,6 +34075,11 @@ async function runAgentMissionSlice(deps) {
33642
34075
  if (event.type === "agent_end" && event.reason === "error") {
33643
34076
  errored2 = true;
33644
34077
  }
34078
+ if (event.type === "message_end" && event.usage) {
34079
+ const u = event.usage;
34080
+ promptTokens += u.promptTokens ?? 0;
34081
+ completionTokens += u.completionTokens ?? 0;
34082
+ }
33645
34083
  if (event.type === "error" && event.severity === "fatal") {
33646
34084
  errored2 = true;
33647
34085
  }
@@ -33664,7 +34102,9 @@ async function runAgentMissionSlice(deps) {
33664
34102
  successfulWrites: counter.state.successfulWrites,
33665
34103
  emittedWrites: counter.state.emittedWrites,
33666
34104
  errored: errored2,
33667
- messages: harness.getMessages()
34105
+ messages: harness.getMessages(),
34106
+ promptTokens,
34107
+ completionTokens
33668
34108
  };
33669
34109
  }
33670
34110
  const initial = [
@@ -33676,6 +34116,8 @@ async function runAgentMissionSlice(deps) {
33676
34116
  let totalEmitted = pass.emittedWrites;
33677
34117
  let synthesisText = pass.text;
33678
34118
  let errored = pass.errored;
34119
+ let totalPromptTokens = pass.promptTokens;
34120
+ let totalCompletionTokens = pass.completionTokens;
33679
34121
  if (writeRetry && totalWrites === 0 && !errored) {
33680
34122
  deps.emit?.(
33681
34123
  "[zelari] build@agent: 0 write \u2014 forcing implementation retry"
@@ -33695,6 +34137,8 @@ async function runAgentMissionSlice(deps) {
33695
34137
  ${retry.text}` : retry.text;
33696
34138
  }
33697
34139
  errored = errored || retry.errored;
34140
+ totalPromptTokens += retry.promptTokens;
34141
+ totalCompletionTokens += retry.completionTokens;
33698
34142
  }
33699
34143
  const cleaned = cleanAgentContent(synthesisText, {
33700
34144
  stripQuestion: false,
@@ -33741,12 +34185,16 @@ ${retry.text}` : retry.text;
33741
34185
  else degraded = true;
33742
34186
  }
33743
34187
  void totalEmitted;
34188
+ const costTokens = totalPromptTokens + totalCompletionTokens;
34189
+ const costUsd = calculateCost(deps.model, totalPromptTokens, totalCompletionTokens);
33744
34190
  return {
33745
34191
  completionOk,
33746
34192
  ran: true,
33747
34193
  synthesisText: cleaned || void 0,
33748
34194
  writeCount: totalWrites,
33749
- degraded
34195
+ degraded,
34196
+ costTokens,
34197
+ costUsd
33750
34198
  };
33751
34199
  }
33752
34200
  var MUTATING, AGENT_MISSION_IMPLEMENTER_PREAMBLE;
@@ -33759,6 +34207,7 @@ var init_missionSlice = __esm({
33759
34207
  init_dist();
33760
34208
  init_buildPolicy();
33761
34209
  init_envNumber();
34210
+ init_modelPricing();
33762
34211
  MUTATING = /* @__PURE__ */ new Set(["write_file", "edit_file", "apply_diff"]);
33763
34212
  AGENT_MISSION_IMPLEMENTER_PREAMBLE = "You are the sole implementer for this Zelari mission slice. A multi-agent council may already have produced a plan under `.zelari/` \u2014 treat it as a SPEC to apply on disk. You MUST create or modify real project files with write_file / edit_file. Prose without successful writes is a failed slice.";
33764
34213
  }
@@ -33885,14 +34334,14 @@ function agentProbeEnv() {
33885
34334
  try {
33886
34335
  const nodeDir = dirname7(process.execPath);
33887
34336
  if (!nodeDir) return env;
33888
- const sep = process.platform === "win32" ? ";" : ":";
34337
+ const sep2 = process.platform === "win32" ? ";" : ":";
33889
34338
  const current = env.PATH ?? env.Path ?? "";
33890
- const parts = current.split(sep).filter((p3) => p3.length > 0);
34339
+ const parts = current.split(sep2).filter((p3) => p3.length > 0);
33891
34340
  const has = parts.some(
33892
34341
  (p3) => p3.toLowerCase() === nodeDir.toLowerCase()
33893
34342
  );
33894
34343
  if (!has) {
33895
- env.PATH = `${nodeDir}${sep}${current}`;
34344
+ env.PATH = `${nodeDir}${sep2}${current}`;
33896
34345
  }
33897
34346
  } catch {
33898
34347
  }
@@ -34148,10 +34597,10 @@ var init_prereqChecks = __esm({
34148
34597
 
34149
34598
  // src/cli/plugins/prefs.ts
34150
34599
  import { existsSync as existsSync33, readFileSync as readFileSync28, writeFileSync as writeFileSync18, mkdirSync as mkdirSync13 } from "node:fs";
34151
- import path31 from "node:path";
34600
+ import path32 from "node:path";
34152
34601
  import os9 from "node:os";
34153
34602
  function getPluginPrefsPath() {
34154
- return process.env.ZELARI_PLUGINS_PREFS_FILE ?? path31.join(os9.homedir(), ".tmp", "zelari-code", "plugins.json");
34603
+ return process.env.ZELARI_PLUGINS_PREFS_FILE ?? path32.join(os9.homedir(), ".tmp", "zelari-code", "plugins.json");
34155
34604
  }
34156
34605
  function getPluginPrefs() {
34157
34606
  const file2 = getPluginPrefsPath();
@@ -34172,7 +34621,7 @@ function getPluginPrefs() {
34172
34621
  }
34173
34622
  function writePluginPrefs(prefs) {
34174
34623
  const file2 = getPluginPrefsPath();
34175
- mkdirSync13(path31.dirname(file2), { recursive: true });
34624
+ mkdirSync13(path32.dirname(file2), { recursive: true });
34176
34625
  writeFileSync18(file2, JSON.stringify(prefs, null, 2), {
34177
34626
  encoding: "utf-8",
34178
34627
  mode: 384
@@ -34209,7 +34658,7 @@ __export(registry_exports, {
34209
34658
  isBinaryOnPath: () => isBinaryOnPath
34210
34659
  });
34211
34660
  import { existsSync as existsSync34 } from "node:fs";
34212
- import path32 from "node:path";
34661
+ import path33 from "node:path";
34213
34662
  function detectLocalBin(bin) {
34214
34663
  return (cwd) => {
34215
34664
  try {
@@ -34227,9 +34676,9 @@ function isBinaryOnPath(bin, opts = {}) {
34227
34676
  const platform = opts.platform ?? process.platform;
34228
34677
  const exists = opts.exists ?? existsSync34;
34229
34678
  const pathEnv = opts.pathEnv ?? process.env.PATH ?? "";
34230
- const pathMod = platform === "win32" ? path32.win32 : path32.posix;
34231
- const sep = platform === "win32" ? ";" : ":";
34232
- const dirs = pathEnv.split(sep).filter((d) => d.length > 0);
34679
+ const pathMod = platform === "win32" ? path33.win32 : path33.posix;
34680
+ const sep2 = platform === "win32" ? ";" : ":";
34681
+ const dirs = pathEnv.split(sep2).filter((d) => d.length > 0);
34233
34682
  const candidates = [bin];
34234
34683
  if (platform === "win32") {
34235
34684
  const pathExt = opts.pathExt ?? process.env.PATHEXT ?? ".EXE;.CMD;.BAT;.COM";
@@ -34357,32 +34806,1268 @@ var init_registry2 = __esm({
34357
34806
  }
34358
34807
  });
34359
34808
 
34809
+ // src/cli/atMentions.ts
34810
+ var atMentions_exports = {};
34811
+ __export(atMentions_exports, {
34812
+ expandAtMentions: () => expandAtMentions,
34813
+ extractAtMentions: () => extractAtMentions,
34814
+ hasAtMentions: () => hasAtMentions
34815
+ });
34816
+ import { existsSync as existsSync37, readFileSync as readFileSync30, statSync as statSync6 } from "node:fs";
34817
+ import { isAbsolute, relative as relative3, resolve, sep } from "node:path";
34818
+ function isProbablyText(name, head) {
34819
+ 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(
34820
+ name
34821
+ )) {
34822
+ return true;
34823
+ }
34824
+ return !head.includes("\0") && /[\x09\x0a\x0d\x20-\x7e]/.test(head.slice(0, 200));
34825
+ }
34826
+ function underRoot(abs, root) {
34827
+ const a = resolve(abs);
34828
+ const r = resolve(root);
34829
+ const prefix = r.endsWith(sep) ? r : r + sep;
34830
+ return a === r || a.startsWith(prefix);
34831
+ }
34832
+ function extractAtMentions(text) {
34833
+ const out = [];
34834
+ const seen = /* @__PURE__ */ new Set();
34835
+ AT_PATH_RE.lastIndex = 0;
34836
+ let m;
34837
+ while ((m = AT_PATH_RE.exec(text)) !== null) {
34838
+ const token = (m[2] ?? "").trim();
34839
+ if (!token || token.includes("@")) continue;
34840
+ const key = token.replace(/\\/g, "/").toLowerCase();
34841
+ if (seen.has(key)) continue;
34842
+ seen.add(key);
34843
+ out.push(token);
34844
+ }
34845
+ return out;
34846
+ }
34847
+ function resolveMention(token, cwd) {
34848
+ const abs = isAbsolute(token) ? resolve(token) : resolve(cwd, token);
34849
+ if (!underRoot(abs, cwd) && !isAbsolute(token)) {
34850
+ if (!underRoot(abs, cwd)) {
34851
+ return {
34852
+ raw: token,
34853
+ path: token,
34854
+ absolute: abs,
34855
+ isDir: false,
34856
+ note: "outside project root \u2014 skipped"
34857
+ };
34858
+ }
34859
+ }
34860
+ if (isAbsolute(token) && !underRoot(abs, cwd)) {
34861
+ return {
34862
+ raw: token,
34863
+ path: token,
34864
+ absolute: abs,
34865
+ isDir: false,
34866
+ note: "outside project root \u2014 skipped"
34867
+ };
34868
+ }
34869
+ if (!existsSync37(abs)) {
34870
+ return {
34871
+ raw: token,
34872
+ path: token,
34873
+ absolute: abs,
34874
+ isDir: false,
34875
+ note: "not found"
34876
+ };
34877
+ }
34878
+ let st;
34879
+ try {
34880
+ st = statSync6(abs);
34881
+ } catch {
34882
+ return {
34883
+ raw: token,
34884
+ path: token,
34885
+ absolute: abs,
34886
+ isDir: false,
34887
+ note: "unreadable"
34888
+ };
34889
+ }
34890
+ const rel2 = relative3(cwd, abs).replace(/\\/g, "/") || ".";
34891
+ if (st.isDirectory()) {
34892
+ return {
34893
+ raw: token,
34894
+ path: rel2,
34895
+ absolute: abs,
34896
+ isDir: true,
34897
+ note: "directory \u2014 list/read with tools as needed"
34898
+ };
34899
+ }
34900
+ if (st.size > ATTACH_FILE_MAX_BYTES) {
34901
+ return {
34902
+ raw: token,
34903
+ path: rel2,
34904
+ absolute: abs,
34905
+ isDir: false,
34906
+ note: `too large (${Math.round(st.size / 1024)} KB) \u2014 path only`
34907
+ };
34908
+ }
34909
+ try {
34910
+ const buf = readFileSync30(abs);
34911
+ const head = buf.subarray(0, 800).toString("utf8");
34912
+ if (!isProbablyText(abs, head)) {
34913
+ return {
34914
+ raw: token,
34915
+ path: rel2,
34916
+ absolute: abs,
34917
+ isDir: false,
34918
+ note: "binary \u2014 path only"
34919
+ };
34920
+ }
34921
+ let text = buf.toString("utf8");
34922
+ if (text.charCodeAt(0) === 65279) text = text.slice(1);
34923
+ if (text.length > ATTACH_TEXT_MAX) {
34924
+ text = text.slice(0, ATTACH_TEXT_MAX) + `
34925
+
34926
+ \u2026 [truncated, ${text.length - ATTACH_TEXT_MAX} more chars]`;
34927
+ }
34928
+ return { raw: token, path: rel2, absolute: abs, isDir: false, text };
34929
+ } catch (err) {
34930
+ return {
34931
+ raw: token,
34932
+ path: rel2,
34933
+ absolute: abs,
34934
+ isDir: false,
34935
+ note: err instanceof Error ? err.message : "read failed"
34936
+ };
34937
+ }
34938
+ }
34939
+ function expandAtMentions(userText, cwd = process.cwd()) {
34940
+ const tokens = extractAtMentions(userText);
34941
+ if (tokens.length === 0) {
34942
+ return { text: userText, hits: [] };
34943
+ }
34944
+ const hits = [];
34945
+ for (const t of tokens) {
34946
+ const hit = resolveMention(t, cwd);
34947
+ if (hit) hits.push(hit);
34948
+ }
34949
+ if (hits.length === 0) {
34950
+ return { text: userText, hits: [] };
34951
+ }
34952
+ const blocks = hits.map((h) => {
34953
+ const label = h.path || h.raw;
34954
+ if (h.text != null && h.text.length > 0) {
34955
+ return `--- File: ${label} ---
34956
+ ${h.text}
34957
+ --- End file ---`;
34958
+ }
34959
+ const extra = h.note ? ` (${h.note})` : "";
34960
+ return `--- ${h.isDir ? "Dir" : "File"}: ${label}${extra} ---`;
34961
+ });
34962
+ const text = `${userText.trim()}
34963
+
34964
+ [Tagged paths]
34965
+ ${blocks.join("\n\n")}`;
34966
+ return { text, hits };
34967
+ }
34968
+ function hasAtMentions(text) {
34969
+ return extractAtMentions(text).length > 0;
34970
+ }
34971
+ var ATTACH_TEXT_MAX, ATTACH_FILE_MAX_BYTES, AT_PATH_RE;
34972
+ var init_atMentions = __esm({
34973
+ "src/cli/atMentions.ts"() {
34974
+ "use strict";
34975
+ ATTACH_TEXT_MAX = 48e3;
34976
+ ATTACH_FILE_MAX_BYTES = 512e3;
34977
+ AT_PATH_RE = /(^|[\s([{])@((?:\.{1,2}\/)?[A-Za-z0-9_.+-]+(?:[\\/][A-Za-z0-9_.+-]+)*)/g;
34978
+ }
34979
+ });
34980
+
34981
+ // src/cli/triggerLock.ts
34982
+ var triggerLock_exports = {};
34983
+ __export(triggerLock_exports, {
34984
+ acquireLock: () => acquireLock,
34985
+ lockPath: () => lockPath,
34986
+ releaseLock: () => releaseLock
34987
+ });
34988
+ import { promises as fs23 } from "node:fs";
34989
+ import * as path38 from "node:path";
34990
+ function lockPath(projectRoot) {
34991
+ return path38.join(projectRoot, ".zelari", "trigger.lock");
34992
+ }
34993
+ function isPidAlive(pid) {
34994
+ try {
34995
+ process.kill(pid, 0);
34996
+ return true;
34997
+ } catch (err) {
34998
+ const code = err.code;
34999
+ return code === "EPERM";
35000
+ }
35001
+ }
35002
+ async function acquireLock(projectRoot, now = () => /* @__PURE__ */ new Date()) {
35003
+ const lp = lockPath(projectRoot);
35004
+ const dir = path38.dirname(lp);
35005
+ await fs23.mkdir(dir, { recursive: true });
35006
+ try {
35007
+ const raw = await fs23.readFile(lp, "utf8");
35008
+ const existing = JSON.parse(raw);
35009
+ if (existing.pid && isPidAlive(existing.pid)) {
35010
+ return { acquired: false, heldBy: existing.pid, lockPath: lp };
35011
+ }
35012
+ } catch {
35013
+ }
35014
+ const payload = {
35015
+ pid: process.pid,
35016
+ acquiredAt: now().toISOString()
35017
+ };
35018
+ await fs23.writeFile(lp, JSON.stringify(payload, null, 2) + "\n", "utf8");
35019
+ return { acquired: true, lockPath: lp };
35020
+ }
35021
+ async function releaseLock(projectRoot) {
35022
+ const lp = lockPath(projectRoot);
35023
+ try {
35024
+ await fs23.unlink(lp);
35025
+ } catch {
35026
+ }
35027
+ }
35028
+ var init_triggerLock = __esm({
35029
+ "src/cli/triggerLock.ts"() {
35030
+ "use strict";
35031
+ }
35032
+ });
35033
+
35034
+ // src/cli/desktopConfig.ts
35035
+ function wantsPrintConfig(argv) {
35036
+ return argv.includes("--print-config");
35037
+ }
35038
+ function wantsSetKey(argv) {
35039
+ return argv.includes("--set-key");
35040
+ }
35041
+ function wantsDiscoverModels(argv) {
35042
+ return argv.includes("--discover-models");
35043
+ }
35044
+ function parseSetConfigFlags(argv) {
35045
+ if (!argv.includes("--set-config")) {
35046
+ return { request: null };
35047
+ }
35048
+ let provider;
35049
+ let model;
35050
+ let endpoint;
35051
+ let endpointClear = false;
35052
+ for (let i = 0; i < argv.length; i++) {
35053
+ const arg = argv[i];
35054
+ if (arg === "--provider") {
35055
+ provider = argv[i + 1];
35056
+ i++;
35057
+ } else if (arg === "--model") {
35058
+ model = argv[i + 1];
35059
+ i++;
35060
+ } else if (arg === "--endpoint") {
35061
+ endpoint = argv[i + 1];
35062
+ i++;
35063
+ } else if (arg === "--endpoint-clear") {
35064
+ endpointClear = true;
35065
+ }
35066
+ }
35067
+ if (!provider && !model && !endpoint && !endpointClear) {
35068
+ return {
35069
+ request: null,
35070
+ error: "--set-config requires --provider, --model, --endpoint, and/or --endpoint-clear"
35071
+ };
35072
+ }
35073
+ if (provider !== void 0 && provider.trim().length === 0) {
35074
+ return { request: null, error: "--provider cannot be empty" };
35075
+ }
35076
+ if (model !== void 0 && model.trim().length === 0) {
35077
+ return { request: null, error: "--model cannot be empty" };
35078
+ }
35079
+ if (endpoint !== void 0 && endpoint.trim().length === 0) {
35080
+ return { request: null, error: "--endpoint cannot be empty" };
35081
+ }
35082
+ if (endpoint && endpointClear) {
35083
+ return { request: null, error: "--endpoint and --endpoint-clear conflict" };
35084
+ }
35085
+ return {
35086
+ request: {
35087
+ provider: provider?.trim(),
35088
+ model: model?.trim(),
35089
+ endpoint: endpoint?.trim(),
35090
+ endpointClear: endpointClear || void 0
35091
+ }
35092
+ };
35093
+ }
35094
+ function parseSetKeyFlags(argv) {
35095
+ if (!argv.includes("--set-key")) {
35096
+ return { request: null };
35097
+ }
35098
+ let provider;
35099
+ let key;
35100
+ for (let i = 0; i < argv.length; i++) {
35101
+ const arg = argv[i];
35102
+ if (arg === "--provider") {
35103
+ provider = argv[i + 1];
35104
+ i++;
35105
+ } else if (arg === "--key") {
35106
+ key = argv[i + 1];
35107
+ i++;
35108
+ }
35109
+ }
35110
+ if (!provider || provider.trim().length === 0) {
35111
+ return { request: null, error: "--set-key requires --provider <id>" };
35112
+ }
35113
+ if (!key || key.trim().length === 0) {
35114
+ return { request: null, error: "--set-key requires --key <secret>" };
35115
+ }
35116
+ return {
35117
+ request: {
35118
+ provider: provider.trim(),
35119
+ key: key.trim()
35120
+ }
35121
+ };
35122
+ }
35123
+ function parseDiscoverModelsFlags(argv) {
35124
+ if (!argv.includes("--discover-models")) {
35125
+ return { provider: null, present: false };
35126
+ }
35127
+ let provider;
35128
+ for (let i = 0; i < argv.length; i++) {
35129
+ if (argv[i] === "--provider") {
35130
+ provider = argv[i + 1];
35131
+ i++;
35132
+ }
35133
+ }
35134
+ return {
35135
+ present: true,
35136
+ provider: provider?.trim() || null
35137
+ };
35138
+ }
35139
+ function buildDesktopConfigSnapshot() {
35140
+ const config2 = getProviderConfig();
35141
+ const providers = PROVIDERS.map((p3) => {
35142
+ const cached2 = getCachedModels(p3.id);
35143
+ const models = cached2?.models.map((m) => m.id) ?? [];
35144
+ const defaultModel = config2.modelByProvider[p3.id] ?? "";
35145
+ if (defaultModel && !models.includes(defaultModel)) {
35146
+ models.unshift(defaultModel);
35147
+ }
35148
+ const custom2 = getCustomEndpoint(p3.id);
35149
+ const builtin = p3.baseUrl ?? null;
35150
+ return {
35151
+ id: p3.id,
35152
+ displayName: p3.displayName,
35153
+ hasKey: !!resolveApiKey(p3.id),
35154
+ envVar: p3.envVar,
35155
+ models,
35156
+ defaultModel,
35157
+ endpoint: custom2 ?? null,
35158
+ baseUrl: custom2 ?? builtin
35159
+ };
35160
+ });
35161
+ return {
35162
+ activeProviderId: config2.activeProviderId,
35163
+ modelByProvider: { ...config2.modelByProvider },
35164
+ providers,
35165
+ cliVersion: getCurrentVersion(),
35166
+ configPaths: {
35167
+ provider: getProviderConfigPath(),
35168
+ keys: getKeyStorePath()
35169
+ }
35170
+ };
35171
+ }
35172
+ function printDesktopConfig() {
35173
+ const snap = buildDesktopConfigSnapshot();
35174
+ process.stdout.write(JSON.stringify(snap, null, 2) + "\n");
35175
+ }
35176
+ function applySetConfig(req) {
35177
+ try {
35178
+ const config2 = getProviderConfig();
35179
+ let targetProvider = req.provider ?? config2.activeProviderId;
35180
+ if (req.provider) {
35181
+ const exists = PROVIDERS.some((p3) => p3.id === req.provider);
35182
+ if (!exists) {
35183
+ return {
35184
+ ok: false,
35185
+ error: `unknown provider '${req.provider}'. Available: ${PROVIDERS.map((p3) => p3.id).join(", ")}`
35186
+ };
35187
+ }
35188
+ setActiveProviderId(req.provider);
35189
+ targetProvider = req.provider;
35190
+ }
35191
+ if (req.endpointClear) {
35192
+ clearCustomEndpoint(targetProvider);
35193
+ }
35194
+ if (req.endpoint) {
35195
+ setCustomEndpoint(targetProvider, req.endpoint);
35196
+ }
35197
+ if (req.model) {
35198
+ setModelForProvider(targetProvider, req.model);
35199
+ }
35200
+ const after = getProviderConfig();
35201
+ const ep = getCustomEndpoint(after.activeProviderId);
35202
+ return {
35203
+ ok: true,
35204
+ message: `activeProvider=${after.activeProviderId} model=${after.modelByProvider[after.activeProviderId]}` + (ep ? ` endpoint=${ep}` : "")
35205
+ };
35206
+ } catch (err) {
35207
+ return {
35208
+ ok: false,
35209
+ error: err instanceof Error ? err.message : String(err)
35210
+ };
35211
+ }
35212
+ }
35213
+ function applySetKey(req) {
35214
+ try {
35215
+ const exists = PROVIDERS.some((p3) => p3.id === req.provider);
35216
+ if (!exists) {
35217
+ return {
35218
+ ok: false,
35219
+ error: `unknown provider '${req.provider}'. Available: ${PROVIDERS.map((p3) => p3.id).join(", ")}`
35220
+ };
35221
+ }
35222
+ setApiKey(req.provider, req.key);
35223
+ setActiveProviderId(req.provider);
35224
+ return {
35225
+ ok: true,
35226
+ provider: req.provider,
35227
+ masked: maskKey(req.key)
35228
+ };
35229
+ } catch (err) {
35230
+ return {
35231
+ ok: false,
35232
+ error: err instanceof Error ? err.message : String(err)
35233
+ };
35234
+ }
35235
+ }
35236
+ async function runDiscoverModels(providerArg) {
35237
+ const config2 = getProviderConfig();
35238
+ const providerId = (providerArg ?? config2.activeProviderId).trim();
35239
+ if (!DISCOVERABLE.includes(providerId)) {
35240
+ if (providerId === "custom") {
35241
+ } else {
35242
+ return {
35243
+ ok: false,
35244
+ error: `provider '${providerId}' is not discoverable. Use: ${DISCOVERABLE.join(", ")}`
35245
+ };
35246
+ }
35247
+ }
35248
+ const discoveryId = providerId === "custom" ? "openai-compatible" : providerId;
35249
+ try {
35250
+ const entry = await discoverModelsForProvider(discoveryId);
35251
+ return {
35252
+ ok: true,
35253
+ payload: {
35254
+ ok: true,
35255
+ provider: providerId,
35256
+ models: entry.models.map((m) => m.id),
35257
+ fetchedAt: entry.fetchedAt,
35258
+ baseUrl: entry.baseUrl
35259
+ }
35260
+ };
35261
+ } catch (err) {
35262
+ return {
35263
+ ok: false,
35264
+ error: err instanceof Error ? err.message : String(err)
35265
+ };
35266
+ }
35267
+ }
35268
+ var DISCOVERABLE;
35269
+ var init_desktopConfig = __esm({
35270
+ "src/cli/desktopConfig.ts"() {
35271
+ "use strict";
35272
+ init_keyStore();
35273
+ init_providerConfig();
35274
+ init_modelDiscovery();
35275
+ init_updater();
35276
+ DISCOVERABLE = [
35277
+ "grok",
35278
+ "glm",
35279
+ "minimax",
35280
+ "deepseek",
35281
+ "openai-compatible"
35282
+ ];
35283
+ }
35284
+ });
35285
+
35286
+ // src/cli/companion/config.ts
35287
+ import {
35288
+ existsSync as existsSync40,
35289
+ mkdirSync as mkdirSync17,
35290
+ readFileSync as readFileSync33,
35291
+ writeFileSync as writeFileSync21
35292
+ } from "node:fs";
35293
+ import { join as join35 } from "node:path";
35294
+ import { homedir as homedir11 } from "node:os";
35295
+ import { createHash as createHash6, randomBytes as randomBytes2, timingSafeEqual } from "node:crypto";
35296
+ function getZelariHome() {
35297
+ return join35(homedir11(), ".zelari-code");
35298
+ }
35299
+ function getCompanionConfigPath() {
35300
+ return join35(getZelariHome(), "companion.json");
35301
+ }
35302
+ function getCompanionTokenPath() {
35303
+ return join35(getZelariHome(), "companion.token");
35304
+ }
35305
+ function ensureHome() {
35306
+ const home = getZelariHome();
35307
+ if (!existsSync40(home)) {
35308
+ mkdirSync17(home, { recursive: true });
35309
+ }
35310
+ }
35311
+ function loadCompanionConfig() {
35312
+ const path40 = getCompanionConfigPath();
35313
+ if (!existsSync40(path40)) {
35314
+ return { projects: [] };
35315
+ }
35316
+ try {
35317
+ const raw = JSON.parse(readFileSync33(path40, "utf8"));
35318
+ const projects = Array.isArray(raw.projects) ? raw.projects.filter(
35319
+ (p3) => p3 && typeof p3.path === "string" && p3.path.trim() && typeof (p3.id ?? p3.name) === "string"
35320
+ ).map((p3) => ({
35321
+ id: String(p3.id || p3.name).trim().toLowerCase().replace(/[^a-z0-9-_]+/g, "-").slice(0, 64),
35322
+ name: String(p3.name || p3.id || "project").trim() || "project",
35323
+ path: String(p3.path).trim()
35324
+ })) : [];
35325
+ return {
35326
+ projects,
35327
+ bind: typeof raw.bind === "string" ? raw.bind : void 0,
35328
+ port: typeof raw.port === "number" ? raw.port : void 0
35329
+ };
35330
+ } catch {
35331
+ return { projects: [] };
35332
+ }
35333
+ }
35334
+ function saveCompanionConfig(cfg) {
35335
+ ensureHome();
35336
+ writeFileSync21(
35337
+ getCompanionConfigPath(),
35338
+ JSON.stringify(
35339
+ {
35340
+ bind: cfg.bind,
35341
+ port: cfg.port,
35342
+ projects: cfg.projects
35343
+ },
35344
+ null,
35345
+ 2
35346
+ ) + "\n",
35347
+ "utf8"
35348
+ );
35349
+ }
35350
+ function loadOrCreateToken(explicit) {
35351
+ if (explicit && explicit.trim()) {
35352
+ return { token: explicit.trim(), created: false };
35353
+ }
35354
+ ensureHome();
35355
+ const path40 = getCompanionTokenPath();
35356
+ if (existsSync40(path40)) {
35357
+ const t = readFileSync33(path40, "utf8").trim();
35358
+ if (t) return { token: t, created: false };
35359
+ }
35360
+ const token = randomBytes2(24).toString("base64url");
35361
+ writeFileSync21(path40, token + "\n", "utf8");
35362
+ try {
35363
+ const fs24 = __require("node:fs");
35364
+ fs24.chmodSync?.(path40, 384);
35365
+ } catch {
35366
+ }
35367
+ return { token, created: true };
35368
+ }
35369
+ function tokenMatches(expected, provided) {
35370
+ if (!provided) return false;
35371
+ const a = createHash6("sha256").update(expected).digest();
35372
+ const b = createHash6("sha256").update(provided).digest();
35373
+ try {
35374
+ return timingSafeEqual(a, b);
35375
+ } catch {
35376
+ return false;
35377
+ }
35378
+ }
35379
+ function slugFromPath(p3) {
35380
+ const base = p3.replace(/[/\\]+$/, "").split(/[/\\]/).pop() || "project";
35381
+ return base.toLowerCase().replace(/[^a-z0-9-_]+/g, "-").replace(/^-+|-+$/g, "").slice(0, 48) || "project";
35382
+ }
35383
+ function mergeProjects(cfg, extraPaths) {
35384
+ const byId = /* @__PURE__ */ new Map();
35385
+ for (const p3 of cfg.projects) {
35386
+ byId.set(p3.id, p3);
35387
+ }
35388
+ for (const raw of extraPaths) {
35389
+ const path40 = raw.trim();
35390
+ if (!path40) continue;
35391
+ let id = slugFromPath(path40);
35392
+ let n = 2;
35393
+ while (byId.has(id) && byId.get(id).path !== path40) {
35394
+ id = `${slugFromPath(path40)}-${n++}`;
35395
+ }
35396
+ byId.set(id, {
35397
+ id,
35398
+ name: slugFromPath(path40),
35399
+ path: path40
35400
+ });
35401
+ }
35402
+ return [...byId.values()];
35403
+ }
35404
+ function resolveProjectPath(projects, cwdOrId) {
35405
+ if (!projects.length) {
35406
+ return {
35407
+ ok: false,
35408
+ error: "No projects configured. Pass --project <path> or edit ~/.zelari-code/companion.json"
35409
+ };
35410
+ }
35411
+ if (!cwdOrId || !String(cwdOrId).trim()) {
35412
+ return { ok: true, project: projects[0] };
35413
+ }
35414
+ const key = String(cwdOrId).trim();
35415
+ const byId = projects.find((p3) => p3.id === key || p3.name === key);
35416
+ if (byId) return { ok: true, project: byId };
35417
+ const norm = key.replace(/\\/g, "/").toLowerCase();
35418
+ const byPath = projects.find(
35419
+ (p3) => p3.path.replace(/\\/g, "/").toLowerCase() === norm
35420
+ );
35421
+ if (byPath) return { ok: true, project: byPath };
35422
+ const under = projects.find((p3) => {
35423
+ const root = p3.path.replace(/\\/g, "/").toLowerCase().replace(/\/$/, "");
35424
+ return norm === root || norm.startsWith(root + "/");
35425
+ });
35426
+ if (under) {
35427
+ return {
35428
+ ok: true,
35429
+ project: { ...under, path: key }
35430
+ };
35431
+ }
35432
+ return {
35433
+ ok: false,
35434
+ error: `cwd/project not in allowlist: ${key}. Allowed: ${projects.map((p3) => p3.id).join(", ")}`
35435
+ };
35436
+ }
35437
+ var DEFAULT_COMPANION_PORT, DEFAULT_COMPANION_BIND;
35438
+ var init_config = __esm({
35439
+ "src/cli/companion/config.ts"() {
35440
+ "use strict";
35441
+ DEFAULT_COMPANION_PORT = 7421;
35442
+ DEFAULT_COMPANION_BIND = "127.0.0.1";
35443
+ }
35444
+ });
35445
+
35446
+ // src/cli/companion/runManager.ts
35447
+ import { spawn as spawn11 } from "node:child_process";
35448
+ import { createInterface } from "node:readline";
35449
+ import { randomUUID as randomUUID6 } from "node:crypto";
35450
+ import { writeFileSync as writeFileSync22, unlinkSync as unlinkSync2 } from "node:fs";
35451
+ import { join as join36 } from "node:path";
35452
+ import { tmpdir as tmpdir3 } from "node:os";
35453
+ var RunManager;
35454
+ var init_runManager = __esm({
35455
+ "src/cli/companion/runManager.ts"() {
35456
+ "use strict";
35457
+ RunManager = class {
35458
+ active = null;
35459
+ recent = [];
35460
+ getActive() {
35461
+ return this.active?.run ?? null;
35462
+ }
35463
+ getRun(id) {
35464
+ if (this.active?.run.id === id) return this.active.run;
35465
+ return this.recent.find((r) => r.id === id) ?? null;
35466
+ }
35467
+ listRecent(limit = 20) {
35468
+ const cur = this.active ? [this.active.run] : [];
35469
+ return [...cur, ...this.recent].slice(0, limit);
35470
+ }
35471
+ subscribe(runId, fn) {
35472
+ if (this.active?.run.id === runId) {
35473
+ for (const ev of this.active.run.events) {
35474
+ try {
35475
+ fn(ev);
35476
+ } catch {
35477
+ }
35478
+ }
35479
+ this.active.listeners.add(fn);
35480
+ return () => {
35481
+ this.active?.listeners.delete(fn);
35482
+ };
35483
+ }
35484
+ const past = this.recent.find((r) => r.id === runId);
35485
+ if (past) {
35486
+ for (const ev of past.events) {
35487
+ try {
35488
+ fn(ev);
35489
+ } catch {
35490
+ }
35491
+ }
35492
+ }
35493
+ return () => {
35494
+ };
35495
+ }
35496
+ emit(ev) {
35497
+ if (!this.active) return;
35498
+ this.active.run.events.push(ev);
35499
+ if (this.active.run.events.length > 5e3) {
35500
+ this.active.run.events.splice(0, this.active.run.events.length - 4e3);
35501
+ }
35502
+ for (const fn of this.active.listeners) {
35503
+ try {
35504
+ fn(ev);
35505
+ } catch {
35506
+ }
35507
+ }
35508
+ }
35509
+ start(args) {
35510
+ if (this.active) {
35511
+ return {
35512
+ ok: false,
35513
+ error: `A run is already active (${this.active.run.id}). Cancel it first.`
35514
+ };
35515
+ }
35516
+ const prompt = args.prompt?.trim();
35517
+ if (!prompt) return { ok: false, error: "prompt is required" };
35518
+ const id = randomUUID6();
35519
+ const mode = (args.mode || "agent").toLowerCase();
35520
+ const phase2 = (args.phase || "build").toLowerCase();
35521
+ const run = {
35522
+ id,
35523
+ status: "running",
35524
+ prompt,
35525
+ mode,
35526
+ phase: phase2,
35527
+ cwd: args.cwd,
35528
+ createdAt: Date.now(),
35529
+ events: []
35530
+ };
35531
+ const cliEntry = process.argv[1];
35532
+ if (!cliEntry) {
35533
+ return { ok: false, error: "Cannot resolve CLI entry (process.argv[1])" };
35534
+ }
35535
+ const argv = [
35536
+ cliEntry,
35537
+ "--headless",
35538
+ "--task",
35539
+ prompt,
35540
+ "--output",
35541
+ "json",
35542
+ "--mode",
35543
+ mode === "council" || mode === "zelari" ? mode : "agent",
35544
+ "--phase",
35545
+ phase2 === "plan" ? "plan" : "build"
35546
+ ];
35547
+ if (args.provider?.trim()) {
35548
+ argv.push("--provider", args.provider.trim());
35549
+ }
35550
+ if (args.model?.trim()) {
35551
+ argv.push("--model", args.model.trim());
35552
+ }
35553
+ let historyFile;
35554
+ if (args.history && Array.isArray(args.history) && args.history.length > 0) {
35555
+ historyFile = join36(tmpdir3(), `zelari-companion-hist-${id}.json`);
35556
+ try {
35557
+ writeFileSync22(historyFile, JSON.stringify(args.history), "utf8");
35558
+ argv.push("--history-file", historyFile);
35559
+ } catch {
35560
+ historyFile = void 0;
35561
+ }
35562
+ }
35563
+ const child = spawn11(process.execPath, argv, {
35564
+ cwd: args.cwd,
35565
+ env: {
35566
+ ...process.env,
35567
+ ZELARI_SKIP_PREFLIGHT: "1",
35568
+ ANATHEMA_DEV: "1",
35569
+ FORCE_COLOR: "0"
35570
+ },
35571
+ stdio: ["ignore", "pipe", "pipe"],
35572
+ windowsHide: true
35573
+ });
35574
+ this.active = { run, child, listeners: /* @__PURE__ */ new Set(), historyFile };
35575
+ this.emit({
35576
+ type: "log",
35577
+ message: `[companion] run ${id} started mode=${mode} phase=${phase2} cwd=${args.cwd}`
35578
+ });
35579
+ if (child.stdout) {
35580
+ const rl = createInterface({ input: child.stdout });
35581
+ rl.on("line", (line) => {
35582
+ const t = line.trim();
35583
+ if (!t) return;
35584
+ try {
35585
+ const ev = JSON.parse(t);
35586
+ this.emit(ev);
35587
+ } catch {
35588
+ this.emit({ type: "log", message: t });
35589
+ }
35590
+ });
35591
+ }
35592
+ child.stderr?.on("data", (buf) => {
35593
+ const text = buf.toString("utf8").trim();
35594
+ if (!text) return;
35595
+ for (const line of text.split(/\r?\n/)) {
35596
+ if (line.trim()) {
35597
+ this.emit({ type: "log", message: line.trim() });
35598
+ }
35599
+ }
35600
+ });
35601
+ child.on("error", (err) => {
35602
+ run.status = "error";
35603
+ run.error = err.message;
35604
+ run.finishedAt = Date.now();
35605
+ this.emit({
35606
+ type: "error",
35607
+ severity: "fatal",
35608
+ message: err.message,
35609
+ code: "spawn"
35610
+ });
35611
+ this.finishActive();
35612
+ });
35613
+ child.on("close", (code) => {
35614
+ if (run.status === "running") {
35615
+ run.status = code === 0 ? "completed" : "error";
35616
+ run.exitCode = code;
35617
+ run.finishedAt = Date.now();
35618
+ if (code !== 0 && !run.error) {
35619
+ run.error = `exit ${code}`;
35620
+ }
35621
+ }
35622
+ this.emit({
35623
+ type: "run_finished",
35624
+ runId: id,
35625
+ status: run.status,
35626
+ exitCode: code
35627
+ });
35628
+ this.finishActive();
35629
+ });
35630
+ return { ok: true, run };
35631
+ }
35632
+ cancel(runId) {
35633
+ if (!this.active) {
35634
+ return { ok: false, error: "No active run" };
35635
+ }
35636
+ if (runId && this.active.run.id !== runId) {
35637
+ return { ok: false, error: `Run ${runId} is not active` };
35638
+ }
35639
+ const { run, child } = this.active;
35640
+ run.status = "cancelled";
35641
+ run.finishedAt = Date.now();
35642
+ this.emit({
35643
+ type: "log",
35644
+ message: `[companion] cancelling run ${run.id}`
35645
+ });
35646
+ try {
35647
+ child.kill("SIGTERM");
35648
+ } catch {
35649
+ }
35650
+ setTimeout(() => {
35651
+ try {
35652
+ if (!child.killed) child.kill("SIGKILL");
35653
+ } catch {
35654
+ }
35655
+ }, 1500);
35656
+ return { ok: true };
35657
+ }
35658
+ finishActive() {
35659
+ if (!this.active) return;
35660
+ const { run, historyFile } = this.active;
35661
+ if (historyFile) {
35662
+ try {
35663
+ unlinkSync2(historyFile);
35664
+ } catch {
35665
+ }
35666
+ }
35667
+ this.recent.unshift(run);
35668
+ if (this.recent.length > 30) this.recent.length = 30;
35669
+ this.active = null;
35670
+ }
35671
+ };
35672
+ }
35673
+ });
35674
+
35675
+ // src/cli/companion/serve.ts
35676
+ var serve_exports = {};
35677
+ __export(serve_exports, {
35678
+ parseServeFlags: () => parseServeFlags,
35679
+ runCompanionServe: () => runCompanionServe
35680
+ });
35681
+ import { createServer } from "node:http";
35682
+ import { existsSync as existsSync41 } from "node:fs";
35683
+ import { resolve as resolve2 } from "node:path";
35684
+ function readBody(req, max = 2e6) {
35685
+ return new Promise((resolveBody, reject) => {
35686
+ const chunks = [];
35687
+ let size = 0;
35688
+ req.on("data", (c) => {
35689
+ size += c.length;
35690
+ if (size > max) {
35691
+ reject(new Error("body too large"));
35692
+ req.destroy();
35693
+ return;
35694
+ }
35695
+ chunks.push(c);
35696
+ });
35697
+ req.on("end", () => resolveBody(Buffer.concat(chunks).toString("utf8")));
35698
+ req.on("error", reject);
35699
+ });
35700
+ }
35701
+ function sendJson(res, status, body, extraHeaders) {
35702
+ const data = JSON.stringify(body);
35703
+ res.writeHead(status, {
35704
+ "content-type": "application/json; charset=utf-8",
35705
+ "content-length": Buffer.byteLength(data),
35706
+ "access-control-allow-origin": "*",
35707
+ "access-control-allow-headers": "authorization, content-type",
35708
+ "access-control-allow-methods": "GET, POST, OPTIONS",
35709
+ ...extraHeaders
35710
+ });
35711
+ res.end(data);
35712
+ }
35713
+ function getBearer(req) {
35714
+ const h = req.headers.authorization;
35715
+ if (!h) return null;
35716
+ const m = /^Bearer\s+(.+)$/i.exec(h.trim());
35717
+ return m?.[1]?.trim() || null;
35718
+ }
35719
+ function parseUrl(req) {
35720
+ return new URL(req.url || "/", `http://${req.headers.host || "localhost"}`);
35721
+ }
35722
+ async function runCompanionServe(opts = {}) {
35723
+ const fileCfg = loadCompanionConfig();
35724
+ const bind = (opts.bind || fileCfg.bind || DEFAULT_COMPANION_BIND).trim();
35725
+ const port = opts.port ?? fileCfg.port ?? DEFAULT_COMPANION_PORT;
35726
+ const { token, created } = loadOrCreateToken(opts.token);
35727
+ let projects = mergeProjects(fileCfg, opts.projects ?? []);
35728
+ projects = projects.filter((p3) => {
35729
+ const abs = resolve2(p3.path);
35730
+ if (!existsSync41(abs)) {
35731
+ process.stderr.write(
35732
+ `[zelari-code serve] skip missing project path: ${p3.path}
35733
+ `
35734
+ );
35735
+ return false;
35736
+ }
35737
+ p3.path = abs;
35738
+ return true;
35739
+ });
35740
+ if (opts.persistProjects && projects.length > 0) {
35741
+ saveCompanionConfig({
35742
+ ...fileCfg,
35743
+ bind,
35744
+ port,
35745
+ projects
35746
+ });
35747
+ }
35748
+ if (projects.length === 0) {
35749
+ const cwd = resolve2(process.cwd());
35750
+ projects = [
35751
+ {
35752
+ id: "default",
35753
+ name: "default",
35754
+ path: cwd
35755
+ }
35756
+ ];
35757
+ process.stderr.write(
35758
+ `[zelari-code serve] no projects configured \u2014 using cwd as default: ${cwd}
35759
+ Add more: zelari-code serve --project <path>
35760
+ Or edit ~/.zelari-code/companion.json
35761
+ `
35762
+ );
35763
+ }
35764
+ const runs = new RunManager();
35765
+ const server = createServer(async (req, res) => {
35766
+ if (req.method === "OPTIONS") {
35767
+ res.writeHead(204, {
35768
+ "access-control-allow-origin": "*",
35769
+ "access-control-allow-headers": "authorization, content-type",
35770
+ "access-control-allow-methods": "GET, POST, OPTIONS"
35771
+ });
35772
+ res.end();
35773
+ return;
35774
+ }
35775
+ const url2 = parseUrl(req);
35776
+ const path40 = url2.pathname.replace(/\/+$/, "") || "/";
35777
+ try {
35778
+ if (req.method === "GET" && (path40 === "/health" || path40 === "/v1/health")) {
35779
+ sendJson(res, 200, {
35780
+ ok: true,
35781
+ service: "zelari-companion",
35782
+ version: getCurrentVersion(),
35783
+ bind,
35784
+ port,
35785
+ projects: projects.length,
35786
+ activeRun: runs.getActive()?.id ?? null
35787
+ });
35788
+ return;
35789
+ }
35790
+ if (path40.startsWith("/v1")) {
35791
+ if (!tokenMatches(token, getBearer(req))) {
35792
+ sendJson(res, 401, { ok: false, error: "unauthorized" });
35793
+ return;
35794
+ }
35795
+ }
35796
+ if (req.method === "GET" && path40 === "/v1/config") {
35797
+ const snap = buildDesktopConfigSnapshot();
35798
+ sendJson(res, 200, { ok: true, ...snap });
35799
+ return;
35800
+ }
35801
+ if (req.method === "GET" && path40 === "/v1/projects") {
35802
+ sendJson(res, 200, {
35803
+ ok: true,
35804
+ projects: projects.map((p3) => ({
35805
+ id: p3.id,
35806
+ name: p3.name,
35807
+ path: p3.path
35808
+ }))
35809
+ });
35810
+ return;
35811
+ }
35812
+ if (req.method === "GET" && path40 === "/v1/runs") {
35813
+ sendJson(res, 200, {
35814
+ ok: true,
35815
+ active: runs.getActive(),
35816
+ recent: runs.listRecent().map((r) => ({
35817
+ id: r.id,
35818
+ status: r.status,
35819
+ mode: r.mode,
35820
+ phase: r.phase,
35821
+ cwd: r.cwd,
35822
+ createdAt: r.createdAt,
35823
+ finishedAt: r.finishedAt,
35824
+ exitCode: r.exitCode,
35825
+ promptPreview: r.prompt.slice(0, 120)
35826
+ }))
35827
+ });
35828
+ return;
35829
+ }
35830
+ if (req.method === "POST" && path40 === "/v1/runs") {
35831
+ const raw = await readBody(req);
35832
+ let body = {};
35833
+ try {
35834
+ body = raw ? JSON.parse(raw) : {};
35835
+ } catch {
35836
+ sendJson(res, 400, { ok: false, error: "invalid JSON body" });
35837
+ return;
35838
+ }
35839
+ const prompt = String(body.prompt ?? body.task ?? "").trim();
35840
+ const mode = String(body.mode ?? "agent");
35841
+ const phase2 = String(body.phase ?? "build");
35842
+ const cwdArg = body.cwd != null ? String(body.cwd) : body.projectId != null ? String(body.projectId) : null;
35843
+ const resolved = resolveProjectPath(projects, cwdArg);
35844
+ if (!resolved.ok) {
35845
+ sendJson(res, 400, { ok: false, error: resolved.error });
35846
+ return;
35847
+ }
35848
+ const history2 = Array.isArray(body.history) ? body.history : void 0;
35849
+ const result = runs.start({
35850
+ prompt,
35851
+ mode,
35852
+ phase: phase2,
35853
+ cwd: resolved.project.path,
35854
+ provider: body.provider != null ? String(body.provider) : void 0,
35855
+ model: body.model != null ? String(body.model) : void 0,
35856
+ history: history2
35857
+ });
35858
+ if (!result.ok) {
35859
+ sendJson(res, 409, { ok: false, error: result.error });
35860
+ return;
35861
+ }
35862
+ sendJson(res, 201, {
35863
+ ok: true,
35864
+ run: {
35865
+ id: result.run.id,
35866
+ status: result.run.status,
35867
+ mode: result.run.mode,
35868
+ phase: result.run.phase,
35869
+ cwd: result.run.cwd,
35870
+ createdAt: result.run.createdAt
35871
+ },
35872
+ eventsUrl: `/v1/runs/${result.run.id}/events`,
35873
+ cancelUrl: `/v1/runs/${result.run.id}/cancel`
35874
+ });
35875
+ return;
35876
+ }
35877
+ const eventsMatch = /^\/v1\/runs\/([^/]+)\/events$/.exec(path40);
35878
+ if (req.method === "GET" && eventsMatch) {
35879
+ const runId = eventsMatch[1];
35880
+ const run = runs.getRun(runId);
35881
+ if (!run) {
35882
+ sendJson(res, 404, { ok: false, error: "run not found" });
35883
+ return;
35884
+ }
35885
+ res.writeHead(200, {
35886
+ "content-type": "text/event-stream; charset=utf-8",
35887
+ "cache-control": "no-cache, no-transform",
35888
+ connection: "keep-alive",
35889
+ "access-control-allow-origin": "*",
35890
+ "access-control-allow-headers": "authorization, content-type"
35891
+ });
35892
+ res.write(`: connected run=${runId}
35893
+
35894
+ `);
35895
+ const writeEv = (ev) => {
35896
+ try {
35897
+ res.write(`data: ${JSON.stringify(ev)}
35898
+
35899
+ `);
35900
+ } catch {
35901
+ }
35902
+ };
35903
+ const unsub = runs.subscribe(runId, writeEv);
35904
+ if (run.status !== "running" && run.status !== "queued") {
35905
+ writeEv({
35906
+ type: "run_finished",
35907
+ runId,
35908
+ status: run.status,
35909
+ exitCode: run.exitCode ?? null
35910
+ });
35911
+ unsub();
35912
+ res.end();
35913
+ return;
35914
+ }
35915
+ const heartbeat = setInterval(() => {
35916
+ try {
35917
+ res.write(`: ping
35918
+
35919
+ `);
35920
+ } catch {
35921
+ }
35922
+ }, 15e3);
35923
+ const onClose = () => {
35924
+ clearInterval(heartbeat);
35925
+ unsub();
35926
+ };
35927
+ req.on("close", onClose);
35928
+ const check2 = setInterval(() => {
35929
+ const r = runs.getRun(runId);
35930
+ if (!r || r.status !== "running" && r.status !== "queued") {
35931
+ clearInterval(check2);
35932
+ clearInterval(heartbeat);
35933
+ unsub();
35934
+ try {
35935
+ res.end();
35936
+ } catch {
35937
+ }
35938
+ }
35939
+ }, 500);
35940
+ return;
35941
+ }
35942
+ const cancelMatch = /^\/v1\/runs\/([^/]+)\/cancel$/.exec(path40);
35943
+ if (req.method === "POST" && cancelMatch) {
35944
+ const runId = cancelMatch[1];
35945
+ const result = runs.cancel(runId);
35946
+ if (!result.ok) {
35947
+ sendJson(res, 404, { ok: false, error: result.error });
35948
+ return;
35949
+ }
35950
+ sendJson(res, 200, { ok: true, cancelled: runId });
35951
+ return;
35952
+ }
35953
+ sendJson(res, 404, { ok: false, error: "not found" });
35954
+ } catch (err) {
35955
+ sendJson(res, 500, {
35956
+ ok: false,
35957
+ error: err instanceof Error ? err.message : String(err)
35958
+ });
35959
+ }
35960
+ });
35961
+ await new Promise((resolveListen, reject) => {
35962
+ server.once("error", reject);
35963
+ server.listen(port, bind, () => resolveListen());
35964
+ });
35965
+ server.ref();
35966
+ try {
35967
+ process.stdin?.resume?.();
35968
+ } catch {
35969
+ }
35970
+ const tokenHint = created ? `NEW token saved to ~/.zelari-code/companion.token` : `token: ~/.zelari-code/companion.token`;
35971
+ const displayHost = bind === "0.0.0.0" || bind === "::" ? "127.0.0.1 (and LAN/Tailscale interfaces)" : bind;
35972
+ process.stderr.write(
35973
+ `
35974
+ [zelari-code serve] companion host listening
35975
+ URL http://${displayHost === "127.0.0.1 (and LAN/Tailscale interfaces)" ? "127.0.0.1" : bind}:${port}
35976
+ Bind ${bind}
35977
+ Health GET /health
35978
+ Auth Authorization: Bearer <token>
35979
+ ${tokenHint}
35980
+ Projects (${projects.length}): ${projects.map((p3) => p3.id).join(", ")}
35981
+ Phone use http://<PC-LAN-or-Tailscale-IP>:${port} (not 127.0.0.1 on the phone)
35982
+ Stop Ctrl+C (keep this window open)
35983
+
35984
+ `
35985
+ );
35986
+ if (created) {
35987
+ process.stderr.write(` Token (copy now): ${token}
35988
+
35989
+ `);
35990
+ }
35991
+ await new Promise((resolveStop) => {
35992
+ let stopped = false;
35993
+ const stop = (sig) => {
35994
+ if (stopped) return;
35995
+ stopped = true;
35996
+ process.stderr.write(`[zelari-code serve] shutting down (${sig})\u2026
35997
+ `);
35998
+ try {
35999
+ runs.cancel();
36000
+ } catch {
36001
+ }
36002
+ server.close(() => resolveStop());
36003
+ setTimeout(() => resolveStop(), 3e3).unref();
36004
+ };
36005
+ process.once("SIGINT", () => stop("SIGINT"));
36006
+ process.once("SIGTERM", () => stop("SIGTERM"));
36007
+ process.once("SIGHUP", () => stop("SIGHUP"));
36008
+ });
36009
+ }
36010
+ function parseServeFlags(argv) {
36011
+ if (!argv.includes("serve") && !argv.includes("--serve")) {
36012
+ return null;
36013
+ }
36014
+ const get = (flag) => {
36015
+ const i = argv.indexOf(flag);
36016
+ return i >= 0 && argv[i + 1] ? argv[i + 1] : void 0;
36017
+ };
36018
+ const projects = [];
36019
+ for (let i = 0; i < argv.length; i++) {
36020
+ if (argv[i] === "--project" && argv[i + 1]) {
36021
+ projects.push(argv[i + 1]);
36022
+ i++;
36023
+ }
36024
+ }
36025
+ const portRaw = get("--port");
36026
+ const port = portRaw ? Number.parseInt(portRaw, 10) : void 0;
36027
+ return {
36028
+ bind: get("--bind"),
36029
+ port: Number.isFinite(port) ? port : void 0,
36030
+ token: get("--token"),
36031
+ projects,
36032
+ persistProjects: argv.includes("--save-projects")
36033
+ };
36034
+ }
36035
+ var init_serve = __esm({
36036
+ "src/cli/companion/serve.ts"() {
36037
+ "use strict";
36038
+ init_updater();
36039
+ init_desktopConfig();
36040
+ init_config();
36041
+ init_runManager();
36042
+ }
36043
+ });
36044
+
34360
36045
  // src/cli/utils/doctor.ts
34361
36046
  var doctor_exports = {};
34362
36047
  __export(doctor_exports, {
34363
36048
  runDoctor: () => runDoctor
34364
36049
  });
34365
36050
  import { execSync as execSync2 } from "node:child_process";
34366
- import { existsSync as existsSync38, readFileSync as readFileSync31, readlinkSync, statSync as statSync6 } from "node:fs";
36051
+ import { existsSync as existsSync42, readFileSync as readFileSync34, readlinkSync, statSync as statSync7 } from "node:fs";
34367
36052
  import { createRequire as createRequire3 } from "node:module";
34368
36053
  import { fileURLToPath as fileURLToPath2 } from "node:url";
34369
- import path37 from "node:path";
36054
+ import path39 from "node:path";
34370
36055
  function findPackageRoot(start) {
34371
36056
  let dir = start;
34372
36057
  for (let i = 0; i < 6; i += 1) {
34373
- const candidate = path37.join(dir, "package.json");
34374
- if (existsSync38(candidate)) {
36058
+ const candidate = path39.join(dir, "package.json");
36059
+ if (existsSync42(candidate)) {
34375
36060
  try {
34376
- const pkg = JSON.parse(readFileSync31(candidate, "utf8"));
36061
+ const pkg = JSON.parse(readFileSync34(candidate, "utf8"));
34377
36062
  if (pkg.name === "zelari-code") return dir;
34378
36063
  } catch {
34379
36064
  }
34380
36065
  }
34381
- const parent = path37.dirname(dir);
36066
+ const parent = path39.dirname(dir);
34382
36067
  if (parent === dir) break;
34383
36068
  dir = parent;
34384
36069
  }
34385
- return path37.resolve(__dirname3, "..", "..", "..");
36070
+ return path39.resolve(__dirname3, "..", "..", "..");
34386
36071
  }
34387
36072
  function tryExec(cmd) {
34388
36073
  try {
@@ -34396,8 +36081,8 @@ function tryExec(cmd) {
34396
36081
  }
34397
36082
  function readPackageJson3() {
34398
36083
  try {
34399
- const pkgPath = path37.join(packageRoot, "package.json");
34400
- return JSON.parse(readFileSync31(pkgPath, "utf8"));
36084
+ const pkgPath = path39.join(packageRoot, "package.json");
36085
+ return JSON.parse(readFileSync34(pkgPath, "utf8"));
34401
36086
  } catch {
34402
36087
  return null;
34403
36088
  }
@@ -34412,17 +36097,17 @@ function checkShim(pkgName) {
34412
36097
  }
34413
36098
  const isWin = process.platform === "win32";
34414
36099
  const shimName = isWin ? "zelari-code.cmd" : "zelari-code";
34415
- const shimPath = path37.join(prefix, shimName);
34416
- if (!existsSync38(shimPath)) {
36100
+ const shimPath = path39.join(prefix, shimName);
36101
+ if (!existsSync42(shimPath)) {
34417
36102
  return FAIL(
34418
36103
  `shim not found at ${shimPath}
34419
36104
  fix: npm install -g ${pkgName}@latest --force`
34420
36105
  );
34421
36106
  }
34422
36107
  try {
34423
- const st = statSync6(shimPath);
36108
+ const st = statSync7(shimPath);
34424
36109
  if (isWin) {
34425
- const content = readFileSync31(shimPath, "utf8");
36110
+ const content = readFileSync34(shimPath, "utf8");
34426
36111
  if (content.includes(`${pkgName}\\bin\\`) || content.includes(`${pkgName}/bin/`)) {
34427
36112
  return OK(`shim OK at ${shimPath} (${st.size} bytes)`);
34428
36113
  }
@@ -34440,8 +36125,8 @@ function checkShim(pkgName) {
34440
36125
  fix: npm install -g ${pkgName}@latest --force`
34441
36126
  );
34442
36127
  }
34443
- const resolved = path37.resolve(path37.dirname(shimPath), target);
34444
- const expected = path37.join(
36128
+ const resolved = path39.resolve(path39.dirname(shimPath), target);
36129
+ const expected = path39.join(
34445
36130
  prefix,
34446
36131
  "node_modules",
34447
36132
  pkgName,
@@ -34480,15 +36165,15 @@ function checkNode(pkg) {
34480
36165
  return OK(`node ${raw}`);
34481
36166
  }
34482
36167
  function checkBundle() {
34483
- const bundle = path37.join(packageRoot, "dist", "cli", "main.bundled.js");
34484
- if (!existsSync38(bundle)) {
36168
+ const bundle = path39.join(packageRoot, "dist", "cli", "main.bundled.js");
36169
+ if (!existsSync42(bundle)) {
34485
36170
  return FAIL(
34486
36171
  `dist/cli/main.bundled.js missing at ${bundle}
34487
36172
  fix: npm run build:cli (then reinstall or run via tsx)`
34488
36173
  );
34489
36174
  }
34490
36175
  try {
34491
- const st = statSync6(bundle);
36176
+ const st = statSync7(bundle);
34492
36177
  return OK(`bundle OK (${(st.size / 1024 / 1024).toFixed(2)} MB)`);
34493
36178
  } catch (err) {
34494
36179
  return FAIL(
@@ -34501,7 +36186,7 @@ function checkRuntimeDeps() {
34501
36186
  const missing = [];
34502
36187
  for (const dep of required2) {
34503
36188
  try {
34504
- const localReq = createRequire3(path37.join(packageRoot, "package.json"));
36189
+ const localReq = createRequire3(path39.join(packageRoot, "package.json"));
34505
36190
  localReq.resolve(dep);
34506
36191
  } catch {
34507
36192
  missing.push(dep);
@@ -34677,7 +36362,7 @@ var init_doctor = __esm({
34677
36362
  "use strict";
34678
36363
  init_prereqChecks();
34679
36364
  require3 = createRequire3(import.meta.url);
34680
- __dirname3 = path37.dirname(fileURLToPath2(import.meta.url));
36365
+ __dirname3 = path39.dirname(fileURLToPath2(import.meta.url));
34681
36366
  packageRoot = findPackageRoot(__dirname3);
34682
36367
  OK = (message) => ({
34683
36368
  ok: true,
@@ -34880,7 +36565,7 @@ function InputBarImpl({ value, onChange, onSubmit, disabled }) {
34880
36565
  value,
34881
36566
  onChange: stableChange,
34882
36567
  onSubmit: stableSubmit,
34883
- placeholder: disabled ? "..." : "Type a prompt or /skill <name>"
36568
+ placeholder: disabled ? "..." : "Prompt, /skills, or @path"
34884
36569
  }
34885
36570
  ));
34886
36571
  }
@@ -35285,83 +36970,7 @@ function StreamingTail(m) {
35285
36970
  // src/cli/components/StatusBar.tsx
35286
36971
  import React6 from "react";
35287
36972
  import { Box as Box5, Text as Text6 } from "ink";
35288
-
35289
- // src/cli/modelPricing.ts
35290
- var PRICES_PER_MILLION = {
35291
- // xAI Grok (list prices; grok-4.5 default reasoning_effort is "high")
35292
- "grok-4.5": { input: 2, output: 6, cachedInput: 0.5 },
35293
- "grok-4.3": { input: 1.25, output: 2.5, cachedInput: 0.2 },
35294
- "grok-4": { input: 3, output: 15 },
35295
- "grok-4-fast": { input: 0.2, output: 0.5 },
35296
- "grok-3": { input: 3, output: 15 },
35297
- "grok-3-mini": { input: 0.3, output: 0.5 },
35298
- "grok-2-vision": { input: 2, output: 10 },
35299
- // GLM / Z.AI
35300
- "glm-4.6": { input: 0.6, output: 2.2 },
35301
- "glm-4.5": { input: 0.5, output: 2 },
35302
- "glm-4.5-air": { input: 0.1, output: 0.6 },
35303
- "glm-z1": { input: 0.5, output: 2 },
35304
- // MiniMax
35305
- "MiniMax-M2.5": { input: 0.2, output: 1.1 },
35306
- "MiniMax-M2": { input: 0.2, output: 1.1 },
35307
- "MiniMax-M2-her": { input: 0.3, output: 1.2 },
35308
- // DeepSeek (global platform) — estimated list prices; override via
35309
- // ANATHEMA_PRICE_DEEPSEEK_V4_FLASH / ANATHEMA_PRICE_DEEPSEEK_V4_PRO.
35310
- // DeepSeek prompt-cache hits are ~10× cheaper than a cache miss.
35311
- "deepseek-v4-flash": { input: 0.14, output: 0.28, cachedInput: 0.014 },
35312
- "deepseek-v4-pro": { input: 0.55, output: 2.19, cachedInput: 0.055 },
35313
- // OpenAI (for openai-compatible fallback)
35314
- "gpt-4o": { input: 2.5, output: 10 },
35315
- "gpt-4o-mini": { input: 0.15, output: 0.6 },
35316
- "gpt-4-turbo": { input: 10, output: 30 },
35317
- "o1-preview": { input: 15, output: 60 },
35318
- "o1-mini": { input: 3, output: 12 }
35319
- };
35320
- var DEFAULT_RATE = { input: 1, output: 3 };
35321
- var DEFAULT_CACHE_DISCOUNT = 0.25;
35322
- function envOverride(model) {
35323
- const key = `ANATHEMA_PRICE_${model.toUpperCase().replace(/[^A-Z0-9]/g, "_")}`;
35324
- const raw = process.env[key] ?? process.env.ANATHEMA_PRICE_DEFAULT;
35325
- return parseRateOverride(raw);
35326
- }
35327
- function parseRateOverride(raw) {
35328
- if (!raw) return null;
35329
- const [inStr, outStr] = raw.split("/");
35330
- const input = Number.parseFloat(inStr);
35331
- const output = outStr !== void 0 ? Number.parseFloat(outStr) : input;
35332
- if (!Number.isFinite(input) || !Number.isFinite(output)) return null;
35333
- return { input, output };
35334
- }
35335
- function getModelRate(model) {
35336
- if (!model) return DEFAULT_RATE;
35337
- return envOverride(model) ?? PRICES_PER_MILLION[model] ?? DEFAULT_RATE;
35338
- }
35339
- function calculateCost(model, promptTokens, completionTokens, cachedPromptTokens = 0) {
35340
- if (!Number.isFinite(promptTokens) || promptTokens < 0) return 0;
35341
- if (!Number.isFinite(completionTokens) || completionTokens < 0) return 0;
35342
- const rate = getModelRate(model);
35343
- const cached2 = Number.isFinite(cachedPromptTokens) && cachedPromptTokens > 0 ? Math.min(cachedPromptTokens, promptTokens) : 0;
35344
- const uncachedPrompt = promptTokens - cached2;
35345
- const cachedRate = rate.cachedInput ?? rate.input * DEFAULT_CACHE_DISCOUNT;
35346
- const inputCost = uncachedPrompt / 1e6 * rate.input + cached2 / 1e6 * cachedRate;
35347
- const outputCost = completionTokens / 1e6 * rate.output;
35348
- return Number((inputCost + outputCost).toFixed(6));
35349
- }
35350
- function formatCost(usd) {
35351
- if (!Number.isFinite(usd) || usd < 0) return "$0.0000";
35352
- if (usd === 0) return "$0.0000";
35353
- if (usd < 1e-4) return "<$0.0001";
35354
- return `$${usd.toFixed(4)}`;
35355
- }
35356
- function formatTokens(tokens) {
35357
- if (!Number.isFinite(tokens) || tokens < 0) return "0";
35358
- if (tokens < 1e3) return `${tokens}`;
35359
- if (tokens < 1e6) return `${(tokens / 1e3).toFixed(1)}k`;
35360
- if (tokens < 1e9) return `${(tokens / 1e6).toFixed(2)}M`;
35361
- return `${(tokens / 1e9).toFixed(2)}B`;
35362
- }
35363
-
35364
- // src/cli/components/StatusBar.tsx
36973
+ init_modelPricing();
35365
36974
  function StatusBar({
35366
36975
  model,
35367
36976
  provider,
@@ -35507,175 +37116,8 @@ function StartupBanner({
35507
37116
  return /* @__PURE__ */ React9.createElement(Box8, { flexDirection: "column", marginBottom: 1 }, /* @__PURE__ */ React9.createElement(Text9, { color: "cyan" }, "zelari-code v", version2, " \u2014 ", providerId, "/", model), /* @__PURE__ */ React9.createElement(Text9, { dimColor: true }, "cwd: ", cwd), /* @__PURE__ */ React9.createElement(Text9, { dimColor: true }, "/help \xB7 /plan \xB7 /build \xB7 /view-plan \xB7 shift+tab mode"));
35508
37117
  }
35509
37118
 
35510
- // src/cli/modelDiscovery.ts
35511
- import { promises as fs3, existsSync as existsSync3, readFileSync as readFileSync3 } from "node:fs";
35512
- import { homedir } from "node:os";
35513
- import path4 from "node:path";
35514
- var PROVIDER_BASE_URLS = {
35515
- "grok": "https://api.x.ai/v1",
35516
- // Must match PROVIDER_ENDPOINTS in provider/openai-compatible.ts (chat host).
35517
- "glm": "https://api.z.ai/api/coding/paas/v4",
35518
- "minimax": "https://api.minimax.io/v1",
35519
- // Must match PROVIDER_ENDPOINTS in provider/openai-compatible.ts (chat host).
35520
- "deepseek": "https://api.deepseek.com",
35521
- "openai-compatible": "https://api.x.ai/v1"
35522
- };
35523
- async function resolveDiscoveryBaseUrl(provider, options) {
35524
- if (options.baseUrl) return options.baseUrl;
35525
- const { getCustomEndpoint: getCustomEndpoint2 } = await Promise.resolve().then(() => (init_providerConfig(), providerConfig_exports));
35526
- const custom2 = getCustomEndpoint2(provider);
35527
- if (custom2) return custom2;
35528
- if (provider === "openai-compatible") {
35529
- const envBase = process.env.OPENAI_BASE_URL;
35530
- if (envBase && envBase.trim().length > 0) return envBase;
35531
- }
35532
- return PROVIDER_BASE_URLS[provider];
35533
- }
35534
- function defaultModelsFilePath() {
35535
- return process.env.ANATHEMA_MODELS_FILE ?? path4.join(homedir(), ".tmp", "zelari-code", "models.json");
35536
- }
35537
- function getModelsFilePath() {
35538
- return defaultModelsFilePath();
35539
- }
35540
- function loadModelsRegistry(file2 = getModelsFilePath()) {
35541
- if (!existsSync3(file2)) return {};
35542
- try {
35543
- const raw = readFileSync3(file2, "utf-8");
35544
- const parsed = JSON.parse(raw);
35545
- if (!parsed || typeof parsed !== "object") return {};
35546
- return parsed;
35547
- } catch {
35548
- return {};
35549
- }
35550
- }
35551
- function getCachedModels(provider, file2 = getModelsFilePath()) {
35552
- const registry3 = loadModelsRegistry(file2);
35553
- return registry3[provider];
35554
- }
35555
- function isModelsCacheStale(provider, maxAgeMs = 6 * 60 * 60 * 1e3, file2 = getModelsFilePath(), now = Date.now()) {
35556
- const entry = getCachedModels(provider, file2);
35557
- if (!entry) return true;
35558
- return now - entry.fetchedAt > maxAgeMs;
35559
- }
35560
- var writeChain = Promise.resolve();
35561
- async function withRegistryLock(fn) {
35562
- const next = writeChain.then(async () => fn());
35563
- writeChain = next.catch(() => void 0);
35564
- return next;
35565
- }
35566
- async function readModifyWriteRegistry(mutator, file2 = getModelsFilePath()) {
35567
- return withRegistryLock(async () => {
35568
- const onDisk = loadModelsRegistry(file2);
35569
- const merged = mutator(onDisk);
35570
- await fs3.mkdir(path4.dirname(file2), { recursive: true });
35571
- const tmp = `${file2}.tmp-${process.pid}-${Date.now()}`;
35572
- await fs3.writeFile(tmp, JSON.stringify(merged, null, 2), "utf-8");
35573
- await fs3.rename(tmp, file2);
35574
- return merged;
35575
- });
35576
- }
35577
- async function resolveAuthToken(provider, options) {
35578
- if (options.authToken) return options.authToken;
35579
- const { resolveApiKeyWithMeta: resolveApiKeyWithMeta2, getOAuthToken: getOAuthToken2 } = await Promise.resolve().then(() => (init_keyStore(), keyStore_exports));
35580
- if (provider === "grok") {
35581
- const oauth = getOAuthToken2("grok");
35582
- if (oauth?.apiKey) return oauth.apiKey;
35583
- }
35584
- const resolved = await resolveApiKeyWithMeta2(provider);
35585
- return resolved?.apiKey;
35586
- }
35587
- function parseOpenAIModelsResponse(json2, baseUrl) {
35588
- if (!json2 || typeof json2 !== "object") return [];
35589
- const obj = json2;
35590
- if (!Array.isArray(obj.data)) return [];
35591
- const models = [];
35592
- for (const m of obj.data) {
35593
- if (!m || typeof m.id !== "string") continue;
35594
- const out = { id: m.id };
35595
- if (typeof m.created === "number") out.created = m.created;
35596
- if (typeof m.owned_by === "string") out.ownedBy = m.owned_by;
35597
- const ctx = m.context_length;
35598
- if (typeof ctx === "number") out.contextLength = ctx;
35599
- models.push(out);
35600
- }
35601
- models.sort((a, b) => a.id.localeCompare(b.id));
35602
- return models;
35603
- }
35604
- async function discoverModelsForProvider(provider, options = {}) {
35605
- const baseUrl = await resolveDiscoveryBaseUrl(provider, options);
35606
- if (!baseUrl || baseUrl.trim().length === 0) {
35607
- throw new ModelDiscoveryError(
35608
- `No base URL for provider "${provider}" \u2014 set one with /provider custom <url> before discovering models`,
35609
- "no_base_url"
35610
- );
35611
- }
35612
- const url2 = `${baseUrl.replace(/\/$/, "")}/models`;
35613
- const authToken = await resolveAuthToken(provider, options);
35614
- const fetchImpl = options.fetchImpl ?? fetch;
35615
- let response;
35616
- try {
35617
- const headers = { Accept: "application/json" };
35618
- if (authToken) headers.Authorization = `Bearer ${authToken}`;
35619
- response = await fetchImpl(url2, { method: "GET", headers });
35620
- } catch (err) {
35621
- throw new ModelDiscoveryError(
35622
- `Network error contacting ${url2}: ${err instanceof Error ? err.message : String(err)}`,
35623
- "network_error"
35624
- );
35625
- }
35626
- if (!response.ok) {
35627
- const body = await response.text().catch(() => "");
35628
- throw new ModelDiscoveryError(
35629
- `HTTP ${response.status} from ${url2}: ${body.slice(0, 200)}`,
35630
- `http_${response.status}`
35631
- );
35632
- }
35633
- let json2;
35634
- try {
35635
- json2 = await response.json();
35636
- } catch (err) {
35637
- throw new ModelDiscoveryError(
35638
- `Invalid JSON from ${url2}: ${err instanceof Error ? err.message : String(err)}`,
35639
- "invalid_json"
35640
- );
35641
- }
35642
- const models = parseOpenAIModelsResponse(json2, baseUrl);
35643
- if (models.length === 0) {
35644
- throw new ModelDiscoveryError(
35645
- `Provider ${provider} returned 0 models \u2014 refusing to overwrite cache`,
35646
- "empty_response"
35647
- );
35648
- }
35649
- const entry = {
35650
- models,
35651
- fetchedAt: Date.now(),
35652
- baseUrl
35653
- };
35654
- if (!options.skipCacheWrite) {
35655
- const file2 = getModelsFilePath();
35656
- await readModifyWriteRegistry((current) => {
35657
- current[provider] = entry;
35658
- return current;
35659
- }, file2);
35660
- }
35661
- return entry;
35662
- }
35663
- function discoverModelsInBackground(provider, options = {}) {
35664
- discoverModelsForProvider(provider, options).catch((err) => {
35665
- if (err instanceof ModelDiscoveryError && options.onError) {
35666
- options.onError(err);
35667
- }
35668
- });
35669
- }
35670
- var ModelDiscoveryError = class extends Error {
35671
- constructor(message, code) {
35672
- super(message);
35673
- this.code = code;
35674
- this.name = "ModelDiscoveryError";
35675
- }
35676
- };
35677
-
35678
37119
  // src/cli/app.tsx
37120
+ init_modelDiscovery();
35679
37121
  init_skills2();
35680
37122
 
35681
37123
  // src/cli/hooks/useGitChanges.ts
@@ -35736,12 +37178,12 @@ function mergeChanges(unstaged, staged, untrackedPaths) {
35736
37178
  return [...byPath.values()].sort((a, b) => churn(b) - churn(a));
35737
37179
  }
35738
37180
  function runGit(args, cwd) {
35739
- return new Promise((resolve, reject) => {
37181
+ return new Promise((resolve3, reject) => {
35740
37182
  execFile(
35741
37183
  "git",
35742
37184
  args,
35743
37185
  { cwd, timeout: 5e3, windowsHide: true, maxBuffer: 4 * 1024 * 1024 },
35744
- (err, stdout) => err ? reject(err) : resolve(stdout)
37186
+ (err, stdout) => err ? reject(err) : resolve3(stdout)
35745
37187
  );
35746
37188
  });
35747
37189
  }
@@ -38391,12 +39833,12 @@ var MetricsLogger = class {
38391
39833
  }
38392
39834
  /** Append a single record synchronously (file I/O). */
38393
39835
  append(rec) {
38394
- return new Promise((resolve, reject) => {
39836
+ return new Promise((resolve3, reject) => {
38395
39837
  try {
38396
39838
  this.maybeRotate();
38397
39839
  const line = JSON.stringify(rec) + "\n";
38398
39840
  appendFileSync(this.file, line, { encoding: "utf-8" });
38399
- resolve();
39841
+ resolve3();
38400
39842
  } catch (err) {
38401
39843
  reject(err);
38402
39844
  }
@@ -38540,7 +39982,7 @@ init_toolRegistry();
38540
39982
  init_toolPermissions();
38541
39983
  function createPermissionAskHandler(opts) {
38542
39984
  const { setPicker: setPicker2, appendSystem: appendSystem2 } = opts;
38543
- return (req) => new Promise((resolve) => {
39985
+ return (req) => new Promise((resolve3) => {
38544
39986
  const cats = req.categories;
38545
39987
  const catLabel = cats.length === 1 ? cats[0] : cats.join("+") || "action";
38546
39988
  const title = `Allow tool "${req.toolName}"?`;
@@ -38557,7 +39999,7 @@ ${detail}
38557
39999
  settled = true;
38558
40000
  setPicker2(null);
38559
40001
  if (note) appendSystem2?.(note, Date.now());
38560
- resolve(ok);
40002
+ resolve3(ok);
38561
40003
  };
38562
40004
  const items = [
38563
40005
  { value: "allow", label: "Allow once" },
@@ -38705,6 +40147,9 @@ function finalizeStreamingAssistant(setMessages) {
38705
40147
  // src/cli/hooks/useChatTurn.ts
38706
40148
  init_conversationContext();
38707
40149
 
40150
+ // src/cli/hooks/chatStats.ts
40151
+ init_modelPricing();
40152
+
38708
40153
  // src/cli/state/promptCacheStats.ts
38709
40154
  function emptyPromptCacheStats() {
38710
40155
  return {
@@ -38948,10 +40393,10 @@ function useChatTurn(params) {
38948
40393
  }
38949
40394
  setBusy(true);
38950
40395
  const workPhase = getPhase();
38951
- const onAskUser = setPicker2 ? (req) => new Promise((resolve) => {
40396
+ const onAskUser = setPicker2 ? (req) => new Promise((resolve3) => {
38952
40397
  const choices = req.choices ?? [];
38953
40398
  if (choices.length < 2) {
38954
- resolve(null);
40399
+ resolve3(null);
38955
40400
  return;
38956
40401
  }
38957
40402
  setLastClarification({
@@ -38972,7 +40417,7 @@ _(${req.context})_` : "") + "\n\u2192 scegli dalla lista (il turno continua dopo
38972
40417
  if (settled) return;
38973
40418
  settled = true;
38974
40419
  setPicker2(null);
38975
- resolve(value);
40420
+ resolve3(value);
38976
40421
  };
38977
40422
  setPicker2({
38978
40423
  kind: "clarification",
@@ -39568,10 +41013,10 @@ async function dispatchCouncilPromptImpl(text, deps, overrides = {}) {
39568
41013
  const { composeProjectContext: composeProjectContext2 } = await Promise.resolve().then(() => (init_composeContext(), composeContext_exports));
39569
41014
  const { FeedbackStore: FeedbackStore2 } = await Promise.resolve().then(() => (init_councilFeedback(), councilFeedback_exports));
39570
41015
  const workPhase = getPhase();
39571
- const onAskUserCouncil = setPicker2 ? (req) => new Promise((resolve) => {
41016
+ const onAskUserCouncil = setPicker2 ? (req) => new Promise((resolve3) => {
39572
41017
  const choices = req.choices ?? [];
39573
41018
  if (choices.length < 2) {
39574
- resolve(null);
41019
+ resolve3(null);
39575
41020
  return;
39576
41021
  }
39577
41022
  setLastClarification({
@@ -39592,7 +41037,7 @@ _(${req.context})_` : "") + "\n\u2192 scegli dalla lista (il membro riprende dop
39592
41037
  if (settled) return;
39593
41038
  settled = true;
39594
41039
  setPicker2(null);
39595
- resolve(value);
41040
+ resolve3(value);
39596
41041
  };
39597
41042
  setPicker2({
39598
41043
  kind: "clarification",
@@ -39721,10 +41166,10 @@ _(${req.context})_` : "") + "\n\u2192 scegli dalla lista (il membro riprende dop
39721
41166
  appendSystem(setMessages, message, Date.now());
39722
41167
  },
39723
41168
  // v1.8.0: pause council when a member asks a structured question.
39724
- onClarification: setPicker2 ? (req) => new Promise((resolve) => {
41169
+ onClarification: setPicker2 ? (req) => new Promise((resolve3) => {
39725
41170
  const choices = req.choices ?? [];
39726
41171
  if (choices.length < 2) {
39727
- resolve(null);
41172
+ resolve3(null);
39728
41173
  return;
39729
41174
  }
39730
41175
  setLastClarification({
@@ -39745,7 +41190,7 @@ _(${req.context})_` : "") + "\n\u2192 scegli dalla lista (il membro riprende dop
39745
41190
  if (settled) return;
39746
41191
  settled = true;
39747
41192
  setPicker2(null);
39748
- resolve(value);
41193
+ resolve3(value);
39749
41194
  };
39750
41195
  setPicker2({
39751
41196
  kind: "clarification",
@@ -40357,7 +41802,8 @@ function handleSlashCommand(text, availableSkills) {
40357
41802
  /provider <name> \u2014 switch the active provider directly
40358
41803
  /provider custom <baseUrl> \u2014 point the active provider at a self-hosted endpoint (Ollama, LM Studio, vLLM, ...)
40359
41804
  /provider custom clear \u2014 clear the custom endpoint override
40360
- /skill <name> [input] \u2014 invoke a skill (autocomplete with /skill <TAB>)
41805
+ /skill [name] [input] \u2014 list+pick a skill (no args) or invoke /skill <name>
41806
+ /skills \u2014 interactive skill picker (same as /skill with no args)
40361
41807
  /skill-stats [name] \u2014 show invocation stats (success rate, avg duration, total tokens)
40362
41808
  /council <input> \u2014 invoke the multi-agent council on input
40363
41809
  /zelari <input> \u2014 run an autonomous mission (multi-run council until the MVP slice is complete)
@@ -40605,13 +42051,19 @@ ${formatSkillList(availableSkills)}`
40605
42051
  ...note ? { feedbackNote: note } : {}
40606
42052
  };
40607
42053
  }
42054
+ case "skills": {
42055
+ return {
42056
+ handled: true,
42057
+ kind: "skill_picker",
42058
+ message: formatSkillList(availableSkills)
42059
+ };
42060
+ }
40608
42061
  case "skill": {
40609
42062
  const skillId = args[0];
40610
42063
  if (!skillId) {
40611
42064
  return {
40612
42065
  handled: true,
40613
- kind: "skill",
40614
- skillError: "Usage: /skill <name> [input]",
42066
+ kind: "skill_picker",
40615
42067
  message: formatSkillList(availableSkills)
40616
42068
  };
40617
42069
  }
@@ -40620,7 +42072,7 @@ ${formatSkillList(availableSkills)}`
40620
42072
  return {
40621
42073
  handled: true,
40622
42074
  kind: "skill",
40623
- skillError: `Unknown skill: "${skillId}". Use /skill <TAB> for autocomplete.`,
42075
+ skillError: `Unknown skill: "${skillId}". Use /skills to pick one.`,
40624
42076
  message: formatSkillList(availableSkills)
40625
42077
  };
40626
42078
  }
@@ -40628,7 +42080,8 @@ ${formatSkillList(availableSkills)}`
40628
42080
  const expandedSkill = expandSkillTemplate(skill, { input });
40629
42081
  return { handled: true, kind: "skill", expandedSkill };
40630
42082
  }
40631
- case "skill_stats": {
42083
+ case "skill_stats":
42084
+ case "skill-stats": {
40632
42085
  const skillId = args[0];
40633
42086
  return {
40634
42087
  handled: true,
@@ -40904,7 +42357,7 @@ ${formatSkillList(availableSkills)}`
40904
42357
  // src/cli/gitOps.ts
40905
42358
  import { execFile as execFile3 } from "node:child_process";
40906
42359
  import { promisify as promisify2 } from "node:util";
40907
- import path30 from "node:path";
42360
+ import path31 from "node:path";
40908
42361
  var execFileAsync2 = promisify2(execFile3);
40909
42362
  async function git2(cwd, args) {
40910
42363
  try {
@@ -40950,7 +42403,7 @@ async function undoWorkingChanges(opts = {}) {
40950
42403
  };
40951
42404
  }
40952
42405
  function defaultProjectRoot() {
40953
- return path30.resolve(__dirname, "..", "..", "..");
42406
+ return path31.resolve(__dirname, "..", "..", "..");
40954
42407
  }
40955
42408
 
40956
42409
  // src/cli/slashHandlers/git.ts
@@ -41474,7 +42927,7 @@ ${browsers.output}` : "") + (browsers.ok ? "" : `
41474
42927
  return result;
41475
42928
  }
41476
42929
  function runPlaywrightInstallChromium(cwd, executor) {
41477
- return new Promise((resolve) => {
42930
+ return new Promise((resolve3) => {
41478
42931
  let stdout = "";
41479
42932
  let stderr = "";
41480
42933
  const stdio = ["ignore", "pipe", "pipe"];
@@ -41487,7 +42940,7 @@ function runPlaywrightInstallChromium(cwd, executor) {
41487
42940
  stderr += chunk.toString();
41488
42941
  });
41489
42942
  child.on("error", (err) => {
41490
- resolve({
42943
+ resolve3({
41491
42944
  ok: false,
41492
42945
  output: stdout + stderr,
41493
42946
  error: err.message,
@@ -41496,7 +42949,7 @@ function runPlaywrightInstallChromium(cwd, executor) {
41496
42949
  });
41497
42950
  child.on("close", (code) => {
41498
42951
  const ok = code === 0;
41499
- resolve({
42952
+ resolve3({
41500
42953
  ok,
41501
42954
  output: stdout + stderr,
41502
42955
  exitCode: code,
@@ -41506,7 +42959,7 @@ function runPlaywrightInstallChromium(cwd, executor) {
41506
42959
  });
41507
42960
  }
41508
42961
  function runNpm2(executor, args, cwd, mode, npmCliPath) {
41509
- return new Promise((resolve) => {
42962
+ return new Promise((resolve3) => {
41510
42963
  let stdout = "";
41511
42964
  let stderr = "";
41512
42965
  const stdio = ["ignore", "pipe", "pipe"];
@@ -41518,7 +42971,7 @@ function runNpm2(executor, args, cwd, mode, npmCliPath) {
41518
42971
  stderr += chunk.toString();
41519
42972
  });
41520
42973
  child.on("error", (err) => {
41521
- resolve({
42974
+ resolve3({
41522
42975
  ok: false,
41523
42976
  output: stdout + stderr,
41524
42977
  error: err.message,
@@ -41527,7 +42980,7 @@ function runNpm2(executor, args, cwd, mode, npmCliPath) {
41527
42980
  });
41528
42981
  child.on("close", (code) => {
41529
42982
  const ok = code === 0;
41530
- resolve({
42983
+ resolve3({
41531
42984
  ok,
41532
42985
  output: stdout + stderr,
41533
42986
  exitCode: code,
@@ -41590,17 +43043,17 @@ ${result.output.split("\n").slice(-8).join("\n")}` : "";
41590
43043
  }
41591
43044
 
41592
43045
  // src/cli/slashHandlers/promoteMember.ts
41593
- import { promises as fs18 } from "node:fs";
41594
- import path33 from "node:path";
43046
+ import { promises as fs19 } from "node:fs";
43047
+ import path34 from "node:path";
41595
43048
  import os10 from "node:os";
41596
43049
  async function handlePromoteMember(ctx, memberId) {
41597
43050
  try {
41598
43051
  const { promoteMember: promoteMember2 } = await Promise.resolve().then(() => (init_council(), council_exports));
41599
43052
  const { skill, markdown } = promoteMember2(memberId);
41600
- const skillDir = process.env.ANATHEMA_SKILL_DIR ?? path33.join(os10.homedir(), ".tmp", "zelari-code", "skills");
41601
- await fs18.mkdir(skillDir, { recursive: true });
41602
- const filePath = path33.join(skillDir, `${skill.id}.md`);
41603
- await fs18.writeFile(filePath, markdown, "utf8");
43053
+ const skillDir = process.env.ANATHEMA_SKILL_DIR ?? path34.join(os10.homedir(), ".tmp", "zelari-code", "skills");
43054
+ await fs19.mkdir(skillDir, { recursive: true });
43055
+ const filePath = path34.join(skillDir, `${skill.id}.md`);
43056
+ await fs19.writeFile(filePath, markdown, "utf8");
41604
43057
  appendSystem(
41605
43058
  ctx.setMessages,
41606
43059
  `[promote-member] ${skill.name} (${memberId}) \u2192 ${filePath}
@@ -41616,25 +43069,25 @@ async function handlePromoteMember(ctx, memberId) {
41616
43069
  }
41617
43070
 
41618
43071
  // src/cli/branchManager.ts
41619
- import { promises as fs19, existsSync as existsSync35, readFileSync as readFileSync29, writeFileSync as writeFileSync19, mkdirSync as mkdirSync14, statSync as statSync4, rmSync as rmSync2 } from "node:fs";
41620
- import path34 from "node:path";
43072
+ import { promises as fs20, existsSync as existsSync35, readFileSync as readFileSync29, writeFileSync as writeFileSync19, mkdirSync as mkdirSync14, statSync as statSync4, rmSync as rmSync2 } from "node:fs";
43073
+ import path35 from "node:path";
41621
43074
  import os11 from "node:os";
41622
43075
  var META_FILENAME = "meta.json";
41623
43076
  var SESSIONS_SUBDIR = "sessions";
41624
43077
  function getBranchesBaseDir() {
41625
- return process.env.ANATHEMA_BRANCHES_DIR ?? path34.join(os11.homedir(), ".tmp", "zelari-code", "branches");
43078
+ return process.env.ANATHEMA_BRANCHES_DIR ?? path35.join(os11.homedir(), ".tmp", "zelari-code", "branches");
41626
43079
  }
41627
43080
  function getSessionsBaseDir() {
41628
- return process.env.ANATHEMA_SESSIONS_DIR ?? path34.join(os11.homedir(), ".tmp", "zelari-code", "sessions");
43081
+ return process.env.ANATHEMA_SESSIONS_DIR ?? path35.join(os11.homedir(), ".tmp", "zelari-code", "sessions");
41629
43082
  }
41630
43083
  function branchPathFor(name, baseDir) {
41631
- return path34.join(baseDir, name);
43084
+ return path35.join(baseDir, name);
41632
43085
  }
41633
43086
  function metaPathFor(name, baseDir) {
41634
- return path34.join(baseDir, name, META_FILENAME);
43087
+ return path35.join(baseDir, name, META_FILENAME);
41635
43088
  }
41636
43089
  function sessionsPathFor(name, baseDir) {
41637
- return path34.join(baseDir, name, SESSIONS_SUBDIR);
43090
+ return path35.join(baseDir, name, SESSIONS_SUBDIR);
41638
43091
  }
41639
43092
  function readBranchMeta(name, baseDir) {
41640
43093
  const metaPath = metaPathFor(name, baseDir);
@@ -41659,13 +43112,13 @@ function readBranchMeta(name, baseDir) {
41659
43112
  }
41660
43113
  function writeBranchMeta(name, baseDir, meta3) {
41661
43114
  const metaPath = metaPathFor(name, baseDir);
41662
- mkdirSync14(path34.dirname(metaPath), { recursive: true });
43115
+ mkdirSync14(path35.dirname(metaPath), { recursive: true });
41663
43116
  writeFileSync19(metaPath, JSON.stringify(meta3, null, 2), "utf-8");
41664
43117
  }
41665
43118
  async function countSessions(name, baseDir) {
41666
43119
  const sessionsPath = sessionsPathFor(name, baseDir);
41667
43120
  try {
41668
- const entries = await fs19.readdir(sessionsPath);
43121
+ const entries = await fs20.readdir(sessionsPath);
41669
43122
  return entries.filter((e) => e.endsWith(".jsonl")).length;
41670
43123
  } catch (err) {
41671
43124
  if (err.code === "ENOENT") return 0;
@@ -41710,15 +43163,15 @@ async function createBranch(name, fromSessionId, baseDir = getBranchesBaseDir(),
41710
43163
  if (branchExists(name, baseDir)) {
41711
43164
  throw new BranchAlreadyExistsError(name);
41712
43165
  }
41713
- const sourcePath = path34.join(sessionsBaseDir, `${fromSessionId}.jsonl`);
43166
+ const sourcePath = path35.join(sessionsBaseDir, `${fromSessionId}.jsonl`);
41714
43167
  if (!existsSync35(sourcePath)) {
41715
43168
  throw new SessionNotFoundError(`Source session "${fromSessionId}" not found at ${sourcePath}`);
41716
43169
  }
41717
43170
  const branchPath = branchPathFor(name, baseDir);
41718
43171
  const branchSessionsPath = sessionsPathFor(name, baseDir);
41719
43172
  mkdirSync14(branchSessionsPath, { recursive: true });
41720
- const destPath = path34.join(branchSessionsPath, `${fromSessionId}.jsonl`);
41721
- await fs19.copyFile(sourcePath, destPath);
43173
+ const destPath = path35.join(branchSessionsPath, `${fromSessionId}.jsonl`);
43174
+ await fs20.copyFile(sourcePath, destPath);
41722
43175
  const meta3 = {
41723
43176
  name,
41724
43177
  createdAt: Date.now(),
@@ -41736,7 +43189,7 @@ async function createBranch(name, fromSessionId, baseDir = getBranchesBaseDir(),
41736
43189
  async function listBranches(baseDir = getBranchesBaseDir()) {
41737
43190
  let entries;
41738
43191
  try {
41739
- entries = await fs19.readdir(baseDir);
43192
+ entries = await fs20.readdir(baseDir);
41740
43193
  } catch (err) {
41741
43194
  if (err.code === "ENOENT") return [];
41742
43195
  throw err;
@@ -41817,26 +43270,26 @@ async function handleBranchCheckout(ctx, branchName) {
41817
43270
  }
41818
43271
 
41819
43272
  // src/cli/slashHandlers/workspace.ts
41820
- import { promises as fs20 } from "node:fs";
41821
- import path35 from "node:path";
43273
+ import { promises as fs21 } from "node:fs";
43274
+ import path36 from "node:path";
41822
43275
  async function handleWorkspaceShow(ctx, what) {
41823
43276
  try {
41824
- const zelari = path35.join(process.cwd(), ".zelari");
43277
+ const zelari = path36.join(process.cwd(), ".zelari");
41825
43278
  let content;
41826
43279
  switch (what) {
41827
43280
  case "plan": {
41828
- const planPath = path35.join(zelari, "plan.md");
43281
+ const planPath = path36.join(zelari, "plan.md");
41829
43282
  try {
41830
- content = await fs20.readFile(planPath, "utf-8");
43283
+ content = await fs21.readFile(planPath, "utf-8");
41831
43284
  } catch {
41832
43285
  content = "(no plan.md yet \u2014 run a council session first)";
41833
43286
  }
41834
43287
  break;
41835
43288
  }
41836
43289
  case "decisions": {
41837
- const decisionsDir = path35.join(zelari, "decisions");
43290
+ const decisionsDir = path36.join(zelari, "decisions");
41838
43291
  try {
41839
- const files = (await fs20.readdir(decisionsDir)).filter((f) => f.endsWith(".md")).sort();
43292
+ const files = (await fs21.readdir(decisionsDir)).filter((f) => f.endsWith(".md")).sort();
41840
43293
  if (files.length === 0) {
41841
43294
  content = "(no ADRs yet \u2014 invoke /council to generate some)";
41842
43295
  } else {
@@ -41844,7 +43297,7 @@ async function handleWorkspaceShow(ctx, what) {
41844
43297
  `];
41845
43298
  const { parseFrontmatter: parseFrontmatter2 } = await Promise.resolve().then(() => (init_storage(), storage_exports));
41846
43299
  for (const f of files) {
41847
- const raw = await fs20.readFile(path35.join(decisionsDir, f), "utf-8");
43300
+ const raw = await fs21.readFile(path36.join(decisionsDir, f), "utf-8");
41848
43301
  const { meta: meta3, body } = parseFrontmatter2(raw);
41849
43302
  const title = meta3.title ?? body.split("\n")[0]?.replace(/^#\s*/, "").trim() ?? f;
41850
43303
  lines.push(`- **${f.replace(/\.md$/, "")}** [${meta3.status ?? "unknown"}] ${title}`);
@@ -41857,27 +43310,27 @@ async function handleWorkspaceShow(ctx, what) {
41857
43310
  break;
41858
43311
  }
41859
43312
  case "risks": {
41860
- const risksPath = path35.join(zelari, "risks.md");
43313
+ const risksPath = path36.join(zelari, "risks.md");
41861
43314
  try {
41862
- content = await fs20.readFile(risksPath, "utf-8");
43315
+ content = await fs21.readFile(risksPath, "utf-8");
41863
43316
  } catch {
41864
43317
  content = "(no risks.md yet)";
41865
43318
  }
41866
43319
  break;
41867
43320
  }
41868
43321
  case "agents": {
41869
- const agentsPath = path35.join(process.cwd(), "AGENTS.MD");
43322
+ const agentsPath = path36.join(process.cwd(), "AGENTS.MD");
41870
43323
  try {
41871
- content = await fs20.readFile(agentsPath, "utf-8");
43324
+ content = await fs21.readFile(agentsPath, "utf-8");
41872
43325
  } catch {
41873
43326
  content = "(no AGENTS.MD yet at project root \u2014 run `/workspace sync` after a council session)";
41874
43327
  }
41875
43328
  break;
41876
43329
  }
41877
43330
  case "docs": {
41878
- const docsDir = path35.join(zelari, "docs");
43331
+ const docsDir = path36.join(zelari, "docs");
41879
43332
  try {
41880
- const files = (await fs20.readdir(docsDir)).filter((f) => f.endsWith(".md")).sort();
43333
+ const files = (await fs21.readdir(docsDir)).filter((f) => f.endsWith(".md")).sort();
41881
43334
  content = files.length ? `# Docs (${files.length})
41882
43335
 
41883
43336
  ` + files.map((f) => `- ${f}`).join("\n") : "(no docs drafts yet)";
@@ -41917,8 +43370,8 @@ async function handleWorkspaceReset(ctx, force) {
41917
43370
  return;
41918
43371
  }
41919
43372
  try {
41920
- const target = path35.join(process.cwd(), ".zelari");
41921
- await fs20.rm(target, { recursive: true, force: true });
43373
+ const target = path36.join(process.cwd(), ".zelari");
43374
+ await fs21.rm(target, { recursive: true, force: true });
41922
43375
  appendSystem(ctx.setMessages, "[workspace] .zelari/ removed");
41923
43376
  } catch (err) {
41924
43377
  appendSystem(ctx.setMessages, `[workspace reset error] ${err instanceof Error ? err.message : String(err)}`);
@@ -41980,6 +43433,7 @@ async function validateApiKey(providerId, apiKey, options = {}) {
41980
43433
 
41981
43434
  // src/cli/slashHandlers/provider.ts
41982
43435
  init_providerConfig();
43436
+ init_modelDiscovery();
41983
43437
  var UNKNOWN_PROVIDER_MSG = (id) => `[provider] unknown: ${id}. Available: openai-compatible, minimax, glm, grok, deepseek, custom`;
41984
43438
  var DISCOVERABLE_PROVIDERS = ["grok", "glm", "minimax", "deepseek", "openai-compatible"];
41985
43439
  function handleProviderList(ctx) {
@@ -42276,16 +43730,16 @@ function handleModelsRefresh(ctx) {
42276
43730
  }
42277
43731
 
42278
43732
  // src/cli/slashHandlers/skills.ts
42279
- import path36 from "node:path";
43733
+ import path37 from "node:path";
42280
43734
  import os12 from "node:os";
42281
43735
 
42282
43736
  // src/cli/skillHistory.ts
42283
- import { promises as fs21, existsSync as existsSync36, statSync as statSync5, renameSync as renameSync4, appendFileSync as appendFileSync3, mkdirSync as mkdirSync15 } from "node:fs";
43737
+ import { promises as fs22, existsSync as existsSync36, statSync as statSync5, renameSync as renameSync4, appendFileSync as appendFileSync3, mkdirSync as mkdirSync15 } from "node:fs";
42284
43738
  var SKILL_HISTORY_ROTATE_BYTES = 10 * 1024 * 1024;
42285
43739
  async function readSkillHistory(file2) {
42286
43740
  let raw = "";
42287
43741
  try {
42288
- raw = await fs21.readFile(file2, "utf-8");
43742
+ raw = await fs22.readFile(file2, "utf-8");
42289
43743
  } catch {
42290
43744
  return [];
42291
43745
  }
@@ -42377,8 +43831,32 @@ async function applySteerInterrupt(options) {
42377
43831
  }
42378
43832
 
42379
43833
  // src/cli/slashHandlers/skills.ts
43834
+ function handleSkillPicker(ctx, skills, openPicker, fallbackMessage) {
43835
+ if (!openPicker) {
43836
+ appendSystem(
43837
+ ctx.setMessages,
43838
+ fallbackMessage ?? formatSkillList(skills)
43839
+ );
43840
+ return;
43841
+ }
43842
+ if (skills.length === 0) {
43843
+ appendSystem(ctx.setMessages, "[skills] no skills registered");
43844
+ return;
43845
+ }
43846
+ const items = [...skills].sort((a, b) => a.id.localeCompare(b.id)).map((s) => ({
43847
+ value: s.id,
43848
+ label: s.name || s.id,
43849
+ hint: [s.category, s.estimatedCost, s.description?.slice(0, 60)].filter(Boolean).join(" \xB7 ")
43850
+ }));
43851
+ openPicker({
43852
+ kind: "skill",
43853
+ title: "Select a skill",
43854
+ items,
43855
+ commandPrefix: "/skill"
43856
+ });
43857
+ }
42380
43858
  async function handleSkillStats(ctx, skillId) {
42381
- const historyFile = process.env.ANATHEMA_SKILL_HISTORY_FILE ?? path36.join(os12.homedir(), ".tmp", "zelari-code", "skill-history.jsonl");
43859
+ const historyFile = process.env.ANATHEMA_SKILL_HISTORY_FILE ?? path37.join(os12.homedir(), ".tmp", "zelari-code", "skill-history.jsonl");
42382
43860
  try {
42383
43861
  const records = await readSkillHistory(historyFile);
42384
43862
  const stats = getSkillStats(records, skillId);
@@ -42394,7 +43872,7 @@ async function handleSkillCompare(ctx, ids, fallbackMessage) {
42394
43872
  appendSystem(ctx.setMessages, fallbackMessage ?? "[skill-compare] missing args");
42395
43873
  return;
42396
43874
  }
42397
- const historyFile = process.env.ANATHEMA_SKILL_HISTORY_FILE ?? path36.join(os12.homedir(), ".tmp", "zelari-code", "skill-history.jsonl");
43875
+ const historyFile = process.env.ANATHEMA_SKILL_HISTORY_FILE ?? path37.join(os12.homedir(), ".tmp", "zelari-code", "skill-history.jsonl");
42398
43876
  try {
42399
43877
  const formatted = await compareSkillsFromFile(ids[0], ids[1], historyFile);
42400
43878
  appendSystem(ctx.setMessages, formatted);
@@ -42450,6 +43928,7 @@ function handleClearChat(setMessages, setSessionActive) {
42450
43928
  }
42451
43929
 
42452
43930
  // src/cli/hooks/useSlashDispatch.ts
43931
+ init_atMentions();
42453
43932
  function useSlashDispatch(params) {
42454
43933
  const {
42455
43934
  skills,
@@ -42490,19 +43969,27 @@ function useSlashDispatch(params) {
42490
43969
  const steerCtx = { ...skillCtx, harnessRef, setQueueCount, dispatchPrompt };
42491
43970
  const { onNewSession, onExit } = params;
42492
43971
  if (!result.handled) {
43972
+ const expanded = expandAtMentions(value, process.cwd());
43973
+ const promptText = expanded.text;
42493
43974
  appendUser(setMessages, value);
43975
+ if (expanded.hits.length > 0) {
43976
+ appendSystem(
43977
+ setMessages,
43978
+ `[tagged] ${expanded.hits.map((h) => h.path).join(", ")}`
43979
+ );
43980
+ }
42494
43981
  setSessionActive(true);
42495
43982
  if (mode === "zelari") {
42496
43983
  setInput("");
42497
- await dispatchZelariPrompt(value);
43984
+ await dispatchZelariPrompt(promptText);
42498
43985
  return;
42499
43986
  }
42500
43987
  if (mode === "council") {
42501
43988
  setInput("");
42502
- await dispatchCouncilPrompt(value);
43989
+ await dispatchCouncilPrompt(promptText);
42503
43990
  return;
42504
43991
  }
42505
- await dispatchPrompt(value);
43992
+ await dispatchPrompt(promptText);
42506
43993
  setInput("");
42507
43994
  return;
42508
43995
  }
@@ -42852,11 +44339,20 @@ function useSlashDispatch(params) {
42852
44339
  setInput("");
42853
44340
  return;
42854
44341
  }
44342
+ if (result.kind === "skill_picker") {
44343
+ handleSkillPicker(skillCtx, skills, params.openPicker, result.message);
44344
+ setInput("");
44345
+ return;
44346
+ }
42855
44347
  if (result.kind === "skill" && result.expandedSkill) {
42856
44348
  const sysMsg = result.message ?? `[skill] ${result.expandedSkill.skillId} \u2014 prompt ready (dispatch lands in Phase 14.7)`;
42857
44349
  appendSystem(setMessages, sysMsg);
42858
44350
  setInput("");
42859
- await dispatchPrompt(result.expandedSkill.prompt, {
44351
+ const skillPrompt = expandAtMentions(
44352
+ result.expandedSkill.prompt,
44353
+ process.cwd()
44354
+ ).text;
44355
+ await dispatchPrompt(skillPrompt, {
42860
44356
  requiredTools: result.expandedSkill.requiredTools
42861
44357
  });
42862
44358
  return;
@@ -43507,9 +45003,9 @@ function ContinueKey({ onContinue }) {
43507
45003
  init_providerConfig();
43508
45004
 
43509
45005
  // src/cli/wizard/firstRun.ts
43510
- import { existsSync as existsSync37 } from "node:fs";
45006
+ import { existsSync as existsSync38 } from "node:fs";
43511
45007
  function shouldRunWizard(input) {
43512
- const exists = input.exists ?? existsSync37;
45008
+ const exists = input.exists ?? existsSync38;
43513
45009
  if (input.hasResetConfigFlag) {
43514
45010
  return { shouldRun: true, reason: "--reset-config flag forced wizard" };
43515
45011
  }
@@ -43794,7 +45290,7 @@ init_keyStore();
43794
45290
  init_providerConfig();
43795
45291
  init_openai_compatible();
43796
45292
  init_phase();
43797
- import { readFileSync as readFileSync30 } from "node:fs";
45293
+ import { readFileSync as readFileSync31 } from "node:fs";
43798
45294
  function parseHeadlessFlags(argv) {
43799
45295
  if (!argv.includes("--headless")) {
43800
45296
  return { options: null };
@@ -43808,6 +45304,7 @@ function parseHeadlessFlags(argv) {
43808
45304
  let provider;
43809
45305
  let model;
43810
45306
  let history2;
45307
+ let once = false;
43811
45308
  for (let i = 0; i < argv.length; i++) {
43812
45309
  const arg = argv[i];
43813
45310
  if (arg === "--headless") continue;
@@ -43862,7 +45359,7 @@ function parseHeadlessFlags(argv) {
43862
45359
  let raw = null;
43863
45360
  if (arg === "--history-file") {
43864
45361
  try {
43865
- raw = readFileSync30(next, "utf-8");
45362
+ raw = readFileSync31(next, "utf-8");
43866
45363
  } catch {
43867
45364
  raw = null;
43868
45365
  }
@@ -43896,6 +45393,8 @@ function parseHeadlessFlags(argv) {
43896
45393
  }
43897
45394
  i++;
43898
45395
  }
45396
+ } else if (arg === "--once") {
45397
+ once = true;
43899
45398
  }
43900
45399
  }
43901
45400
  if (councilFlag && !modeExplicit) {
@@ -43918,7 +45417,8 @@ function parseHeadlessFlags(argv) {
43918
45417
  useCouncil: mode === "council",
43919
45418
  provider,
43920
45419
  model,
43921
- ...history2 && history2.length > 0 ? { history: history2 } : {}
45420
+ ...history2 && history2.length > 0 ? { history: history2 } : {},
45421
+ ...once ? { once: true } : {}
43922
45422
  }
43923
45423
  };
43924
45424
  }
@@ -43989,6 +45489,17 @@ function createStreamScrubber() {
43989
45489
 
43990
45490
  // src/cli/runHeadless.ts
43991
45491
  async function runHeadless(opts) {
45492
+ try {
45493
+ const { expandAtMentions: expandAtMentions2 } = await Promise.resolve().then(() => (init_atMentions(), atMentions_exports));
45494
+ const task = typeof opts.task === "string" ? opts.task : "";
45495
+ if (task.includes("@")) {
45496
+ const { text } = expandAtMentions2(task, process.cwd());
45497
+ if (text !== task) {
45498
+ opts = { ...opts, task: text };
45499
+ }
45500
+ }
45501
+ } catch {
45502
+ }
43992
45503
  let crashed = false;
43993
45504
  const handleFatal = (label, err) => {
43994
45505
  if (crashed) return;
@@ -44601,6 +46112,22 @@ ${JSON.stringify({ deliverable: brief.deliverableThisMission, mvp: brief.sliceMv
44601
46112
  }
44602
46113
  let exitCode = 0;
44603
46114
  let lastMissionAssistant = "";
46115
+ let lockAcquired = false;
46116
+ if (opts.once) {
46117
+ const { acquireLock: acquireLock2 } = await Promise.resolve().then(() => (init_triggerLock(), triggerLock_exports));
46118
+ const lockRes = await acquireLock2(projectRoot);
46119
+ if (!lockRes.acquired) {
46120
+ process.stderr.write(
46121
+ `[zelari-code --once] skip: another mission is running (pid ${lockRes.heldBy}).
46122
+ `
46123
+ );
46124
+ return 0;
46125
+ }
46126
+ lockAcquired = true;
46127
+ if (!process.env.ZELARI_MISSION_MAX_ITER) {
46128
+ process.env.ZELARI_MISSION_MAX_ITER = "1";
46129
+ }
46130
+ }
44604
46131
  try {
44605
46132
  const state2 = await runZelariMission2(missionTask, brief, {
44606
46133
  projectRoot,
@@ -44819,6 +46346,10 @@ ${ragContext}` : slicePrompt;
44819
46346
  );
44820
46347
  return 2;
44821
46348
  } finally {
46349
+ if (lockAcquired) {
46350
+ const { releaseLock: releaseLock2 } = await Promise.resolve().then(() => (init_triggerLock(), triggerLock_exports));
46351
+ await releaseLock2(projectRoot);
46352
+ }
44822
46353
  await memory.close().catch(() => void 0);
44823
46354
  }
44824
46355
  if (opts.output === "json") {
@@ -44836,250 +46367,8 @@ ${ragContext}` : slicePrompt;
44836
46367
  return exitCode;
44837
46368
  }
44838
46369
 
44839
- // src/cli/desktopConfig.ts
44840
- init_keyStore();
44841
- init_providerConfig();
44842
- init_updater();
44843
- function wantsPrintConfig(argv) {
44844
- return argv.includes("--print-config");
44845
- }
44846
- function wantsSetKey(argv) {
44847
- return argv.includes("--set-key");
44848
- }
44849
- function wantsDiscoverModels(argv) {
44850
- return argv.includes("--discover-models");
44851
- }
44852
- function parseSetConfigFlags(argv) {
44853
- if (!argv.includes("--set-config")) {
44854
- return { request: null };
44855
- }
44856
- let provider;
44857
- let model;
44858
- let endpoint;
44859
- let endpointClear = false;
44860
- for (let i = 0; i < argv.length; i++) {
44861
- const arg = argv[i];
44862
- if (arg === "--provider") {
44863
- provider = argv[i + 1];
44864
- i++;
44865
- } else if (arg === "--model") {
44866
- model = argv[i + 1];
44867
- i++;
44868
- } else if (arg === "--endpoint") {
44869
- endpoint = argv[i + 1];
44870
- i++;
44871
- } else if (arg === "--endpoint-clear") {
44872
- endpointClear = true;
44873
- }
44874
- }
44875
- if (!provider && !model && !endpoint && !endpointClear) {
44876
- return {
44877
- request: null,
44878
- error: "--set-config requires --provider, --model, --endpoint, and/or --endpoint-clear"
44879
- };
44880
- }
44881
- if (provider !== void 0 && provider.trim().length === 0) {
44882
- return { request: null, error: "--provider cannot be empty" };
44883
- }
44884
- if (model !== void 0 && model.trim().length === 0) {
44885
- return { request: null, error: "--model cannot be empty" };
44886
- }
44887
- if (endpoint !== void 0 && endpoint.trim().length === 0) {
44888
- return { request: null, error: "--endpoint cannot be empty" };
44889
- }
44890
- if (endpoint && endpointClear) {
44891
- return { request: null, error: "--endpoint and --endpoint-clear conflict" };
44892
- }
44893
- return {
44894
- request: {
44895
- provider: provider?.trim(),
44896
- model: model?.trim(),
44897
- endpoint: endpoint?.trim(),
44898
- endpointClear: endpointClear || void 0
44899
- }
44900
- };
44901
- }
44902
- function parseSetKeyFlags(argv) {
44903
- if (!argv.includes("--set-key")) {
44904
- return { request: null };
44905
- }
44906
- let provider;
44907
- let key;
44908
- for (let i = 0; i < argv.length; i++) {
44909
- const arg = argv[i];
44910
- if (arg === "--provider") {
44911
- provider = argv[i + 1];
44912
- i++;
44913
- } else if (arg === "--key") {
44914
- key = argv[i + 1];
44915
- i++;
44916
- }
44917
- }
44918
- if (!provider || provider.trim().length === 0) {
44919
- return { request: null, error: "--set-key requires --provider <id>" };
44920
- }
44921
- if (!key || key.trim().length === 0) {
44922
- return { request: null, error: "--set-key requires --key <secret>" };
44923
- }
44924
- return {
44925
- request: {
44926
- provider: provider.trim(),
44927
- key: key.trim()
44928
- }
44929
- };
44930
- }
44931
- function parseDiscoverModelsFlags(argv) {
44932
- if (!argv.includes("--discover-models")) {
44933
- return { provider: null, present: false };
44934
- }
44935
- let provider;
44936
- for (let i = 0; i < argv.length; i++) {
44937
- if (argv[i] === "--provider") {
44938
- provider = argv[i + 1];
44939
- i++;
44940
- }
44941
- }
44942
- return {
44943
- present: true,
44944
- provider: provider?.trim() || null
44945
- };
44946
- }
44947
- function buildDesktopConfigSnapshot() {
44948
- const config2 = getProviderConfig();
44949
- const providers = PROVIDERS.map((p3) => {
44950
- const cached2 = getCachedModels(p3.id);
44951
- const models = cached2?.models.map((m) => m.id) ?? [];
44952
- const defaultModel = config2.modelByProvider[p3.id] ?? "";
44953
- if (defaultModel && !models.includes(defaultModel)) {
44954
- models.unshift(defaultModel);
44955
- }
44956
- const custom2 = getCustomEndpoint(p3.id);
44957
- const builtin = p3.baseUrl ?? null;
44958
- return {
44959
- id: p3.id,
44960
- displayName: p3.displayName,
44961
- hasKey: !!resolveApiKey(p3.id),
44962
- envVar: p3.envVar,
44963
- models,
44964
- defaultModel,
44965
- endpoint: custom2 ?? null,
44966
- baseUrl: custom2 ?? builtin
44967
- };
44968
- });
44969
- return {
44970
- activeProviderId: config2.activeProviderId,
44971
- modelByProvider: { ...config2.modelByProvider },
44972
- providers,
44973
- cliVersion: getCurrentVersion(),
44974
- configPaths: {
44975
- provider: getProviderConfigPath(),
44976
- keys: getKeyStorePath()
44977
- }
44978
- };
44979
- }
44980
- function printDesktopConfig() {
44981
- const snap = buildDesktopConfigSnapshot();
44982
- process.stdout.write(JSON.stringify(snap, null, 2) + "\n");
44983
- }
44984
- function applySetConfig(req) {
44985
- try {
44986
- const config2 = getProviderConfig();
44987
- let targetProvider = req.provider ?? config2.activeProviderId;
44988
- if (req.provider) {
44989
- const exists = PROVIDERS.some((p3) => p3.id === req.provider);
44990
- if (!exists) {
44991
- return {
44992
- ok: false,
44993
- error: `unknown provider '${req.provider}'. Available: ${PROVIDERS.map((p3) => p3.id).join(", ")}`
44994
- };
44995
- }
44996
- setActiveProviderId(req.provider);
44997
- targetProvider = req.provider;
44998
- }
44999
- if (req.endpointClear) {
45000
- clearCustomEndpoint(targetProvider);
45001
- }
45002
- if (req.endpoint) {
45003
- setCustomEndpoint(targetProvider, req.endpoint);
45004
- }
45005
- if (req.model) {
45006
- setModelForProvider(targetProvider, req.model);
45007
- }
45008
- const after = getProviderConfig();
45009
- const ep = getCustomEndpoint(after.activeProviderId);
45010
- return {
45011
- ok: true,
45012
- message: `activeProvider=${after.activeProviderId} model=${after.modelByProvider[after.activeProviderId]}` + (ep ? ` endpoint=${ep}` : "")
45013
- };
45014
- } catch (err) {
45015
- return {
45016
- ok: false,
45017
- error: err instanceof Error ? err.message : String(err)
45018
- };
45019
- }
45020
- }
45021
- function applySetKey(req) {
45022
- try {
45023
- const exists = PROVIDERS.some((p3) => p3.id === req.provider);
45024
- if (!exists) {
45025
- return {
45026
- ok: false,
45027
- error: `unknown provider '${req.provider}'. Available: ${PROVIDERS.map((p3) => p3.id).join(", ")}`
45028
- };
45029
- }
45030
- setApiKey(req.provider, req.key);
45031
- setActiveProviderId(req.provider);
45032
- return {
45033
- ok: true,
45034
- provider: req.provider,
45035
- masked: maskKey(req.key)
45036
- };
45037
- } catch (err) {
45038
- return {
45039
- ok: false,
45040
- error: err instanceof Error ? err.message : String(err)
45041
- };
45042
- }
45043
- }
45044
- var DISCOVERABLE = [
45045
- "grok",
45046
- "glm",
45047
- "minimax",
45048
- "deepseek",
45049
- "openai-compatible"
45050
- ];
45051
- async function runDiscoverModels(providerArg) {
45052
- const config2 = getProviderConfig();
45053
- const providerId = (providerArg ?? config2.activeProviderId).trim();
45054
- if (!DISCOVERABLE.includes(providerId)) {
45055
- if (providerId === "custom") {
45056
- } else {
45057
- return {
45058
- ok: false,
45059
- error: `provider '${providerId}' is not discoverable. Use: ${DISCOVERABLE.join(", ")}`
45060
- };
45061
- }
45062
- }
45063
- const discoveryId = providerId === "custom" ? "openai-compatible" : providerId;
45064
- try {
45065
- const entry = await discoverModelsForProvider(discoveryId);
45066
- return {
45067
- ok: true,
45068
- payload: {
45069
- ok: true,
45070
- provider: providerId,
45071
- models: entry.models.map((m) => m.id),
45072
- fetchedAt: entry.fetchedAt,
45073
- baseUrl: entry.baseUrl
45074
- }
45075
- };
45076
- } catch (err) {
45077
- return {
45078
- ok: false,
45079
- error: err instanceof Error ? err.message : String(err)
45080
- };
45081
- }
45082
- }
46370
+ // src/cli/main.ts
46371
+ init_desktopConfig();
45083
46372
 
45084
46373
  // src/cli/plugins/cliFlags.ts
45085
46374
  init_registry2();
@@ -45178,6 +46467,451 @@ init_skillsMd();
45178
46467
  init_skills2();
45179
46468
  init_updater();
45180
46469
  init_mcpConfigIo();
46470
+
46471
+ // src/cli/skillConfigIo.ts
46472
+ init_skills2();
46473
+ init_skillsMd();
46474
+ import {
46475
+ existsSync as existsSync39,
46476
+ mkdirSync as mkdirSync16,
46477
+ readdirSync as readdirSync6,
46478
+ readFileSync as readFileSync32,
46479
+ rmSync as rmSync3,
46480
+ writeFileSync as writeFileSync20
46481
+ } from "node:fs";
46482
+ import { dirname as dirname9, join as join34 } from "node:path";
46483
+ import { homedir as homedir10 } from "node:os";
46484
+
46485
+ // src/cli/skillCategories.ts
46486
+ var CODING_CATEGORIES_LIST = [
46487
+ "plan",
46488
+ "refactor",
46489
+ "debug",
46490
+ "review",
46491
+ "test",
46492
+ "docs",
46493
+ "ops",
46494
+ "git",
46495
+ "db",
46496
+ "maint"
46497
+ ];
46498
+ var CODING_CATEGORIES2 = new Set(CODING_CATEGORIES_LIST);
46499
+
46500
+ // src/cli/skillConfigIo.ts
46501
+ var NAME_RE = /^[a-z0-9][a-z0-9-]{0,63}$/;
46502
+ var BUILTIN_SKILL_MODULES = [
46503
+ "@zelari/core/skills/builtin/debugging",
46504
+ "@zelari/core/skills/builtin/docs",
46505
+ "@zelari/core/skills/builtin/git-ops",
46506
+ "@zelari/core/skills/builtin/planning",
46507
+ "@zelari/core/skills/builtin/refactoring",
46508
+ "@zelari/core/skills/builtin/review",
46509
+ "@zelari/core/skills/builtin/testing",
46510
+ "@zelari/core/skills/builtin/schema-loop",
46511
+ "@zelari/core/skills/builtin/computer-use-cua"
46512
+ ];
46513
+ var builtinsLoaded = false;
46514
+ function ensureBuiltinSkillsLoadedSync() {
46515
+ if (builtinsLoaded) return;
46516
+ for (const spec of BUILTIN_SKILL_MODULES) {
46517
+ try {
46518
+ require?.(spec);
46519
+ } catch {
46520
+ }
46521
+ }
46522
+ }
46523
+ function getUserSkillsDir() {
46524
+ return join34(homedir10(), ".zelari-code", "skills");
46525
+ }
46526
+ function getProjectSkillsDir(projectRoot) {
46527
+ return join34(projectRoot, ".zelari", "skills");
46528
+ }
46529
+ function skillFilePath(dir, name) {
46530
+ return join34(dir, name, "SKILL.md");
46531
+ }
46532
+ function classifyScope(skillPath, projectRoot) {
46533
+ const userDir = getUserSkillsDir().replace(/\\/g, "/");
46534
+ const norm = skillPath.replace(/\\/g, "/");
46535
+ if (norm.startsWith(userDir + "/") || norm === userDir) {
46536
+ return { scope: "user", writable: true };
46537
+ }
46538
+ if (projectRoot) {
46539
+ const projDir = getProjectSkillsDir(projectRoot).replace(/\\/g, "/");
46540
+ if (norm.startsWith(projDir + "/") || norm === projDir) {
46541
+ return { scope: "project", writable: true };
46542
+ }
46543
+ }
46544
+ return { scope: "compat", writable: false };
46545
+ }
46546
+ function entryFromParsed(parsed, projectRoot) {
46547
+ const { scope, writable } = classifyScope(parsed.sourcePath, projectRoot);
46548
+ return {
46549
+ id: parsed.name,
46550
+ name: parsed.name,
46551
+ description: parsed.description,
46552
+ category: parsed.category,
46553
+ estimatedCost: parsed.estimatedCost,
46554
+ requiredTools: parsed.requiredTools,
46555
+ scope,
46556
+ path: parsed.sourcePath,
46557
+ body: parsed.body,
46558
+ builtin: false,
46559
+ writable
46560
+ };
46561
+ }
46562
+ function entryFromBuiltin(skill) {
46563
+ return {
46564
+ id: skill.id,
46565
+ name: skill.name || skill.id,
46566
+ description: skill.description || "",
46567
+ category: skill.category,
46568
+ estimatedCost: skill.estimatedCost,
46569
+ requiredTools: skill.requiredTools,
46570
+ scope: "builtin",
46571
+ path: null,
46572
+ // Expose body so Desktop skill picker can expand like `/skill <id>`.
46573
+ body: skill.systemPromptFragment || void 0,
46574
+ builtin: true,
46575
+ writable: false
46576
+ };
46577
+ }
46578
+ function scanSkillsDir(dir, projectRoot, seen, out) {
46579
+ if (!existsSync39(dir)) return;
46580
+ let entries;
46581
+ try {
46582
+ entries = readdirSync6(dir, { withFileTypes: true }).filter((e) => e.isDirectory()).map((e) => e.name);
46583
+ } catch {
46584
+ return;
46585
+ }
46586
+ for (const entry of entries) {
46587
+ const skillPath = skillFilePath(dir, entry);
46588
+ if (!existsSync39(skillPath)) continue;
46589
+ try {
46590
+ const parsed = parseSkillMd(readFileSync32(skillPath, "utf8"), skillPath);
46591
+ if (!parsed) continue;
46592
+ if (seen.has(parsed.name)) continue;
46593
+ seen.add(parsed.name);
46594
+ out.push(entryFromParsed(parsed, projectRoot));
46595
+ } catch {
46596
+ }
46597
+ }
46598
+ }
46599
+ function listSkillsSnapshot(projectRoot) {
46600
+ const root = projectRoot && projectRoot.trim() ? projectRoot.trim() : null;
46601
+ const userSkillsDir = getUserSkillsDir();
46602
+ const projectSkillsDir = root ? getProjectSkillsDir(root) : null;
46603
+ const skills = [];
46604
+ const seen = /* @__PURE__ */ new Set();
46605
+ if (root) {
46606
+ scanSkillsDir(join34(root, ".zelari", "skills"), root, seen, skills);
46607
+ scanSkillsDir(join34(root, ".claude", "skills"), root, seen, skills);
46608
+ scanSkillsDir(join34(root, ".opencode", "skills"), root, seen, skills);
46609
+ }
46610
+ scanSkillsDir(userSkillsDir, root, seen, skills);
46611
+ for (const s of listCodingSkills()) {
46612
+ if (seen.has(s.id)) continue;
46613
+ seen.add(s.id);
46614
+ if (s.builtin) {
46615
+ skills.push(entryFromBuiltin(s));
46616
+ }
46617
+ }
46618
+ skills.sort((a, b) => {
46619
+ const order = (s) => s === "project" ? 0 : s === "user" ? 1 : s === "compat" ? 2 : 3;
46620
+ const d = order(a.scope) - order(b.scope);
46621
+ if (d !== 0) return d;
46622
+ return a.id.localeCompare(b.id);
46623
+ });
46624
+ return { userSkillsDir, projectSkillsDir, skills };
46625
+ }
46626
+ function serializeSkillMd(opts) {
46627
+ const name = opts.name.trim().toLowerCase();
46628
+ const description = opts.description.trim();
46629
+ const body = opts.body.trim();
46630
+ const lines = ["---", `name: ${name}`, `description: ${description}`];
46631
+ const cat = (opts.category ?? "").trim().toLowerCase();
46632
+ if (cat && CODING_CATEGORIES2.has(cat)) {
46633
+ lines.push(`category: ${cat}`);
46634
+ }
46635
+ if (opts.tools && opts.tools.length > 0) {
46636
+ lines.push(`tools: [${opts.tools.map((t) => t.trim()).filter(Boolean).join(", ")}]`);
46637
+ }
46638
+ const cost = (opts.cost ?? "").trim().toLowerCase();
46639
+ if (cost === "low" || cost === "medium" || cost === "high") {
46640
+ lines.push(`cost: ${cost}`);
46641
+ }
46642
+ lines.push("---", "", body, "");
46643
+ return lines.join("\n");
46644
+ }
46645
+ function upsertSkill(opts) {
46646
+ const name = opts.name.trim().toLowerCase();
46647
+ if (!name || !NAME_RE.test(name)) {
46648
+ return {
46649
+ ok: false,
46650
+ error: "Invalid skill name (use lowercase letters, digits, hyphens; max 64 chars)"
46651
+ };
46652
+ }
46653
+ const description = opts.description.trim();
46654
+ if (!description) {
46655
+ return { ok: false, error: "description is required" };
46656
+ }
46657
+ const body = opts.body.trim();
46658
+ if (!body) {
46659
+ return { ok: false, error: "body is required" };
46660
+ }
46661
+ let dir;
46662
+ if (opts.scope === "user") {
46663
+ dir = getUserSkillsDir();
46664
+ } else {
46665
+ const root = opts.projectRoot?.trim();
46666
+ if (!root) {
46667
+ return {
46668
+ ok: false,
46669
+ error: "projectRoot required for project scope (Open Folder first)"
46670
+ };
46671
+ }
46672
+ dir = getProjectSkillsDir(root);
46673
+ }
46674
+ const path40 = skillFilePath(dir, name);
46675
+ const content = serializeSkillMd({
46676
+ name,
46677
+ description,
46678
+ body,
46679
+ category: opts.category,
46680
+ tools: opts.tools,
46681
+ cost: opts.cost
46682
+ });
46683
+ const parsed = parseSkillMd(content, path40);
46684
+ if (!parsed) {
46685
+ return { ok: false, error: "Generated SKILL.md failed validation" };
46686
+ }
46687
+ mkdirSync16(dirname9(path40), { recursive: true });
46688
+ writeFileSync20(path40, content, "utf8");
46689
+ return { ok: true, path: path40 };
46690
+ }
46691
+ function removeSkill(opts) {
46692
+ const name = opts.name.trim().toLowerCase();
46693
+ if (!name) {
46694
+ return { ok: false, error: "name is required" };
46695
+ }
46696
+ let dir;
46697
+ if (opts.scope === "user") {
46698
+ dir = getUserSkillsDir();
46699
+ } else {
46700
+ const root = opts.projectRoot?.trim();
46701
+ if (!root) {
46702
+ return { ok: false, error: "projectRoot required for project scope" };
46703
+ }
46704
+ dir = getProjectSkillsDir(root);
46705
+ }
46706
+ const skillDir = join34(dir, name);
46707
+ const path40 = skillFilePath(dir, name);
46708
+ if (!existsSync39(path40) && !existsSync39(skillDir)) {
46709
+ return { ok: false, error: `Skill "${name}" not found in ${dir}` };
46710
+ }
46711
+ try {
46712
+ rmSync3(skillDir, { recursive: true, force: true });
46713
+ } catch (err) {
46714
+ return {
46715
+ ok: false,
46716
+ error: err instanceof Error ? err.message : String(err)
46717
+ };
46718
+ }
46719
+ return { ok: true, path: path40 };
46720
+ }
46721
+
46722
+ // src/cli/generateSkillFromUrl.ts
46723
+ init_providerConfig();
46724
+ init_keyStore();
46725
+ init_openai_compatible();
46726
+ var MAX_PAGE_CHARS = 24e3;
46727
+ var FETCH_TIMEOUT_MS2 = 25e3;
46728
+ var LLM_TIMEOUT_MS = 9e4;
46729
+ var SYSTEM = `You convert web page content into a Zelari Code coding skill (SKILL.md style).
46730
+
46731
+ Return ONLY a single JSON object (no markdown fences) with keys:
46732
+ - name: string, kebab-case id (a-z0-9-hyphens, max 64, start with letter/digit)
46733
+ - description: string, one line, what the skill does
46734
+ - body: string, markdown instructions the agent should follow (steps, rules, checks)
46735
+ - category: one of ${CODING_CATEGORIES_LIST.join("|")} (optional)
46736
+ - tools: string[] of tool names the skill needs (optional, e.g. bash, read_file, write_file)
46737
+ - cost: "low" | "medium" | "high" (optional, default medium)
46738
+
46739
+ Rules:
46740
+ - Prefer actionable coding-agent instructions over marketing copy.
46741
+ - If the page is a skill/prompt already, adapt it cleanly to this schema.
46742
+ - If the page is docs/blog, distill a reusable workflow skill.
46743
+ - name must be unique-looking and descriptive (not "untitled" or "skill").
46744
+ - body must be non-empty markdown with clear steps.`;
46745
+ function stripHtml(html) {
46746
+ let t = html.replace(/<script[\s\S]*?<\/script>/gi, " ").replace(/<style[\s\S]*?<\/style>/gi, " ").replace(/<noscript[\s\S]*?<\/noscript>/gi, " ");
46747
+ t = t.replace(/<!--[\s\S]*?-->/g, " ");
46748
+ t = t.replace(/<[^>]+>/g, " ");
46749
+ t = t.replace(/&nbsp;/g, " ").replace(/&amp;/g, "&").replace(/&lt;/g, "<").replace(/&gt;/g, ">").replace(/&quot;/g, '"').replace(/&#39;/g, "'");
46750
+ t = t.replace(/[ \t]+\n/g, "\n").replace(/\n{3,}/g, "\n\n");
46751
+ t = t.replace(/[ \t]{2,}/g, " ").trim();
46752
+ return t;
46753
+ }
46754
+ async function fetchUrlText(url2) {
46755
+ const u = url2.trim();
46756
+ if (!/^https?:\/\//i.test(u)) {
46757
+ throw new Error("URL must start with http:// or https://");
46758
+ }
46759
+ const controller = new AbortController();
46760
+ const t = setTimeout(() => controller.abort(), FETCH_TIMEOUT_MS2);
46761
+ try {
46762
+ const res = await fetch(u, {
46763
+ signal: controller.signal,
46764
+ headers: {
46765
+ "user-agent": "zelari-code-skill-import/1.0",
46766
+ accept: "text/html,text/plain,application/json;q=0.9,*/*;q=0.8"
46767
+ },
46768
+ redirect: "follow"
46769
+ });
46770
+ if (!res.ok) {
46771
+ throw new Error(`Fetch failed HTTP ${res.status}`);
46772
+ }
46773
+ const ct = (res.headers.get("content-type") || "").toLowerCase();
46774
+ const raw = await res.text();
46775
+ if (ct.includes("json") || raw.trimStart().startsWith("{") || raw.trimStart().startsWith("[")) {
46776
+ return raw.slice(0, MAX_PAGE_CHARS);
46777
+ }
46778
+ if (ct.includes("html") || /<html[\s>]/i.test(raw) || /<body[\s>]/i.test(raw)) {
46779
+ return stripHtml(raw).slice(0, MAX_PAGE_CHARS);
46780
+ }
46781
+ return raw.slice(0, MAX_PAGE_CHARS);
46782
+ } finally {
46783
+ clearTimeout(t);
46784
+ }
46785
+ }
46786
+ function extractJsonObject(text) {
46787
+ const trimmed = text.trim();
46788
+ const fenced = /^```(?:json)?\s*([\s\S]*?)```$/i.exec(trimmed);
46789
+ const body = fenced ? fenced[1].trim() : trimmed;
46790
+ try {
46791
+ return JSON.parse(body);
46792
+ } catch {
46793
+ const start = body.indexOf("{");
46794
+ const end = body.lastIndexOf("}");
46795
+ if (start >= 0 && end > start) {
46796
+ return JSON.parse(body.slice(start, end + 1));
46797
+ }
46798
+ throw new Error("Model did not return valid JSON");
46799
+ }
46800
+ }
46801
+ function normalizeDraft(raw, sourceUrl, provider, model) {
46802
+ if (!raw || typeof raw !== "object") {
46803
+ throw new Error("Invalid skill draft object");
46804
+ }
46805
+ const o = raw;
46806
+ let name = String(o.name ?? "").trim().toLowerCase().replace(/[^a-z0-9-]+/g, "-").replace(/^-+|-+$/g, "").slice(0, 64);
46807
+ if (!name || !/^[a-z0-9][a-z0-9-]{0,63}$/.test(name)) {
46808
+ try {
46809
+ const path40 = new URL(sourceUrl).pathname.split("/").filter(Boolean).pop()?.replace(/\.[a-z0-9]+$/i, "").toLowerCase().replace(/[^a-z0-9-]+/g, "-").replace(/^-+|-+$/g, "").slice(0, 48);
46810
+ name = path40 && /^[a-z0-9]/.test(path40) ? path40 : "imported-skill";
46811
+ } catch {
46812
+ name = "imported-skill";
46813
+ }
46814
+ }
46815
+ const description = String(o.description ?? "").trim();
46816
+ const body = String(o.body ?? "").trim();
46817
+ if (!description) throw new Error("Model draft missing description");
46818
+ if (!body) throw new Error("Model draft missing body");
46819
+ const catRaw = String(o.category ?? "").trim().toLowerCase();
46820
+ const category = CODING_CATEGORIES_LIST.includes(catRaw) ? catRaw : void 0;
46821
+ let tools;
46822
+ if (Array.isArray(o.tools)) {
46823
+ tools = o.tools.map((t) => String(t).trim()).filter(Boolean);
46824
+ } else if (typeof o.tools === "string" && o.tools.trim()) {
46825
+ tools = o.tools.split(",").map((t) => t.trim()).filter(Boolean);
46826
+ }
46827
+ const costRaw = String(o.cost ?? "medium").trim().toLowerCase();
46828
+ const cost = costRaw === "low" || costRaw === "high" || costRaw === "medium" ? costRaw : "medium";
46829
+ return {
46830
+ name,
46831
+ description,
46832
+ body,
46833
+ category,
46834
+ tools,
46835
+ cost,
46836
+ sourceUrl,
46837
+ model,
46838
+ provider
46839
+ };
46840
+ }
46841
+ async function resolveLlm(opts) {
46842
+ const active = opts.provider?.trim() || getProviderConfig().activeProviderId;
46843
+ const meta3 = await resolveApiKeyWithMeta(active);
46844
+ if (!meta3?.apiKey) {
46845
+ throw new Error(
46846
+ `No API key for provider '${active}'. Save a key in Settings \u2192 Provider.`
46847
+ );
46848
+ }
46849
+ const baseUrl = resolveBaseUrl(active);
46850
+ if (!baseUrl) {
46851
+ throw new Error(
46852
+ `No base URL for provider '${active}'. Set a custom endpoint in Settings.`
46853
+ );
46854
+ }
46855
+ const model = opts.model?.trim() || getModelForProvider(active) || process.env.ZELARI_MODEL || "";
46856
+ if (!model) {
46857
+ throw new Error(`No model selected for provider '${active}'`);
46858
+ }
46859
+ return { provider: active, model, apiKey: meta3.apiKey, baseUrl };
46860
+ }
46861
+ async function generateSkillFromUrl(opts) {
46862
+ const page = await fetchUrlText(opts.url);
46863
+ if (!page.trim()) {
46864
+ throw new Error("Page content is empty after fetch");
46865
+ }
46866
+ const llm = await resolveLlm({
46867
+ provider: opts.provider,
46868
+ model: opts.model
46869
+ });
46870
+ const controller = new AbortController();
46871
+ const t = setTimeout(() => controller.abort(), LLM_TIMEOUT_MS);
46872
+ try {
46873
+ const url2 = `${llm.baseUrl.replace(/\/$/, "")}/chat/completions`;
46874
+ const res = await fetch(url2, {
46875
+ method: "POST",
46876
+ signal: controller.signal,
46877
+ headers: {
46878
+ "content-type": "application/json",
46879
+ authorization: `Bearer ${llm.apiKey}`
46880
+ },
46881
+ body: JSON.stringify({
46882
+ model: llm.model,
46883
+ temperature: 0.2,
46884
+ max_tokens: 4096,
46885
+ stream: false,
46886
+ messages: [
46887
+ { role: "system", content: SYSTEM },
46888
+ {
46889
+ role: "user",
46890
+ content: `Source URL: ${opts.url.trim()}
46891
+
46892
+ Page content (truncated):
46893
+ ${page}`
46894
+ }
46895
+ ]
46896
+ })
46897
+ });
46898
+ if (!res.ok) {
46899
+ const errBody = await res.text().catch(() => "");
46900
+ throw new Error(
46901
+ `LLM HTTP ${res.status}${errBody ? `: ${errBody.slice(0, 200)}` : ""}`
46902
+ );
46903
+ }
46904
+ const json2 = await res.json();
46905
+ const text = json2.choices?.[0]?.message?.content?.trim();
46906
+ if (!text) throw new Error("Empty model response");
46907
+ const parsed = extractJsonObject(text);
46908
+ return normalizeDraft(parsed, opts.url.trim(), llm.provider, llm.model);
46909
+ } finally {
46910
+ clearTimeout(t);
46911
+ }
46912
+ }
46913
+
46914
+ // src/cli/main.ts
45181
46915
  init_mcpPresets();
45182
46916
  init_targets();
45183
46917
  var VERSION = getCurrentVersion();
@@ -45217,7 +46951,7 @@ function runPreflight() {
45217
46951
  }
45218
46952
  async function backgroundUpdateCheck() {
45219
46953
  if (process.env.ANATHEMA_DEV === "1") return;
45220
- await new Promise((resolve) => setTimeout(resolve, 3e3));
46954
+ await new Promise((resolve3) => setTimeout(resolve3, 3e3));
45221
46955
  try {
45222
46956
  const { checkForUpdate: checkForUpdate2 } = await Promise.resolve().then(() => (init_updater(), updater_exports));
45223
46957
  const info = await checkForUpdate2();
@@ -45255,6 +46989,35 @@ function pickRootComponent() {
45255
46989
  void runPluginsInstall(argv).then((code) => process.exit(code));
45256
46990
  return { kind: "done" };
45257
46991
  }
46992
+ if (argv.includes("serve") || argv.includes("--serve")) {
46993
+ const { parseServeFlags: parseServeFlags2 } = (init_serve(), __toCommonJS(serve_exports));
46994
+ return { kind: "serve", serveOpts: parseServeFlags2(argv) ?? {} };
46995
+ }
46996
+ if (argv.includes("--generate-skill-from-url")) {
46997
+ void (async () => {
46998
+ try {
46999
+ const get = (flag) => {
47000
+ const i = argv.indexOf(flag);
47001
+ return i >= 0 && argv[i + 1] ? argv[i + 1] : void 0;
47002
+ };
47003
+ const url2 = get("--url");
47004
+ if (!url2) throw new Error("--url is required");
47005
+ const draft = await generateSkillFromUrl({
47006
+ url: url2,
47007
+ provider: get("--provider"),
47008
+ model: get("--model")
47009
+ });
47010
+ console.log(JSON.stringify({ ok: true, ...draft }, null, 2));
47011
+ process.exit(0);
47012
+ } catch (err) {
47013
+ console.error(
47014
+ `[zelari-code --generate-skill-from-url] ${err instanceof Error ? err.message : String(err)}`
47015
+ );
47016
+ process.exit(1);
47017
+ }
47018
+ })();
47019
+ return { kind: "done" };
47020
+ }
45258
47021
  if (argv.includes("--doctor") || argv.includes("doctor")) {
45259
47022
  const { runDoctor: runDoctor2 } = (init_doctor(), __toCommonJS(doctor_exports));
45260
47023
  void runDoctor2().then((healthy) => process.exit(healthy ? 0 : 1));
@@ -45306,7 +47069,7 @@ function pickRootComponent() {
45306
47069
  }
45307
47070
  if (argv.includes("--help") || argv.includes("-h")) {
45308
47071
  console.log(
45309
- "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 agent|council|zelari Dispatch mode (default: agent)\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 --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-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"
47072
+ "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 agent|council|zelari Dispatch mode (default: agent)\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"
45310
47073
  );
45311
47074
  process.exit(0);
45312
47075
  }
@@ -45385,6 +47148,82 @@ function pickRootComponent() {
45385
47148
  process.exit(1);
45386
47149
  }
45387
47150
  }
47151
+ if (argv.includes("--print-skills")) {
47152
+ try {
47153
+ const cwdIdx = argv.indexOf("--cwd");
47154
+ const cwd = cwdIdx >= 0 && argv[cwdIdx + 1] ? argv[cwdIdx + 1] : process.cwd();
47155
+ ensureBuiltinSkillsLoadedSync();
47156
+ const snap = listSkillsSnapshot(cwd);
47157
+ console.log(JSON.stringify(snap, null, 2));
47158
+ process.exit(0);
47159
+ } catch (err) {
47160
+ console.error(
47161
+ `[zelari-code --print-skills] ${err instanceof Error ? err.message : String(err)}`
47162
+ );
47163
+ process.exit(1);
47164
+ }
47165
+ }
47166
+ if (argv.includes("--set-skill")) {
47167
+ try {
47168
+ const get = (flag) => {
47169
+ const i = argv.indexOf(flag);
47170
+ return i >= 0 && argv[i + 1] ? argv[i + 1] : void 0;
47171
+ };
47172
+ const name = get("--name");
47173
+ const description = get("--description");
47174
+ const body = get("--body");
47175
+ const category = get("--category");
47176
+ const toolsRaw = get("--tools");
47177
+ const cost = get("--cost");
47178
+ const scopeRaw = get("--scope") ?? "user";
47179
+ const scope = scopeRaw === "project" ? "project" : "user";
47180
+ const cwd = get("--cwd") ?? process.cwd();
47181
+ if (!name) throw new Error("--name is required");
47182
+ if (!description) throw new Error("--description is required");
47183
+ if (!body) throw new Error("--body is required");
47184
+ const tools = toolsRaw ? toolsRaw.split(",").map((t) => t.trim()).filter(Boolean) : void 0;
47185
+ const result = upsertSkill({
47186
+ scope,
47187
+ name,
47188
+ description,
47189
+ body,
47190
+ category,
47191
+ tools,
47192
+ cost,
47193
+ projectRoot: cwd
47194
+ });
47195
+ if (!result.ok) throw new Error(result.error);
47196
+ console.log(JSON.stringify({ ok: true, path: result.path, name, scope }));
47197
+ process.exit(0);
47198
+ } catch (err) {
47199
+ console.error(
47200
+ `[zelari-code --set-skill] ${err instanceof Error ? err.message : String(err)}`
47201
+ );
47202
+ process.exit(1);
47203
+ }
47204
+ }
47205
+ if (argv.includes("--remove-skill")) {
47206
+ try {
47207
+ const get = (flag) => {
47208
+ const i = argv.indexOf(flag);
47209
+ return i >= 0 && argv[i + 1] ? argv[i + 1] : void 0;
47210
+ };
47211
+ const name = get("--name");
47212
+ const scopeRaw = get("--scope") ?? "user";
47213
+ const scope = scopeRaw === "project" ? "project" : "user";
47214
+ const cwd = get("--cwd") ?? process.cwd();
47215
+ if (!name) throw new Error("--name is required");
47216
+ const result = removeSkill({ scope, name, projectRoot: cwd });
47217
+ if (!result.ok) throw new Error(result.error);
47218
+ console.log(JSON.stringify({ ok: true, path: result.path, name, scope }));
47219
+ process.exit(0);
47220
+ } catch (err) {
47221
+ console.error(
47222
+ `[zelari-code --remove-skill] ${err instanceof Error ? err.message : String(err)}`
47223
+ );
47224
+ process.exit(1);
47225
+ }
47226
+ }
45388
47227
  if (argv.includes("--set-mcp-preset")) {
45389
47228
  try {
45390
47229
  const get = (flag) => {
@@ -45657,6 +47496,15 @@ function loadUserSkills() {
45657
47496
  function main() {
45658
47497
  const picked = pickRootComponent();
45659
47498
  if (picked.kind === "done") return;
47499
+ if (picked.kind === "serve") {
47500
+ void Promise.resolve().then(() => (init_serve(), serve_exports)).then(({ runCompanionServe: runCompanionServe2 }) => runCompanionServe2(picked.serveOpts ?? {})).then(() => process.exit(0)).catch((err) => {
47501
+ console.error(
47502
+ `[zelari-code serve] ${err instanceof Error ? err.message : String(err)}`
47503
+ );
47504
+ process.exit(1);
47505
+ });
47506
+ return;
47507
+ }
45660
47508
  runPreflight();
45661
47509
  loadUserSkills();
45662
47510
  if (picked.kind === "headless") {
@@ -45670,7 +47518,7 @@ function main() {
45670
47518
  closeMcpClients2();
45671
47519
  } catch {
45672
47520
  }
45673
- await new Promise((resolve) => setImmediate(resolve));
47521
+ await new Promise((resolve3) => setImmediate(resolve3));
45674
47522
  process.exit(code);
45675
47523
  }).catch(async (err) => {
45676
47524
  console.error(