zam-core 0.5.1 → 0.5.3

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
@@ -2,7 +2,7 @@
2
2
 
3
3
  // src/cli/index.ts
4
4
  import { readFileSync as readFileSync15 } from "fs";
5
- import { dirname as dirname10, join as join24 } from "path";
5
+ import { dirname as dirname10, join as join25 } from "path";
6
6
  import { fileURLToPath as fileURLToPath5 } from "url";
7
7
  import { Command as Command25 } from "commander";
8
8
 
@@ -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();
@@ -4463,25 +4478,41 @@ function saveMachineAiConfig(ai, path = defaultConfigPath()) {
4463
4478
  function getConfiguredWorkspaces(path = defaultConfigPath()) {
4464
4479
  return loadInstallConfig(path).workspaces ?? [];
4465
4480
  }
4466
- function saveConfiguredWorkspaces(workspaces, path = defaultConfigPath()) {
4481
+ function setActiveWorkspaceId(id, path = defaultConfigPath()) {
4467
4482
  const config = loadInstallConfig(path);
4468
- config.workspaces = workspaces;
4483
+ if (id) {
4484
+ config.activeWorkspaceId = id;
4485
+ } else {
4486
+ delete config.activeWorkspaceId;
4487
+ }
4469
4488
  saveInstallConfig(config, path);
4470
4489
  }
4490
+ function getActiveWorkspace(path = defaultConfigPath()) {
4491
+ const config = loadInstallConfig(path);
4492
+ const id = config.activeWorkspaceId;
4493
+ return id ? config.workspaces?.find((workspace) => workspace.id === id) : void 0;
4494
+ }
4471
4495
  function upsertConfiguredWorkspace(workspace, path = defaultConfigPath()) {
4472
- const current = getConfiguredWorkspaces(path);
4496
+ const config = loadInstallConfig(path);
4497
+ const current = config.workspaces ?? [];
4473
4498
  const next = [
4474
4499
  ...current.filter((candidate) => candidate.id !== workspace.id),
4475
4500
  workspace
4476
4501
  ];
4477
- saveConfiguredWorkspaces(next, path);
4502
+ config.workspaces = next;
4503
+ saveInstallConfig(config, path);
4478
4504
  return next;
4479
4505
  }
4480
4506
  function removeConfiguredWorkspace(id, path = defaultConfigPath()) {
4481
- const next = getConfiguredWorkspaces(path).filter(
4507
+ const config = loadInstallConfig(path);
4508
+ const next = (config.workspaces ?? []).filter(
4482
4509
  (workspace) => workspace.id !== id
4483
4510
  );
4484
- saveConfiguredWorkspaces(next, path);
4511
+ config.workspaces = next;
4512
+ if (config.activeWorkspaceId === id) {
4513
+ config.activeWorkspaceId = next[0]?.id;
4514
+ }
4515
+ saveInstallConfig(config, path);
4485
4516
  return next;
4486
4517
  }
4487
4518
  function detectSyncProvider(dir) {
@@ -4813,7 +4844,7 @@ function getSystemProfile() {
4813
4844
  import { existsSync as existsSync10 } from "fs";
4814
4845
  import { resolve as resolve2 } from "path";
4815
4846
  async function getRepoPaths(db) {
4816
- const personalSetting = await getSetting(db, "repo.personal") || await getSetting(db, "personal.workspace_dir");
4847
+ const personalSetting = await getSetting(db, "repo.personal") || getActiveWorkspace()?.path;
4817
4848
  const teamSetting = await getSetting(db, "repo.team");
4818
4849
  const orgSetting = await getSetting(db, "repo.org");
4819
4850
  return {
@@ -5357,23 +5388,33 @@ var openCmd = new Command("open").description("Open an agent harness in the work
5357
5388
  var agentCommand = new Command("agent").description("Provision and inspect the agent that drives ZAM sessions").addCommand(installCmd).addCommand(statusCmd).addCommand(openCmd).addCommand(listCmd).action(printStatus);
5358
5389
 
5359
5390
  // src/cli/commands/bridge.ts
5360
- import { execFileSync as execFileSync4 } from "child_process";
5391
+ import { execFileSync as execFileSync3 } from "child_process";
5361
5392
  import { randomBytes as randomBytes2 } from "crypto";
5362
- import {
5363
- existsSync as existsSync17,
5364
- mkdirSync as mkdirSync10,
5365
- readdirSync as readdirSync3,
5366
- readFileSync as readFileSync11,
5367
- rmSync as rmSync3
5368
- } from "fs";
5393
+ import { existsSync as existsSync17, readdirSync as readdirSync2, readFileSync as readFileSync11, rmSync as rmSync3 } from "fs";
5369
5394
  import { homedir as homedir10, tmpdir as tmpdir3 } from "os";
5370
- import { basename as basename5, join as join17, resolve as resolve5 } from "path";
5371
- import { Command as Command4 } from "commander";
5395
+ import { join as join18, resolve as resolve4 } from "path";
5396
+ import { Command as Command2 } from "commander";
5372
5397
 
5373
5398
  // src/cli/llm/client.ts
5374
5399
  import { spawn as spawn2 } from "child_process";
5375
5400
  import { existsSync as existsSync14 } from "fs";
5376
5401
  var DEFAULT_LLM_URL = "http://localhost:8000/v1";
5402
+ var DEFAULT_LLM_MAX_TOKENS = 1e4;
5403
+ var RECALL_QUESTION_MAX_OUTPUT_TOKENS = 400;
5404
+ var RECALL_EVALUATION_MAX_OUTPUT_TOKENS = 1200;
5405
+ var RECALL_ENDPOINT_CACHE_MS = 6e4;
5406
+ var cachedRecallEndpoint = null;
5407
+ function recallEndpointSignature(cfg) {
5408
+ return [
5409
+ cfg.enabled ? "on" : "off",
5410
+ cfg.providerName ?? "",
5411
+ cfg.url,
5412
+ cfg.model,
5413
+ cfg.apiFlavor,
5414
+ cfg.fallback?.url ?? "",
5415
+ cfg.fallback?.model ?? ""
5416
+ ].join("|");
5417
+ }
5377
5418
  var DEFAULT_LLM_MODEL = "qwen3.5:4b";
5378
5419
  var DEFAULT_LLM_API_KEY = "sk-none";
5379
5420
  async function getLlmConfig(db) {
@@ -5388,19 +5429,25 @@ async function getLlmConfig(db) {
5388
5429
  function getCloudModelRecommendation(url) {
5389
5430
  const lowercase = url.toLowerCase();
5390
5431
  if (lowercase.includes("openrouter.ai")) {
5391
- return "openrouter/free";
5432
+ return { model: "openrouter/free", flavor: "chat-completions" };
5392
5433
  }
5393
5434
  if (lowercase.includes("openai.com") || lowercase.includes("api.openai")) {
5394
- return "gpt-5-mini";
5435
+ return { model: "gpt-5-mini", flavor: "chat-completions" };
5395
5436
  }
5396
5437
  if (lowercase.includes("googleapis.com") || lowercase.includes("google")) {
5397
- return "gemini-3.5-flash";
5438
+ return { model: "gemini-3.5-flash", flavor: "chat-completions" };
5398
5439
  }
5399
5440
  if (lowercase.includes("deepseek.com")) {
5400
- return "deepseek-v4-flash";
5441
+ return { model: "deepseek-v4-flash", flavor: "chat-completions" };
5401
5442
  }
5402
5443
  if (lowercase.includes("mimo")) {
5403
- return "mimo-v2.5";
5444
+ return { model: "mimo-v2.5", flavor: "chat-completions" };
5445
+ }
5446
+ if (lowercase.includes("anthropic.com")) {
5447
+ return {
5448
+ model: "claude-haiku-4-5-20251001",
5449
+ flavor: "anthropic-messages"
5450
+ };
5404
5451
  }
5405
5452
  return null;
5406
5453
  }
@@ -5484,6 +5531,7 @@ function materializeProvider(rec, base, role, meta) {
5484
5531
  label: rec.label,
5485
5532
  source: meta.source,
5486
5533
  local: rec.local ?? isLocalEndpoint(url),
5534
+ ...rec.runner ? { runner: rec.runner } : {},
5487
5535
  ...role === "vision" ? { maxFrames: base.maxFrames } : {}
5488
5536
  };
5489
5537
  }
@@ -5493,14 +5541,15 @@ async function getLegacyRoleConfig(db, role, enabled) {
5493
5541
  const maxFramesStr = await getSetting(db, "llm.vision.max_frames");
5494
5542
  const parsed = maxFramesStr ? parseInt(maxFramesStr, 10) : 100;
5495
5543
  const url = await getSetting(db, "llm.vision.url") || base.url;
5544
+ const recommendation = getCloudModelRecommendation(url);
5496
5545
  let model = await getSetting(db, "llm.vision.model");
5497
- if (!model) model = getCloudModelRecommendation(url) || base.model;
5546
+ if (!model) model = recommendation?.model || base.model;
5498
5547
  return {
5499
5548
  enabled,
5500
5549
  url,
5501
5550
  model,
5502
5551
  apiKey: await getSetting(db, "llm.vision.api_key") || base.apiKey,
5503
- apiFlavor: inferApiFlavor(url),
5552
+ apiFlavor: recommendation?.flavor || inferApiFlavor(url),
5504
5553
  locale: base.locale,
5505
5554
  source: "legacy",
5506
5555
  local: isLocalEndpoint(url),
@@ -5566,10 +5615,7 @@ async function readChatContent(res, label) {
5566
5615
  }
5567
5616
  async function generateQuestionViaLLM(db, input8) {
5568
5617
  const cfg = await getProviderForRole(db, "recall");
5569
- if (!cfg.enabled) {
5570
- throw new Error("LLM integration is disabled in settings (llm.enabled)");
5571
- }
5572
- assertChatCompletions(cfg);
5618
+ const endpoint = await resolveUsableRecallEndpoint(db);
5573
5619
  const bloom = input8.bloomLevel >= 1 && input8.bloomLevel <= 5 ? input8.bloomLevel : 1;
5574
5620
  const verb = BLOOM_VERBS2[bloom];
5575
5621
  const langName = LANGUAGE_NAMES[cfg.locale] || "English";
@@ -5589,31 +5635,36 @@ ${input8.sourceLinkContent ? `Source Reference:
5589
5635
  ${input8.sourceLinkContent}` : ""}
5590
5636
 
5591
5637
  Active-Recall Question:`;
5592
- const res = await fetchWithInteractiveTimeout(`${cfg.url}/chat/completions`, {
5593
- method: "POST",
5594
- headers: {
5595
- "Content-Type": "application/json",
5596
- Authorization: `Bearer ${cfg.apiKey}`
5597
- },
5598
- body: JSON.stringify({
5599
- model: cfg.model,
5600
- messages: [
5601
- { role: "system", content: systemPrompt },
5602
- { role: "user", content: userPrompt }
5603
- ],
5604
- temperature: 0.1,
5605
- max_tokens: 150
5606
- }),
5607
- locale: cfg.locale
5608
- });
5609
- return readChatContent(res, "LLM request");
5638
+ const res = await fetchWithInteractiveTimeout(
5639
+ `${endpoint.url}/chat/completions`,
5640
+ {
5641
+ method: "POST",
5642
+ headers: {
5643
+ "Content-Type": "application/json",
5644
+ Authorization: `Bearer ${endpoint.apiKey}`
5645
+ },
5646
+ body: JSON.stringify({
5647
+ model: endpoint.model,
5648
+ messages: [
5649
+ { role: "system", content: systemPrompt },
5650
+ { role: "user", content: userPrompt }
5651
+ ],
5652
+ temperature: 0.1,
5653
+ max_tokens: RECALL_QUESTION_MAX_OUTPUT_TOKENS
5654
+ }),
5655
+ locale: cfg.locale
5656
+ }
5657
+ );
5658
+ const text = await readChatContent(res, "LLM request");
5659
+ return {
5660
+ text,
5661
+ model: endpoint.model,
5662
+ providerName: endpoint.providerName
5663
+ };
5610
5664
  }
5611
5665
  async function evaluateAnswerViaLLM(db, input8) {
5612
5666
  const cfg = await getProviderForRole(db, "recall");
5613
- if (!cfg.enabled) {
5614
- throw new Error("LLM integration is disabled in settings (llm.enabled)");
5615
- }
5616
- assertChatCompletions(cfg);
5667
+ const endpoint = await resolveUsableRecallEndpoint(db);
5617
5668
  const langName = LANGUAGE_NAMES[cfg.locale] || "English";
5618
5669
  const ratingPrefix = LOCALIZED_RATING_PREFIX[cfg.locale] || "Suggested rating";
5619
5670
  const systemPrompt = `You are ZAM, an extremely warm, encouraging, and patient skills trainer.
@@ -5643,51 +5694,59 @@ ${input8.sourceLinkContent ? `Source Code Reference:
5643
5694
  ${input8.sourceLinkContent}` : ""}
5644
5695
 
5645
5696
  Evaluation:`;
5646
- const res = await fetchWithInteractiveTimeout(`${cfg.url}/chat/completions`, {
5647
- method: "POST",
5648
- headers: {
5649
- "Content-Type": "application/json",
5650
- Authorization: `Bearer ${cfg.apiKey}`
5651
- },
5652
- body: JSON.stringify({
5653
- model: cfg.model,
5654
- messages: [
5655
- { role: "system", content: systemPrompt },
5656
- { role: "user", content: userPrompt }
5657
- ],
5658
- temperature: 0.2,
5659
- max_tokens: 300
5660
- }),
5661
- locale: cfg.locale
5662
- });
5663
- return readChatContent(res, "LLM evaluation");
5697
+ const res = await fetchWithInteractiveTimeout(
5698
+ `${endpoint.url}/chat/completions`,
5699
+ {
5700
+ method: "POST",
5701
+ headers: {
5702
+ "Content-Type": "application/json",
5703
+ Authorization: `Bearer ${endpoint.apiKey}`
5704
+ },
5705
+ body: JSON.stringify({
5706
+ model: endpoint.model,
5707
+ messages: [
5708
+ { role: "system", content: systemPrompt },
5709
+ { role: "user", content: userPrompt }
5710
+ ],
5711
+ temperature: 0.2,
5712
+ max_tokens: RECALL_EVALUATION_MAX_OUTPUT_TOKENS
5713
+ }),
5714
+ locale: cfg.locale
5715
+ }
5716
+ );
5717
+ const text = await readChatContent(res, "LLM evaluation");
5718
+ return {
5719
+ text,
5720
+ model: endpoint.model,
5721
+ providerName: endpoint.providerName
5722
+ };
5664
5723
  }
5665
5724
  async function translateQuestionViaLLM(db, question) {
5666
5725
  const cfg = await getProviderForRole(db, "recall");
5667
- if (!cfg.enabled) {
5668
- throw new Error("LLM integration is disabled in settings");
5669
- }
5670
- assertChatCompletions(cfg);
5726
+ const endpoint = await resolveUsableRecallEndpoint(db);
5671
5727
  const targetLang = LANGUAGE_NAMES[cfg.locale] || "English";
5672
5728
  const systemPrompt = `You are a highly precise translator. Translate the given active-recall question into clear, natural ${targetLang}.
5673
5729
  Output ONLY the raw translation. Do not include any headers, preamble, quotes, or conversational filler.`;
5674
- const res = await fetchWithInteractiveTimeout(`${cfg.url}/chat/completions`, {
5675
- method: "POST",
5676
- headers: {
5677
- "Content-Type": "application/json",
5678
- Authorization: `Bearer ${cfg.apiKey}`
5679
- },
5680
- body: JSON.stringify({
5681
- model: cfg.model,
5682
- messages: [
5683
- { role: "system", content: systemPrompt },
5684
- { role: "user", content: question }
5685
- ],
5686
- temperature: 0.1,
5687
- max_tokens: 150
5688
- }),
5689
- locale: cfg.locale
5690
- });
5730
+ const res = await fetchWithInteractiveTimeout(
5731
+ `${endpoint.url}/chat/completions`,
5732
+ {
5733
+ method: "POST",
5734
+ headers: {
5735
+ "Content-Type": "application/json",
5736
+ Authorization: `Bearer ${endpoint.apiKey}`
5737
+ },
5738
+ body: JSON.stringify({
5739
+ model: endpoint.model,
5740
+ messages: [
5741
+ { role: "system", content: systemPrompt },
5742
+ { role: "user", content: question }
5743
+ ],
5744
+ temperature: 0.1,
5745
+ max_tokens: DEFAULT_LLM_MAX_TOKENS
5746
+ }),
5747
+ locale: cfg.locale
5748
+ }
5749
+ );
5691
5750
  return readChatContent(res, "Translation");
5692
5751
  }
5693
5752
  async function isLlmOnline(url) {
@@ -5760,6 +5819,107 @@ async function checkProviderChain(primary) {
5760
5819
  }
5761
5820
  return { primary: first };
5762
5821
  }
5822
+ async function resolveUsableRecallEndpoint(db) {
5823
+ const cfg = await getProviderForRole(db, "recall");
5824
+ if (!cfg.enabled) {
5825
+ throw new Error("LLM integration is disabled in settings (llm.enabled)");
5826
+ }
5827
+ assertChatCompletions(cfg);
5828
+ const signature = recallEndpointSignature(cfg);
5829
+ if (cachedRecallEndpoint && cachedRecallEndpoint.signature === signature && cachedRecallEndpoint.expiresAt > Date.now()) {
5830
+ return cachedRecallEndpoint.endpoint;
5831
+ }
5832
+ const chain = await checkProviderChain(cfg);
5833
+ const selected = chain.firstUsable;
5834
+ if (!selected || !isEndpointUsable(selected)) {
5835
+ throw new Error("No recall LLM endpoint is online");
5836
+ }
5837
+ cachedRecallEndpoint = {
5838
+ endpoint: selected.endpoint,
5839
+ signature,
5840
+ expiresAt: Date.now() + RECALL_ENDPOINT_CACHE_MS
5841
+ };
5842
+ return selected.endpoint;
5843
+ }
5844
+ async function prepareRecallChain(db, opts) {
5845
+ const cfg = await getProviderForRole(db, "recall");
5846
+ const fail = (reason, partial = {}) => ({
5847
+ usable: false,
5848
+ reason,
5849
+ online: false,
5850
+ model: cfg.model,
5851
+ availableModels: [],
5852
+ providerName: cfg.providerName,
5853
+ label: cfg.label,
5854
+ local: cfg.local ?? isLocalEndpoint(cfg.url),
5855
+ activeTier: "primary",
5856
+ ...partial
5857
+ });
5858
+ if (!cfg.enabled) return fail("disabled");
5859
+ if (cfg.apiFlavor !== "chat-completions") return fail("unsupported-provider");
5860
+ const chain = providerChain(cfg);
5861
+ const deadline = Date.now() + opts.timeoutMs;
5862
+ let lastReason = "offline";
5863
+ let lastOnline = false;
5864
+ let lastModel = cfg.model;
5865
+ let lastAvailable = [];
5866
+ for (let index = 0; index < chain.length; index++) {
5867
+ const endpoint = chain[index];
5868
+ let online = await isLlmOnline(endpoint.url);
5869
+ if (!online && (endpoint.local ?? isLocalEndpoint(endpoint.url))) {
5870
+ if (opts.interactive) {
5871
+ online = await startLocalRunner(
5872
+ endpoint.url,
5873
+ endpoint.model,
5874
+ cfg.locale,
5875
+ endpoint.runner
5876
+ );
5877
+ } else {
5878
+ spawnLocalRunner(endpoint.url, endpoint.model, endpoint.runner);
5879
+ while (Date.now() < deadline) {
5880
+ await new Promise((resolve10) => setTimeout(resolve10, 1e3));
5881
+ if (await isLlmOnline(endpoint.url)) {
5882
+ online = true;
5883
+ break;
5884
+ }
5885
+ }
5886
+ }
5887
+ }
5888
+ lastOnline = online;
5889
+ lastModel = endpoint.model;
5890
+ if (!online) {
5891
+ lastReason = "offline";
5892
+ continue;
5893
+ }
5894
+ const availableModels = await getAvailableModels(
5895
+ endpoint.url,
5896
+ endpoint.apiKey
5897
+ );
5898
+ lastAvailable = availableModels;
5899
+ const modelKnown = availableModels.length === 0 || availableModels.some(
5900
+ (candidate) => candidate.toLowerCase() === endpoint.model.toLowerCase()
5901
+ );
5902
+ if (!modelKnown) {
5903
+ lastReason = "model-not-found";
5904
+ continue;
5905
+ }
5906
+ return {
5907
+ usable: true,
5908
+ online: true,
5909
+ model: endpoint.model,
5910
+ availableModels,
5911
+ providerName: endpoint.providerName,
5912
+ label: endpoint.label,
5913
+ local: endpoint.local ?? isLocalEndpoint(endpoint.url),
5914
+ activeTier: index === 0 ? "primary" : "fallback"
5915
+ };
5916
+ }
5917
+ return fail(lastReason, {
5918
+ online: lastOnline,
5919
+ model: lastModel,
5920
+ availableModels: lastAvailable
5921
+ });
5922
+ }
5763
5923
  async function isVisionProviderModelExplicit(db, active) {
5764
5924
  if (await getSetting(db, "llm.vision.model")) return true;
5765
5925
  const providers = await readJsonSetting(db, "llm.providers");
@@ -5791,7 +5951,7 @@ async function checkVisionReadiness(db) {
5791
5951
  let warning;
5792
5952
  if (cfg.enabled && online && modelAvailable && !visionModelExplicit) {
5793
5953
  const cloudRec = getCloudModelRecommendation(active.url);
5794
- if (cloudRec && active.model === cloudRec) {
5954
+ if (cloudRec && active.model === cloudRec.model) {
5795
5955
  } else {
5796
5956
  warning = `No explicit vision model configured (llm.vision.model). Falling back to base model "${active.model}", which may not support image input. Set a multimodal model: zam settings set llm.vision.model <model>`;
5797
5957
  }
@@ -5861,13 +6021,8 @@ async function getProviderRoleStatus(db, role) {
5861
6021
  fallback: summarizeFallback(cfg.fallback)
5862
6022
  };
5863
6023
  }
5864
- let selected;
5865
- if (role === "vision") {
5866
- const chain = await checkProviderChain(cfg);
5867
- selected = chain.firstUsable ?? chain.primary;
5868
- } else {
5869
- selected = await checkProviderEndpoint(cfg);
5870
- }
6024
+ const chain = await checkProviderChain(cfg);
6025
+ const selected = chain.firstUsable ?? chain.primary;
5871
6026
  const active = selected.endpoint;
5872
6027
  const usable = selected.online && selected.modelAvailable;
5873
6028
  const reason = usable ? void 0 : selected.online ? "model-not-found" : "offline";
@@ -5889,7 +6044,35 @@ async function getProviderRoleStatus(db, role) {
5889
6044
  fallback: summarizeFallback(cfg.fallback)
5890
6045
  };
5891
6046
  }
5892
- function detectRunner(url, model) {
6047
+ function runnerKindFromHint(hint) {
6048
+ if (!hint) return void 0;
6049
+ const normalized = hint.trim().toLowerCase();
6050
+ if (normalized === "flm" || normalized === "fastflowlm") {
6051
+ return "fastflowlm";
6052
+ }
6053
+ if (normalized === "ollama") return "ollama";
6054
+ if (normalized === "foundry-local" || normalized === "foundry" || normalized === "generic") {
6055
+ return "generic";
6056
+ }
6057
+ return void 0;
6058
+ }
6059
+ function defaultPortForRunner(runner, url) {
6060
+ try {
6061
+ const urlObj = new URL(url);
6062
+ const explicit = urlObj.port;
6063
+ if (explicit) return explicit;
6064
+ if (runner === "ollama") return "11434";
6065
+ if (urlObj.protocol === "https:") return "443";
6066
+ return "80";
6067
+ } catch {
6068
+ return runner === "ollama" ? "11434" : "8000";
6069
+ }
6070
+ }
6071
+ function detectRunner(url, model, hint) {
6072
+ const fromHint = runnerKindFromHint(hint);
6073
+ if (fromHint) {
6074
+ return { runner: fromHint, port: defaultPortForRunner(fromHint, url) };
6075
+ }
5893
6076
  let runner = "unknown";
5894
6077
  let port = "8000";
5895
6078
  try {
@@ -5905,8 +6088,8 @@ function detectRunner(url, model) {
5905
6088
  }
5906
6089
  return { runner, port };
5907
6090
  }
5908
- function spawnLocalRunner(url, model) {
5909
- const { runner, port } = detectRunner(url, model);
6091
+ function spawnLocalRunner(url, model, hint) {
6092
+ const { runner, port } = detectRunner(url, model, hint);
5910
6093
  try {
5911
6094
  if (runner === "fastflowlm") {
5912
6095
  const flmExe = existsSync14("C:\\Program Files\\flm\\flm.exe") ? "C:\\Program Files\\flm\\flm.exe" : "flm";
@@ -5927,8 +6110,8 @@ function spawnLocalRunner(url, model) {
5927
6110
  } catch {
5928
6111
  }
5929
6112
  }
5930
- async function startLocalRunner(url, model, locale) {
5931
- const { runner, port } = detectRunner(url, model);
6113
+ async function startLocalRunner(url, model, locale, hint) {
6114
+ const { runner, port } = detectRunner(url, model, hint);
5932
6115
  if (runner === "fastflowlm") {
5933
6116
  const flmExe = existsSync14("C:\\Program Files\\flm\\flm.exe") ? "C:\\Program Files\\flm\\flm.exe" : "flm";
5934
6117
  if (!hasCommand("flm") && flmExe === "flm") {
@@ -5987,7 +6170,7 @@ async function startLocalRunner(url, model, locale) {
5987
6170
  let attempts = 0;
5988
6171
  const dotsPerLine = 30;
5989
6172
  while (true) {
5990
- await new Promise((resolve9) => setTimeout(resolve9, 500));
6173
+ await new Promise((resolve10) => setTimeout(resolve10, 500));
5991
6174
  if (await isLlmOnline(url)) {
5992
6175
  if (attempts > 0) process.stdout.write("\n");
5993
6176
  return true;
@@ -6012,105 +6195,39 @@ async function startLocalRunner(url, model, locale) {
6012
6195
  }
6013
6196
  }
6014
6197
  async function ensureLocalLlmRunning(db) {
6015
- const cfg = await getProviderForRole(db, "recall");
6016
- if (!cfg.enabled) {
6017
- return { usable: false, reason: "disabled" };
6018
- }
6019
- if (cfg.apiFlavor !== "chat-completions") {
6020
- console.warn(
6021
- `\x1B[31m\u2717 Recall is configured for "${cfg.apiFlavor}", but active recall currently supports chat-completions providers only.\x1B[0m`
6022
- );
6023
- return { usable: false, reason: "unsupported-provider" };
6024
- }
6025
- const { url, model, apiKey, locale } = cfg;
6026
- const isLocal = isLocalEndpoint(url);
6027
- console.log(`Checking if local LLM server is online at ${url}...`);
6028
- let online = await isLlmOnline(url);
6029
- if (!online && isLocal) {
6030
- console.log(`\x1B[33m\u26A0 Local LLM server is offline on ${url}.\x1B[0m`);
6031
- online = await startLocalRunner(url, model, locale);
6032
- }
6033
- if (!online) {
6034
- console.warn(
6035
- `\x1B[33m\u26A0 LLM server is not reachable at ${url}. Continuing without AI coaching.\x1B[0m
6036
- `
6198
+ const result = await prepareRecallChain(db, {
6199
+ timeoutMs: 25e3,
6200
+ interactive: true
6201
+ });
6202
+ if (result.usable) {
6203
+ const location = result.local ? "local" : "cloud";
6204
+ console.log(
6205
+ `\x1B[32m\u2713 Recall LLM ready (${location}: ${result.model}).\x1B[0m`
6037
6206
  );
6038
- return { usable: false, reason: "offline" };
6207
+ return { usable: true };
6039
6208
  }
6040
- console.log("\x1B[32m\u2713 Local LLM server is online.\x1B[0m");
6041
- const available = await getAvailableModels(url, apiKey);
6042
- const modelKnown = available.length === 0 || available.some((m) => m.toLowerCase() === model.toLowerCase());
6043
- if (!modelKnown) {
6209
+ if (result.reason === "unsupported-provider") {
6044
6210
  console.warn(
6045
- `\x1B[31m\u2717 Configured model "${model}" is not available on the server.\x1B[0m`
6211
+ `\x1B[31m\u2717 Recall provider is not supported for active recall.\x1B[0m`
6046
6212
  );
6047
- console.warn(` Available models: ${available.join(", ")}`);
6213
+ } else if (result.reason === "model-not-found") {
6048
6214
  console.warn(
6049
- ` Set the right one: \x1B[36mzam settings set llm.model <name>\x1B[0m`
6215
+ `\x1B[31m\u2717 Configured model "${result.model}" is not available on the server.\x1B[0m`
6050
6216
  );
6217
+ console.warn(` Available models: ${result.availableModels.join(", ")}`);
6218
+ } else if (result.reason === "offline") {
6051
6219
  console.warn(
6052
- "\x1B[33m Continuing this session without AI coaching.\x1B[0m\n"
6220
+ `\x1B[33m\u26A0 No recall LLM endpoint is reachable. Continuing without AI coaching.\x1B[0m
6221
+ `
6053
6222
  );
6054
- return { usable: false, reason: "model-not-found" };
6055
6223
  }
6056
- return { usable: true };
6224
+ return { usable: false, reason: result.reason };
6057
6225
  }
6058
6226
  async function ensureLlmReadyHeadless(db, opts = {}) {
6059
- const timeoutMs = opts.timeoutMs ?? 25e3;
6060
- const cfg = await getProviderForRole(db, "recall");
6061
- const { url, model, apiKey } = cfg;
6062
- if (!cfg.enabled) {
6063
- return {
6064
- usable: false,
6065
- reason: "disabled",
6066
- online: false,
6067
- model,
6068
- availableModels: []
6069
- };
6070
- }
6071
- if (cfg.apiFlavor !== "chat-completions") {
6072
- return {
6073
- usable: false,
6074
- reason: "unsupported-provider",
6075
- online: false,
6076
- model,
6077
- availableModels: []
6078
- };
6079
- }
6080
- const isLocal = isLocalEndpoint(url);
6081
- let online = await isLlmOnline(url);
6082
- if (!online && isLocal) {
6083
- spawnLocalRunner(url, model);
6084
- const deadline = Date.now() + timeoutMs;
6085
- while (Date.now() < deadline) {
6086
- await new Promise((r) => setTimeout(r, 1e3));
6087
- if (await isLlmOnline(url)) {
6088
- online = true;
6089
- break;
6090
- }
6091
- }
6092
- }
6093
- if (!online) {
6094
- return {
6095
- usable: false,
6096
- reason: "offline",
6097
- online: false,
6098
- model,
6099
- availableModels: []
6100
- };
6101
- }
6102
- const availableModels = await getAvailableModels(url, apiKey);
6103
- const modelKnown = availableModels.length === 0 || availableModels.some((m) => m.toLowerCase() === model.toLowerCase());
6104
- if (!modelKnown) {
6105
- return {
6106
- usable: false,
6107
- reason: "model-not-found",
6108
- online: true,
6109
- model,
6110
- availableModels
6111
- };
6112
- }
6113
- return { usable: true, online: true, model, availableModels };
6227
+ return prepareRecallChain(db, {
6228
+ timeoutMs: opts.timeoutMs ?? 25e3,
6229
+ interactive: false
6230
+ });
6114
6231
  }
6115
6232
  async function fetchWithInteractiveTimeout(url, options = {}) {
6116
6233
  const {
@@ -6142,8 +6259,8 @@ async function fetchWithInteractiveTimeout(url, options = {}) {
6142
6259
  const dotsPerLine = 30;
6143
6260
  while (true) {
6144
6261
  let timeoutId;
6145
- const timeoutPromise = new Promise((resolve9) => {
6146
- timeoutId = setTimeout(() => resolve9("timeout"), timeoutMs);
6262
+ const timeoutPromise = new Promise((resolve10) => {
6263
+ timeoutId = setTimeout(() => resolve10("timeout"), timeoutMs);
6147
6264
  });
6148
6265
  const dotsInterval = setInterval(() => {
6149
6266
  process.stdout.write(".");
@@ -6200,15 +6317,22 @@ async function ensureHighQualityQuestion(db, token) {
6200
6317
  bloomLevel: token.bloomLevel,
6201
6318
  sourceLinkContent
6202
6319
  });
6203
- if (generated && generated.trim().length > 0) {
6204
- await updateToken(db, token.slug, { question: generated });
6205
- return generated;
6320
+ if (generated.text.trim().length > 0) {
6321
+ await updateToken(db, token.slug, { question: generated.text });
6322
+ return {
6323
+ question: generated.text,
6324
+ source: "llm",
6325
+ model: generated.model
6326
+ };
6206
6327
  }
6207
6328
  } catch {
6208
6329
  }
6209
6330
  }
6210
6331
  if (token.question && token.question.trim().length > 0) {
6211
- return token.question;
6332
+ return {
6333
+ question: token.question.trim(),
6334
+ source: "original"
6335
+ };
6212
6336
  }
6213
6337
  return null;
6214
6338
  }
@@ -6253,7 +6377,7 @@ async function observeUiSnapshotViaLLM(db, input8) {
6253
6377
  const imageUrls = [];
6254
6378
  const isVideo = /\.(mp4|mov|m4v|avi|mkv|webm)$/i.test(input8.imagePath);
6255
6379
  if (isVideo) {
6256
- const { mkdirSync: mkdirSync14, readdirSync: readdirSync4, rmSync: rmSync4 } = await import("fs");
6380
+ const { mkdirSync: mkdirSync14, readdirSync: readdirSync3, rmSync: rmSync4 } = await import("fs");
6257
6381
  const { execSync: execSync6 } = await import("child_process");
6258
6382
  const tempDir = join14(
6259
6383
  tmpdir2(),
@@ -6265,13 +6389,13 @@ async function observeUiSnapshotViaLLM(db, input8) {
6265
6389
  `ffmpeg -i "${input8.imagePath}" -vf "fps=1/3,scale=1280:-1" -vsync vfr "${tempDir}/frame_%03d.png"`,
6266
6390
  { stdio: "ignore" }
6267
6391
  );
6268
- let files = readdirSync4(tempDir).filter((f) => f.endsWith(".png")).sort();
6392
+ let files = readdirSync3(tempDir).filter((f) => f.endsWith(".png")).sort();
6269
6393
  if (files.length === 0) {
6270
6394
  execSync6(
6271
6395
  `ffmpeg -i "${input8.imagePath}" -vframes 1 "${tempDir}/frame_001.png"`,
6272
6396
  { stdio: "ignore" }
6273
6397
  );
6274
- files = readdirSync4(tempDir).filter((f) => f.endsWith(".png")).sort();
6398
+ files = readdirSync3(tempDir).filter((f) => f.endsWith(".png")).sort();
6275
6399
  }
6276
6400
  const maxFrames = cfg.maxFrames ?? 100;
6277
6401
  let sampledFiles = files;
@@ -6416,7 +6540,7 @@ async function requestChatCompletionsVisionDraft(args) {
6416
6540
  }
6417
6541
  ],
6418
6542
  temperature: 0,
6419
- max_tokens: args.input.maxTokens ?? 450,
6543
+ max_tokens: args.input.maxTokens ?? DEFAULT_LLM_MAX_TOKENS,
6420
6544
  ...args.url.includes("11434") || args.url.includes("localhost") || args.url.includes("127.0.0.1") ? { options: { num_ctx: 32768 } } : {}
6421
6545
  }),
6422
6546
  locale: args.locale,
@@ -6472,7 +6596,7 @@ async function requestAnthropicVisionDraft(args) {
6472
6596
  },
6473
6597
  body: JSON.stringify({
6474
6598
  model: args.model,
6475
- max_tokens: args.input.maxTokens ?? 450,
6599
+ max_tokens: args.input.maxTokens ?? DEFAULT_LLM_MAX_TOKENS,
6476
6600
  system: VISION_SYSTEM_PROMPT,
6477
6601
  messages: [
6478
6602
  {
@@ -6627,54 +6751,165 @@ function isRecord2(value) {
6627
6751
  return typeof value === "object" && value !== null && !Array.isArray(value);
6628
6752
  }
6629
6753
 
6630
- // src/cli/commands/resolve-user.ts
6631
- async function ensureDefaultUser(db, preferredUserId) {
6632
- const stored = await getSetting(db, "user.id");
6633
- if (stored) return stored;
6634
- const userId = preferredUserId?.trim() || process.env.ZAM_USER?.trim() || process.env.USERNAME?.trim() || process.env.USER?.trim() || "default";
6635
- await setSetting(db, "user.id", userId);
6636
- return userId;
6754
+ // src/cli/commands/shared/db.ts
6755
+ function defaultErrorHandler(message) {
6756
+ console.error("Error:", message);
6757
+ process.exit(1);
6637
6758
  }
6638
- async function resolveUser(opts, db, resolveOpts) {
6639
- if (opts.user) return opts.user;
6640
- const stored = await getSetting(db, "user.id");
6641
- if (stored) return stored;
6642
- const message = "No user specified. Set a default with: zam whoami --set <id>";
6643
- if (resolveOpts?.json) {
6644
- console.log(JSON.stringify({ error: message }, null, 2));
6645
- } else {
6646
- console.error(message);
6759
+ async function withDb(fn, onError = defaultErrorHandler) {
6760
+ let db;
6761
+ try {
6762
+ db = await openDatabase();
6763
+ await fn(db);
6764
+ } catch (err) {
6765
+ onError(err.message);
6766
+ } finally {
6767
+ await db?.close();
6647
6768
  }
6648
- process.exit(1);
6769
+ }
6770
+ function jsonOut(data) {
6771
+ console.log(JSON.stringify(data, null, 2));
6649
6772
  }
6650
6773
 
6651
- // src/cli/commands/setup.ts
6652
- import {
6653
- existsSync as existsSync15,
6654
- lstatSync,
6655
- mkdirSync as mkdirSync8,
6656
- readdirSync as readdirSync2,
6657
- readFileSync as readFileSync10,
6658
- realpathSync,
6659
- rmSync as rmSync2,
6660
- symlinkSync,
6661
- writeFileSync as writeFileSync7
6662
- } from "fs";
6663
- import { basename as basename4, dirname as dirname5, join as join15, resolve as resolve3 } from "path";
6664
- import { fileURLToPath as fileURLToPath2 } from "url";
6665
- import { Command as Command2 } from "commander";
6666
- var packageRoot = [
6667
- fileURLToPath2(new URL("../..", import.meta.url)),
6668
- fileURLToPath2(new URL("../../..", import.meta.url))
6669
- ].find((candidate) => existsSync15(join15(candidate, "package.json"))) ?? fileURLToPath2(new URL("../..", import.meta.url));
6670
- var ALL_SETUP_AGENTS = ["claude", "copilot", "codex", "agent"];
6671
- var SKILL_PAIRS = [
6672
- {
6673
- from: join15(packageRoot, ".claude", "skills", "zam", "SKILL.md"),
6674
- to: join15(".claude", "skills", "zam", "SKILL.md"),
6675
- agents: ["claude", "copilot"]
6676
- },
6677
- {
6774
+ // src/cli/providers/config.ts
6775
+ var VALID_API_FLAVORS = [
6776
+ "chat-completions",
6777
+ "anthropic-messages"
6778
+ ];
6779
+ var VALID_ROLES = ["vision", "recall", "text"];
6780
+ function upsertProviderRecord(providers, name, patch) {
6781
+ const merged = { ...providers[name] ?? {} };
6782
+ if (patch.url !== void 0) merged.url = patch.url;
6783
+ if (patch.model !== void 0) merged.model = patch.model;
6784
+ if (patch.apiFlavor !== void 0) merged.apiFlavor = patch.apiFlavor;
6785
+ if (patch.apiKeyRef !== void 0) merged.apiKeyRef = patch.apiKeyRef;
6786
+ if (patch.label !== void 0) merged.label = patch.label;
6787
+ if (patch.local !== void 0) merged.local = patch.local;
6788
+ if (patch.runner !== void 0) merged.runner = patch.runner;
6789
+ return { ...providers, [name]: merged };
6790
+ }
6791
+ function removeProviderRecord(providers, name) {
6792
+ if (!(name in providers)) return { providers, removed: false };
6793
+ const next = { ...providers };
6794
+ delete next[name];
6795
+ return { providers: next, removed: true };
6796
+ }
6797
+ function rolesReferencing(roles, name) {
6798
+ return VALID_ROLES.filter((role) => {
6799
+ const binding = roles[role];
6800
+ return binding?.primary === name || binding?.fallback === name;
6801
+ });
6802
+ }
6803
+ function bindRoleProviders(roles, role, primary, fallback) {
6804
+ const binding = { primary };
6805
+ if (fallback) binding.fallback = fallback;
6806
+ return { ...roles, [role]: binding };
6807
+ }
6808
+ function maskSecret(key) {
6809
+ return key.length <= 4 ? "\u2022\u2022\u2022\u2022" : `\u2026${key.slice(-4)}`;
6810
+ }
6811
+ function buildProviderListing(providers, hasKey) {
6812
+ return Object.entries(providers).map(([name, rec]) => {
6813
+ const apiFlavor = rec.apiFlavor ?? (rec.url ? inferApiFlavor(rec.url) : "chat-completions");
6814
+ let keyState;
6815
+ if (!rec.apiKeyRef) keyState = "none";
6816
+ else keyState = hasKey(rec.apiKeyRef) ? "set" : "missing";
6817
+ const row = {
6818
+ name,
6819
+ url: rec.url,
6820
+ model: rec.model,
6821
+ apiFlavor,
6822
+ apiKeyRef: rec.apiKeyRef,
6823
+ keyState
6824
+ };
6825
+ if (rec.label !== void 0) row.label = rec.label;
6826
+ if (rec.local !== void 0) row.local = rec.local;
6827
+ if (rec.runner !== void 0) row.runner = rec.runner;
6828
+ return row;
6829
+ });
6830
+ }
6831
+ function findOrphanKeyRefs(storedRefs, providers) {
6832
+ const used = new Set(
6833
+ Object.values(providers).map((rec) => rec.apiKeyRef).filter((ref) => !!ref)
6834
+ );
6835
+ return storedRefs.filter((ref) => !used.has(ref));
6836
+ }
6837
+ async function readJson(db, key, fallback) {
6838
+ const raw = await getSetting(db, key);
6839
+ if (!raw) return fallback;
6840
+ try {
6841
+ return JSON.parse(raw);
6842
+ } catch {
6843
+ return fallback;
6844
+ }
6845
+ }
6846
+ var readProviders = (db) => readJson(db, "llm.providers", {});
6847
+ var readRoles = (db) => readJson(db, "llm.roles", {});
6848
+ async function readScopedProviders(db, machine) {
6849
+ if (machine) return getMachineAiConfig().providers ?? {};
6850
+ if (!db)
6851
+ throw new Error("Database is required for shared provider settings.");
6852
+ return readProviders(db);
6853
+ }
6854
+ async function readScopedRoles(db, machine) {
6855
+ if (machine) return getMachineAiConfig().roles ?? {};
6856
+ if (!db)
6857
+ throw new Error("Database is required for shared provider settings.");
6858
+ return readRoles(db);
6859
+ }
6860
+ async function writeScopedProviders(db, machine, p) {
6861
+ if (machine) {
6862
+ const config = getMachineAiConfig();
6863
+ saveMachineAiConfig({ ...config, providers: p });
6864
+ return;
6865
+ }
6866
+ if (!db)
6867
+ throw new Error("Database is required for shared provider settings.");
6868
+ await setSetting(db, "llm.providers", JSON.stringify(p));
6869
+ }
6870
+ async function writeScopedRoles(db, machine, r) {
6871
+ if (machine) {
6872
+ const config = getMachineAiConfig();
6873
+ saveMachineAiConfig({ ...config, roles: r });
6874
+ return;
6875
+ }
6876
+ if (!db)
6877
+ throw new Error("Database is required for shared provider settings.");
6878
+ await setSetting(db, "llm.roles", JSON.stringify(r));
6879
+ }
6880
+ async function withProviderScope(machine, action) {
6881
+ if (machine) {
6882
+ await action(void 0);
6883
+ return;
6884
+ }
6885
+ await withDb(action);
6886
+ }
6887
+
6888
+ // src/cli/provisioning/index.ts
6889
+ import {
6890
+ existsSync as existsSync15,
6891
+ lstatSync,
6892
+ mkdirSync as mkdirSync8,
6893
+ readFileSync as readFileSync10,
6894
+ realpathSync,
6895
+ rmSync as rmSync2,
6896
+ symlinkSync,
6897
+ writeFileSync as writeFileSync7
6898
+ } from "fs";
6899
+ import { basename as basename4, dirname as dirname5, join as join15 } from "path";
6900
+ import { fileURLToPath as fileURLToPath2 } from "url";
6901
+ var packageRoot = [
6902
+ fileURLToPath2(new URL("../..", import.meta.url)),
6903
+ fileURLToPath2(new URL("../../..", import.meta.url))
6904
+ ].find((candidate) => existsSync15(join15(candidate, "package.json"))) ?? fileURLToPath2(new URL("../..", import.meta.url));
6905
+ var ALL_SETUP_AGENTS = ["claude", "copilot", "codex", "agent"];
6906
+ var SKILL_PAIRS = [
6907
+ {
6908
+ from: join15(packageRoot, ".claude", "skills", "zam", "SKILL.md"),
6909
+ to: join15(".claude", "skills", "zam", "SKILL.md"),
6910
+ agents: ["claude", "copilot"]
6911
+ },
6912
+ {
6678
6913
  from: join15(packageRoot, ".agent", "skills", "zam", "SKILL.md"),
6679
6914
  to: join15(".agent", "skills", "zam", "SKILL.md"),
6680
6915
  agents: ["agent"]
@@ -6717,6 +6952,13 @@ function pathExists(path) {
6717
6952
  return false;
6718
6953
  }
6719
6954
  }
6955
+ function isSymbolicLink(path) {
6956
+ try {
6957
+ return lstatSync(path).isSymbolicLink();
6958
+ } catch {
6959
+ return false;
6960
+ }
6961
+ }
6720
6962
  function comparableRealPath(path) {
6721
6963
  const real = realpathSync(path);
6722
6964
  return process.platform === "win32" ? real.toLowerCase() : real;
@@ -6729,24 +6971,29 @@ function pathsResolveToSameDirectory(sourceDir, destinationDir) {
6729
6971
  }
6730
6972
  }
6731
6973
  function linkPointsTo(sourceDir, destinationDir) {
6732
- try {
6733
- return lstatSync(destinationDir).isSymbolicLink() && pathsResolveToSameDirectory(sourceDir, destinationDir);
6734
- } catch {
6735
- return false;
6736
- }
6974
+ return isSymbolicLink(destinationDir) && pathsResolveToSameDirectory(sourceDir, destinationDir);
6737
6975
  }
6738
- function isReplaceableCopiedSkill(destinationDir) {
6976
+ function isZamSkillCopy(destinationDir) {
6739
6977
  try {
6740
6978
  if (!lstatSync(destinationDir).isDirectory()) return false;
6741
- const entries = readdirSync2(destinationDir);
6742
- if (entries.length !== 1 || entries[0] !== "SKILL.md") return false;
6743
- return /^name:\s*zam\s*$/m.test(
6744
- readFileSync10(join15(destinationDir, "SKILL.md"), "utf8")
6745
- );
6979
+ const skillFile = join15(destinationDir, "SKILL.md");
6980
+ if (!existsSync15(skillFile)) return false;
6981
+ return /^name:\s*zam\s*$/m.test(readFileSync10(skillFile, "utf8"));
6746
6982
  } catch {
6747
6983
  return false;
6748
6984
  }
6749
6985
  }
6986
+ function classifySkillDestination(sourceDir, destinationDir) {
6987
+ if (!existsSync15(sourceDir)) return "source-missing";
6988
+ if (!isSymbolicLink(destinationDir) && pathsResolveToSameDirectory(sourceDir, destinationDir)) {
6989
+ return "source-directory";
6990
+ }
6991
+ if (linkPointsTo(sourceDir, destinationDir)) return "linked";
6992
+ if (!pathExists(destinationDir)) return "missing";
6993
+ if (isSymbolicLink(destinationDir)) return "broken";
6994
+ if (isZamSkillCopy(destinationDir)) return "stale-copy";
6995
+ return "unmanaged";
6996
+ }
6750
6997
  function wireSkills(cwd = process.cwd(), agents = parseSetupAgents(), opts = {}) {
6751
6998
  const results = [];
6752
6999
  const log = (message) => {
@@ -6756,7 +7003,9 @@ function wireSkills(cwd = process.cwd(), agents = parseSetupAgents(), opts = {})
6756
7003
  if (!pairAgents.some((agent) => agents.has(agent))) continue;
6757
7004
  const sourceDir = dirname5(from);
6758
7005
  const destinationDir = dirname5(join15(cwd, to));
6759
- if (!existsSync15(sourceDir)) {
7006
+ const label = dirname5(to);
7007
+ const state = classifySkillDestination(sourceDir, destinationDir);
7008
+ if (state === "source-missing") {
6760
7009
  if (!opts.quiet) {
6761
7010
  console.warn(` warn source not found, skipping: ${sourceDir}`);
6762
7011
  }
@@ -6768,8 +7017,8 @@ function wireSkills(cwd = process.cwd(), agents = parseSetupAgents(), opts = {})
6768
7017
  });
6769
7018
  continue;
6770
7019
  }
6771
- if (pathsResolveToSameDirectory(sourceDir, destinationDir) && !lstatSync(destinationDir).isSymbolicLink()) {
6772
- log(` skip ${dirname5(to)} (package source)`);
7020
+ if (state === "source-directory") {
7021
+ log(` skip ${label} (package source)`);
6773
7022
  results.push({
6774
7023
  source: sourceDir,
6775
7024
  destination: destinationDir,
@@ -6778,8 +7027,8 @@ function wireSkills(cwd = process.cwd(), agents = parseSetupAgents(), opts = {})
6778
7027
  });
6779
7028
  continue;
6780
7029
  }
6781
- if (linkPointsTo(sourceDir, destinationDir)) {
6782
- log(` skip ${dirname5(to)} (already linked)`);
7030
+ if (state === "linked") {
7031
+ log(` skip ${label} (already linked)`);
6783
7032
  results.push({
6784
7033
  source: sourceDir,
6785
7034
  destination: destinationDir,
@@ -6788,12 +7037,12 @@ function wireSkills(cwd = process.cwd(), agents = parseSetupAgents(), opts = {})
6788
7037
  });
6789
7038
  continue;
6790
7039
  }
6791
- const destinationExists = pathExists(destinationDir);
6792
- const replaceExisting = destinationExists && (Boolean(opts.force) || isReplaceableCopiedSkill(destinationDir));
7040
+ const destinationExists = state !== "missing";
7041
+ const replaceExisting = destinationExists && (Boolean(opts.force) || state === "broken" || state === "stale-copy");
6793
7042
  if (destinationExists && !replaceExisting) {
6794
7043
  if (!opts.quiet) {
6795
7044
  console.warn(
6796
- ` warn ${dirname5(to)} already exists and is not managed by ZAM; use --force to replace it`
7045
+ ` warn ${label} already exists and is not managed by ZAM; use --force to replace it`
6797
7046
  );
6798
7047
  }
6799
7048
  results.push({
@@ -6806,7 +7055,7 @@ function wireSkills(cwd = process.cwd(), agents = parseSetupAgents(), opts = {})
6806
7055
  }
6807
7056
  const action = destinationExists ? "relinked" : "linked";
6808
7057
  if (opts.dryRun) {
6809
- log(` would ${action === "linked" ? "link" : "relink"} ${dirname5(to)}`);
7058
+ log(` would ${action === "linked" ? "link" : "relink"} ${label}`);
6810
7059
  } else {
6811
7060
  if (destinationExists) {
6812
7061
  rmSync2(destinationDir, { recursive: true, force: true });
@@ -6817,39 +7066,36 @@ function wireSkills(cwd = process.cwd(), agents = parseSetupAgents(), opts = {})
6817
7066
  destinationDir,
6818
7067
  process.platform === "win32" ? "junction" : "dir"
6819
7068
  );
6820
- log(` ${action === "linked" ? "link" : "relink"} ${dirname5(to)}`);
7069
+ log(` ${action === "linked" ? "link" : "relink"} ${label}`);
6821
7070
  }
6822
7071
  results.push({ source: sourceDir, destination: destinationDir, action });
6823
7072
  }
6824
7073
  return results;
6825
7074
  }
6826
- function formatDatabaseInitTarget(target) {
6827
- switch (target.kind) {
6828
- case "local":
6829
- return `ZAM database at ${target.location} (local SQLite)`;
6830
- case "turso-remote":
6831
- return `ZAM database via Turso remote at ${target.location}`;
6832
- case "turso-native":
6833
- return `ZAM database via Turso native driver at ${target.location}`;
6834
- case "turso-replica":
6835
- return `ZAM database replica at ${target.location} syncing from ${target.syncUrl}`;
7075
+ function inspectSkillLinks(cwd = process.cwd(), agents = parseSetupAgents()) {
7076
+ const results = [];
7077
+ for (const { from, to, agents: pairAgents } of SKILL_PAIRS) {
7078
+ if (!pairAgents.some((agent) => agents.has(agent))) continue;
7079
+ const sourceDir = dirname5(from);
7080
+ const destinationDir = dirname5(join15(cwd, to));
7081
+ results.push({
7082
+ agents: pairAgents,
7083
+ source: sourceDir,
7084
+ destination: destinationDir,
7085
+ state: classifySkillDestination(sourceDir, destinationDir)
7086
+ });
6836
7087
  }
7088
+ return results;
6837
7089
  }
6838
- async function initDatabase(skipInit) {
6839
- if (skipInit) return;
6840
- try {
6841
- const target = getDatabaseTargetInfo();
6842
- const db = await openDatabaseWithSync({ initialize: true });
6843
- await db.close();
6844
- console.log(` init ${formatDatabaseInitTarget(target)}`);
6845
- } catch (err) {
6846
- const msg = err.message;
6847
- if (!msg.includes("already")) {
6848
- console.warn(` warn database init: ${msg}`);
6849
- } else {
6850
- console.log(` skip database already initialized`);
6851
- }
7090
+ function summarizeSkillLinkHealth(inspections) {
7091
+ if (inspections.some((item) => item.state === "unmanaged")) {
7092
+ return "unmanaged";
6852
7093
  }
7094
+ const repairable = ["missing", "broken", "stale-copy"];
7095
+ if (inspections.some((item) => repairable.includes(item.state))) {
7096
+ return "needs-repair";
7097
+ }
7098
+ return "healthy";
6853
7099
  }
6854
7100
  var ZAM_BLOCK_START = "<!-- ZAM:START -->";
6855
7101
  var ZAM_BLOCK_END = "<!-- ZAM:END -->";
@@ -6906,10 +7152,10 @@ ZAM is available in this repository. Use the \`zam\` skill in Claude Code to tur
6906
7152
  return;
6907
7153
  }
6908
7154
  const name = basename4(cwd);
6909
- const content = `# ZAM Personal Kernel \xE2\u20AC\u201D ${name}
7155
+ const content = `# ZAM Personal Kernel \u2014 ${name}
6910
7156
 
6911
7157
  This is a ZAM personal instance. ZAM builds lasting skills through spaced
6912
- repetition during real work \xE2\u20AC\u201D not separate study sessions.
7158
+ repetition during real work \u2014 not separate study sessions.
6913
7159
 
6914
7160
  ## First time here?
6915
7161
  Run \`/setup\` in Claude Code or Gemini CLI to complete first-time setup.
@@ -6918,8 +7164,8 @@ Run \`/setup\` in Claude Code or Gemini CLI to complete first-time setup.
6918
7164
  Run \`/zam\` to start a learning session on whatever you are working on.
6919
7165
 
6920
7166
  ## What lives here
6921
- - \`beliefs/\` \xE2\u20AC\u201D your worldview, approved by git commit
6922
- - \`goals/\` \xE2\u20AC\u201D your objectives, decomposed into tasks and learning tokens
7167
+ - \`beliefs/\` \u2014 your worldview, approved by git commit
7168
+ - \`goals/\` \u2014 your objectives, decomposed into tasks and learning tokens
6923
7169
 
6924
7170
  ## Fast-changing data
6925
7171
  Learning tokens, cards, and review history live in local SQLite by default.
@@ -7008,605 +7254,356 @@ ZAM is available in this repository through \`.claude/skills/zam/\`. Use the \`z
7008
7254
  Boolean(opts.dryRun)
7009
7255
  );
7010
7256
  }
7011
- var setupCommand = new Command2("setup").description(
7012
- "Link ZAM skill directories into this workspace and initialize the database"
7013
- ).option("--force", "replace an unmanaged existing ZAM skill directory", false).option("--skip-init", "skip database initialization", false).option("--skip-claude-md", "skip CLAUDE.md generation", false).option("--skip-agents-md", "skip AGENTS.md generation", false).option("--target <path>", "repository/workspace directory to set up").option(
7014
- "--agents <list>",
7015
- "comma-separated agents to wire: all, claude, copilot, codex, agent"
7016
- ).option(
7017
- "--dry-run",
7018
- "show what would be written without changing files",
7019
- false
7020
- ).action(
7021
- async (opts) => {
7022
- let agents;
7023
- try {
7024
- agents = parseSetupAgents(opts.agents);
7025
- } catch (err) {
7026
- console.error(`Error: ${err.message}`);
7027
- process.exit(1);
7028
- }
7029
- const target = resolve3(opts.target ?? process.cwd());
7030
- const updateExistingInstructions = Boolean(opts.target) || opts.force;
7031
- console.log(
7032
- `Setting up ZAM in ${target}${opts.dryRun ? " (dry run)" : ""}
7033
- `
7034
- );
7035
- wireSkills(target, agents, {
7036
- force: opts.force,
7037
- dryRun: opts.dryRun
7038
- });
7039
- await initDatabase(opts.skipInit || opts.dryRun);
7040
- if (agents.has("claude")) {
7041
- writeClaudeMd(opts.skipClaudeMd, target, {
7042
- dryRun: opts.dryRun,
7043
- updateExisting: updateExistingInstructions
7044
- });
7045
- }
7046
- if (agents.has("codex") || agents.has("agent")) {
7047
- writeAgentsMd(opts.skipAgentsMd, target, {
7048
- dryRun: opts.dryRun,
7049
- updateExisting: updateExistingInstructions
7050
- });
7051
- }
7052
- if (agents.has("copilot") && (opts.target || opts.agents)) {
7053
- writeCopilotInstructions(target, {
7054
- dryRun: opts.dryRun,
7055
- updateExisting: updateExistingInstructions
7056
- });
7057
- }
7058
- console.log(
7059
- "\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."
7060
- );
7061
- }
7062
- );
7063
7257
 
7064
- // src/cli/commands/shared/db.ts
7065
- function defaultErrorHandler(message) {
7066
- console.error("Error:", message);
7258
+ // src/cli/users/identity.ts
7259
+ async function ensureDefaultUser(db, preferredUserId) {
7260
+ const stored = await getSetting(db, "user.id");
7261
+ if (stored) return stored;
7262
+ const userId = preferredUserId?.trim() || process.env.ZAM_USER?.trim() || process.env.USERNAME?.trim() || process.env.USER?.trim() || "default";
7263
+ await setSetting(db, "user.id", userId);
7264
+ return userId;
7265
+ }
7266
+ async function resolveUser(opts, db, resolveOpts) {
7267
+ if (opts.user) return opts.user;
7268
+ const stored = await getSetting(db, "user.id");
7269
+ if (stored) return stored;
7270
+ const message = "No user specified. Set a default with: zam whoami --set <id>";
7271
+ if (resolveOpts?.json) {
7272
+ console.log(JSON.stringify({ error: message }, null, 2));
7273
+ } else {
7274
+ console.error(message);
7275
+ }
7067
7276
  process.exit(1);
7068
7277
  }
7069
- async function withDb(fn, onError = defaultErrorHandler) {
7070
- let db;
7071
- try {
7072
- db = await openDatabase();
7073
- await fn(db);
7074
- } catch (err) {
7075
- onError(err.message);
7076
- } finally {
7077
- await db?.close();
7278
+
7279
+ // src/cli/workspaces/active.ts
7280
+ import { existsSync as existsSync16, mkdirSync as mkdirSync9 } from "fs";
7281
+ import { homedir as homedir9 } from "os";
7282
+ import { basename as basename5, join as join16, resolve as resolve3 } from "path";
7283
+ var LEGACY_WORKSPACE_DIR_KEY = "personal.workspace_dir";
7284
+ var DEFAULT_WORKSPACE_ID = "personal";
7285
+ function defaultWorkspaceDir() {
7286
+ return join16(homedir9(), "Documents", "zam");
7287
+ }
7288
+ function normalizeWorkspacePath(path) {
7289
+ const resolved = resolve3(path);
7290
+ return process.platform === "win32" ? resolved.toLowerCase() : resolved;
7291
+ }
7292
+ function sameWorkspacePath(left, right) {
7293
+ return normalizeWorkspacePath(left) === normalizeWorkspacePath(right);
7294
+ }
7295
+ function labelFromPath(path) {
7296
+ return basename5(path) || "ZAM";
7297
+ }
7298
+ function workspaceIdFromPath(dir) {
7299
+ const base = basename5(dir).toLowerCase().replace(/[^a-z0-9]+/g, "-").replace(/^-+|-+$/g, "");
7300
+ const prefix = base || "workspace";
7301
+ const existing = new Set(getConfiguredWorkspaces().map((item) => item.id));
7302
+ if (!existing.has(prefix)) return prefix;
7303
+ let index = 2;
7304
+ while (existing.has(`${prefix}-${index}`)) index++;
7305
+ return `${prefix}-${index}`;
7306
+ }
7307
+ function findWorkspaceByPath(dir) {
7308
+ return getConfiguredWorkspaces().find(
7309
+ (workspace) => sameWorkspacePath(workspace.path, dir)
7310
+ );
7311
+ }
7312
+ async function clearLegacyWorkspaceDir(db) {
7313
+ await deleteSetting(db, LEGACY_WORKSPACE_DIR_KEY);
7314
+ }
7315
+ async function migrateLegacyWorkspaceDir(db) {
7316
+ const legacyDir = await getSetting(db, LEGACY_WORKSPACE_DIR_KEY);
7317
+ if (!legacyDir) return void 0;
7318
+ const path = resolve3(legacyDir);
7319
+ const existing = findWorkspaceByPath(path);
7320
+ const migrated = existing ?? {
7321
+ id: getConfiguredWorkspaces().some(
7322
+ (workspace) => workspace.id === DEFAULT_WORKSPACE_ID
7323
+ ) ? workspaceIdFromPath(path) : DEFAULT_WORKSPACE_ID,
7324
+ label: labelFromPath(path),
7325
+ kind: "personal",
7326
+ path
7327
+ };
7328
+ if (!existing) {
7329
+ upsertConfiguredWorkspace(migrated);
7330
+ }
7331
+ if (!getActiveWorkspace()) {
7332
+ setActiveWorkspaceId(migrated.id);
7078
7333
  }
7334
+ await clearLegacyWorkspaceDir(db);
7335
+ return migrated;
7079
7336
  }
7080
- function jsonOut(data) {
7081
- console.log(JSON.stringify(data, null, 2));
7337
+ async function ensureActiveWorkspace(db) {
7338
+ await migrateLegacyWorkspaceDir(db);
7339
+ const active = getActiveWorkspace();
7340
+ if (active) {
7341
+ mkdirSync9(active.path, { recursive: true });
7342
+ return active;
7343
+ }
7344
+ const configured = getConfiguredWorkspaces()[0];
7345
+ if (configured) {
7346
+ setActiveWorkspaceId(configured.id);
7347
+ mkdirSync9(configured.path, { recursive: true });
7348
+ return configured;
7349
+ }
7350
+ const workspace = {
7351
+ id: DEFAULT_WORKSPACE_ID,
7352
+ label: "Personal",
7353
+ kind: "personal",
7354
+ path: defaultWorkspaceDir()
7355
+ };
7356
+ mkdirSync9(workspace.path, { recursive: true });
7357
+ upsertConfiguredWorkspace(workspace);
7358
+ setActiveWorkspaceId(workspace.id);
7359
+ await clearLegacyWorkspaceDir(db);
7360
+ return workspace;
7082
7361
  }
7083
-
7084
- // src/cli/commands/workspace.ts
7085
- import { execFileSync as execFileSync3 } from "child_process";
7086
- import { existsSync as existsSync16, mkdirSync as mkdirSync9, writeFileSync as writeFileSync8 } from "fs";
7087
- import { homedir as homedir9 } from "os";
7088
- import { join as join16, resolve as resolve4 } from "path";
7089
- import { confirm, input } from "@inquirer/prompts";
7090
- import { Command as Command3 } from "commander";
7091
- function runGit(cwd, args) {
7092
- try {
7093
- return execFileSync3("git", args, {
7094
- cwd,
7095
- stdio: "pipe",
7096
- encoding: "utf8"
7097
- }).trim();
7098
- } catch (err) {
7099
- throw new Error(`Git command failed: ${err.message}`);
7362
+ async function activateWorkspace(db, workspace) {
7363
+ upsertConfiguredWorkspace(workspace);
7364
+ setActiveWorkspaceId(workspace.id);
7365
+ await clearLegacyWorkspaceDir(db);
7366
+ return workspace;
7367
+ }
7368
+ async function activateWorkspacePath(db, dir, opts = {}) {
7369
+ await migrateLegacyWorkspaceDir(db);
7370
+ const path = resolve3(dir);
7371
+ const existing = opts.id ? void 0 : findWorkspaceByPath(path);
7372
+ if (existing) {
7373
+ setActiveWorkspaceId(existing.id);
7374
+ await clearLegacyWorkspaceDir(db);
7375
+ return existing;
7100
7376
  }
7377
+ const workspace = {
7378
+ id: opts.id || workspaceIdFromPath(path),
7379
+ label: opts.label || labelFromPath(path),
7380
+ kind: opts.kind || "personal",
7381
+ path
7382
+ };
7383
+ return activateWorkspace(db, workspace);
7101
7384
  }
7102
- function ghRepoCreateArgs(repoName, repoVisibility) {
7103
- return ["repo", "create", repoName, repoVisibility, "--source=.", "--push"];
7385
+ async function removeWorkspaceAndResolveActive(db, id) {
7386
+ removeConfiguredWorkspace(id);
7387
+ await clearLegacyWorkspaceDir(db);
7388
+ const activeWorkspace = await ensureActiveWorkspace(db);
7389
+ return {
7390
+ activeWorkspace,
7391
+ workspaces: getConfiguredWorkspaces()
7392
+ };
7104
7393
  }
7105
- function gitRemoteArgs(githubUrl, hasOrigin) {
7106
- return hasOrigin ? ["remote", "set-url", "origin", githubUrl] : ["remote", "add", "origin", githubUrl];
7394
+ function existingWorkspaceDirOrHome(workspace) {
7395
+ return existsSync16(workspace.path) ? workspace.path : homedir9();
7107
7396
  }
7108
- var workspaceCommand = new Command3("workspace").description(
7109
- "Manage your ZAM learning workspace"
7110
- );
7111
- var WORKSPACE_KINDS = [
7112
- "personal",
7113
- "team",
7114
- "family",
7115
- "community",
7116
- "organization",
7117
- "custom"
7118
- ];
7119
- var WORKSPACE_SOURCE_CONTROLS = [
7120
- "github",
7121
- "azure-devops",
7122
- "git",
7123
- "none"
7124
- ];
7125
- function parseWorkspaceKind(value) {
7126
- const kind = (value ?? "custom").toLowerCase();
7127
- if (WORKSPACE_KINDS.includes(kind)) {
7128
- return kind;
7397
+
7398
+ // src/cli/workspaces/backup.ts
7399
+ import { mkdirSync as mkdirSync10 } from "fs";
7400
+ import { join as join17 } from "path";
7401
+ async function backupDatabaseTo(db, targetDir) {
7402
+ const backupDir = join17(targetDir, "zam-backups");
7403
+ mkdirSync10(backupDir, { recursive: true });
7404
+ const stamp = (/* @__PURE__ */ new Date()).toISOString().replace(/[:.]/g, "-");
7405
+ const dest = join17(backupDir, `zam-${stamp}.db`);
7406
+ await db.exec(`VACUUM INTO '${dest.replace(/'/g, "''")}'`);
7407
+ return dest;
7408
+ }
7409
+
7410
+ // src/cli/commands/bridge.ts
7411
+ var isServeMode = false;
7412
+ function jsonOut2(data) {
7413
+ console.log(JSON.stringify(data, null, 2));
7414
+ }
7415
+ function jsonError(message) {
7416
+ if (isServeMode) {
7417
+ throw new Error(JSON.stringify({ error: message }));
7129
7418
  }
7130
- throw new Error(
7131
- `Invalid workspace kind: ${value}. Use ${WORKSPACE_KINDS.join(", ")}.`
7132
- );
7419
+ console.log(JSON.stringify({ error: message }, null, 2));
7420
+ process.exit(1);
7133
7421
  }
7134
- function parseWorkspaceSourceControl(value) {
7135
- if (!value) return void 0;
7136
- const source = value.toLowerCase();
7137
- if (WORKSPACE_SOURCE_CONTROLS.includes(source)) {
7138
- return source;
7422
+ function parseNonNegativeIntegerOption(name, value) {
7423
+ const parsed = Number(value);
7424
+ if (!Number.isInteger(parsed) || parsed < 0) {
7425
+ jsonError(`${name} must be a non-negative integer`);
7139
7426
  }
7140
- throw new Error(
7141
- `Invalid source control: ${value}. Use ${WORKSPACE_SOURCE_CONTROLS.join(", ")}.`
7142
- );
7427
+ return parsed;
7143
7428
  }
7144
- function parseScopes(value) {
7145
- if (!value) return void 0;
7146
- const scopes = value.split(",").map((item) => item.trim()).filter(Boolean);
7147
- return scopes.length > 0 ? scopes : void 0;
7429
+ function parseProviderScope(scope) {
7430
+ const value = scope ?? "machine";
7431
+ if (value === "machine") return true;
7432
+ if (value === "shared") return false;
7433
+ jsonError(`Invalid --scope: ${value}. Use machine or shared.`);
7148
7434
  }
7149
- function requireWorkspace(id) {
7150
- const workspace = getConfiguredWorkspaces().find((item) => item.id === id);
7151
- if (!workspace) {
7152
- throw new Error(
7153
- `Workspace "${id}" is not configured. Add it with: zam workspace add ${id} --path <dir>`
7154
- );
7155
- }
7156
- return workspace;
7435
+ async function withDb2(fn) {
7436
+ await withDb(fn, jsonError);
7157
7437
  }
7158
- workspaceCommand.command("list").description("List configured ZAM workspaces").option("--json", "Output as JSON").action((opts) => {
7159
- const workspaces = getConfiguredWorkspaces();
7160
- if (opts.json) {
7161
- console.log(JSON.stringify({ workspaces }, null, 2));
7162
- return;
7438
+ async function getReviewTarget2(db, cardId, userId) {
7439
+ const target = await db.prepare(
7440
+ `SELECT c.id AS card_id, c.token_id, c.user_id, t.slug
7441
+ FROM cards c
7442
+ JOIN tokens t ON t.id = c.token_id
7443
+ WHERE c.id = ?`
7444
+ ).get(cardId);
7445
+ if (!target) {
7446
+ jsonError(`Card not found: ${cardId}`);
7163
7447
  }
7164
- console.log("Configured ZAM workspaces:\n");
7165
- if (workspaces.length === 0) {
7166
- console.log(
7167
- " (none) \u2014 add one: zam workspace add personal --path <dir>"
7168
- );
7169
- return;
7448
+ if (target.user_id !== userId) {
7449
+ jsonError(`Card ${cardId} does not belong to user ${userId}`);
7170
7450
  }
7171
- for (const workspace of workspaces) {
7172
- const label = workspace.label ? ` (${workspace.label})` : "";
7173
- console.log(` ${workspace.id}${label}`);
7174
- console.log(` kind: ${workspace.kind}`);
7175
- console.log(` path: ${workspace.path}`);
7176
- if (workspace.sourceControl) {
7177
- console.log(` source: ${workspace.sourceControl}`);
7178
- }
7179
- if (workspace.knowledgeScopes?.length) {
7180
- console.log(` scopes: ${workspace.knowledgeScopes.join(", ")}`);
7181
- }
7451
+ return target;
7452
+ }
7453
+ function parseTokenUpdates(opts) {
7454
+ const updates = {};
7455
+ if (opts.concept !== void 0) updates.concept = opts.concept;
7456
+ if (opts.domain !== void 0) updates.domain = opts.domain;
7457
+ if (opts.bloom !== void 0)
7458
+ updates.bloom_level = Number(opts.bloom);
7459
+ if (opts.context !== void 0) updates.context = opts.context;
7460
+ if (opts.sourceLink !== void 0) {
7461
+ updates.source_link = opts.sourceLink === "" ? null : opts.sourceLink;
7182
7462
  }
7183
- });
7184
- workspaceCommand.command("add <id>").description("Register an existing directory as a ZAM workspace").requiredOption("--path <dir>", "Existing workspace/repository directory").option(
7185
- "--kind <kind>",
7186
- `Workspace kind (${WORKSPACE_KINDS.join(" | ")})`,
7187
- "custom"
7188
- ).option("--label <label>", "Human-readable label").option(
7189
- "--source-control <provider>",
7190
- `Source-control provider (${WORKSPACE_SOURCE_CONTROLS.join(" | ")})`
7191
- ).option("--scopes <list>", "Comma-separated knowledge scopes").option("--default-agent <id>", "Default agent harness for this workspace").action((id, opts) => {
7192
- try {
7193
- const path = resolve4(String(opts.path));
7194
- if (!existsSync16(path)) {
7195
- console.error(`Workspace path does not exist: ${path}`);
7196
- process.exit(1);
7463
+ if (opts.mode !== void 0) {
7464
+ const validModes = ["shadowing", "copilot", "autonomy", "none"];
7465
+ if (!validModes.includes(opts.mode)) {
7466
+ jsonError(`Invalid mode: ${opts.mode}`);
7197
7467
  }
7198
- const workspace = {
7199
- id,
7200
- kind: parseWorkspaceKind(opts.kind),
7201
- path,
7202
- ...opts.label ? { label: opts.label } : {},
7203
- ...opts.sourceControl ? { sourceControl: parseWorkspaceSourceControl(opts.sourceControl) } : {},
7204
- ...parseScopes(opts.scopes) ? { knowledgeScopes: parseScopes(opts.scopes) } : {},
7205
- ...opts.defaultAgent ? { defaultAgent: opts.defaultAgent } : {}
7206
- };
7207
- wireSkills(path, parseSetupAgents());
7208
- upsertConfiguredWorkspace(workspace);
7209
- console.log(`Registered and linked workspace "${id}" at ${path}.`);
7210
- } catch (err) {
7211
- console.error(`Error: ${err.message}`);
7212
- process.exit(1);
7468
+ updates.symbiosis_mode = opts.mode === "none" ? null : opts.mode;
7213
7469
  }
7470
+ return updates;
7471
+ }
7472
+ var bridgeCommand = new Command2("bridge").description(
7473
+ "Machine-readable JSON protocol for AI integration"
7474
+ );
7475
+ 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) => {
7476
+ await withDb2(async (db) => {
7477
+ const userId = await resolveUser(opts, db, { json: true });
7478
+ const dueCards = await getDueCards(db, userId, void 0, opts.domain);
7479
+ const domains = [
7480
+ ...new Set(dueCards.map((c) => c.domain).filter(Boolean))
7481
+ ].sort();
7482
+ jsonOut2({
7483
+ userId,
7484
+ domain: opts.domain ?? null,
7485
+ dueCount: dueCards.length,
7486
+ domains,
7487
+ cards: dueCards.map((c) => ({
7488
+ cardId: c.id,
7489
+ tokenId: c.token_id,
7490
+ slug: c.slug,
7491
+ concept: c.concept,
7492
+ domain: c.domain,
7493
+ bloomLevel: c.bloom_level,
7494
+ state: c.state,
7495
+ dueAt: c.due_at
7496
+ }))
7497
+ });
7498
+ });
7214
7499
  });
7215
- workspaceCommand.command("remove <id>").description("Unregister a ZAM workspace without deleting its files").action((id) => {
7216
- const existing = getConfiguredWorkspaces().find((item) => item.id === id);
7217
- if (!existing) {
7218
- console.error(`Workspace "${id}" is not configured.`);
7219
- process.exit(1);
7500
+ bridgeCommand.command("backup-db").description("Back up the local database into the workspace (JSON)").option(
7501
+ "--dir <path>",
7502
+ "Target directory (default: workspace dir, else ~/Documents/zam)"
7503
+ ).action(async (opts) => {
7504
+ const target = getDatabaseTargetInfo();
7505
+ if (target.kind !== "local") {
7506
+ jsonOut2({
7507
+ ok: false,
7508
+ reason: "remote",
7509
+ target: target.kind,
7510
+ location: target.location
7511
+ });
7512
+ return;
7220
7513
  }
7221
- removeConfiguredWorkspace(id);
7222
- console.log(
7223
- `Unregistered workspace "${id}". Files in ${existing.path} were not changed.`
7224
- );
7514
+ await withDb2(async (db) => {
7515
+ const workspaceDir = opts.dir || (await ensureActiveWorkspace(db)).path;
7516
+ const path = await backupDatabaseTo(db, workspaceDir);
7517
+ jsonOut2({ ok: true, path });
7518
+ });
7225
7519
  });
7226
- workspaceCommand.command("setup <id>").description("Install ZAM skills into a configured workspace").option(
7227
- "--agents <list>",
7228
- "comma-separated agents to wire: all, claude, copilot, codex, agent"
7229
- ).option(
7230
- "--force",
7231
- "overwrite existing skill files and refresh ZAM blocks",
7232
- false
7233
- ).option(
7234
- "--dry-run",
7235
- "show what would be written without changing files",
7236
- false
7237
- ).action((id, opts) => {
7238
- try {
7239
- const workspace = requireWorkspace(id);
7240
- const agents = parseSetupAgents(opts.agents);
7241
- console.log(
7242
- `Setting up workspace "${workspace.id}" in ${workspace.path}${opts.dryRun ? " (dry run)" : ""}
7243
- `
7244
- );
7245
- wireSkills(workspace.path, agents, {
7246
- force: Boolean(opts.force),
7247
- dryRun: Boolean(opts.dryRun)
7248
- });
7249
- if (agents.has("claude")) {
7250
- writeClaudeMd(false, workspace.path, {
7251
- dryRun: Boolean(opts.dryRun),
7252
- updateExisting: true
7253
- });
7254
- }
7255
- if (agents.has("codex") || agents.has("agent")) {
7256
- writeAgentsMd(false, workspace.path, {
7257
- dryRun: Boolean(opts.dryRun),
7258
- updateExisting: true
7259
- });
7260
- }
7261
- if (agents.has("copilot")) {
7262
- writeCopilotInstructions(workspace.path, {
7263
- dryRun: Boolean(opts.dryRun),
7264
- updateExisting: true
7265
- });
7266
- }
7267
- } catch (err) {
7268
- console.error(`Error: ${err.message}`);
7269
- process.exit(1);
7270
- }
7271
- });
7272
- workspaceCommand.command("publish").description("Publish your local workspace sandbox to GitHub").action(async () => {
7273
- let db;
7274
- let workspaceDir = "";
7275
- try {
7276
- db = await openDatabase();
7277
- workspaceDir = await getSetting(db, "personal.workspace_dir") || "";
7278
- await db.close();
7279
- } catch {
7280
- await db?.close();
7281
- }
7282
- if (!workspaceDir) {
7283
- console.error(
7284
- "\x1B[31m\u2717 No active workspace configured. Please run `zam init` first.\x1B[0m"
7285
- );
7286
- process.exit(1);
7287
- }
7288
- if (!existsSync16(workspaceDir)) {
7289
- console.error(
7290
- `\x1B[31m\u2717 Workspace directory does not exist: ${workspaceDir}\x1B[0m`
7291
- );
7292
- process.exit(1);
7293
- }
7294
- console.log(`
7295
- Active workspace: \x1B[36m${workspaceDir}\x1B[0m`);
7296
- if (!hasCommand("git")) {
7297
- console.error(
7298
- "\x1B[31m\u2717 Git command was not found on this system. Please install Git first.\x1B[0m"
7299
- );
7300
- process.exit(1);
7301
- }
7302
- const gitignorePath = join16(workspaceDir, ".gitignore");
7303
- if (!existsSync16(gitignorePath)) {
7304
- writeFileSync8(
7305
- gitignorePath,
7306
- "node_modules/\n.agent/\n.agents/\n.claude/\n.gemini/\n.goose/\n*.log\n",
7307
- "utf8"
7308
- );
7309
- }
7310
- const hasGitRepo = existsSync16(join16(workspaceDir, ".git"));
7311
- if (!hasGitRepo) {
7312
- console.log("Initializing local Git repository...");
7313
- runGit(workspaceDir, ["init", "-b", "main"]);
7314
- runGit(workspaceDir, ["add", "."]);
7315
- runGit(workspaceDir, [
7316
- "commit",
7317
- "-m",
7318
- "chore: initial workspace sandbox bootstrap"
7319
- ]);
7320
- console.log("\x1B[32m\u2713 Local Git repository initialized.\x1B[0m");
7321
- } else {
7322
- console.log("Git repository is already initialized.");
7323
- }
7324
- const repoName = await input({
7325
- message: "Choose a name for your GitHub repository:",
7326
- default: "zam-personal"
7327
- });
7328
- const isPrivate = await confirm({
7329
- message: "Should the repository be private?",
7330
- default: true
7331
- });
7332
- const repoVisibility = isPrivate ? "--private" : "--public";
7333
- if (hasCommand("gh")) {
7334
- console.log("GitHub CLI detected! Automating repository creation...");
7335
- const proceedGh = await confirm({
7336
- message: "Would you like ZAM to create the repository using the GitHub CLI?",
7337
- default: true
7520
+ bridgeCommand.command("workspace-info").description("Report the workspace dir, its default, and the data dir (JSON)").action(async () => {
7521
+ await withDb2(async (db) => {
7522
+ const activeWorkspace = await ensureActiveWorkspace(db);
7523
+ jsonOut2({
7524
+ activeWorkspaceId: activeWorkspace.id,
7525
+ activeWorkspace,
7526
+ workspaceDir: activeWorkspace.path,
7527
+ defaultWorkspaceDir: defaultWorkspaceDir(),
7528
+ dataDir: join18(homedir10(), ".zam")
7338
7529
  });
7339
- if (proceedGh) {
7340
- try {
7341
- console.log(`Creating GitHub repository ${repoName}...`);
7342
- execFileSync3("gh", ghRepoCreateArgs(repoName, repoVisibility), {
7343
- cwd: workspaceDir,
7344
- stdio: "inherit"
7345
- });
7346
- console.log(
7347
- "\n\x1B[32m\u2713 Successfully published workspace to GitHub!\x1B[0m"
7348
- );
7349
- process.exit(0);
7350
- } catch (err) {
7351
- console.warn(
7352
- `\x1B[33m\u26A0 GitHub CLI creation failed: ${err.message}\x1B[0m`
7353
- );
7354
- }
7355
- }
7356
- }
7357
- console.log(
7358
- "\n\x1B[1mPlease create the repository manually on GitHub:\x1B[0m"
7359
- );
7360
- console.log(" 1. Go to https://github.com/new");
7361
- console.log(` 2. Name it exactly: \x1B[36m${repoName}\x1B[0m`);
7362
- console.log(
7363
- ` 3. Choose \x1B[36m${isPrivate ? "Private" : "Public"}\x1B[0m`
7364
- );
7365
- console.log(
7366
- " 4. Do NOT initialize it with README, .gitignore, or license"
7367
- );
7368
- console.log(" 5. Click 'Create repository'\n");
7369
- const githubUrl = await input({
7370
- message: "Paste the repository Git URL (e.g. git@github.com:user/repo.git):"
7371
7530
  });
7372
- if (githubUrl) {
7373
- try {
7374
- console.log("Linking remote repository and pushing...");
7375
- let hasOrigin = false;
7376
- try {
7377
- runGit(workspaceDir, ["remote", "get-url", "origin"]);
7378
- hasOrigin = true;
7379
- } catch {
7380
- }
7381
- runGit(workspaceDir, gitRemoteArgs(githubUrl, hasOrigin));
7382
- runGit(workspaceDir, ["push", "-u", "origin", "main"]);
7383
- console.log(
7384
- "\x1B[32m\u2713 Successfully linked and pushed to GitHub!\x1B[0m"
7385
- );
7386
- } catch (err) {
7387
- console.error(
7388
- `\x1B[31m\u2717 Push failed: ${err.message}\x1B[0m`
7389
- );
7390
- console.log(
7391
- "You can push manually later using: git push -u origin main"
7392
- );
7393
- }
7394
- }
7395
7531
  });
7396
- async function backupDatabaseTo(db, targetDir) {
7397
- const backupDir = join16(targetDir, "zam-backups");
7398
- mkdirSync9(backupDir, { recursive: true });
7399
- const stamp = (/* @__PURE__ */ new Date()).toISOString().replace(/[:.]/g, "-");
7400
- const dest = join16(backupDir, `zam-${stamp}.db`);
7401
- await db.exec(`VACUUM INTO '${dest.replace(/'/g, "''")}'`);
7402
- return dest;
7403
- }
7404
- workspaceCommand.command("data-dir").description("Print the ZAM data directory (database, credentials, config)").option("--json", "Output as JSON").action((opts) => {
7405
- const dir = join16(homedir9(), ".zam");
7406
- console.log(opts.json ? JSON.stringify({ dataDir: dir }) : dir);
7407
- });
7408
- workspaceCommand.command("backup").description("Back up the local ZAM database into your workspace").option(
7409
- "--dir <path>",
7410
- "Target directory (default: workspace dir, else ~/Documents/zam)"
7411
- ).option("--json", "Output as JSON").action(async (opts) => {
7412
- const target = getDatabaseTargetInfo();
7413
- if (target.kind !== "local") {
7414
- 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.`;
7415
- if (opts.json) {
7416
- console.log(JSON.stringify({ ok: false, reason }));
7417
- return;
7418
- }
7419
- console.error(`\x1B[33m\u26A0 ${reason}\x1B[0m`);
7420
- process.exit(1);
7421
- }
7422
- let db;
7423
- try {
7424
- db = await openDatabase();
7425
- const workspaceDir = opts.dir || await getSetting(db, "personal.workspace_dir") || join16(homedir9(), "Documents", "zam");
7426
- const dest = await backupDatabaseTo(db, workspaceDir);
7427
- if (opts.json) {
7428
- console.log(JSON.stringify({ ok: true, path: dest }));
7429
- } else {
7430
- console.log(`\x1B[32m\u2713 Database backed up to ${dest}\x1B[0m`);
7431
- }
7432
- } catch (err) {
7433
- const reason = err.message;
7434
- if (opts.json) {
7435
- console.log(JSON.stringify({ ok: false, reason }));
7436
- } else {
7437
- console.error(`\x1B[31m\u2717 Backup failed: ${reason}\x1B[0m`);
7438
- }
7439
- process.exit(1);
7440
- } finally {
7441
- await db?.close();
7442
- }
7443
- });
7444
-
7445
- // src/cli/commands/bridge.ts
7446
- var isServeMode = false;
7447
- function jsonOut2(data) {
7448
- console.log(JSON.stringify(data, null, 2));
7449
- }
7450
- function jsonError(message) {
7451
- if (isServeMode) {
7452
- throw new Error(JSON.stringify({ error: message }));
7453
- }
7454
- console.log(JSON.stringify({ error: message }, null, 2));
7455
- process.exit(1);
7456
- }
7457
- function parseNonNegativeIntegerOption(name, value) {
7458
- const parsed = Number(value);
7459
- if (!Number.isInteger(parsed) || parsed < 0) {
7460
- jsonError(`${name} must be a non-negative integer`);
7461
- }
7462
- return parsed;
7463
- }
7464
- async function withDb2(fn) {
7465
- await withDb(fn, jsonError);
7532
+ function provisionConfiguredWorkspaces() {
7533
+ return getConfiguredWorkspaces().flatMap(
7534
+ (workspace) => existsSync17(workspace.path) ? wireSkills(workspace.path, parseSetupAgents(), { quiet: true }) : []
7535
+ );
7466
7536
  }
7467
- async function getReviewTarget2(db, cardId, userId) {
7468
- const target = await db.prepare(
7469
- `SELECT c.id AS card_id, c.token_id, c.user_id, t.slug
7470
- FROM cards c
7471
- JOIN tokens t ON t.id = c.token_id
7472
- WHERE c.id = ?`
7473
- ).get(cardId);
7474
- if (!target) {
7475
- jsonError(`Card not found: ${cardId}`);
7476
- }
7477
- if (target.user_id !== userId) {
7478
- jsonError(`Card ${cardId} does not belong to user ${userId}`);
7537
+ function buildWorkspaceLinkHealth(workspaces) {
7538
+ const agents = parseSetupAgents();
7539
+ const map = {};
7540
+ for (const workspace of workspaces) {
7541
+ if (!existsSync17(workspace.path)) continue;
7542
+ const links = inspectSkillLinks(workspace.path, agents);
7543
+ map[workspace.id] = {
7544
+ health: summarizeSkillLinkHealth(links),
7545
+ states: Object.fromEntries(
7546
+ links.map((link) => [link.agents.join("+"), link.state])
7547
+ )
7548
+ };
7479
7549
  }
7480
- return target;
7550
+ return map;
7481
7551
  }
7482
- function parseTokenUpdates(opts) {
7483
- const updates = {};
7484
- if (opts.concept !== void 0) updates.concept = opts.concept;
7485
- if (opts.domain !== void 0) updates.domain = opts.domain;
7486
- if (opts.bloom !== void 0)
7487
- updates.bloom_level = Number(opts.bloom);
7488
- if (opts.context !== void 0) updates.context = opts.context;
7489
- if (opts.sourceLink !== void 0) {
7490
- updates.source_link = opts.sourceLink === "" ? null : opts.sourceLink;
7491
- }
7492
- if (opts.mode !== void 0) {
7493
- const validModes = ["shadowing", "copilot", "autonomy", "none"];
7494
- if (!validModes.includes(opts.mode)) {
7495
- jsonError(`Invalid mode: ${opts.mode}`);
7496
- }
7497
- updates.symbiosis_mode = opts.mode === "none" ? null : opts.mode;
7498
- }
7499
- return updates;
7552
+ async function ensureDesktopWorkspace(db) {
7553
+ const activeWorkspace = await ensureActiveWorkspace(db);
7554
+ const skillLinks = provisionConfiguredWorkspaces();
7555
+ return {
7556
+ workspaceDir: activeWorkspace.path,
7557
+ activeWorkspaceId: activeWorkspace.id,
7558
+ skillLinks
7559
+ };
7500
7560
  }
7501
- var bridgeCommand = new Command4("bridge").description(
7502
- "Machine-readable JSON protocol for AI integration"
7503
- );
7504
- 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) => {
7561
+ bridgeCommand.command("workspace-list").description("List configured ZAM workspaces (JSON)").action(async () => {
7505
7562
  await withDb2(async (db) => {
7506
- const userId = await resolveUser(opts, db, { json: true });
7507
- const dueCards = await getDueCards(db, userId, void 0, opts.domain);
7508
- const domains = [
7509
- ...new Set(dueCards.map((c) => c.domain).filter(Boolean))
7510
- ].sort();
7563
+ const activeWorkspace = await ensureActiveWorkspace(db);
7564
+ const workspaces = getConfiguredWorkspaces();
7511
7565
  jsonOut2({
7512
- userId,
7513
- domain: opts.domain ?? null,
7514
- dueCount: dueCards.length,
7515
- domains,
7516
- cards: dueCards.map((c) => ({
7517
- cardId: c.id,
7518
- tokenId: c.token_id,
7519
- slug: c.slug,
7520
- concept: c.concept,
7521
- domain: c.domain,
7522
- bloomLevel: c.bloom_level,
7523
- state: c.state,
7524
- dueAt: c.due_at
7525
- }))
7566
+ workspaces,
7567
+ activeWorkspaceId: activeWorkspace.id,
7568
+ activeWorkspace,
7569
+ workspaceDir: activeWorkspace.path,
7570
+ defaultWorkspaceDir: defaultWorkspaceDir(),
7571
+ dataDir: join18(homedir10(), ".zam"),
7572
+ linkHealth: buildWorkspaceLinkHealth(workspaces)
7526
7573
  });
7527
7574
  });
7528
7575
  });
7529
- bridgeCommand.command("backup-db").description("Back up the local database into the workspace (JSON)").option(
7530
- "--dir <path>",
7531
- "Target directory (default: workspace dir, else ~/Documents/zam)"
7576
+ bridgeCommand.command("workspace-repair-links").description(
7577
+ "Relink ZAM skill junctions for a configured workspace, replacing broken links and outdated copies (JSON)"
7578
+ ).requiredOption("--id <id>", "Workspace id").option(
7579
+ "--agents <list>",
7580
+ "comma-separated agents to wire: all, claude, copilot, codex, agent"
7532
7581
  ).action(async (opts) => {
7533
- const target = getDatabaseTargetInfo();
7534
- if (target.kind !== "local") {
7535
- jsonOut2({
7536
- ok: false,
7537
- reason: "remote",
7538
- target: target.kind,
7539
- location: target.location
7540
- });
7541
- return;
7582
+ const id = String(opts.id ?? "").trim();
7583
+ if (!id) jsonError("A non-empty --id is required");
7584
+ const workspace = getConfiguredWorkspaces().find((item) => item.id === id);
7585
+ if (!workspace) jsonError(`Workspace "${id}" is not configured`);
7586
+ if (!existsSync17(workspace.path)) {
7587
+ jsonError(`Workspace path does not exist: ${workspace.path}`);
7542
7588
  }
7543
- await withDb2(async (db) => {
7544
- const workspaceDir = opts.dir || await getSetting(db, "personal.workspace_dir") || join17(homedir10(), "Documents", "zam");
7545
- const path = await backupDatabaseTo(db, workspaceDir);
7546
- jsonOut2({ ok: true, path });
7547
- });
7548
- });
7549
- bridgeCommand.command("workspace-info").description("Report the workspace dir, its default, and the data dir (JSON)").action(async () => {
7550
- await withDb2(async (db) => {
7551
- const workspaceDir = await getSetting(db, "personal.workspace_dir") || null;
7552
- jsonOut2({
7553
- workspaceDir,
7554
- defaultWorkspaceDir: join17(homedir10(), "Documents", "zam"),
7555
- dataDir: join17(homedir10(), ".zam")
7556
- });
7589
+ const agents = parseSetupAgents(opts.agents);
7590
+ const skillLinks = wireSkills(workspace.path, agents, {
7591
+ force: true,
7592
+ quiet: true
7557
7593
  });
7558
- });
7559
- function sameWorkspacePath(left, right) {
7560
- const normalize = (value) => {
7561
- const path = resolve5(value);
7562
- return process.platform === "win32" ? path.toLowerCase() : path;
7563
- };
7564
- return normalize(left) === normalize(right);
7565
- }
7566
- async function ensureDesktopWorkspace(db) {
7567
- const configured = getConfiguredWorkspaces();
7568
- const savedWorkspaceDir = await getSetting(db, "personal.workspace_dir");
7569
- const workspaceDir = savedWorkspaceDir || configured.find((workspace) => workspace.kind === "personal")?.path || join17(homedir10(), "Documents", "zam");
7570
- mkdirSync10(workspaceDir, { recursive: true });
7571
- if (!savedWorkspaceDir) {
7572
- await setSetting(db, "personal.workspace_dir", workspaceDir);
7573
- }
7574
- if (!configured.some(
7575
- (workspace) => sameWorkspacePath(workspace.path, workspaceDir)
7576
- )) {
7577
- const id = configured.some((workspace) => workspace.id === "personal") ? workspaceIdFromPath(workspaceDir) : "personal";
7578
- upsertConfiguredWorkspace({
7579
- id,
7580
- label: basename5(workspaceDir) || "ZAM",
7581
- kind: "personal",
7582
- path: workspaceDir
7583
- });
7584
- }
7585
- const skillLinks = getConfiguredWorkspaces().flatMap(
7586
- (workspace) => existsSync17(workspace.path) ? wireSkills(workspace.path, parseSetupAgents(), { quiet: true }) : []
7587
- );
7588
- return { workspaceDir, skillLinks };
7589
- }
7590
- bridgeCommand.command("workspace-list").description("List configured ZAM workspaces (JSON)").action(async () => {
7591
- await withDb2(async (db) => {
7592
- const workspaceDir = await getSetting(db, "personal.workspace_dir") || null;
7593
- jsonOut2({
7594
- workspaces: getConfiguredWorkspaces(),
7595
- workspaceDir,
7596
- defaultWorkspaceDir: join17(homedir10(), "Documents", "zam"),
7597
- dataDir: join17(homedir10(), ".zam")
7598
- });
7594
+ const links = inspectSkillLinks(workspace.path, agents);
7595
+ jsonOut2({
7596
+ ok: true,
7597
+ workspace,
7598
+ skillLinks,
7599
+ linkHealth: {
7600
+ health: summarizeSkillLinkHealth(links),
7601
+ states: Object.fromEntries(
7602
+ links.map((link) => [link.agents.join("+"), link.state])
7603
+ )
7604
+ }
7599
7605
  });
7600
7606
  });
7601
- function workspaceIdFromPath(dir) {
7602
- const base = basename5(dir).toLowerCase().replace(/[^a-z0-9]+/g, "-").replace(/^-+|-+$/g, "");
7603
- const prefix = base || "workspace";
7604
- const existing = new Set(getConfiguredWorkspaces().map((item) => item.id));
7605
- if (!existing.has(prefix)) return prefix;
7606
- let index = 2;
7607
- while (existing.has(`${prefix}-${index}`)) index++;
7608
- return `${prefix}-${index}`;
7609
- }
7610
7607
  function parseBridgeWorkspaceKind(value) {
7611
7608
  const kind = (value || "custom").toLowerCase();
7612
7609
  if (kind === "personal" || kind === "team" || kind === "family" || kind === "community" || kind === "organization" || kind === "custom") {
@@ -7617,25 +7614,25 @@ function parseBridgeWorkspaceKind(value) {
7617
7614
  bridgeCommand.command("workspace-add").description("Register an existing directory as a ZAM workspace (JSON)").requiredOption("--path <dir>", "Existing workspace/repository directory").option("--id <id>", "Workspace id").option("--label <label>", "Human-readable label").option("--kind <kind>", "Workspace kind", "custom").action(async (opts) => {
7618
7615
  const raw = String(opts.path ?? "").trim();
7619
7616
  if (!raw) jsonError("A non-empty --path is required");
7620
- const path = resolve5(raw);
7617
+ const path = resolve4(raw);
7621
7618
  if (!existsSync17(path)) jsonError(`Workspace path does not exist: ${path}`);
7622
- const id = String(opts.id || workspaceIdFromPath(path)).trim();
7623
- if (!id) jsonError("A non-empty --id is required");
7624
- const workspace = {
7625
- id,
7626
- label: opts.label || basename5(path) || id,
7627
- kind: parseBridgeWorkspaceKind(opts.kind),
7628
- path
7629
- };
7619
+ const id = opts.id ? String(opts.id).trim() : void 0;
7620
+ if (opts.id && !id) jsonError("A non-empty --id is required");
7621
+ const kind = parseBridgeWorkspaceKind(opts.kind);
7630
7622
  const skillLinks = wireSkills(path, parseSetupAgents(), { quiet: true });
7631
7623
  await withDb2(async (db) => {
7632
- await setSetting(db, "personal.workspace_dir", path);
7633
- upsertConfiguredWorkspace(workspace);
7624
+ const workspace = await activateWorkspacePath(db, path, {
7625
+ ...id ? { id } : {},
7626
+ ...opts.label ? { label: opts.label } : {},
7627
+ kind
7628
+ });
7634
7629
  jsonOut2({
7635
7630
  ok: true,
7636
7631
  workspace,
7637
7632
  workspaces: getConfiguredWorkspaces(),
7638
- workspaceDir: path,
7633
+ activeWorkspaceId: workspace.id,
7634
+ activeWorkspace: workspace,
7635
+ workspaceDir: workspace.path,
7639
7636
  skillLinks
7640
7637
  });
7641
7638
  });
@@ -7646,27 +7643,15 @@ bridgeCommand.command("workspace-remove").description("Unregister a ZAM workspac
7646
7643
  const workspace = getConfiguredWorkspaces().find((item) => item.id === id);
7647
7644
  if (!workspace) jsonError(`Workspace "${id}" is not configured`);
7648
7645
  await withDb2(async (db) => {
7649
- const activeWorkspaceDir = await getSetting(db, "personal.workspace_dir") || null;
7650
- const remaining = removeConfiguredWorkspace(id);
7651
- let workspaceDir = activeWorkspaceDir;
7652
- let skillLinks = [];
7653
- if (activeWorkspaceDir && sameWorkspacePath(activeWorkspaceDir, workspace.path)) {
7654
- if (remaining.length > 0) {
7655
- workspaceDir = remaining[0].path;
7656
- await setSetting(db, "personal.workspace_dir", workspaceDir);
7657
- } else {
7658
- workspaceDir = join17(homedir10(), "Documents", "zam");
7659
- await setSetting(db, "personal.workspace_dir", workspaceDir);
7660
- const ensured = await ensureDesktopWorkspace(db);
7661
- workspaceDir = ensured.workspaceDir;
7662
- skillLinks = ensured.skillLinks;
7663
- }
7664
- }
7646
+ const { activeWorkspace, workspaces } = await removeWorkspaceAndResolveActive(db, id);
7647
+ const skillLinks = provisionConfiguredWorkspaces();
7665
7648
  jsonOut2({
7666
7649
  ok: true,
7667
7650
  removed: workspace,
7668
- workspaces: getConfiguredWorkspaces(),
7669
- workspaceDir,
7651
+ workspaces,
7652
+ activeWorkspaceId: activeWorkspace.id,
7653
+ activeWorkspace,
7654
+ workspaceDir: activeWorkspace.path,
7670
7655
  skillLinks
7671
7656
  });
7672
7657
  });
@@ -7674,12 +7659,20 @@ bridgeCommand.command("workspace-remove").description("Unregister a ZAM workspac
7674
7659
  bridgeCommand.command("set-workspace-dir").description("Set the personal workspace directory (JSON)").requiredOption("--dir <path>", "Path to the workspace directory").action(async (opts) => {
7675
7660
  const raw = String(opts.dir ?? "").trim();
7676
7661
  if (!raw) jsonError("A non-empty --dir is required");
7677
- const dir = resolve5(raw);
7662
+ const dir = resolve4(raw);
7678
7663
  if (!existsSync17(dir)) jsonError(`Workspace path does not exist: ${dir}`);
7679
7664
  const skillLinks = wireSkills(dir, parseSetupAgents(), { quiet: true });
7680
7665
  await withDb2(async (db) => {
7681
- await setSetting(db, "personal.workspace_dir", dir);
7682
- jsonOut2({ ok: true, workspaceDir: dir, skillLinks });
7666
+ const workspace = await activateWorkspacePath(db, dir);
7667
+ jsonOut2({
7668
+ ok: true,
7669
+ workspace,
7670
+ workspaces: getConfiguredWorkspaces(),
7671
+ activeWorkspaceId: workspace.id,
7672
+ activeWorkspace: workspace,
7673
+ workspaceDir: workspace.path,
7674
+ skillLinks
7675
+ });
7683
7676
  });
7684
7677
  });
7685
7678
  bridgeCommand.command("agent-list").description("List agent harnesses with detection state + the default (JSON)").action(async () => {
@@ -7729,8 +7722,8 @@ bridgeCommand.command("agent-open").description("Launch an agent harness in the
7729
7722
  if (opts.workspace && !configuredWorkspace) {
7730
7723
  jsonError(`Workspace is not configured: ${opts.workspace}`);
7731
7724
  }
7732
- let workspace = opts.dir || configuredWorkspace?.path || await getSetting(db, "personal.workspace_dir") || join17(homedir10(), "Documents", "zam");
7733
- if (!existsSync17(workspace)) workspace = homedir10();
7725
+ const activeWorkspace = await ensureActiveWorkspace(db);
7726
+ const workspace = opts.dir ? existsSync17(opts.dir) ? opts.dir : homedir10() : existingWorkspaceDirOrHome(configuredWorkspace ?? activeWorkspace);
7734
7727
  launchHarness(harness, {
7735
7728
  executable,
7736
7729
  workspace,
@@ -7768,6 +7761,8 @@ bridgeCommand.command("get-review").description("Get next review card with promp
7768
7761
  const item = queue.items[0];
7769
7762
  const isLlmEnabled = await getSetting(db, "llm.enabled") === "true";
7770
7763
  let resolvedQuestion = item.question;
7764
+ let questionSource = "original";
7765
+ let questionModel;
7771
7766
  if (isLlmEnabled) {
7772
7767
  try {
7773
7768
  const healed = await ensureHighQualityQuestion(db, {
@@ -7780,7 +7775,9 @@ bridgeCommand.command("get-review").description("Get next review card with promp
7780
7775
  question: item.question
7781
7776
  });
7782
7777
  if (healed) {
7783
- resolvedQuestion = healed;
7778
+ resolvedQuestion = healed.question;
7779
+ questionSource = healed.source;
7780
+ questionModel = healed.model;
7784
7781
  }
7785
7782
  } catch {
7786
7783
  }
@@ -7809,6 +7806,8 @@ bridgeCommand.command("get-review").description("Get next review card with promp
7809
7806
  hasReview: true,
7810
7807
  card: item,
7811
7808
  prompt,
7809
+ questionSource,
7810
+ questionModel: questionModel ?? null,
7812
7811
  resolvedContext,
7813
7812
  queueSize: fullQueue.items.length
7814
7813
  });
@@ -8104,10 +8103,10 @@ bridgeCommand.command("discover-skills").description(
8104
8103
  "20"
8105
8104
  ).action(async (opts) => {
8106
8105
  try {
8107
- const monitorDir = join17(homedir10(), ".zam", "monitor");
8106
+ const monitorDir = join18(homedir10(), ".zam", "monitor");
8108
8107
  let files;
8109
8108
  try {
8110
- files = readdirSync3(monitorDir).filter((f) => f.endsWith(".jsonl"));
8109
+ files = readdirSync2(monitorDir).filter((f) => f.endsWith(".jsonl"));
8111
8110
  } catch {
8112
8111
  jsonOut2({ proposals: [], message: "No monitor logs found." });
8113
8112
  return;
@@ -8117,7 +8116,7 @@ bridgeCommand.command("discover-skills").description(
8117
8116
  return;
8118
8117
  }
8119
8118
  const limit = Number(opts.limit);
8120
- const sorted = files.map((f) => ({ name: f, path: join17(monitorDir, f) })).sort((a, b) => b.name.localeCompare(a.name)).slice(0, limit);
8119
+ const sorted = files.map((f) => ({ name: f, path: join18(monitorDir, f) })).sort((a, b) => b.name.localeCompare(a.name)).slice(0, limit);
8121
8120
  const sessionCommands = /* @__PURE__ */ new Map();
8122
8121
  for (const file of sorted) {
8123
8122
  const sessionId = file.name.replace(".jsonl", "");
@@ -8229,7 +8228,7 @@ bridgeCommand.command("observe-ui-snapshot").description(
8229
8228
  });
8230
8229
  function resolveWindowsPowerShell() {
8231
8230
  try {
8232
- execFileSync4("where.exe", ["pwsh.exe"], { stdio: "ignore" });
8231
+ execFileSync3("where.exe", ["pwsh.exe"], { stdio: "ignore" });
8233
8232
  return "pwsh";
8234
8233
  } catch {
8235
8234
  return "powershell";
@@ -8244,7 +8243,7 @@ function captureScreenshot(outputPath, hwnd, processName) {
8244
8243
  throw new Error(`Invalid process name format: ${processName}`);
8245
8244
  }
8246
8245
  if (platform === "win32") {
8247
- const stdout = execFileSync4(
8246
+ const stdout = execFileSync3(
8248
8247
  resolveWindowsPowerShell(),
8249
8248
  [
8250
8249
  "-NoProfile",
@@ -8557,13 +8556,13 @@ Write-CaptureResult "fullscreen" $hwndVal $matchedBy
8557
8556
  } else if (platform === "darwin") {
8558
8557
  if (hwnd) {
8559
8558
  const parsedHwnd = hwnd.startsWith("0x") ? parseInt(hwnd, 16) : parseInt(hwnd, 10);
8560
- execFileSync4("screencapture", ["-l", String(parsedHwnd), outputPath], {
8559
+ execFileSync3("screencapture", ["-l", String(parsedHwnd), outputPath], {
8561
8560
  stdio: "pipe"
8562
8561
  });
8563
8562
  return { method: "screencapture-window", target: null };
8564
8563
  } else if (processName) {
8565
8564
  try {
8566
- const windowId = execFileSync4(
8565
+ const windowId = execFileSync3(
8567
8566
  "osascript",
8568
8567
  [
8569
8568
  "-e",
@@ -8572,17 +8571,17 @@ Write-CaptureResult "fullscreen" $hwndVal $matchedBy
8572
8571
  { encoding: "utf8", stdio: ["pipe", "pipe", "ignore"] }
8573
8572
  ).trim();
8574
8573
  if (windowId && /^\d+$/.test(windowId)) {
8575
- execFileSync4("screencapture", ["-l", windowId, outputPath], {
8574
+ execFileSync3("screencapture", ["-l", windowId, outputPath], {
8576
8575
  stdio: "pipe"
8577
8576
  });
8578
8577
  return { method: "screencapture-window", target: null };
8579
8578
  }
8580
8579
  } catch {
8581
8580
  }
8582
- execFileSync4("screencapture", ["-x", outputPath], { stdio: "pipe" });
8581
+ execFileSync3("screencapture", ["-x", outputPath], { stdio: "pipe" });
8583
8582
  return { method: "screencapture-full", target: null };
8584
8583
  } else {
8585
- execFileSync4("screencapture", ["-x", outputPath], { stdio: "pipe" });
8584
+ execFileSync3("screencapture", ["-x", outputPath], { stdio: "pipe" });
8586
8585
  return { method: "screencapture-full", target: null };
8587
8586
  }
8588
8587
  } else {
@@ -8619,7 +8618,7 @@ bridgeCommand.command("capture-ui").description("Capture a screenshot for agent-
8619
8618
  return;
8620
8619
  }
8621
8620
  }
8622
- const outputPath = opts.image ?? opts.output ?? join17(tmpdir3(), `zam-capture-${randomBytes2(4).toString("hex")}.png`);
8621
+ const outputPath = opts.image ?? opts.output ?? join18(tmpdir3(), `zam-capture-${randomBytes2(4).toString("hex")}.png`);
8623
8622
  const captureResult = isProvided ? { method: "provided", target: null } : captureScreenshot(outputPath, opts.hwnd, opts.processName);
8624
8623
  if (!isProvided) {
8625
8624
  const post = decidePostCapture(policy, {
@@ -8674,11 +8673,11 @@ bridgeCommand.command("start-recording").description("Start screen recording in
8674
8673
  return;
8675
8674
  }
8676
8675
  const sessionId = opts.session;
8677
- const statePath = join17(tmpdir3(), `zam-recording-${sessionId}.json`);
8676
+ const statePath = join18(tmpdir3(), `zam-recording-${sessionId}.json`);
8678
8677
  const defaultExt = platform === "win32" ? ".mkv" : ".mov";
8679
- const outputPath = opts.output ?? join17(tmpdir3(), `zam-recording-${sessionId}${defaultExt}`);
8680
- const { existsSync: existsSync25, writeFileSync: writeFileSync12, openSync, closeSync } = await import("fs");
8681
- if (existsSync25(statePath)) {
8678
+ const outputPath = opts.output ?? join18(tmpdir3(), `zam-recording-${sessionId}${defaultExt}`);
8679
+ const { existsSync: existsSync26, writeFileSync: writeFileSync12, openSync, closeSync } = await import("fs");
8680
+ if (existsSync26(statePath)) {
8682
8681
  jsonOut2({
8683
8682
  sessionId,
8684
8683
  started: false,
@@ -8686,7 +8685,7 @@ bridgeCommand.command("start-recording").description("Start screen recording in
8686
8685
  });
8687
8686
  return;
8688
8687
  }
8689
- const logPath = join17(tmpdir3(), `zam-recording-${sessionId}.log`);
8688
+ const logPath = join18(tmpdir3(), `zam-recording-${sessionId}.log`);
8690
8689
  let logFd;
8691
8690
  try {
8692
8691
  logFd = openSync(logPath, "w");
@@ -8768,9 +8767,9 @@ bridgeCommand.command("stop-recording").description(
8768
8767
  return;
8769
8768
  }
8770
8769
  const sessionId = opts.session;
8771
- const statePath = join17(tmpdir3(), `zam-recording-${sessionId}.json`);
8772
- const { existsSync: existsSync25, readFileSync: readFileSync16, rmSync: rmSync4 } = await import("fs");
8773
- if (!existsSync25(statePath)) {
8770
+ const statePath = join18(tmpdir3(), `zam-recording-${sessionId}.json`);
8771
+ const { existsSync: existsSync26, readFileSync: readFileSync16, rmSync: rmSync4 } = await import("fs");
8772
+ if (!existsSync26(statePath)) {
8774
8773
  jsonOut2({
8775
8774
  sessionId,
8776
8775
  stopped: false,
@@ -8794,7 +8793,7 @@ bridgeCommand.command("stop-recording").description(
8794
8793
  };
8795
8794
  let attempts = 0;
8796
8795
  while (isProcessRunning(pid) && attempts < 20) {
8797
- await new Promise((resolve9) => setTimeout(resolve9, 250));
8796
+ await new Promise((resolve10) => setTimeout(resolve10, 250));
8798
8797
  attempts++;
8799
8798
  }
8800
8799
  if (isProcessRunning(pid)) {
@@ -8807,7 +8806,7 @@ bridgeCommand.command("stop-recording").description(
8807
8806
  rmSync4(statePath, { force: true });
8808
8807
  } catch {
8809
8808
  }
8810
- if (!existsSync25(outputPath)) {
8809
+ if (!existsSync26(outputPath)) {
8811
8810
  jsonOut2({
8812
8811
  sessionId,
8813
8812
  stopped: false,
@@ -8887,25 +8886,207 @@ bridgeCommand.command("check-llm").description("Check if LLM is enabled and onli
8887
8886
  }
8888
8887
  }
8889
8888
  jsonOut2({
8890
- enabled,
8891
- online,
8892
- url,
8893
- model,
8894
- modelAvailable,
8895
- availableModels,
8896
- apiFlavor: provider.apiFlavor,
8897
- unsupportedProvider
8889
+ enabled,
8890
+ online,
8891
+ url,
8892
+ model,
8893
+ modelAvailable,
8894
+ availableModels,
8895
+ apiFlavor: provider.apiFlavor,
8896
+ unsupportedProvider
8897
+ });
8898
+ });
8899
+ });
8900
+ bridgeCommand.command("provider-status").description("Show secret-safe provider status for LLM roles (JSON)").action(async () => {
8901
+ await withDb2(async (db) => {
8902
+ const [recall, vision, text] = await Promise.all([
8903
+ getProviderRoleStatus(db, "recall"),
8904
+ getProviderRoleStatus(db, "vision"),
8905
+ getProviderRoleStatus(db, "text")
8906
+ ]);
8907
+ jsonOut2({ roles: { recall, vision, text } });
8908
+ });
8909
+ });
8910
+ bridgeCommand.command("provider-config-list").description("List provider records and role bindings (JSON)").option("--scope <scope>", "machine (default) or shared", "machine").action(async (opts) => {
8911
+ const machine = parseProviderScope(opts.scope);
8912
+ await withProviderScope(machine, async (db) => {
8913
+ const providers = await readScopedProviders(db, machine);
8914
+ const roles = await readScopedRoles(db, machine);
8915
+ const rows = buildProviderListing(
8916
+ providers,
8917
+ (ref) => getProviderApiKey(ref) !== null
8918
+ );
8919
+ jsonOut2({
8920
+ scope: machine ? "machine" : "shared",
8921
+ providers: rows,
8922
+ roles,
8923
+ orphans: findOrphanKeyRefs(listProviderApiKeyRefs(), providers)
8924
+ });
8925
+ });
8926
+ });
8927
+ 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(
8928
+ "--flavor <flavor>",
8929
+ `Wire protocol: ${VALID_API_FLAVORS.join(" | ")}`
8930
+ ).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) => {
8931
+ const machine = parseProviderScope(opts.scope);
8932
+ let apiFlavor;
8933
+ if (opts.flavor) {
8934
+ if (!VALID_API_FLAVORS.includes(opts.flavor)) {
8935
+ jsonError(
8936
+ `Invalid --flavor: ${opts.flavor}. Use ${VALID_API_FLAVORS.join(" or ")}.`
8937
+ );
8938
+ }
8939
+ apiFlavor = opts.flavor;
8940
+ }
8941
+ let local;
8942
+ if (command.getOptionValueSource("local") === "cli") {
8943
+ local = opts.local === true;
8944
+ }
8945
+ const patch = {};
8946
+ if (opts.label !== void 0) patch.label = opts.label;
8947
+ if (opts.url !== void 0) patch.url = opts.url;
8948
+ if (opts.model !== void 0) patch.model = opts.model;
8949
+ if (apiFlavor !== void 0) patch.apiFlavor = apiFlavor;
8950
+ if (opts.keyRef !== void 0) patch.apiKeyRef = opts.keyRef;
8951
+ if (local !== void 0) patch.local = local;
8952
+ if (opts.runner !== void 0) patch.runner = opts.runner;
8953
+ await withProviderScope(machine, async (db) => {
8954
+ const providers = await readScopedProviders(db, machine);
8955
+ const next = upsertProviderRecord(providers, opts.name, patch);
8956
+ await writeScopedProviders(db, machine, next);
8957
+ const rows = buildProviderListing(
8958
+ next,
8959
+ (ref) => getProviderApiKey(ref) !== null
8960
+ );
8961
+ const row = rows.find((entry) => entry.name === opts.name);
8962
+ jsonOut2({
8963
+ ok: true,
8964
+ scope: machine ? "machine" : "shared",
8965
+ name: opts.name,
8966
+ provider: row,
8967
+ cloudModelHint: opts.url ? getCloudModelRecommendation(opts.url) : null
8968
+ });
8969
+ });
8970
+ });
8971
+ 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) => {
8972
+ const machine = parseProviderScope(opts.scope);
8973
+ await withProviderScope(machine, async (db) => {
8974
+ const providers = await readScopedProviders(db, machine);
8975
+ const { providers: next, removed } = removeProviderRecord(
8976
+ providers,
8977
+ opts.name
8978
+ );
8979
+ if (!removed) {
8980
+ jsonError(`No such provider: ${opts.name}`);
8981
+ }
8982
+ await writeScopedProviders(db, machine, next);
8983
+ jsonOut2({
8984
+ ok: true,
8985
+ scope: machine ? "machine" : "shared",
8986
+ name: opts.name,
8987
+ removed: true,
8988
+ referencingRoles: rolesReferencing(
8989
+ await readScopedRoles(db, machine),
8990
+ opts.name
8991
+ )
8992
+ });
8993
+ });
8994
+ });
8995
+ 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) => {
8996
+ if (!VALID_ROLES.includes(opts.role)) {
8997
+ jsonError(`Invalid --role: ${opts.role}. Use ${VALID_ROLES.join(", ")}.`);
8998
+ }
8999
+ const machine = parseProviderScope(opts.scope);
9000
+ await withProviderScope(machine, async (db) => {
9001
+ const providers = await readScopedProviders(db, machine);
9002
+ const roles = await readScopedRoles(db, machine);
9003
+ const nextRoles = bindRoleProviders(
9004
+ roles,
9005
+ opts.role,
9006
+ opts.primary,
9007
+ opts.fallback
9008
+ );
9009
+ await writeScopedRoles(db, machine, nextRoles);
9010
+ const binding = nextRoles[opts.role];
9011
+ const primary = providers[opts.primary];
9012
+ jsonOut2({
9013
+ ok: true,
9014
+ scope: machine ? "machine" : "shared",
9015
+ role: opts.role,
9016
+ binding,
9017
+ warnings: [
9018
+ ...primary && primary.apiFlavor === "anthropic-messages" && (opts.role === "recall" || opts.role === "text") ? ["unsupported-provider-for-role"] : [],
9019
+ ...opts.primary && !(opts.primary in providers) ? ["primary-provider-undefined"] : [],
9020
+ ...opts.fallback && !(opts.fallback in providers) ? ["fallback-provider-undefined"] : []
9021
+ ]
8898
9022
  });
8899
9023
  });
8900
9024
  });
8901
- bridgeCommand.command("provider-status").description("Show secret-safe provider status for LLM roles (JSON)").action(async () => {
9025
+ 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) => {
9026
+ const key = opts.key.trim();
9027
+ if (!key) jsonError("No key provided.");
9028
+ setProviderApiKey(opts.ref, key);
9029
+ jsonOut2({ ok: true, ref: opts.ref, masked: maskSecret(key) });
9030
+ });
9031
+ bridgeCommand.command("provider-clear-key").description("Remove a stored provider API key (JSON)").requiredOption("--ref <ref>", "Credential reference name").action((opts) => {
9032
+ clearProviderApiKey(opts.ref);
9033
+ jsonOut2({ ok: true, ref: opts.ref });
9034
+ });
9035
+ 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) => {
9036
+ const apiKey = opts.keyRef ? getProviderApiKey(opts.keyRef) ?? void 0 : void 0;
9037
+ const models = await getAvailableModels(opts.url, apiKey);
9038
+ jsonOut2({ models });
9039
+ });
9040
+ bridgeCommand.command("cloud-model-hint").description("Suggest a cloud model for an endpoint URL (JSON)").requiredOption("--url <url>", "Endpoint base URL").action((opts) => {
9041
+ jsonOut2({ recommendation: getCloudModelRecommendation(opts.url) });
9042
+ });
9043
+ bridgeCommand.command("local-llm-hints").description("Detect installed local LLM servers and suggest defaults (JSON)").action(() => {
9044
+ const profile = getSystemProfile();
9045
+ const flmInstalled = hasCommand("flm") || existsSync17("C:\\Program Files\\flm\\flm.exe");
9046
+ const ollamaInstalled = hasCommand("ollama");
9047
+ const runners = [
9048
+ { id: "flm", label: "FastFlowLM", installed: flmInstalled },
9049
+ { id: "ollama", label: "Ollama", installed: ollamaInstalled },
9050
+ {
9051
+ id: "foundry-local",
9052
+ label: "Foundry Local",
9053
+ installed: false
9054
+ }
9055
+ ];
9056
+ let recommended = "ollama";
9057
+ if (profile.recommendedRunner === "fastflowlm" && flmInstalled) {
9058
+ recommended = "flm";
9059
+ } else if (profile.recommendedRunner === "ollama" && ollamaInstalled) {
9060
+ recommended = "ollama";
9061
+ } else if (flmInstalled) {
9062
+ recommended = "flm";
9063
+ } else if (ollamaInstalled) {
9064
+ recommended = "ollama";
9065
+ } else if (profile.recommendedRunner === "fastflowlm") {
9066
+ recommended = "flm";
9067
+ }
9068
+ const defaultUrl = recommended === "ollama" ? "http://localhost:11434/v1" : DEFAULT_LLM_URL;
9069
+ jsonOut2({
9070
+ runners,
9071
+ recommended,
9072
+ defaultUrl,
9073
+ defaultModel: profile.recommendedModel || DEFAULT_LLM_MODEL
9074
+ });
9075
+ });
9076
+ var UI_WRITABLE_SETTINGS = /* @__PURE__ */ new Set([
9077
+ "llm.enabled",
9078
+ "llm.vision.enabled",
9079
+ "system.locale"
9080
+ ]);
9081
+ 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) => {
9082
+ if (!UI_WRITABLE_SETTINGS.has(opts.key)) {
9083
+ jsonError(
9084
+ `Setting "${opts.key}" is not writable via setting-set. Allowed: ${[...UI_WRITABLE_SETTINGS].join(", ")}.`
9085
+ );
9086
+ }
8902
9087
  await withDb2(async (db) => {
8903
- const [recall, vision, text] = await Promise.all([
8904
- getProviderRoleStatus(db, "recall"),
8905
- getProviderRoleStatus(db, "vision"),
8906
- getProviderRoleStatus(db, "text")
8907
- ]);
8908
- jsonOut2({ roles: { recall, vision, text } });
9088
+ await setSetting(db, opts.key, opts.value);
9089
+ jsonOut2({ ok: true, key: opts.key, value: opts.value });
8909
9090
  });
8910
9091
  });
8911
9092
  bridgeCommand.command("check-vision").description(
@@ -8954,7 +9135,10 @@ bridgeCommand.command("translate-question").description("Translate a question dy
8954
9135
  });
8955
9136
  bridgeCommand.command("evaluate-answer").description(
8956
9137
  "Evaluate the learner's active-recall answer using the local LLM (JSON)"
8957
- ).requiredOption("--slug <slug>", "Token slug").requiredOption("--concept <concept>", "Target concept text").requiredOption("--domain <domain>", "Token domain").requiredOption("--bloom-level <level>", "Bloom taxonomy level").requiredOption("--question <question>", "Question prompt presented").requiredOption("--user-answer <answer>", "User's typed answer").option("--context <context>", "Optional token context details").option("--source-link <link>", "Optional source link").action(async (opts) => {
9138
+ ).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(
9139
+ "--source-content <content>",
9140
+ "Pre-resolved source reference content (skips re-fetch when set)"
9141
+ ).action(async (opts) => {
8958
9142
  await withDb2(async (db) => {
8959
9143
  const isEnabled = await getSetting(db, "llm.enabled") === "true";
8960
9144
  if (!isEnabled) {
@@ -8965,8 +9149,8 @@ bridgeCommand.command("evaluate-answer").description(
8965
9149
  });
8966
9150
  return;
8967
9151
  }
8968
- let resolvedContextContent = null;
8969
- if (opts.sourceLink) {
9152
+ let resolvedContextContent = opts.sourceContent ?? null;
9153
+ if (resolvedContextContent == null && opts.sourceLink) {
8970
9154
  try {
8971
9155
  const resolved = await resolveReviewContext(opts.sourceLink);
8972
9156
  resolvedContextContent = resolved?.content ?? null;
@@ -8974,7 +9158,7 @@ bridgeCommand.command("evaluate-answer").description(
8974
9158
  }
8975
9159
  }
8976
9160
  try {
8977
- const evaluation = await evaluateAnswerViaLLM(db, {
9161
+ const result = await evaluateAnswerViaLLM(db, {
8978
9162
  slug: opts.slug,
8979
9163
  concept: opts.concept,
8980
9164
  domain: opts.domain,
@@ -8984,7 +9168,11 @@ bridgeCommand.command("evaluate-answer").description(
8984
9168
  userAnswer: opts.userAnswer,
8985
9169
  sourceLinkContent: resolvedContextContent
8986
9170
  });
8987
- jsonOut2({ success: true, evaluation });
9171
+ jsonOut2({
9172
+ success: true,
9173
+ evaluation: result.text,
9174
+ evaluationModel: result.model
9175
+ });
8988
9176
  } catch (err) {
8989
9177
  jsonOut2({
8990
9178
  success: false,
@@ -8998,11 +9186,12 @@ bridgeCommand.command("desktop-bootstrap").description("Initialize first-run des
8998
9186
  await withDb2(async (db) => {
8999
9187
  const userId = await ensureDefaultUser(db, opts.user);
9000
9188
  const { enabled, url, model, locale } = await getLlmConfig(db);
9001
- const { workspaceDir, skillLinks } = await ensureDesktopWorkspace(db);
9189
+ const { workspaceDir, activeWorkspaceId, skillLinks } = await ensureDesktopWorkspace(db);
9002
9190
  jsonOut2({
9003
9191
  userId,
9004
9192
  locale,
9005
9193
  llm: { enabled, url, model },
9194
+ activeWorkspaceId,
9006
9195
  workspaceDir,
9007
9196
  skillLinks
9008
9197
  });
@@ -9223,8 +9412,8 @@ bridgeCommand.command("serve").description("Start the persistent JSON-RPC stdin/
9223
9412
  });
9224
9413
 
9225
9414
  // src/cli/commands/card.ts
9226
- import { Command as Command5 } from "commander";
9227
- var cardCommand = new Command5("card").description(
9415
+ import { Command as Command3 } from "commander";
9416
+ var cardCommand = new Command3("card").description(
9228
9417
  "Manage spaced-repetition cards"
9229
9418
  );
9230
9419
  cardCommand.command("due").description("Show due tokens for a user").option("--user <id>", "User ID (default: whoami)").option("--domain <domain>", "Filter by knowledge domain").option("--json", "Output as JSON").option("--summary", "Show only counts per domain (no slugs or concepts)").action(async (opts) => {
@@ -9422,9 +9611,9 @@ cardCommand.command("delete").description("Delete one user's card for a token").
9422
9611
  });
9423
9612
 
9424
9613
  // src/cli/commands/connector.ts
9425
- import { input as input2, password } from "@inquirer/prompts";
9426
- import { Command as Command6 } from "commander";
9427
- var connectorCommand = new Command6("connector").description(
9614
+ import { input, password } from "@inquirer/prompts";
9615
+ import { Command as Command4 } from "commander";
9616
+ var connectorCommand = new Command4("connector").description(
9428
9617
  "Manage external service connectors"
9429
9618
  );
9430
9619
  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(
@@ -9443,10 +9632,10 @@ connectorCommand.command("setup").description("Configure a connector").argument(
9443
9632
  process.exit(1);
9444
9633
  }
9445
9634
  try {
9446
- const orgUrl = await input2({
9635
+ const orgUrl = await input({
9447
9636
  message: "Organization URL (e.g. https://dev.azure.com/myorg):"
9448
9637
  });
9449
- const project = await input2({
9638
+ const project = await input({
9450
9639
  message: "Project name:"
9451
9640
  });
9452
9641
  const pat = await password({
@@ -9536,8 +9725,16 @@ connectorCommand.command("sync").description("Verify the Turso cloud database co
9536
9725
  });
9537
9726
  async function setupTurso(urlArg, tokenArg, mode) {
9538
9727
  let db;
9728
+ const arch = getSystemProfile().arch;
9729
+ const isWindowsArm64 = process.platform === "win32" && arch === "arm64";
9730
+ const effectiveMode = mode ?? (isWindowsArm64 ? "remote" : void 0);
9731
+ if (!mode && isWindowsArm64) {
9732
+ console.log(
9733
+ "Detected Windows ARM64 \u2014 defaulting to remote (HTTP) mode.\n The native libsql driver is not available on this architecture.\n Remote mode uses the Turso HTTP API and works everywhere.\n"
9734
+ );
9735
+ }
9539
9736
  try {
9540
- const url = urlArg ?? await input2({
9737
+ const url = urlArg ?? await input({
9541
9738
  message: "Turso database URL (e.g. libsql://my-db-user.turso.io):"
9542
9739
  });
9543
9740
  const token = tokenArg ?? await password({
@@ -9547,12 +9744,12 @@ async function setupTurso(urlArg, tokenArg, mode) {
9547
9744
  console.error("Both URL and token are required.");
9548
9745
  process.exit(1);
9549
9746
  }
9550
- setTursoCredentials(url, token, void 0, mode);
9747
+ setTursoCredentials(url, token, void 0, effectiveMode);
9551
9748
  db = await openDatabaseWithSync({ initialize: true });
9552
9749
  await db.prepare("SELECT 1").get();
9553
9750
  await db.close();
9554
9751
  console.log(
9555
- `Turso cloud database configured and verified: ${url}` + (mode ? ` (mode: ${mode})` : "")
9752
+ `Turso cloud database configured and verified: ${url}` + (effectiveMode ? ` (mode: ${effectiveMode})` : "")
9556
9753
  );
9557
9754
  } catch (err) {
9558
9755
  await db?.close();
@@ -9567,26 +9764,26 @@ async function setupTurso(urlArg, tokenArg, mode) {
9567
9764
 
9568
9765
  // src/cli/commands/git-sync.ts
9569
9766
  import { execSync as execSync5 } from "child_process";
9570
- import { chmodSync as chmodSync2, existsSync as existsSync18, writeFileSync as writeFileSync9 } from "fs";
9571
- import { join as join18 } from "path";
9572
- import { Command as Command7 } from "commander";
9767
+ import { chmodSync as chmodSync2, existsSync as existsSync18, writeFileSync as writeFileSync8 } from "fs";
9768
+ import { join as join19 } from "path";
9769
+ import { Command as Command5 } from "commander";
9573
9770
  function installHook2() {
9574
- const gitDir = join18(process.cwd(), ".git");
9771
+ const gitDir = join19(process.cwd(), ".git");
9575
9772
  if (!existsSync18(gitDir)) {
9576
9773
  console.error(
9577
9774
  "Error: Current directory is not the root of a Git repository."
9578
9775
  );
9579
9776
  process.exit(1);
9580
9777
  }
9581
- const hooksDir = join18(gitDir, "hooks");
9582
- const hookPath = join18(hooksDir, "post-commit");
9778
+ const hooksDir = join19(gitDir, "hooks");
9779
+ const hookPath = join19(hooksDir, "post-commit");
9583
9780
  const hookContent = `#!/bin/sh
9584
9781
  # ZAM Spaced Repetition Auto-Stale Hook
9585
9782
  # Triggered automatically on git commits to decay modified concept cards.
9586
9783
  zam git-sync --commit HEAD --quiet
9587
9784
  `;
9588
9785
  try {
9589
- writeFileSync9(hookPath, hookContent, { encoding: "utf-8", flag: "w" });
9786
+ writeFileSync8(hookPath, hookContent, { encoding: "utf-8", flag: "w" });
9590
9787
  try {
9591
9788
  chmodSync2(hookPath, "755");
9592
9789
  } catch (_e) {
@@ -9599,7 +9796,7 @@ zam git-sync --commit HEAD --quiet
9599
9796
  process.exit(1);
9600
9797
  }
9601
9798
  }
9602
- var gitSyncCommand = new Command7("git-sync").description("Sync learning cards with recent Git file modifications").option("--commit <hash>", "Git commit hash to check", "HEAD").option("--user <id>", "User ID (default: whoami)").option("--install", "Install git post-commit hook in current repo").option("--quiet", "Suppress verbose output").action(async (opts) => {
9799
+ var gitSyncCommand = new Command5("git-sync").description("Sync learning cards with recent Git file modifications").option("--commit <hash>", "Git commit hash to check", "HEAD").option("--user <id>", "User ID (default: whoami)").option("--install", "Install git post-commit hook in current repo").option("--quiet", "Suppress verbose output").action(async (opts) => {
9603
9800
  if (opts.install) {
9604
9801
  installHook2();
9605
9802
  return;
@@ -9689,9 +9886,9 @@ ZAM Auto-Stale Complete: Scanned ${changedFiles.length} file(s).`
9689
9886
 
9690
9887
  // src/cli/commands/goal.ts
9691
9888
  import { existsSync as existsSync19, mkdirSync as mkdirSync11 } from "fs";
9692
- import { resolve as resolve6 } from "path";
9693
- import { input as input3 } from "@inquirer/prompts";
9694
- import { Command as Command8 } from "commander";
9889
+ import { resolve as resolve5 } from "path";
9890
+ import { input as input2 } from "@inquirer/prompts";
9891
+ import { Command as Command6 } from "commander";
9695
9892
  async function resolveGoalsDir() {
9696
9893
  let goalsDir;
9697
9894
  let db;
@@ -9702,9 +9899,9 @@ async function resolveGoalsDir() {
9702
9899
  } finally {
9703
9900
  await db?.close();
9704
9901
  }
9705
- return goalsDir ? resolve6(goalsDir) : resolve6("goals");
9902
+ return goalsDir ? resolve5(goalsDir) : resolve5("goals");
9706
9903
  }
9707
- var goalCommand = new Command8("goal").description(
9904
+ var goalCommand = new Command6("goal").description(
9708
9905
  "Manage learning goals (markdown files)"
9709
9906
  );
9710
9907
  goalCommand.command("list").description("List all goals").option(
@@ -9823,11 +10020,11 @@ goalCommand.command("create").description("Create a new goal").option("--slug <s
9823
10020
  if (!slug || !title) {
9824
10021
  try {
9825
10022
  if (!title) {
9826
- title = await input3({ message: "Goal title:" });
10023
+ title = await input2({ message: "Goal title:" });
9827
10024
  }
9828
10025
  if (!slug) {
9829
10026
  const suggested = title.toLowerCase().replace(/[^a-z0-9]+/g, "-").replace(/^-|-$/g, "");
9830
- slug = await input3({
10027
+ slug = await input2({
9831
10028
  message: "Goal slug (filename):",
9832
10029
  default: suggested
9833
10030
  });
@@ -9873,22 +10070,22 @@ goalCommand.command("status <slug> <status>").description("Update a goal's statu
9873
10070
  });
9874
10071
 
9875
10072
  // src/cli/commands/init.ts
9876
- import { existsSync as existsSync20, mkdirSync as mkdirSync12, writeFileSync as writeFileSync10 } from "fs";
10073
+ import { existsSync as existsSync20, mkdirSync as mkdirSync12, writeFileSync as writeFileSync9 } from "fs";
9877
10074
  import { homedir as homedir11 } from "os";
9878
- import { join as join19, resolve as resolve7 } from "path";
9879
- import { confirm as confirm2, input as input4 } from "@inquirer/prompts";
9880
- import { Command as Command9 } from "commander";
10075
+ import { join as join20, resolve as resolve6 } from "path";
10076
+ import { confirm, input as input3 } from "@inquirer/prompts";
10077
+ import { Command as Command7 } from "commander";
9881
10078
  var HOME2 = homedir11();
9882
10079
  function printLine(char = "\u2550", len = 60, color = "\x1B[36m") {
9883
10080
  console.log(`${color}${char.repeat(len)}\x1B[0m`);
9884
10081
  }
9885
10082
  function bootstrapSandboxWorkspace(workspaceDir) {
9886
- mkdirSync12(join19(workspaceDir, "beliefs"), { recursive: true });
9887
- mkdirSync12(join19(workspaceDir, "goals"), { recursive: true });
9888
- mkdirSync12(join19(workspaceDir, "skills"), { recursive: true });
9889
- const worldviewFile = join19(workspaceDir, "beliefs", "worldview.md");
10083
+ mkdirSync12(join20(workspaceDir, "beliefs"), { recursive: true });
10084
+ mkdirSync12(join20(workspaceDir, "goals"), { recursive: true });
10085
+ mkdirSync12(join20(workspaceDir, "skills"), { recursive: true });
10086
+ const worldviewFile = join20(workspaceDir, "beliefs", "worldview.md");
9890
10087
  if (!existsSync20(worldviewFile)) {
9891
- writeFileSync10(
10088
+ writeFileSync9(
9892
10089
  worldviewFile,
9893
10090
  `# Personal Worldview
9894
10091
 
@@ -9900,9 +10097,9 @@ Here, I declare the core concepts and principles I want to master.
9900
10097
  "utf8"
9901
10098
  );
9902
10099
  }
9903
- const goalsFile = join19(workspaceDir, "goals", "goals.md");
10100
+ const goalsFile = join20(workspaceDir, "goals", "goals.md");
9904
10101
  if (!existsSync20(goalsFile)) {
9905
- writeFileSync10(
10102
+ writeFileSync9(
9906
10103
  goalsFile,
9907
10104
  `# Personal Goals
9908
10105
 
@@ -9914,7 +10111,7 @@ Here, I declare the core concepts and principles I want to master.
9914
10111
  );
9915
10112
  }
9916
10113
  }
9917
- var initCommand = new Command9("init").description("Launch the guided interactive onboarding wizard").action(async () => {
10114
+ var initCommand = new Command7("init").description("Launch the guided interactive onboarding wizard").action(async () => {
9918
10115
  printLine();
9919
10116
  console.log(
9920
10117
  "\x1B[1m\x1B[32m ZAM \u2014 The Symbiotic Learning Agent Onboarding\x1B[0m"
@@ -9924,9 +10121,9 @@ var initCommand = new Command9("init").description("Launch the guided interactiv
9924
10121
  );
9925
10122
  printLine();
9926
10123
  console.log("\n\x1B[1m[1/5] Setting up Local Workspace Sandbox\x1B[0m");
9927
- const defaultWorkspace = join19(HOME2, "Documents", "zam");
9928
- const workspacePath = resolve7(
9929
- await input4({
10124
+ const defaultWorkspace = join20(HOME2, "Documents", "zam");
10125
+ const workspacePath = resolve6(
10126
+ await input3({
9930
10127
  message: "Choose your ZAM workspace directory:",
9931
10128
  default: defaultWorkspace
9932
10129
  })
@@ -9940,6 +10137,7 @@ var initCommand = new Command9("init").description("Launch the guided interactiv
9940
10137
  kind: "personal",
9941
10138
  path: workspacePath
9942
10139
  });
10140
+ setActiveWorkspaceId("personal");
9943
10141
  console.log(
9944
10142
  `\x1B[32m\u2713 Local Sandbox created at: ${workspacePath}\x1B[0m`
9945
10143
  );
@@ -9970,7 +10168,7 @@ var initCommand = new Command9("init").description("Launch the guided interactiv
9970
10168
  \x1B[1mRecommendation:\x1B[0m ZAM suggests installing \x1B[32m${runnerLabel}\x1B[0m with \x1B[36m${profile.recommendedModel}\x1B[0m.`
9971
10169
  );
9972
10170
  console.log("\n\x1B[1m[3/5] Setting up Local LLM Runner\x1B[0m");
9973
- const proceedInstall = await confirm2({
10171
+ const proceedInstall = await confirm({
9974
10172
  message: `Would you like ZAM to install and configure ${runnerLabel} automatically?`,
9975
10173
  default: true
9976
10174
  });
@@ -10007,7 +10205,7 @@ var initCommand = new Command9("init").description("Launch the guided interactiv
10007
10205
  let db;
10008
10206
  try {
10009
10207
  db = await openDatabaseWithSync({ initialize: true });
10010
- await setSetting(db, "personal.workspace_dir", workspacePath);
10208
+ await deleteSetting(db, "personal.workspace_dir");
10011
10209
  const detectedLocale = detectSystemLocale();
10012
10210
  await setSetting(db, "system.locale", detectedLocale);
10013
10211
  console.log(
@@ -10038,7 +10236,7 @@ var initCommand = new Command9("init").description("Launch the guided interactiv
10038
10236
  console.log(
10039
10237
  "\n\x1B[1m[5/5] Wiring Developer Agents & Terminal Helpers\x1B[0m"
10040
10238
  );
10041
- const proceedHooks = await confirm2({
10239
+ const proceedHooks = await confirm({
10042
10240
  message: "Distribute ZAM skills and install optional monitored-session shell helpers?",
10043
10241
  default: true
10044
10242
  });
@@ -10089,8 +10287,8 @@ var initCommand = new Command9("init").description("Launch the guided interactiv
10089
10287
  });
10090
10288
 
10091
10289
  // src/cli/commands/learn.ts
10092
- import { input as input6 } from "@inquirer/prompts";
10093
- import { Command as Command10 } from "commander";
10290
+ import { input as input5 } from "@inquirer/prompts";
10291
+ import { Command as Command8 } from "commander";
10094
10292
 
10095
10293
  // src/cli/learn-format.ts
10096
10294
  var BLOOM_VERBS3 = {
@@ -10139,7 +10337,7 @@ function formatReveal(input8) {
10139
10337
  }
10140
10338
 
10141
10339
  // src/cli/review-actions.ts
10142
- import { confirm as confirm3, input as input5, select } from "@inquirer/prompts";
10340
+ import { confirm as confirm2, input as input4, select } from "@inquirer/prompts";
10143
10341
  async function runInteractiveReviewAction(inputData) {
10144
10342
  let currentItem = { ...inputData.item };
10145
10343
  while (true) {
@@ -10254,7 +10452,7 @@ async function runInteractiveReviewAction(inputData) {
10254
10452
  continue;
10255
10453
  }
10256
10454
  if (choice === "deprecate-token") {
10257
- const approved = await confirm3({
10455
+ const approved = await confirm2({
10258
10456
  message: `Deprecate ${currentItem.slug} so it stops appearing in review queues?`,
10259
10457
  default: false
10260
10458
  });
@@ -10285,7 +10483,7 @@ async function runInteractiveReviewAction(inputData) {
10285
10483
  console.log(` Session steps: ${impact.session_steps}`);
10286
10484
  console.log(` Sessions touched: ${impact.sessions_touched}`);
10287
10485
  console.log(` Agent skills updated: ${impact.agent_skills}`);
10288
- const approved = await confirm3({
10486
+ const approved = await confirm2({
10289
10487
  message: "Permanently delete this token and its dependent learning data?",
10290
10488
  default: false
10291
10489
  });
@@ -10310,7 +10508,7 @@ async function runInteractiveReviewAction(inputData) {
10310
10508
  );
10311
10509
  console.log(`Delete your card for ${currentItem.slug}?`);
10312
10510
  console.log(` Review logs removed: ${impact.review_logs}`);
10313
- const approved = await confirm3({
10511
+ const approved = await confirm2({
10314
10512
  message: "Delete only your card for this token?",
10315
10513
  default: false
10316
10514
  });
@@ -10348,21 +10546,21 @@ async function promptTokenEdit(token) {
10348
10546
  }
10349
10547
  switch (field) {
10350
10548
  case "concept": {
10351
- const concept = await input5({
10549
+ const concept = await input4({
10352
10550
  message: "Concept:",
10353
10551
  default: token.concept
10354
10552
  });
10355
10553
  return concept === token.concept ? null : { concept };
10356
10554
  }
10357
10555
  case "domain": {
10358
- const domain = await input5({
10556
+ const domain = await input4({
10359
10557
  message: "Domain (blank allowed):",
10360
10558
  default: token.domain
10361
10559
  });
10362
10560
  return domain === token.domain ? null : { domain };
10363
10561
  }
10364
10562
  case "context": {
10365
- const context = await input5({
10563
+ const context = await input4({
10366
10564
  message: "Context (blank allowed):",
10367
10565
  default: token.context
10368
10566
  });
@@ -10403,14 +10601,14 @@ async function promptTokenEdit(token) {
10403
10601
  return mode === token.symbiosis_mode ? null : { symbiosis_mode: mode };
10404
10602
  }
10405
10603
  case "source_link": {
10406
- const link = await input5({
10604
+ const link = await input4({
10407
10605
  message: "Source link (blank allowed):",
10408
10606
  default: token.source_link ?? ""
10409
10607
  });
10410
10608
  return link === token.source_link ? null : { source_link: link || null };
10411
10609
  }
10412
10610
  case "question": {
10413
- const question = await input5({
10611
+ const question = await input4({
10414
10612
  message: "Recall question (blank allowed):",
10415
10613
  default: token.question ?? ""
10416
10614
  });
@@ -10428,7 +10626,7 @@ var STOP_WORDS = /* @__PURE__ */ new Set(["q", ":q", "quit", "stop"]);
10428
10626
  function isExitPrompt(err) {
10429
10627
  return err instanceof Error && err.name === "ExitPromptError";
10430
10628
  }
10431
- var learnCommand = new Command10("learn").description(
10629
+ var learnCommand = new Command8("learn").description(
10432
10630
  "Run a spoiler-free, in-process learning session (recall \u2192 reveal \u2192 self-rate)"
10433
10631
  ).option("--user <id>", "User ID (default: whoami)").option("--max-new <n>", "Maximum new cards", "10").option("--max-reviews <n>", "Maximum review cards", "50").option("--no-resolve", "Skip resolving source_link into the revealed answer").action(async (opts) => {
10434
10632
  let db;
@@ -10484,7 +10682,7 @@ ${t(locale, "welcome", { count: queue.items.length })}`);
10484
10682
  question: item.question
10485
10683
  });
10486
10684
  if (healed) {
10487
- resolvedQuestion = healed;
10685
+ resolvedQuestion = healed.question;
10488
10686
  }
10489
10687
  } catch {
10490
10688
  }
@@ -10508,7 +10706,7 @@ ${"\u2500".repeat(50)}`);
10508
10706
  ${prompt.question}`);
10509
10707
  let answer;
10510
10708
  try {
10511
- answer = await input6({
10709
+ answer = await input5({
10512
10710
  message: t(locale, "prompt_answer")
10513
10711
  });
10514
10712
  } catch (err) {
@@ -10549,7 +10747,8 @@ ${"\u2500".repeat(50)}`);
10549
10747
  `
10550
10748
  ${t(locale, "feedback_title", { line: "\u2500".repeat(34) })}`
10551
10749
  );
10552
- for (const line of evaluation.split("\n")) {
10750
+ console.log(` \x1B[2m[${evaluation.model}]\x1B[0m`);
10751
+ for (const line of evaluation.text.split("\n")) {
10553
10752
  console.log(` ${line}`);
10554
10753
  }
10555
10754
  } catch (err) {
@@ -10672,8 +10871,8 @@ learnCommand.command("open").description("Open a new terminal window running zam
10672
10871
  });
10673
10872
 
10674
10873
  // src/cli/commands/monitor.ts
10675
- import { Command as Command11 } from "commander";
10676
- var monitorCommand = new Command11("monitor").description(
10874
+ import { Command as Command9 } from "commander";
10875
+ var monitorCommand = new Command9("monitor").description(
10677
10876
  "Shell observation for real-time task monitoring"
10678
10877
  );
10679
10878
  monitorCommand.command("start").description("Output shell hook code to install monitoring").requiredOption("--session <id>", "Session ID to monitor").option(
@@ -10855,8 +11054,8 @@ monitorCommand.command("open").description("Open a new monitored terminal window
10855
11054
  });
10856
11055
 
10857
11056
  // src/cli/commands/observer.ts
10858
- import { Command as Command12 } from "commander";
10859
- var observerCommand = new Command12("observer").description(
11057
+ import { Command as Command10 } from "commander";
11058
+ var observerCommand = new Command10("observer").description(
10860
11059
  "Configure what the UI observer may capture (Layer 2 policy)"
10861
11060
  );
10862
11061
  function applyObserverListChange(current, entry, op) {
@@ -10918,9 +11117,8 @@ observerCommand.command("revoke <process>").description("Remove a process from o
10918
11117
  });
10919
11118
 
10920
11119
  // src/cli/commands/profile.ts
10921
- import { homedir as homedir12 } from "os";
10922
- import { dirname as dirname6, join as join20, resolve as resolve8 } from "path";
10923
- import { Command as Command13 } from "commander";
11120
+ import { dirname as dirname6, resolve as resolve7 } from "path";
11121
+ import { Command as Command11 } from "commander";
10924
11122
  var C2 = {
10925
11123
  reset: "\x1B[0m",
10926
11124
  bold: "\x1B[1m",
@@ -10928,9 +11126,6 @@ var C2 = {
10928
11126
  dim: "\x1B[2m",
10929
11127
  green: "\x1B[32m"
10930
11128
  };
10931
- function defaultPersonalDir() {
10932
- return join20(homedir12(), "Documents", "zam");
10933
- }
10934
11129
  function render(profile) {
10935
11130
  const sync = profile.syncProvider ? `${C2.green}${profile.syncProvider}${C2.reset} ${C2.dim}(good for cross-device snapshots)${C2.reset}` : `${C2.dim}local folder (use a synced folder or snapshots to move between machines)${C2.reset}`;
10936
11131
  console.log(`${C2.bold}ZAM install profile${C2.reset}`);
@@ -10940,7 +11135,7 @@ function render(profile) {
10940
11135
  console.log(` data dir: ${C2.cyan}${profile.dataDir}${C2.reset}`);
10941
11136
  console.log(` database: ${C2.cyan}${profile.dbPath}${C2.reset}`);
10942
11137
  }
10943
- var profileCommand = new Command13("profile").description("Show or change this machine's ZAM install profile").option("--mode <mode>", "Set install mode: developer | default").option("--dir <path>", "Set the personal-content folder").option("--json", "Output as JSON").action(async (opts) => {
11138
+ var profileCommand = new Command11("profile").description("Show or change this machine's ZAM install profile").option("--mode <mode>", "Set install mode: developer | default").option("--dir <path>", "Set the personal-content folder").option("--json", "Output as JSON").action(async (opts) => {
10944
11139
  if (opts.mode && opts.mode !== "developer" && opts.mode !== "default") {
10945
11140
  console.error(`Invalid --mode: ${opts.mode}. Use developer or default.`);
10946
11141
  process.exit(1);
@@ -10950,9 +11145,12 @@ var profileCommand = new Command13("profile").description("Show or change this m
10950
11145
  if (opts.mode) setInstallMode(opts.mode);
10951
11146
  db = await openDatabaseWithSync({ initialize: true });
10952
11147
  if (opts.dir) {
10953
- await setSetting(db, "personal.workspace_dir", resolve8(opts.dir));
11148
+ await activateWorkspacePath(db, resolve7(opts.dir), {
11149
+ kind: "personal",
11150
+ label: "Personal"
11151
+ });
10954
11152
  }
10955
- const personalDir = await getSetting(db, "personal.workspace_dir") || defaultPersonalDir();
11153
+ const personalDir = (await ensureActiveWorkspace(db)).path;
10956
11154
  await db.close();
10957
11155
  db = void 0;
10958
11156
  const dbPath = getDefaultDbPath();
@@ -10977,120 +11175,8 @@ var profileCommand = new Command13("profile").description("Show or change this m
10977
11175
 
10978
11176
  // src/cli/commands/provider.ts
10979
11177
  import { password as password2 } from "@inquirer/prompts";
10980
- import { Command as Command14 } from "commander";
10981
- var VALID_API_FLAVORS = [
10982
- "chat-completions",
10983
- "anthropic-messages"
10984
- ];
10985
- var VALID_ROLES = ["vision", "recall", "text"];
10986
- function upsertProviderRecord(providers, name, patch) {
10987
- const merged = { ...providers[name] ?? {} };
10988
- if (patch.url !== void 0) merged.url = patch.url;
10989
- if (patch.model !== void 0) merged.model = patch.model;
10990
- if (patch.apiFlavor !== void 0) merged.apiFlavor = patch.apiFlavor;
10991
- if (patch.apiKeyRef !== void 0) merged.apiKeyRef = patch.apiKeyRef;
10992
- if (patch.label !== void 0) merged.label = patch.label;
10993
- if (patch.local !== void 0) merged.local = patch.local;
10994
- if (patch.runner !== void 0) merged.runner = patch.runner;
10995
- return { ...providers, [name]: merged };
10996
- }
10997
- function removeProviderRecord(providers, name) {
10998
- if (!(name in providers)) return { providers, removed: false };
10999
- const next = { ...providers };
11000
- delete next[name];
11001
- return { providers: next, removed: true };
11002
- }
11003
- function rolesReferencing(roles, name) {
11004
- return VALID_ROLES.filter((role) => {
11005
- const binding = roles[role];
11006
- return binding?.primary === name || binding?.fallback === name;
11007
- });
11008
- }
11009
- function bindRoleProviders(roles, role, primary, fallback) {
11010
- const binding = { primary };
11011
- if (fallback) binding.fallback = fallback;
11012
- return { ...roles, [role]: binding };
11013
- }
11014
- function maskSecret(key) {
11015
- return key.length <= 4 ? "\u2022\u2022\u2022\u2022" : `\u2026${key.slice(-4)}`;
11016
- }
11017
- function buildProviderListing(providers, hasKey) {
11018
- return Object.entries(providers).map(([name, rec]) => {
11019
- const apiFlavor = rec.apiFlavor ?? (rec.url ? inferApiFlavor(rec.url) : "chat-completions");
11020
- let keyState;
11021
- if (!rec.apiKeyRef) keyState = "none";
11022
- else keyState = hasKey(rec.apiKeyRef) ? "set" : "missing";
11023
- const row = {
11024
- name,
11025
- url: rec.url,
11026
- model: rec.model,
11027
- apiFlavor,
11028
- apiKeyRef: rec.apiKeyRef,
11029
- keyState
11030
- };
11031
- if (rec.label !== void 0) row.label = rec.label;
11032
- if (rec.local !== void 0) row.local = rec.local;
11033
- if (rec.runner !== void 0) row.runner = rec.runner;
11034
- return row;
11035
- });
11036
- }
11037
- function findOrphanKeyRefs(storedRefs, providers) {
11038
- const used = new Set(
11039
- Object.values(providers).map((rec) => rec.apiKeyRef).filter((ref) => !!ref)
11040
- );
11041
- return storedRefs.filter((ref) => !used.has(ref));
11042
- }
11043
- async function readJson(db, key, fallback) {
11044
- const raw = await getSetting(db, key);
11045
- if (!raw) return fallback;
11046
- try {
11047
- return JSON.parse(raw);
11048
- } catch {
11049
- return fallback;
11050
- }
11051
- }
11052
- var readProviders = (db) => readJson(db, "llm.providers", {});
11053
- var readRoles = (db) => readJson(db, "llm.roles", {});
11054
- async function readScopedProviders(db, machine) {
11055
- if (machine) return getMachineAiConfig().providers ?? {};
11056
- if (!db)
11057
- throw new Error("Database is required for shared provider settings.");
11058
- return readProviders(db);
11059
- }
11060
- async function readScopedRoles(db, machine) {
11061
- if (machine) return getMachineAiConfig().roles ?? {};
11062
- if (!db)
11063
- throw new Error("Database is required for shared provider settings.");
11064
- return readRoles(db);
11065
- }
11066
- async function writeScopedProviders(db, machine, p) {
11067
- if (machine) {
11068
- const config = getMachineAiConfig();
11069
- saveMachineAiConfig({ ...config, providers: p });
11070
- return;
11071
- }
11072
- if (!db)
11073
- throw new Error("Database is required for shared provider settings.");
11074
- await setSetting(db, "llm.providers", JSON.stringify(p));
11075
- }
11076
- async function writeScopedRoles(db, machine, r) {
11077
- if (machine) {
11078
- const config = getMachineAiConfig();
11079
- saveMachineAiConfig({ ...config, roles: r });
11080
- return;
11081
- }
11082
- if (!db)
11083
- throw new Error("Database is required for shared provider settings.");
11084
- await setSetting(db, "llm.roles", JSON.stringify(r));
11085
- }
11086
- async function withProviderScope(machine, action) {
11087
- if (machine) {
11088
- await action(void 0);
11089
- return;
11090
- }
11091
- await withDb(action);
11092
- }
11093
- var providerCommand = new Command14("provider").description(
11178
+ import { Command as Command12 } from "commander";
11179
+ var providerCommand = new Command12("provider").description(
11094
11180
  "Configure role-based AI providers (url/model/flavor/key, per role)"
11095
11181
  );
11096
11182
  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) => {
@@ -11325,8 +11411,8 @@ providerCommand.command("clear-key <ref>").description("Remove a stored API key"
11325
11411
  });
11326
11412
 
11327
11413
  // src/cli/commands/review.ts
11328
- import { Command as Command15 } from "commander";
11329
- var reviewCommand = new Command15("review").description("Start an interactive review session").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 inline context").action(async (opts) => {
11414
+ import { Command as Command13 } from "commander";
11415
+ var reviewCommand = new Command13("review").description("Start an interactive review session").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 inline context").action(async (opts) => {
11330
11416
  let db;
11331
11417
  try {
11332
11418
  db = await openDatabase();
@@ -11440,9 +11526,9 @@ ${"\u2550".repeat(50)}`);
11440
11526
 
11441
11527
  // src/cli/commands/session.ts
11442
11528
  import { readFileSync as readFileSync12 } from "fs";
11443
- import { input as input7, select as select2 } from "@inquirer/prompts";
11444
- import { Command as Command16 } from "commander";
11445
- var sessionCommand = new Command16("session").description(
11529
+ import { input as input6, select as select2 } from "@inquirer/prompts";
11530
+ import { Command as Command14 } from "commander";
11531
+ var sessionCommand = new Command14("session").description(
11446
11532
  "Manage learning sessions"
11447
11533
  );
11448
11534
  sessionCommand.command("start").description("Start a new learning session (review \u2192 task)").option("--user <id>", "User ID (default: whoami)").option("--task <description>", "Task description (interactive if omitted)").option(
@@ -11619,7 +11705,7 @@ async function selectTask() {
11619
11705
  console.log("No active work items found in Azure DevOps.");
11620
11706
  }
11621
11707
  }
11622
- return input7({ message: "Task description:" });
11708
+ return input6({ message: "Task description:" });
11623
11709
  }
11624
11710
  var RATING_LABELS = {
11625
11711
  1: "Again",
@@ -11822,8 +11908,8 @@ sessionCommand.command("end").description("End a session and show summary").requ
11822
11908
 
11823
11909
  // src/cli/commands/settings.ts
11824
11910
  import { existsSync as existsSync21 } from "fs";
11825
- import { Command as Command17 } from "commander";
11826
- var settingsCommand = new Command17("settings").description(
11911
+ import { Command as Command15 } from "commander";
11912
+ var settingsCommand = new Command15("settings").description(
11827
11913
  "Manage user settings"
11828
11914
  );
11829
11915
  var BOOLEAN_SETTING_KEYS = /* @__PURE__ */ new Set(["llm.enabled", "llm.vision.enabled"]);
@@ -11994,12 +12080,108 @@ settingsCommand.command("repos").description("Show or set Personal, Team, and Or
11994
12080
  console.log(` ${name.padEnd(9)}: \x1B[33m- Not Set\x1B[0m`);
11995
12081
  }
11996
12082
  }
11997
- });
11998
- });
12083
+ });
12084
+ });
12085
+
12086
+ // src/cli/commands/setup.ts
12087
+ import { resolve as resolve8 } from "path";
12088
+ import { Command as Command16 } from "commander";
12089
+ function formatDatabaseInitTarget(target) {
12090
+ switch (target.kind) {
12091
+ case "local":
12092
+ return `ZAM database at ${target.location} (local SQLite)`;
12093
+ case "turso-remote":
12094
+ return `ZAM database via Turso remote at ${target.location}`;
12095
+ case "turso-native":
12096
+ return `ZAM database via Turso native driver at ${target.location}`;
12097
+ case "turso-replica":
12098
+ return `ZAM database replica at ${target.location} syncing from ${target.syncUrl}`;
12099
+ }
12100
+ }
12101
+ async function initDatabase(skipInit) {
12102
+ if (skipInit) return;
12103
+ try {
12104
+ const target = getDatabaseTargetInfo();
12105
+ const db = await openDatabaseWithSync({ initialize: true });
12106
+ await activateMachineProviderConfig(db);
12107
+ await db.close();
12108
+ console.log(` init ${formatDatabaseInitTarget(target)}`);
12109
+ } catch (err) {
12110
+ const msg = err.message;
12111
+ if (!msg.includes("already")) {
12112
+ console.warn(` warn database init: ${msg}`);
12113
+ } else {
12114
+ console.log(` skip database already initialized`);
12115
+ }
12116
+ }
12117
+ }
12118
+ async function activateMachineProviderConfig(db) {
12119
+ const machineAi = getMachineAiConfig();
12120
+ const providerCount = Object.keys(machineAi.providers ?? {}).length;
12121
+ const roleCount = Object.keys(machineAi.roles ?? {}).length;
12122
+ if (providerCount === 0 && roleCount === 0) return;
12123
+ if (await getSetting(db, "llm.enabled") !== void 0) return;
12124
+ await setSetting(db, "llm.enabled", "true");
12125
+ console.log(
12126
+ ` activate ${providerCount} machine-local provider(s) from ~/.zam/config.json`
12127
+ );
12128
+ }
12129
+ var setupCommand = new Command16("setup").description(
12130
+ "Link ZAM skill directories into this workspace and initialize the database"
12131
+ ).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(
12132
+ "--agents <list>",
12133
+ "comma-separated agents to wire: all, claude, copilot, codex, agent"
12134
+ ).option(
12135
+ "--dry-run",
12136
+ "show what would be written without changing files",
12137
+ false
12138
+ ).action(
12139
+ async (opts) => {
12140
+ let agents;
12141
+ try {
12142
+ agents = parseSetupAgents(opts.agents);
12143
+ } catch (err) {
12144
+ console.error(`Error: ${err.message}`);
12145
+ process.exit(1);
12146
+ }
12147
+ const target = resolve8(opts.target ?? process.cwd());
12148
+ const updateExistingInstructions = Boolean(opts.target) || opts.force;
12149
+ console.log(
12150
+ `Setting up ZAM in ${target}${opts.dryRun ? " (dry run)" : ""}
12151
+ `
12152
+ );
12153
+ wireSkills(target, agents, {
12154
+ force: opts.force,
12155
+ dryRun: opts.dryRun
12156
+ });
12157
+ await initDatabase(opts.skipInit || opts.dryRun);
12158
+ if (agents.has("claude")) {
12159
+ writeClaudeMd(opts.skipClaudeMd, target, {
12160
+ dryRun: opts.dryRun,
12161
+ updateExisting: updateExistingInstructions
12162
+ });
12163
+ }
12164
+ if (agents.has("codex") || agents.has("agent")) {
12165
+ writeAgentsMd(opts.skipAgentsMd, target, {
12166
+ dryRun: opts.dryRun,
12167
+ updateExisting: updateExistingInstructions
12168
+ });
12169
+ }
12170
+ if (agents.has("copilot") && (opts.target || opts.agents)) {
12171
+ writeCopilotInstructions(target, {
12172
+ dryRun: opts.dryRun,
12173
+ updateExisting: updateExistingInstructions
12174
+ });
12175
+ }
12176
+ console.log(
12177
+ "\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."
12178
+ );
12179
+ }
12180
+ );
11999
12181
 
12000
12182
  // src/cli/commands/skill.ts
12001
- import { Command as Command18 } from "commander";
12002
- var skillCommand = new Command18("skill").description(
12183
+ import { Command as Command17 } from "commander";
12184
+ var skillCommand = new Command17("skill").description(
12003
12185
  "Manage agent skill entries (task recipes)"
12004
12186
  );
12005
12187
  skillCommand.command("list").description("List all agent skills").option("--json", "Output as JSON").action(async (opts) => {
@@ -12084,10 +12266,9 @@ skillCommand.command("add").description("Register a new agent skill").requiredOp
12084
12266
  });
12085
12267
 
12086
12268
  // src/cli/commands/snapshot.ts
12087
- import { existsSync as existsSync22, mkdirSync as mkdirSync13, readFileSync as readFileSync13, writeFileSync as writeFileSync11 } from "fs";
12088
- import { homedir as homedir13 } from "os";
12269
+ import { existsSync as existsSync22, mkdirSync as mkdirSync13, readFileSync as readFileSync13, writeFileSync as writeFileSync10 } from "fs";
12089
12270
  import { dirname as dirname7, join as join21 } from "path";
12090
- import { Command as Command19 } from "commander";
12271
+ import { Command as Command18 } from "commander";
12091
12272
  function defaultOutName() {
12092
12273
  const stamp = (/* @__PURE__ */ new Date()).toISOString().replace(/\.\d+Z$/, "").replace(/:/g, "-");
12093
12274
  return `zam-snapshot-${stamp}.sql`;
@@ -12101,12 +12282,12 @@ function summarize(tables) {
12101
12282
  }
12102
12283
  return { total, nonEmpty };
12103
12284
  }
12104
- var exportCmd = new Command19("export").description("Write a portable SQL-text snapshot of the database").option("--out <file>", "Output file (use - for stdout)").action(async (opts) => {
12285
+ var exportCmd = new Command18("export").description("Write a portable SQL-text snapshot of the database").option("--out <file>", "Output file (use - for stdout)").action(async (opts) => {
12105
12286
  let db;
12106
12287
  try {
12107
12288
  db = await openDatabaseWithSync({ initialize: true });
12108
12289
  const snapshot = await exportSnapshot(db);
12109
- const personalDir = await getSetting(db, "personal.workspace_dir") || join21(homedir13(), "Documents", "zam");
12290
+ const personalDir = (await ensureActiveWorkspace(db)).path;
12110
12291
  await db.close();
12111
12292
  db = void 0;
12112
12293
  const manifest = verifySnapshot(snapshot);
@@ -12120,7 +12301,7 @@ var exportCmd = new Command19("export").description("Write a portable SQL-text s
12120
12301
  if (dir && dir !== "." && !existsSync22(dir)) {
12121
12302
  mkdirSync13(dir, { recursive: true });
12122
12303
  }
12123
- writeFileSync11(out, snapshot, "utf-8");
12304
+ writeFileSync10(out, snapshot, "utf-8");
12124
12305
  console.log(`Snapshot written: ${out}`);
12125
12306
  console.log(
12126
12307
  ` ${total} row(s)${nonEmpty.length ? ` \u2014 ${nonEmpty.join(", ")}` : ""}`
@@ -12131,7 +12312,7 @@ var exportCmd = new Command19("export").description("Write a portable SQL-text s
12131
12312
  process.exit(1);
12132
12313
  }
12133
12314
  });
12134
- var importCmd = new Command19("import").description("Restore a snapshot into the database").argument("<file>", "Snapshot file to restore").option("--force", "Overwrite a non-empty database", false).action(async (file, opts) => {
12315
+ var importCmd = new Command18("import").description("Restore a snapshot into the database").argument("<file>", "Snapshot file to restore").option("--force", "Overwrite a non-empty database", false).action(async (file, opts) => {
12135
12316
  let db;
12136
12317
  try {
12137
12318
  if (!existsSync22(file)) {
@@ -12154,7 +12335,7 @@ var importCmd = new Command19("import").description("Restore a snapshot into the
12154
12335
  process.exit(1);
12155
12336
  }
12156
12337
  });
12157
- var verifyCmd = new Command19("verify").description("Check a snapshot's manifest and checksum without importing").argument("<file>", "Snapshot file to verify").action((file) => {
12338
+ var verifyCmd = new Command18("verify").description("Check a snapshot's manifest and checksum without importing").argument("<file>", "Snapshot file to verify").action((file) => {
12158
12339
  try {
12159
12340
  if (!existsSync22(file)) {
12160
12341
  console.error(`Error: Snapshot file not found: ${file}`);
@@ -12172,11 +12353,11 @@ var verifyCmd = new Command19("verify").description("Check a snapshot's manifest
12172
12353
  process.exit(1);
12173
12354
  }
12174
12355
  });
12175
- var snapshotCommand = new Command19("snapshot").description("Export, import, or verify a portable database snapshot").addCommand(exportCmd).addCommand(importCmd).addCommand(verifyCmd);
12356
+ var snapshotCommand = new Command18("snapshot").description("Export, import, or verify a portable database snapshot").addCommand(exportCmd).addCommand(importCmd).addCommand(verifyCmd);
12176
12357
 
12177
12358
  // src/cli/commands/stats.ts
12178
- import { Command as Command20 } from "commander";
12179
- var statsCommand = new Command20("stats").description("Show learning dashboard for a user").option("--user <id>", "User ID (default: whoami)").option("--json", "Output as JSON").action(async (opts) => {
12359
+ import { Command as Command19 } from "commander";
12360
+ var statsCommand = new Command19("stats").description("Show learning dashboard for a user").option("--user <id>", "User ID (default: whoami)").option("--json", "Output as JSON").action(async (opts) => {
12180
12361
  await withDb(async (db) => {
12181
12362
  const userId = await resolveUser(opts, db);
12182
12363
  const stats = await getUserStats(db, userId);
@@ -12212,8 +12393,8 @@ var statsCommand = new Command20("stats").description("Show learning dashboard f
12212
12393
  });
12213
12394
 
12214
12395
  // src/cli/commands/token.ts
12215
- import { Command as Command21 } from "commander";
12216
- var tokenCommand = new Command21("token").description(
12396
+ import { Command as Command20 } from "commander";
12397
+ var tokenCommand = new Command20("token").description(
12217
12398
  "Manage knowledge tokens"
12218
12399
  );
12219
12400
  tokenCommand.command("register").description("Register a new knowledge token").requiredOption("--slug <slug>", "Unique token slug").requiredOption("--concept <concept>", "Concept description").option("--domain <domain>", "Knowledge domain", "").option("--bloom <level>", "Bloom taxonomy level (1-5)", "1").option("--source-link <link>", "Source file path or reference URL", "").option("--question <question>", "Specific question prompt for recall", "").option("--json", "Output as JSON").option("--quiet", "Suppress output (exit code only)").action(async (opts) => {
@@ -12501,10 +12682,10 @@ tokenCommand.command("status").description("Show full status of a token for a us
12501
12682
  // src/cli/commands/ui.ts
12502
12683
  import { spawn as spawn3, spawnSync } from "child_process";
12503
12684
  import { existsSync as existsSync23 } from "fs";
12504
- import { homedir as homedir14 } from "os";
12685
+ import { homedir as homedir12 } from "os";
12505
12686
  import { dirname as dirname8, join as join22 } from "path";
12506
12687
  import { fileURLToPath as fileURLToPath3 } from "url";
12507
- import { Command as Command22 } from "commander";
12688
+ import { Command as Command21 } from "commander";
12508
12689
  var C3 = {
12509
12690
  reset: "\x1B[0m",
12510
12691
  red: "\x1B[31m",
@@ -12551,7 +12732,7 @@ function findInstalledApp() {
12551
12732
  process.env.LOCALAPPDATA && join22(process.env.LOCALAPPDATA, "Programs", "ZAM", "ZAM.exe"),
12552
12733
  process.env.ProgramFiles && join22(process.env.ProgramFiles, "ZAM", "ZAM.exe"),
12553
12734
  process.env["ProgramFiles(x86)"] && join22(process.env["ProgramFiles(x86)"], "ZAM", "ZAM.exe")
12554
- ] : process.platform === "darwin" ? ["/Applications/ZAM.app", join22(homedir14(), "Applications", "ZAM.app")] : ["/opt/ZAM/zam", "/usr/bin/zam-desktop"];
12735
+ ] : process.platform === "darwin" ? ["/Applications/ZAM.app", join22(homedir12(), "Applications", "ZAM.app")] : ["/opt/ZAM/zam", "/usr/bin/zam-desktop"];
12555
12736
  return candidates.find((candidate) => candidate && existsSync23(candidate)) || null;
12556
12737
  }
12557
12738
  function runNpm(args, opts) {
@@ -12684,13 +12865,13 @@ function createShortcuts(appPath, repoRoot) {
12684
12865
  console.error(`${C3.red}\u2717 Could not create shortcuts.${C3.reset}`);
12685
12866
  }
12686
12867
  }
12687
- var uiCommand = new Command22("ui").description("Launch the ZAM Desktop GUI (Active-Recall Studio)").option("--dev", "Run in hot-reload development mode (needs Rust)").option(
12868
+ var uiCommand = new Command21("ui").description("Launch the ZAM Desktop GUI (Active-Recall Studio)").option("--dev", "Run in hot-reload development mode (needs Rust)").option(
12688
12869
  "--build",
12689
12870
  "Build the native installer for your OS (needs Rust, one-time)"
12690
12871
  ).option("--shortcut", "Create Desktop + Start-menu shortcuts to the GUI").action((opts) => {
12691
12872
  const installedApp = findInstalledApp();
12692
12873
  if (!opts.dev && !opts.build && !opts.shortcut && installedApp) {
12693
- launchApp(installedApp, homedir14());
12874
+ launchApp(installedApp, homedir12());
12694
12875
  return;
12695
12876
  }
12696
12877
  const desktopDir = findDesktopDir();
@@ -12791,8 +12972,8 @@ import { spawnSync as spawnSync2 } from "child_process";
12791
12972
  import { existsSync as existsSync24, readFileSync as readFileSync14, realpathSync as realpathSync2 } from "fs";
12792
12973
  import { dirname as dirname9, join as join23 } from "path";
12793
12974
  import { fileURLToPath as fileURLToPath4 } from "url";
12794
- import { confirm as confirm4 } from "@inquirer/prompts";
12795
- import { Command as Command23 } from "commander";
12975
+ import { confirm as confirm3 } from "@inquirer/prompts";
12976
+ import { Command as Command22 } from "commander";
12796
12977
  var GITHUB_REPO = "zam-os/zam";
12797
12978
  var CHANNELS = [
12798
12979
  "developer",
@@ -12820,273 +13001,647 @@ function currentVersion() {
12820
13001
  } catch {
12821
13002
  }
12822
13003
  }
12823
- return "0.0.0";
13004
+ return "0.0.0";
13005
+ }
13006
+ function versionAt(dir) {
13007
+ try {
13008
+ const pkg2 = JSON.parse(
13009
+ readFileSync14(join23(dir, "package.json"), "utf-8")
13010
+ );
13011
+ return pkg2.version ?? "unknown";
13012
+ } catch {
13013
+ return "unknown";
13014
+ }
13015
+ }
13016
+ async function fetchLatestVersion(repo) {
13017
+ const res = await fetch(
13018
+ `https://api.github.com/repos/${repo}/releases/latest`,
13019
+ {
13020
+ headers: {
13021
+ Accept: "application/vnd.github+json",
13022
+ "User-Agent": "zam-cli"
13023
+ }
13024
+ }
13025
+ );
13026
+ if (!res.ok) {
13027
+ throw new Error(
13028
+ `Could not reach the release server (HTTP ${res.status}). Pass --latest <version> to check offline.`
13029
+ );
13030
+ }
13031
+ const data = await res.json();
13032
+ if (!data.tag_name) throw new Error("No published release found yet.");
13033
+ return data.tag_name;
13034
+ }
13035
+ function render2(decision) {
13036
+ if (!decision.updateAvailable) {
13037
+ console.log(
13038
+ `${C4.green}\u2713${C4.reset} ZAM is up to date (${C4.cyan}${decision.currentVersion}${C4.reset}).`
13039
+ );
13040
+ return;
13041
+ }
13042
+ console.log(
13043
+ `${C4.yellow}\u2191${C4.reset} ${C4.bold}Update available${C4.reset}: ${C4.dim}${decision.currentVersion}${C4.reset} \u2192 ${C4.cyan}${decision.latestVersion}${C4.reset}`
13044
+ );
13045
+ console.log(` ${decision.reason}`);
13046
+ if (decision.action === "run-command" || decision.action === "inform") {
13047
+ console.log(` Run: ${C4.cyan}${decision.command}${C4.reset}`);
13048
+ } else if (decision.action === "self-update") {
13049
+ console.log(
13050
+ ` ${C4.dim}The desktop app can apply this update for you.${C4.reset}`
13051
+ );
13052
+ }
13053
+ }
13054
+ var checkCmd = new Command22("check").description("Check whether a newer ZAM has been released").option(
13055
+ "--latest <version>",
13056
+ "Compare against this version instead of fetching"
13057
+ ).option("--channel <channel>", "Override the detected install channel").option("--json", "Output as JSON").action(
13058
+ async (opts) => {
13059
+ try {
13060
+ if (opts.channel && !CHANNELS.includes(opts.channel)) {
13061
+ console.error(
13062
+ `Invalid --channel: ${opts.channel}. Use ${CHANNELS.join(", ")}.`
13063
+ );
13064
+ process.exit(1);
13065
+ }
13066
+ const current = currentVersion();
13067
+ const latest = opts.latest ?? await fetchLatestVersion(GITHUB_REPO);
13068
+ const channel = opts.channel ?? getInstallChannel();
13069
+ const decision = decideUpdate({
13070
+ currentVersion: current,
13071
+ latestVersion: latest,
13072
+ channel
13073
+ });
13074
+ if (opts.json) {
13075
+ console.log(JSON.stringify(decision, null, 2));
13076
+ return;
13077
+ }
13078
+ render2(decision);
13079
+ } catch (err) {
13080
+ console.error("Error:", err.message);
13081
+ process.exit(1);
13082
+ }
13083
+ }
13084
+ );
13085
+ function findSourceRepo() {
13086
+ let dir = realpathSync2(dirname9(fileURLToPath4(import.meta.url)));
13087
+ let parent = dirname9(dir);
13088
+ while (parent !== dir) {
13089
+ if (existsSync24(join23(dir, ".git"))) return dir;
13090
+ dir = parent;
13091
+ parent = dirname9(dir);
13092
+ }
13093
+ return existsSync24(join23(dir, ".git")) ? dir : null;
13094
+ }
13095
+ function runGit(cwd, args, capture) {
13096
+ const res = spawnSync2("git", args, {
13097
+ cwd,
13098
+ stdio: capture ? ["ignore", "pipe", "pipe"] : "inherit",
13099
+ encoding: "utf8"
13100
+ });
13101
+ return { ok: res.status === 0, out: (res.stdout ?? "").trim() };
13102
+ }
13103
+ function runNpm2(args, cwd) {
13104
+ const res = spawnSync2("npm", args, {
13105
+ cwd,
13106
+ stdio: "inherit",
13107
+ shell: process.platform === "win32"
13108
+ });
13109
+ return res.status ?? 1;
13110
+ }
13111
+ function runShell(command) {
13112
+ const res = spawnSync2(command, { stdio: "inherit", shell: true });
13113
+ return res.status === 0;
13114
+ }
13115
+ function applyDeveloperUpdate(force) {
13116
+ if (!hasCommand("git")) {
13117
+ console.error(`${C4.red}\u2717${C4.reset} git was not found on PATH.`);
13118
+ process.exit(1);
13119
+ }
13120
+ const src = findSourceRepo();
13121
+ if (!src) {
13122
+ console.error(
13123
+ `${C4.red}\u2717${C4.reset} Could not locate the ZAM source checkout to update.`
13124
+ );
13125
+ process.exit(1);
13126
+ }
13127
+ const status = runGit(src, ["status", "--porcelain"], true);
13128
+ if (status.ok && status.out && !force) {
13129
+ console.error(
13130
+ `${C4.red}\u2717${C4.reset} The source checkout has uncommitted changes:
13131
+ ${status.out}
13132
+ Commit or stash them, or re-run with ${C4.cyan}--force${C4.reset}.`
13133
+ );
13134
+ process.exit(1);
13135
+ }
13136
+ console.log(
13137
+ `${C4.dim}\u2192 git pull --ff-only${C4.reset} ${C4.dim}(${src})${C4.reset}`
13138
+ );
13139
+ if (!runGit(src, ["pull", "--ff-only"], false).ok) {
13140
+ console.error(
13141
+ `${C4.red}\u2717${C4.reset} git pull failed \u2014 the branch may have diverged or you are offline. Resolve it manually, then retry.`
13142
+ );
13143
+ process.exit(1);
13144
+ }
13145
+ console.log(`${C4.dim}\u2192 npm install${C4.reset}`);
13146
+ if (runNpm2(["install"], src) !== 0) {
13147
+ console.error(`${C4.red}\u2717${C4.reset} npm install failed.`);
13148
+ process.exit(1);
13149
+ }
13150
+ console.log(`${C4.dim}\u2192 npm run build${C4.reset}`);
13151
+ if (runNpm2(["run", "build"], src) !== 0) {
13152
+ console.error(`${C4.red}\u2717${C4.reset} Build failed.`);
13153
+ process.exit(1);
13154
+ }
13155
+ console.log(`${C4.dim}\u2192 zam setup --force${C4.reset}`);
13156
+ const setup = spawnSync2(
13157
+ process.execPath,
13158
+ [join23(src, "dist", "cli", "index.js"), "setup", "--force"],
13159
+ { cwd: process.cwd(), stdio: "inherit" }
13160
+ );
13161
+ if (setup.status !== 0) {
13162
+ console.warn(
13163
+ `${C4.yellow}\u26A0${C4.reset} Skill refresh reported a problem \u2014 run '${C4.cyan}zam setup --force${C4.reset}' here manually.`
13164
+ );
13165
+ }
13166
+ console.log(
13167
+ `
13168
+ ${C4.green}\u2713${C4.reset} Updated to ${C4.cyan}${versionAt(src)}${C4.reset}. Restart your agent client (e.g. Claude Code) to load the refreshed ${C4.cyan}/zam${C4.reset} skill.`
13169
+ );
13170
+ }
13171
+ async function applyUpdate(opts) {
13172
+ try {
13173
+ const current = currentVersion();
13174
+ const latest = await fetchLatestVersion(GITHUB_REPO);
13175
+ const channel = getInstallChannel();
13176
+ const decision = decideUpdate({
13177
+ currentVersion: current,
13178
+ latestVersion: latest,
13179
+ channel
13180
+ });
13181
+ if (!decision.updateAvailable) {
13182
+ console.log(
13183
+ `${C4.green}\u2713${C4.reset} ZAM is up to date (${C4.cyan}${current}${C4.reset}).`
13184
+ );
13185
+ return;
13186
+ }
13187
+ console.log(
13188
+ `${C4.yellow}\u2191${C4.reset} ${C4.bold}Update available${C4.reset}: ${C4.dim}${current}${C4.reset} \u2192 ${C4.cyan}${latest}${C4.reset} ${C4.dim}(${channel} install)${C4.reset}`
13189
+ );
13190
+ for (const step of planUpdate(decision)) {
13191
+ console.log(` ${C4.dim}\u2022${C4.reset} ${step.label}`);
13192
+ }
13193
+ if (channel === "direct") {
13194
+ console.log(
13195
+ `
13196
+ ${C4.dim}This install updates through the desktop app's signed updater \u2014 open ZAM Desktop to install ${latest}.${C4.reset}`
13197
+ );
13198
+ return;
13199
+ }
13200
+ if (!opts.yes) {
13201
+ const ok = await confirm3({
13202
+ message: "Apply this update?",
13203
+ default: true
13204
+ });
13205
+ if (!ok) {
13206
+ console.log("Aborted.");
13207
+ return;
13208
+ }
13209
+ }
13210
+ console.log();
13211
+ if (channel === "winget" || channel === "homebrew") {
13212
+ if (!decision.command || !runShell(decision.command)) process.exit(1);
13213
+ return;
13214
+ }
13215
+ applyDeveloperUpdate(opts.force ?? false);
13216
+ } catch (err) {
13217
+ console.error("Error:", err.message);
13218
+ process.exit(1);
13219
+ }
13220
+ }
13221
+ var updateCommand = new Command22("update").description(
13222
+ "Update ZAM to the latest release (use `update check` to only check)"
13223
+ ).option("-y, --yes", "Apply without confirmation").option(
13224
+ "--force",
13225
+ "Update even if the source checkout has uncommitted changes"
13226
+ ).action(async (opts) => {
13227
+ await applyUpdate(opts);
13228
+ }).addCommand(checkCmd);
13229
+
13230
+ // src/cli/commands/whoami.ts
13231
+ import { Command as Command23 } from "commander";
13232
+ var whoamiCommand = new Command23("whoami").description("Show or set the default user identity").option("--set <id>", "Set the default user ID").option("--clear", "Remove the default user ID").option("--json", "Output as JSON").action(async (opts) => {
13233
+ await withDb(async (db) => {
13234
+ if (opts.set) {
13235
+ await setSetting(db, "user.id", opts.set);
13236
+ if (opts.json) {
13237
+ console.log(JSON.stringify({ userId: opts.set }));
13238
+ } else {
13239
+ console.log(`Default user set to: ${opts.set}`);
13240
+ }
13241
+ return;
13242
+ }
13243
+ if (opts.clear) {
13244
+ const deleted = await deleteSetting(db, "user.id");
13245
+ if (opts.json) {
13246
+ console.log(JSON.stringify({ userId: null, cleared: deleted }));
13247
+ } else if (deleted) {
13248
+ console.log("Default user cleared.");
13249
+ } else {
13250
+ console.log("No default user was set.");
13251
+ }
13252
+ return;
13253
+ }
13254
+ const userId = await getSetting(db, "user.id");
13255
+ if (opts.json) {
13256
+ console.log(JSON.stringify({ userId: userId ?? null }));
13257
+ return;
13258
+ }
13259
+ if (userId) {
13260
+ console.log(userId);
13261
+ } else {
13262
+ console.log("No default user set. Use: zam whoami --set <id>");
13263
+ }
13264
+ });
13265
+ });
13266
+
13267
+ // src/cli/commands/workspace.ts
13268
+ import { execFileSync as execFileSync4 } from "child_process";
13269
+ import { existsSync as existsSync25, writeFileSync as writeFileSync11 } from "fs";
13270
+ import { homedir as homedir13 } from "os";
13271
+ import { join as join24, resolve as resolve9 } from "path";
13272
+ import { confirm as confirm4, input as input7 } from "@inquirer/prompts";
13273
+ import { Command as Command24 } from "commander";
13274
+ function runGit2(cwd, args) {
13275
+ try {
13276
+ return execFileSync4("git", args, {
13277
+ cwd,
13278
+ stdio: "pipe",
13279
+ encoding: "utf8"
13280
+ }).trim();
13281
+ } catch (err) {
13282
+ throw new Error(`Git command failed: ${err.message}`);
13283
+ }
12824
13284
  }
12825
- function versionAt(dir) {
12826
- try {
12827
- const pkg2 = JSON.parse(
12828
- readFileSync14(join23(dir, "package.json"), "utf-8")
12829
- );
12830
- return pkg2.version ?? "unknown";
12831
- } catch {
12832
- return "unknown";
13285
+ function ghRepoCreateArgs(repoName, repoVisibility) {
13286
+ return ["repo", "create", repoName, repoVisibility, "--source=.", "--push"];
13287
+ }
13288
+ function gitRemoteArgs(githubUrl, hasOrigin) {
13289
+ return hasOrigin ? ["remote", "set-url", "origin", githubUrl] : ["remote", "add", "origin", githubUrl];
13290
+ }
13291
+ var workspaceCommand = new Command24("workspace").description(
13292
+ "Manage your ZAM learning workspace"
13293
+ );
13294
+ var WORKSPACE_KINDS = [
13295
+ "personal",
13296
+ "team",
13297
+ "family",
13298
+ "community",
13299
+ "organization",
13300
+ "custom"
13301
+ ];
13302
+ var WORKSPACE_SOURCE_CONTROLS = [
13303
+ "github",
13304
+ "azure-devops",
13305
+ "git",
13306
+ "none"
13307
+ ];
13308
+ function parseWorkspaceKind(value) {
13309
+ const kind = (value ?? "custom").toLowerCase();
13310
+ if (WORKSPACE_KINDS.includes(kind)) {
13311
+ return kind;
12833
13312
  }
13313
+ throw new Error(
13314
+ `Invalid workspace kind: ${value}. Use ${WORKSPACE_KINDS.join(", ")}.`
13315
+ );
12834
13316
  }
12835
- async function fetchLatestVersion(repo) {
12836
- const res = await fetch(
12837
- `https://api.github.com/repos/${repo}/releases/latest`,
12838
- {
12839
- headers: {
12840
- Accept: "application/vnd.github+json",
12841
- "User-Agent": "zam-cli"
12842
- }
12843
- }
13317
+ function parseWorkspaceSourceControl(value) {
13318
+ if (!value) return void 0;
13319
+ const source = value.toLowerCase();
13320
+ if (WORKSPACE_SOURCE_CONTROLS.includes(source)) {
13321
+ return source;
13322
+ }
13323
+ throw new Error(
13324
+ `Invalid source control: ${value}. Use ${WORKSPACE_SOURCE_CONTROLS.join(", ")}.`
12844
13325
  );
12845
- if (!res.ok) {
13326
+ }
13327
+ function parseScopes(value) {
13328
+ if (!value) return void 0;
13329
+ const scopes = value.split(",").map((item) => item.trim()).filter(Boolean);
13330
+ return scopes.length > 0 ? scopes : void 0;
13331
+ }
13332
+ function requireWorkspace(id) {
13333
+ const workspace = getConfiguredWorkspaces().find((item) => item.id === id);
13334
+ if (!workspace) {
12846
13335
  throw new Error(
12847
- `Could not reach the release server (HTTP ${res.status}). Pass --latest <version> to check offline.`
13336
+ `Workspace "${id}" is not configured. Add it with: zam workspace add ${id} --path <dir>`
12848
13337
  );
12849
13338
  }
12850
- const data = await res.json();
12851
- if (!data.tag_name) throw new Error("No published release found yet.");
12852
- return data.tag_name;
13339
+ return workspace;
12853
13340
  }
12854
- function render2(decision) {
12855
- if (!decision.updateAvailable) {
12856
- console.log(
12857
- `${C4.green}\u2713${C4.reset} ZAM is up to date (${C4.cyan}${decision.currentVersion}${C4.reset}).`
12858
- );
12859
- return;
13341
+ function formatLinkHealth(id, health) {
13342
+ switch (health) {
13343
+ case "healthy":
13344
+ return "ok";
13345
+ case "needs-repair":
13346
+ return `needs repair \u2014 run: zam workspace setup ${id}`;
13347
+ case "unmanaged":
13348
+ return `unmanaged skill dir \u2014 run: zam workspace setup ${id} --force`;
12860
13349
  }
12861
- console.log(
12862
- `${C4.yellow}\u2191${C4.reset} ${C4.bold}Update available${C4.reset}: ${C4.dim}${decision.currentVersion}${C4.reset} \u2192 ${C4.cyan}${decision.latestVersion}${C4.reset}`
13350
+ }
13351
+ workspaceCommand.command("list").description("List configured ZAM workspaces").option("--json", "Output as JSON").action((opts) => {
13352
+ const workspaces = getConfiguredWorkspaces();
13353
+ const agents = parseSetupAgents();
13354
+ const linkHealth = Object.fromEntries(
13355
+ workspaces.filter((workspace) => existsSync25(workspace.path)).map((workspace) => [
13356
+ workspace.id,
13357
+ summarizeSkillLinkHealth(inspectSkillLinks(workspace.path, agents))
13358
+ ])
12863
13359
  );
12864
- console.log(` ${decision.reason}`);
12865
- if (decision.action === "run-command" || decision.action === "inform") {
12866
- console.log(` Run: ${C4.cyan}${decision.command}${C4.reset}`);
12867
- } else if (decision.action === "self-update") {
13360
+ if (opts.json) {
13361
+ console.log(JSON.stringify({ workspaces, linkHealth }, null, 2));
13362
+ return;
13363
+ }
13364
+ console.log("Configured ZAM workspaces:\n");
13365
+ if (workspaces.length === 0) {
12868
13366
  console.log(
12869
- ` ${C4.dim}The desktop app can apply this update for you.${C4.reset}`
13367
+ " (none) \u2014 add one: zam workspace add personal --path <dir>"
12870
13368
  );
13369
+ return;
12871
13370
  }
12872
- }
12873
- var checkCmd = new Command23("check").description("Check whether a newer ZAM has been released").option(
12874
- "--latest <version>",
12875
- "Compare against this version instead of fetching"
12876
- ).option("--channel <channel>", "Override the detected install channel").option("--json", "Output as JSON").action(
12877
- async (opts) => {
12878
- try {
12879
- if (opts.channel && !CHANNELS.includes(opts.channel)) {
12880
- console.error(
12881
- `Invalid --channel: ${opts.channel}. Use ${CHANNELS.join(", ")}.`
12882
- );
12883
- process.exit(1);
12884
- }
12885
- const current = currentVersion();
12886
- const latest = opts.latest ?? await fetchLatestVersion(GITHUB_REPO);
12887
- const channel = opts.channel ?? getInstallChannel();
12888
- const decision = decideUpdate({
12889
- currentVersion: current,
12890
- latestVersion: latest,
12891
- channel
12892
- });
12893
- if (opts.json) {
12894
- console.log(JSON.stringify(decision, null, 2));
12895
- return;
12896
- }
12897
- render2(decision);
12898
- } catch (err) {
12899
- console.error("Error:", err.message);
13371
+ for (const workspace of workspaces) {
13372
+ const label = workspace.label ? ` (${workspace.label})` : "";
13373
+ console.log(` ${workspace.id}${label}`);
13374
+ console.log(` kind: ${workspace.kind}`);
13375
+ console.log(` path: ${workspace.path}`);
13376
+ if (workspace.sourceControl) {
13377
+ console.log(` source: ${workspace.sourceControl}`);
13378
+ }
13379
+ if (workspace.knowledgeScopes?.length) {
13380
+ console.log(` scopes: ${workspace.knowledgeScopes.join(", ")}`);
13381
+ }
13382
+ const health = linkHealth[workspace.id];
13383
+ if (health) {
13384
+ console.log(` links: ${formatLinkHealth(workspace.id, health)}`);
13385
+ }
13386
+ }
13387
+ });
13388
+ workspaceCommand.command("add <id>").description("Register an existing directory as a ZAM workspace").requiredOption("--path <dir>", "Existing workspace/repository directory").option(
13389
+ "--kind <kind>",
13390
+ `Workspace kind (${WORKSPACE_KINDS.join(" | ")})`,
13391
+ "custom"
13392
+ ).option("--label <label>", "Human-readable label").option(
13393
+ "--source-control <provider>",
13394
+ `Source-control provider (${WORKSPACE_SOURCE_CONTROLS.join(" | ")})`
13395
+ ).option("--scopes <list>", "Comma-separated knowledge scopes").option("--default-agent <id>", "Default agent harness for this workspace").action((id, opts) => {
13396
+ try {
13397
+ const path = resolve9(String(opts.path));
13398
+ if (!existsSync25(path)) {
13399
+ console.error(`Workspace path does not exist: ${path}`);
12900
13400
  process.exit(1);
12901
13401
  }
13402
+ const workspace = {
13403
+ id,
13404
+ kind: parseWorkspaceKind(opts.kind),
13405
+ path,
13406
+ ...opts.label ? { label: opts.label } : {},
13407
+ ...opts.sourceControl ? { sourceControl: parseWorkspaceSourceControl(opts.sourceControl) } : {},
13408
+ ...parseScopes(opts.scopes) ? { knowledgeScopes: parseScopes(opts.scopes) } : {},
13409
+ ...opts.defaultAgent ? { defaultAgent: opts.defaultAgent } : {}
13410
+ };
13411
+ wireSkills(path, parseSetupAgents());
13412
+ upsertConfiguredWorkspace(workspace);
13413
+ console.log(`Registered and linked workspace "${id}" at ${path}.`);
13414
+ } catch (err) {
13415
+ console.error(`Error: ${err.message}`);
13416
+ process.exit(1);
12902
13417
  }
12903
- );
12904
- function findSourceRepo() {
12905
- let dir = realpathSync2(dirname9(fileURLToPath4(import.meta.url)));
12906
- let parent = dirname9(dir);
12907
- while (parent !== dir) {
12908
- if (existsSync24(join23(dir, ".git"))) return dir;
12909
- dir = parent;
12910
- parent = dirname9(dir);
13418
+ });
13419
+ workspaceCommand.command("remove <id>").description("Unregister a ZAM workspace without deleting its files").action((id) => {
13420
+ const existing = getConfiguredWorkspaces().find((item) => item.id === id);
13421
+ if (!existing) {
13422
+ console.error(`Workspace "${id}" is not configured.`);
13423
+ process.exit(1);
12911
13424
  }
12912
- return existsSync24(join23(dir, ".git")) ? dir : null;
12913
- }
12914
- function runGit2(cwd, args, capture) {
12915
- const res = spawnSync2("git", args, {
12916
- cwd,
12917
- stdio: capture ? ["ignore", "pipe", "pipe"] : "inherit",
12918
- encoding: "utf8"
12919
- });
12920
- return { ok: res.status === 0, out: (res.stdout ?? "").trim() };
12921
- }
12922
- function runNpm2(args, cwd) {
12923
- const res = spawnSync2("npm", args, {
12924
- cwd,
12925
- stdio: "inherit",
12926
- shell: process.platform === "win32"
12927
- });
12928
- return res.status ?? 1;
12929
- }
12930
- function runShell(command) {
12931
- const res = spawnSync2(command, { stdio: "inherit", shell: true });
12932
- return res.status === 0;
12933
- }
12934
- function applyDeveloperUpdate(force) {
12935
- if (!hasCommand("git")) {
12936
- console.error(`${C4.red}\u2717${C4.reset} git was not found on PATH.`);
13425
+ removeConfiguredWorkspace(id);
13426
+ console.log(
13427
+ `Unregistered workspace "${id}". Files in ${existing.path} were not changed.`
13428
+ );
13429
+ });
13430
+ workspaceCommand.command("setup <id>").description("Install ZAM skills into a configured workspace").option(
13431
+ "--agents <list>",
13432
+ "comma-separated agents to wire: all, claude, copilot, codex, agent"
13433
+ ).option(
13434
+ "--force",
13435
+ "overwrite existing skill files and refresh ZAM blocks",
13436
+ false
13437
+ ).option(
13438
+ "--dry-run",
13439
+ "show what would be written without changing files",
13440
+ false
13441
+ ).action((id, opts) => {
13442
+ try {
13443
+ const workspace = requireWorkspace(id);
13444
+ const agents = parseSetupAgents(opts.agents);
13445
+ console.log(
13446
+ `Setting up workspace "${workspace.id}" in ${workspace.path}${opts.dryRun ? " (dry run)" : ""}
13447
+ `
13448
+ );
13449
+ wireSkills(workspace.path, agents, {
13450
+ force: Boolean(opts.force),
13451
+ dryRun: Boolean(opts.dryRun)
13452
+ });
13453
+ if (agents.has("claude")) {
13454
+ writeClaudeMd(false, workspace.path, {
13455
+ dryRun: Boolean(opts.dryRun),
13456
+ updateExisting: true
13457
+ });
13458
+ }
13459
+ if (agents.has("codex") || agents.has("agent")) {
13460
+ writeAgentsMd(false, workspace.path, {
13461
+ dryRun: Boolean(opts.dryRun),
13462
+ updateExisting: true
13463
+ });
13464
+ }
13465
+ if (agents.has("copilot")) {
13466
+ writeCopilotInstructions(workspace.path, {
13467
+ dryRun: Boolean(opts.dryRun),
13468
+ updateExisting: true
13469
+ });
13470
+ }
13471
+ } catch (err) {
13472
+ console.error(`Error: ${err.message}`);
12937
13473
  process.exit(1);
12938
13474
  }
12939
- const src = findSourceRepo();
12940
- if (!src) {
13475
+ });
13476
+ workspaceCommand.command("publish").description("Publish your local workspace sandbox to GitHub").action(async () => {
13477
+ let db;
13478
+ let workspaceDir = "";
13479
+ try {
13480
+ db = await openDatabase();
13481
+ workspaceDir = (await ensureActiveWorkspace(db)).path;
13482
+ await db.close();
13483
+ } catch {
13484
+ await db?.close();
13485
+ }
13486
+ if (!workspaceDir) {
12941
13487
  console.error(
12942
- `${C4.red}\u2717${C4.reset} Could not locate the ZAM source checkout to update.`
13488
+ "\x1B[31m\u2717 No active workspace configured. Please run `zam init` first.\x1B[0m"
12943
13489
  );
12944
13490
  process.exit(1);
12945
13491
  }
12946
- const status = runGit2(src, ["status", "--porcelain"], true);
12947
- if (status.ok && status.out && !force) {
13492
+ if (!existsSync25(workspaceDir)) {
12948
13493
  console.error(
12949
- `${C4.red}\u2717${C4.reset} The source checkout has uncommitted changes:
12950
- ${status.out}
12951
- Commit or stash them, or re-run with ${C4.cyan}--force${C4.reset}.`
13494
+ `\x1B[31m\u2717 Workspace directory does not exist: ${workspaceDir}\x1B[0m`
12952
13495
  );
12953
13496
  process.exit(1);
12954
13497
  }
12955
- console.log(
12956
- `${C4.dim}\u2192 git pull --ff-only${C4.reset} ${C4.dim}(${src})${C4.reset}`
12957
- );
12958
- if (!runGit2(src, ["pull", "--ff-only"], false).ok) {
13498
+ console.log(`
13499
+ Active workspace: \x1B[36m${workspaceDir}\x1B[0m`);
13500
+ if (!hasCommand("git")) {
12959
13501
  console.error(
12960
- `${C4.red}\u2717${C4.reset} git pull failed \u2014 the branch may have diverged or you are offline. Resolve it manually, then retry.`
13502
+ "\x1B[31m\u2717 Git command was not found on this system. Please install Git first.\x1B[0m"
12961
13503
  );
12962
13504
  process.exit(1);
12963
13505
  }
12964
- console.log(`${C4.dim}\u2192 npm install${C4.reset}`);
12965
- if (runNpm2(["install"], src) !== 0) {
12966
- console.error(`${C4.red}\u2717${C4.reset} npm install failed.`);
12967
- process.exit(1);
13506
+ const gitignorePath = join24(workspaceDir, ".gitignore");
13507
+ if (!existsSync25(gitignorePath)) {
13508
+ writeFileSync11(
13509
+ gitignorePath,
13510
+ "node_modules/\n.agent/\n.agents/\n.claude/\n.gemini/\n.goose/\n*.log\n",
13511
+ "utf8"
13512
+ );
12968
13513
  }
12969
- console.log(`${C4.dim}\u2192 npm run build${C4.reset}`);
12970
- if (runNpm2(["run", "build"], src) !== 0) {
12971
- console.error(`${C4.red}\u2717${C4.reset} Build failed.`);
12972
- process.exit(1);
13514
+ const hasGitRepo = existsSync25(join24(workspaceDir, ".git"));
13515
+ if (!hasGitRepo) {
13516
+ console.log("Initializing local Git repository...");
13517
+ runGit2(workspaceDir, ["init", "-b", "main"]);
13518
+ runGit2(workspaceDir, ["add", "."]);
13519
+ runGit2(workspaceDir, [
13520
+ "commit",
13521
+ "-m",
13522
+ "chore: initial workspace sandbox bootstrap"
13523
+ ]);
13524
+ console.log("\x1B[32m\u2713 Local Git repository initialized.\x1B[0m");
13525
+ } else {
13526
+ console.log("Git repository is already initialized.");
12973
13527
  }
12974
- console.log(`${C4.dim}\u2192 zam setup --force${C4.reset}`);
12975
- const setup = spawnSync2(
12976
- process.execPath,
12977
- [join23(src, "dist", "cli", "index.js"), "setup", "--force"],
12978
- { cwd: process.cwd(), stdio: "inherit" }
12979
- );
12980
- if (setup.status !== 0) {
12981
- console.warn(
12982
- `${C4.yellow}\u26A0${C4.reset} Skill refresh reported a problem \u2014 run '${C4.cyan}zam setup --force${C4.reset}' here manually.`
12983
- );
13528
+ const repoName = await input7({
13529
+ message: "Choose a name for your GitHub repository:",
13530
+ default: "zam-personal"
13531
+ });
13532
+ const isPrivate = await confirm4({
13533
+ message: "Should the repository be private?",
13534
+ default: true
13535
+ });
13536
+ const repoVisibility = isPrivate ? "--private" : "--public";
13537
+ if (hasCommand("gh")) {
13538
+ console.log("GitHub CLI detected! Automating repository creation...");
13539
+ const proceedGh = await confirm4({
13540
+ message: "Would you like ZAM to create the repository using the GitHub CLI?",
13541
+ default: true
13542
+ });
13543
+ if (proceedGh) {
13544
+ try {
13545
+ console.log(`Creating GitHub repository ${repoName}...`);
13546
+ execFileSync4("gh", ghRepoCreateArgs(repoName, repoVisibility), {
13547
+ cwd: workspaceDir,
13548
+ stdio: "inherit"
13549
+ });
13550
+ console.log(
13551
+ "\n\x1B[32m\u2713 Successfully published workspace to GitHub!\x1B[0m"
13552
+ );
13553
+ process.exit(0);
13554
+ } catch (err) {
13555
+ console.warn(
13556
+ `\x1B[33m\u26A0 GitHub CLI creation failed: ${err.message}\x1B[0m`
13557
+ );
13558
+ }
13559
+ }
12984
13560
  }
12985
13561
  console.log(
12986
- `
12987
- ${C4.green}\u2713${C4.reset} Updated to ${C4.cyan}${versionAt(src)}${C4.reset}. Restart your agent client (e.g. Claude Code) to load the refreshed ${C4.cyan}/zam${C4.reset} skill.`
13562
+ "\n\x1B[1mPlease create the repository manually on GitHub:\x1B[0m"
12988
13563
  );
12989
- }
12990
- async function applyUpdate(opts) {
12991
- try {
12992
- const current = currentVersion();
12993
- const latest = await fetchLatestVersion(GITHUB_REPO);
12994
- const channel = getInstallChannel();
12995
- const decision = decideUpdate({
12996
- currentVersion: current,
12997
- latestVersion: latest,
12998
- channel
12999
- });
13000
- if (!decision.updateAvailable) {
13564
+ console.log(" 1. Go to https://github.com/new");
13565
+ console.log(` 2. Name it exactly: \x1B[36m${repoName}\x1B[0m`);
13566
+ console.log(
13567
+ ` 3. Choose \x1B[36m${isPrivate ? "Private" : "Public"}\x1B[0m`
13568
+ );
13569
+ console.log(
13570
+ " 4. Do NOT initialize it with README, .gitignore, or license"
13571
+ );
13572
+ console.log(" 5. Click 'Create repository'\n");
13573
+ const githubUrl = await input7({
13574
+ message: "Paste the repository Git URL (e.g. git@github.com:user/repo.git):"
13575
+ });
13576
+ if (githubUrl) {
13577
+ try {
13578
+ console.log("Linking remote repository and pushing...");
13579
+ let hasOrigin = false;
13580
+ try {
13581
+ runGit2(workspaceDir, ["remote", "get-url", "origin"]);
13582
+ hasOrigin = true;
13583
+ } catch {
13584
+ }
13585
+ runGit2(workspaceDir, gitRemoteArgs(githubUrl, hasOrigin));
13586
+ runGit2(workspaceDir, ["push", "-u", "origin", "main"]);
13001
13587
  console.log(
13002
- `${C4.green}\u2713${C4.reset} ZAM is up to date (${C4.cyan}${current}${C4.reset}).`
13588
+ "\x1B[32m\u2713 Successfully linked and pushed to GitHub!\x1B[0m"
13589
+ );
13590
+ } catch (err) {
13591
+ console.error(
13592
+ `\x1B[31m\u2717 Push failed: ${err.message}\x1B[0m`
13003
13593
  );
13004
- return;
13005
- }
13006
- console.log(
13007
- `${C4.yellow}\u2191${C4.reset} ${C4.bold}Update available${C4.reset}: ${C4.dim}${current}${C4.reset} \u2192 ${C4.cyan}${latest}${C4.reset} ${C4.dim}(${channel} install)${C4.reset}`
13008
- );
13009
- for (const step of planUpdate(decision)) {
13010
- console.log(` ${C4.dim}\u2022${C4.reset} ${step.label}`);
13011
- }
13012
- if (channel === "direct") {
13013
13594
  console.log(
13014
- `
13015
- ${C4.dim}This install updates through the desktop app's signed updater \u2014 open ZAM Desktop to install ${latest}.${C4.reset}`
13595
+ "You can push manually later using: git push -u origin main"
13016
13596
  );
13017
- return;
13018
- }
13019
- if (!opts.yes) {
13020
- const ok = await confirm4({
13021
- message: "Apply this update?",
13022
- default: true
13023
- });
13024
- if (!ok) {
13025
- console.log("Aborted.");
13026
- return;
13027
- }
13028
13597
  }
13029
- console.log();
13030
- if (channel === "winget" || channel === "homebrew") {
13031
- if (!decision.command || !runShell(decision.command)) process.exit(1);
13598
+ }
13599
+ });
13600
+ workspaceCommand.command("data-dir").description("Print the ZAM data directory (database, credentials, config)").option("--json", "Output as JSON").action((opts) => {
13601
+ const dir = join24(homedir13(), ".zam");
13602
+ console.log(opts.json ? JSON.stringify({ dataDir: dir }) : dir);
13603
+ });
13604
+ workspaceCommand.command("backup").description("Back up the local ZAM database into your workspace").option(
13605
+ "--dir <path>",
13606
+ "Target directory (default: workspace dir, else ~/Documents/zam)"
13607
+ ).option("--json", "Output as JSON").action(async (opts) => {
13608
+ const target = getDatabaseTargetInfo();
13609
+ if (target.kind !== "local") {
13610
+ 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.`;
13611
+ if (opts.json) {
13612
+ console.log(JSON.stringify({ ok: false, reason }));
13032
13613
  return;
13033
13614
  }
13034
- applyDeveloperUpdate(opts.force ?? false);
13035
- } catch (err) {
13036
- console.error("Error:", err.message);
13615
+ console.error(`\x1B[33m\u26A0 ${reason}\x1B[0m`);
13037
13616
  process.exit(1);
13038
13617
  }
13039
- }
13040
- var updateCommand = new Command23("update").description(
13041
- "Update ZAM to the latest release (use `update check` to only check)"
13042
- ).option("-y, --yes", "Apply without confirmation").option(
13043
- "--force",
13044
- "Update even if the source checkout has uncommitted changes"
13045
- ).action(async (opts) => {
13046
- await applyUpdate(opts);
13047
- }).addCommand(checkCmd);
13048
-
13049
- // src/cli/commands/whoami.ts
13050
- import { Command as Command24 } from "commander";
13051
- var whoamiCommand = new Command24("whoami").description("Show or set the default user identity").option("--set <id>", "Set the default user ID").option("--clear", "Remove the default user ID").option("--json", "Output as JSON").action(async (opts) => {
13052
- await withDb(async (db) => {
13053
- if (opts.set) {
13054
- await setSetting(db, "user.id", opts.set);
13055
- if (opts.json) {
13056
- console.log(JSON.stringify({ userId: opts.set }));
13057
- } else {
13058
- console.log(`Default user set to: ${opts.set}`);
13059
- }
13060
- return;
13061
- }
13062
- if (opts.clear) {
13063
- const deleted = await deleteSetting(db, "user.id");
13064
- if (opts.json) {
13065
- console.log(JSON.stringify({ userId: null, cleared: deleted }));
13066
- } else if (deleted) {
13067
- console.log("Default user cleared.");
13068
- } else {
13069
- console.log("No default user was set.");
13070
- }
13071
- return;
13072
- }
13073
- const userId = await getSetting(db, "user.id");
13618
+ let db;
13619
+ try {
13620
+ db = await openDatabase();
13621
+ const workspaceDir = opts.dir || (await ensureActiveWorkspace(db)).path;
13622
+ const dest = await backupDatabaseTo(db, workspaceDir);
13074
13623
  if (opts.json) {
13075
- console.log(JSON.stringify({ userId: userId ?? null }));
13076
- return;
13624
+ console.log(JSON.stringify({ ok: true, path: dest }));
13625
+ } else {
13626
+ console.log(`\x1B[32m\u2713 Database backed up to ${dest}\x1B[0m`);
13077
13627
  }
13078
- if (userId) {
13079
- console.log(userId);
13628
+ } catch (err) {
13629
+ const reason = err.message;
13630
+ if (opts.json) {
13631
+ console.log(JSON.stringify({ ok: false, reason }));
13080
13632
  } else {
13081
- console.log("No default user set. Use: zam whoami --set <id>");
13633
+ console.error(`\x1B[31m\u2717 Backup failed: ${reason}\x1B[0m`);
13082
13634
  }
13083
- });
13635
+ process.exit(1);
13636
+ } finally {
13637
+ await db?.close();
13638
+ }
13084
13639
  });
13085
13640
 
13086
13641
  // src/cli/index.ts
13087
13642
  var __dirname = dirname10(fileURLToPath5(import.meta.url));
13088
13643
  var pkg = JSON.parse(
13089
- readFileSync15(join24(__dirname, "..", "..", "package.json"), "utf-8")
13644
+ readFileSync15(join25(__dirname, "..", "..", "package.json"), "utf-8")
13090
13645
  );
13091
13646
  var program = new Command25();
13092
13647
  program.name("zam").description(