zam-core 0.5.0 → 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();
@@ -4477,6 +4492,13 @@ function upsertConfiguredWorkspace(workspace, path = defaultConfigPath()) {
4477
4492
  saveConfiguredWorkspaces(next, path);
4478
4493
  return next;
4479
4494
  }
4495
+ function removeConfiguredWorkspace(id, path = defaultConfigPath()) {
4496
+ const next = getConfiguredWorkspaces(path).filter(
4497
+ (workspace) => workspace.id !== id
4498
+ );
4499
+ saveConfiguredWorkspaces(next, path);
4500
+ return next;
4501
+ }
4480
4502
  function detectSyncProvider(dir) {
4481
4503
  const p = dir.toLowerCase();
4482
4504
  if (p.includes("onedrive")) return "OneDrive";
@@ -5352,15 +5374,37 @@ var agentCommand = new Command("agent").description("Provision and inspect the a
5352
5374
  // src/cli/commands/bridge.ts
5353
5375
  import { execFileSync as execFileSync4 } from "child_process";
5354
5376
  import { randomBytes as randomBytes2 } from "crypto";
5355
- import { existsSync as existsSync17, readdirSync as readdirSync2, readFileSync as readFileSync11, rmSync as rmSync2 } from "fs";
5377
+ import {
5378
+ existsSync as existsSync17,
5379
+ mkdirSync as mkdirSync10,
5380
+ readdirSync as readdirSync3,
5381
+ readFileSync as readFileSync11,
5382
+ rmSync as rmSync3
5383
+ } from "fs";
5356
5384
  import { homedir as homedir10, tmpdir as tmpdir3 } from "os";
5357
5385
  import { basename as basename5, join as join17, resolve as resolve5 } from "path";
5358
- import { Command as Command4 } from "commander";
5386
+ import { Command as Command5 } from "commander";
5359
5387
 
5360
5388
  // src/cli/llm/client.ts
5361
5389
  import { spawn as spawn2 } from "child_process";
5362
5390
  import { existsSync as existsSync14 } from "fs";
5363
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
+ }
5364
5408
  var DEFAULT_LLM_MODEL = "qwen3.5:4b";
5365
5409
  var DEFAULT_LLM_API_KEY = "sk-none";
5366
5410
  async function getLlmConfig(db) {
@@ -5471,6 +5515,7 @@ function materializeProvider(rec, base, role, meta) {
5471
5515
  label: rec.label,
5472
5516
  source: meta.source,
5473
5517
  local: rec.local ?? isLocalEndpoint(url),
5518
+ ...rec.runner ? { runner: rec.runner } : {},
5474
5519
  ...role === "vision" ? { maxFrames: base.maxFrames } : {}
5475
5520
  };
5476
5521
  }
@@ -5553,10 +5598,7 @@ async function readChatContent(res, label) {
5553
5598
  }
5554
5599
  async function generateQuestionViaLLM(db, input8) {
5555
5600
  const cfg = await getProviderForRole(db, "recall");
5556
- if (!cfg.enabled) {
5557
- throw new Error("LLM integration is disabled in settings (llm.enabled)");
5558
- }
5559
- assertChatCompletions(cfg);
5601
+ const endpoint = await resolveUsableRecallEndpoint(db);
5560
5602
  const bloom = input8.bloomLevel >= 1 && input8.bloomLevel <= 5 ? input8.bloomLevel : 1;
5561
5603
  const verb = BLOOM_VERBS2[bloom];
5562
5604
  const langName = LANGUAGE_NAMES[cfg.locale] || "English";
@@ -5576,31 +5618,36 @@ ${input8.sourceLinkContent ? `Source Reference:
5576
5618
  ${input8.sourceLinkContent}` : ""}
5577
5619
 
5578
5620
  Active-Recall Question:`;
5579
- const res = await fetchWithInteractiveTimeout(`${cfg.url}/chat/completions`, {
5580
- method: "POST",
5581
- headers: {
5582
- "Content-Type": "application/json",
5583
- Authorization: `Bearer ${cfg.apiKey}`
5584
- },
5585
- body: JSON.stringify({
5586
- model: cfg.model,
5587
- messages: [
5588
- { role: "system", content: systemPrompt },
5589
- { role: "user", content: userPrompt }
5590
- ],
5591
- temperature: 0.1,
5592
- max_tokens: 150
5593
- }),
5594
- locale: cfg.locale
5595
- });
5596
- 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
+ };
5597
5647
  }
5598
5648
  async function evaluateAnswerViaLLM(db, input8) {
5599
5649
  const cfg = await getProviderForRole(db, "recall");
5600
- if (!cfg.enabled) {
5601
- throw new Error("LLM integration is disabled in settings (llm.enabled)");
5602
- }
5603
- assertChatCompletions(cfg);
5650
+ const endpoint = await resolveUsableRecallEndpoint(db);
5604
5651
  const langName = LANGUAGE_NAMES[cfg.locale] || "English";
5605
5652
  const ratingPrefix = LOCALIZED_RATING_PREFIX[cfg.locale] || "Suggested rating";
5606
5653
  const systemPrompt = `You are ZAM, an extremely warm, encouraging, and patient skills trainer.
@@ -5630,51 +5677,59 @@ ${input8.sourceLinkContent ? `Source Code Reference:
5630
5677
  ${input8.sourceLinkContent}` : ""}
5631
5678
 
5632
5679
  Evaluation:`;
5633
- const res = await fetchWithInteractiveTimeout(`${cfg.url}/chat/completions`, {
5634
- method: "POST",
5635
- headers: {
5636
- "Content-Type": "application/json",
5637
- Authorization: `Bearer ${cfg.apiKey}`
5638
- },
5639
- body: JSON.stringify({
5640
- model: cfg.model,
5641
- messages: [
5642
- { role: "system", content: systemPrompt },
5643
- { role: "user", content: userPrompt }
5644
- ],
5645
- temperature: 0.2,
5646
- max_tokens: 300
5647
- }),
5648
- locale: cfg.locale
5649
- });
5650
- 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
+ };
5651
5706
  }
5652
5707
  async function translateQuestionViaLLM(db, question) {
5653
5708
  const cfg = await getProviderForRole(db, "recall");
5654
- if (!cfg.enabled) {
5655
- throw new Error("LLM integration is disabled in settings");
5656
- }
5657
- assertChatCompletions(cfg);
5709
+ const endpoint = await resolveUsableRecallEndpoint(db);
5658
5710
  const targetLang = LANGUAGE_NAMES[cfg.locale] || "English";
5659
5711
  const systemPrompt = `You are a highly precise translator. Translate the given active-recall question into clear, natural ${targetLang}.
5660
5712
  Output ONLY the raw translation. Do not include any headers, preamble, quotes, or conversational filler.`;
5661
- const res = await fetchWithInteractiveTimeout(`${cfg.url}/chat/completions`, {
5662
- method: "POST",
5663
- headers: {
5664
- "Content-Type": "application/json",
5665
- Authorization: `Bearer ${cfg.apiKey}`
5666
- },
5667
- body: JSON.stringify({
5668
- model: cfg.model,
5669
- messages: [
5670
- { role: "system", content: systemPrompt },
5671
- { role: "user", content: question }
5672
- ],
5673
- temperature: 0.1,
5674
- max_tokens: 150
5675
- }),
5676
- locale: cfg.locale
5677
- });
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
+ );
5678
5733
  return readChatContent(res, "Translation");
5679
5734
  }
5680
5735
  async function isLlmOnline(url) {
@@ -5747,6 +5802,107 @@ async function checkProviderChain(primary) {
5747
5802
  }
5748
5803
  return { primary: first };
5749
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
+ }
5750
5906
  async function isVisionProviderModelExplicit(db, active) {
5751
5907
  if (await getSetting(db, "llm.vision.model")) return true;
5752
5908
  const providers = await readJsonSetting(db, "llm.providers");
@@ -5848,13 +6004,8 @@ async function getProviderRoleStatus(db, role) {
5848
6004
  fallback: summarizeFallback(cfg.fallback)
5849
6005
  };
5850
6006
  }
5851
- let selected;
5852
- if (role === "vision") {
5853
- const chain = await checkProviderChain(cfg);
5854
- selected = chain.firstUsable ?? chain.primary;
5855
- } else {
5856
- selected = await checkProviderEndpoint(cfg);
5857
- }
6007
+ const chain = await checkProviderChain(cfg);
6008
+ const selected = chain.firstUsable ?? chain.primary;
5858
6009
  const active = selected.endpoint;
5859
6010
  const usable = selected.online && selected.modelAvailable;
5860
6011
  const reason = usable ? void 0 : selected.online ? "model-not-found" : "offline";
@@ -5876,7 +6027,35 @@ async function getProviderRoleStatus(db, role) {
5876
6027
  fallback: summarizeFallback(cfg.fallback)
5877
6028
  };
5878
6029
  }
5879
- 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
+ }
5880
6059
  let runner = "unknown";
5881
6060
  let port = "8000";
5882
6061
  try {
@@ -5892,8 +6071,8 @@ function detectRunner(url, model) {
5892
6071
  }
5893
6072
  return { runner, port };
5894
6073
  }
5895
- function spawnLocalRunner(url, model) {
5896
- const { runner, port } = detectRunner(url, model);
6074
+ function spawnLocalRunner(url, model, hint) {
6075
+ const { runner, port } = detectRunner(url, model, hint);
5897
6076
  try {
5898
6077
  if (runner === "fastflowlm") {
5899
6078
  const flmExe = existsSync14("C:\\Program Files\\flm\\flm.exe") ? "C:\\Program Files\\flm\\flm.exe" : "flm";
@@ -5914,8 +6093,8 @@ function spawnLocalRunner(url, model) {
5914
6093
  } catch {
5915
6094
  }
5916
6095
  }
5917
- async function startLocalRunner(url, model, locale) {
5918
- const { runner, port } = detectRunner(url, model);
6096
+ async function startLocalRunner(url, model, locale, hint) {
6097
+ const { runner, port } = detectRunner(url, model, hint);
5919
6098
  if (runner === "fastflowlm") {
5920
6099
  const flmExe = existsSync14("C:\\Program Files\\flm\\flm.exe") ? "C:\\Program Files\\flm\\flm.exe" : "flm";
5921
6100
  if (!hasCommand("flm") && flmExe === "flm") {
@@ -5974,7 +6153,7 @@ async function startLocalRunner(url, model, locale) {
5974
6153
  let attempts = 0;
5975
6154
  const dotsPerLine = 30;
5976
6155
  while (true) {
5977
- await new Promise((resolve8) => setTimeout(resolve8, 500));
6156
+ await new Promise((resolve9) => setTimeout(resolve9, 500));
5978
6157
  if (await isLlmOnline(url)) {
5979
6158
  if (attempts > 0) process.stdout.write("\n");
5980
6159
  return true;
@@ -5999,105 +6178,39 @@ async function startLocalRunner(url, model, locale) {
5999
6178
  }
6000
6179
  }
6001
6180
  async function ensureLocalLlmRunning(db) {
6002
- const cfg = await getProviderForRole(db, "recall");
6003
- if (!cfg.enabled) {
6004
- return { usable: false, reason: "disabled" };
6005
- }
6006
- if (cfg.apiFlavor !== "chat-completions") {
6007
- console.warn(
6008
- `\x1B[31m\u2717 Recall is configured for "${cfg.apiFlavor}", but active recall currently supports chat-completions providers only.\x1B[0m`
6009
- );
6010
- return { usable: false, reason: "unsupported-provider" };
6011
- }
6012
- const { url, model, apiKey, locale } = cfg;
6013
- const isLocal = isLocalEndpoint(url);
6014
- console.log(`Checking if local LLM server is online at ${url}...`);
6015
- let online = await isLlmOnline(url);
6016
- if (!online && isLocal) {
6017
- console.log(`\x1B[33m\u26A0 Local LLM server is offline on ${url}.\x1B[0m`);
6018
- online = await startLocalRunner(url, model, locale);
6019
- }
6020
- if (!online) {
6021
- console.warn(
6022
- `\x1B[33m\u26A0 LLM server is not reachable at ${url}. Continuing without AI coaching.\x1B[0m
6023
- `
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`
6024
6189
  );
6025
- return { usable: false, reason: "offline" };
6190
+ return { usable: true };
6026
6191
  }
6027
- console.log("\x1B[32m\u2713 Local LLM server is online.\x1B[0m");
6028
- const available = await getAvailableModels(url, apiKey);
6029
- const modelKnown = available.length === 0 || available.some((m) => m.toLowerCase() === model.toLowerCase());
6030
- if (!modelKnown) {
6192
+ if (result.reason === "unsupported-provider") {
6031
6193
  console.warn(
6032
- `\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`
6033
6195
  );
6034
- console.warn(` Available models: ${available.join(", ")}`);
6196
+ } else if (result.reason === "model-not-found") {
6035
6197
  console.warn(
6036
- ` 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`
6037
6199
  );
6200
+ console.warn(` Available models: ${result.availableModels.join(", ")}`);
6201
+ } else if (result.reason === "offline") {
6038
6202
  console.warn(
6039
- "\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
+ `
6040
6205
  );
6041
- return { usable: false, reason: "model-not-found" };
6042
6206
  }
6043
- return { usable: true };
6207
+ return { usable: false, reason: result.reason };
6044
6208
  }
6045
6209
  async function ensureLlmReadyHeadless(db, opts = {}) {
6046
- const timeoutMs = opts.timeoutMs ?? 25e3;
6047
- const cfg = await getProviderForRole(db, "recall");
6048
- const { url, model, apiKey } = cfg;
6049
- if (!cfg.enabled) {
6050
- return {
6051
- usable: false,
6052
- reason: "disabled",
6053
- online: false,
6054
- model,
6055
- availableModels: []
6056
- };
6057
- }
6058
- if (cfg.apiFlavor !== "chat-completions") {
6059
- return {
6060
- usable: false,
6061
- reason: "unsupported-provider",
6062
- online: false,
6063
- model,
6064
- availableModels: []
6065
- };
6066
- }
6067
- const isLocal = isLocalEndpoint(url);
6068
- let online = await isLlmOnline(url);
6069
- if (!online && isLocal) {
6070
- spawnLocalRunner(url, model);
6071
- const deadline = Date.now() + timeoutMs;
6072
- while (Date.now() < deadline) {
6073
- await new Promise((r) => setTimeout(r, 1e3));
6074
- if (await isLlmOnline(url)) {
6075
- online = true;
6076
- break;
6077
- }
6078
- }
6079
- }
6080
- if (!online) {
6081
- return {
6082
- usable: false,
6083
- reason: "offline",
6084
- online: false,
6085
- model,
6086
- availableModels: []
6087
- };
6088
- }
6089
- const availableModels = await getAvailableModels(url, apiKey);
6090
- const modelKnown = availableModels.length === 0 || availableModels.some((m) => m.toLowerCase() === model.toLowerCase());
6091
- if (!modelKnown) {
6092
- return {
6093
- usable: false,
6094
- reason: "model-not-found",
6095
- online: true,
6096
- model,
6097
- availableModels
6098
- };
6099
- }
6100
- return { usable: true, online: true, model, availableModels };
6210
+ return prepareRecallChain(db, {
6211
+ timeoutMs: opts.timeoutMs ?? 25e3,
6212
+ interactive: false
6213
+ });
6101
6214
  }
6102
6215
  async function fetchWithInteractiveTimeout(url, options = {}) {
6103
6216
  const {
@@ -6129,8 +6242,8 @@ async function fetchWithInteractiveTimeout(url, options = {}) {
6129
6242
  const dotsPerLine = 30;
6130
6243
  while (true) {
6131
6244
  let timeoutId;
6132
- const timeoutPromise = new Promise((resolve8) => {
6133
- timeoutId = setTimeout(() => resolve8("timeout"), timeoutMs);
6245
+ const timeoutPromise = new Promise((resolve9) => {
6246
+ timeoutId = setTimeout(() => resolve9("timeout"), timeoutMs);
6134
6247
  });
6135
6248
  const dotsInterval = setInterval(() => {
6136
6249
  process.stdout.write(".");
@@ -6187,15 +6300,22 @@ async function ensureHighQualityQuestion(db, token) {
6187
6300
  bloomLevel: token.bloomLevel,
6188
6301
  sourceLinkContent
6189
6302
  });
6190
- if (generated && generated.trim().length > 0) {
6191
- await updateToken(db, token.slug, { question: generated });
6192
- 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
+ };
6193
6310
  }
6194
6311
  } catch {
6195
6312
  }
6196
6313
  }
6197
6314
  if (token.question && token.question.trim().length > 0) {
6198
- return token.question;
6315
+ return {
6316
+ question: token.question.trim(),
6317
+ source: "original"
6318
+ };
6199
6319
  }
6200
6320
  return null;
6201
6321
  }
@@ -6240,25 +6360,25 @@ async function observeUiSnapshotViaLLM(db, input8) {
6240
6360
  const imageUrls = [];
6241
6361
  const isVideo = /\.(mp4|mov|m4v|avi|mkv|webm)$/i.test(input8.imagePath);
6242
6362
  if (isVideo) {
6243
- const { mkdirSync: mkdirSync13, readdirSync: readdirSync3, rmSync: rmSync3 } = await import("fs");
6363
+ const { mkdirSync: mkdirSync14, readdirSync: readdirSync4, rmSync: rmSync4 } = await import("fs");
6244
6364
  const { execSync: execSync6 } = await import("child_process");
6245
6365
  const tempDir = join14(
6246
6366
  tmpdir2(),
6247
6367
  `zam-frames-${randomBytes(4).toString("hex")}`
6248
6368
  );
6249
- mkdirSync13(tempDir, { recursive: true });
6369
+ mkdirSync14(tempDir, { recursive: true });
6250
6370
  try {
6251
6371
  execSync6(
6252
6372
  `ffmpeg -i "${input8.imagePath}" -vf "fps=1/3,scale=1280:-1" -vsync vfr "${tempDir}/frame_%03d.png"`,
6253
6373
  { stdio: "ignore" }
6254
6374
  );
6255
- let files = readdirSync3(tempDir).filter((f) => f.endsWith(".png")).sort();
6375
+ let files = readdirSync4(tempDir).filter((f) => f.endsWith(".png")).sort();
6256
6376
  if (files.length === 0) {
6257
6377
  execSync6(
6258
6378
  `ffmpeg -i "${input8.imagePath}" -vframes 1 "${tempDir}/frame_001.png"`,
6259
6379
  { stdio: "ignore" }
6260
6380
  );
6261
- files = readdirSync3(tempDir).filter((f) => f.endsWith(".png")).sort();
6381
+ files = readdirSync4(tempDir).filter((f) => f.endsWith(".png")).sort();
6262
6382
  }
6263
6383
  const maxFrames = cfg.maxFrames ?? 100;
6264
6384
  let sampledFiles = files;
@@ -6280,7 +6400,7 @@ async function observeUiSnapshotViaLLM(db, input8) {
6280
6400
  }
6281
6401
  } finally {
6282
6402
  try {
6283
- rmSync3(tempDir, { recursive: true, force: true });
6403
+ rmSync4(tempDir, { recursive: true, force: true });
6284
6404
  } catch {
6285
6405
  }
6286
6406
  }
@@ -6403,7 +6523,7 @@ async function requestChatCompletionsVisionDraft(args) {
6403
6523
  }
6404
6524
  ],
6405
6525
  temperature: 0,
6406
- max_tokens: args.input.maxTokens ?? 450,
6526
+ max_tokens: args.input.maxTokens ?? DEFAULT_LLM_MAX_TOKENS,
6407
6527
  ...args.url.includes("11434") || args.url.includes("localhost") || args.url.includes("127.0.0.1") ? { options: { num_ctx: 32768 } } : {}
6408
6528
  }),
6409
6529
  locale: args.locale,
@@ -6459,7 +6579,7 @@ async function requestAnthropicVisionDraft(args) {
6459
6579
  },
6460
6580
  body: JSON.stringify({
6461
6581
  model: args.model,
6462
- max_tokens: args.input.maxTokens ?? 450,
6582
+ max_tokens: args.input.maxTokens ?? DEFAULT_LLM_MAX_TOKENS,
6463
6583
  system: VISION_SYSTEM_PROMPT,
6464
6584
  messages: [
6465
6585
  {
@@ -6614,26 +6734,9 @@ function isRecord2(value) {
6614
6734
  return typeof value === "object" && value !== null && !Array.isArray(value);
6615
6735
  }
6616
6736
 
6617
- // src/cli/commands/resolve-user.ts
6618
- async function ensureDefaultUser(db, preferredUserId) {
6619
- const stored = await getSetting(db, "user.id");
6620
- if (stored) return stored;
6621
- const userId = preferredUserId?.trim() || process.env.ZAM_USER?.trim() || process.env.USERNAME?.trim() || process.env.USER?.trim() || "default";
6622
- await setSetting(db, "user.id", userId);
6623
- return userId;
6624
- }
6625
- async function resolveUser(opts, db, resolveOpts) {
6626
- if (opts.user) return opts.user;
6627
- const stored = await getSetting(db, "user.id");
6628
- if (stored) return stored;
6629
- const message = "No user specified. Set a default with: zam whoami --set <id>";
6630
- if (resolveOpts?.json) {
6631
- console.log(JSON.stringify({ error: message }, null, 2));
6632
- } else {
6633
- console.error(message);
6634
- }
6635
- process.exit(1);
6636
- }
6737
+ // src/cli/commands/provider.ts
6738
+ import { password } from "@inquirer/prompts";
6739
+ import { Command as Command2 } from "commander";
6637
6740
 
6638
6741
  // src/cli/commands/shared/db.ts
6639
6742
  function defaultErrorHandler(message) {
@@ -6655,717 +6758,1187 @@ function jsonOut(data) {
6655
6758
  console.log(JSON.stringify(data, null, 2));
6656
6759
  }
6657
6760
 
6658
- // src/cli/commands/workspace.ts
6659
- import { execFileSync as execFileSync3 } from "child_process";
6660
- import { existsSync as existsSync16, mkdirSync as mkdirSync9, writeFileSync as writeFileSync8 } from "fs";
6661
- import { homedir as homedir9 } from "os";
6662
- import { join as join16, resolve as resolve4 } from "path";
6663
- import { confirm, input } from "@inquirer/prompts";
6664
- import { Command as Command3 } from "commander";
6665
-
6666
- // src/cli/commands/setup.ts
6667
- import {
6668
- copyFileSync as copyFileSync2,
6669
- existsSync as existsSync15,
6670
- mkdirSync as mkdirSync8,
6671
- readFileSync as readFileSync10,
6672
- writeFileSync as writeFileSync7
6673
- } from "fs";
6674
- import { basename as basename4, dirname as dirname5, join as join15, resolve as resolve3 } from "path";
6675
- import { fileURLToPath as fileURLToPath2 } from "url";
6676
- import { Command as Command2 } from "commander";
6677
- var packageRoot = [
6678
- fileURLToPath2(new URL("../..", import.meta.url)),
6679
- fileURLToPath2(new URL("../../..", import.meta.url))
6680
- ].find((candidate) => existsSync15(join15(candidate, "package.json"))) ?? fileURLToPath2(new URL("../..", import.meta.url));
6681
- var ALL_SETUP_AGENTS = ["claude", "copilot", "codex", "agent"];
6682
- var SKILL_PAIRS = [
6683
- {
6684
- from: join15(packageRoot, ".claude", "skills", "zam", "SKILL.md"),
6685
- to: join15(".claude", "skills", "zam", "SKILL.md"),
6686
- agents: ["claude"]
6687
- },
6688
- {
6689
- from: join15(packageRoot, ".agent", "skills", "zam", "SKILL.md"),
6690
- to: join15(".agent", "skills", "zam", "SKILL.md"),
6691
- agents: ["agent"]
6692
- },
6693
- {
6694
- from: join15(packageRoot, ".agents", "skills", "zam", "SKILL.md"),
6695
- to: join15(".agents", "skills", "zam", "SKILL.md"),
6696
- agents: ["copilot", "codex"]
6697
- }
6761
+ // src/cli/commands/provider.ts
6762
+ var VALID_API_FLAVORS = [
6763
+ "chat-completions",
6764
+ "anthropic-messages"
6698
6765
  ];
6699
- function parseSetupAgents(value) {
6700
- if (!value || value.trim().toLowerCase() === "all") {
6701
- return new Set(ALL_SETUP_AGENTS);
6702
- }
6703
- const aliases = {
6704
- claude: ["claude"],
6705
- copilot: ["copilot"],
6706
- codex: ["codex"],
6707
- agent: ["agent"],
6708
- opencode: ["agent"]
6709
- };
6710
- const selected = /* @__PURE__ */ new Set();
6711
- for (const raw of value.split(",")) {
6712
- const key = raw.trim().toLowerCase();
6713
- const mapped = aliases[key];
6714
- if (!mapped) {
6715
- throw new Error(
6716
- `Unknown agent "${raw}". Use: all, claude, copilot, codex, agent.`
6717
- );
6718
- }
6719
- for (const item of mapped) selected.add(item);
6720
- }
6721
- return selected;
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 };
6722
6777
  }
6723
- function copySkills(force, cwd = process.cwd(), agents = parseSetupAgents(), dryRun = false) {
6724
- let anyAction = false;
6725
- for (const { from, to, agents: pairAgents } of SKILL_PAIRS) {
6726
- if (!pairAgents.some((agent) => agents.has(agent))) continue;
6727
- const dest = join15(cwd, to);
6728
- if (!existsSync15(from)) {
6729
- console.warn(` warn source not found, skipping: ${from}`);
6730
- continue;
6731
- }
6732
- if (existsSync15(dest) && !force) {
6733
- console.log(` skip ${to} (already present \xE2\u20AC\u201D use --force to update)`);
6734
- continue;
6735
- }
6736
- if (dryRun) {
6737
- console.log(` would copy ${to}`);
6738
- anyAction = true;
6739
- continue;
6740
- }
6741
- mkdirSync8(dirname5(dest), { recursive: true });
6742
- copyFileSync2(from, dest);
6743
- console.log(` copy ${to}`);
6744
- anyAction = true;
6745
- }
6746
- if (!anyAction && !force) {
6747
- console.log(
6748
- "\nSkill files are already up to date. Run with --force to overwrite."
6749
- );
6750
- }
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 };
6751
6783
  }
6752
- function formatDatabaseInitTarget(target) {
6753
- switch (target.kind) {
6754
- case "local":
6755
- return `ZAM database at ${target.location} (local SQLite)`;
6756
- case "turso-remote":
6757
- return `ZAM database via Turso remote at ${target.location}`;
6758
- case "turso-native":
6759
- return `ZAM database via Turso native driver at ${target.location}`;
6760
- case "turso-replica":
6761
- return `ZAM database replica at ${target.location} syncing from ${target.syncUrl}`;
6762
- }
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
+ });
6763
6789
  }
6764
- async function initDatabase(skipInit) {
6765
- if (skipInit) return;
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;
6766
6827
  try {
6767
- const target = getDatabaseTargetInfo();
6768
- const db = await openDatabaseWithSync({ initialize: true });
6769
- await db.close();
6770
- console.log(` init ${formatDatabaseInitTarget(target)}`);
6771
- } catch (err) {
6772
- const msg = err.message;
6773
- if (!msg.includes("already")) {
6774
- console.warn(` warn database init: ${msg}`);
6775
- } else {
6776
- console.log(` skip database already initialized`);
6777
- }
6828
+ return JSON.parse(raw);
6829
+ } catch {
6830
+ return fallback;
6778
6831
  }
6779
6832
  }
6780
- var ZAM_BLOCK_START = "<!-- ZAM:START -->";
6781
- var ZAM_BLOCK_END = "<!-- ZAM:END -->";
6782
- function upsertMarkedBlock(dest, blockBody, dryRun) {
6783
- const block = `${ZAM_BLOCK_START}
6784
- ${blockBody.trim()}
6785
- ${ZAM_BLOCK_END}`;
6786
- if (!existsSync15(dest)) {
6787
- if (!dryRun) {
6788
- mkdirSync8(dirname5(dest), { recursive: true });
6789
- writeFileSync7(dest, `${block}
6790
- `, "utf8");
6791
- }
6792
- return "write";
6793
- }
6794
- const existing = readFileSync10(dest, "utf8");
6795
- if (existing.includes(block)) return "skip";
6796
- const start = existing.indexOf(ZAM_BLOCK_START);
6797
- const end = existing.indexOf(ZAM_BLOCK_END);
6798
- const next = start >= 0 && end > start ? `${existing.slice(0, start)}${block}${existing.slice(end + ZAM_BLOCK_END.length)}` : `${existing.trimEnd()}
6799
-
6800
- ${block}
6801
- `;
6802
- if (!dryRun) writeFileSync7(dest, next, "utf8");
6803
- return start >= 0 && end > start ? "update" : "write";
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);
6804
6840
  }
6805
- function logInstructionAction(action, label, dryRun) {
6806
- if (action === "skip") {
6807
- console.log(` skip ${label} (ZAM block already present)`);
6808
- } else {
6809
- console.log(` ${dryRun ? "would " : ""}${action} ${label}`);
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;
6810
6852
  }
6853
+ if (!db)
6854
+ throw new Error("Database is required for shared provider settings.");
6855
+ await setSetting(db, "llm.providers", JSON.stringify(p));
6811
6856
  }
6812
- function writeClaudeMd(skipClaudeMd, cwd = process.cwd(), opts = {}) {
6813
- if (skipClaudeMd) return;
6814
- const dest = join15(cwd, "CLAUDE.md");
6815
- if (existsSync15(dest)) {
6816
- if (!opts.updateExisting) {
6817
- console.log(` skip CLAUDE.md (already present)`);
6818
- return;
6819
- }
6820
- const action = upsertMarkedBlock(
6821
- dest,
6822
- `## ZAM learning sessions
6823
-
6824
- ZAM is available in this repository. Use the \`zam\` skill in Claude Code to turn real work into an observed learning session with active recall and FSRS scheduling.
6825
-
6826
- - Skill files live under \`.claude/skills/zam/\`.
6827
- - Fast-changing review data lives in \`~/.zam/\`, not in this repository.
6828
- - Run \`zam setup --target . --agents claude --force\` after upgrading ZAM to refresh the skill.`,
6829
- Boolean(opts.dryRun)
6830
- );
6831
- logInstructionAction(action, "CLAUDE.md", Boolean(opts.dryRun));
6857
+ async function writeScopedRoles(db, machine, r) {
6858
+ if (machine) {
6859
+ const config = getMachineAiConfig();
6860
+ saveMachineAiConfig({ ...config, roles: r });
6832
6861
  return;
6833
6862
  }
6834
- const name = basename4(cwd);
6835
- const content = `# ZAM Personal Kernel \xE2\u20AC\u201D ${name}
6836
-
6837
- This is a ZAM personal instance. ZAM builds lasting skills through spaced
6838
- repetition during real work \xE2\u20AC\u201D not separate study sessions.
6839
-
6840
- ## First time here?
6841
- Run \`/setup\` in Claude Code or Gemini CLI to complete first-time setup.
6842
-
6843
- ## Regular use
6844
- Run \`/zam\` to start a learning session on whatever you are working on.
6845
-
6846
- ## What lives here
6847
- - \`beliefs/\` \xE2\u20AC\u201D your worldview, approved by git commit
6848
- - \`goals/\` \xE2\u20AC\u201D your objectives, decomposed into tasks and learning tokens
6849
-
6850
- ## Fast-changing data
6851
- Learning tokens, cards, and review history live in local SQLite by default.
6852
- Use \`zam connector setup turso\` to store cloud credentials in
6853
- \`~/.zam/credentials.json\` and use a Turso database across machines.
6854
- `;
6855
- if (opts.dryRun) {
6856
- console.log(` would write CLAUDE.md`);
6857
- } else {
6858
- writeFileSync7(dest, content, "utf8");
6859
- console.log(` write CLAUDE.md`);
6863
+ if (!db)
6864
+ throw new Error("Database is required for shared provider settings.");
6865
+ await setSetting(db, "llm.roles", JSON.stringify(r));
6866
+ }
6867
+ async function withProviderScope(machine, action) {
6868
+ if (machine) {
6869
+ await action(void 0);
6870
+ return;
6860
6871
  }
6872
+ await withDb(action);
6861
6873
  }
6862
- function writeAgentsMd(skipAgentsMd, cwd = process.cwd(), opts = {}) {
6863
- if (skipAgentsMd) return;
6864
- const dest = join15(cwd, "AGENTS.md");
6865
- if (existsSync15(dest)) {
6866
- if (!opts.updateExisting) {
6867
- console.log(` skip AGENTS.md (already present)`);
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
+ );
6868
6900
  return;
6869
6901
  }
6870
- const action = upsertMarkedBlock(
6871
- dest,
6872
- `## ZAM learning sessions
6873
-
6874
- ZAM is available in this repository. Select the \`zam\` skill through \`/skills\` or invoke \`$zam\` where supported to turn real work into an observed learning session with active recall and FSRS scheduling.
6875
-
6876
- - Skill files live under \`.agents/skills/zam/\`.
6877
- - Fast-changing review data lives in \`~/.zam/\`, not in this repository.
6878
- - Run \`zam setup --target . --agents copilot,codex --force\` after upgrading ZAM to refresh the skill.`,
6879
- Boolean(opts.dryRun)
6902
+ console.log(
6903
+ `Providers (${opts.machine ? "~/.zam/config.json ai.providers" : "llm.providers"}):
6904
+ `
6880
6905
  );
6881
- logInstructionAction(action, "AGENTS.md", Boolean(opts.dryRun));
6882
- return;
6883
- }
6884
- const name = basename4(cwd);
6885
- const content = `# ZAM Personal Kernel - ${name}
6886
-
6887
- This is a ZAM personal instance. ZAM builds lasting skills through spaced
6888
- repetition during real work, not separate study sessions.
6889
-
6890
- ## First time here?
6891
- Run \`zam setup\` from the shell. When this repository includes
6892
- \`.agents/skills/setup/\`, you can instead select \`setup\` through \`/skills\`
6893
- or invoke \`$setup\`.
6894
-
6895
- ## Regular use
6896
- Select the \`zam\` skill through \`/skills\` or invoke \`$zam\` to start a
6897
- learning session on whatever you are working on.
6898
-
6899
- ## What lives here
6900
- - \`beliefs/\` - your worldview, approved by git commit
6901
- - \`goals/\` - your objectives, decomposed into tasks and learning tokens
6902
-
6903
- ## Fast-changing data
6904
- Learning tokens, cards, and review history live in local SQLite by default.
6905
- Use \`zam connector setup turso\` to store cloud credentials in
6906
- \`~/.zam/credentials.json\` and use a Turso database across machines.
6907
-
6908
- ## Codex skills
6909
- Codex discovers repository skills under \`.agents/skills/\`. Run
6910
- \`zam setup --force\` after upgrading \`zam-core\` to refresh them.
6911
- `;
6912
- if (opts.dryRun) {
6913
- console.log(` would write AGENTS.md`);
6914
- } else {
6915
- writeFileSync7(dest, content, "utf8");
6916
- console.log(` write AGENTS.md`);
6917
- }
6918
- }
6919
- function writeCopilotInstructions(cwd = process.cwd(), opts = {}) {
6920
- const dest = join15(cwd, ".github", "copilot-instructions.md");
6921
- const action = upsertMarkedBlock(
6922
- dest,
6923
- `## ZAM learning sessions
6924
-
6925
- ZAM is available in this repository through \`.agents/skills/zam/\`. Use the \`zam\` skill from Copilot-compatible skill selection surfaces to turn real work into an observed learning session with active recall and FSRS scheduling.
6926
-
6927
- - Fast-changing review data lives in \`~/.zam/\`, not in this repository.
6928
- - Run \`zam setup --target . --agents copilot --force\` after upgrading ZAM to refresh the skill.`,
6929
- Boolean(opts.dryRun)
6930
- );
6931
- logInstructionAction(
6932
- action,
6933
- ".github/copilot-instructions.md",
6934
- Boolean(opts.dryRun)
6935
- );
6936
- }
6937
- var setupCommand = new Command2("setup").description(
6938
- "Distribute ZAM skill files into this personal instance and initialize the database"
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}`);
6923
+ }
6924
+ }
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
+ }
6934
+ }
6935
+ if (orphans.length > 0) {
6936
+ console.log(
6937
+ `
6938
+ \x1B[33mOrphan keys (stored but unreferenced):\x1B[0m ${orphans.join(", ")}`
6939
+ );
6940
+ }
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, ...)"
6939
6946
  ).option(
6940
- "--force",
6941
- "overwrite existing skill files (use after upgrading zam)",
6942
- false
6943
- ).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(
6944
- "--agents <list>",
6945
- "comma-separated agents to wire: all, claude, copilot, codex, agent"
6947
+ "--flavor <flavor>",
6948
+ `Wire protocol: ${VALID_API_FLAVORS.join(" | ")} (default: inferred from URL)`
6946
6949
  ).option(
6947
- "--dry-run",
6948
- "show what would be written without changing files",
6949
- false
6950
- ).action(
6951
- async (opts) => {
6952
- let agents;
6953
- try {
6954
- agents = parseSetupAgents(opts.agents);
6955
- } catch (err) {
6956
- console.error(`Error: ${err.message}`);
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
+ );
6957
6962
  process.exit(1);
6958
6963
  }
6959
- const target = resolve3(opts.target ?? process.cwd());
6960
- const updateExistingInstructions = Boolean(opts.target) || opts.force;
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];
6961
6982
  console.log(
6962
- `Setting up ZAM in ${target}${opts.dryRun ? " (dry run)" : ""}
6963
- `
6983
+ `Provider "${name}" saved (${machine ? "machine-local" : "shared"}):`
6964
6984
  );
6965
- copySkills(opts.force, target, agents, opts.dryRun);
6966
- await initDatabase(opts.skipInit || opts.dryRun);
6967
- if (agents.has("claude")) {
6968
- writeClaudeMd(opts.skipClaudeMd, target, {
6969
- dryRun: opts.dryRun,
6970
- updateExisting: updateExistingInstructions
6971
- });
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"}`);
6972
6990
  }
6973
- if (agents.has("copilot") || agents.has("codex") || agents.has("agent")) {
6974
- writeAgentsMd(opts.skipAgentsMd, target, {
6975
- dryRun: opts.dryRun,
6976
- updateExisting: updateExistingInstructions
6977
- });
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}`
7002
+ );
6978
7003
  }
6979
- if (agents.has("copilot") && (opts.target || opts.agents)) {
6980
- writeCopilotInstructions(target, {
6981
- dryRun: opts.dryRun,
6982
- updateExisting: updateExistingInstructions
6983
- });
7004
+ if (!rec.url) {
7005
+ console.log(
7006
+ "\n \u26A0 No --url set; this provider inherits the base llm.url."
7007
+ );
6984
7008
  }
6985
7009
  console.log(
6986
- "\nDone. Run `zam whoami --set <your-id>` to set your identity. Start the `zam` skill with `/zam` in Claude/Gemini-compatible clients or `$zam` (or `/skills`) in Codex."
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
6987
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);
6988
7046
  }
6989
- );
6990
-
6991
- // src/cli/commands/workspace.ts
6992
- function runGit(cwd, args) {
6993
- try {
6994
- return execFileSync3("git", args, {
6995
- cwd,
6996
- stdio: "pipe",
6997
- encoding: "utf8"
6998
- }).trim();
6999
- } catch (err) {
7000
- throw new Error(`Git command failed: ${err.message}`);
7047
+ if (!opts.primary) {
7048
+ console.error("--primary is required.");
7049
+ process.exit(1);
7001
7050
  }
7002
- }
7003
- function ghRepoCreateArgs(repoName, repoVisibility) {
7004
- return ["repo", "create", repoName, repoVisibility, "--source=.", "--push"];
7005
- }
7006
- function gitRemoteArgs(githubUrl, hasOrigin) {
7007
- return hasOrigin ? ["remote", "set-url", "origin", githubUrl] : ["remote", "add", "origin", githubUrl];
7008
- }
7009
- var workspaceCommand = new Command3("workspace").description(
7010
- "Manage your ZAM learning workspace"
7011
- );
7012
- var WORKSPACE_KINDS = [
7013
- "personal",
7014
- "team",
7015
- "family",
7016
- "community",
7017
- "organization",
7018
- "custom"
7019
- ];
7020
- var WORKSPACE_SOURCE_CONTROLS = [
7021
- "github",
7022
- "azure-devops",
7023
- "git",
7024
- "none"
7025
- ];
7026
- function parseWorkspaceKind(value) {
7027
- const kind = (value ?? "custom").toLowerCase();
7028
- if (WORKSPACE_KINDS.includes(kind)) {
7029
- return kind;
7030
- }
7031
- throw new Error(
7032
- `Invalid workspace kind: ${value}. Use ${WORKSPACE_KINDS.join(", ")}.`
7033
- );
7034
- }
7035
- function parseWorkspaceSourceControl(value) {
7036
- if (!value) return void 0;
7037
- const source = value.toLowerCase();
7038
- if (WORKSPACE_SOURCE_CONTROLS.includes(source)) {
7039
- return source;
7040
- }
7041
- throw new Error(
7042
- `Invalid source control: ${value}. Use ${WORKSPACE_SOURCE_CONTROLS.join(", ")}.`
7043
- );
7044
- }
7045
- function parseScopes(value) {
7046
- if (!value) return void 0;
7047
- const scopes = value.split(",").map((item) => item.trim()).filter(Boolean);
7048
- return scopes.length > 0 ? scopes : void 0;
7049
- }
7050
- function requireWorkspace(id) {
7051
- const workspace = getConfiguredWorkspaces().find((item) => item.id === id);
7052
- if (!workspace) {
7053
- throw new Error(
7054
- `Workspace "${id}" is not configured. Add it with: zam workspace add ${id} --path <dir>`
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
+ )
7055
7063
  );
7056
- }
7057
- return workspace;
7058
- }
7059
- workspaceCommand.command("list").description("List configured ZAM workspaces").option("--json", "Output as JSON").action((opts) => {
7060
- const workspaces = getConfiguredWorkspaces();
7061
- if (opts.json) {
7062
- console.log(JSON.stringify({ workspaces }, null, 2));
7063
- return;
7064
- }
7065
- console.log("Configured ZAM workspaces:\n");
7066
- if (workspaces.length === 0) {
7064
+ const fb = opts.fallback ? `, fallback: ${opts.fallback}` : "";
7067
7065
  console.log(
7068
- " (none) \u2014 add one: zam workspace add personal --path <dir>"
7066
+ `Role "${role}" \u2192 primary: ${opts.primary}${fb} (${machine ? "machine-local" : "shared"})`
7069
7067
  );
7070
- return;
7071
- }
7072
- for (const workspace of workspaces) {
7073
- const label = workspace.label ? ` (${workspace.label})` : "";
7074
- console.log(` ${workspace.id}${label}`);
7075
- console.log(` kind: ${workspace.kind}`);
7076
- console.log(` path: ${workspace.path}`);
7077
- if (workspace.sourceControl) {
7078
- console.log(` source: ${workspace.sourceControl}`);
7079
- }
7080
- if (workspace.knowledgeScopes?.length) {
7081
- console.log(` scopes: ${workspace.knowledgeScopes.join(", ")}`);
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
+ }
7082
7077
  }
7083
- }
7078
+ });
7084
7079
  });
7085
- workspaceCommand.command("add <id>").description("Register an existing directory as a ZAM workspace").requiredOption("--path <dir>", "Existing workspace/repository directory").option(
7086
- "--kind <kind>",
7087
- `Workspace kind (${WORKSPACE_KINDS.join(" | ")})`,
7088
- "custom"
7089
- ).option("--label <label>", "Human-readable label").option(
7090
- "--source-control <provider>",
7091
- `Source-control provider (${WORKSPACE_SOURCE_CONTROLS.join(" | ")})`
7092
- ).option("--scopes <list>", "Comma-separated knowledge scopes").option("--default-agent <id>", "Default agent harness for this workspace").action((id, opts) => {
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) => {
7093
7086
  try {
7094
- const path = resolve4(String(opts.path));
7095
- if (!existsSync16(path)) {
7096
- console.error(`Workspace path does not exist: ${path}`);
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.");
7097
7090
  process.exit(1);
7098
7091
  }
7099
- const workspace = {
7100
- id,
7101
- kind: parseWorkspaceKind(opts.kind),
7102
- path,
7103
- ...opts.label ? { label: opts.label } : {},
7104
- ...opts.sourceControl ? { sourceControl: parseWorkspaceSourceControl(opts.sourceControl) } : {},
7105
- ...parseScopes(opts.scopes) ? { knowledgeScopes: parseScopes(opts.scopes) } : {},
7106
- ...opts.defaultAgent ? { defaultAgent: opts.defaultAgent } : {}
7107
- };
7108
- upsertConfiguredWorkspace(workspace);
7109
- console.log(`Registered workspace "${id}" at ${path}.`);
7092
+ setProviderApiKey(ref, key.trim());
7093
+ console.log(`Stored API key for "${ref}" (${maskSecret(key.trim())}).`);
7110
7094
  } catch (err) {
7111
- console.error(`Error: ${err.message}`);
7095
+ if (err.name === "ExitPromptError") {
7096
+ console.log("\nCancelled.");
7097
+ process.exit(0);
7098
+ }
7099
+ console.error("Error:", err.message);
7112
7100
  process.exit(1);
7113
7101
  }
7114
7102
  });
7115
- workspaceCommand.command("setup <id>").description("Install ZAM skills into a configured workspace").option(
7116
- "--agents <list>",
7117
- "comma-separated agents to wire: all, claude, copilot, codex, agent"
7118
- ).option(
7119
- "--force",
7120
- "overwrite existing skill files and refresh ZAM blocks",
7121
- false
7122
- ).option(
7123
- "--dry-run",
7124
- "show what would be written without changing files",
7125
- false
7126
- ).action((id, opts) => {
7127
- try {
7128
- const workspace = requireWorkspace(id);
7129
- const agents = parseSetupAgents(opts.agents);
7130
- console.log(
7131
- `Setting up workspace "${workspace.id}" in ${workspace.path}${opts.dryRun ? " (dry run)" : ""}
7132
- `
7133
- );
7134
- copySkills(
7135
- Boolean(opts.force),
7136
- workspace.path,
7137
- agents,
7138
- Boolean(opts.dryRun)
7139
- );
7140
- if (agents.has("claude")) {
7141
- writeClaudeMd(false, workspace.path, {
7142
- dryRun: Boolean(opts.dryRun),
7143
- updateExisting: true
7144
- });
7145
- }
7146
- if (agents.has("copilot") || agents.has("codex") || agents.has("agent")) {
7147
- writeAgentsMd(false, workspace.path, {
7148
- dryRun: Boolean(opts.dryRun),
7149
- updateExisting: true
7150
- });
7151
- }
7152
- if (agents.has("copilot")) {
7153
- writeCopilotInstructions(workspace.path, {
7154
- dryRun: Boolean(opts.dryRun),
7155
- updateExisting: true
7156
- });
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
+ });
7107
+
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;
7115
+ }
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));
7123
+ } else {
7124
+ console.error(message);
7125
+ }
7126
+ process.exit(1);
7127
+ }
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
+ );
7157
7185
  }
7158
- } catch (err) {
7159
- console.error(`Error: ${err.message}`);
7160
- process.exit(1);
7186
+ for (const item of mapped) selected.add(item);
7161
7187
  }
7162
- });
7163
- workspaceCommand.command("publish").description("Publish your local workspace sandbox to GitHub").action(async () => {
7164
- let db;
7165
- let workspaceDir = "";
7188
+ return selected;
7189
+ }
7190
+ function pathExists(path) {
7166
7191
  try {
7167
- db = await openDatabase();
7168
- workspaceDir = await getSetting(db, "personal.workspace_dir") || "";
7169
- await db.close();
7192
+ lstatSync(path);
7193
+ return true;
7170
7194
  } catch {
7171
- await db?.close();
7195
+ return false;
7172
7196
  }
7173
- if (!workspaceDir) {
7174
- console.error(
7175
- "\x1B[31m\u2717 No active workspace configured. Please run `zam init` first.\x1B[0m"
7176
- );
7177
- process.exit(1);
7178
- }
7179
- if (!existsSync16(workspaceDir)) {
7180
- console.error(
7181
- `\x1B[31m\u2717 Workspace directory does not exist: ${workspaceDir}\x1B[0m`
7182
- );
7183
- process.exit(1);
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;
7184
7207
  }
7185
- console.log(`
7186
- Active workspace: \x1B[36m${workspaceDir}\x1B[0m`);
7187
- if (!hasCommand("git")) {
7188
- console.error(
7189
- "\x1B[31m\u2717 Git command was not found on this system. Please install Git first.\x1B[0m"
7190
- );
7191
- process.exit(1);
7208
+ }
7209
+ function linkPointsTo(sourceDir, destinationDir) {
7210
+ try {
7211
+ return lstatSync(destinationDir).isSymbolicLink() && pathsResolveToSameDirectory(sourceDir, destinationDir);
7212
+ } catch {
7213
+ return false;
7192
7214
  }
7193
- const gitignorePath = join16(workspaceDir, ".gitignore");
7194
- if (!existsSync16(gitignorePath)) {
7195
- writeFileSync8(
7196
- gitignorePath,
7197
- "node_modules/\n.agent/\n.agents/\n.claude/\n.gemini/\n.goose/\n*.log\n",
7198
- "utf8"
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")
7199
7223
  );
7224
+ } catch {
7225
+ return false;
7200
7226
  }
7201
- const hasGitRepo = existsSync16(join16(workspaceDir, ".git"));
7202
- if (!hasGitRepo) {
7203
- console.log("Initializing local Git repository...");
7204
- runGit(workspaceDir, ["init", "-b", "main"]);
7205
- runGit(workspaceDir, ["add", "."]);
7206
- runGit(workspaceDir, [
7207
- "commit",
7208
- "-m",
7209
- "chore: initial workspace sandbox bootstrap"
7210
- ]);
7211
- console.log("\x1B[32m\u2713 Local Git repository initialized.\x1B[0m");
7212
- } else {
7213
- console.log("Git repository is already initialized.");
7214
- }
7215
- const repoName = await input({
7216
- message: "Choose a name for your GitHub repository:",
7217
- default: "zam-personal"
7218
- });
7219
- const isPrivate = await confirm({
7220
- message: "Should the repository be private?",
7221
- default: true
7222
- });
7223
- const repoVisibility = isPrivate ? "--private" : "--public";
7224
- if (hasCommand("gh")) {
7225
- console.log("GitHub CLI detected! Automating repository creation...");
7226
- const proceedGh = await confirm({
7227
- message: "Would you like ZAM to create the repository using the GitHub CLI?",
7228
- default: true
7229
- });
7230
- if (proceedGh) {
7231
- try {
7232
- console.log(`Creating GitHub repository ${repoName}...`);
7233
- execFileSync3("gh", ghRepoCreateArgs(repoName, repoVisibility), {
7234
- cwd: workspaceDir,
7235
- stdio: "inherit"
7236
- });
7237
- console.log(
7238
- "\n\x1B[32m\u2713 Successfully published workspace to GitHub!\x1B[0m"
7239
- );
7240
- process.exit(0);
7241
- } catch (err) {
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) {
7242
7273
  console.warn(
7243
- `\x1B[33m\u26A0 GitHub CLI creation failed: ${err.message}\x1B[0m`
7274
+ ` warn ${dirname5(to)} already exists and is not managed by ZAM; use --force to replace it`
7244
7275
  );
7245
7276
  }
7277
+ results.push({
7278
+ source: sourceDir,
7279
+ destination: destinationDir,
7280
+ action: "skipped",
7281
+ reason: "unmanaged-destination"
7282
+ });
7283
+ continue;
7246
7284
  }
7247
- }
7248
- console.log(
7249
- "\n\x1B[1mPlease create the repository manually on GitHub:\x1B[0m"
7250
- );
7251
- console.log(" 1. Go to https://github.com/new");
7252
- console.log(` 2. Name it exactly: \x1B[36m${repoName}\x1B[0m`);
7253
- console.log(
7254
- ` 3. Choose \x1B[36m${isPrivate ? "Private" : "Public"}\x1B[0m`
7255
- );
7256
- console.log(
7257
- " 4. Do NOT initialize it with README, .gitignore, or license"
7258
- );
7259
- console.log(" 5. Click 'Create repository'\n");
7260
- const githubUrl = await input({
7261
- message: "Paste the repository Git URL (e.g. git@github.com:user/repo.git):"
7262
- });
7263
- if (githubUrl) {
7264
- try {
7265
- console.log("Linking remote repository and pushing...");
7266
- let hasOrigin = false;
7267
- try {
7268
- runGit(workspaceDir, ["remote", "get-url", "origin"]);
7269
- hasOrigin = true;
7270
- } catch {
7271
- }
7272
- runGit(workspaceDir, gitRemoteArgs(githubUrl, hasOrigin));
7273
- runGit(workspaceDir, ["push", "-u", "origin", "main"]);
7274
- console.log(
7275
- "\x1B[32m\u2713 Successfully linked and pushed to GitHub!\x1B[0m"
7276
- );
7277
- } catch (err) {
7278
- console.error(
7279
- `\x1B[31m\u2717 Push failed: ${err.message}\x1B[0m`
7280
- );
7281
- console.log(
7282
- "You can push manually later using: git push -u origin main"
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"
7283
7297
  );
7298
+ log(` ${action === "linked" ? "link" : "relink"} ${dirname5(to)}`);
7284
7299
  }
7300
+ results.push({ source: sourceDir, destination: destinationDir, action });
7285
7301
  }
7286
- });
7287
- async function backupDatabaseTo(db, targetDir) {
7288
- const backupDir = join16(targetDir, "zam-backups");
7289
- mkdirSync9(backupDir, { recursive: true });
7290
- const stamp = (/* @__PURE__ */ new Date()).toISOString().replace(/[:.]/g, "-");
7291
- const dest = join16(backupDir, `zam-${stamp}.db`);
7292
- await db.exec(`VACUUM INTO '${dest.replace(/'/g, "''")}'`);
7293
- return dest;
7302
+ return results;
7294
7303
  }
7295
- workspaceCommand.command("data-dir").description("Print the ZAM data directory (database, credentials, config)").option("--json", "Output as JSON").action((opts) => {
7296
- const dir = join16(homedir9(), ".zam");
7297
- console.log(opts.json ? JSON.stringify({ dataDir: dir }) : dir);
7298
- });
7299
- workspaceCommand.command("backup").description("Back up the local ZAM database into your workspace").option(
7300
- "--dir <path>",
7301
- "Target directory (default: workspace dir, else ~/Documents/zam)"
7302
- ).option("--json", "Output as JSON").action(async (opts) => {
7303
- const target = getDatabaseTargetInfo();
7304
- if (target.kind !== "local") {
7305
- const reason = `The database is ${target.kind} (${target.location}); file backup applies only to a local database \u2014 your Turso remote is already the cloud backup.`;
7306
- if (opts.json) {
7307
- console.log(JSON.stringify({ ok: false, reason }));
7308
- return;
7309
- }
7310
- console.error(`\x1B[33m\u26A0 ${reason}\x1B[0m`);
7311
- process.exit(1);
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}`;
7312
7314
  }
7313
- let db;
7315
+ }
7316
+ async function initDatabase(skipInit) {
7317
+ if (skipInit) return;
7314
7318
  try {
7315
- db = await openDatabase();
7316
- const workspaceDir = opts.dir || await getSetting(db, "personal.workspace_dir") || join16(homedir9(), "Documents", "zam");
7317
- const dest = await backupDatabaseTo(db, workspaceDir);
7318
- if (opts.json) {
7319
- console.log(JSON.stringify({ ok: true, path: dest }));
7320
- } else {
7321
- console.log(`\x1B[32m\u2713 Database backed up to ${dest}\x1B[0m`);
7322
- }
7319
+ const target = getDatabaseTargetInfo();
7320
+ const db = await openDatabaseWithSync({ initialize: true });
7321
+ await db.close();
7322
+ console.log(` init ${formatDatabaseInitTarget(target)}`);
7323
7323
  } catch (err) {
7324
- const reason = err.message;
7325
- if (opts.json) {
7326
- console.log(JSON.stringify({ ok: false, reason }));
7324
+ const msg = err.message;
7325
+ if (!msg.includes("already")) {
7326
+ console.warn(` warn database init: ${msg}`);
7327
7327
  } else {
7328
- console.error(`\x1B[31m\u2717 Backup failed: ${reason}\x1B[0m`);
7328
+ console.log(` skip database already initialized`);
7329
7329
  }
7330
- process.exit(1);
7331
- } finally {
7332
- await db?.close();
7333
7330
  }
7334
- });
7335
-
7336
- // src/cli/commands/bridge.ts
7337
- var isServeMode = false;
7338
- function jsonOut2(data) {
7339
- console.log(JSON.stringify(data, null, 2));
7340
7331
  }
7341
- function jsonError(message) {
7342
- if (isServeMode) {
7343
- throw new Error(JSON.stringify({ error: message }));
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";
7344
7345
  }
7345
- console.log(JSON.stringify({ error: message }, null, 2));
7346
- process.exit(1);
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";
7347
7356
  }
7348
- function parseNonNegativeIntegerOption(name, value) {
7349
- const parsed = Number(value);
7350
- if (!Number.isInteger(parsed) || parsed < 0) {
7351
- jsonError(`${name} must be a non-negative integer`);
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}`);
7352
7362
  }
7353
- return parsed;
7354
- }
7355
- async function withDb2(fn) {
7356
- await withDb(fn, jsonError);
7357
7363
  }
7358
- async function getReviewTarget2(db, cardId, userId) {
7359
- const target = await db.prepare(
7360
- `SELECT c.id AS card_id, c.token_id, c.user_id, t.slug
7361
- FROM cards c
7362
- JOIN tokens t ON t.id = c.token_id
7363
- WHERE c.id = ?`
7364
- ).get(cardId);
7365
- if (!target) {
7366
- jsonError(`Card not found: ${cardId}`);
7367
- }
7368
- if (target.user_id !== userId) {
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(
7373
+ dest,
7374
+ `## ZAM learning sessions
7375
+
7376
+ ZAM is available in this repository. Use the \`zam\` skill in Claude Code to turn real work into an observed learning session with active recall and FSRS scheduling.
7377
+
7378
+ - Skill files live under \`.claude/skills/zam/\`.
7379
+ - Fast-changing review data lives in \`~/.zam/\`, not in this repository.
7380
+ - The skill directory is linked to this ZAM installation and updates with it.`,
7381
+ Boolean(opts.dryRun)
7382
+ );
7383
+ logInstructionAction(action, "CLAUDE.md", Boolean(opts.dryRun));
7384
+ return;
7385
+ }
7386
+ const name = basename4(cwd);
7387
+ const content = `# ZAM Personal Kernel \xE2\u20AC\u201D ${name}
7388
+
7389
+ This is a ZAM personal instance. ZAM builds lasting skills through spaced
7390
+ repetition during real work \xE2\u20AC\u201D not separate study sessions.
7391
+
7392
+ ## First time here?
7393
+ Run \`/setup\` in Claude Code or Gemini CLI to complete first-time setup.
7394
+
7395
+ ## Regular use
7396
+ Run \`/zam\` to start a learning session on whatever you are working on.
7397
+
7398
+ ## What lives here
7399
+ - \`beliefs/\` \xE2\u20AC\u201D your worldview, approved by git commit
7400
+ - \`goals/\` \xE2\u20AC\u201D your objectives, decomposed into tasks and learning tokens
7401
+
7402
+ ## Fast-changing data
7403
+ Learning tokens, cards, and review history live in local SQLite by default.
7404
+ Use \`zam connector setup turso\` to store cloud credentials in
7405
+ \`~/.zam/credentials.json\` and use a Turso database across machines.
7406
+ `;
7407
+ if (opts.dryRun) {
7408
+ console.log(` would write CLAUDE.md`);
7409
+ } else {
7410
+ writeFileSync7(dest, content, "utf8");
7411
+ console.log(` write CLAUDE.md`);
7412
+ }
7413
+ }
7414
+ function writeAgentsMd(skipAgentsMd, cwd = process.cwd(), opts = {}) {
7415
+ if (skipAgentsMd) return;
7416
+ const dest = join15(cwd, "AGENTS.md");
7417
+ if (existsSync15(dest)) {
7418
+ if (!opts.updateExisting) {
7419
+ console.log(` skip AGENTS.md (already present)`);
7420
+ return;
7421
+ }
7422
+ const action = upsertMarkedBlock(
7423
+ dest,
7424
+ `## ZAM learning sessions
7425
+
7426
+ ZAM is available in this repository. Select the \`zam\` skill through \`/skills\` or invoke \`$zam\` where supported to turn real work into an observed learning session with active recall and FSRS scheduling.
7427
+
7428
+ - Skill files live under \`.agents/skills/zam/\`.
7429
+ - Fast-changing review data lives in \`~/.zam/\`, not in this repository.
7430
+ - The skill directory is linked to this ZAM installation and updates with it.`,
7431
+ Boolean(opts.dryRun)
7432
+ );
7433
+ logInstructionAction(action, "AGENTS.md", Boolean(opts.dryRun));
7434
+ return;
7435
+ }
7436
+ const name = basename4(cwd);
7437
+ const content = `# ZAM Personal Kernel - ${name}
7438
+
7439
+ This is a ZAM personal instance. ZAM builds lasting skills through spaced
7440
+ repetition during real work, not separate study sessions.
7441
+
7442
+ ## First time here?
7443
+ Run \`zam setup\` from the shell. When this repository includes
7444
+ \`.agents/skills/setup/\`, you can instead select \`setup\` through \`/skills\`
7445
+ or invoke \`$setup\`.
7446
+
7447
+ ## Regular use
7448
+ Select the \`zam\` skill through \`/skills\` or invoke \`$zam\` to start a
7449
+ learning session on whatever you are working on.
7450
+
7451
+ ## What lives here
7452
+ - \`beliefs/\` - your worldview, approved by git commit
7453
+ - \`goals/\` - your objectives, decomposed into tasks and learning tokens
7454
+
7455
+ ## Fast-changing data
7456
+ Learning tokens, cards, and review history live in local SQLite by default.
7457
+ Use \`zam connector setup turso\` to store cloud credentials in
7458
+ \`~/.zam/credentials.json\` and use a Turso database across machines.
7459
+
7460
+ ## Codex skills
7461
+ Codex discovers repository skills under \`.agents/skills/\`. Run
7462
+ \`zam setup --force\` after upgrading \`zam-core\` to refresh them.
7463
+ `;
7464
+ if (opts.dryRun) {
7465
+ console.log(` would write AGENTS.md`);
7466
+ } else {
7467
+ writeFileSync7(dest, content, "utf8");
7468
+ console.log(` write AGENTS.md`);
7469
+ }
7470
+ }
7471
+ function writeCopilotInstructions(cwd = process.cwd(), opts = {}) {
7472
+ const dest = join15(cwd, ".github", "copilot-instructions.md");
7473
+ const action = upsertMarkedBlock(
7474
+ dest,
7475
+ `## ZAM learning sessions
7476
+
7477
+ ZAM is available in this repository through \`.claude/skills/zam/\`. Use the \`zam\` project skill from Copilot-compatible skill selection surfaces to turn real work into an observed learning session with active recall and FSRS scheduling.
7478
+
7479
+ - Fast-changing review data lives in \`~/.zam/\`, not in this repository.
7480
+ - The skill directory is linked to this ZAM installation and updates with it.`,
7481
+ Boolean(opts.dryRun)
7482
+ );
7483
+ logInstructionAction(
7484
+ action,
7485
+ ".github/copilot-instructions.md",
7486
+ Boolean(opts.dryRun)
7487
+ );
7488
+ }
7489
+ var setupCommand = new Command3("setup").description(
7490
+ "Link ZAM skill directories into this workspace and initialize the database"
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(
7492
+ "--agents <list>",
7493
+ "comma-separated agents to wire: all, claude, copilot, codex, agent"
7494
+ ).option(
7495
+ "--dry-run",
7496
+ "show what would be written without changing files",
7497
+ false
7498
+ ).action(
7499
+ async (opts) => {
7500
+ let agents;
7501
+ try {
7502
+ agents = parseSetupAgents(opts.agents);
7503
+ } catch (err) {
7504
+ console.error(`Error: ${err.message}`);
7505
+ process.exit(1);
7506
+ }
7507
+ const target = resolve3(opts.target ?? process.cwd());
7508
+ const updateExistingInstructions = Boolean(opts.target) || opts.force;
7509
+ console.log(
7510
+ `Setting up ZAM in ${target}${opts.dryRun ? " (dry run)" : ""}
7511
+ `
7512
+ );
7513
+ wireSkills(target, agents, {
7514
+ force: opts.force,
7515
+ dryRun: opts.dryRun
7516
+ });
7517
+ await initDatabase(opts.skipInit || opts.dryRun);
7518
+ if (agents.has("claude")) {
7519
+ writeClaudeMd(opts.skipClaudeMd, target, {
7520
+ dryRun: opts.dryRun,
7521
+ updateExisting: updateExistingInstructions
7522
+ });
7523
+ }
7524
+ if (agents.has("codex") || agents.has("agent")) {
7525
+ writeAgentsMd(opts.skipAgentsMd, target, {
7526
+ dryRun: opts.dryRun,
7527
+ updateExisting: updateExistingInstructions
7528
+ });
7529
+ }
7530
+ if (agents.has("copilot") && (opts.target || opts.agents)) {
7531
+ writeCopilotInstructions(target, {
7532
+ dryRun: opts.dryRun,
7533
+ updateExisting: updateExistingInstructions
7534
+ });
7535
+ }
7536
+ console.log(
7537
+ "\nDone. Run `zam whoami --set <your-id>` to set your identity. Start the `zam` skill with `/zam` in Claude/Copilot/Gemini-compatible clients or `$zam` (or `/skills`) in Codex."
7538
+ );
7539
+ }
7540
+ );
7541
+
7542
+ // src/cli/commands/workspace.ts
7543
+ import { execFileSync as execFileSync3 } from "child_process";
7544
+ import { existsSync as existsSync16, mkdirSync as mkdirSync9, writeFileSync as writeFileSync8 } from "fs";
7545
+ import { homedir as homedir9 } from "os";
7546
+ import { join as join16, resolve as resolve4 } from "path";
7547
+ import { confirm, input } from "@inquirer/prompts";
7548
+ import { Command as Command4 } from "commander";
7549
+ function runGit(cwd, args) {
7550
+ try {
7551
+ return execFileSync3("git", args, {
7552
+ cwd,
7553
+ stdio: "pipe",
7554
+ encoding: "utf8"
7555
+ }).trim();
7556
+ } catch (err) {
7557
+ throw new Error(`Git command failed: ${err.message}`);
7558
+ }
7559
+ }
7560
+ function ghRepoCreateArgs(repoName, repoVisibility) {
7561
+ return ["repo", "create", repoName, repoVisibility, "--source=.", "--push"];
7562
+ }
7563
+ function gitRemoteArgs(githubUrl, hasOrigin) {
7564
+ return hasOrigin ? ["remote", "set-url", "origin", githubUrl] : ["remote", "add", "origin", githubUrl];
7565
+ }
7566
+ var workspaceCommand = new Command4("workspace").description(
7567
+ "Manage your ZAM learning workspace"
7568
+ );
7569
+ var WORKSPACE_KINDS = [
7570
+ "personal",
7571
+ "team",
7572
+ "family",
7573
+ "community",
7574
+ "organization",
7575
+ "custom"
7576
+ ];
7577
+ var WORKSPACE_SOURCE_CONTROLS = [
7578
+ "github",
7579
+ "azure-devops",
7580
+ "git",
7581
+ "none"
7582
+ ];
7583
+ function parseWorkspaceKind(value) {
7584
+ const kind = (value ?? "custom").toLowerCase();
7585
+ if (WORKSPACE_KINDS.includes(kind)) {
7586
+ return kind;
7587
+ }
7588
+ throw new Error(
7589
+ `Invalid workspace kind: ${value}. Use ${WORKSPACE_KINDS.join(", ")}.`
7590
+ );
7591
+ }
7592
+ function parseWorkspaceSourceControl(value) {
7593
+ if (!value) return void 0;
7594
+ const source = value.toLowerCase();
7595
+ if (WORKSPACE_SOURCE_CONTROLS.includes(source)) {
7596
+ return source;
7597
+ }
7598
+ throw new Error(
7599
+ `Invalid source control: ${value}. Use ${WORKSPACE_SOURCE_CONTROLS.join(", ")}.`
7600
+ );
7601
+ }
7602
+ function parseScopes(value) {
7603
+ if (!value) return void 0;
7604
+ const scopes = value.split(",").map((item) => item.trim()).filter(Boolean);
7605
+ return scopes.length > 0 ? scopes : void 0;
7606
+ }
7607
+ function requireWorkspace(id) {
7608
+ const workspace = getConfiguredWorkspaces().find((item) => item.id === id);
7609
+ if (!workspace) {
7610
+ throw new Error(
7611
+ `Workspace "${id}" is not configured. Add it with: zam workspace add ${id} --path <dir>`
7612
+ );
7613
+ }
7614
+ return workspace;
7615
+ }
7616
+ workspaceCommand.command("list").description("List configured ZAM workspaces").option("--json", "Output as JSON").action((opts) => {
7617
+ const workspaces = getConfiguredWorkspaces();
7618
+ if (opts.json) {
7619
+ console.log(JSON.stringify({ workspaces }, null, 2));
7620
+ return;
7621
+ }
7622
+ console.log("Configured ZAM workspaces:\n");
7623
+ if (workspaces.length === 0) {
7624
+ console.log(
7625
+ " (none) \u2014 add one: zam workspace add personal --path <dir>"
7626
+ );
7627
+ return;
7628
+ }
7629
+ for (const workspace of workspaces) {
7630
+ const label = workspace.label ? ` (${workspace.label})` : "";
7631
+ console.log(` ${workspace.id}${label}`);
7632
+ console.log(` kind: ${workspace.kind}`);
7633
+ console.log(` path: ${workspace.path}`);
7634
+ if (workspace.sourceControl) {
7635
+ console.log(` source: ${workspace.sourceControl}`);
7636
+ }
7637
+ if (workspace.knowledgeScopes?.length) {
7638
+ console.log(` scopes: ${workspace.knowledgeScopes.join(", ")}`);
7639
+ }
7640
+ }
7641
+ });
7642
+ workspaceCommand.command("add <id>").description("Register an existing directory as a ZAM workspace").requiredOption("--path <dir>", "Existing workspace/repository directory").option(
7643
+ "--kind <kind>",
7644
+ `Workspace kind (${WORKSPACE_KINDS.join(" | ")})`,
7645
+ "custom"
7646
+ ).option("--label <label>", "Human-readable label").option(
7647
+ "--source-control <provider>",
7648
+ `Source-control provider (${WORKSPACE_SOURCE_CONTROLS.join(" | ")})`
7649
+ ).option("--scopes <list>", "Comma-separated knowledge scopes").option("--default-agent <id>", "Default agent harness for this workspace").action((id, opts) => {
7650
+ try {
7651
+ const path = resolve4(String(opts.path));
7652
+ if (!existsSync16(path)) {
7653
+ console.error(`Workspace path does not exist: ${path}`);
7654
+ process.exit(1);
7655
+ }
7656
+ const workspace = {
7657
+ id,
7658
+ kind: parseWorkspaceKind(opts.kind),
7659
+ path,
7660
+ ...opts.label ? { label: opts.label } : {},
7661
+ ...opts.sourceControl ? { sourceControl: parseWorkspaceSourceControl(opts.sourceControl) } : {},
7662
+ ...parseScopes(opts.scopes) ? { knowledgeScopes: parseScopes(opts.scopes) } : {},
7663
+ ...opts.defaultAgent ? { defaultAgent: opts.defaultAgent } : {}
7664
+ };
7665
+ wireSkills(path, parseSetupAgents());
7666
+ upsertConfiguredWorkspace(workspace);
7667
+ console.log(`Registered and linked workspace "${id}" at ${path}.`);
7668
+ } catch (err) {
7669
+ console.error(`Error: ${err.message}`);
7670
+ process.exit(1);
7671
+ }
7672
+ });
7673
+ workspaceCommand.command("remove <id>").description("Unregister a ZAM workspace without deleting its files").action((id) => {
7674
+ const existing = getConfiguredWorkspaces().find((item) => item.id === id);
7675
+ if (!existing) {
7676
+ console.error(`Workspace "${id}" is not configured.`);
7677
+ process.exit(1);
7678
+ }
7679
+ removeConfiguredWorkspace(id);
7680
+ console.log(
7681
+ `Unregistered workspace "${id}". Files in ${existing.path} were not changed.`
7682
+ );
7683
+ });
7684
+ workspaceCommand.command("setup <id>").description("Install ZAM skills into a configured workspace").option(
7685
+ "--agents <list>",
7686
+ "comma-separated agents to wire: all, claude, copilot, codex, agent"
7687
+ ).option(
7688
+ "--force",
7689
+ "overwrite existing skill files and refresh ZAM blocks",
7690
+ false
7691
+ ).option(
7692
+ "--dry-run",
7693
+ "show what would be written without changing files",
7694
+ false
7695
+ ).action((id, opts) => {
7696
+ try {
7697
+ const workspace = requireWorkspace(id);
7698
+ const agents = parseSetupAgents(opts.agents);
7699
+ console.log(
7700
+ `Setting up workspace "${workspace.id}" in ${workspace.path}${opts.dryRun ? " (dry run)" : ""}
7701
+ `
7702
+ );
7703
+ wireSkills(workspace.path, agents, {
7704
+ force: Boolean(opts.force),
7705
+ dryRun: Boolean(opts.dryRun)
7706
+ });
7707
+ if (agents.has("claude")) {
7708
+ writeClaudeMd(false, workspace.path, {
7709
+ dryRun: Boolean(opts.dryRun),
7710
+ updateExisting: true
7711
+ });
7712
+ }
7713
+ if (agents.has("codex") || agents.has("agent")) {
7714
+ writeAgentsMd(false, workspace.path, {
7715
+ dryRun: Boolean(opts.dryRun),
7716
+ updateExisting: true
7717
+ });
7718
+ }
7719
+ if (agents.has("copilot")) {
7720
+ writeCopilotInstructions(workspace.path, {
7721
+ dryRun: Boolean(opts.dryRun),
7722
+ updateExisting: true
7723
+ });
7724
+ }
7725
+ } catch (err) {
7726
+ console.error(`Error: ${err.message}`);
7727
+ process.exit(1);
7728
+ }
7729
+ });
7730
+ workspaceCommand.command("publish").description("Publish your local workspace sandbox to GitHub").action(async () => {
7731
+ let db;
7732
+ let workspaceDir = "";
7733
+ try {
7734
+ db = await openDatabase();
7735
+ workspaceDir = await getSetting(db, "personal.workspace_dir") || "";
7736
+ await db.close();
7737
+ } catch {
7738
+ await db?.close();
7739
+ }
7740
+ if (!workspaceDir) {
7741
+ console.error(
7742
+ "\x1B[31m\u2717 No active workspace configured. Please run `zam init` first.\x1B[0m"
7743
+ );
7744
+ process.exit(1);
7745
+ }
7746
+ if (!existsSync16(workspaceDir)) {
7747
+ console.error(
7748
+ `\x1B[31m\u2717 Workspace directory does not exist: ${workspaceDir}\x1B[0m`
7749
+ );
7750
+ process.exit(1);
7751
+ }
7752
+ console.log(`
7753
+ Active workspace: \x1B[36m${workspaceDir}\x1B[0m`);
7754
+ if (!hasCommand("git")) {
7755
+ console.error(
7756
+ "\x1B[31m\u2717 Git command was not found on this system. Please install Git first.\x1B[0m"
7757
+ );
7758
+ process.exit(1);
7759
+ }
7760
+ const gitignorePath = join16(workspaceDir, ".gitignore");
7761
+ if (!existsSync16(gitignorePath)) {
7762
+ writeFileSync8(
7763
+ gitignorePath,
7764
+ "node_modules/\n.agent/\n.agents/\n.claude/\n.gemini/\n.goose/\n*.log\n",
7765
+ "utf8"
7766
+ );
7767
+ }
7768
+ const hasGitRepo = existsSync16(join16(workspaceDir, ".git"));
7769
+ if (!hasGitRepo) {
7770
+ console.log("Initializing local Git repository...");
7771
+ runGit(workspaceDir, ["init", "-b", "main"]);
7772
+ runGit(workspaceDir, ["add", "."]);
7773
+ runGit(workspaceDir, [
7774
+ "commit",
7775
+ "-m",
7776
+ "chore: initial workspace sandbox bootstrap"
7777
+ ]);
7778
+ console.log("\x1B[32m\u2713 Local Git repository initialized.\x1B[0m");
7779
+ } else {
7780
+ console.log("Git repository is already initialized.");
7781
+ }
7782
+ const repoName = await input({
7783
+ message: "Choose a name for your GitHub repository:",
7784
+ default: "zam-personal"
7785
+ });
7786
+ const isPrivate = await confirm({
7787
+ message: "Should the repository be private?",
7788
+ default: true
7789
+ });
7790
+ const repoVisibility = isPrivate ? "--private" : "--public";
7791
+ if (hasCommand("gh")) {
7792
+ console.log("GitHub CLI detected! Automating repository creation...");
7793
+ const proceedGh = await confirm({
7794
+ message: "Would you like ZAM to create the repository using the GitHub CLI?",
7795
+ default: true
7796
+ });
7797
+ if (proceedGh) {
7798
+ try {
7799
+ console.log(`Creating GitHub repository ${repoName}...`);
7800
+ execFileSync3("gh", ghRepoCreateArgs(repoName, repoVisibility), {
7801
+ cwd: workspaceDir,
7802
+ stdio: "inherit"
7803
+ });
7804
+ console.log(
7805
+ "\n\x1B[32m\u2713 Successfully published workspace to GitHub!\x1B[0m"
7806
+ );
7807
+ process.exit(0);
7808
+ } catch (err) {
7809
+ console.warn(
7810
+ `\x1B[33m\u26A0 GitHub CLI creation failed: ${err.message}\x1B[0m`
7811
+ );
7812
+ }
7813
+ }
7814
+ }
7815
+ console.log(
7816
+ "\n\x1B[1mPlease create the repository manually on GitHub:\x1B[0m"
7817
+ );
7818
+ console.log(" 1. Go to https://github.com/new");
7819
+ console.log(` 2. Name it exactly: \x1B[36m${repoName}\x1B[0m`);
7820
+ console.log(
7821
+ ` 3. Choose \x1B[36m${isPrivate ? "Private" : "Public"}\x1B[0m`
7822
+ );
7823
+ console.log(
7824
+ " 4. Do NOT initialize it with README, .gitignore, or license"
7825
+ );
7826
+ console.log(" 5. Click 'Create repository'\n");
7827
+ const githubUrl = await input({
7828
+ message: "Paste the repository Git URL (e.g. git@github.com:user/repo.git):"
7829
+ });
7830
+ if (githubUrl) {
7831
+ try {
7832
+ console.log("Linking remote repository and pushing...");
7833
+ let hasOrigin = false;
7834
+ try {
7835
+ runGit(workspaceDir, ["remote", "get-url", "origin"]);
7836
+ hasOrigin = true;
7837
+ } catch {
7838
+ }
7839
+ runGit(workspaceDir, gitRemoteArgs(githubUrl, hasOrigin));
7840
+ runGit(workspaceDir, ["push", "-u", "origin", "main"]);
7841
+ console.log(
7842
+ "\x1B[32m\u2713 Successfully linked and pushed to GitHub!\x1B[0m"
7843
+ );
7844
+ } catch (err) {
7845
+ console.error(
7846
+ `\x1B[31m\u2717 Push failed: ${err.message}\x1B[0m`
7847
+ );
7848
+ console.log(
7849
+ "You can push manually later using: git push -u origin main"
7850
+ );
7851
+ }
7852
+ }
7853
+ });
7854
+ async function backupDatabaseTo(db, targetDir) {
7855
+ const backupDir = join16(targetDir, "zam-backups");
7856
+ mkdirSync9(backupDir, { recursive: true });
7857
+ const stamp = (/* @__PURE__ */ new Date()).toISOString().replace(/[:.]/g, "-");
7858
+ const dest = join16(backupDir, `zam-${stamp}.db`);
7859
+ await db.exec(`VACUUM INTO '${dest.replace(/'/g, "''")}'`);
7860
+ return dest;
7861
+ }
7862
+ workspaceCommand.command("data-dir").description("Print the ZAM data directory (database, credentials, config)").option("--json", "Output as JSON").action((opts) => {
7863
+ const dir = join16(homedir9(), ".zam");
7864
+ console.log(opts.json ? JSON.stringify({ dataDir: dir }) : dir);
7865
+ });
7866
+ workspaceCommand.command("backup").description("Back up the local ZAM database into your workspace").option(
7867
+ "--dir <path>",
7868
+ "Target directory (default: workspace dir, else ~/Documents/zam)"
7869
+ ).option("--json", "Output as JSON").action(async (opts) => {
7870
+ const target = getDatabaseTargetInfo();
7871
+ if (target.kind !== "local") {
7872
+ const reason = `The database is ${target.kind} (${target.location}); file backup applies only to a local database \u2014 your Turso remote is already the cloud backup.`;
7873
+ if (opts.json) {
7874
+ console.log(JSON.stringify({ ok: false, reason }));
7875
+ return;
7876
+ }
7877
+ console.error(`\x1B[33m\u26A0 ${reason}\x1B[0m`);
7878
+ process.exit(1);
7879
+ }
7880
+ let db;
7881
+ try {
7882
+ db = await openDatabase();
7883
+ const workspaceDir = opts.dir || await getSetting(db, "personal.workspace_dir") || join16(homedir9(), "Documents", "zam");
7884
+ const dest = await backupDatabaseTo(db, workspaceDir);
7885
+ if (opts.json) {
7886
+ console.log(JSON.stringify({ ok: true, path: dest }));
7887
+ } else {
7888
+ console.log(`\x1B[32m\u2713 Database backed up to ${dest}\x1B[0m`);
7889
+ }
7890
+ } catch (err) {
7891
+ const reason = err.message;
7892
+ if (opts.json) {
7893
+ console.log(JSON.stringify({ ok: false, reason }));
7894
+ } else {
7895
+ console.error(`\x1B[31m\u2717 Backup failed: ${reason}\x1B[0m`);
7896
+ }
7897
+ process.exit(1);
7898
+ } finally {
7899
+ await db?.close();
7900
+ }
7901
+ });
7902
+
7903
+ // src/cli/commands/bridge.ts
7904
+ var isServeMode = false;
7905
+ function jsonOut2(data) {
7906
+ console.log(JSON.stringify(data, null, 2));
7907
+ }
7908
+ function jsonError(message) {
7909
+ if (isServeMode) {
7910
+ throw new Error(JSON.stringify({ error: message }));
7911
+ }
7912
+ console.log(JSON.stringify({ error: message }, null, 2));
7913
+ process.exit(1);
7914
+ }
7915
+ function parseNonNegativeIntegerOption(name, value) {
7916
+ const parsed = Number(value);
7917
+ if (!Number.isInteger(parsed) || parsed < 0) {
7918
+ jsonError(`${name} must be a non-negative integer`);
7919
+ }
7920
+ return parsed;
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
+ }
7928
+ async function withDb2(fn) {
7929
+ await withDb(fn, jsonError);
7930
+ }
7931
+ async function getReviewTarget2(db, cardId, userId) {
7932
+ const target = await db.prepare(
7933
+ `SELECT c.id AS card_id, c.token_id, c.user_id, t.slug
7934
+ FROM cards c
7935
+ JOIN tokens t ON t.id = c.token_id
7936
+ WHERE c.id = ?`
7937
+ ).get(cardId);
7938
+ if (!target) {
7939
+ jsonError(`Card not found: ${cardId}`);
7940
+ }
7941
+ if (target.user_id !== userId) {
7369
7942
  jsonError(`Card ${cardId} does not belong to user ${userId}`);
7370
7943
  }
7371
7944
  return target;
@@ -7389,7 +7962,7 @@ function parseTokenUpdates(opts) {
7389
7962
  }
7390
7963
  return updates;
7391
7964
  }
7392
- var bridgeCommand = new Command4("bridge").description(
7965
+ var bridgeCommand = new Command5("bridge").description(
7393
7966
  "Machine-readable JSON protocol for AI integration"
7394
7967
  );
7395
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) => {
@@ -7447,6 +8020,37 @@ bridgeCommand.command("workspace-info").description("Report the workspace dir, i
7447
8020
  });
7448
8021
  });
7449
8022
  });
8023
+ function sameWorkspacePath(left, right) {
8024
+ const normalize = (value) => {
8025
+ const path = resolve5(value);
8026
+ return process.platform === "win32" ? path.toLowerCase() : path;
8027
+ };
8028
+ return normalize(left) === normalize(right);
8029
+ }
8030
+ async function ensureDesktopWorkspace(db) {
8031
+ const configured = getConfiguredWorkspaces();
8032
+ const savedWorkspaceDir = await getSetting(db, "personal.workspace_dir");
8033
+ const workspaceDir = savedWorkspaceDir || configured.find((workspace) => workspace.kind === "personal")?.path || join17(homedir10(), "Documents", "zam");
8034
+ mkdirSync10(workspaceDir, { recursive: true });
8035
+ if (!savedWorkspaceDir) {
8036
+ await setSetting(db, "personal.workspace_dir", workspaceDir);
8037
+ }
8038
+ if (!configured.some(
8039
+ (workspace) => sameWorkspacePath(workspace.path, workspaceDir)
8040
+ )) {
8041
+ const id = configured.some((workspace) => workspace.id === "personal") ? workspaceIdFromPath(workspaceDir) : "personal";
8042
+ upsertConfiguredWorkspace({
8043
+ id,
8044
+ label: basename5(workspaceDir) || "ZAM",
8045
+ kind: "personal",
8046
+ path: workspaceDir
8047
+ });
8048
+ }
8049
+ const skillLinks = getConfiguredWorkspaces().flatMap(
8050
+ (workspace) => existsSync17(workspace.path) ? wireSkills(workspace.path, parseSetupAgents(), { quiet: true }) : []
8051
+ );
8052
+ return { workspaceDir, skillLinks };
8053
+ }
7450
8054
  bridgeCommand.command("workspace-list").description("List configured ZAM workspaces (JSON)").action(async () => {
7451
8055
  await withDb2(async (db) => {
7452
8056
  const workspaceDir = await getSetting(db, "personal.workspace_dir") || null;
@@ -7487,6 +8091,7 @@ bridgeCommand.command("workspace-add").description("Register an existing directo
7487
8091
  kind: parseBridgeWorkspaceKind(opts.kind),
7488
8092
  path
7489
8093
  };
8094
+ const skillLinks = wireSkills(path, parseSetupAgents(), { quiet: true });
7490
8095
  await withDb2(async (db) => {
7491
8096
  await setSetting(db, "personal.workspace_dir", path);
7492
8097
  upsertConfiguredWorkspace(workspace);
@@ -7494,7 +8099,39 @@ bridgeCommand.command("workspace-add").description("Register an existing directo
7494
8099
  ok: true,
7495
8100
  workspace,
7496
8101
  workspaces: getConfiguredWorkspaces(),
7497
- workspaceDir: path
8102
+ workspaceDir: path,
8103
+ skillLinks
8104
+ });
8105
+ });
8106
+ });
8107
+ bridgeCommand.command("workspace-remove").description("Unregister a ZAM workspace without deleting its files (JSON)").requiredOption("--id <id>", "Workspace id").action(async (opts) => {
8108
+ const id = String(opts.id ?? "").trim();
8109
+ if (!id) jsonError("A non-empty --id is required");
8110
+ const workspace = getConfiguredWorkspaces().find((item) => item.id === id);
8111
+ if (!workspace) jsonError(`Workspace "${id}" is not configured`);
8112
+ await withDb2(async (db) => {
8113
+ const activeWorkspaceDir = await getSetting(db, "personal.workspace_dir") || null;
8114
+ const remaining = removeConfiguredWorkspace(id);
8115
+ let workspaceDir = activeWorkspaceDir;
8116
+ let skillLinks = [];
8117
+ if (activeWorkspaceDir && sameWorkspacePath(activeWorkspaceDir, workspace.path)) {
8118
+ if (remaining.length > 0) {
8119
+ workspaceDir = remaining[0].path;
8120
+ await setSetting(db, "personal.workspace_dir", workspaceDir);
8121
+ } else {
8122
+ workspaceDir = join17(homedir10(), "Documents", "zam");
8123
+ await setSetting(db, "personal.workspace_dir", workspaceDir);
8124
+ const ensured = await ensureDesktopWorkspace(db);
8125
+ workspaceDir = ensured.workspaceDir;
8126
+ skillLinks = ensured.skillLinks;
8127
+ }
8128
+ }
8129
+ jsonOut2({
8130
+ ok: true,
8131
+ removed: workspace,
8132
+ workspaces: getConfiguredWorkspaces(),
8133
+ workspaceDir,
8134
+ skillLinks
7498
8135
  });
7499
8136
  });
7500
8137
  });
@@ -7502,9 +8139,11 @@ bridgeCommand.command("set-workspace-dir").description("Set the personal workspa
7502
8139
  const raw = String(opts.dir ?? "").trim();
7503
8140
  if (!raw) jsonError("A non-empty --dir is required");
7504
8141
  const dir = resolve5(raw);
8142
+ if (!existsSync17(dir)) jsonError(`Workspace path does not exist: ${dir}`);
8143
+ const skillLinks = wireSkills(dir, parseSetupAgents(), { quiet: true });
7505
8144
  await withDb2(async (db) => {
7506
8145
  await setSetting(db, "personal.workspace_dir", dir);
7507
- jsonOut2({ ok: true, workspaceDir: dir });
8146
+ jsonOut2({ ok: true, workspaceDir: dir, skillLinks });
7508
8147
  });
7509
8148
  });
7510
8149
  bridgeCommand.command("agent-list").description("List agent harnesses with detection state + the default (JSON)").action(async () => {
@@ -7593,6 +8232,8 @@ bridgeCommand.command("get-review").description("Get next review card with promp
7593
8232
  const item = queue.items[0];
7594
8233
  const isLlmEnabled = await getSetting(db, "llm.enabled") === "true";
7595
8234
  let resolvedQuestion = item.question;
8235
+ let questionSource = "original";
8236
+ let questionModel;
7596
8237
  if (isLlmEnabled) {
7597
8238
  try {
7598
8239
  const healed = await ensureHighQualityQuestion(db, {
@@ -7605,7 +8246,9 @@ bridgeCommand.command("get-review").description("Get next review card with promp
7605
8246
  question: item.question
7606
8247
  });
7607
8248
  if (healed) {
7608
- resolvedQuestion = healed;
8249
+ resolvedQuestion = healed.question;
8250
+ questionSource = healed.source;
8251
+ questionModel = healed.model;
7609
8252
  }
7610
8253
  } catch {
7611
8254
  }
@@ -7634,6 +8277,8 @@ bridgeCommand.command("get-review").description("Get next review card with promp
7634
8277
  hasReview: true,
7635
8278
  card: item,
7636
8279
  prompt,
8280
+ questionSource,
8281
+ questionModel: questionModel ?? null,
7637
8282
  resolvedContext,
7638
8283
  queueSize: fullQueue.items.length
7639
8284
  });
@@ -7932,7 +8577,7 @@ bridgeCommand.command("discover-skills").description(
7932
8577
  const monitorDir = join17(homedir10(), ".zam", "monitor");
7933
8578
  let files;
7934
8579
  try {
7935
- files = readdirSync2(monitorDir).filter((f) => f.endsWith(".jsonl"));
8580
+ files = readdirSync3(monitorDir).filter((f) => f.endsWith(".jsonl"));
7936
8581
  } catch {
7937
8582
  jsonOut2({ proposals: [], message: "No monitor logs found." });
7938
8583
  return;
@@ -8455,7 +9100,7 @@ bridgeCommand.command("capture-ui").description("Capture a screenshot for agent-
8455
9100
  if (!post.allowed) {
8456
9101
  if (!opts.output) {
8457
9102
  try {
8458
- rmSync2(outputPath, { force: true });
9103
+ rmSync3(outputPath, { force: true });
8459
9104
  } catch {
8460
9105
  }
8461
9106
  }
@@ -8594,7 +9239,7 @@ bridgeCommand.command("stop-recording").description(
8594
9239
  }
8595
9240
  const sessionId = opts.session;
8596
9241
  const statePath = join17(tmpdir3(), `zam-recording-${sessionId}.json`);
8597
- const { existsSync: existsSync25, readFileSync: readFileSync16, rmSync: rmSync3 } = await import("fs");
9242
+ const { existsSync: existsSync25, readFileSync: readFileSync16, rmSync: rmSync4 } = await import("fs");
8598
9243
  if (!existsSync25(statePath)) {
8599
9244
  jsonOut2({
8600
9245
  sessionId,
@@ -8619,7 +9264,7 @@ bridgeCommand.command("stop-recording").description(
8619
9264
  };
8620
9265
  let attempts = 0;
8621
9266
  while (isProcessRunning(pid) && attempts < 20) {
8622
- await new Promise((resolve8) => setTimeout(resolve8, 250));
9267
+ await new Promise((resolve9) => setTimeout(resolve9, 250));
8623
9268
  attempts++;
8624
9269
  }
8625
9270
  if (isProcessRunning(pid)) {
@@ -8629,7 +9274,7 @@ bridgeCommand.command("stop-recording").description(
8629
9274
  }
8630
9275
  }
8631
9276
  try {
8632
- rmSync3(statePath, { force: true });
9277
+ rmSync4(statePath, { force: true });
8633
9278
  } catch {
8634
9279
  }
8635
9280
  if (!existsSync25(outputPath)) {
@@ -8658,7 +9303,7 @@ bridgeCommand.command("stop-recording").description(
8658
9303
  return;
8659
9304
  }
8660
9305
  try {
8661
- rmSync3(outputPath, { force: true });
9306
+ rmSync4(outputPath, { force: true });
8662
9307
  } catch {
8663
9308
  }
8664
9309
  jsonOut2({
@@ -8725,12 +9370,194 @@ bridgeCommand.command("check-llm").description("Check if LLM is enabled and onli
8725
9370
  });
8726
9371
  bridgeCommand.command("provider-status").description("Show secret-safe provider status for LLM roles (JSON)").action(async () => {
8727
9372
  await withDb2(async (db) => {
8728
- const [recall, vision, text] = await Promise.all([
8729
- getProviderRoleStatus(db, "recall"),
8730
- getProviderRoleStatus(db, "vision"),
8731
- getProviderRoleStatus(db, "text")
8732
- ]);
8733
- jsonOut2({ roles: { recall, vision, text } });
9373
+ const [recall, vision, text] = await Promise.all([
9374
+ getProviderRoleStatus(db, "recall"),
9375
+ getProviderRoleStatus(db, "vision"),
9376
+ getProviderRoleStatus(db, "text")
9377
+ ]);
9378
+ jsonOut2({ roles: { recall, vision, text } });
9379
+ });
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 });
8734
9561
  });
8735
9562
  });
8736
9563
  bridgeCommand.command("check-vision").description(
@@ -8779,7 +9606,10 @@ bridgeCommand.command("translate-question").description("Translate a question dy
8779
9606
  });
8780
9607
  bridgeCommand.command("evaluate-answer").description(
8781
9608
  "Evaluate the learner's active-recall answer using the local LLM (JSON)"
8782
- ).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) => {
8783
9613
  await withDb2(async (db) => {
8784
9614
  const isEnabled = await getSetting(db, "llm.enabled") === "true";
8785
9615
  if (!isEnabled) {
@@ -8790,8 +9620,8 @@ bridgeCommand.command("evaluate-answer").description(
8790
9620
  });
8791
9621
  return;
8792
9622
  }
8793
- let resolvedContextContent = null;
8794
- if (opts.sourceLink) {
9623
+ let resolvedContextContent = opts.sourceContent ?? null;
9624
+ if (resolvedContextContent == null && opts.sourceLink) {
8795
9625
  try {
8796
9626
  const resolved = await resolveReviewContext(opts.sourceLink);
8797
9627
  resolvedContextContent = resolved?.content ?? null;
@@ -8799,7 +9629,7 @@ bridgeCommand.command("evaluate-answer").description(
8799
9629
  }
8800
9630
  }
8801
9631
  try {
8802
- const evaluation = await evaluateAnswerViaLLM(db, {
9632
+ const result = await evaluateAnswerViaLLM(db, {
8803
9633
  slug: opts.slug,
8804
9634
  concept: opts.concept,
8805
9635
  domain: opts.domain,
@@ -8809,7 +9639,11 @@ bridgeCommand.command("evaluate-answer").description(
8809
9639
  userAnswer: opts.userAnswer,
8810
9640
  sourceLinkContent: resolvedContextContent
8811
9641
  });
8812
- jsonOut2({ success: true, evaluation });
9642
+ jsonOut2({
9643
+ success: true,
9644
+ evaluation: result.text,
9645
+ evaluationModel: result.model
9646
+ });
8813
9647
  } catch (err) {
8814
9648
  jsonOut2({
8815
9649
  success: false,
@@ -8823,10 +9657,13 @@ bridgeCommand.command("desktop-bootstrap").description("Initialize first-run des
8823
9657
  await withDb2(async (db) => {
8824
9658
  const userId = await ensureDefaultUser(db, opts.user);
8825
9659
  const { enabled, url, model, locale } = await getLlmConfig(db);
9660
+ const { workspaceDir, skillLinks } = await ensureDesktopWorkspace(db);
8826
9661
  jsonOut2({
8827
9662
  userId,
8828
9663
  locale,
8829
- llm: { enabled, url, model }
9664
+ llm: { enabled, url, model },
9665
+ workspaceDir,
9666
+ skillLinks
8830
9667
  });
8831
9668
  });
8832
9669
  });
@@ -9045,8 +9882,8 @@ bridgeCommand.command("serve").description("Start the persistent JSON-RPC stdin/
9045
9882
  });
9046
9883
 
9047
9884
  // src/cli/commands/card.ts
9048
- import { Command as Command5 } from "commander";
9049
- var cardCommand = new Command5("card").description(
9885
+ import { Command as Command6 } from "commander";
9886
+ var cardCommand = new Command6("card").description(
9050
9887
  "Manage spaced-repetition cards"
9051
9888
  );
9052
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) => {
@@ -9244,9 +10081,9 @@ cardCommand.command("delete").description("Delete one user's card for a token").
9244
10081
  });
9245
10082
 
9246
10083
  // src/cli/commands/connector.ts
9247
- import { input as input2, password } from "@inquirer/prompts";
9248
- import { Command as Command6 } from "commander";
9249
- 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(
9250
10087
  "Manage external service connectors"
9251
10088
  );
9252
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(
@@ -9271,7 +10108,7 @@ connectorCommand.command("setup").description("Configure a connector").argument(
9271
10108
  const project = await input2({
9272
10109
  message: "Project name:"
9273
10110
  });
9274
- const pat = await password({
10111
+ const pat = await password2({
9275
10112
  message: "Personal Access Token:"
9276
10113
  });
9277
10114
  if (!orgUrl || !project || !pat) {
@@ -9362,7 +10199,7 @@ async function setupTurso(urlArg, tokenArg, mode) {
9362
10199
  const url = urlArg ?? await input2({
9363
10200
  message: "Turso database URL (e.g. libsql://my-db-user.turso.io):"
9364
10201
  });
9365
- const token = tokenArg ?? await password({
10202
+ const token = tokenArg ?? await password2({
9366
10203
  message: "Auth token:"
9367
10204
  });
9368
10205
  if (!url || !token) {
@@ -9391,7 +10228,7 @@ async function setupTurso(urlArg, tokenArg, mode) {
9391
10228
  import { execSync as execSync5 } from "child_process";
9392
10229
  import { chmodSync as chmodSync2, existsSync as existsSync18, writeFileSync as writeFileSync9 } from "fs";
9393
10230
  import { join as join18 } from "path";
9394
- import { Command as Command7 } from "commander";
10231
+ import { Command as Command8 } from "commander";
9395
10232
  function installHook2() {
9396
10233
  const gitDir = join18(process.cwd(), ".git");
9397
10234
  if (!existsSync18(gitDir)) {
@@ -9421,7 +10258,7 @@ zam git-sync --commit HEAD --quiet
9421
10258
  process.exit(1);
9422
10259
  }
9423
10260
  }
9424
- 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) => {
9425
10262
  if (opts.install) {
9426
10263
  installHook2();
9427
10264
  return;
@@ -9510,10 +10347,10 @@ ZAM Auto-Stale Complete: Scanned ${changedFiles.length} file(s).`
9510
10347
  });
9511
10348
 
9512
10349
  // src/cli/commands/goal.ts
9513
- import { existsSync as existsSync19, mkdirSync as mkdirSync10 } from "fs";
10350
+ import { existsSync as existsSync19, mkdirSync as mkdirSync11 } from "fs";
9514
10351
  import { resolve as resolve6 } from "path";
9515
10352
  import { input as input3 } from "@inquirer/prompts";
9516
- import { Command as Command8 } from "commander";
10353
+ import { Command as Command9 } from "commander";
9517
10354
  async function resolveGoalsDir() {
9518
10355
  let goalsDir;
9519
10356
  let db;
@@ -9526,7 +10363,7 @@ async function resolveGoalsDir() {
9526
10363
  }
9527
10364
  return goalsDir ? resolve6(goalsDir) : resolve6("goals");
9528
10365
  }
9529
- var goalCommand = new Command8("goal").description(
10366
+ var goalCommand = new Command9("goal").description(
9530
10367
  "Manage learning goals (markdown files)"
9531
10368
  );
9532
10369
  goalCommand.command("list").description("List all goals").option(
@@ -9636,7 +10473,7 @@ ${"\u2500".repeat(50)}`);
9636
10473
  goalCommand.command("create").description("Create a new goal").option("--slug <slug>", "Goal slug (used as filename)").option("--title <title>", "Goal title").option("--parent <slug>", "Parent goal slug").option("--description <text>", "Goal description").option("--json", "Output as JSON").action(async (opts) => {
9637
10474
  const goalsDir = await resolveGoalsDir();
9638
10475
  if (!existsSync19(goalsDir)) {
9639
- mkdirSync10(goalsDir, { recursive: true });
10476
+ mkdirSync11(goalsDir, { recursive: true });
9640
10477
  }
9641
10478
  let slug = opts.slug;
9642
10479
  let title = opts.title;
@@ -9695,19 +10532,19 @@ goalCommand.command("status <slug> <status>").description("Update a goal's statu
9695
10532
  });
9696
10533
 
9697
10534
  // src/cli/commands/init.ts
9698
- import { existsSync as existsSync20, mkdirSync as mkdirSync11, writeFileSync as writeFileSync10 } from "fs";
10535
+ import { existsSync as existsSync20, mkdirSync as mkdirSync12, writeFileSync as writeFileSync10 } from "fs";
9699
10536
  import { homedir as homedir11 } from "os";
9700
- import { join as join19 } from "path";
10537
+ import { join as join19, resolve as resolve7 } from "path";
9701
10538
  import { confirm as confirm2, input as input4 } from "@inquirer/prompts";
9702
- import { Command as Command9 } from "commander";
10539
+ import { Command as Command10 } from "commander";
9703
10540
  var HOME2 = homedir11();
9704
10541
  function printLine(char = "\u2550", len = 60, color = "\x1B[36m") {
9705
10542
  console.log(`${color}${char.repeat(len)}\x1B[0m`);
9706
10543
  }
9707
10544
  function bootstrapSandboxWorkspace(workspaceDir) {
9708
- mkdirSync11(join19(workspaceDir, "beliefs"), { recursive: true });
9709
- mkdirSync11(join19(workspaceDir, "goals"), { recursive: true });
9710
- mkdirSync11(join19(workspaceDir, "skills"), { recursive: true });
10545
+ mkdirSync12(join19(workspaceDir, "beliefs"), { recursive: true });
10546
+ mkdirSync12(join19(workspaceDir, "goals"), { recursive: true });
10547
+ mkdirSync12(join19(workspaceDir, "skills"), { recursive: true });
9711
10548
  const worldviewFile = join19(workspaceDir, "beliefs", "worldview.md");
9712
10549
  if (!existsSync20(worldviewFile)) {
9713
10550
  writeFileSync10(
@@ -9736,7 +10573,7 @@ Here, I declare the core concepts and principles I want to master.
9736
10573
  );
9737
10574
  }
9738
10575
  }
9739
- 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 () => {
9740
10577
  printLine();
9741
10578
  console.log(
9742
10579
  "\x1B[1m\x1B[32m ZAM \u2014 The Symbiotic Learning Agent Onboarding\x1B[0m"
@@ -9747,12 +10584,21 @@ var initCommand = new Command9("init").description("Launch the guided interactiv
9747
10584
  printLine();
9748
10585
  console.log("\n\x1B[1m[1/5] Setting up Local Workspace Sandbox\x1B[0m");
9749
10586
  const defaultWorkspace = join19(HOME2, "Documents", "zam");
9750
- const workspacePath = await input4({
9751
- message: "Choose your ZAM workspace directory:",
9752
- default: defaultWorkspace
9753
- });
10587
+ const workspacePath = resolve7(
10588
+ await input4({
10589
+ message: "Choose your ZAM workspace directory:",
10590
+ default: defaultWorkspace
10591
+ })
10592
+ );
9754
10593
  try {
9755
10594
  bootstrapSandboxWorkspace(workspacePath);
10595
+ wireSkills(workspacePath, parseSetupAgents());
10596
+ upsertConfiguredWorkspace({
10597
+ id: "personal",
10598
+ label: "Personal",
10599
+ kind: "personal",
10600
+ path: workspacePath
10601
+ });
9756
10602
  console.log(
9757
10603
  `\x1B[32m\u2713 Local Sandbox created at: ${workspacePath}\x1B[0m`
9758
10604
  );
@@ -9903,7 +10749,7 @@ var initCommand = new Command9("init").description("Launch the guided interactiv
9903
10749
 
9904
10750
  // src/cli/commands/learn.ts
9905
10751
  import { input as input6 } from "@inquirer/prompts";
9906
- import { Command as Command10 } from "commander";
10752
+ import { Command as Command11 } from "commander";
9907
10753
 
9908
10754
  // src/cli/learn-format.ts
9909
10755
  var BLOOM_VERBS3 = {
@@ -10241,7 +11087,7 @@ var STOP_WORDS = /* @__PURE__ */ new Set(["q", ":q", "quit", "stop"]);
10241
11087
  function isExitPrompt(err) {
10242
11088
  return err instanceof Error && err.name === "ExitPromptError";
10243
11089
  }
10244
- var learnCommand = new Command10("learn").description(
11090
+ var learnCommand = new Command11("learn").description(
10245
11091
  "Run a spoiler-free, in-process learning session (recall \u2192 reveal \u2192 self-rate)"
10246
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) => {
10247
11093
  let db;
@@ -10297,7 +11143,7 @@ ${t(locale, "welcome", { count: queue.items.length })}`);
10297
11143
  question: item.question
10298
11144
  });
10299
11145
  if (healed) {
10300
- resolvedQuestion = healed;
11146
+ resolvedQuestion = healed.question;
10301
11147
  }
10302
11148
  } catch {
10303
11149
  }
@@ -10362,7 +11208,8 @@ ${"\u2500".repeat(50)}`);
10362
11208
  `
10363
11209
  ${t(locale, "feedback_title", { line: "\u2500".repeat(34) })}`
10364
11210
  );
10365
- 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")) {
10366
11213
  console.log(` ${line}`);
10367
11214
  }
10368
11215
  } catch (err) {
@@ -10485,8 +11332,8 @@ learnCommand.command("open").description("Open a new terminal window running zam
10485
11332
  });
10486
11333
 
10487
11334
  // src/cli/commands/monitor.ts
10488
- import { Command as Command11 } from "commander";
10489
- var monitorCommand = new Command11("monitor").description(
11335
+ import { Command as Command12 } from "commander";
11336
+ var monitorCommand = new Command12("monitor").description(
10490
11337
  "Shell observation for real-time task monitoring"
10491
11338
  );
10492
11339
  monitorCommand.command("start").description("Output shell hook code to install monitoring").requiredOption("--session <id>", "Session ID to monitor").option(
@@ -10549,593 +11396,244 @@ monitorCommand.command("stop").description("Output shell code to remove monitori
10549
11396
  session_id: opts.session
10550
11397
  };
10551
11398
  writeMonitorEvent(opts.session, meta);
10552
- }
10553
- let shell;
10554
- try {
10555
- shell = normalizeShell(opts.shell);
10556
- } catch (err) {
10557
- console.error(`# Error: ${err.message}`);
10558
- process.exit(1);
10559
- }
10560
- if (shell === "bash") {
10561
- console.log(generateBashUnhooks());
10562
- } else if (isPowerShellShell(shell)) {
10563
- console.log(generatePowerShellUnhooks());
10564
- } else {
10565
- console.log(generateZshUnhooks());
10566
- }
10567
- });
10568
- monitorCommand.command("status").description("Show monitoring status for a session").requiredOption("--session <id>", "Session ID").option("--json", "Output as JSON").action((opts) => {
10569
- const stats = getMonitorLogStats(opts.session);
10570
- if (!stats.exists) {
10571
- if (opts.json) {
10572
- console.log(JSON.stringify({ exists: false }));
10573
- } else {
10574
- console.log(`No monitor log found for session ${opts.session}`);
10575
- }
10576
- return;
10577
- }
10578
- const events = readMonitorLog(opts.session);
10579
- const commands = pairCommands(events);
10580
- const errors = commands.filter(
10581
- (c) => c.exitCode != null && c.exitCode !== 0
10582
- ).length;
10583
- const meta = events.find(
10584
- (e) => e.type === "monitor_meta" && e.event === "start"
10585
- );
10586
- const stopped = events.some(
10587
- (e) => e.type === "monitor_meta" && e.event === "stop"
10588
- );
10589
- const result = {
10590
- sessionId: opts.session,
10591
- exists: true,
10592
- active: !stopped,
10593
- shell: meta?.shell ?? "unknown",
10594
- totalCommands: commands.length,
10595
- errors,
10596
- sizeBytes: stats.sizeBytes,
10597
- timeSpan: commands.length > 0 ? {
10598
- start: commands[0].startedAt,
10599
- end: commands[commands.length - 1].endedAt ?? commands[commands.length - 1].startedAt
10600
- } : null
10601
- };
10602
- if (opts.json) {
10603
- console.log(JSON.stringify(result, null, 2));
10604
- return;
10605
- }
10606
- console.log(`Monitor: ${opts.session}`);
10607
- console.log(` Status: ${result.active ? "active" : "stopped"}`);
10608
- console.log(` Shell: ${result.shell}`);
10609
- console.log(` Commands: ${result.totalCommands}`);
10610
- console.log(` Errors: ${result.errors}`);
10611
- if (result.timeSpan) {
10612
- console.log(` From: ${result.timeSpan.start}`);
10613
- console.log(` To: ${result.timeSpan.end}`);
10614
- }
10615
- });
10616
- function buildMonitorSetupCommand(dir, sessionId, shell) {
10617
- const zamInvocation = resolveZamInvocation(shell);
10618
- if (isPowerShellShell(shell)) {
10619
- return [
10620
- `Set-Location -LiteralPath ${psSingleQuoted2(dir)}`,
10621
- `$__zamHook = ${zamInvocation} monitor start --session ${psSingleQuoted2(sessionId)} --shell ${shell}`,
10622
- "Invoke-Expression ($__zamHook -join [Environment]::NewLine)",
10623
- "Remove-Variable __zamHook"
10624
- ].join("; ");
10625
- }
10626
- return `cd ${JSON.stringify(dir)} && eval "$(${zamInvocation} monitor start --session ${sessionId} --shell ${shell})"`;
10627
- }
10628
- monitorCommand.command("open").description("Open a new monitored terminal window for a session").requiredOption("--session <id>", "Session ID to monitor").option("--dir <path>", "Working directory (defaults to cwd)").option(
10629
- "--shell <type>",
10630
- "Shell type: zsh | bash | pwsh | powershell (auto-detected)"
10631
- ).action(async (opts) => {
10632
- let shell;
10633
- try {
10634
- shell = normalizeShell(opts.shell);
10635
- } catch (err) {
10636
- console.error(`Error: ${err.message}`);
10637
- process.exit(1);
10638
- }
10639
- let db;
10640
- try {
10641
- db = await openDatabase();
10642
- const session = await db.prepare("SELECT id, completed_at FROM sessions WHERE id = ?").get(opts.session);
10643
- if (!session) {
10644
- console.error(`Error: Session not found: ${opts.session}`);
10645
- process.exit(1);
10646
- }
10647
- if (session.completed_at) {
10648
- console.error(`Error: Session already completed: ${opts.session}`);
10649
- process.exit(1);
10650
- }
10651
- if (!await getSetting(db, "monitor_method")) {
10652
- await setSetting(db, "monitor_method", "terminal");
10653
- }
10654
- } catch (err) {
10655
- console.error(`Error: ${err.message}`);
10656
- process.exit(1);
10657
- } finally {
10658
- await db?.close();
10659
- }
10660
- const dir = opts.dir ?? process.cwd();
10661
- const shellSetup = buildMonitorSetupCommand(dir, opts.session, shell);
10662
- openTerminalWindow({
10663
- shellSetup,
10664
- label: `monitor-${opts.session}`,
10665
- dir,
10666
- shell
10667
- });
10668
- });
10669
-
10670
- // src/cli/commands/observer.ts
10671
- import { Command as Command12 } from "commander";
10672
- var observerCommand = new Command12("observer").description(
10673
- "Configure what the UI observer may capture (Layer 2 policy)"
10674
- );
10675
- function applyObserverListChange(current, entry, op) {
10676
- const normalized = entry.trim().toLowerCase();
10677
- const list = parseObserverList(current);
10678
- const next = op === "add" ? [.../* @__PURE__ */ new Set([...list, normalized])] : list.filter((item) => item !== normalized);
10679
- return next.join(",");
10680
- }
10681
- observerCommand.command("status").description("Show the active observer policy").option("--json", "Output as JSON").action(async (opts) => {
10682
- await withDb(async (db) => {
10683
- const policy = await resolveObserverPolicy(db);
10684
- if (opts.json) {
10685
- console.log(JSON.stringify(policy, null, 2));
10686
- return;
10687
- }
10688
- console.log("Observer policy:\n");
10689
- console.log(` Scope: ${policy.scope}`);
10690
- console.log(` Consent: ${policy.consent}`);
10691
- console.log(` Retention: ${policy.retention}`);
10692
- console.log(
10693
- ` Allowlist: ${policy.allowlist.length ? policy.allowlist.join(", ") : "(any targeted window)"}`
10694
- );
10695
- console.log(
10696
- ` Denylist: ${policy.denylist.length ? policy.denylist.join(", ") : "(none)"}`
10697
- );
10698
- console.log(` Redact titles: ${policy.redactWindowTitles}`);
10699
- console.log(
10700
- "\nBuilt-in sensitive surfaces (password managers, auth/UAC dialogs, banking) are always refused and cannot be allowlisted."
10701
- );
10702
- });
10703
- });
10704
- observerCommand.command("grant <process>").description(
10705
- "Allow the observer to capture a process (adds it to observer.allowlist)"
10706
- ).action(async (processName) => {
10707
- await withDb(async (db) => {
10708
- const next = applyObserverListChange(
10709
- await getSetting(db, "observer.allowlist"),
10710
- processName,
10711
- "add"
10712
- );
10713
- await setSetting(db, "observer.allowlist", next);
10714
- await syncObserverSidecarPolicy(db);
10715
- console.log(`Granted: ${processName.trim().toLowerCase()}`);
10716
- console.log(`observer.allowlist = ${next || "(empty)"}`);
10717
- });
10718
- });
10719
- observerCommand.command("revoke <process>").description("Remove a process from observer.allowlist").action(async (processName) => {
10720
- await withDb(async (db) => {
10721
- const next = applyObserverListChange(
10722
- await getSetting(db, "observer.allowlist"),
10723
- processName,
10724
- "remove"
10725
- );
10726
- await setSetting(db, "observer.allowlist", next);
10727
- await syncObserverSidecarPolicy(db);
10728
- console.log(`Revoked: ${processName.trim().toLowerCase()}`);
10729
- console.log(`observer.allowlist = ${next || "(empty)"}`);
10730
- });
10731
- });
10732
-
10733
- // src/cli/commands/profile.ts
10734
- import { homedir as homedir12 } from "os";
10735
- import { dirname as dirname6, join as join20, resolve as resolve7 } from "path";
10736
- import { Command as Command13 } from "commander";
10737
- var C2 = {
10738
- reset: "\x1B[0m",
10739
- bold: "\x1B[1m",
10740
- cyan: "\x1B[36m",
10741
- dim: "\x1B[2m",
10742
- green: "\x1B[32m"
10743
- };
10744
- function defaultPersonalDir() {
10745
- return join20(homedir12(), "Documents", "zam");
10746
- }
10747
- function render(profile) {
10748
- 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}`;
10749
- console.log(`${C2.bold}ZAM install profile${C2.reset}`);
10750
- console.log(` mode: ${C2.cyan}${profile.mode}${C2.reset}`);
10751
- console.log(` personal dir: ${C2.cyan}${profile.personalDir}${C2.reset}`);
10752
- console.log(` sync: ${sync}`);
10753
- console.log(` data dir: ${C2.cyan}${profile.dataDir}${C2.reset}`);
10754
- console.log(` database: ${C2.cyan}${profile.dbPath}${C2.reset}`);
10755
- }
10756
- 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) => {
10757
- if (opts.mode && opts.mode !== "developer" && opts.mode !== "default") {
10758
- console.error(`Invalid --mode: ${opts.mode}. Use developer or default.`);
10759
- process.exit(1);
10760
- }
10761
- let db;
10762
- try {
10763
- if (opts.mode) setInstallMode(opts.mode);
10764
- db = await openDatabaseWithSync({ initialize: true });
10765
- if (opts.dir) {
10766
- await setSetting(db, "personal.workspace_dir", resolve7(opts.dir));
10767
- }
10768
- const personalDir = await getSetting(db, "personal.workspace_dir") || defaultPersonalDir();
10769
- await db.close();
10770
- db = void 0;
10771
- const dbPath = getDefaultDbPath();
10772
- const profile = {
10773
- mode: getInstallMode(),
10774
- personalDir,
10775
- syncProvider: detectSyncProvider(personalDir),
10776
- dataDir: dirname6(dbPath),
10777
- dbPath
10778
- };
10779
- if (opts.json) {
10780
- console.log(JSON.stringify(profile, null, 2));
10781
- return;
10782
- }
10783
- render(profile);
11399
+ }
11400
+ let shell;
11401
+ try {
11402
+ shell = normalizeShell(opts.shell);
10784
11403
  } catch (err) {
10785
- await db?.close();
10786
- console.error("Error:", err.message);
11404
+ console.error(`# Error: ${err.message}`);
10787
11405
  process.exit(1);
10788
11406
  }
10789
- });
10790
-
10791
- // src/cli/commands/provider.ts
10792
- import { password as password2 } from "@inquirer/prompts";
10793
- import { Command as Command14 } from "commander";
10794
- var VALID_API_FLAVORS = [
10795
- "chat-completions",
10796
- "anthropic-messages"
10797
- ];
10798
- var VALID_ROLES = ["vision", "recall", "text"];
10799
- function upsertProviderRecord(providers, name, patch) {
10800
- const merged = { ...providers[name] ?? {} };
10801
- if (patch.url !== void 0) merged.url = patch.url;
10802
- if (patch.model !== void 0) merged.model = patch.model;
10803
- if (patch.apiFlavor !== void 0) merged.apiFlavor = patch.apiFlavor;
10804
- if (patch.apiKeyRef !== void 0) merged.apiKeyRef = patch.apiKeyRef;
10805
- if (patch.label !== void 0) merged.label = patch.label;
10806
- if (patch.local !== void 0) merged.local = patch.local;
10807
- if (patch.runner !== void 0) merged.runner = patch.runner;
10808
- return { ...providers, [name]: merged };
10809
- }
10810
- function removeProviderRecord(providers, name) {
10811
- if (!(name in providers)) return { providers, removed: false };
10812
- const next = { ...providers };
10813
- delete next[name];
10814
- return { providers: next, removed: true };
10815
- }
10816
- function rolesReferencing(roles, name) {
10817
- return VALID_ROLES.filter((role) => {
10818
- const binding = roles[role];
10819
- return binding?.primary === name || binding?.fallback === name;
10820
- });
10821
- }
10822
- function bindRoleProviders(roles, role, primary, fallback) {
10823
- const binding = { primary };
10824
- if (fallback) binding.fallback = fallback;
10825
- return { ...roles, [role]: binding };
10826
- }
10827
- function maskSecret(key) {
10828
- return key.length <= 4 ? "\u2022\u2022\u2022\u2022" : `\u2026${key.slice(-4)}`;
10829
- }
10830
- function buildProviderListing(providers, hasKey) {
10831
- return Object.entries(providers).map(([name, rec]) => {
10832
- const apiFlavor = rec.apiFlavor ?? (rec.url ? inferApiFlavor(rec.url) : "chat-completions");
10833
- let keyState;
10834
- if (!rec.apiKeyRef) keyState = "none";
10835
- else keyState = hasKey(rec.apiKeyRef) ? "set" : "missing";
10836
- const row = {
10837
- name,
10838
- url: rec.url,
10839
- model: rec.model,
10840
- apiFlavor,
10841
- apiKeyRef: rec.apiKeyRef,
10842
- keyState
10843
- };
10844
- if (rec.label !== void 0) row.label = rec.label;
10845
- if (rec.local !== void 0) row.local = rec.local;
10846
- if (rec.runner !== void 0) row.runner = rec.runner;
10847
- return row;
10848
- });
10849
- }
10850
- function findOrphanKeyRefs(storedRefs, providers) {
10851
- const used = new Set(
10852
- Object.values(providers).map((rec) => rec.apiKeyRef).filter((ref) => !!ref)
10853
- );
10854
- return storedRefs.filter((ref) => !used.has(ref));
10855
- }
10856
- async function readJson(db, key, fallback) {
10857
- const raw = await getSetting(db, key);
10858
- if (!raw) return fallback;
10859
- try {
10860
- return JSON.parse(raw);
10861
- } catch {
10862
- return fallback;
11407
+ if (shell === "bash") {
11408
+ console.log(generateBashUnhooks());
11409
+ } else if (isPowerShellShell(shell)) {
11410
+ console.log(generatePowerShellUnhooks());
11411
+ } else {
11412
+ console.log(generateZshUnhooks());
10863
11413
  }
10864
- }
10865
- var readProviders = (db) => readJson(db, "llm.providers", {});
10866
- var readRoles = (db) => readJson(db, "llm.roles", {});
10867
- async function readScopedProviders(db, machine) {
10868
- if (machine) return getMachineAiConfig().providers ?? {};
10869
- if (!db)
10870
- throw new Error("Database is required for shared provider settings.");
10871
- return readProviders(db);
10872
- }
10873
- async function readScopedRoles(db, machine) {
10874
- if (machine) return getMachineAiConfig().roles ?? {};
10875
- if (!db)
10876
- throw new Error("Database is required for shared provider settings.");
10877
- return readRoles(db);
10878
- }
10879
- async function writeScopedProviders(db, machine, p) {
10880
- if (machine) {
10881
- const config = getMachineAiConfig();
10882
- saveMachineAiConfig({ ...config, providers: p });
11414
+ });
11415
+ monitorCommand.command("status").description("Show monitoring status for a session").requiredOption("--session <id>", "Session ID").option("--json", "Output as JSON").action((opts) => {
11416
+ const stats = getMonitorLogStats(opts.session);
11417
+ if (!stats.exists) {
11418
+ if (opts.json) {
11419
+ console.log(JSON.stringify({ exists: false }));
11420
+ } else {
11421
+ console.log(`No monitor log found for session ${opts.session}`);
11422
+ }
10883
11423
  return;
10884
11424
  }
10885
- if (!db)
10886
- throw new Error("Database is required for shared provider settings.");
10887
- await setSetting(db, "llm.providers", JSON.stringify(p));
10888
- }
10889
- async function writeScopedRoles(db, machine, r) {
10890
- if (machine) {
10891
- const config = getMachineAiConfig();
10892
- saveMachineAiConfig({ ...config, roles: r });
11425
+ const events = readMonitorLog(opts.session);
11426
+ const commands = pairCommands(events);
11427
+ const errors = commands.filter(
11428
+ (c) => c.exitCode != null && c.exitCode !== 0
11429
+ ).length;
11430
+ const meta = events.find(
11431
+ (e) => e.type === "monitor_meta" && e.event === "start"
11432
+ );
11433
+ const stopped = events.some(
11434
+ (e) => e.type === "monitor_meta" && e.event === "stop"
11435
+ );
11436
+ const result = {
11437
+ sessionId: opts.session,
11438
+ exists: true,
11439
+ active: !stopped,
11440
+ shell: meta?.shell ?? "unknown",
11441
+ totalCommands: commands.length,
11442
+ errors,
11443
+ sizeBytes: stats.sizeBytes,
11444
+ timeSpan: commands.length > 0 ? {
11445
+ start: commands[0].startedAt,
11446
+ end: commands[commands.length - 1].endedAt ?? commands[commands.length - 1].startedAt
11447
+ } : null
11448
+ };
11449
+ if (opts.json) {
11450
+ console.log(JSON.stringify(result, null, 2));
10893
11451
  return;
10894
11452
  }
10895
- if (!db)
10896
- throw new Error("Database is required for shared provider settings.");
10897
- await setSetting(db, "llm.roles", JSON.stringify(r));
10898
- }
10899
- async function withProviderScope(machine, action) {
10900
- if (machine) {
10901
- await action(void 0);
10902
- return;
11453
+ console.log(`Monitor: ${opts.session}`);
11454
+ console.log(` Status: ${result.active ? "active" : "stopped"}`);
11455
+ console.log(` Shell: ${result.shell}`);
11456
+ console.log(` Commands: ${result.totalCommands}`);
11457
+ console.log(` Errors: ${result.errors}`);
11458
+ if (result.timeSpan) {
11459
+ console.log(` From: ${result.timeSpan.start}`);
11460
+ console.log(` To: ${result.timeSpan.end}`);
10903
11461
  }
10904
- await withDb(action);
11462
+ });
11463
+ function buildMonitorSetupCommand(dir, sessionId, shell) {
11464
+ const zamInvocation = resolveZamInvocation(shell);
11465
+ if (isPowerShellShell(shell)) {
11466
+ return [
11467
+ `Set-Location -LiteralPath ${psSingleQuoted2(dir)}`,
11468
+ `$__zamHook = ${zamInvocation} monitor start --session ${psSingleQuoted2(sessionId)} --shell ${shell}`,
11469
+ "Invoke-Expression ($__zamHook -join [Environment]::NewLine)",
11470
+ "Remove-Variable __zamHook"
11471
+ ].join("; ");
11472
+ }
11473
+ return `cd ${JSON.stringify(dir)} && eval "$(${zamInvocation} monitor start --session ${sessionId} --shell ${shell})"`;
10905
11474
  }
10906
- var providerCommand = new Command14("provider").description(
10907
- "Configure role-based AI providers (url/model/flavor/key, per role)"
10908
- );
10909
- 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) => {
10910
- const machine = Boolean(opts.machine);
10911
- await withProviderScope(machine, async (db) => {
10912
- const providers = await readScopedProviders(db, machine);
10913
- const roles = await readScopedRoles(db, machine);
10914
- const rows = buildProviderListing(
10915
- providers,
10916
- (ref) => getProviderApiKey(ref) !== null
10917
- );
10918
- const orphans = findOrphanKeyRefs(listProviderApiKeyRefs(), providers);
10919
- if (opts.json) {
10920
- console.log(
10921
- JSON.stringify(
10922
- {
10923
- scope: machine ? "machine" : "shared",
10924
- providers: rows,
10925
- roles,
10926
- orphans
10927
- },
10928
- null,
10929
- 2
10930
- )
10931
- );
10932
- return;
10933
- }
10934
- console.log(
10935
- `Providers (${opts.machine ? "~/.zam/config.json ai.providers" : "llm.providers"}):
10936
- `
10937
- );
10938
- if (rows.length === 0) {
10939
- console.log(
10940
- " (none) \u2014 add one: zam provider add <name> --url <url> --model <model>"
10941
- );
10942
- } else {
10943
- for (const row of rows) {
10944
- 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";
10945
- console.log(` \x1B[36m${row.name.padEnd(12)}\x1B[0m ${key}`);
10946
- console.log(` url: ${row.url ?? "(inherits llm.url)"}`);
10947
- console.log(` model: ${row.model ?? "(inherits llm.model)"}`);
10948
- console.log(` flavor: ${row.apiFlavor}`);
10949
- if (row.label) console.log(` label: ${row.label}`);
10950
- if (row.local !== void 0) {
10951
- console.log(` local: ${row.local ? "yes" : "no"}`);
10952
- }
10953
- if (row.runner) console.log(` runner: ${row.runner}`);
10954
- if (row.apiKeyRef) console.log(` key-ref: ${row.apiKeyRef}`);
10955
- }
11475
+ monitorCommand.command("open").description("Open a new monitored terminal window for a session").requiredOption("--session <id>", "Session ID to monitor").option("--dir <path>", "Working directory (defaults to cwd)").option(
11476
+ "--shell <type>",
11477
+ "Shell type: zsh | bash | pwsh | powershell (auto-detected)"
11478
+ ).action(async (opts) => {
11479
+ let shell;
11480
+ try {
11481
+ shell = normalizeShell(opts.shell);
11482
+ } catch (err) {
11483
+ console.error(`Error: ${err.message}`);
11484
+ process.exit(1);
11485
+ }
11486
+ let db;
11487
+ try {
11488
+ db = await openDatabase();
11489
+ const session = await db.prepare("SELECT id, completed_at FROM sessions WHERE id = ?").get(opts.session);
11490
+ if (!session) {
11491
+ console.error(`Error: Session not found: ${opts.session}`);
11492
+ process.exit(1);
10956
11493
  }
10957
- console.log("\nRoles (llm.roles):\n");
10958
- for (const role of VALID_ROLES) {
10959
- const binding = roles[role];
10960
- if (!binding?.primary) {
10961
- console.log(` ${role.padEnd(7)} \x1B[90m(unset)\x1B[0m`);
10962
- } else {
10963
- const fb = binding.fallback ? ` \u2192 fallback: ${binding.fallback}` : "";
10964
- console.log(` ${role.padEnd(7)} primary: ${binding.primary}${fb}`);
10965
- }
11494
+ if (session.completed_at) {
11495
+ console.error(`Error: Session already completed: ${opts.session}`);
11496
+ process.exit(1);
10966
11497
  }
10967
- if (orphans.length > 0) {
10968
- console.log(
10969
- `
10970
- \x1B[33mOrphan keys (stored but unreferenced):\x1B[0m ${orphans.join(", ")}`
10971
- );
11498
+ if (!await getSetting(db, "monitor_method")) {
11499
+ await setSetting(db, "monitor_method", "terminal");
10972
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
10973
11514
  });
10974
11515
  });
10975
- 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(
10976
- "--runner <runner>",
10977
- "Local runner hint (flm, foundry-local, ollama, ...)"
10978
- ).option(
10979
- "--flavor <flavor>",
10980
- `Wire protocol: ${VALID_API_FLAVORS.join(" | ")} (default: inferred from URL)`
10981
- ).option(
10982
- "--key-ref <ref>",
10983
- "Credential reference for the API key (default: <name> when --key is given)"
10984
- ).option(
10985
- "--key <value>",
10986
- "Store this API key now (prefer `set-key` to keep it out of shell history)"
10987
- ).option("--machine", "Store this provider in ~/.zam/config.json").action(async (name, opts) => {
10988
- let apiFlavor;
10989
- if (opts.flavor) {
10990
- if (!VALID_API_FLAVORS.includes(opts.flavor)) {
10991
- console.error(
10992
- `Invalid --flavor: ${opts.flavor}. Use ${VALID_API_FLAVORS.join(" or ")}.`
10993
- );
10994
- 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;
10995
11534
  }
10996
- apiFlavor = opts.flavor;
10997
- }
10998
- const apiKeyRef = opts.keyRef ?? (opts.key ? name : void 0);
10999
- const machine = Boolean(opts.machine);
11000
- await withProviderScope(machine, async (db) => {
11001
- const providers = await readScopedProviders(db, machine);
11002
- const next = upsertProviderRecord(providers, name, {
11003
- label: opts.label,
11004
- url: opts.url,
11005
- model: opts.model,
11006
- apiFlavor,
11007
- apiKeyRef,
11008
- local: opts.local ? true : void 0,
11009
- runner: opts.runner
11010
- });
11011
- await writeScopedProviders(db, machine, next);
11012
- if (opts.key && apiKeyRef) setProviderApiKey(apiKeyRef, opts.key);
11013
- 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}`);
11014
11539
  console.log(
11015
- `Provider "${name}" saved (${machine ? "machine-local" : "shared"}):`
11540
+ ` Allowlist: ${policy.allowlist.length ? policy.allowlist.join(", ") : "(any targeted window)"}`
11016
11541
  );
11017
- console.log(` url: ${rec.url ?? "(inherits llm.url)"}`);
11018
- console.log(` model: ${rec.model ?? "(inherits llm.model)"}`);
11019
- if (rec.label) console.log(` label: ${rec.label}`);
11020
- if (rec.local !== void 0) {
11021
- console.log(` local: ${rec.local ? "yes" : "no"}`);
11022
- }
11023
- if (rec.runner) console.log(` runner: ${rec.runner}`);
11024
11542
  console.log(
11025
- ` flavor: ${rec.apiFlavor ?? `${rec.url ? inferApiFlavor(rec.url) : "chat-completions"} (inferred)`}`
11543
+ ` Denylist: ${policy.denylist.length ? policy.denylist.join(", ") : "(none)"}`
11026
11544
  );
11027
- console.log(` key-ref: ${rec.apiKeyRef ?? "(none \u2014 uses default key)"}`);
11028
- if (opts.key) {
11029
- console.log(` key: stored (${maskSecret(opts.key)})`);
11030
- } else if (rec.apiKeyRef && getProviderApiKey(rec.apiKeyRef) === null) {
11031
- console.log(
11032
- `
11033
- \u26A0 No key stored for "${rec.apiKeyRef}". Run: zam provider set-key ${rec.apiKeyRef}`
11034
- );
11035
- }
11036
- if (!rec.url) {
11037
- console.log(
11038
- "\n \u26A0 No --url set; this provider inherits the base llm.url."
11039
- );
11040
- }
11545
+ console.log(` Redact titles: ${policy.redactWindowTitles}`);
11041
11546
  console.log(
11042
- `
11043
- 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."
11044
11548
  );
11045
11549
  });
11046
11550
  });
11047
- 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) => {
11048
- const machine = Boolean(opts.machine);
11049
- await withProviderScope(machine, async (db) => {
11050
- const providers = await readScopedProviders(db, machine);
11051
- const { providers: next, removed } = removeProviderRecord(
11052
- providers,
11053
- name
11054
- );
11055
- if (!removed) {
11056
- console.log(`No such provider: ${name}`);
11057
- return;
11058
- }
11059
- await writeScopedProviders(db, machine, next);
11060
- console.log(
11061
- `Removed provider "${name}" (${machine ? "machine-local" : "shared"}).`
11062
- );
11063
- const referencing = rolesReferencing(
11064
- await readScopedRoles(db, machine),
11065
- 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"
11066
11559
  );
11067
- if (referencing.length > 0) {
11068
- console.log(
11069
- ` \u26A0 Still referenced by role(s): ${referencing.join(", ")} \u2014 rebind with: zam provider use <role> --primary <name>`
11070
- );
11071
- }
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)"}`);
11072
11564
  });
11073
11565
  });
11074
- 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) => {
11075
- if (!VALID_ROLES.includes(role)) {
11076
- console.error(`Invalid role: ${role}. Use ${VALID_ROLES.join(", ")}.`);
11077
- process.exit(1);
11078
- }
11079
- if (!opts.primary) {
11080
- console.error("--primary is required.");
11081
- process.exit(1);
11082
- }
11083
- const machine = Boolean(opts.machine);
11084
- await withProviderScope(machine, async (db) => {
11085
- const providers = await readScopedProviders(db, machine);
11086
- await writeScopedRoles(
11087
- db,
11088
- machine,
11089
- bindRoleProviders(
11090
- await readScopedRoles(db, machine),
11091
- role,
11092
- opts.primary,
11093
- opts.fallback
11094
- )
11095
- );
11096
- const fb = opts.fallback ? `, fallback: ${opts.fallback}` : "";
11097
- console.log(
11098
- `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"
11099
11572
  );
11100
- for (const [label, ref] of [
11101
- ["primary", opts.primary],
11102
- ["fallback", opts.fallback]
11103
- ]) {
11104
- if (ref && !(ref in providers)) {
11105
- console.log(
11106
- ` \u26A0 ${label} "${ref}" is not a defined provider yet. Add it: zam provider add ${ref} --url <url> --model <model>`
11107
- );
11108
- }
11109
- }
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)"}`);
11110
11577
  });
11111
11578
  });
11112
- providerCommand.command("set-key <ref>").description(
11113
- "Store an API key for a provider reference (in credentials.json)"
11114
- ).option(
11115
- "--key <value>",
11116
- "The API key (omit to enter it interactively, hidden)"
11117
- ).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;
11118
11609
  try {
11119
- const key = opts.key ?? await password2({ message: `API key for "${ref}":` });
11120
- if (!key || key.trim().length === 0) {
11121
- console.error("No key provided.");
11122
- 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));
11123
11614
  }
11124
- setProviderApiKey(ref, key.trim());
11125
- console.log(`Stored API key for "${ref}" (${maskSecret(key.trim())}).`);
11126
- } catch (err) {
11127
- if (err.name === "ExitPromptError") {
11128
- console.log("\nCancelled.");
11129
- 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;
11130
11629
  }
11630
+ render(profile);
11631
+ } catch (err) {
11632
+ await db?.close();
11131
11633
  console.error("Error:", err.message);
11132
11634
  process.exit(1);
11133
11635
  }
11134
11636
  });
11135
- providerCommand.command("clear-key <ref>").description("Remove a stored API key").action((ref) => {
11136
- clearProviderApiKey(ref);
11137
- console.log(`Cleared API key for "${ref}".`);
11138
- });
11139
11637
 
11140
11638
  // src/cli/commands/review.ts
11141
11639
  import { Command as Command15 } from "commander";
@@ -11897,7 +12395,7 @@ skillCommand.command("add").description("Register a new agent skill").requiredOp
11897
12395
  });
11898
12396
 
11899
12397
  // src/cli/commands/snapshot.ts
11900
- import { existsSync as existsSync22, mkdirSync as mkdirSync12, readFileSync as readFileSync13, writeFileSync as writeFileSync11 } from "fs";
12398
+ import { existsSync as existsSync22, mkdirSync as mkdirSync13, readFileSync as readFileSync13, writeFileSync as writeFileSync11 } from "fs";
11901
12399
  import { homedir as homedir13 } from "os";
11902
12400
  import { dirname as dirname7, join as join21 } from "path";
11903
12401
  import { Command as Command19 } from "commander";
@@ -11931,7 +12429,7 @@ var exportCmd = new Command19("export").description("Write a portable SQL-text s
11931
12429
  const out = opts.out ?? join21(personalDir, "snapshots", defaultOutName());
11932
12430
  const dir = dirname7(out);
11933
12431
  if (dir && dir !== "." && !existsSync22(dir)) {
11934
- mkdirSync12(dir, { recursive: true });
12432
+ mkdirSync13(dir, { recursive: true });
11935
12433
  }
11936
12434
  writeFileSync11(out, snapshot, "utf-8");
11937
12435
  console.log(`Snapshot written: ${out}`);
@@ -12601,7 +13099,7 @@ ${C3.dim}After that, 'zam ui' launches it directly.${C3.reset}`
12601
13099
 
12602
13100
  // src/cli/commands/update.ts
12603
13101
  import { spawnSync as spawnSync2 } from "child_process";
12604
- import { existsSync as existsSync24, readFileSync as readFileSync14, realpathSync } from "fs";
13102
+ import { existsSync as existsSync24, readFileSync as readFileSync14, realpathSync as realpathSync2 } from "fs";
12605
13103
  import { dirname as dirname9, join as join23 } from "path";
12606
13104
  import { fileURLToPath as fileURLToPath4 } from "url";
12607
13105
  import { confirm as confirm4 } from "@inquirer/prompts";
@@ -12715,7 +13213,7 @@ var checkCmd = new Command23("check").description("Check whether a newer ZAM has
12715
13213
  }
12716
13214
  );
12717
13215
  function findSourceRepo() {
12718
- let dir = realpathSync(dirname9(fileURLToPath4(import.meta.url)));
13216
+ let dir = realpathSync2(dirname9(fileURLToPath4(import.meta.url)));
12719
13217
  let parent = dirname9(dir);
12720
13218
  while (parent !== dir) {
12721
13219
  if (existsSync24(join23(dir, ".git"))) return dir;