zam-core 0.5.1 → 0.5.2

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/dist/cli/index.js CHANGED
@@ -3630,6 +3630,11 @@ function generatePrompt(input8) {
3630
3630
  import { existsSync as existsSync6, readFileSync as readFileSync6 } from "fs";
3631
3631
  import { dirname as dirname3, join as join7, resolve } from "path";
3632
3632
  var DEFAULT_REVIEW_CONTEXT_MAX_CHARS = 6e3;
3633
+ var REVIEW_CONTEXT_CACHE_TTL_MS = 5 * 60 * 1e3;
3634
+ var reviewContextCache = /* @__PURE__ */ new Map();
3635
+ function reviewContextCacheKey(sourceLink, maxChars) {
3636
+ return `${sourceLink}\0${maxChars}`;
3637
+ }
3633
3638
  function htmlToText(html) {
3634
3639
  let content = html;
3635
3640
  const bodyMatch = /<body[^>]*>([\s\S]*?)<\/body>/i.exec(html);
@@ -3772,6 +3777,11 @@ async function resolveReviewContext(sourceLink, opts = {}) {
3772
3777
  const cleaned = sourceLink?.trim();
3773
3778
  if (!cleaned) return null;
3774
3779
  const maxChars = opts.maxChars ?? DEFAULT_REVIEW_CONTEXT_MAX_CHARS;
3780
+ const cacheKey = reviewContextCacheKey(cleaned, maxChars);
3781
+ const cached = reviewContextCache.get(cacheKey);
3782
+ if (cached && cached.expiresAt > Date.now()) {
3783
+ return cached.context;
3784
+ }
3775
3785
  const resolved = await resolveReference(cleaned);
3776
3786
  let content = resolved.content;
3777
3787
  let truncated = false;
@@ -3779,7 +3789,7 @@ async function resolveReviewContext(sourceLink, opts = {}) {
3779
3789
  content = content.slice(0, maxChars);
3780
3790
  truncated = true;
3781
3791
  }
3782
- return {
3792
+ const context = {
3783
3793
  sourceLink: cleaned,
3784
3794
  sourceType: resolved.sourceType,
3785
3795
  content,
@@ -3787,6 +3797,11 @@ async function resolveReviewContext(sourceLink, opts = {}) {
3787
3797
  url: resolved.url,
3788
3798
  truncated
3789
3799
  };
3800
+ reviewContextCache.set(cacheKey, {
3801
+ context,
3802
+ expiresAt: Date.now() + REVIEW_CONTEXT_CACHE_TTL_MS
3803
+ });
3804
+ return context;
3790
3805
  }
3791
3806
  function normalizePath(p) {
3792
3807
  const base = p.split("#")[0].trim();
@@ -5368,12 +5383,28 @@ import {
5368
5383
  } from "fs";
5369
5384
  import { homedir as homedir10, tmpdir as tmpdir3 } from "os";
5370
5385
  import { basename as basename5, join as join17, resolve as resolve5 } from "path";
5371
- import { Command as Command4 } from "commander";
5386
+ import { Command as Command5 } from "commander";
5372
5387
 
5373
5388
  // src/cli/llm/client.ts
5374
5389
  import { spawn as spawn2 } from "child_process";
5375
5390
  import { existsSync as existsSync14 } from "fs";
5376
5391
  var DEFAULT_LLM_URL = "http://localhost:8000/v1";
5392
+ var DEFAULT_LLM_MAX_TOKENS = 1e4;
5393
+ var RECALL_QUESTION_MAX_OUTPUT_TOKENS = 400;
5394
+ var RECALL_EVALUATION_MAX_OUTPUT_TOKENS = 1200;
5395
+ var RECALL_ENDPOINT_CACHE_MS = 6e4;
5396
+ var cachedRecallEndpoint = null;
5397
+ function recallEndpointSignature(cfg) {
5398
+ return [
5399
+ cfg.enabled ? "on" : "off",
5400
+ cfg.providerName ?? "",
5401
+ cfg.url,
5402
+ cfg.model,
5403
+ cfg.apiFlavor,
5404
+ cfg.fallback?.url ?? "",
5405
+ cfg.fallback?.model ?? ""
5406
+ ].join("|");
5407
+ }
5377
5408
  var DEFAULT_LLM_MODEL = "qwen3.5:4b";
5378
5409
  var DEFAULT_LLM_API_KEY = "sk-none";
5379
5410
  async function getLlmConfig(db) {
@@ -5484,6 +5515,7 @@ function materializeProvider(rec, base, role, meta) {
5484
5515
  label: rec.label,
5485
5516
  source: meta.source,
5486
5517
  local: rec.local ?? isLocalEndpoint(url),
5518
+ ...rec.runner ? { runner: rec.runner } : {},
5487
5519
  ...role === "vision" ? { maxFrames: base.maxFrames } : {}
5488
5520
  };
5489
5521
  }
@@ -5566,10 +5598,7 @@ async function readChatContent(res, label) {
5566
5598
  }
5567
5599
  async function generateQuestionViaLLM(db, input8) {
5568
5600
  const cfg = await getProviderForRole(db, "recall");
5569
- if (!cfg.enabled) {
5570
- throw new Error("LLM integration is disabled in settings (llm.enabled)");
5571
- }
5572
- assertChatCompletions(cfg);
5601
+ const endpoint = await resolveUsableRecallEndpoint(db);
5573
5602
  const bloom = input8.bloomLevel >= 1 && input8.bloomLevel <= 5 ? input8.bloomLevel : 1;
5574
5603
  const verb = BLOOM_VERBS2[bloom];
5575
5604
  const langName = LANGUAGE_NAMES[cfg.locale] || "English";
@@ -5589,31 +5618,36 @@ ${input8.sourceLinkContent ? `Source Reference:
5589
5618
  ${input8.sourceLinkContent}` : ""}
5590
5619
 
5591
5620
  Active-Recall Question:`;
5592
- const res = await fetchWithInteractiveTimeout(`${cfg.url}/chat/completions`, {
5593
- method: "POST",
5594
- headers: {
5595
- "Content-Type": "application/json",
5596
- Authorization: `Bearer ${cfg.apiKey}`
5597
- },
5598
- body: JSON.stringify({
5599
- model: cfg.model,
5600
- messages: [
5601
- { role: "system", content: systemPrompt },
5602
- { role: "user", content: userPrompt }
5603
- ],
5604
- temperature: 0.1,
5605
- max_tokens: 150
5606
- }),
5607
- locale: cfg.locale
5608
- });
5609
- return readChatContent(res, "LLM request");
5621
+ const res = await fetchWithInteractiveTimeout(
5622
+ `${endpoint.url}/chat/completions`,
5623
+ {
5624
+ method: "POST",
5625
+ headers: {
5626
+ "Content-Type": "application/json",
5627
+ Authorization: `Bearer ${endpoint.apiKey}`
5628
+ },
5629
+ body: JSON.stringify({
5630
+ model: endpoint.model,
5631
+ messages: [
5632
+ { role: "system", content: systemPrompt },
5633
+ { role: "user", content: userPrompt }
5634
+ ],
5635
+ temperature: 0.1,
5636
+ max_tokens: RECALL_QUESTION_MAX_OUTPUT_TOKENS
5637
+ }),
5638
+ locale: cfg.locale
5639
+ }
5640
+ );
5641
+ const text = await readChatContent(res, "LLM request");
5642
+ return {
5643
+ text,
5644
+ model: endpoint.model,
5645
+ providerName: endpoint.providerName
5646
+ };
5610
5647
  }
5611
5648
  async function evaluateAnswerViaLLM(db, input8) {
5612
5649
  const cfg = await getProviderForRole(db, "recall");
5613
- if (!cfg.enabled) {
5614
- throw new Error("LLM integration is disabled in settings (llm.enabled)");
5615
- }
5616
- assertChatCompletions(cfg);
5650
+ const endpoint = await resolveUsableRecallEndpoint(db);
5617
5651
  const langName = LANGUAGE_NAMES[cfg.locale] || "English";
5618
5652
  const ratingPrefix = LOCALIZED_RATING_PREFIX[cfg.locale] || "Suggested rating";
5619
5653
  const systemPrompt = `You are ZAM, an extremely warm, encouraging, and patient skills trainer.
@@ -5643,51 +5677,59 @@ ${input8.sourceLinkContent ? `Source Code Reference:
5643
5677
  ${input8.sourceLinkContent}` : ""}
5644
5678
 
5645
5679
  Evaluation:`;
5646
- const res = await fetchWithInteractiveTimeout(`${cfg.url}/chat/completions`, {
5647
- method: "POST",
5648
- headers: {
5649
- "Content-Type": "application/json",
5650
- Authorization: `Bearer ${cfg.apiKey}`
5651
- },
5652
- body: JSON.stringify({
5653
- model: cfg.model,
5654
- messages: [
5655
- { role: "system", content: systemPrompt },
5656
- { role: "user", content: userPrompt }
5657
- ],
5658
- temperature: 0.2,
5659
- max_tokens: 300
5660
- }),
5661
- locale: cfg.locale
5662
- });
5663
- return readChatContent(res, "LLM evaluation");
5680
+ const res = await fetchWithInteractiveTimeout(
5681
+ `${endpoint.url}/chat/completions`,
5682
+ {
5683
+ method: "POST",
5684
+ headers: {
5685
+ "Content-Type": "application/json",
5686
+ Authorization: `Bearer ${endpoint.apiKey}`
5687
+ },
5688
+ body: JSON.stringify({
5689
+ model: endpoint.model,
5690
+ messages: [
5691
+ { role: "system", content: systemPrompt },
5692
+ { role: "user", content: userPrompt }
5693
+ ],
5694
+ temperature: 0.2,
5695
+ max_tokens: RECALL_EVALUATION_MAX_OUTPUT_TOKENS
5696
+ }),
5697
+ locale: cfg.locale
5698
+ }
5699
+ );
5700
+ const text = await readChatContent(res, "LLM evaluation");
5701
+ return {
5702
+ text,
5703
+ model: endpoint.model,
5704
+ providerName: endpoint.providerName
5705
+ };
5664
5706
  }
5665
5707
  async function translateQuestionViaLLM(db, question) {
5666
5708
  const cfg = await getProviderForRole(db, "recall");
5667
- if (!cfg.enabled) {
5668
- throw new Error("LLM integration is disabled in settings");
5669
- }
5670
- assertChatCompletions(cfg);
5709
+ const endpoint = await resolveUsableRecallEndpoint(db);
5671
5710
  const targetLang = LANGUAGE_NAMES[cfg.locale] || "English";
5672
5711
  const systemPrompt = `You are a highly precise translator. Translate the given active-recall question into clear, natural ${targetLang}.
5673
5712
  Output ONLY the raw translation. Do not include any headers, preamble, quotes, or conversational filler.`;
5674
- const res = await fetchWithInteractiveTimeout(`${cfg.url}/chat/completions`, {
5675
- method: "POST",
5676
- headers: {
5677
- "Content-Type": "application/json",
5678
- Authorization: `Bearer ${cfg.apiKey}`
5679
- },
5680
- body: JSON.stringify({
5681
- model: cfg.model,
5682
- messages: [
5683
- { role: "system", content: systemPrompt },
5684
- { role: "user", content: question }
5685
- ],
5686
- temperature: 0.1,
5687
- max_tokens: 150
5688
- }),
5689
- locale: cfg.locale
5690
- });
5713
+ const res = await fetchWithInteractiveTimeout(
5714
+ `${endpoint.url}/chat/completions`,
5715
+ {
5716
+ method: "POST",
5717
+ headers: {
5718
+ "Content-Type": "application/json",
5719
+ Authorization: `Bearer ${endpoint.apiKey}`
5720
+ },
5721
+ body: JSON.stringify({
5722
+ model: endpoint.model,
5723
+ messages: [
5724
+ { role: "system", content: systemPrompt },
5725
+ { role: "user", content: question }
5726
+ ],
5727
+ temperature: 0.1,
5728
+ max_tokens: DEFAULT_LLM_MAX_TOKENS
5729
+ }),
5730
+ locale: cfg.locale
5731
+ }
5732
+ );
5691
5733
  return readChatContent(res, "Translation");
5692
5734
  }
5693
5735
  async function isLlmOnline(url) {
@@ -5760,6 +5802,107 @@ async function checkProviderChain(primary) {
5760
5802
  }
5761
5803
  return { primary: first };
5762
5804
  }
5805
+ async function resolveUsableRecallEndpoint(db) {
5806
+ const cfg = await getProviderForRole(db, "recall");
5807
+ if (!cfg.enabled) {
5808
+ throw new Error("LLM integration is disabled in settings (llm.enabled)");
5809
+ }
5810
+ assertChatCompletions(cfg);
5811
+ const signature = recallEndpointSignature(cfg);
5812
+ if (cachedRecallEndpoint && cachedRecallEndpoint.signature === signature && cachedRecallEndpoint.expiresAt > Date.now()) {
5813
+ return cachedRecallEndpoint.endpoint;
5814
+ }
5815
+ const chain = await checkProviderChain(cfg);
5816
+ const selected = chain.firstUsable;
5817
+ if (!selected || !isEndpointUsable(selected)) {
5818
+ throw new Error("No recall LLM endpoint is online");
5819
+ }
5820
+ cachedRecallEndpoint = {
5821
+ endpoint: selected.endpoint,
5822
+ signature,
5823
+ expiresAt: Date.now() + RECALL_ENDPOINT_CACHE_MS
5824
+ };
5825
+ return selected.endpoint;
5826
+ }
5827
+ async function prepareRecallChain(db, opts) {
5828
+ const cfg = await getProviderForRole(db, "recall");
5829
+ const fail = (reason, partial = {}) => ({
5830
+ usable: false,
5831
+ reason,
5832
+ online: false,
5833
+ model: cfg.model,
5834
+ availableModels: [],
5835
+ providerName: cfg.providerName,
5836
+ label: cfg.label,
5837
+ local: cfg.local ?? isLocalEndpoint(cfg.url),
5838
+ activeTier: "primary",
5839
+ ...partial
5840
+ });
5841
+ if (!cfg.enabled) return fail("disabled");
5842
+ if (cfg.apiFlavor !== "chat-completions") return fail("unsupported-provider");
5843
+ const chain = providerChain(cfg);
5844
+ const deadline = Date.now() + opts.timeoutMs;
5845
+ let lastReason = "offline";
5846
+ let lastOnline = false;
5847
+ let lastModel = cfg.model;
5848
+ let lastAvailable = [];
5849
+ for (let index = 0; index < chain.length; index++) {
5850
+ const endpoint = chain[index];
5851
+ let online = await isLlmOnline(endpoint.url);
5852
+ if (!online && (endpoint.local ?? isLocalEndpoint(endpoint.url))) {
5853
+ if (opts.interactive) {
5854
+ online = await startLocalRunner(
5855
+ endpoint.url,
5856
+ endpoint.model,
5857
+ cfg.locale,
5858
+ endpoint.runner
5859
+ );
5860
+ } else {
5861
+ spawnLocalRunner(endpoint.url, endpoint.model, endpoint.runner);
5862
+ while (Date.now() < deadline) {
5863
+ await new Promise((resolve9) => setTimeout(resolve9, 1e3));
5864
+ if (await isLlmOnline(endpoint.url)) {
5865
+ online = true;
5866
+ break;
5867
+ }
5868
+ }
5869
+ }
5870
+ }
5871
+ lastOnline = online;
5872
+ lastModel = endpoint.model;
5873
+ if (!online) {
5874
+ lastReason = "offline";
5875
+ continue;
5876
+ }
5877
+ const availableModels = await getAvailableModels(
5878
+ endpoint.url,
5879
+ endpoint.apiKey
5880
+ );
5881
+ lastAvailable = availableModels;
5882
+ const modelKnown = availableModels.length === 0 || availableModels.some(
5883
+ (candidate) => candidate.toLowerCase() === endpoint.model.toLowerCase()
5884
+ );
5885
+ if (!modelKnown) {
5886
+ lastReason = "model-not-found";
5887
+ continue;
5888
+ }
5889
+ return {
5890
+ usable: true,
5891
+ online: true,
5892
+ model: endpoint.model,
5893
+ availableModels,
5894
+ providerName: endpoint.providerName,
5895
+ label: endpoint.label,
5896
+ local: endpoint.local ?? isLocalEndpoint(endpoint.url),
5897
+ activeTier: index === 0 ? "primary" : "fallback"
5898
+ };
5899
+ }
5900
+ return fail(lastReason, {
5901
+ online: lastOnline,
5902
+ model: lastModel,
5903
+ availableModels: lastAvailable
5904
+ });
5905
+ }
5763
5906
  async function isVisionProviderModelExplicit(db, active) {
5764
5907
  if (await getSetting(db, "llm.vision.model")) return true;
5765
5908
  const providers = await readJsonSetting(db, "llm.providers");
@@ -5861,13 +6004,8 @@ async function getProviderRoleStatus(db, role) {
5861
6004
  fallback: summarizeFallback(cfg.fallback)
5862
6005
  };
5863
6006
  }
5864
- let selected;
5865
- if (role === "vision") {
5866
- const chain = await checkProviderChain(cfg);
5867
- selected = chain.firstUsable ?? chain.primary;
5868
- } else {
5869
- selected = await checkProviderEndpoint(cfg);
5870
- }
6007
+ const chain = await checkProviderChain(cfg);
6008
+ const selected = chain.firstUsable ?? chain.primary;
5871
6009
  const active = selected.endpoint;
5872
6010
  const usable = selected.online && selected.modelAvailable;
5873
6011
  const reason = usable ? void 0 : selected.online ? "model-not-found" : "offline";
@@ -5889,7 +6027,35 @@ async function getProviderRoleStatus(db, role) {
5889
6027
  fallback: summarizeFallback(cfg.fallback)
5890
6028
  };
5891
6029
  }
5892
- function detectRunner(url, model) {
6030
+ function runnerKindFromHint(hint) {
6031
+ if (!hint) return void 0;
6032
+ const normalized = hint.trim().toLowerCase();
6033
+ if (normalized === "flm" || normalized === "fastflowlm") {
6034
+ return "fastflowlm";
6035
+ }
6036
+ if (normalized === "ollama") return "ollama";
6037
+ if (normalized === "foundry-local" || normalized === "foundry" || normalized === "generic") {
6038
+ return "generic";
6039
+ }
6040
+ return void 0;
6041
+ }
6042
+ function defaultPortForRunner(runner, url) {
6043
+ try {
6044
+ const urlObj = new URL(url);
6045
+ const explicit = urlObj.port;
6046
+ if (explicit) return explicit;
6047
+ if (runner === "ollama") return "11434";
6048
+ if (urlObj.protocol === "https:") return "443";
6049
+ return "80";
6050
+ } catch {
6051
+ return runner === "ollama" ? "11434" : "8000";
6052
+ }
6053
+ }
6054
+ function detectRunner(url, model, hint) {
6055
+ const fromHint = runnerKindFromHint(hint);
6056
+ if (fromHint) {
6057
+ return { runner: fromHint, port: defaultPortForRunner(fromHint, url) };
6058
+ }
5893
6059
  let runner = "unknown";
5894
6060
  let port = "8000";
5895
6061
  try {
@@ -5905,8 +6071,8 @@ function detectRunner(url, model) {
5905
6071
  }
5906
6072
  return { runner, port };
5907
6073
  }
5908
- function spawnLocalRunner(url, model) {
5909
- const { runner, port } = detectRunner(url, model);
6074
+ function spawnLocalRunner(url, model, hint) {
6075
+ const { runner, port } = detectRunner(url, model, hint);
5910
6076
  try {
5911
6077
  if (runner === "fastflowlm") {
5912
6078
  const flmExe = existsSync14("C:\\Program Files\\flm\\flm.exe") ? "C:\\Program Files\\flm\\flm.exe" : "flm";
@@ -5927,8 +6093,8 @@ function spawnLocalRunner(url, model) {
5927
6093
  } catch {
5928
6094
  }
5929
6095
  }
5930
- async function startLocalRunner(url, model, locale) {
5931
- const { runner, port } = detectRunner(url, model);
6096
+ async function startLocalRunner(url, model, locale, hint) {
6097
+ const { runner, port } = detectRunner(url, model, hint);
5932
6098
  if (runner === "fastflowlm") {
5933
6099
  const flmExe = existsSync14("C:\\Program Files\\flm\\flm.exe") ? "C:\\Program Files\\flm\\flm.exe" : "flm";
5934
6100
  if (!hasCommand("flm") && flmExe === "flm") {
@@ -6012,105 +6178,39 @@ async function startLocalRunner(url, model, locale) {
6012
6178
  }
6013
6179
  }
6014
6180
  async function ensureLocalLlmRunning(db) {
6015
- const cfg = await getProviderForRole(db, "recall");
6016
- if (!cfg.enabled) {
6017
- return { usable: false, reason: "disabled" };
6018
- }
6019
- if (cfg.apiFlavor !== "chat-completions") {
6020
- console.warn(
6021
- `\x1B[31m\u2717 Recall is configured for "${cfg.apiFlavor}", but active recall currently supports chat-completions providers only.\x1B[0m`
6022
- );
6023
- return { usable: false, reason: "unsupported-provider" };
6024
- }
6025
- const { url, model, apiKey, locale } = cfg;
6026
- const isLocal = isLocalEndpoint(url);
6027
- console.log(`Checking if local LLM server is online at ${url}...`);
6028
- let online = await isLlmOnline(url);
6029
- if (!online && isLocal) {
6030
- console.log(`\x1B[33m\u26A0 Local LLM server is offline on ${url}.\x1B[0m`);
6031
- online = await startLocalRunner(url, model, locale);
6032
- }
6033
- if (!online) {
6034
- console.warn(
6035
- `\x1B[33m\u26A0 LLM server is not reachable at ${url}. Continuing without AI coaching.\x1B[0m
6036
- `
6181
+ const result = await prepareRecallChain(db, {
6182
+ timeoutMs: 25e3,
6183
+ interactive: true
6184
+ });
6185
+ if (result.usable) {
6186
+ const location = result.local ? "local" : "cloud";
6187
+ console.log(
6188
+ `\x1B[32m\u2713 Recall LLM ready (${location}: ${result.model}).\x1B[0m`
6037
6189
  );
6038
- return { usable: false, reason: "offline" };
6190
+ return { usable: true };
6039
6191
  }
6040
- console.log("\x1B[32m\u2713 Local LLM server is online.\x1B[0m");
6041
- const available = await getAvailableModels(url, apiKey);
6042
- const modelKnown = available.length === 0 || available.some((m) => m.toLowerCase() === model.toLowerCase());
6043
- if (!modelKnown) {
6192
+ if (result.reason === "unsupported-provider") {
6044
6193
  console.warn(
6045
- `\x1B[31m\u2717 Configured model "${model}" is not available on the server.\x1B[0m`
6194
+ `\x1B[31m\u2717 Recall provider is not supported for active recall.\x1B[0m`
6046
6195
  );
6047
- console.warn(` Available models: ${available.join(", ")}`);
6196
+ } else if (result.reason === "model-not-found") {
6048
6197
  console.warn(
6049
- ` Set the right one: \x1B[36mzam settings set llm.model <name>\x1B[0m`
6198
+ `\x1B[31m\u2717 Configured model "${result.model}" is not available on the server.\x1B[0m`
6050
6199
  );
6200
+ console.warn(` Available models: ${result.availableModels.join(", ")}`);
6201
+ } else if (result.reason === "offline") {
6051
6202
  console.warn(
6052
- "\x1B[33m Continuing this session without AI coaching.\x1B[0m\n"
6203
+ `\x1B[33m\u26A0 No recall LLM endpoint is reachable. Continuing without AI coaching.\x1B[0m
6204
+ `
6053
6205
  );
6054
- return { usable: false, reason: "model-not-found" };
6055
6206
  }
6056
- return { usable: true };
6207
+ return { usable: false, reason: result.reason };
6057
6208
  }
6058
6209
  async function ensureLlmReadyHeadless(db, opts = {}) {
6059
- const timeoutMs = opts.timeoutMs ?? 25e3;
6060
- const cfg = await getProviderForRole(db, "recall");
6061
- const { url, model, apiKey } = cfg;
6062
- if (!cfg.enabled) {
6063
- return {
6064
- usable: false,
6065
- reason: "disabled",
6066
- online: false,
6067
- model,
6068
- availableModels: []
6069
- };
6070
- }
6071
- if (cfg.apiFlavor !== "chat-completions") {
6072
- return {
6073
- usable: false,
6074
- reason: "unsupported-provider",
6075
- online: false,
6076
- model,
6077
- availableModels: []
6078
- };
6079
- }
6080
- const isLocal = isLocalEndpoint(url);
6081
- let online = await isLlmOnline(url);
6082
- if (!online && isLocal) {
6083
- spawnLocalRunner(url, model);
6084
- const deadline = Date.now() + timeoutMs;
6085
- while (Date.now() < deadline) {
6086
- await new Promise((r) => setTimeout(r, 1e3));
6087
- if (await isLlmOnline(url)) {
6088
- online = true;
6089
- break;
6090
- }
6091
- }
6092
- }
6093
- if (!online) {
6094
- return {
6095
- usable: false,
6096
- reason: "offline",
6097
- online: false,
6098
- model,
6099
- availableModels: []
6100
- };
6101
- }
6102
- const availableModels = await getAvailableModels(url, apiKey);
6103
- const modelKnown = availableModels.length === 0 || availableModels.some((m) => m.toLowerCase() === model.toLowerCase());
6104
- if (!modelKnown) {
6105
- return {
6106
- usable: false,
6107
- reason: "model-not-found",
6108
- online: true,
6109
- model,
6110
- availableModels
6111
- };
6112
- }
6113
- return { usable: true, online: true, model, availableModels };
6210
+ return prepareRecallChain(db, {
6211
+ timeoutMs: opts.timeoutMs ?? 25e3,
6212
+ interactive: false
6213
+ });
6114
6214
  }
6115
6215
  async function fetchWithInteractiveTimeout(url, options = {}) {
6116
6216
  const {
@@ -6200,15 +6300,22 @@ async function ensureHighQualityQuestion(db, token) {
6200
6300
  bloomLevel: token.bloomLevel,
6201
6301
  sourceLinkContent
6202
6302
  });
6203
- if (generated && generated.trim().length > 0) {
6204
- await updateToken(db, token.slug, { question: generated });
6205
- return generated;
6303
+ if (generated.text.trim().length > 0) {
6304
+ await updateToken(db, token.slug, { question: generated.text });
6305
+ return {
6306
+ question: generated.text,
6307
+ source: "llm",
6308
+ model: generated.model
6309
+ };
6206
6310
  }
6207
6311
  } catch {
6208
6312
  }
6209
6313
  }
6210
6314
  if (token.question && token.question.trim().length > 0) {
6211
- return token.question;
6315
+ return {
6316
+ question: token.question.trim(),
6317
+ source: "original"
6318
+ };
6212
6319
  }
6213
6320
  return null;
6214
6321
  }
@@ -6416,7 +6523,7 @@ async function requestChatCompletionsVisionDraft(args) {
6416
6523
  }
6417
6524
  ],
6418
6525
  temperature: 0,
6419
- max_tokens: args.input.maxTokens ?? 450,
6526
+ max_tokens: args.input.maxTokens ?? DEFAULT_LLM_MAX_TOKENS,
6420
6527
  ...args.url.includes("11434") || args.url.includes("localhost") || args.url.includes("127.0.0.1") ? { options: { num_ctx: 32768 } } : {}
6421
6528
  }),
6422
6529
  locale: args.locale,
@@ -6472,7 +6579,7 @@ async function requestAnthropicVisionDraft(args) {
6472
6579
  },
6473
6580
  body: JSON.stringify({
6474
6581
  model: args.model,
6475
- max_tokens: args.input.maxTokens ?? 450,
6582
+ max_tokens: args.input.maxTokens ?? DEFAULT_LLM_MAX_TOKENS,
6476
6583
  system: VISION_SYSTEM_PROMPT,
6477
6584
  messages: [
6478
6585
  {
@@ -6627,271 +6734,642 @@ function isRecord2(value) {
6627
6734
  return typeof value === "object" && value !== null && !Array.isArray(value);
6628
6735
  }
6629
6736
 
6630
- // src/cli/commands/resolve-user.ts
6631
- async function ensureDefaultUser(db, preferredUserId) {
6632
- const stored = await getSetting(db, "user.id");
6633
- if (stored) return stored;
6634
- const userId = preferredUserId?.trim() || process.env.ZAM_USER?.trim() || process.env.USERNAME?.trim() || process.env.USER?.trim() || "default";
6635
- await setSetting(db, "user.id", userId);
6636
- return userId;
6737
+ // src/cli/commands/provider.ts
6738
+ import { password } from "@inquirer/prompts";
6739
+ import { Command as Command2 } from "commander";
6740
+
6741
+ // src/cli/commands/shared/db.ts
6742
+ function defaultErrorHandler(message) {
6743
+ console.error("Error:", message);
6744
+ process.exit(1);
6637
6745
  }
6638
- async function resolveUser(opts, db, resolveOpts) {
6639
- if (opts.user) return opts.user;
6640
- const stored = await getSetting(db, "user.id");
6641
- if (stored) return stored;
6642
- const message = "No user specified. Set a default with: zam whoami --set <id>";
6643
- if (resolveOpts?.json) {
6644
- console.log(JSON.stringify({ error: message }, null, 2));
6645
- } else {
6646
- console.error(message);
6746
+ async function withDb(fn, onError = defaultErrorHandler) {
6747
+ let db;
6748
+ try {
6749
+ db = await openDatabase();
6750
+ await fn(db);
6751
+ } catch (err) {
6752
+ onError(err.message);
6753
+ } finally {
6754
+ await db?.close();
6647
6755
  }
6648
- process.exit(1);
6756
+ }
6757
+ function jsonOut(data) {
6758
+ console.log(JSON.stringify(data, null, 2));
6649
6759
  }
6650
6760
 
6651
- // src/cli/commands/setup.ts
6652
- import {
6653
- existsSync as existsSync15,
6654
- lstatSync,
6655
- mkdirSync as mkdirSync8,
6656
- readdirSync as readdirSync2,
6657
- readFileSync as readFileSync10,
6658
- realpathSync,
6659
- rmSync as rmSync2,
6660
- symlinkSync,
6661
- writeFileSync as writeFileSync7
6662
- } from "fs";
6663
- import { basename as basename4, dirname as dirname5, join as join15, resolve as resolve3 } from "path";
6664
- import { fileURLToPath as fileURLToPath2 } from "url";
6665
- import { Command as Command2 } from "commander";
6666
- var packageRoot = [
6667
- fileURLToPath2(new URL("../..", import.meta.url)),
6668
- fileURLToPath2(new URL("../../..", import.meta.url))
6669
- ].find((candidate) => existsSync15(join15(candidate, "package.json"))) ?? fileURLToPath2(new URL("../..", import.meta.url));
6670
- var ALL_SETUP_AGENTS = ["claude", "copilot", "codex", "agent"];
6671
- var SKILL_PAIRS = [
6672
- {
6673
- from: join15(packageRoot, ".claude", "skills", "zam", "SKILL.md"),
6674
- to: join15(".claude", "skills", "zam", "SKILL.md"),
6675
- agents: ["claude", "copilot"]
6676
- },
6677
- {
6678
- from: join15(packageRoot, ".agent", "skills", "zam", "SKILL.md"),
6679
- to: join15(".agent", "skills", "zam", "SKILL.md"),
6680
- agents: ["agent"]
6681
- },
6682
- {
6683
- from: join15(packageRoot, ".agents", "skills", "zam", "SKILL.md"),
6684
- to: join15(".agents", "skills", "zam", "SKILL.md"),
6685
- agents: ["codex"]
6686
- }
6687
- ];
6688
- function parseSetupAgents(value) {
6689
- if (!value || value.trim().toLowerCase() === "all") {
6690
- return new Set(ALL_SETUP_AGENTS);
6691
- }
6692
- const aliases = {
6693
- claude: ["claude"],
6694
- copilot: ["copilot"],
6695
- codex: ["codex"],
6696
- agent: ["agent"],
6697
- opencode: ["agent"]
6698
- };
6699
- const selected = /* @__PURE__ */ new Set();
6700
- for (const raw of value.split(",")) {
6701
- const key = raw.trim().toLowerCase();
6702
- const mapped = aliases[key];
6703
- if (!mapped) {
6704
- throw new Error(
6705
- `Unknown agent "${raw}". Use: all, claude, copilot, codex, agent.`
6706
- );
6707
- }
6708
- for (const item of mapped) selected.add(item);
6709
- }
6710
- return selected;
6761
+ // src/cli/commands/provider.ts
6762
+ var VALID_API_FLAVORS = [
6763
+ "chat-completions",
6764
+ "anthropic-messages"
6765
+ ];
6766
+ var VALID_ROLES = ["vision", "recall", "text"];
6767
+ function upsertProviderRecord(providers, name, patch) {
6768
+ const merged = { ...providers[name] ?? {} };
6769
+ if (patch.url !== void 0) merged.url = patch.url;
6770
+ if (patch.model !== void 0) merged.model = patch.model;
6771
+ if (patch.apiFlavor !== void 0) merged.apiFlavor = patch.apiFlavor;
6772
+ if (patch.apiKeyRef !== void 0) merged.apiKeyRef = patch.apiKeyRef;
6773
+ if (patch.label !== void 0) merged.label = patch.label;
6774
+ if (patch.local !== void 0) merged.local = patch.local;
6775
+ if (patch.runner !== void 0) merged.runner = patch.runner;
6776
+ return { ...providers, [name]: merged };
6711
6777
  }
6712
- function pathExists(path) {
6778
+ function removeProviderRecord(providers, name) {
6779
+ if (!(name in providers)) return { providers, removed: false };
6780
+ const next = { ...providers };
6781
+ delete next[name];
6782
+ return { providers: next, removed: true };
6783
+ }
6784
+ function rolesReferencing(roles, name) {
6785
+ return VALID_ROLES.filter((role) => {
6786
+ const binding = roles[role];
6787
+ return binding?.primary === name || binding?.fallback === name;
6788
+ });
6789
+ }
6790
+ function bindRoleProviders(roles, role, primary, fallback) {
6791
+ const binding = { primary };
6792
+ if (fallback) binding.fallback = fallback;
6793
+ return { ...roles, [role]: binding };
6794
+ }
6795
+ function maskSecret(key) {
6796
+ return key.length <= 4 ? "\u2022\u2022\u2022\u2022" : `\u2026${key.slice(-4)}`;
6797
+ }
6798
+ function buildProviderListing(providers, hasKey) {
6799
+ return Object.entries(providers).map(([name, rec]) => {
6800
+ const apiFlavor = rec.apiFlavor ?? (rec.url ? inferApiFlavor(rec.url) : "chat-completions");
6801
+ let keyState;
6802
+ if (!rec.apiKeyRef) keyState = "none";
6803
+ else keyState = hasKey(rec.apiKeyRef) ? "set" : "missing";
6804
+ const row = {
6805
+ name,
6806
+ url: rec.url,
6807
+ model: rec.model,
6808
+ apiFlavor,
6809
+ apiKeyRef: rec.apiKeyRef,
6810
+ keyState
6811
+ };
6812
+ if (rec.label !== void 0) row.label = rec.label;
6813
+ if (rec.local !== void 0) row.local = rec.local;
6814
+ if (rec.runner !== void 0) row.runner = rec.runner;
6815
+ return row;
6816
+ });
6817
+ }
6818
+ function findOrphanKeyRefs(storedRefs, providers) {
6819
+ const used = new Set(
6820
+ Object.values(providers).map((rec) => rec.apiKeyRef).filter((ref) => !!ref)
6821
+ );
6822
+ return storedRefs.filter((ref) => !used.has(ref));
6823
+ }
6824
+ async function readJson(db, key, fallback) {
6825
+ const raw = await getSetting(db, key);
6826
+ if (!raw) return fallback;
6713
6827
  try {
6714
- lstatSync(path);
6715
- return true;
6828
+ return JSON.parse(raw);
6716
6829
  } catch {
6717
- return false;
6830
+ return fallback;
6718
6831
  }
6719
6832
  }
6720
- function comparableRealPath(path) {
6721
- const real = realpathSync(path);
6722
- return process.platform === "win32" ? real.toLowerCase() : real;
6833
+ var readProviders = (db) => readJson(db, "llm.providers", {});
6834
+ var readRoles = (db) => readJson(db, "llm.roles", {});
6835
+ async function readScopedProviders(db, machine) {
6836
+ if (machine) return getMachineAiConfig().providers ?? {};
6837
+ if (!db)
6838
+ throw new Error("Database is required for shared provider settings.");
6839
+ return readProviders(db);
6723
6840
  }
6724
- function pathsResolveToSameDirectory(sourceDir, destinationDir) {
6725
- try {
6726
- return comparableRealPath(destinationDir) === comparableRealPath(sourceDir);
6727
- } catch {
6728
- return false;
6841
+ async function readScopedRoles(db, machine) {
6842
+ if (machine) return getMachineAiConfig().roles ?? {};
6843
+ if (!db)
6844
+ throw new Error("Database is required for shared provider settings.");
6845
+ return readRoles(db);
6846
+ }
6847
+ async function writeScopedProviders(db, machine, p) {
6848
+ if (machine) {
6849
+ const config = getMachineAiConfig();
6850
+ saveMachineAiConfig({ ...config, providers: p });
6851
+ return;
6729
6852
  }
6853
+ if (!db)
6854
+ throw new Error("Database is required for shared provider settings.");
6855
+ await setSetting(db, "llm.providers", JSON.stringify(p));
6730
6856
  }
6731
- function linkPointsTo(sourceDir, destinationDir) {
6732
- try {
6733
- return lstatSync(destinationDir).isSymbolicLink() && pathsResolveToSameDirectory(sourceDir, destinationDir);
6734
- } catch {
6735
- return false;
6857
+ async function writeScopedRoles(db, machine, r) {
6858
+ if (machine) {
6859
+ const config = getMachineAiConfig();
6860
+ saveMachineAiConfig({ ...config, roles: r });
6861
+ return;
6736
6862
  }
6863
+ if (!db)
6864
+ throw new Error("Database is required for shared provider settings.");
6865
+ await setSetting(db, "llm.roles", JSON.stringify(r));
6737
6866
  }
6738
- function isReplaceableCopiedSkill(destinationDir) {
6739
- try {
6740
- if (!lstatSync(destinationDir).isDirectory()) return false;
6741
- const entries = readdirSync2(destinationDir);
6742
- if (entries.length !== 1 || entries[0] !== "SKILL.md") return false;
6743
- return /^name:\s*zam\s*$/m.test(
6744
- readFileSync10(join15(destinationDir, "SKILL.md"), "utf8")
6745
- );
6746
- } catch {
6747
- return false;
6867
+ async function withProviderScope(machine, action) {
6868
+ if (machine) {
6869
+ await action(void 0);
6870
+ return;
6748
6871
  }
6872
+ await withDb(action);
6749
6873
  }
6750
- function wireSkills(cwd = process.cwd(), agents = parseSetupAgents(), opts = {}) {
6751
- const results = [];
6752
- const log = (message) => {
6753
- if (!opts.quiet) console.log(message);
6754
- };
6755
- for (const { from, to, agents: pairAgents } of SKILL_PAIRS) {
6756
- if (!pairAgents.some((agent) => agents.has(agent))) continue;
6757
- const sourceDir = dirname5(from);
6758
- const destinationDir = dirname5(join15(cwd, to));
6759
- if (!existsSync15(sourceDir)) {
6760
- if (!opts.quiet) {
6761
- console.warn(` warn source not found, skipping: ${sourceDir}`);
6874
+ var providerCommand = new Command2("provider").description(
6875
+ "Configure role-based AI providers (url/model/flavor/key, per role)"
6876
+ );
6877
+ providerCommand.command("list").description("Show configured providers, role bindings, and key status").option("--json", "Output as JSON").option("--machine", "Read machine-local providers from ~/.zam/config.json").action(async (opts) => {
6878
+ const machine = Boolean(opts.machine);
6879
+ await withProviderScope(machine, async (db) => {
6880
+ const providers = await readScopedProviders(db, machine);
6881
+ const roles = await readScopedRoles(db, machine);
6882
+ const rows = buildProviderListing(
6883
+ providers,
6884
+ (ref) => getProviderApiKey(ref) !== null
6885
+ );
6886
+ const orphans = findOrphanKeyRefs(listProviderApiKeyRefs(), providers);
6887
+ if (opts.json) {
6888
+ console.log(
6889
+ JSON.stringify(
6890
+ {
6891
+ scope: machine ? "machine" : "shared",
6892
+ providers: rows,
6893
+ roles,
6894
+ orphans
6895
+ },
6896
+ null,
6897
+ 2
6898
+ )
6899
+ );
6900
+ return;
6901
+ }
6902
+ console.log(
6903
+ `Providers (${opts.machine ? "~/.zam/config.json ai.providers" : "llm.providers"}):
6904
+ `
6905
+ );
6906
+ if (rows.length === 0) {
6907
+ console.log(
6908
+ " (none) \u2014 add one: zam provider add <name> --url <url> --model <model>"
6909
+ );
6910
+ } else {
6911
+ for (const row of rows) {
6912
+ const key = row.keyState === "set" ? "\x1B[32mkey \u2713\x1B[0m" : row.keyState === "missing" ? `\x1B[31mkey \u2717 (set: zam provider set-key ${row.apiKeyRef})\x1B[0m` : "\x1B[90mno key\x1B[0m";
6913
+ console.log(` \x1B[36m${row.name.padEnd(12)}\x1B[0m ${key}`);
6914
+ console.log(` url: ${row.url ?? "(inherits llm.url)"}`);
6915
+ console.log(` model: ${row.model ?? "(inherits llm.model)"}`);
6916
+ console.log(` flavor: ${row.apiFlavor}`);
6917
+ if (row.label) console.log(` label: ${row.label}`);
6918
+ if (row.local !== void 0) {
6919
+ console.log(` local: ${row.local ? "yes" : "no"}`);
6920
+ }
6921
+ if (row.runner) console.log(` runner: ${row.runner}`);
6922
+ if (row.apiKeyRef) console.log(` key-ref: ${row.apiKeyRef}`);
6762
6923
  }
6763
- results.push({
6764
- source: sourceDir,
6765
- destination: destinationDir,
6766
- action: "skipped",
6767
- reason: "source-not-found"
6768
- });
6769
- continue;
6770
6924
  }
6771
- if (pathsResolveToSameDirectory(sourceDir, destinationDir) && !lstatSync(destinationDir).isSymbolicLink()) {
6772
- log(` skip ${dirname5(to)} (package source)`);
6773
- results.push({
6774
- source: sourceDir,
6775
- destination: destinationDir,
6776
- action: "skipped",
6777
- reason: "source-directory"
6778
- });
6779
- continue;
6925
+ console.log("\nRoles (llm.roles):\n");
6926
+ for (const role of VALID_ROLES) {
6927
+ const binding = roles[role];
6928
+ if (!binding?.primary) {
6929
+ console.log(` ${role.padEnd(7)} \x1B[90m(unset)\x1B[0m`);
6930
+ } else {
6931
+ const fb = binding.fallback ? ` \u2192 fallback: ${binding.fallback}` : "";
6932
+ console.log(` ${role.padEnd(7)} primary: ${binding.primary}${fb}`);
6933
+ }
6780
6934
  }
6781
- if (linkPointsTo(sourceDir, destinationDir)) {
6782
- log(` skip ${dirname5(to)} (already linked)`);
6783
- results.push({
6784
- source: sourceDir,
6785
- destination: destinationDir,
6786
- action: "skipped",
6787
- reason: "already-linked"
6788
- });
6789
- continue;
6935
+ if (orphans.length > 0) {
6936
+ console.log(
6937
+ `
6938
+ \x1B[33mOrphan keys (stored but unreferenced):\x1B[0m ${orphans.join(", ")}`
6939
+ );
6790
6940
  }
6791
- const destinationExists = pathExists(destinationDir);
6792
- const replaceExisting = destinationExists && (Boolean(opts.force) || isReplaceableCopiedSkill(destinationDir));
6793
- if (destinationExists && !replaceExisting) {
6794
- if (!opts.quiet) {
6795
- console.warn(
6796
- ` warn ${dirname5(to)} already exists and is not managed by ZAM; use --force to replace it`
6797
- );
6798
- }
6799
- results.push({
6800
- source: sourceDir,
6801
- destination: destinationDir,
6802
- action: "skipped",
6803
- reason: "unmanaged-destination"
6804
- });
6805
- continue;
6941
+ });
6942
+ });
6943
+ providerCommand.command("add <name>").description("Add or update a provider record in llm.providers").option("--label <label>", "Human-readable provider label").option("--url <url>", "Endpoint base URL (e.g. https://api.deepseek.com/v1)").option("--model <model>", "Model id (e.g. deepseek-v4-flash)").option("--local", "Mark this provider as a local/on-device endpoint").option(
6944
+ "--runner <runner>",
6945
+ "Local runner hint (flm, foundry-local, ollama, ...)"
6946
+ ).option(
6947
+ "--flavor <flavor>",
6948
+ `Wire protocol: ${VALID_API_FLAVORS.join(" | ")} (default: inferred from URL)`
6949
+ ).option(
6950
+ "--key-ref <ref>",
6951
+ "Credential reference for the API key (default: <name> when --key is given)"
6952
+ ).option(
6953
+ "--key <value>",
6954
+ "Store this API key now (prefer `set-key` to keep it out of shell history)"
6955
+ ).option("--machine", "Store this provider in ~/.zam/config.json").action(async (name, opts) => {
6956
+ let apiFlavor;
6957
+ if (opts.flavor) {
6958
+ if (!VALID_API_FLAVORS.includes(opts.flavor)) {
6959
+ console.error(
6960
+ `Invalid --flavor: ${opts.flavor}. Use ${VALID_API_FLAVORS.join(" or ")}.`
6961
+ );
6962
+ process.exit(1);
6806
6963
  }
6807
- const action = destinationExists ? "relinked" : "linked";
6808
- if (opts.dryRun) {
6809
- log(` would ${action === "linked" ? "link" : "relink"} ${dirname5(to)}`);
6810
- } else {
6811
- if (destinationExists) {
6812
- rmSync2(destinationDir, { recursive: true, force: true });
6813
- }
6814
- mkdirSync8(dirname5(destinationDir), { recursive: true });
6815
- symlinkSync(
6816
- sourceDir,
6817
- destinationDir,
6818
- process.platform === "win32" ? "junction" : "dir"
6964
+ apiFlavor = opts.flavor;
6965
+ }
6966
+ const apiKeyRef = opts.keyRef ?? (opts.key ? name : void 0);
6967
+ const machine = Boolean(opts.machine);
6968
+ await withProviderScope(machine, async (db) => {
6969
+ const providers = await readScopedProviders(db, machine);
6970
+ const next = upsertProviderRecord(providers, name, {
6971
+ label: opts.label,
6972
+ url: opts.url,
6973
+ model: opts.model,
6974
+ apiFlavor,
6975
+ apiKeyRef,
6976
+ local: opts.local ? true : void 0,
6977
+ runner: opts.runner
6978
+ });
6979
+ await writeScopedProviders(db, machine, next);
6980
+ if (opts.key && apiKeyRef) setProviderApiKey(apiKeyRef, opts.key);
6981
+ const rec = next[name];
6982
+ console.log(
6983
+ `Provider "${name}" saved (${machine ? "machine-local" : "shared"}):`
6984
+ );
6985
+ console.log(` url: ${rec.url ?? "(inherits llm.url)"}`);
6986
+ console.log(` model: ${rec.model ?? "(inherits llm.model)"}`);
6987
+ if (rec.label) console.log(` label: ${rec.label}`);
6988
+ if (rec.local !== void 0) {
6989
+ console.log(` local: ${rec.local ? "yes" : "no"}`);
6990
+ }
6991
+ if (rec.runner) console.log(` runner: ${rec.runner}`);
6992
+ console.log(
6993
+ ` flavor: ${rec.apiFlavor ?? `${rec.url ? inferApiFlavor(rec.url) : "chat-completions"} (inferred)`}`
6994
+ );
6995
+ console.log(` key-ref: ${rec.apiKeyRef ?? "(none \u2014 uses default key)"}`);
6996
+ if (opts.key) {
6997
+ console.log(` key: stored (${maskSecret(opts.key)})`);
6998
+ } else if (rec.apiKeyRef && getProviderApiKey(rec.apiKeyRef) === null) {
6999
+ console.log(
7000
+ `
7001
+ \u26A0 No key stored for "${rec.apiKeyRef}". Run: zam provider set-key ${rec.apiKeyRef}`
6819
7002
  );
6820
- log(` ${action === "linked" ? "link" : "relink"} ${dirname5(to)}`);
6821
7003
  }
6822
- results.push({ source: sourceDir, destination: destinationDir, action });
7004
+ if (!rec.url) {
7005
+ console.log(
7006
+ "\n \u26A0 No --url set; this provider inherits the base llm.url."
7007
+ );
7008
+ }
7009
+ console.log(
7010
+ `
7011
+ Bind it to a role: zam provider use recall --primary ${name}`
7012
+ );
7013
+ });
7014
+ });
7015
+ providerCommand.command("remove <name>").description("Remove a provider record from llm.providers").option("--machine", "Remove from ~/.zam/config.json instead of the DB").action(async (name, opts) => {
7016
+ const machine = Boolean(opts.machine);
7017
+ await withProviderScope(machine, async (db) => {
7018
+ const providers = await readScopedProviders(db, machine);
7019
+ const { providers: next, removed } = removeProviderRecord(
7020
+ providers,
7021
+ name
7022
+ );
7023
+ if (!removed) {
7024
+ console.log(`No such provider: ${name}`);
7025
+ return;
7026
+ }
7027
+ await writeScopedProviders(db, machine, next);
7028
+ console.log(
7029
+ `Removed provider "${name}" (${machine ? "machine-local" : "shared"}).`
7030
+ );
7031
+ const referencing = rolesReferencing(
7032
+ await readScopedRoles(db, machine),
7033
+ name
7034
+ );
7035
+ if (referencing.length > 0) {
7036
+ console.log(
7037
+ ` \u26A0 Still referenced by role(s): ${referencing.join(", ")} \u2014 rebind with: zam provider use <role> --primary <name>`
7038
+ );
7039
+ }
7040
+ });
7041
+ });
7042
+ providerCommand.command("use <role>").description(`Bind providers to a role (${VALID_ROLES.join(" | ")})`).option("--primary <name>", "Primary provider").option("--fallback <name>", "Fallback provider (optional)").option("--machine", "Bind the role in ~/.zam/config.json").action(async (role, opts) => {
7043
+ if (!VALID_ROLES.includes(role)) {
7044
+ console.error(`Invalid role: ${role}. Use ${VALID_ROLES.join(", ")}.`);
7045
+ process.exit(1);
6823
7046
  }
6824
- return results;
6825
- }
6826
- function formatDatabaseInitTarget(target) {
6827
- switch (target.kind) {
6828
- case "local":
6829
- return `ZAM database at ${target.location} (local SQLite)`;
6830
- case "turso-remote":
6831
- return `ZAM database via Turso remote at ${target.location}`;
6832
- case "turso-native":
6833
- return `ZAM database via Turso native driver at ${target.location}`;
6834
- case "turso-replica":
6835
- return `ZAM database replica at ${target.location} syncing from ${target.syncUrl}`;
7047
+ if (!opts.primary) {
7048
+ console.error("--primary is required.");
7049
+ process.exit(1);
6836
7050
  }
6837
- }
6838
- async function initDatabase(skipInit) {
6839
- if (skipInit) return;
7051
+ const machine = Boolean(opts.machine);
7052
+ await withProviderScope(machine, async (db) => {
7053
+ const providers = await readScopedProviders(db, machine);
7054
+ await writeScopedRoles(
7055
+ db,
7056
+ machine,
7057
+ bindRoleProviders(
7058
+ await readScopedRoles(db, machine),
7059
+ role,
7060
+ opts.primary,
7061
+ opts.fallback
7062
+ )
7063
+ );
7064
+ const fb = opts.fallback ? `, fallback: ${opts.fallback}` : "";
7065
+ console.log(
7066
+ `Role "${role}" \u2192 primary: ${opts.primary}${fb} (${machine ? "machine-local" : "shared"})`
7067
+ );
7068
+ for (const [label, ref] of [
7069
+ ["primary", opts.primary],
7070
+ ["fallback", opts.fallback]
7071
+ ]) {
7072
+ if (ref && !(ref in providers)) {
7073
+ console.log(
7074
+ ` \u26A0 ${label} "${ref}" is not a defined provider yet. Add it: zam provider add ${ref} --url <url> --model <model>`
7075
+ );
7076
+ }
7077
+ }
7078
+ });
7079
+ });
7080
+ providerCommand.command("set-key <ref>").description(
7081
+ "Store an API key for a provider reference (in credentials.json)"
7082
+ ).option(
7083
+ "--key <value>",
7084
+ "The API key (omit to enter it interactively, hidden)"
7085
+ ).action(async (ref, opts) => {
6840
7086
  try {
6841
- const target = getDatabaseTargetInfo();
6842
- const db = await openDatabaseWithSync({ initialize: true });
6843
- await db.close();
6844
- console.log(` init ${formatDatabaseInitTarget(target)}`);
6845
- } catch (err) {
6846
- const msg = err.message;
6847
- if (!msg.includes("already")) {
6848
- console.warn(` warn database init: ${msg}`);
6849
- } else {
6850
- console.log(` skip database already initialized`);
7087
+ const key = opts.key ?? await password({ message: `API key for "${ref}":` });
7088
+ if (!key || key.trim().length === 0) {
7089
+ console.error("No key provided.");
7090
+ process.exit(1);
6851
7091
  }
6852
- }
6853
- }
6854
- var ZAM_BLOCK_START = "<!-- ZAM:START -->";
6855
- var ZAM_BLOCK_END = "<!-- ZAM:END -->";
6856
- function upsertMarkedBlock(dest, blockBody, dryRun) {
6857
- const block = `${ZAM_BLOCK_START}
6858
- ${blockBody.trim()}
6859
- ${ZAM_BLOCK_END}`;
6860
- if (!existsSync15(dest)) {
6861
- if (!dryRun) {
6862
- mkdirSync8(dirname5(dest), { recursive: true });
6863
- writeFileSync7(dest, `${block}
6864
- `, "utf8");
7092
+ setProviderApiKey(ref, key.trim());
7093
+ console.log(`Stored API key for "${ref}" (${maskSecret(key.trim())}).`);
7094
+ } catch (err) {
7095
+ if (err.name === "ExitPromptError") {
7096
+ console.log("\nCancelled.");
7097
+ process.exit(0);
6865
7098
  }
6866
- return "write";
7099
+ console.error("Error:", err.message);
7100
+ process.exit(1);
6867
7101
  }
6868
- const existing = readFileSync10(dest, "utf8");
6869
- if (existing.includes(block)) return "skip";
6870
- const start = existing.indexOf(ZAM_BLOCK_START);
6871
- const end = existing.indexOf(ZAM_BLOCK_END);
6872
- const next = start >= 0 && end > start ? `${existing.slice(0, start)}${block}${existing.slice(end + ZAM_BLOCK_END.length)}` : `${existing.trimEnd()}
7102
+ });
7103
+ providerCommand.command("clear-key <ref>").description("Remove a stored API key").action((ref) => {
7104
+ clearProviderApiKey(ref);
7105
+ console.log(`Cleared API key for "${ref}".`);
7106
+ });
6873
7107
 
6874
- ${block}
6875
- `;
6876
- if (!dryRun) writeFileSync7(dest, next, "utf8");
6877
- return start >= 0 && end > start ? "update" : "write";
7108
+ // src/cli/commands/resolve-user.ts
7109
+ async function ensureDefaultUser(db, preferredUserId) {
7110
+ const stored = await getSetting(db, "user.id");
7111
+ if (stored) return stored;
7112
+ const userId = preferredUserId?.trim() || process.env.ZAM_USER?.trim() || process.env.USERNAME?.trim() || process.env.USER?.trim() || "default";
7113
+ await setSetting(db, "user.id", userId);
7114
+ return userId;
6878
7115
  }
6879
- function logInstructionAction(action, label, dryRun) {
6880
- if (action === "skip") {
6881
- console.log(` skip ${label} (ZAM block already present)`);
7116
+ async function resolveUser(opts, db, resolveOpts) {
7117
+ if (opts.user) return opts.user;
7118
+ const stored = await getSetting(db, "user.id");
7119
+ if (stored) return stored;
7120
+ const message = "No user specified. Set a default with: zam whoami --set <id>";
7121
+ if (resolveOpts?.json) {
7122
+ console.log(JSON.stringify({ error: message }, null, 2));
6882
7123
  } else {
6883
- console.log(` ${dryRun ? "would " : ""}${action} ${label}`);
7124
+ console.error(message);
6884
7125
  }
7126
+ process.exit(1);
6885
7127
  }
6886
- function writeClaudeMd(skipClaudeMd, cwd = process.cwd(), opts = {}) {
6887
- if (skipClaudeMd) return;
6888
- const dest = join15(cwd, "CLAUDE.md");
6889
- if (existsSync15(dest)) {
6890
- if (!opts.updateExisting) {
6891
- console.log(` skip CLAUDE.md (already present)`);
6892
- return;
6893
- }
6894
- const action = upsertMarkedBlock(
7128
+
7129
+ // src/cli/commands/setup.ts
7130
+ import {
7131
+ existsSync as existsSync15,
7132
+ lstatSync,
7133
+ mkdirSync as mkdirSync8,
7134
+ readdirSync as readdirSync2,
7135
+ readFileSync as readFileSync10,
7136
+ realpathSync,
7137
+ rmSync as rmSync2,
7138
+ symlinkSync,
7139
+ writeFileSync as writeFileSync7
7140
+ } from "fs";
7141
+ import { basename as basename4, dirname as dirname5, join as join15, resolve as resolve3 } from "path";
7142
+ import { fileURLToPath as fileURLToPath2 } from "url";
7143
+ import { Command as Command3 } from "commander";
7144
+ var packageRoot = [
7145
+ fileURLToPath2(new URL("../..", import.meta.url)),
7146
+ fileURLToPath2(new URL("../../..", import.meta.url))
7147
+ ].find((candidate) => existsSync15(join15(candidate, "package.json"))) ?? fileURLToPath2(new URL("../..", import.meta.url));
7148
+ var ALL_SETUP_AGENTS = ["claude", "copilot", "codex", "agent"];
7149
+ var SKILL_PAIRS = [
7150
+ {
7151
+ from: join15(packageRoot, ".claude", "skills", "zam", "SKILL.md"),
7152
+ to: join15(".claude", "skills", "zam", "SKILL.md"),
7153
+ agents: ["claude", "copilot"]
7154
+ },
7155
+ {
7156
+ from: join15(packageRoot, ".agent", "skills", "zam", "SKILL.md"),
7157
+ to: join15(".agent", "skills", "zam", "SKILL.md"),
7158
+ agents: ["agent"]
7159
+ },
7160
+ {
7161
+ from: join15(packageRoot, ".agents", "skills", "zam", "SKILL.md"),
7162
+ to: join15(".agents", "skills", "zam", "SKILL.md"),
7163
+ agents: ["codex"]
7164
+ }
7165
+ ];
7166
+ function parseSetupAgents(value) {
7167
+ if (!value || value.trim().toLowerCase() === "all") {
7168
+ return new Set(ALL_SETUP_AGENTS);
7169
+ }
7170
+ const aliases = {
7171
+ claude: ["claude"],
7172
+ copilot: ["copilot"],
7173
+ codex: ["codex"],
7174
+ agent: ["agent"],
7175
+ opencode: ["agent"]
7176
+ };
7177
+ const selected = /* @__PURE__ */ new Set();
7178
+ for (const raw of value.split(",")) {
7179
+ const key = raw.trim().toLowerCase();
7180
+ const mapped = aliases[key];
7181
+ if (!mapped) {
7182
+ throw new Error(
7183
+ `Unknown agent "${raw}". Use: all, claude, copilot, codex, agent.`
7184
+ );
7185
+ }
7186
+ for (const item of mapped) selected.add(item);
7187
+ }
7188
+ return selected;
7189
+ }
7190
+ function pathExists(path) {
7191
+ try {
7192
+ lstatSync(path);
7193
+ return true;
7194
+ } catch {
7195
+ return false;
7196
+ }
7197
+ }
7198
+ function comparableRealPath(path) {
7199
+ const real = realpathSync(path);
7200
+ return process.platform === "win32" ? real.toLowerCase() : real;
7201
+ }
7202
+ function pathsResolveToSameDirectory(sourceDir, destinationDir) {
7203
+ try {
7204
+ return comparableRealPath(destinationDir) === comparableRealPath(sourceDir);
7205
+ } catch {
7206
+ return false;
7207
+ }
7208
+ }
7209
+ function linkPointsTo(sourceDir, destinationDir) {
7210
+ try {
7211
+ return lstatSync(destinationDir).isSymbolicLink() && pathsResolveToSameDirectory(sourceDir, destinationDir);
7212
+ } catch {
7213
+ return false;
7214
+ }
7215
+ }
7216
+ function isReplaceableCopiedSkill(destinationDir) {
7217
+ try {
7218
+ if (!lstatSync(destinationDir).isDirectory()) return false;
7219
+ const entries = readdirSync2(destinationDir);
7220
+ if (entries.length !== 1 || entries[0] !== "SKILL.md") return false;
7221
+ return /^name:\s*zam\s*$/m.test(
7222
+ readFileSync10(join15(destinationDir, "SKILL.md"), "utf8")
7223
+ );
7224
+ } catch {
7225
+ return false;
7226
+ }
7227
+ }
7228
+ function wireSkills(cwd = process.cwd(), agents = parseSetupAgents(), opts = {}) {
7229
+ const results = [];
7230
+ const log = (message) => {
7231
+ if (!opts.quiet) console.log(message);
7232
+ };
7233
+ for (const { from, to, agents: pairAgents } of SKILL_PAIRS) {
7234
+ if (!pairAgents.some((agent) => agents.has(agent))) continue;
7235
+ const sourceDir = dirname5(from);
7236
+ const destinationDir = dirname5(join15(cwd, to));
7237
+ if (!existsSync15(sourceDir)) {
7238
+ if (!opts.quiet) {
7239
+ console.warn(` warn source not found, skipping: ${sourceDir}`);
7240
+ }
7241
+ results.push({
7242
+ source: sourceDir,
7243
+ destination: destinationDir,
7244
+ action: "skipped",
7245
+ reason: "source-not-found"
7246
+ });
7247
+ continue;
7248
+ }
7249
+ if (pathsResolveToSameDirectory(sourceDir, destinationDir) && !lstatSync(destinationDir).isSymbolicLink()) {
7250
+ log(` skip ${dirname5(to)} (package source)`);
7251
+ results.push({
7252
+ source: sourceDir,
7253
+ destination: destinationDir,
7254
+ action: "skipped",
7255
+ reason: "source-directory"
7256
+ });
7257
+ continue;
7258
+ }
7259
+ if (linkPointsTo(sourceDir, destinationDir)) {
7260
+ log(` skip ${dirname5(to)} (already linked)`);
7261
+ results.push({
7262
+ source: sourceDir,
7263
+ destination: destinationDir,
7264
+ action: "skipped",
7265
+ reason: "already-linked"
7266
+ });
7267
+ continue;
7268
+ }
7269
+ const destinationExists = pathExists(destinationDir);
7270
+ const replaceExisting = destinationExists && (Boolean(opts.force) || isReplaceableCopiedSkill(destinationDir));
7271
+ if (destinationExists && !replaceExisting) {
7272
+ if (!opts.quiet) {
7273
+ console.warn(
7274
+ ` warn ${dirname5(to)} already exists and is not managed by ZAM; use --force to replace it`
7275
+ );
7276
+ }
7277
+ results.push({
7278
+ source: sourceDir,
7279
+ destination: destinationDir,
7280
+ action: "skipped",
7281
+ reason: "unmanaged-destination"
7282
+ });
7283
+ continue;
7284
+ }
7285
+ const action = destinationExists ? "relinked" : "linked";
7286
+ if (opts.dryRun) {
7287
+ log(` would ${action === "linked" ? "link" : "relink"} ${dirname5(to)}`);
7288
+ } else {
7289
+ if (destinationExists) {
7290
+ rmSync2(destinationDir, { recursive: true, force: true });
7291
+ }
7292
+ mkdirSync8(dirname5(destinationDir), { recursive: true });
7293
+ symlinkSync(
7294
+ sourceDir,
7295
+ destinationDir,
7296
+ process.platform === "win32" ? "junction" : "dir"
7297
+ );
7298
+ log(` ${action === "linked" ? "link" : "relink"} ${dirname5(to)}`);
7299
+ }
7300
+ results.push({ source: sourceDir, destination: destinationDir, action });
7301
+ }
7302
+ return results;
7303
+ }
7304
+ function formatDatabaseInitTarget(target) {
7305
+ switch (target.kind) {
7306
+ case "local":
7307
+ return `ZAM database at ${target.location} (local SQLite)`;
7308
+ case "turso-remote":
7309
+ return `ZAM database via Turso remote at ${target.location}`;
7310
+ case "turso-native":
7311
+ return `ZAM database via Turso native driver at ${target.location}`;
7312
+ case "turso-replica":
7313
+ return `ZAM database replica at ${target.location} syncing from ${target.syncUrl}`;
7314
+ }
7315
+ }
7316
+ async function initDatabase(skipInit) {
7317
+ if (skipInit) return;
7318
+ try {
7319
+ const target = getDatabaseTargetInfo();
7320
+ const db = await openDatabaseWithSync({ initialize: true });
7321
+ await db.close();
7322
+ console.log(` init ${formatDatabaseInitTarget(target)}`);
7323
+ } catch (err) {
7324
+ const msg = err.message;
7325
+ if (!msg.includes("already")) {
7326
+ console.warn(` warn database init: ${msg}`);
7327
+ } else {
7328
+ console.log(` skip database already initialized`);
7329
+ }
7330
+ }
7331
+ }
7332
+ var ZAM_BLOCK_START = "<!-- ZAM:START -->";
7333
+ var ZAM_BLOCK_END = "<!-- ZAM:END -->";
7334
+ function upsertMarkedBlock(dest, blockBody, dryRun) {
7335
+ const block = `${ZAM_BLOCK_START}
7336
+ ${blockBody.trim()}
7337
+ ${ZAM_BLOCK_END}`;
7338
+ if (!existsSync15(dest)) {
7339
+ if (!dryRun) {
7340
+ mkdirSync8(dirname5(dest), { recursive: true });
7341
+ writeFileSync7(dest, `${block}
7342
+ `, "utf8");
7343
+ }
7344
+ return "write";
7345
+ }
7346
+ const existing = readFileSync10(dest, "utf8");
7347
+ if (existing.includes(block)) return "skip";
7348
+ const start = existing.indexOf(ZAM_BLOCK_START);
7349
+ const end = existing.indexOf(ZAM_BLOCK_END);
7350
+ const next = start >= 0 && end > start ? `${existing.slice(0, start)}${block}${existing.slice(end + ZAM_BLOCK_END.length)}` : `${existing.trimEnd()}
7351
+
7352
+ ${block}
7353
+ `;
7354
+ if (!dryRun) writeFileSync7(dest, next, "utf8");
7355
+ return start >= 0 && end > start ? "update" : "write";
7356
+ }
7357
+ function logInstructionAction(action, label, dryRun) {
7358
+ if (action === "skip") {
7359
+ console.log(` skip ${label} (ZAM block already present)`);
7360
+ } else {
7361
+ console.log(` ${dryRun ? "would " : ""}${action} ${label}`);
7362
+ }
7363
+ }
7364
+ function writeClaudeMd(skipClaudeMd, cwd = process.cwd(), opts = {}) {
7365
+ if (skipClaudeMd) return;
7366
+ const dest = join15(cwd, "CLAUDE.md");
7367
+ if (existsSync15(dest)) {
7368
+ if (!opts.updateExisting) {
7369
+ console.log(` skip CLAUDE.md (already present)`);
7370
+ return;
7371
+ }
7372
+ const action = upsertMarkedBlock(
6895
7373
  dest,
6896
7374
  `## ZAM learning sessions
6897
7375
 
@@ -7008,7 +7486,7 @@ ZAM is available in this repository through \`.claude/skills/zam/\`. Use the \`z
7008
7486
  Boolean(opts.dryRun)
7009
7487
  );
7010
7488
  }
7011
- var setupCommand = new Command2("setup").description(
7489
+ var setupCommand = new Command3("setup").description(
7012
7490
  "Link ZAM skill directories into this workspace and initialize the database"
7013
7491
  ).option("--force", "replace an unmanaged existing ZAM skill directory", false).option("--skip-init", "skip database initialization", false).option("--skip-claude-md", "skip CLAUDE.md generation", false).option("--skip-agents-md", "skip AGENTS.md generation", false).option("--target <path>", "repository/workspace directory to set up").option(
7014
7492
  "--agents <list>",
@@ -7061,33 +7539,13 @@ var setupCommand = new Command2("setup").description(
7061
7539
  }
7062
7540
  );
7063
7541
 
7064
- // src/cli/commands/shared/db.ts
7065
- function defaultErrorHandler(message) {
7066
- console.error("Error:", message);
7067
- process.exit(1);
7068
- }
7069
- async function withDb(fn, onError = defaultErrorHandler) {
7070
- let db;
7071
- try {
7072
- db = await openDatabase();
7073
- await fn(db);
7074
- } catch (err) {
7075
- onError(err.message);
7076
- } finally {
7077
- await db?.close();
7078
- }
7079
- }
7080
- function jsonOut(data) {
7081
- console.log(JSON.stringify(data, null, 2));
7082
- }
7083
-
7084
7542
  // src/cli/commands/workspace.ts
7085
7543
  import { execFileSync as execFileSync3 } from "child_process";
7086
7544
  import { existsSync as existsSync16, mkdirSync as mkdirSync9, writeFileSync as writeFileSync8 } from "fs";
7087
7545
  import { homedir as homedir9 } from "os";
7088
7546
  import { join as join16, resolve as resolve4 } from "path";
7089
7547
  import { confirm, input } from "@inquirer/prompts";
7090
- import { Command as Command3 } from "commander";
7548
+ import { Command as Command4 } from "commander";
7091
7549
  function runGit(cwd, args) {
7092
7550
  try {
7093
7551
  return execFileSync3("git", args, {
@@ -7105,7 +7563,7 @@ function ghRepoCreateArgs(repoName, repoVisibility) {
7105
7563
  function gitRemoteArgs(githubUrl, hasOrigin) {
7106
7564
  return hasOrigin ? ["remote", "set-url", "origin", githubUrl] : ["remote", "add", "origin", githubUrl];
7107
7565
  }
7108
- var workspaceCommand = new Command3("workspace").description(
7566
+ var workspaceCommand = new Command4("workspace").description(
7109
7567
  "Manage your ZAM learning workspace"
7110
7568
  );
7111
7569
  var WORKSPACE_KINDS = [
@@ -7461,6 +7919,12 @@ function parseNonNegativeIntegerOption(name, value) {
7461
7919
  }
7462
7920
  return parsed;
7463
7921
  }
7922
+ function parseProviderScope(scope) {
7923
+ const value = scope ?? "machine";
7924
+ if (value === "machine") return true;
7925
+ if (value === "shared") return false;
7926
+ jsonError(`Invalid --scope: ${value}. Use machine or shared.`);
7927
+ }
7464
7928
  async function withDb2(fn) {
7465
7929
  await withDb(fn, jsonError);
7466
7930
  }
@@ -7498,7 +7962,7 @@ function parseTokenUpdates(opts) {
7498
7962
  }
7499
7963
  return updates;
7500
7964
  }
7501
- var bridgeCommand = new Command4("bridge").description(
7965
+ var bridgeCommand = new Command5("bridge").description(
7502
7966
  "Machine-readable JSON protocol for AI integration"
7503
7967
  );
7504
7968
  bridgeCommand.command("check-due").description("Check due cards for a user (JSON)").option("--user <id>", "User ID (default: whoami)").option("--domain <domain>", "Filter by knowledge domain").action(async (opts) => {
@@ -7768,6 +8232,8 @@ bridgeCommand.command("get-review").description("Get next review card with promp
7768
8232
  const item = queue.items[0];
7769
8233
  const isLlmEnabled = await getSetting(db, "llm.enabled") === "true";
7770
8234
  let resolvedQuestion = item.question;
8235
+ let questionSource = "original";
8236
+ let questionModel;
7771
8237
  if (isLlmEnabled) {
7772
8238
  try {
7773
8239
  const healed = await ensureHighQualityQuestion(db, {
@@ -7780,7 +8246,9 @@ bridgeCommand.command("get-review").description("Get next review card with promp
7780
8246
  question: item.question
7781
8247
  });
7782
8248
  if (healed) {
7783
- resolvedQuestion = healed;
8249
+ resolvedQuestion = healed.question;
8250
+ questionSource = healed.source;
8251
+ questionModel = healed.model;
7784
8252
  }
7785
8253
  } catch {
7786
8254
  }
@@ -7809,6 +8277,8 @@ bridgeCommand.command("get-review").description("Get next review card with promp
7809
8277
  hasReview: true,
7810
8278
  card: item,
7811
8279
  prompt,
8280
+ questionSource,
8281
+ questionModel: questionModel ?? null,
7812
8282
  resolvedContext,
7813
8283
  queueSize: fullQueue.items.length
7814
8284
  });
@@ -8908,6 +9378,188 @@ bridgeCommand.command("provider-status").description("Show secret-safe provider
8908
9378
  jsonOut2({ roles: { recall, vision, text } });
8909
9379
  });
8910
9380
  });
9381
+ bridgeCommand.command("provider-config-list").description("List provider records and role bindings (JSON)").option("--scope <scope>", "machine (default) or shared", "machine").action(async (opts) => {
9382
+ const machine = parseProviderScope(opts.scope);
9383
+ await withProviderScope(machine, async (db) => {
9384
+ const providers = await readScopedProviders(db, machine);
9385
+ const roles = await readScopedRoles(db, machine);
9386
+ const rows = buildProviderListing(
9387
+ providers,
9388
+ (ref) => getProviderApiKey(ref) !== null
9389
+ );
9390
+ jsonOut2({
9391
+ scope: machine ? "machine" : "shared",
9392
+ providers: rows,
9393
+ roles,
9394
+ orphans: findOrphanKeyRefs(listProviderApiKeyRefs(), providers)
9395
+ });
9396
+ });
9397
+ });
9398
+ bridgeCommand.command("provider-config-upsert").description("Add or update a provider record (JSON)").requiredOption("--name <name>", "Provider name").option("--label <label>", "Human-readable label").option("--url <url>", "Endpoint base URL").option("--model <model>", "Model id").option(
9399
+ "--flavor <flavor>",
9400
+ `Wire protocol: ${VALID_API_FLAVORS.join(" | ")}`
9401
+ ).option("--local", "Mark as local endpoint").option("--no-local", "Mark as cloud/non-local endpoint").option("--runner <runner>", "Local runner hint").option("--key-ref <ref>", "Credential reference for API key").option("--scope <scope>", "machine (default) or shared", "machine").action(async (opts, command) => {
9402
+ const machine = parseProviderScope(opts.scope);
9403
+ let apiFlavor;
9404
+ if (opts.flavor) {
9405
+ if (!VALID_API_FLAVORS.includes(opts.flavor)) {
9406
+ jsonError(
9407
+ `Invalid --flavor: ${opts.flavor}. Use ${VALID_API_FLAVORS.join(" or ")}.`
9408
+ );
9409
+ }
9410
+ apiFlavor = opts.flavor;
9411
+ }
9412
+ let local;
9413
+ if (command.getOptionValueSource("local") === "cli") {
9414
+ local = opts.local === true;
9415
+ }
9416
+ const patch = {};
9417
+ if (opts.label !== void 0) patch.label = opts.label;
9418
+ if (opts.url !== void 0) patch.url = opts.url;
9419
+ if (opts.model !== void 0) patch.model = opts.model;
9420
+ if (apiFlavor !== void 0) patch.apiFlavor = apiFlavor;
9421
+ if (opts.keyRef !== void 0) patch.apiKeyRef = opts.keyRef;
9422
+ if (local !== void 0) patch.local = local;
9423
+ if (opts.runner !== void 0) patch.runner = opts.runner;
9424
+ await withProviderScope(machine, async (db) => {
9425
+ const providers = await readScopedProviders(db, machine);
9426
+ const next = upsertProviderRecord(providers, opts.name, patch);
9427
+ await writeScopedProviders(db, machine, next);
9428
+ const rows = buildProviderListing(
9429
+ next,
9430
+ (ref) => getProviderApiKey(ref) !== null
9431
+ );
9432
+ const row = rows.find((entry) => entry.name === opts.name);
9433
+ jsonOut2({
9434
+ ok: true,
9435
+ scope: machine ? "machine" : "shared",
9436
+ name: opts.name,
9437
+ provider: row,
9438
+ cloudModelHint: opts.url ? getCloudModelRecommendation(opts.url) : null
9439
+ });
9440
+ });
9441
+ });
9442
+ bridgeCommand.command("provider-config-remove").description("Remove a provider record (JSON)").requiredOption("--name <name>", "Provider name").option("--scope <scope>", "machine (default) or shared", "machine").action(async (opts) => {
9443
+ const machine = parseProviderScope(opts.scope);
9444
+ await withProviderScope(machine, async (db) => {
9445
+ const providers = await readScopedProviders(db, machine);
9446
+ const { providers: next, removed } = removeProviderRecord(
9447
+ providers,
9448
+ opts.name
9449
+ );
9450
+ if (!removed) {
9451
+ jsonError(`No such provider: ${opts.name}`);
9452
+ }
9453
+ await writeScopedProviders(db, machine, next);
9454
+ jsonOut2({
9455
+ ok: true,
9456
+ scope: machine ? "machine" : "shared",
9457
+ name: opts.name,
9458
+ removed: true,
9459
+ referencingRoles: rolesReferencing(
9460
+ await readScopedRoles(db, machine),
9461
+ opts.name
9462
+ )
9463
+ });
9464
+ });
9465
+ });
9466
+ bridgeCommand.command("provider-config-bind").description("Bind providers to an LLM role (JSON)").requiredOption("--role <role>", `Role: ${VALID_ROLES.join(" | ")}`).requiredOption("--primary <name>", "Primary provider name").option("--fallback <name>", "Fallback provider name").option("--scope <scope>", "machine (default) or shared", "machine").action(async (opts) => {
9467
+ if (!VALID_ROLES.includes(opts.role)) {
9468
+ jsonError(`Invalid --role: ${opts.role}. Use ${VALID_ROLES.join(", ")}.`);
9469
+ }
9470
+ const machine = parseProviderScope(opts.scope);
9471
+ await withProviderScope(machine, async (db) => {
9472
+ const providers = await readScopedProviders(db, machine);
9473
+ const roles = await readScopedRoles(db, machine);
9474
+ const nextRoles = bindRoleProviders(
9475
+ roles,
9476
+ opts.role,
9477
+ opts.primary,
9478
+ opts.fallback
9479
+ );
9480
+ await writeScopedRoles(db, machine, nextRoles);
9481
+ const binding = nextRoles[opts.role];
9482
+ const primary = providers[opts.primary];
9483
+ jsonOut2({
9484
+ ok: true,
9485
+ scope: machine ? "machine" : "shared",
9486
+ role: opts.role,
9487
+ binding,
9488
+ warnings: [
9489
+ ...primary && primary.apiFlavor === "anthropic-messages" && (opts.role === "recall" || opts.role === "text") ? ["unsupported-provider-for-role"] : [],
9490
+ ...opts.primary && !(opts.primary in providers) ? ["primary-provider-undefined"] : [],
9491
+ ...opts.fallback && !(opts.fallback in providers) ? ["fallback-provider-undefined"] : []
9492
+ ]
9493
+ });
9494
+ });
9495
+ });
9496
+ bridgeCommand.command("provider-set-key").description("Store an API key for a provider reference (JSON)").requiredOption("--ref <ref>", "Credential reference name").requiredOption("--key <value>", "API key value (write-only)").action((opts) => {
9497
+ const key = opts.key.trim();
9498
+ if (!key) jsonError("No key provided.");
9499
+ setProviderApiKey(opts.ref, key);
9500
+ jsonOut2({ ok: true, ref: opts.ref, masked: maskSecret(key) });
9501
+ });
9502
+ bridgeCommand.command("provider-clear-key").description("Remove a stored provider API key (JSON)").requiredOption("--ref <ref>", "Credential reference name").action((opts) => {
9503
+ clearProviderApiKey(opts.ref);
9504
+ jsonOut2({ ok: true, ref: opts.ref });
9505
+ });
9506
+ bridgeCommand.command("list-models").description("List models exposed by an LLM endpoint (JSON)").requiredOption("--url <url>", "Endpoint base URL").option("--key-ref <ref>", "Resolve API key from credentials by reference").action(async (opts) => {
9507
+ const apiKey = opts.keyRef ? getProviderApiKey(opts.keyRef) ?? void 0 : void 0;
9508
+ const models = await getAvailableModels(opts.url, apiKey);
9509
+ jsonOut2({ models });
9510
+ });
9511
+ bridgeCommand.command("cloud-model-hint").description("Suggest a cloud model for an endpoint URL (JSON)").requiredOption("--url <url>", "Endpoint base URL").action((opts) => {
9512
+ jsonOut2({ recommendation: getCloudModelRecommendation(opts.url) });
9513
+ });
9514
+ bridgeCommand.command("local-llm-hints").description("Detect installed local LLM servers and suggest defaults (JSON)").action(() => {
9515
+ const profile = getSystemProfile();
9516
+ const flmInstalled = hasCommand("flm") || existsSync17("C:\\Program Files\\flm\\flm.exe");
9517
+ const ollamaInstalled = hasCommand("ollama");
9518
+ const runners = [
9519
+ { id: "flm", label: "FastFlowLM", installed: flmInstalled },
9520
+ { id: "ollama", label: "Ollama", installed: ollamaInstalled },
9521
+ {
9522
+ id: "foundry-local",
9523
+ label: "Foundry Local",
9524
+ installed: false
9525
+ }
9526
+ ];
9527
+ let recommended = "ollama";
9528
+ if (profile.recommendedRunner === "fastflowlm" && flmInstalled) {
9529
+ recommended = "flm";
9530
+ } else if (profile.recommendedRunner === "ollama" && ollamaInstalled) {
9531
+ recommended = "ollama";
9532
+ } else if (flmInstalled) {
9533
+ recommended = "flm";
9534
+ } else if (ollamaInstalled) {
9535
+ recommended = "ollama";
9536
+ } else if (profile.recommendedRunner === "fastflowlm") {
9537
+ recommended = "flm";
9538
+ }
9539
+ const defaultUrl = recommended === "ollama" ? "http://localhost:11434/v1" : DEFAULT_LLM_URL;
9540
+ jsonOut2({
9541
+ runners,
9542
+ recommended,
9543
+ defaultUrl,
9544
+ defaultModel: profile.recommendedModel || DEFAULT_LLM_MODEL
9545
+ });
9546
+ });
9547
+ var UI_WRITABLE_SETTINGS = /* @__PURE__ */ new Set([
9548
+ "llm.enabled",
9549
+ "llm.vision.enabled",
9550
+ "system.locale"
9551
+ ]);
9552
+ bridgeCommand.command("setting-set").description("Set a single allowlisted ZAM setting value (JSON)").requiredOption("--key <key>", "Setting key").requiredOption("--value <value>", "Setting value").action(async (opts) => {
9553
+ if (!UI_WRITABLE_SETTINGS.has(opts.key)) {
9554
+ jsonError(
9555
+ `Setting "${opts.key}" is not writable via setting-set. Allowed: ${[...UI_WRITABLE_SETTINGS].join(", ")}.`
9556
+ );
9557
+ }
9558
+ await withDb2(async (db) => {
9559
+ await setSetting(db, opts.key, opts.value);
9560
+ jsonOut2({ ok: true, key: opts.key, value: opts.value });
9561
+ });
9562
+ });
8911
9563
  bridgeCommand.command("check-vision").description(
8912
9564
  "Check if UI observer vision analysis is enabled and ready (JSON)"
8913
9565
  ).action(async () => {
@@ -8954,7 +9606,10 @@ bridgeCommand.command("translate-question").description("Translate a question dy
8954
9606
  });
8955
9607
  bridgeCommand.command("evaluate-answer").description(
8956
9608
  "Evaluate the learner's active-recall answer using the local LLM (JSON)"
8957
- ).requiredOption("--slug <slug>", "Token slug").requiredOption("--concept <concept>", "Target concept text").requiredOption("--domain <domain>", "Token domain").requiredOption("--bloom-level <level>", "Bloom taxonomy level").requiredOption("--question <question>", "Question prompt presented").requiredOption("--user-answer <answer>", "User's typed answer").option("--context <context>", "Optional token context details").option("--source-link <link>", "Optional source link").action(async (opts) => {
9609
+ ).requiredOption("--slug <slug>", "Token slug").requiredOption("--concept <concept>", "Target concept text").requiredOption("--domain <domain>", "Token domain").requiredOption("--bloom-level <level>", "Bloom taxonomy level").requiredOption("--question <question>", "Question prompt presented").requiredOption("--user-answer <answer>", "User's typed answer").option("--context <context>", "Optional token context details").option("--source-link <link>", "Optional source link").option(
9610
+ "--source-content <content>",
9611
+ "Pre-resolved source reference content (skips re-fetch when set)"
9612
+ ).action(async (opts) => {
8958
9613
  await withDb2(async (db) => {
8959
9614
  const isEnabled = await getSetting(db, "llm.enabled") === "true";
8960
9615
  if (!isEnabled) {
@@ -8965,8 +9620,8 @@ bridgeCommand.command("evaluate-answer").description(
8965
9620
  });
8966
9621
  return;
8967
9622
  }
8968
- let resolvedContextContent = null;
8969
- if (opts.sourceLink) {
9623
+ let resolvedContextContent = opts.sourceContent ?? null;
9624
+ if (resolvedContextContent == null && opts.sourceLink) {
8970
9625
  try {
8971
9626
  const resolved = await resolveReviewContext(opts.sourceLink);
8972
9627
  resolvedContextContent = resolved?.content ?? null;
@@ -8974,7 +9629,7 @@ bridgeCommand.command("evaluate-answer").description(
8974
9629
  }
8975
9630
  }
8976
9631
  try {
8977
- const evaluation = await evaluateAnswerViaLLM(db, {
9632
+ const result = await evaluateAnswerViaLLM(db, {
8978
9633
  slug: opts.slug,
8979
9634
  concept: opts.concept,
8980
9635
  domain: opts.domain,
@@ -8984,7 +9639,11 @@ bridgeCommand.command("evaluate-answer").description(
8984
9639
  userAnswer: opts.userAnswer,
8985
9640
  sourceLinkContent: resolvedContextContent
8986
9641
  });
8987
- jsonOut2({ success: true, evaluation });
9642
+ jsonOut2({
9643
+ success: true,
9644
+ evaluation: result.text,
9645
+ evaluationModel: result.model
9646
+ });
8988
9647
  } catch (err) {
8989
9648
  jsonOut2({
8990
9649
  success: false,
@@ -9223,8 +9882,8 @@ bridgeCommand.command("serve").description("Start the persistent JSON-RPC stdin/
9223
9882
  });
9224
9883
 
9225
9884
  // src/cli/commands/card.ts
9226
- import { Command as Command5 } from "commander";
9227
- var cardCommand = new Command5("card").description(
9885
+ import { Command as Command6 } from "commander";
9886
+ var cardCommand = new Command6("card").description(
9228
9887
  "Manage spaced-repetition cards"
9229
9888
  );
9230
9889
  cardCommand.command("due").description("Show due tokens for a user").option("--user <id>", "User ID (default: whoami)").option("--domain <domain>", "Filter by knowledge domain").option("--json", "Output as JSON").option("--summary", "Show only counts per domain (no slugs or concepts)").action(async (opts) => {
@@ -9422,9 +10081,9 @@ cardCommand.command("delete").description("Delete one user's card for a token").
9422
10081
  });
9423
10082
 
9424
10083
  // src/cli/commands/connector.ts
9425
- import { input as input2, password } from "@inquirer/prompts";
9426
- import { Command as Command6 } from "commander";
9427
- var connectorCommand = new Command6("connector").description(
10084
+ import { input as input2, password as password2 } from "@inquirer/prompts";
10085
+ import { Command as Command7 } from "commander";
10086
+ var connectorCommand = new Command7("connector").description(
9428
10087
  "Manage external service connectors"
9429
10088
  );
9430
10089
  connectorCommand.command("setup").description("Configure a connector").argument("<type>", "Connector type (ado, turso)").option("--url <url>", "Turso database URL (non-interactive)").option("--token <token>", "Turso auth token (non-interactive)").option(
@@ -9449,7 +10108,7 @@ connectorCommand.command("setup").description("Configure a connector").argument(
9449
10108
  const project = await input2({
9450
10109
  message: "Project name:"
9451
10110
  });
9452
- const pat = await password({
10111
+ const pat = await password2({
9453
10112
  message: "Personal Access Token:"
9454
10113
  });
9455
10114
  if (!orgUrl || !project || !pat) {
@@ -9540,7 +10199,7 @@ async function setupTurso(urlArg, tokenArg, mode) {
9540
10199
  const url = urlArg ?? await input2({
9541
10200
  message: "Turso database URL (e.g. libsql://my-db-user.turso.io):"
9542
10201
  });
9543
- const token = tokenArg ?? await password({
10202
+ const token = tokenArg ?? await password2({
9544
10203
  message: "Auth token:"
9545
10204
  });
9546
10205
  if (!url || !token) {
@@ -9569,7 +10228,7 @@ async function setupTurso(urlArg, tokenArg, mode) {
9569
10228
  import { execSync as execSync5 } from "child_process";
9570
10229
  import { chmodSync as chmodSync2, existsSync as existsSync18, writeFileSync as writeFileSync9 } from "fs";
9571
10230
  import { join as join18 } from "path";
9572
- import { Command as Command7 } from "commander";
10231
+ import { Command as Command8 } from "commander";
9573
10232
  function installHook2() {
9574
10233
  const gitDir = join18(process.cwd(), ".git");
9575
10234
  if (!existsSync18(gitDir)) {
@@ -9599,7 +10258,7 @@ zam git-sync --commit HEAD --quiet
9599
10258
  process.exit(1);
9600
10259
  }
9601
10260
  }
9602
- var gitSyncCommand = new Command7("git-sync").description("Sync learning cards with recent Git file modifications").option("--commit <hash>", "Git commit hash to check", "HEAD").option("--user <id>", "User ID (default: whoami)").option("--install", "Install git post-commit hook in current repo").option("--quiet", "Suppress verbose output").action(async (opts) => {
10261
+ var gitSyncCommand = new Command8("git-sync").description("Sync learning cards with recent Git file modifications").option("--commit <hash>", "Git commit hash to check", "HEAD").option("--user <id>", "User ID (default: whoami)").option("--install", "Install git post-commit hook in current repo").option("--quiet", "Suppress verbose output").action(async (opts) => {
9603
10262
  if (opts.install) {
9604
10263
  installHook2();
9605
10264
  return;
@@ -9691,7 +10350,7 @@ ZAM Auto-Stale Complete: Scanned ${changedFiles.length} file(s).`
9691
10350
  import { existsSync as existsSync19, mkdirSync as mkdirSync11 } from "fs";
9692
10351
  import { resolve as resolve6 } from "path";
9693
10352
  import { input as input3 } from "@inquirer/prompts";
9694
- import { Command as Command8 } from "commander";
10353
+ import { Command as Command9 } from "commander";
9695
10354
  async function resolveGoalsDir() {
9696
10355
  let goalsDir;
9697
10356
  let db;
@@ -9704,7 +10363,7 @@ async function resolveGoalsDir() {
9704
10363
  }
9705
10364
  return goalsDir ? resolve6(goalsDir) : resolve6("goals");
9706
10365
  }
9707
- var goalCommand = new Command8("goal").description(
10366
+ var goalCommand = new Command9("goal").description(
9708
10367
  "Manage learning goals (markdown files)"
9709
10368
  );
9710
10369
  goalCommand.command("list").description("List all goals").option(
@@ -9877,7 +10536,7 @@ import { existsSync as existsSync20, mkdirSync as mkdirSync12, writeFileSync as
9877
10536
  import { homedir as homedir11 } from "os";
9878
10537
  import { join as join19, resolve as resolve7 } from "path";
9879
10538
  import { confirm as confirm2, input as input4 } from "@inquirer/prompts";
9880
- import { Command as Command9 } from "commander";
10539
+ import { Command as Command10 } from "commander";
9881
10540
  var HOME2 = homedir11();
9882
10541
  function printLine(char = "\u2550", len = 60, color = "\x1B[36m") {
9883
10542
  console.log(`${color}${char.repeat(len)}\x1B[0m`);
@@ -9914,7 +10573,7 @@ Here, I declare the core concepts and principles I want to master.
9914
10573
  );
9915
10574
  }
9916
10575
  }
9917
- var initCommand = new Command9("init").description("Launch the guided interactive onboarding wizard").action(async () => {
10576
+ var initCommand = new Command10("init").description("Launch the guided interactive onboarding wizard").action(async () => {
9918
10577
  printLine();
9919
10578
  console.log(
9920
10579
  "\x1B[1m\x1B[32m ZAM \u2014 The Symbiotic Learning Agent Onboarding\x1B[0m"
@@ -10090,7 +10749,7 @@ var initCommand = new Command9("init").description("Launch the guided interactiv
10090
10749
 
10091
10750
  // src/cli/commands/learn.ts
10092
10751
  import { input as input6 } from "@inquirer/prompts";
10093
- import { Command as Command10 } from "commander";
10752
+ import { Command as Command11 } from "commander";
10094
10753
 
10095
10754
  // src/cli/learn-format.ts
10096
10755
  var BLOOM_VERBS3 = {
@@ -10428,7 +11087,7 @@ var STOP_WORDS = /* @__PURE__ */ new Set(["q", ":q", "quit", "stop"]);
10428
11087
  function isExitPrompt(err) {
10429
11088
  return err instanceof Error && err.name === "ExitPromptError";
10430
11089
  }
10431
- var learnCommand = new Command10("learn").description(
11090
+ var learnCommand = new Command11("learn").description(
10432
11091
  "Run a spoiler-free, in-process learning session (recall \u2192 reveal \u2192 self-rate)"
10433
11092
  ).option("--user <id>", "User ID (default: whoami)").option("--max-new <n>", "Maximum new cards", "10").option("--max-reviews <n>", "Maximum review cards", "50").option("--no-resolve", "Skip resolving source_link into the revealed answer").action(async (opts) => {
10434
11093
  let db;
@@ -10484,7 +11143,7 @@ ${t(locale, "welcome", { count: queue.items.length })}`);
10484
11143
  question: item.question
10485
11144
  });
10486
11145
  if (healed) {
10487
- resolvedQuestion = healed;
11146
+ resolvedQuestion = healed.question;
10488
11147
  }
10489
11148
  } catch {
10490
11149
  }
@@ -10549,7 +11208,8 @@ ${"\u2500".repeat(50)}`);
10549
11208
  `
10550
11209
  ${t(locale, "feedback_title", { line: "\u2500".repeat(34) })}`
10551
11210
  );
10552
- for (const line of evaluation.split("\n")) {
11211
+ console.log(` \x1B[2m[${evaluation.model}]\x1B[0m`);
11212
+ for (const line of evaluation.text.split("\n")) {
10553
11213
  console.log(` ${line}`);
10554
11214
  }
10555
11215
  } catch (err) {
@@ -10672,8 +11332,8 @@ learnCommand.command("open").description("Open a new terminal window running zam
10672
11332
  });
10673
11333
 
10674
11334
  // src/cli/commands/monitor.ts
10675
- import { Command as Command11 } from "commander";
10676
- var monitorCommand = new Command11("monitor").description(
11335
+ import { Command as Command12 } from "commander";
11336
+ var monitorCommand = new Command12("monitor").description(
10677
11337
  "Shell observation for real-time task monitoring"
10678
11338
  );
10679
11339
  monitorCommand.command("start").description("Output shell hook code to install monitoring").requiredOption("--session <id>", "Session ID to monitor").option(
@@ -10832,497 +11492,148 @@ monitorCommand.command("open").description("Open a new monitored terminal window
10832
11492
  process.exit(1);
10833
11493
  }
10834
11494
  if (session.completed_at) {
10835
- console.error(`Error: Session already completed: ${opts.session}`);
10836
- process.exit(1);
10837
- }
10838
- if (!await getSetting(db, "monitor_method")) {
10839
- await setSetting(db, "monitor_method", "terminal");
10840
- }
10841
- } catch (err) {
10842
- console.error(`Error: ${err.message}`);
10843
- process.exit(1);
10844
- } finally {
10845
- await db?.close();
10846
- }
10847
- const dir = opts.dir ?? process.cwd();
10848
- const shellSetup = buildMonitorSetupCommand(dir, opts.session, shell);
10849
- openTerminalWindow({
10850
- shellSetup,
10851
- label: `monitor-${opts.session}`,
10852
- dir,
10853
- shell
10854
- });
10855
- });
10856
-
10857
- // src/cli/commands/observer.ts
10858
- import { Command as Command12 } from "commander";
10859
- var observerCommand = new Command12("observer").description(
10860
- "Configure what the UI observer may capture (Layer 2 policy)"
10861
- );
10862
- function applyObserverListChange(current, entry, op) {
10863
- const normalized = entry.trim().toLowerCase();
10864
- const list = parseObserverList(current);
10865
- const next = op === "add" ? [.../* @__PURE__ */ new Set([...list, normalized])] : list.filter((item) => item !== normalized);
10866
- return next.join(",");
10867
- }
10868
- observerCommand.command("status").description("Show the active observer policy").option("--json", "Output as JSON").action(async (opts) => {
10869
- await withDb(async (db) => {
10870
- const policy = await resolveObserverPolicy(db);
10871
- if (opts.json) {
10872
- console.log(JSON.stringify(policy, null, 2));
10873
- return;
10874
- }
10875
- console.log("Observer policy:\n");
10876
- console.log(` Scope: ${policy.scope}`);
10877
- console.log(` Consent: ${policy.consent}`);
10878
- console.log(` Retention: ${policy.retention}`);
10879
- console.log(
10880
- ` Allowlist: ${policy.allowlist.length ? policy.allowlist.join(", ") : "(any targeted window)"}`
10881
- );
10882
- console.log(
10883
- ` Denylist: ${policy.denylist.length ? policy.denylist.join(", ") : "(none)"}`
10884
- );
10885
- console.log(` Redact titles: ${policy.redactWindowTitles}`);
10886
- console.log(
10887
- "\nBuilt-in sensitive surfaces (password managers, auth/UAC dialogs, banking) are always refused and cannot be allowlisted."
10888
- );
10889
- });
10890
- });
10891
- observerCommand.command("grant <process>").description(
10892
- "Allow the observer to capture a process (adds it to observer.allowlist)"
10893
- ).action(async (processName) => {
10894
- await withDb(async (db) => {
10895
- const next = applyObserverListChange(
10896
- await getSetting(db, "observer.allowlist"),
10897
- processName,
10898
- "add"
10899
- );
10900
- await setSetting(db, "observer.allowlist", next);
10901
- await syncObserverSidecarPolicy(db);
10902
- console.log(`Granted: ${processName.trim().toLowerCase()}`);
10903
- console.log(`observer.allowlist = ${next || "(empty)"}`);
10904
- });
10905
- });
10906
- observerCommand.command("revoke <process>").description("Remove a process from observer.allowlist").action(async (processName) => {
10907
- await withDb(async (db) => {
10908
- const next = applyObserverListChange(
10909
- await getSetting(db, "observer.allowlist"),
10910
- processName,
10911
- "remove"
10912
- );
10913
- await setSetting(db, "observer.allowlist", next);
10914
- await syncObserverSidecarPolicy(db);
10915
- console.log(`Revoked: ${processName.trim().toLowerCase()}`);
10916
- console.log(`observer.allowlist = ${next || "(empty)"}`);
10917
- });
10918
- });
10919
-
10920
- // src/cli/commands/profile.ts
10921
- import { homedir as homedir12 } from "os";
10922
- import { dirname as dirname6, join as join20, resolve as resolve8 } from "path";
10923
- import { Command as Command13 } from "commander";
10924
- var C2 = {
10925
- reset: "\x1B[0m",
10926
- bold: "\x1B[1m",
10927
- cyan: "\x1B[36m",
10928
- dim: "\x1B[2m",
10929
- green: "\x1B[32m"
10930
- };
10931
- function defaultPersonalDir() {
10932
- return join20(homedir12(), "Documents", "zam");
10933
- }
10934
- function render(profile) {
10935
- const sync = profile.syncProvider ? `${C2.green}${profile.syncProvider}${C2.reset} ${C2.dim}(good for cross-device snapshots)${C2.reset}` : `${C2.dim}local folder (use a synced folder or snapshots to move between machines)${C2.reset}`;
10936
- console.log(`${C2.bold}ZAM install profile${C2.reset}`);
10937
- console.log(` mode: ${C2.cyan}${profile.mode}${C2.reset}`);
10938
- console.log(` personal dir: ${C2.cyan}${profile.personalDir}${C2.reset}`);
10939
- console.log(` sync: ${sync}`);
10940
- console.log(` data dir: ${C2.cyan}${profile.dataDir}${C2.reset}`);
10941
- console.log(` database: ${C2.cyan}${profile.dbPath}${C2.reset}`);
10942
- }
10943
- var profileCommand = new Command13("profile").description("Show or change this machine's ZAM install profile").option("--mode <mode>", "Set install mode: developer | default").option("--dir <path>", "Set the personal-content folder").option("--json", "Output as JSON").action(async (opts) => {
10944
- if (opts.mode && opts.mode !== "developer" && opts.mode !== "default") {
10945
- console.error(`Invalid --mode: ${opts.mode}. Use developer or default.`);
10946
- process.exit(1);
10947
- }
10948
- let db;
10949
- try {
10950
- if (opts.mode) setInstallMode(opts.mode);
10951
- db = await openDatabaseWithSync({ initialize: true });
10952
- if (opts.dir) {
10953
- await setSetting(db, "personal.workspace_dir", resolve8(opts.dir));
10954
- }
10955
- const personalDir = await getSetting(db, "personal.workspace_dir") || defaultPersonalDir();
10956
- await db.close();
10957
- db = void 0;
10958
- const dbPath = getDefaultDbPath();
10959
- const profile = {
10960
- mode: getInstallMode(),
10961
- personalDir,
10962
- syncProvider: detectSyncProvider(personalDir),
10963
- dataDir: dirname6(dbPath),
10964
- dbPath
10965
- };
10966
- if (opts.json) {
10967
- console.log(JSON.stringify(profile, null, 2));
10968
- return;
10969
- }
10970
- render(profile);
10971
- } catch (err) {
10972
- await db?.close();
10973
- console.error("Error:", err.message);
10974
- process.exit(1);
10975
- }
10976
- });
10977
-
10978
- // src/cli/commands/provider.ts
10979
- import { password as password2 } from "@inquirer/prompts";
10980
- import { Command as Command14 } from "commander";
10981
- var VALID_API_FLAVORS = [
10982
- "chat-completions",
10983
- "anthropic-messages"
10984
- ];
10985
- var VALID_ROLES = ["vision", "recall", "text"];
10986
- function upsertProviderRecord(providers, name, patch) {
10987
- const merged = { ...providers[name] ?? {} };
10988
- if (patch.url !== void 0) merged.url = patch.url;
10989
- if (patch.model !== void 0) merged.model = patch.model;
10990
- if (patch.apiFlavor !== void 0) merged.apiFlavor = patch.apiFlavor;
10991
- if (patch.apiKeyRef !== void 0) merged.apiKeyRef = patch.apiKeyRef;
10992
- if (patch.label !== void 0) merged.label = patch.label;
10993
- if (patch.local !== void 0) merged.local = patch.local;
10994
- if (patch.runner !== void 0) merged.runner = patch.runner;
10995
- return { ...providers, [name]: merged };
10996
- }
10997
- function removeProviderRecord(providers, name) {
10998
- if (!(name in providers)) return { providers, removed: false };
10999
- const next = { ...providers };
11000
- delete next[name];
11001
- return { providers: next, removed: true };
11002
- }
11003
- function rolesReferencing(roles, name) {
11004
- return VALID_ROLES.filter((role) => {
11005
- const binding = roles[role];
11006
- return binding?.primary === name || binding?.fallback === name;
11007
- });
11008
- }
11009
- function bindRoleProviders(roles, role, primary, fallback) {
11010
- const binding = { primary };
11011
- if (fallback) binding.fallback = fallback;
11012
- return { ...roles, [role]: binding };
11013
- }
11014
- function maskSecret(key) {
11015
- return key.length <= 4 ? "\u2022\u2022\u2022\u2022" : `\u2026${key.slice(-4)}`;
11016
- }
11017
- function buildProviderListing(providers, hasKey) {
11018
- return Object.entries(providers).map(([name, rec]) => {
11019
- const apiFlavor = rec.apiFlavor ?? (rec.url ? inferApiFlavor(rec.url) : "chat-completions");
11020
- let keyState;
11021
- if (!rec.apiKeyRef) keyState = "none";
11022
- else keyState = hasKey(rec.apiKeyRef) ? "set" : "missing";
11023
- const row = {
11024
- name,
11025
- url: rec.url,
11026
- model: rec.model,
11027
- apiFlavor,
11028
- apiKeyRef: rec.apiKeyRef,
11029
- keyState
11030
- };
11031
- if (rec.label !== void 0) row.label = rec.label;
11032
- if (rec.local !== void 0) row.local = rec.local;
11033
- if (rec.runner !== void 0) row.runner = rec.runner;
11034
- return row;
11035
- });
11036
- }
11037
- function findOrphanKeyRefs(storedRefs, providers) {
11038
- const used = new Set(
11039
- Object.values(providers).map((rec) => rec.apiKeyRef).filter((ref) => !!ref)
11040
- );
11041
- return storedRefs.filter((ref) => !used.has(ref));
11042
- }
11043
- async function readJson(db, key, fallback) {
11044
- const raw = await getSetting(db, key);
11045
- if (!raw) return fallback;
11046
- try {
11047
- return JSON.parse(raw);
11048
- } catch {
11049
- return fallback;
11050
- }
11051
- }
11052
- var readProviders = (db) => readJson(db, "llm.providers", {});
11053
- var readRoles = (db) => readJson(db, "llm.roles", {});
11054
- async function readScopedProviders(db, machine) {
11055
- if (machine) return getMachineAiConfig().providers ?? {};
11056
- if (!db)
11057
- throw new Error("Database is required for shared provider settings.");
11058
- return readProviders(db);
11059
- }
11060
- async function readScopedRoles(db, machine) {
11061
- if (machine) return getMachineAiConfig().roles ?? {};
11062
- if (!db)
11063
- throw new Error("Database is required for shared provider settings.");
11064
- return readRoles(db);
11065
- }
11066
- async function writeScopedProviders(db, machine, p) {
11067
- if (machine) {
11068
- const config = getMachineAiConfig();
11069
- saveMachineAiConfig({ ...config, providers: p });
11070
- return;
11071
- }
11072
- if (!db)
11073
- throw new Error("Database is required for shared provider settings.");
11074
- await setSetting(db, "llm.providers", JSON.stringify(p));
11075
- }
11076
- async function writeScopedRoles(db, machine, r) {
11077
- if (machine) {
11078
- const config = getMachineAiConfig();
11079
- saveMachineAiConfig({ ...config, roles: r });
11080
- return;
11081
- }
11082
- if (!db)
11083
- throw new Error("Database is required for shared provider settings.");
11084
- await setSetting(db, "llm.roles", JSON.stringify(r));
11085
- }
11086
- async function withProviderScope(machine, action) {
11087
- if (machine) {
11088
- await action(void 0);
11089
- return;
11090
- }
11091
- await withDb(action);
11092
- }
11093
- var providerCommand = new Command14("provider").description(
11094
- "Configure role-based AI providers (url/model/flavor/key, per role)"
11095
- );
11096
- providerCommand.command("list").description("Show configured providers, role bindings, and key status").option("--json", "Output as JSON").option("--machine", "Read machine-local providers from ~/.zam/config.json").action(async (opts) => {
11097
- const machine = Boolean(opts.machine);
11098
- await withProviderScope(machine, async (db) => {
11099
- const providers = await readScopedProviders(db, machine);
11100
- const roles = await readScopedRoles(db, machine);
11101
- const rows = buildProviderListing(
11102
- providers,
11103
- (ref) => getProviderApiKey(ref) !== null
11104
- );
11105
- const orphans = findOrphanKeyRefs(listProviderApiKeyRefs(), providers);
11106
- if (opts.json) {
11107
- console.log(
11108
- JSON.stringify(
11109
- {
11110
- scope: machine ? "machine" : "shared",
11111
- providers: rows,
11112
- roles,
11113
- orphans
11114
- },
11115
- null,
11116
- 2
11117
- )
11118
- );
11119
- return;
11120
- }
11121
- console.log(
11122
- `Providers (${opts.machine ? "~/.zam/config.json ai.providers" : "llm.providers"}):
11123
- `
11124
- );
11125
- if (rows.length === 0) {
11126
- console.log(
11127
- " (none) \u2014 add one: zam provider add <name> --url <url> --model <model>"
11128
- );
11129
- } else {
11130
- for (const row of rows) {
11131
- const key = row.keyState === "set" ? "\x1B[32mkey \u2713\x1B[0m" : row.keyState === "missing" ? `\x1B[31mkey \u2717 (set: zam provider set-key ${row.apiKeyRef})\x1B[0m` : "\x1B[90mno key\x1B[0m";
11132
- console.log(` \x1B[36m${row.name.padEnd(12)}\x1B[0m ${key}`);
11133
- console.log(` url: ${row.url ?? "(inherits llm.url)"}`);
11134
- console.log(` model: ${row.model ?? "(inherits llm.model)"}`);
11135
- console.log(` flavor: ${row.apiFlavor}`);
11136
- if (row.label) console.log(` label: ${row.label}`);
11137
- if (row.local !== void 0) {
11138
- console.log(` local: ${row.local ? "yes" : "no"}`);
11139
- }
11140
- if (row.runner) console.log(` runner: ${row.runner}`);
11141
- if (row.apiKeyRef) console.log(` key-ref: ${row.apiKeyRef}`);
11142
- }
11143
- }
11144
- console.log("\nRoles (llm.roles):\n");
11145
- for (const role of VALID_ROLES) {
11146
- const binding = roles[role];
11147
- if (!binding?.primary) {
11148
- console.log(` ${role.padEnd(7)} \x1B[90m(unset)\x1B[0m`);
11149
- } else {
11150
- const fb = binding.fallback ? ` \u2192 fallback: ${binding.fallback}` : "";
11151
- console.log(` ${role.padEnd(7)} primary: ${binding.primary}${fb}`);
11152
- }
11153
- }
11154
- if (orphans.length > 0) {
11155
- console.log(
11156
- `
11157
- \x1B[33mOrphan keys (stored but unreferenced):\x1B[0m ${orphans.join(", ")}`
11158
- );
11495
+ console.error(`Error: Session already completed: ${opts.session}`);
11496
+ process.exit(1);
11497
+ }
11498
+ if (!await getSetting(db, "monitor_method")) {
11499
+ await setSetting(db, "monitor_method", "terminal");
11159
11500
  }
11501
+ } catch (err) {
11502
+ console.error(`Error: ${err.message}`);
11503
+ process.exit(1);
11504
+ } finally {
11505
+ await db?.close();
11506
+ }
11507
+ const dir = opts.dir ?? process.cwd();
11508
+ const shellSetup = buildMonitorSetupCommand(dir, opts.session, shell);
11509
+ openTerminalWindow({
11510
+ shellSetup,
11511
+ label: `monitor-${opts.session}`,
11512
+ dir,
11513
+ shell
11160
11514
  });
11161
11515
  });
11162
- providerCommand.command("add <name>").description("Add or update a provider record in llm.providers").option("--label <label>", "Human-readable provider label").option("--url <url>", "Endpoint base URL (e.g. https://api.deepseek.com/v1)").option("--model <model>", "Model id (e.g. deepseek-v4-flash)").option("--local", "Mark this provider as a local/on-device endpoint").option(
11163
- "--runner <runner>",
11164
- "Local runner hint (flm, foundry-local, ollama, ...)"
11165
- ).option(
11166
- "--flavor <flavor>",
11167
- `Wire protocol: ${VALID_API_FLAVORS.join(" | ")} (default: inferred from URL)`
11168
- ).option(
11169
- "--key-ref <ref>",
11170
- "Credential reference for the API key (default: <name> when --key is given)"
11171
- ).option(
11172
- "--key <value>",
11173
- "Store this API key now (prefer `set-key` to keep it out of shell history)"
11174
- ).option("--machine", "Store this provider in ~/.zam/config.json").action(async (name, opts) => {
11175
- let apiFlavor;
11176
- if (opts.flavor) {
11177
- if (!VALID_API_FLAVORS.includes(opts.flavor)) {
11178
- console.error(
11179
- `Invalid --flavor: ${opts.flavor}. Use ${VALID_API_FLAVORS.join(" or ")}.`
11180
- );
11181
- process.exit(1);
11516
+
11517
+ // src/cli/commands/observer.ts
11518
+ import { Command as Command13 } from "commander";
11519
+ var observerCommand = new Command13("observer").description(
11520
+ "Configure what the UI observer may capture (Layer 2 policy)"
11521
+ );
11522
+ function applyObserverListChange(current, entry, op) {
11523
+ const normalized = entry.trim().toLowerCase();
11524
+ const list = parseObserverList(current);
11525
+ const next = op === "add" ? [.../* @__PURE__ */ new Set([...list, normalized])] : list.filter((item) => item !== normalized);
11526
+ return next.join(",");
11527
+ }
11528
+ observerCommand.command("status").description("Show the active observer policy").option("--json", "Output as JSON").action(async (opts) => {
11529
+ await withDb(async (db) => {
11530
+ const policy = await resolveObserverPolicy(db);
11531
+ if (opts.json) {
11532
+ console.log(JSON.stringify(policy, null, 2));
11533
+ return;
11182
11534
  }
11183
- apiFlavor = opts.flavor;
11184
- }
11185
- const apiKeyRef = opts.keyRef ?? (opts.key ? name : void 0);
11186
- const machine = Boolean(opts.machine);
11187
- await withProviderScope(machine, async (db) => {
11188
- const providers = await readScopedProviders(db, machine);
11189
- const next = upsertProviderRecord(providers, name, {
11190
- label: opts.label,
11191
- url: opts.url,
11192
- model: opts.model,
11193
- apiFlavor,
11194
- apiKeyRef,
11195
- local: opts.local ? true : void 0,
11196
- runner: opts.runner
11197
- });
11198
- await writeScopedProviders(db, machine, next);
11199
- if (opts.key && apiKeyRef) setProviderApiKey(apiKeyRef, opts.key);
11200
- const rec = next[name];
11535
+ console.log("Observer policy:\n");
11536
+ console.log(` Scope: ${policy.scope}`);
11537
+ console.log(` Consent: ${policy.consent}`);
11538
+ console.log(` Retention: ${policy.retention}`);
11201
11539
  console.log(
11202
- `Provider "${name}" saved (${machine ? "machine-local" : "shared"}):`
11540
+ ` Allowlist: ${policy.allowlist.length ? policy.allowlist.join(", ") : "(any targeted window)"}`
11203
11541
  );
11204
- console.log(` url: ${rec.url ?? "(inherits llm.url)"}`);
11205
- console.log(` model: ${rec.model ?? "(inherits llm.model)"}`);
11206
- if (rec.label) console.log(` label: ${rec.label}`);
11207
- if (rec.local !== void 0) {
11208
- console.log(` local: ${rec.local ? "yes" : "no"}`);
11209
- }
11210
- if (rec.runner) console.log(` runner: ${rec.runner}`);
11211
11542
  console.log(
11212
- ` flavor: ${rec.apiFlavor ?? `${rec.url ? inferApiFlavor(rec.url) : "chat-completions"} (inferred)`}`
11543
+ ` Denylist: ${policy.denylist.length ? policy.denylist.join(", ") : "(none)"}`
11213
11544
  );
11214
- console.log(` key-ref: ${rec.apiKeyRef ?? "(none \u2014 uses default key)"}`);
11215
- if (opts.key) {
11216
- console.log(` key: stored (${maskSecret(opts.key)})`);
11217
- } else if (rec.apiKeyRef && getProviderApiKey(rec.apiKeyRef) === null) {
11218
- console.log(
11219
- `
11220
- \u26A0 No key stored for "${rec.apiKeyRef}". Run: zam provider set-key ${rec.apiKeyRef}`
11221
- );
11222
- }
11223
- if (!rec.url) {
11224
- console.log(
11225
- "\n \u26A0 No --url set; this provider inherits the base llm.url."
11226
- );
11227
- }
11545
+ console.log(` Redact titles: ${policy.redactWindowTitles}`);
11228
11546
  console.log(
11229
- `
11230
- Bind it to a role: zam provider use recall --primary ${name}`
11547
+ "\nBuilt-in sensitive surfaces (password managers, auth/UAC dialogs, banking) are always refused and cannot be allowlisted."
11231
11548
  );
11232
11549
  });
11233
11550
  });
11234
- providerCommand.command("remove <name>").description("Remove a provider record from llm.providers").option("--machine", "Remove from ~/.zam/config.json instead of the DB").action(async (name, opts) => {
11235
- const machine = Boolean(opts.machine);
11236
- await withProviderScope(machine, async (db) => {
11237
- const providers = await readScopedProviders(db, machine);
11238
- const { providers: next, removed } = removeProviderRecord(
11239
- providers,
11240
- name
11241
- );
11242
- if (!removed) {
11243
- console.log(`No such provider: ${name}`);
11244
- return;
11245
- }
11246
- await writeScopedProviders(db, machine, next);
11247
- console.log(
11248
- `Removed provider "${name}" (${machine ? "machine-local" : "shared"}).`
11249
- );
11250
- const referencing = rolesReferencing(
11251
- await readScopedRoles(db, machine),
11252
- name
11551
+ observerCommand.command("grant <process>").description(
11552
+ "Allow the observer to capture a process (adds it to observer.allowlist)"
11553
+ ).action(async (processName) => {
11554
+ await withDb(async (db) => {
11555
+ const next = applyObserverListChange(
11556
+ await getSetting(db, "observer.allowlist"),
11557
+ processName,
11558
+ "add"
11253
11559
  );
11254
- if (referencing.length > 0) {
11255
- console.log(
11256
- ` \u26A0 Still referenced by role(s): ${referencing.join(", ")} \u2014 rebind with: zam provider use <role> --primary <name>`
11257
- );
11258
- }
11560
+ await setSetting(db, "observer.allowlist", next);
11561
+ await syncObserverSidecarPolicy(db);
11562
+ console.log(`Granted: ${processName.trim().toLowerCase()}`);
11563
+ console.log(`observer.allowlist = ${next || "(empty)"}`);
11259
11564
  });
11260
11565
  });
11261
- providerCommand.command("use <role>").description(`Bind providers to a role (${VALID_ROLES.join(" | ")})`).option("--primary <name>", "Primary provider").option("--fallback <name>", "Fallback provider (optional)").option("--machine", "Bind the role in ~/.zam/config.json").action(async (role, opts) => {
11262
- if (!VALID_ROLES.includes(role)) {
11263
- console.error(`Invalid role: ${role}. Use ${VALID_ROLES.join(", ")}.`);
11264
- process.exit(1);
11265
- }
11266
- if (!opts.primary) {
11267
- console.error("--primary is required.");
11268
- process.exit(1);
11269
- }
11270
- const machine = Boolean(opts.machine);
11271
- await withProviderScope(machine, async (db) => {
11272
- const providers = await readScopedProviders(db, machine);
11273
- await writeScopedRoles(
11274
- db,
11275
- machine,
11276
- bindRoleProviders(
11277
- await readScopedRoles(db, machine),
11278
- role,
11279
- opts.primary,
11280
- opts.fallback
11281
- )
11282
- );
11283
- const fb = opts.fallback ? `, fallback: ${opts.fallback}` : "";
11284
- console.log(
11285
- `Role "${role}" \u2192 primary: ${opts.primary}${fb} (${machine ? "machine-local" : "shared"})`
11566
+ observerCommand.command("revoke <process>").description("Remove a process from observer.allowlist").action(async (processName) => {
11567
+ await withDb(async (db) => {
11568
+ const next = applyObserverListChange(
11569
+ await getSetting(db, "observer.allowlist"),
11570
+ processName,
11571
+ "remove"
11286
11572
  );
11287
- for (const [label, ref] of [
11288
- ["primary", opts.primary],
11289
- ["fallback", opts.fallback]
11290
- ]) {
11291
- if (ref && !(ref in providers)) {
11292
- console.log(
11293
- ` \u26A0 ${label} "${ref}" is not a defined provider yet. Add it: zam provider add ${ref} --url <url> --model <model>`
11294
- );
11295
- }
11296
- }
11573
+ await setSetting(db, "observer.allowlist", next);
11574
+ await syncObserverSidecarPolicy(db);
11575
+ console.log(`Revoked: ${processName.trim().toLowerCase()}`);
11576
+ console.log(`observer.allowlist = ${next || "(empty)"}`);
11297
11577
  });
11298
11578
  });
11299
- providerCommand.command("set-key <ref>").description(
11300
- "Store an API key for a provider reference (in credentials.json)"
11301
- ).option(
11302
- "--key <value>",
11303
- "The API key (omit to enter it interactively, hidden)"
11304
- ).action(async (ref, opts) => {
11579
+
11580
+ // src/cli/commands/profile.ts
11581
+ import { homedir as homedir12 } from "os";
11582
+ import { dirname as dirname6, join as join20, resolve as resolve8 } from "path";
11583
+ import { Command as Command14 } from "commander";
11584
+ var C2 = {
11585
+ reset: "\x1B[0m",
11586
+ bold: "\x1B[1m",
11587
+ cyan: "\x1B[36m",
11588
+ dim: "\x1B[2m",
11589
+ green: "\x1B[32m"
11590
+ };
11591
+ function defaultPersonalDir() {
11592
+ return join20(homedir12(), "Documents", "zam");
11593
+ }
11594
+ function render(profile) {
11595
+ const sync = profile.syncProvider ? `${C2.green}${profile.syncProvider}${C2.reset} ${C2.dim}(good for cross-device snapshots)${C2.reset}` : `${C2.dim}local folder (use a synced folder or snapshots to move between machines)${C2.reset}`;
11596
+ console.log(`${C2.bold}ZAM install profile${C2.reset}`);
11597
+ console.log(` mode: ${C2.cyan}${profile.mode}${C2.reset}`);
11598
+ console.log(` personal dir: ${C2.cyan}${profile.personalDir}${C2.reset}`);
11599
+ console.log(` sync: ${sync}`);
11600
+ console.log(` data dir: ${C2.cyan}${profile.dataDir}${C2.reset}`);
11601
+ console.log(` database: ${C2.cyan}${profile.dbPath}${C2.reset}`);
11602
+ }
11603
+ var profileCommand = new Command14("profile").description("Show or change this machine's ZAM install profile").option("--mode <mode>", "Set install mode: developer | default").option("--dir <path>", "Set the personal-content folder").option("--json", "Output as JSON").action(async (opts) => {
11604
+ if (opts.mode && opts.mode !== "developer" && opts.mode !== "default") {
11605
+ console.error(`Invalid --mode: ${opts.mode}. Use developer or default.`);
11606
+ process.exit(1);
11607
+ }
11608
+ let db;
11305
11609
  try {
11306
- const key = opts.key ?? await password2({ message: `API key for "${ref}":` });
11307
- if (!key || key.trim().length === 0) {
11308
- console.error("No key provided.");
11309
- process.exit(1);
11610
+ if (opts.mode) setInstallMode(opts.mode);
11611
+ db = await openDatabaseWithSync({ initialize: true });
11612
+ if (opts.dir) {
11613
+ await setSetting(db, "personal.workspace_dir", resolve8(opts.dir));
11310
11614
  }
11311
- setProviderApiKey(ref, key.trim());
11312
- console.log(`Stored API key for "${ref}" (${maskSecret(key.trim())}).`);
11313
- } catch (err) {
11314
- if (err.name === "ExitPromptError") {
11315
- console.log("\nCancelled.");
11316
- process.exit(0);
11615
+ const personalDir = await getSetting(db, "personal.workspace_dir") || defaultPersonalDir();
11616
+ await db.close();
11617
+ db = void 0;
11618
+ const dbPath = getDefaultDbPath();
11619
+ const profile = {
11620
+ mode: getInstallMode(),
11621
+ personalDir,
11622
+ syncProvider: detectSyncProvider(personalDir),
11623
+ dataDir: dirname6(dbPath),
11624
+ dbPath
11625
+ };
11626
+ if (opts.json) {
11627
+ console.log(JSON.stringify(profile, null, 2));
11628
+ return;
11317
11629
  }
11630
+ render(profile);
11631
+ } catch (err) {
11632
+ await db?.close();
11318
11633
  console.error("Error:", err.message);
11319
11634
  process.exit(1);
11320
11635
  }
11321
11636
  });
11322
- providerCommand.command("clear-key <ref>").description("Remove a stored API key").action((ref) => {
11323
- clearProviderApiKey(ref);
11324
- console.log(`Cleared API key for "${ref}".`);
11325
- });
11326
11637
 
11327
11638
  // src/cli/commands/review.ts
11328
11639
  import { Command as Command15 } from "commander";