zam-core 0.4.4 → 0.5.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/dist/cli/index.js CHANGED
@@ -1,7 +1,7 @@
1
1
  #!/usr/bin/env node
2
2
 
3
3
  // src/cli/index.ts
4
- import { readFileSync as readFileSync14 } from "fs";
4
+ import { readFileSync as readFileSync15 } from "fs";
5
5
  import { dirname as dirname10, join as join24 } from "path";
6
6
  import { fileURLToPath as fileURLToPath5 } from "url";
7
7
  import { Command as Command25 } from "commander";
@@ -4422,8 +4422,10 @@ function t(locale, key, params = {}) {
4422
4422
  import { existsSync as existsSync8, mkdirSync as mkdirSync7, readFileSync as readFileSync8, writeFileSync as writeFileSync5 } from "fs";
4423
4423
  import { homedir as homedir6 } from "os";
4424
4424
  import { dirname as dirname4, join as join9 } from "path";
4425
- var DEFAULT_CONFIG_PATH = join9(homedir6(), ".zam", "config.json");
4426
- function loadInstallConfig(path = DEFAULT_CONFIG_PATH) {
4425
+ function defaultConfigPath() {
4426
+ return process.env.ZAM_CONFIG_PATH || join9(homedir6(), ".zam", "config.json");
4427
+ }
4428
+ function loadInstallConfig(path = defaultConfigPath()) {
4427
4429
  if (!existsSync8(path)) return {};
4428
4430
  try {
4429
4431
  return JSON.parse(readFileSync8(path, "utf-8"));
@@ -4431,25 +4433,50 @@ function loadInstallConfig(path = DEFAULT_CONFIG_PATH) {
4431
4433
  return {};
4432
4434
  }
4433
4435
  }
4434
- function saveInstallConfig(config, path = DEFAULT_CONFIG_PATH) {
4436
+ function saveInstallConfig(config, path = defaultConfigPath()) {
4435
4437
  const dir = dirname4(path);
4436
4438
  if (!existsSync8(dir)) mkdirSync7(dir, { recursive: true });
4437
4439
  writeFileSync5(path, `${JSON.stringify(config, null, 2)}
4438
4440
  `, "utf-8");
4439
4441
  }
4440
- function getInstallMode(path = DEFAULT_CONFIG_PATH) {
4442
+ function getInstallMode(path = defaultConfigPath()) {
4441
4443
  return loadInstallConfig(path).mode ?? "developer";
4442
4444
  }
4443
- function setInstallMode(mode, path = DEFAULT_CONFIG_PATH) {
4445
+ function setInstallMode(mode, path = defaultConfigPath()) {
4444
4446
  const config = loadInstallConfig(path);
4445
4447
  config.mode = mode;
4446
4448
  saveInstallConfig(config, path);
4447
4449
  }
4448
- function getInstallChannel(path = DEFAULT_CONFIG_PATH) {
4450
+ function getInstallChannel(path = defaultConfigPath()) {
4449
4451
  const config = loadInstallConfig(path);
4450
4452
  if (config.channel) return config.channel;
4451
4453
  return (config.mode ?? "developer") === "developer" ? "developer" : "direct";
4452
4454
  }
4455
+ function getMachineAiConfig(path = defaultConfigPath()) {
4456
+ return loadInstallConfig(path).ai ?? {};
4457
+ }
4458
+ function saveMachineAiConfig(ai, path = defaultConfigPath()) {
4459
+ const config = loadInstallConfig(path);
4460
+ config.ai = ai;
4461
+ saveInstallConfig(config, path);
4462
+ }
4463
+ function getConfiguredWorkspaces(path = defaultConfigPath()) {
4464
+ return loadInstallConfig(path).workspaces ?? [];
4465
+ }
4466
+ function saveConfiguredWorkspaces(workspaces, path = defaultConfigPath()) {
4467
+ const config = loadInstallConfig(path);
4468
+ config.workspaces = workspaces;
4469
+ saveInstallConfig(config, path);
4470
+ }
4471
+ function upsertConfiguredWorkspace(workspace, path = defaultConfigPath()) {
4472
+ const current = getConfiguredWorkspaces(path);
4473
+ const next = [
4474
+ ...current.filter((candidate) => candidate.id !== workspace.id),
4475
+ workspace
4476
+ ];
4477
+ saveConfiguredWorkspaces(next, path);
4478
+ return next;
4479
+ }
4453
4480
  function detectSyncProvider(dir) {
4454
4481
  const p = dir.toLowerCase();
4455
4482
  if (p.includes("onedrive")) return "OneDrive";
@@ -5031,7 +5058,7 @@ function isItermRunning() {
5031
5058
  return false;
5032
5059
  }
5033
5060
  }
5034
- function openMacTerminal(shellSetup, label, dir) {
5061
+ function openMacTerminal(shellSetup, label, dir, silent) {
5035
5062
  const useIterm = isItermRunning();
5036
5063
  const escaped = shellSetup.replace(/\\/g, "\\\\").replace(/"/g, '\\"');
5037
5064
  const appleScript = useIterm ? `tell application "iTerm2"
@@ -5048,16 +5075,20 @@ end tell`;
5048
5075
  try {
5049
5076
  writeFileSync6(tmpFile, appleScript);
5050
5077
  execSync4(`osascript ${JSON.stringify(tmpFile)}`, { stdio: "ignore" });
5051
- console.log(
5052
- `Opened ${useIterm ? "iTerm2" : "Terminal.app"} window (${label})`
5053
- );
5054
- console.log(` Directory: ${dir}`);
5078
+ if (!silent) {
5079
+ console.log(
5080
+ `Opened ${useIterm ? "iTerm2" : "Terminal.app"} window (${label})`
5081
+ );
5082
+ console.log(` Directory: ${dir}`);
5083
+ }
5055
5084
  } catch (err) {
5056
- console.error(`Failed to open terminal: ${err.message}`);
5057
- console.log(`
5085
+ if (!silent) {
5086
+ console.error(`Failed to open terminal: ${err.message}`);
5087
+ console.log(`
5058
5088
  Run this manually in a new terminal:
5059
5089
  `);
5060
- console.log(` ${shellSetup}`);
5090
+ console.log(` ${shellSetup}`);
5091
+ }
5061
5092
  } finally {
5062
5093
  try {
5063
5094
  unlinkSync(tmpFile);
@@ -5065,7 +5096,7 @@ Run this manually in a new terminal:
5065
5096
  }
5066
5097
  }
5067
5098
  }
5068
- function openWindowsPowerShell(shellSetup, label, dir, requestedShell) {
5099
+ function openWindowsPowerShell(shellSetup, label, dir, requestedShell, silent) {
5069
5100
  const requestedExecutable = requestedShell === "powershell" ? "powershell.exe" : "pwsh.exe";
5070
5101
  const executable = findExecutable(requestedExecutable) ? requestedExecutable : "powershell.exe";
5071
5102
  const startCommand = [
@@ -5082,35 +5113,42 @@ function openWindowsPowerShell(shellSetup, label, dir, requestedShell) {
5082
5113
  stdio: "ignore"
5083
5114
  }
5084
5115
  );
5085
- console.log(
5086
- `Opened ${executable === "pwsh.exe" ? "PowerShell" : "Windows PowerShell"} window (${label})`
5087
- );
5088
- console.log(` Directory: ${dir}`);
5116
+ if (!silent) {
5117
+ console.log(
5118
+ `Opened ${executable === "pwsh.exe" ? "PowerShell" : "Windows PowerShell"} window (${label})`
5119
+ );
5120
+ console.log(` Directory: ${dir}`);
5121
+ }
5089
5122
  } catch (err) {
5090
- console.error(`Failed to open PowerShell: ${err.message}`);
5091
- console.log(`
5123
+ if (!silent) {
5124
+ console.error(`Failed to open PowerShell: ${err.message}`);
5125
+ console.log(`
5092
5126
  Run this manually in a new PowerShell terminal:
5093
5127
  `);
5094
- console.log(` ${shellSetup}`);
5128
+ console.log(` ${shellSetup}`);
5129
+ }
5095
5130
  }
5096
5131
  }
5097
5132
  function openTerminalWindow(opts) {
5098
- const { shellSetup, label, dir, shell } = opts;
5099
- if (process.platform === "darwin" && !isPowerShellShell(shell)) {
5100
- openMacTerminal(shellSetup, label, dir);
5133
+ const { shellSetup, label, dir, shell, silent = false } = opts;
5134
+ const platform = opts.platform ?? process.platform;
5135
+ if (platform === "darwin" && !isPowerShellShell(shell)) {
5136
+ openMacTerminal(shellSetup, label, dir, silent);
5101
5137
  return;
5102
5138
  }
5103
- if (process.platform === "win32" && isPowerShellShell(shell)) {
5104
- openWindowsPowerShell(shellSetup, label, dir, shell);
5139
+ if (platform === "win32" && isPowerShellShell(shell)) {
5140
+ openWindowsPowerShell(shellSetup, label, dir, shell, silent);
5105
5141
  return;
5106
5142
  }
5107
- console.log(`Run this in a new terminal:
5143
+ if (!silent) {
5144
+ console.log(`Run this in a new terminal:
5108
5145
  `);
5109
- console.log(` ${shellSetup}
5146
+ console.log(` ${shellSetup}
5110
5147
  `);
5111
- console.log(
5112
- `(Automatic terminal opening is only supported on macOS Terminal/iTerm2 and Windows PowerShell for now.)`
5113
- );
5148
+ console.log(
5149
+ `(Automatic terminal opening is only supported on macOS Terminal/iTerm2 and Windows PowerShell for now.)`
5150
+ );
5151
+ }
5114
5152
  }
5115
5153
 
5116
5154
  // src/cli/agent-harness.ts
@@ -5176,7 +5214,9 @@ function launchHarness(harness, opts) {
5176
5214
  shellSetup: plan.shellSetup,
5177
5215
  label: `agent-${harness.id}`,
5178
5216
  dir: opts.workspace,
5179
- shell: plan.shell
5217
+ shell: plan.shell,
5218
+ silent: opts.silent,
5219
+ platform: opts.platform
5180
5220
  });
5181
5221
  return;
5182
5222
  }
@@ -5185,7 +5225,9 @@ function launchHarness(harness, opts) {
5185
5225
  stdio: "ignore",
5186
5226
  windowsHide: true
5187
5227
  }).unref();
5188
- console.log(`Launched ${harness.label} in ${opts.workspace}`);
5228
+ if (!opts.silent) {
5229
+ console.log(`Launched ${harness.label} in ${opts.workspace}`);
5230
+ }
5189
5231
  }
5190
5232
 
5191
5233
  // src/cli/commands/agent.ts
@@ -5310,10 +5352,10 @@ var agentCommand = new Command("agent").description("Provision and inspect the a
5310
5352
  // src/cli/commands/bridge.ts
5311
5353
  import { execFileSync as execFileSync4 } from "child_process";
5312
5354
  import { randomBytes as randomBytes2 } from "crypto";
5313
- import { readdirSync as readdirSync2, readFileSync as readFileSync10, rmSync as rmSync2 } from "fs";
5355
+ import { existsSync as existsSync17, readdirSync as readdirSync2, readFileSync as readFileSync11, rmSync as rmSync2 } from "fs";
5314
5356
  import { homedir as homedir10, tmpdir as tmpdir3 } from "os";
5315
- import { join as join16, resolve as resolve3 } from "path";
5316
- import { Command as Command3 } from "commander";
5357
+ import { basename as basename5, join as join17, resolve as resolve5 } from "path";
5358
+ import { Command as Command4 } from "commander";
5317
5359
 
5318
5360
  // src/cli/llm/client.ts
5319
5361
  import { spawn as spawn2 } from "child_process";
@@ -5379,14 +5421,44 @@ async function getProviderForRole(db, role) {
5379
5421
  const providers = await readJsonSetting(db, "llm.providers");
5380
5422
  const roles = await readJsonSetting(db, "llm.roles");
5381
5423
  const binding = roles?.[role];
5424
+ let resolved = base;
5382
5425
  if (providers && binding?.primary && providers[binding.primary]) {
5383
- const primary = materializeProvider(providers[binding.primary], base, role);
5384
- const fallback = binding.fallback && providers[binding.fallback] ? materializeProvider(providers[binding.fallback], base, role) : void 0;
5426
+ const primary = materializeProvider(
5427
+ providers[binding.primary],
5428
+ base,
5429
+ role,
5430
+ {
5431
+ providerName: binding.primary,
5432
+ source: "shared"
5433
+ }
5434
+ );
5435
+ const fallback = binding.fallback && providers[binding.fallback] ? materializeProvider(providers[binding.fallback], base, role, {
5436
+ providerName: binding.fallback,
5437
+ source: "shared"
5438
+ }) : void 0;
5439
+ resolved = { ...primary, fallback };
5440
+ }
5441
+ const machine = getMachineAiConfig();
5442
+ const machineProviders = machine.providers;
5443
+ const machineBinding = machine.roles?.[role];
5444
+ if (machineProviders && machineBinding?.primary && machineProviders[machineBinding.primary]) {
5445
+ const primary = materializeProvider(
5446
+ machineProviders[machineBinding.primary],
5447
+ resolved,
5448
+ role,
5449
+ { providerName: machineBinding.primary, source: "machine" }
5450
+ );
5451
+ const fallback = machineBinding.fallback && machineProviders[machineBinding.fallback] ? materializeProvider(
5452
+ machineProviders[machineBinding.fallback],
5453
+ resolved,
5454
+ role,
5455
+ { providerName: machineBinding.fallback, source: "machine" }
5456
+ ) : void 0;
5385
5457
  return { ...primary, fallback };
5386
5458
  }
5387
- return base;
5459
+ return resolved;
5388
5460
  }
5389
- function materializeProvider(rec, base, role) {
5461
+ function materializeProvider(rec, base, role, meta) {
5390
5462
  const url = rec.url || base.url;
5391
5463
  return {
5392
5464
  enabled: base.enabled,
@@ -5395,6 +5467,10 @@ function materializeProvider(rec, base, role) {
5395
5467
  apiKey: resolveProviderApiKey(rec),
5396
5468
  apiFlavor: rec.apiFlavor || inferApiFlavor(url),
5397
5469
  locale: base.locale,
5470
+ providerName: meta.providerName,
5471
+ label: rec.label,
5472
+ source: meta.source,
5473
+ local: rec.local ?? isLocalEndpoint(url),
5398
5474
  ...role === "vision" ? { maxFrames: base.maxFrames } : {}
5399
5475
  };
5400
5476
  }
@@ -5413,6 +5489,8 @@ async function getLegacyRoleConfig(db, role, enabled) {
5413
5489
  apiKey: await getSetting(db, "llm.vision.api_key") || base.apiKey,
5414
5490
  apiFlavor: inferApiFlavor(url),
5415
5491
  locale: base.locale,
5492
+ source: "legacy",
5493
+ local: isLocalEndpoint(url),
5416
5494
  maxFrames: Number.isNaN(parsed) ? 100 : parsed
5417
5495
  };
5418
5496
  }
@@ -5422,7 +5500,9 @@ async function getLegacyRoleConfig(db, role, enabled) {
5422
5500
  model: base.model,
5423
5501
  apiKey: base.apiKey,
5424
5502
  apiFlavor: inferApiFlavor(base.url),
5425
- locale: base.locale
5503
+ locale: base.locale,
5504
+ source: "legacy",
5505
+ local: isLocalEndpoint(base.url)
5426
5506
  };
5427
5507
  }
5428
5508
  function assertChatCompletions(cfg) {
@@ -5672,10 +5752,17 @@ async function isVisionProviderModelExplicit(db, active) {
5672
5752
  const providers = await readJsonSetting(db, "llm.providers");
5673
5753
  const roles = await readJsonSetting(db, "llm.roles");
5674
5754
  const binding = roles?.vision;
5675
- if (!providers || !binding) return false;
5676
- return [binding.primary, binding.fallback].some((id) => {
5755
+ const sharedExplicit = [binding?.primary, binding?.fallback].some((id) => {
5756
+ if (!id) return false;
5757
+ const rec = providers?.[id];
5758
+ return rec?.model === active.model && (rec.url === void 0 || rec.url === active.url);
5759
+ });
5760
+ if (sharedExplicit) return true;
5761
+ const machine = getMachineAiConfig();
5762
+ const machineBinding = machine.roles?.vision;
5763
+ return [machineBinding?.primary, machineBinding?.fallback].some((id) => {
5677
5764
  if (!id) return false;
5678
- const rec = providers[id];
5765
+ const rec = machine.providers?.[id];
5679
5766
  return rec?.model === active.model && (rec.url === void 0 || rec.url === active.url);
5680
5767
  });
5681
5768
  }
@@ -5708,6 +5795,87 @@ async function checkVisionReadiness(db) {
5708
5795
  warning
5709
5796
  };
5710
5797
  }
5798
+ function summarizeFallback(provider) {
5799
+ if (!provider) return void 0;
5800
+ return {
5801
+ providerName: provider.providerName,
5802
+ label: provider.label,
5803
+ source: provider.source,
5804
+ url: provider.url,
5805
+ model: provider.model,
5806
+ apiFlavor: provider.apiFlavor,
5807
+ local: provider.local
5808
+ };
5809
+ }
5810
+ async function getProviderRoleStatus(db, role) {
5811
+ const cfg = await getProviderForRole(db, role);
5812
+ const unsupportedProvider = role !== "vision" && cfg.apiFlavor !== "chat-completions";
5813
+ if (!cfg.enabled) {
5814
+ return {
5815
+ role,
5816
+ enabled: false,
5817
+ providerName: cfg.providerName,
5818
+ label: cfg.label,
5819
+ source: cfg.source,
5820
+ url: cfg.url,
5821
+ model: cfg.model,
5822
+ apiFlavor: cfg.apiFlavor,
5823
+ local: cfg.local,
5824
+ online: false,
5825
+ modelAvailable: false,
5826
+ availableModels: [],
5827
+ usable: false,
5828
+ reason: "disabled",
5829
+ fallback: summarizeFallback(cfg.fallback)
5830
+ };
5831
+ }
5832
+ if (unsupportedProvider) {
5833
+ return {
5834
+ role,
5835
+ enabled: true,
5836
+ providerName: cfg.providerName,
5837
+ label: cfg.label,
5838
+ source: cfg.source,
5839
+ url: cfg.url,
5840
+ model: cfg.model,
5841
+ apiFlavor: cfg.apiFlavor,
5842
+ local: cfg.local,
5843
+ online: false,
5844
+ modelAvailable: false,
5845
+ availableModels: [],
5846
+ usable: false,
5847
+ reason: "unsupported-provider",
5848
+ fallback: summarizeFallback(cfg.fallback)
5849
+ };
5850
+ }
5851
+ let selected;
5852
+ if (role === "vision") {
5853
+ const chain = await checkProviderChain(cfg);
5854
+ selected = chain.firstUsable ?? chain.primary;
5855
+ } else {
5856
+ selected = await checkProviderEndpoint(cfg);
5857
+ }
5858
+ const active = selected.endpoint;
5859
+ const usable = selected.online && selected.modelAvailable;
5860
+ const reason = usable ? void 0 : selected.online ? "model-not-found" : "offline";
5861
+ return {
5862
+ role,
5863
+ enabled: true,
5864
+ providerName: active.providerName,
5865
+ label: active.label,
5866
+ source: active.source,
5867
+ url: active.url,
5868
+ model: active.model,
5869
+ apiFlavor: active.apiFlavor,
5870
+ local: active.local,
5871
+ online: selected.online,
5872
+ modelAvailable: selected.modelAvailable,
5873
+ availableModels: selected.availableModels,
5874
+ usable,
5875
+ reason,
5876
+ fallback: summarizeFallback(cfg.fallback)
5877
+ };
5878
+ }
5711
5879
  function detectRunner(url, model) {
5712
5880
  let runner = "unknown";
5713
5881
  let port = "8000";
@@ -5806,7 +5974,7 @@ async function startLocalRunner(url, model, locale) {
5806
5974
  let attempts = 0;
5807
5975
  const dotsPerLine = 30;
5808
5976
  while (true) {
5809
- await new Promise((resolve6) => setTimeout(resolve6, 500));
5977
+ await new Promise((resolve8) => setTimeout(resolve8, 500));
5810
5978
  if (await isLlmOnline(url)) {
5811
5979
  if (attempts > 0) process.stdout.write("\n");
5812
5980
  return true;
@@ -5961,8 +6129,8 @@ async function fetchWithInteractiveTimeout(url, options = {}) {
5961
6129
  const dotsPerLine = 30;
5962
6130
  while (true) {
5963
6131
  let timeoutId;
5964
- const timeoutPromise = new Promise((resolve6) => {
5965
- timeoutId = setTimeout(() => resolve6("timeout"), timeoutMs);
6132
+ const timeoutPromise = new Promise((resolve8) => {
6133
+ timeoutId = setTimeout(() => resolve8("timeout"), timeoutMs);
5966
6134
  });
5967
6135
  const dotsInterval = setInterval(() => {
5968
6136
  process.stdout.write(".");
@@ -6489,11 +6657,338 @@ function jsonOut(data) {
6489
6657
 
6490
6658
  // src/cli/commands/workspace.ts
6491
6659
  import { execFileSync as execFileSync3 } from "child_process";
6492
- import { existsSync as existsSync15, mkdirSync as mkdirSync8, writeFileSync as writeFileSync7 } from "fs";
6660
+ import { existsSync as existsSync16, mkdirSync as mkdirSync9, writeFileSync as writeFileSync8 } from "fs";
6493
6661
  import { homedir as homedir9 } from "os";
6494
- import { join as join15 } from "path";
6662
+ import { join as join16, resolve as resolve4 } from "path";
6495
6663
  import { confirm, input } from "@inquirer/prompts";
6664
+ import { Command as Command3 } from "commander";
6665
+
6666
+ // src/cli/commands/setup.ts
6667
+ import {
6668
+ copyFileSync as copyFileSync2,
6669
+ existsSync as existsSync15,
6670
+ mkdirSync as mkdirSync8,
6671
+ readFileSync as readFileSync10,
6672
+ writeFileSync as writeFileSync7
6673
+ } from "fs";
6674
+ import { basename as basename4, dirname as dirname5, join as join15, resolve as resolve3 } from "path";
6675
+ import { fileURLToPath as fileURLToPath2 } from "url";
6496
6676
  import { Command as Command2 } from "commander";
6677
+ var packageRoot = [
6678
+ fileURLToPath2(new URL("../..", import.meta.url)),
6679
+ fileURLToPath2(new URL("../../..", import.meta.url))
6680
+ ].find((candidate) => existsSync15(join15(candidate, "package.json"))) ?? fileURLToPath2(new URL("../..", import.meta.url));
6681
+ var ALL_SETUP_AGENTS = ["claude", "copilot", "codex", "agent"];
6682
+ var SKILL_PAIRS = [
6683
+ {
6684
+ from: join15(packageRoot, ".claude", "skills", "zam", "SKILL.md"),
6685
+ to: join15(".claude", "skills", "zam", "SKILL.md"),
6686
+ agents: ["claude"]
6687
+ },
6688
+ {
6689
+ from: join15(packageRoot, ".agent", "skills", "zam", "SKILL.md"),
6690
+ to: join15(".agent", "skills", "zam", "SKILL.md"),
6691
+ agents: ["agent"]
6692
+ },
6693
+ {
6694
+ from: join15(packageRoot, ".agents", "skills", "zam", "SKILL.md"),
6695
+ to: join15(".agents", "skills", "zam", "SKILL.md"),
6696
+ agents: ["copilot", "codex"]
6697
+ }
6698
+ ];
6699
+ function parseSetupAgents(value) {
6700
+ if (!value || value.trim().toLowerCase() === "all") {
6701
+ return new Set(ALL_SETUP_AGENTS);
6702
+ }
6703
+ const aliases = {
6704
+ claude: ["claude"],
6705
+ copilot: ["copilot"],
6706
+ codex: ["codex"],
6707
+ agent: ["agent"],
6708
+ opencode: ["agent"]
6709
+ };
6710
+ const selected = /* @__PURE__ */ new Set();
6711
+ for (const raw of value.split(",")) {
6712
+ const key = raw.trim().toLowerCase();
6713
+ const mapped = aliases[key];
6714
+ if (!mapped) {
6715
+ throw new Error(
6716
+ `Unknown agent "${raw}". Use: all, claude, copilot, codex, agent.`
6717
+ );
6718
+ }
6719
+ for (const item of mapped) selected.add(item);
6720
+ }
6721
+ return selected;
6722
+ }
6723
+ function copySkills(force, cwd = process.cwd(), agents = parseSetupAgents(), dryRun = false) {
6724
+ let anyAction = false;
6725
+ for (const { from, to, agents: pairAgents } of SKILL_PAIRS) {
6726
+ if (!pairAgents.some((agent) => agents.has(agent))) continue;
6727
+ const dest = join15(cwd, to);
6728
+ if (!existsSync15(from)) {
6729
+ console.warn(` warn source not found, skipping: ${from}`);
6730
+ continue;
6731
+ }
6732
+ if (existsSync15(dest) && !force) {
6733
+ console.log(` skip ${to} (already present \xE2\u20AC\u201D use --force to update)`);
6734
+ continue;
6735
+ }
6736
+ if (dryRun) {
6737
+ console.log(` would copy ${to}`);
6738
+ anyAction = true;
6739
+ continue;
6740
+ }
6741
+ mkdirSync8(dirname5(dest), { recursive: true });
6742
+ copyFileSync2(from, dest);
6743
+ console.log(` copy ${to}`);
6744
+ anyAction = true;
6745
+ }
6746
+ if (!anyAction && !force) {
6747
+ console.log(
6748
+ "\nSkill files are already up to date. Run with --force to overwrite."
6749
+ );
6750
+ }
6751
+ }
6752
+ function formatDatabaseInitTarget(target) {
6753
+ switch (target.kind) {
6754
+ case "local":
6755
+ return `ZAM database at ${target.location} (local SQLite)`;
6756
+ case "turso-remote":
6757
+ return `ZAM database via Turso remote at ${target.location}`;
6758
+ case "turso-native":
6759
+ return `ZAM database via Turso native driver at ${target.location}`;
6760
+ case "turso-replica":
6761
+ return `ZAM database replica at ${target.location} syncing from ${target.syncUrl}`;
6762
+ }
6763
+ }
6764
+ async function initDatabase(skipInit) {
6765
+ if (skipInit) return;
6766
+ try {
6767
+ const target = getDatabaseTargetInfo();
6768
+ const db = await openDatabaseWithSync({ initialize: true });
6769
+ await db.close();
6770
+ console.log(` init ${formatDatabaseInitTarget(target)}`);
6771
+ } catch (err) {
6772
+ const msg = err.message;
6773
+ if (!msg.includes("already")) {
6774
+ console.warn(` warn database init: ${msg}`);
6775
+ } else {
6776
+ console.log(` skip database already initialized`);
6777
+ }
6778
+ }
6779
+ }
6780
+ var ZAM_BLOCK_START = "<!-- ZAM:START -->";
6781
+ var ZAM_BLOCK_END = "<!-- ZAM:END -->";
6782
+ function upsertMarkedBlock(dest, blockBody, dryRun) {
6783
+ const block = `${ZAM_BLOCK_START}
6784
+ ${blockBody.trim()}
6785
+ ${ZAM_BLOCK_END}`;
6786
+ if (!existsSync15(dest)) {
6787
+ if (!dryRun) {
6788
+ mkdirSync8(dirname5(dest), { recursive: true });
6789
+ writeFileSync7(dest, `${block}
6790
+ `, "utf8");
6791
+ }
6792
+ return "write";
6793
+ }
6794
+ const existing = readFileSync10(dest, "utf8");
6795
+ if (existing.includes(block)) return "skip";
6796
+ const start = existing.indexOf(ZAM_BLOCK_START);
6797
+ const end = existing.indexOf(ZAM_BLOCK_END);
6798
+ const next = start >= 0 && end > start ? `${existing.slice(0, start)}${block}${existing.slice(end + ZAM_BLOCK_END.length)}` : `${existing.trimEnd()}
6799
+
6800
+ ${block}
6801
+ `;
6802
+ if (!dryRun) writeFileSync7(dest, next, "utf8");
6803
+ return start >= 0 && end > start ? "update" : "write";
6804
+ }
6805
+ function logInstructionAction(action, label, dryRun) {
6806
+ if (action === "skip") {
6807
+ console.log(` skip ${label} (ZAM block already present)`);
6808
+ } else {
6809
+ console.log(` ${dryRun ? "would " : ""}${action} ${label}`);
6810
+ }
6811
+ }
6812
+ function writeClaudeMd(skipClaudeMd, cwd = process.cwd(), opts = {}) {
6813
+ if (skipClaudeMd) return;
6814
+ const dest = join15(cwd, "CLAUDE.md");
6815
+ if (existsSync15(dest)) {
6816
+ if (!opts.updateExisting) {
6817
+ console.log(` skip CLAUDE.md (already present)`);
6818
+ return;
6819
+ }
6820
+ const action = upsertMarkedBlock(
6821
+ dest,
6822
+ `## ZAM learning sessions
6823
+
6824
+ ZAM is available in this repository. Use the \`zam\` skill in Claude Code to turn real work into an observed learning session with active recall and FSRS scheduling.
6825
+
6826
+ - Skill files live under \`.claude/skills/zam/\`.
6827
+ - Fast-changing review data lives in \`~/.zam/\`, not in this repository.
6828
+ - Run \`zam setup --target . --agents claude --force\` after upgrading ZAM to refresh the skill.`,
6829
+ Boolean(opts.dryRun)
6830
+ );
6831
+ logInstructionAction(action, "CLAUDE.md", Boolean(opts.dryRun));
6832
+ return;
6833
+ }
6834
+ const name = basename4(cwd);
6835
+ const content = `# ZAM Personal Kernel \xE2\u20AC\u201D ${name}
6836
+
6837
+ This is a ZAM personal instance. ZAM builds lasting skills through spaced
6838
+ repetition during real work \xE2\u20AC\u201D not separate study sessions.
6839
+
6840
+ ## First time here?
6841
+ Run \`/setup\` in Claude Code or Gemini CLI to complete first-time setup.
6842
+
6843
+ ## Regular use
6844
+ Run \`/zam\` to start a learning session on whatever you are working on.
6845
+
6846
+ ## What lives here
6847
+ - \`beliefs/\` \xE2\u20AC\u201D your worldview, approved by git commit
6848
+ - \`goals/\` \xE2\u20AC\u201D your objectives, decomposed into tasks and learning tokens
6849
+
6850
+ ## Fast-changing data
6851
+ Learning tokens, cards, and review history live in local SQLite by default.
6852
+ Use \`zam connector setup turso\` to store cloud credentials in
6853
+ \`~/.zam/credentials.json\` and use a Turso database across machines.
6854
+ `;
6855
+ if (opts.dryRun) {
6856
+ console.log(` would write CLAUDE.md`);
6857
+ } else {
6858
+ writeFileSync7(dest, content, "utf8");
6859
+ console.log(` write CLAUDE.md`);
6860
+ }
6861
+ }
6862
+ function writeAgentsMd(skipAgentsMd, cwd = process.cwd(), opts = {}) {
6863
+ if (skipAgentsMd) return;
6864
+ const dest = join15(cwd, "AGENTS.md");
6865
+ if (existsSync15(dest)) {
6866
+ if (!opts.updateExisting) {
6867
+ console.log(` skip AGENTS.md (already present)`);
6868
+ return;
6869
+ }
6870
+ const action = upsertMarkedBlock(
6871
+ dest,
6872
+ `## ZAM learning sessions
6873
+
6874
+ ZAM is available in this repository. Select the \`zam\` skill through \`/skills\` or invoke \`$zam\` where supported to turn real work into an observed learning session with active recall and FSRS scheduling.
6875
+
6876
+ - Skill files live under \`.agents/skills/zam/\`.
6877
+ - Fast-changing review data lives in \`~/.zam/\`, not in this repository.
6878
+ - Run \`zam setup --target . --agents copilot,codex --force\` after upgrading ZAM to refresh the skill.`,
6879
+ Boolean(opts.dryRun)
6880
+ );
6881
+ logInstructionAction(action, "AGENTS.md", Boolean(opts.dryRun));
6882
+ return;
6883
+ }
6884
+ const name = basename4(cwd);
6885
+ const content = `# ZAM Personal Kernel - ${name}
6886
+
6887
+ This is a ZAM personal instance. ZAM builds lasting skills through spaced
6888
+ repetition during real work, not separate study sessions.
6889
+
6890
+ ## First time here?
6891
+ Run \`zam setup\` from the shell. When this repository includes
6892
+ \`.agents/skills/setup/\`, you can instead select \`setup\` through \`/skills\`
6893
+ or invoke \`$setup\`.
6894
+
6895
+ ## Regular use
6896
+ Select the \`zam\` skill through \`/skills\` or invoke \`$zam\` to start a
6897
+ learning session on whatever you are working on.
6898
+
6899
+ ## What lives here
6900
+ - \`beliefs/\` - your worldview, approved by git commit
6901
+ - \`goals/\` - your objectives, decomposed into tasks and learning tokens
6902
+
6903
+ ## Fast-changing data
6904
+ Learning tokens, cards, and review history live in local SQLite by default.
6905
+ Use \`zam connector setup turso\` to store cloud credentials in
6906
+ \`~/.zam/credentials.json\` and use a Turso database across machines.
6907
+
6908
+ ## Codex skills
6909
+ Codex discovers repository skills under \`.agents/skills/\`. Run
6910
+ \`zam setup --force\` after upgrading \`zam-core\` to refresh them.
6911
+ `;
6912
+ if (opts.dryRun) {
6913
+ console.log(` would write AGENTS.md`);
6914
+ } else {
6915
+ writeFileSync7(dest, content, "utf8");
6916
+ console.log(` write AGENTS.md`);
6917
+ }
6918
+ }
6919
+ function writeCopilotInstructions(cwd = process.cwd(), opts = {}) {
6920
+ const dest = join15(cwd, ".github", "copilot-instructions.md");
6921
+ const action = upsertMarkedBlock(
6922
+ dest,
6923
+ `## ZAM learning sessions
6924
+
6925
+ ZAM is available in this repository through \`.agents/skills/zam/\`. Use the \`zam\` skill from Copilot-compatible skill selection surfaces to turn real work into an observed learning session with active recall and FSRS scheduling.
6926
+
6927
+ - Fast-changing review data lives in \`~/.zam/\`, not in this repository.
6928
+ - Run \`zam setup --target . --agents copilot --force\` after upgrading ZAM to refresh the skill.`,
6929
+ Boolean(opts.dryRun)
6930
+ );
6931
+ logInstructionAction(
6932
+ action,
6933
+ ".github/copilot-instructions.md",
6934
+ Boolean(opts.dryRun)
6935
+ );
6936
+ }
6937
+ var setupCommand = new Command2("setup").description(
6938
+ "Distribute ZAM skill files into this personal instance and initialize the database"
6939
+ ).option(
6940
+ "--force",
6941
+ "overwrite existing skill files (use after upgrading zam)",
6942
+ false
6943
+ ).option("--skip-init", "skip database initialization", false).option("--skip-claude-md", "skip CLAUDE.md generation", false).option("--skip-agents-md", "skip AGENTS.md generation", false).option("--target <path>", "repository/workspace directory to set up").option(
6944
+ "--agents <list>",
6945
+ "comma-separated agents to wire: all, claude, copilot, codex, agent"
6946
+ ).option(
6947
+ "--dry-run",
6948
+ "show what would be written without changing files",
6949
+ false
6950
+ ).action(
6951
+ async (opts) => {
6952
+ let agents;
6953
+ try {
6954
+ agents = parseSetupAgents(opts.agents);
6955
+ } catch (err) {
6956
+ console.error(`Error: ${err.message}`);
6957
+ process.exit(1);
6958
+ }
6959
+ const target = resolve3(opts.target ?? process.cwd());
6960
+ const updateExistingInstructions = Boolean(opts.target) || opts.force;
6961
+ console.log(
6962
+ `Setting up ZAM in ${target}${opts.dryRun ? " (dry run)" : ""}
6963
+ `
6964
+ );
6965
+ copySkills(opts.force, target, agents, opts.dryRun);
6966
+ await initDatabase(opts.skipInit || opts.dryRun);
6967
+ if (agents.has("claude")) {
6968
+ writeClaudeMd(opts.skipClaudeMd, target, {
6969
+ dryRun: opts.dryRun,
6970
+ updateExisting: updateExistingInstructions
6971
+ });
6972
+ }
6973
+ if (agents.has("copilot") || agents.has("codex") || agents.has("agent")) {
6974
+ writeAgentsMd(opts.skipAgentsMd, target, {
6975
+ dryRun: opts.dryRun,
6976
+ updateExisting: updateExistingInstructions
6977
+ });
6978
+ }
6979
+ if (agents.has("copilot") && (opts.target || opts.agents)) {
6980
+ writeCopilotInstructions(target, {
6981
+ dryRun: opts.dryRun,
6982
+ updateExisting: updateExistingInstructions
6983
+ });
6984
+ }
6985
+ console.log(
6986
+ "\nDone. Run `zam whoami --set <your-id>` to set your identity. Start the `zam` skill with `/zam` in Claude/Gemini-compatible clients or `$zam` (or `/skills`) in Codex."
6987
+ );
6988
+ }
6989
+ );
6990
+
6991
+ // src/cli/commands/workspace.ts
6497
6992
  function runGit(cwd, args) {
6498
6993
  try {
6499
6994
  return execFileSync3("git", args, {
@@ -6502,18 +6997,169 @@ function runGit(cwd, args) {
6502
6997
  encoding: "utf8"
6503
6998
  }).trim();
6504
6999
  } catch (err) {
6505
- throw new Error(`Git command failed: ${err.message}`);
7000
+ throw new Error(`Git command failed: ${err.message}`);
7001
+ }
7002
+ }
7003
+ function ghRepoCreateArgs(repoName, repoVisibility) {
7004
+ return ["repo", "create", repoName, repoVisibility, "--source=.", "--push"];
7005
+ }
7006
+ function gitRemoteArgs(githubUrl, hasOrigin) {
7007
+ return hasOrigin ? ["remote", "set-url", "origin", githubUrl] : ["remote", "add", "origin", githubUrl];
7008
+ }
7009
+ var workspaceCommand = new Command3("workspace").description(
7010
+ "Manage your ZAM learning workspace"
7011
+ );
7012
+ var WORKSPACE_KINDS = [
7013
+ "personal",
7014
+ "team",
7015
+ "family",
7016
+ "community",
7017
+ "organization",
7018
+ "custom"
7019
+ ];
7020
+ var WORKSPACE_SOURCE_CONTROLS = [
7021
+ "github",
7022
+ "azure-devops",
7023
+ "git",
7024
+ "none"
7025
+ ];
7026
+ function parseWorkspaceKind(value) {
7027
+ const kind = (value ?? "custom").toLowerCase();
7028
+ if (WORKSPACE_KINDS.includes(kind)) {
7029
+ return kind;
7030
+ }
7031
+ throw new Error(
7032
+ `Invalid workspace kind: ${value}. Use ${WORKSPACE_KINDS.join(", ")}.`
7033
+ );
7034
+ }
7035
+ function parseWorkspaceSourceControl(value) {
7036
+ if (!value) return void 0;
7037
+ const source = value.toLowerCase();
7038
+ if (WORKSPACE_SOURCE_CONTROLS.includes(source)) {
7039
+ return source;
7040
+ }
7041
+ throw new Error(
7042
+ `Invalid source control: ${value}. Use ${WORKSPACE_SOURCE_CONTROLS.join(", ")}.`
7043
+ );
7044
+ }
7045
+ function parseScopes(value) {
7046
+ if (!value) return void 0;
7047
+ const scopes = value.split(",").map((item) => item.trim()).filter(Boolean);
7048
+ return scopes.length > 0 ? scopes : void 0;
7049
+ }
7050
+ function requireWorkspace(id) {
7051
+ const workspace = getConfiguredWorkspaces().find((item) => item.id === id);
7052
+ if (!workspace) {
7053
+ throw new Error(
7054
+ `Workspace "${id}" is not configured. Add it with: zam workspace add ${id} --path <dir>`
7055
+ );
7056
+ }
7057
+ return workspace;
7058
+ }
7059
+ workspaceCommand.command("list").description("List configured ZAM workspaces").option("--json", "Output as JSON").action((opts) => {
7060
+ const workspaces = getConfiguredWorkspaces();
7061
+ if (opts.json) {
7062
+ console.log(JSON.stringify({ workspaces }, null, 2));
7063
+ return;
7064
+ }
7065
+ console.log("Configured ZAM workspaces:\n");
7066
+ if (workspaces.length === 0) {
7067
+ console.log(
7068
+ " (none) \u2014 add one: zam workspace add personal --path <dir>"
7069
+ );
7070
+ return;
7071
+ }
7072
+ for (const workspace of workspaces) {
7073
+ const label = workspace.label ? ` (${workspace.label})` : "";
7074
+ console.log(` ${workspace.id}${label}`);
7075
+ console.log(` kind: ${workspace.kind}`);
7076
+ console.log(` path: ${workspace.path}`);
7077
+ if (workspace.sourceControl) {
7078
+ console.log(` source: ${workspace.sourceControl}`);
7079
+ }
7080
+ if (workspace.knowledgeScopes?.length) {
7081
+ console.log(` scopes: ${workspace.knowledgeScopes.join(", ")}`);
7082
+ }
7083
+ }
7084
+ });
7085
+ workspaceCommand.command("add <id>").description("Register an existing directory as a ZAM workspace").requiredOption("--path <dir>", "Existing workspace/repository directory").option(
7086
+ "--kind <kind>",
7087
+ `Workspace kind (${WORKSPACE_KINDS.join(" | ")})`,
7088
+ "custom"
7089
+ ).option("--label <label>", "Human-readable label").option(
7090
+ "--source-control <provider>",
7091
+ `Source-control provider (${WORKSPACE_SOURCE_CONTROLS.join(" | ")})`
7092
+ ).option("--scopes <list>", "Comma-separated knowledge scopes").option("--default-agent <id>", "Default agent harness for this workspace").action((id, opts) => {
7093
+ try {
7094
+ const path = resolve4(String(opts.path));
7095
+ if (!existsSync16(path)) {
7096
+ console.error(`Workspace path does not exist: ${path}`);
7097
+ process.exit(1);
7098
+ }
7099
+ const workspace = {
7100
+ id,
7101
+ kind: parseWorkspaceKind(opts.kind),
7102
+ path,
7103
+ ...opts.label ? { label: opts.label } : {},
7104
+ ...opts.sourceControl ? { sourceControl: parseWorkspaceSourceControl(opts.sourceControl) } : {},
7105
+ ...parseScopes(opts.scopes) ? { knowledgeScopes: parseScopes(opts.scopes) } : {},
7106
+ ...opts.defaultAgent ? { defaultAgent: opts.defaultAgent } : {}
7107
+ };
7108
+ upsertConfiguredWorkspace(workspace);
7109
+ console.log(`Registered workspace "${id}" at ${path}.`);
7110
+ } catch (err) {
7111
+ console.error(`Error: ${err.message}`);
7112
+ process.exit(1);
7113
+ }
7114
+ });
7115
+ workspaceCommand.command("setup <id>").description("Install ZAM skills into a configured workspace").option(
7116
+ "--agents <list>",
7117
+ "comma-separated agents to wire: all, claude, copilot, codex, agent"
7118
+ ).option(
7119
+ "--force",
7120
+ "overwrite existing skill files and refresh ZAM blocks",
7121
+ false
7122
+ ).option(
7123
+ "--dry-run",
7124
+ "show what would be written without changing files",
7125
+ false
7126
+ ).action((id, opts) => {
7127
+ try {
7128
+ const workspace = requireWorkspace(id);
7129
+ const agents = parseSetupAgents(opts.agents);
7130
+ console.log(
7131
+ `Setting up workspace "${workspace.id}" in ${workspace.path}${opts.dryRun ? " (dry run)" : ""}
7132
+ `
7133
+ );
7134
+ copySkills(
7135
+ Boolean(opts.force),
7136
+ workspace.path,
7137
+ agents,
7138
+ Boolean(opts.dryRun)
7139
+ );
7140
+ if (agents.has("claude")) {
7141
+ writeClaudeMd(false, workspace.path, {
7142
+ dryRun: Boolean(opts.dryRun),
7143
+ updateExisting: true
7144
+ });
7145
+ }
7146
+ if (agents.has("copilot") || agents.has("codex") || agents.has("agent")) {
7147
+ writeAgentsMd(false, workspace.path, {
7148
+ dryRun: Boolean(opts.dryRun),
7149
+ updateExisting: true
7150
+ });
7151
+ }
7152
+ if (agents.has("copilot")) {
7153
+ writeCopilotInstructions(workspace.path, {
7154
+ dryRun: Boolean(opts.dryRun),
7155
+ updateExisting: true
7156
+ });
7157
+ }
7158
+ } catch (err) {
7159
+ console.error(`Error: ${err.message}`);
7160
+ process.exit(1);
6506
7161
  }
6507
- }
6508
- function ghRepoCreateArgs(repoName, repoVisibility) {
6509
- return ["repo", "create", repoName, repoVisibility, "--source=.", "--push"];
6510
- }
6511
- function gitRemoteArgs(githubUrl, hasOrigin) {
6512
- return hasOrigin ? ["remote", "set-url", "origin", githubUrl] : ["remote", "add", "origin", githubUrl];
6513
- }
6514
- var workspaceCommand = new Command2("workspace").description(
6515
- "Manage your ZAM learning workspace"
6516
- );
7162
+ });
6517
7163
  workspaceCommand.command("publish").description("Publish your local workspace sandbox to GitHub").action(async () => {
6518
7164
  let db;
6519
7165
  let workspaceDir = "";
@@ -6530,7 +7176,7 @@ workspaceCommand.command("publish").description("Publish your local workspace sa
6530
7176
  );
6531
7177
  process.exit(1);
6532
7178
  }
6533
- if (!existsSync15(workspaceDir)) {
7179
+ if (!existsSync16(workspaceDir)) {
6534
7180
  console.error(
6535
7181
  `\x1B[31m\u2717 Workspace directory does not exist: ${workspaceDir}\x1B[0m`
6536
7182
  );
@@ -6544,15 +7190,15 @@ Active workspace: \x1B[36m${workspaceDir}\x1B[0m`);
6544
7190
  );
6545
7191
  process.exit(1);
6546
7192
  }
6547
- const gitignorePath = join15(workspaceDir, ".gitignore");
6548
- if (!existsSync15(gitignorePath)) {
6549
- writeFileSync7(
7193
+ const gitignorePath = join16(workspaceDir, ".gitignore");
7194
+ if (!existsSync16(gitignorePath)) {
7195
+ writeFileSync8(
6550
7196
  gitignorePath,
6551
7197
  "node_modules/\n.agent/\n.agents/\n.claude/\n.gemini/\n.goose/\n*.log\n",
6552
7198
  "utf8"
6553
7199
  );
6554
7200
  }
6555
- const hasGitRepo = existsSync15(join15(workspaceDir, ".git"));
7201
+ const hasGitRepo = existsSync16(join16(workspaceDir, ".git"));
6556
7202
  if (!hasGitRepo) {
6557
7203
  console.log("Initializing local Git repository...");
6558
7204
  runGit(workspaceDir, ["init", "-b", "main"]);
@@ -6639,15 +7285,15 @@ Active workspace: \x1B[36m${workspaceDir}\x1B[0m`);
6639
7285
  }
6640
7286
  });
6641
7287
  async function backupDatabaseTo(db, targetDir) {
6642
- const backupDir = join15(targetDir, "zam-backups");
6643
- mkdirSync8(backupDir, { recursive: true });
7288
+ const backupDir = join16(targetDir, "zam-backups");
7289
+ mkdirSync9(backupDir, { recursive: true });
6644
7290
  const stamp = (/* @__PURE__ */ new Date()).toISOString().replace(/[:.]/g, "-");
6645
- const dest = join15(backupDir, `zam-${stamp}.db`);
7291
+ const dest = join16(backupDir, `zam-${stamp}.db`);
6646
7292
  await db.exec(`VACUUM INTO '${dest.replace(/'/g, "''")}'`);
6647
7293
  return dest;
6648
7294
  }
6649
7295
  workspaceCommand.command("data-dir").description("Print the ZAM data directory (database, credentials, config)").option("--json", "Output as JSON").action((opts) => {
6650
- const dir = join15(homedir9(), ".zam");
7296
+ const dir = join16(homedir9(), ".zam");
6651
7297
  console.log(opts.json ? JSON.stringify({ dataDir: dir }) : dir);
6652
7298
  });
6653
7299
  workspaceCommand.command("backup").description("Back up the local ZAM database into your workspace").option(
@@ -6667,7 +7313,7 @@ workspaceCommand.command("backup").description("Back up the local ZAM database i
6667
7313
  let db;
6668
7314
  try {
6669
7315
  db = await openDatabase();
6670
- const workspaceDir = opts.dir || await getSetting(db, "personal.workspace_dir") || join15(homedir9(), "Documents", "zam");
7316
+ const workspaceDir = opts.dir || await getSetting(db, "personal.workspace_dir") || join16(homedir9(), "Documents", "zam");
6671
7317
  const dest = await backupDatabaseTo(db, workspaceDir);
6672
7318
  if (opts.json) {
6673
7319
  console.log(JSON.stringify({ ok: true, path: dest }));
@@ -6743,7 +7389,7 @@ function parseTokenUpdates(opts) {
6743
7389
  }
6744
7390
  return updates;
6745
7391
  }
6746
- var bridgeCommand = new Command3("bridge").description(
7392
+ var bridgeCommand = new Command4("bridge").description(
6747
7393
  "Machine-readable JSON protocol for AI integration"
6748
7394
  );
6749
7395
  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) => {
@@ -6786,7 +7432,7 @@ bridgeCommand.command("backup-db").description("Back up the local database into
6786
7432
  return;
6787
7433
  }
6788
7434
  await withDb2(async (db) => {
6789
- const workspaceDir = opts.dir || await getSetting(db, "personal.workspace_dir") || join16(homedir10(), "Documents", "zam");
7435
+ const workspaceDir = opts.dir || await getSetting(db, "personal.workspace_dir") || join17(homedir10(), "Documents", "zam");
6790
7436
  const path = await backupDatabaseTo(db, workspaceDir);
6791
7437
  jsonOut2({ ok: true, path });
6792
7438
  });
@@ -6796,20 +7442,135 @@ bridgeCommand.command("workspace-info").description("Report the workspace dir, i
6796
7442
  const workspaceDir = await getSetting(db, "personal.workspace_dir") || null;
6797
7443
  jsonOut2({
6798
7444
  workspaceDir,
6799
- defaultWorkspaceDir: join16(homedir10(), "Documents", "zam"),
6800
- dataDir: join16(homedir10(), ".zam")
7445
+ defaultWorkspaceDir: join17(homedir10(), "Documents", "zam"),
7446
+ dataDir: join17(homedir10(), ".zam")
7447
+ });
7448
+ });
7449
+ });
7450
+ bridgeCommand.command("workspace-list").description("List configured ZAM workspaces (JSON)").action(async () => {
7451
+ await withDb2(async (db) => {
7452
+ const workspaceDir = await getSetting(db, "personal.workspace_dir") || null;
7453
+ jsonOut2({
7454
+ workspaces: getConfiguredWorkspaces(),
7455
+ workspaceDir,
7456
+ defaultWorkspaceDir: join17(homedir10(), "Documents", "zam"),
7457
+ dataDir: join17(homedir10(), ".zam")
7458
+ });
7459
+ });
7460
+ });
7461
+ function workspaceIdFromPath(dir) {
7462
+ const base = basename5(dir).toLowerCase().replace(/[^a-z0-9]+/g, "-").replace(/^-+|-+$/g, "");
7463
+ const prefix = base || "workspace";
7464
+ const existing = new Set(getConfiguredWorkspaces().map((item) => item.id));
7465
+ if (!existing.has(prefix)) return prefix;
7466
+ let index = 2;
7467
+ while (existing.has(`${prefix}-${index}`)) index++;
7468
+ return `${prefix}-${index}`;
7469
+ }
7470
+ function parseBridgeWorkspaceKind(value) {
7471
+ const kind = (value || "custom").toLowerCase();
7472
+ if (kind === "personal" || kind === "team" || kind === "family" || kind === "community" || kind === "organization" || kind === "custom") {
7473
+ return kind;
7474
+ }
7475
+ jsonError(`Invalid workspace kind: ${value}`);
7476
+ }
7477
+ 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) => {
7478
+ const raw = String(opts.path ?? "").trim();
7479
+ if (!raw) jsonError("A non-empty --path is required");
7480
+ const path = resolve5(raw);
7481
+ if (!existsSync17(path)) jsonError(`Workspace path does not exist: ${path}`);
7482
+ const id = String(opts.id || workspaceIdFromPath(path)).trim();
7483
+ if (!id) jsonError("A non-empty --id is required");
7484
+ const workspace = {
7485
+ id,
7486
+ label: opts.label || basename5(path) || id,
7487
+ kind: parseBridgeWorkspaceKind(opts.kind),
7488
+ path
7489
+ };
7490
+ await withDb2(async (db) => {
7491
+ await setSetting(db, "personal.workspace_dir", path);
7492
+ upsertConfiguredWorkspace(workspace);
7493
+ jsonOut2({
7494
+ ok: true,
7495
+ workspace,
7496
+ workspaces: getConfiguredWorkspaces(),
7497
+ workspaceDir: path
6801
7498
  });
6802
7499
  });
6803
7500
  });
6804
7501
  bridgeCommand.command("set-workspace-dir").description("Set the personal workspace directory (JSON)").requiredOption("--dir <path>", "Path to the workspace directory").action(async (opts) => {
6805
7502
  const raw = String(opts.dir ?? "").trim();
6806
7503
  if (!raw) jsonError("A non-empty --dir is required");
6807
- const dir = resolve3(raw);
7504
+ const dir = resolve5(raw);
6808
7505
  await withDb2(async (db) => {
6809
7506
  await setSetting(db, "personal.workspace_dir", dir);
6810
7507
  jsonOut2({ ok: true, workspaceDir: dir });
6811
7508
  });
6812
7509
  });
7510
+ bridgeCommand.command("agent-list").description("List agent harnesses with detection state + the default (JSON)").action(async () => {
7511
+ await withDb2(async (db) => {
7512
+ const configuredDefault = await getSetting(db, "agent.default") || null;
7513
+ const harnesses = await Promise.all(
7514
+ AGENT_HARNESSES.map(async (h) => {
7515
+ const override = await getSetting(db, `agent.${h.id}.command`) || void 0;
7516
+ return {
7517
+ id: h.id,
7518
+ label: h.label,
7519
+ kind: h.kind,
7520
+ detected: resolveHarnessExecutable(h, override) !== null
7521
+ };
7522
+ })
7523
+ );
7524
+ const fallbackDefault = harnesses.find((h) => h.detected)?.id ?? null;
7525
+ jsonOut2({ harnesses, default: configuredDefault ?? fallbackDefault });
7526
+ });
7527
+ });
7528
+ bridgeCommand.command("agent-open").description("Launch an agent harness in the workspace (JSON)").option(
7529
+ "--id <id>",
7530
+ "Harness id (default: agent.default setting, else first detected)"
7531
+ ).option("--workspace <id>", "Configured workspace id to open").option("--dir <path>", "Explicit workspace directory to open").action(async (opts) => {
7532
+ await withDb2(async (db) => {
7533
+ let id = opts.id || await getSetting(db, "agent.default") || void 0;
7534
+ if (!id) {
7535
+ id = AGENT_HARNESSES.find((h) => resolveHarnessExecutable(h))?.id;
7536
+ }
7537
+ if (!id) {
7538
+ jsonError(
7539
+ "No agent harness configured or detected. Install one (Claude Code, Codex, opencode) or set agent.default."
7540
+ );
7541
+ }
7542
+ const harness = getHarness(id);
7543
+ if (!harness) {
7544
+ jsonError(`Unknown harness: ${id}`);
7545
+ }
7546
+ const override = await getSetting(db, `agent.${harness.id}.command`) || void 0;
7547
+ const executable = resolveHarnessExecutable(harness, override);
7548
+ if (!executable) {
7549
+ jsonError(
7550
+ `${harness.label} was not detected. Set its path: zam settings set agent.${harness.id}.command <path>`
7551
+ );
7552
+ }
7553
+ const configuredWorkspace = opts.workspace ? getConfiguredWorkspaces().find((item) => item.id === opts.workspace) : void 0;
7554
+ if (opts.workspace && !configuredWorkspace) {
7555
+ jsonError(`Workspace is not configured: ${opts.workspace}`);
7556
+ }
7557
+ let workspace = opts.dir || configuredWorkspace?.path || await getSetting(db, "personal.workspace_dir") || join17(homedir10(), "Documents", "zam");
7558
+ if (!existsSync17(workspace)) workspace = homedir10();
7559
+ launchHarness(harness, {
7560
+ executable,
7561
+ workspace,
7562
+ shell: normalizeShell(void 0),
7563
+ silent: true
7564
+ });
7565
+ jsonOut2({
7566
+ ok: true,
7567
+ id: harness.id,
7568
+ label: harness.label,
7569
+ kind: harness.kind,
7570
+ workspace
7571
+ });
7572
+ });
7573
+ });
6813
7574
  bridgeCommand.command("get-review").description("Get next review card with prompt (JSON)").option("--user <id>", "User ID (default: whoami)").option("--no-resolve", "Skip resolving the token's source_link into context").action(async (opts) => {
6814
7575
  await withDb2(async (db) => {
6815
7576
  const userId = await resolveUser(opts, db, { json: true });
@@ -7168,7 +7929,7 @@ bridgeCommand.command("discover-skills").description(
7168
7929
  "20"
7169
7930
  ).action(async (opts) => {
7170
7931
  try {
7171
- const monitorDir = join16(homedir10(), ".zam", "monitor");
7932
+ const monitorDir = join17(homedir10(), ".zam", "monitor");
7172
7933
  let files;
7173
7934
  try {
7174
7935
  files = readdirSync2(monitorDir).filter((f) => f.endsWith(".jsonl"));
@@ -7181,7 +7942,7 @@ bridgeCommand.command("discover-skills").description(
7181
7942
  return;
7182
7943
  }
7183
7944
  const limit = Number(opts.limit);
7184
- const sorted = files.map((f) => ({ name: f, path: join16(monitorDir, f) })).sort((a, b) => b.name.localeCompare(a.name)).slice(0, limit);
7945
+ const sorted = files.map((f) => ({ name: f, path: join17(monitorDir, f) })).sort((a, b) => b.name.localeCompare(a.name)).slice(0, limit);
7185
7946
  const sessionCommands = /* @__PURE__ */ new Map();
7186
7947
  for (const file of sorted) {
7187
7948
  const sessionId = file.name.replace(".jsonl", "");
@@ -7683,7 +8444,7 @@ bridgeCommand.command("capture-ui").description("Capture a screenshot for agent-
7683
8444
  return;
7684
8445
  }
7685
8446
  }
7686
- const outputPath = opts.image ?? opts.output ?? join16(tmpdir3(), `zam-capture-${randomBytes2(4).toString("hex")}.png`);
8447
+ const outputPath = opts.image ?? opts.output ?? join17(tmpdir3(), `zam-capture-${randomBytes2(4).toString("hex")}.png`);
7687
8448
  const captureResult = isProvided ? { method: "provided", target: null } : captureScreenshot(outputPath, opts.hwnd, opts.processName);
7688
8449
  if (!isProvided) {
7689
8450
  const post = decidePostCapture(policy, {
@@ -7711,7 +8472,7 @@ bridgeCommand.command("capture-ui").description("Capture a screenshot for agent-
7711
8472
  return;
7712
8473
  }
7713
8474
  }
7714
- const imageBytes = readFileSync10(outputPath);
8475
+ const imageBytes = readFileSync11(outputPath);
7715
8476
  const base64 = imageBytes.toString("base64");
7716
8477
  jsonOut2({
7717
8478
  sessionId: opts.session ?? null,
@@ -7738,11 +8499,11 @@ bridgeCommand.command("start-recording").description("Start screen recording in
7738
8499
  return;
7739
8500
  }
7740
8501
  const sessionId = opts.session;
7741
- const statePath = join16(tmpdir3(), `zam-recording-${sessionId}.json`);
8502
+ const statePath = join17(tmpdir3(), `zam-recording-${sessionId}.json`);
7742
8503
  const defaultExt = platform === "win32" ? ".mkv" : ".mov";
7743
- const outputPath = opts.output ?? join16(tmpdir3(), `zam-recording-${sessionId}${defaultExt}`);
7744
- const { existsSync: existsSync24, writeFileSync: writeFileSync12, openSync, closeSync } = await import("fs");
7745
- if (existsSync24(statePath)) {
8504
+ const outputPath = opts.output ?? join17(tmpdir3(), `zam-recording-${sessionId}${defaultExt}`);
8505
+ const { existsSync: existsSync25, writeFileSync: writeFileSync12, openSync, closeSync } = await import("fs");
8506
+ if (existsSync25(statePath)) {
7746
8507
  jsonOut2({
7747
8508
  sessionId,
7748
8509
  started: false,
@@ -7750,7 +8511,7 @@ bridgeCommand.command("start-recording").description("Start screen recording in
7750
8511
  });
7751
8512
  return;
7752
8513
  }
7753
- const logPath = join16(tmpdir3(), `zam-recording-${sessionId}.log`);
8514
+ const logPath = join17(tmpdir3(), `zam-recording-${sessionId}.log`);
7754
8515
  let logFd;
7755
8516
  try {
7756
8517
  logFd = openSync(logPath, "w");
@@ -7832,9 +8593,9 @@ bridgeCommand.command("stop-recording").description(
7832
8593
  return;
7833
8594
  }
7834
8595
  const sessionId = opts.session;
7835
- const statePath = join16(tmpdir3(), `zam-recording-${sessionId}.json`);
7836
- const { existsSync: existsSync24, readFileSync: readFileSync15, rmSync: rmSync3 } = await import("fs");
7837
- if (!existsSync24(statePath)) {
8596
+ const statePath = join17(tmpdir3(), `zam-recording-${sessionId}.json`);
8597
+ const { existsSync: existsSync25, readFileSync: readFileSync16, rmSync: rmSync3 } = await import("fs");
8598
+ if (!existsSync25(statePath)) {
7838
8599
  jsonOut2({
7839
8600
  sessionId,
7840
8601
  stopped: false,
@@ -7842,7 +8603,7 @@ bridgeCommand.command("stop-recording").description(
7842
8603
  });
7843
8604
  return;
7844
8605
  }
7845
- const state = JSON.parse(readFileSync15(statePath, "utf8"));
8606
+ const state = JSON.parse(readFileSync16(statePath, "utf8"));
7846
8607
  const { pid, outputPath } = state;
7847
8608
  try {
7848
8609
  process.kill(pid, "SIGINT");
@@ -7858,7 +8619,7 @@ bridgeCommand.command("stop-recording").description(
7858
8619
  };
7859
8620
  let attempts = 0;
7860
8621
  while (isProcessRunning(pid) && attempts < 20) {
7861
- await new Promise((resolve6) => setTimeout(resolve6, 250));
8622
+ await new Promise((resolve8) => setTimeout(resolve8, 250));
7862
8623
  attempts++;
7863
8624
  }
7864
8625
  if (isProcessRunning(pid)) {
@@ -7871,7 +8632,7 @@ bridgeCommand.command("stop-recording").description(
7871
8632
  rmSync3(statePath, { force: true });
7872
8633
  } catch {
7873
8634
  }
7874
- if (!existsSync24(outputPath)) {
8635
+ if (!existsSync25(outputPath)) {
7875
8636
  jsonOut2({
7876
8637
  sessionId,
7877
8638
  stopped: false,
@@ -7962,6 +8723,16 @@ bridgeCommand.command("check-llm").description("Check if LLM is enabled and onli
7962
8723
  });
7963
8724
  });
7964
8725
  });
8726
+ bridgeCommand.command("provider-status").description("Show secret-safe provider status for LLM roles (JSON)").action(async () => {
8727
+ await withDb2(async (db) => {
8728
+ const [recall, vision, text] = await Promise.all([
8729
+ getProviderRoleStatus(db, "recall"),
8730
+ getProviderRoleStatus(db, "vision"),
8731
+ getProviderRoleStatus(db, "text")
8732
+ ]);
8733
+ jsonOut2({ roles: { recall, vision, text } });
8734
+ });
8735
+ });
7965
8736
  bridgeCommand.command("check-vision").description(
7966
8737
  "Check if UI observer vision analysis is enabled and ready (JSON)"
7967
8738
  ).action(async () => {
@@ -8274,8 +9045,8 @@ bridgeCommand.command("serve").description("Start the persistent JSON-RPC stdin/
8274
9045
  });
8275
9046
 
8276
9047
  // src/cli/commands/card.ts
8277
- import { Command as Command4 } from "commander";
8278
- var cardCommand = new Command4("card").description(
9048
+ import { Command as Command5 } from "commander";
9049
+ var cardCommand = new Command5("card").description(
8279
9050
  "Manage spaced-repetition cards"
8280
9051
  );
8281
9052
  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) => {
@@ -8474,8 +9245,8 @@ cardCommand.command("delete").description("Delete one user's card for a token").
8474
9245
 
8475
9246
  // src/cli/commands/connector.ts
8476
9247
  import { input as input2, password } from "@inquirer/prompts";
8477
- import { Command as Command5 } from "commander";
8478
- var connectorCommand = new Command5("connector").description(
9248
+ import { Command as Command6 } from "commander";
9249
+ var connectorCommand = new Command6("connector").description(
8479
9250
  "Manage external service connectors"
8480
9251
  );
8481
9252
  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(
@@ -8618,26 +9389,26 @@ async function setupTurso(urlArg, tokenArg, mode) {
8618
9389
 
8619
9390
  // src/cli/commands/git-sync.ts
8620
9391
  import { execSync as execSync5 } from "child_process";
8621
- import { chmodSync as chmodSync2, existsSync as existsSync16, writeFileSync as writeFileSync8 } from "fs";
8622
- import { join as join17 } from "path";
8623
- import { Command as Command6 } from "commander";
9392
+ import { chmodSync as chmodSync2, existsSync as existsSync18, writeFileSync as writeFileSync9 } from "fs";
9393
+ import { join as join18 } from "path";
9394
+ import { Command as Command7 } from "commander";
8624
9395
  function installHook2() {
8625
- const gitDir = join17(process.cwd(), ".git");
8626
- if (!existsSync16(gitDir)) {
9396
+ const gitDir = join18(process.cwd(), ".git");
9397
+ if (!existsSync18(gitDir)) {
8627
9398
  console.error(
8628
9399
  "Error: Current directory is not the root of a Git repository."
8629
9400
  );
8630
9401
  process.exit(1);
8631
9402
  }
8632
- const hooksDir = join17(gitDir, "hooks");
8633
- const hookPath = join17(hooksDir, "post-commit");
9403
+ const hooksDir = join18(gitDir, "hooks");
9404
+ const hookPath = join18(hooksDir, "post-commit");
8634
9405
  const hookContent = `#!/bin/sh
8635
9406
  # ZAM Spaced Repetition Auto-Stale Hook
8636
9407
  # Triggered automatically on git commits to decay modified concept cards.
8637
9408
  zam git-sync --commit HEAD --quiet
8638
9409
  `;
8639
9410
  try {
8640
- writeFileSync8(hookPath, hookContent, { encoding: "utf-8", flag: "w" });
9411
+ writeFileSync9(hookPath, hookContent, { encoding: "utf-8", flag: "w" });
8641
9412
  try {
8642
9413
  chmodSync2(hookPath, "755");
8643
9414
  } catch (_e) {
@@ -8650,7 +9421,7 @@ zam git-sync --commit HEAD --quiet
8650
9421
  process.exit(1);
8651
9422
  }
8652
9423
  }
8653
- var gitSyncCommand = new Command6("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) => {
9424
+ var gitSyncCommand = new Command7("git-sync").description("Sync learning cards with recent Git file modifications").option("--commit <hash>", "Git commit hash to check", "HEAD").option("--user <id>", "User ID (default: whoami)").option("--install", "Install git post-commit hook in current repo").option("--quiet", "Suppress verbose output").action(async (opts) => {
8654
9425
  if (opts.install) {
8655
9426
  installHook2();
8656
9427
  return;
@@ -8739,10 +9510,10 @@ ZAM Auto-Stale Complete: Scanned ${changedFiles.length} file(s).`
8739
9510
  });
8740
9511
 
8741
9512
  // src/cli/commands/goal.ts
8742
- import { existsSync as existsSync17, mkdirSync as mkdirSync9 } from "fs";
8743
- import { resolve as resolve4 } from "path";
9513
+ import { existsSync as existsSync19, mkdirSync as mkdirSync10 } from "fs";
9514
+ import { resolve as resolve6 } from "path";
8744
9515
  import { input as input3 } from "@inquirer/prompts";
8745
- import { Command as Command7 } from "commander";
9516
+ import { Command as Command8 } from "commander";
8746
9517
  async function resolveGoalsDir() {
8747
9518
  let goalsDir;
8748
9519
  let db;
@@ -8753,9 +9524,9 @@ async function resolveGoalsDir() {
8753
9524
  } finally {
8754
9525
  await db?.close();
8755
9526
  }
8756
- return goalsDir ? resolve4(goalsDir) : resolve4("goals");
9527
+ return goalsDir ? resolve6(goalsDir) : resolve6("goals");
8757
9528
  }
8758
- var goalCommand = new Command7("goal").description(
9529
+ var goalCommand = new Command8("goal").description(
8759
9530
  "Manage learning goals (markdown files)"
8760
9531
  );
8761
9532
  goalCommand.command("list").description("List all goals").option(
@@ -8763,7 +9534,7 @@ goalCommand.command("list").description("List all goals").option(
8763
9534
  "Filter by status (active, completed, paused, abandoned)"
8764
9535
  ).option("--tree", "Show goals as a tree with parent/child relationships").option("--json", "Output as JSON").action(async (opts) => {
8765
9536
  const goalsDir = await resolveGoalsDir();
8766
- if (!existsSync17(goalsDir)) {
9537
+ if (!existsSync19(goalsDir)) {
8767
9538
  console.error(`Goals directory not found: ${goalsDir}`);
8768
9539
  console.error(
8769
9540
  "Set it with: zam settings set personal.goals_dir /path/to/goals"
@@ -8864,8 +9635,8 @@ ${"\u2500".repeat(50)}`);
8864
9635
  });
8865
9636
  goalCommand.command("create").description("Create a new goal").option("--slug <slug>", "Goal slug (used as filename)").option("--title <title>", "Goal title").option("--parent <slug>", "Parent goal slug").option("--description <text>", "Goal description").option("--json", "Output as JSON").action(async (opts) => {
8866
9637
  const goalsDir = await resolveGoalsDir();
8867
- if (!existsSync17(goalsDir)) {
8868
- mkdirSync9(goalsDir, { recursive: true });
9638
+ if (!existsSync19(goalsDir)) {
9639
+ mkdirSync10(goalsDir, { recursive: true });
8869
9640
  }
8870
9641
  let slug = opts.slug;
8871
9642
  let title = opts.title;
@@ -8924,22 +9695,22 @@ goalCommand.command("status <slug> <status>").description("Update a goal's statu
8924
9695
  });
8925
9696
 
8926
9697
  // src/cli/commands/init.ts
8927
- import { existsSync as existsSync18, mkdirSync as mkdirSync10, writeFileSync as writeFileSync9 } from "fs";
9698
+ import { existsSync as existsSync20, mkdirSync as mkdirSync11, writeFileSync as writeFileSync10 } from "fs";
8928
9699
  import { homedir as homedir11 } from "os";
8929
- import { join as join18 } from "path";
9700
+ import { join as join19 } from "path";
8930
9701
  import { confirm as confirm2, input as input4 } from "@inquirer/prompts";
8931
- import { Command as Command8 } from "commander";
9702
+ import { Command as Command9 } from "commander";
8932
9703
  var HOME2 = homedir11();
8933
9704
  function printLine(char = "\u2550", len = 60, color = "\x1B[36m") {
8934
9705
  console.log(`${color}${char.repeat(len)}\x1B[0m`);
8935
9706
  }
8936
9707
  function bootstrapSandboxWorkspace(workspaceDir) {
8937
- mkdirSync10(join18(workspaceDir, "beliefs"), { recursive: true });
8938
- mkdirSync10(join18(workspaceDir, "goals"), { recursive: true });
8939
- mkdirSync10(join18(workspaceDir, "skills"), { recursive: true });
8940
- const worldviewFile = join18(workspaceDir, "beliefs", "worldview.md");
8941
- if (!existsSync18(worldviewFile)) {
8942
- writeFileSync9(
9708
+ mkdirSync11(join19(workspaceDir, "beliefs"), { recursive: true });
9709
+ mkdirSync11(join19(workspaceDir, "goals"), { recursive: true });
9710
+ mkdirSync11(join19(workspaceDir, "skills"), { recursive: true });
9711
+ const worldviewFile = join19(workspaceDir, "beliefs", "worldview.md");
9712
+ if (!existsSync20(worldviewFile)) {
9713
+ writeFileSync10(
8943
9714
  worldviewFile,
8944
9715
  `# Personal Worldview
8945
9716
 
@@ -8951,9 +9722,9 @@ Here, I declare the core concepts and principles I want to master.
8951
9722
  "utf8"
8952
9723
  );
8953
9724
  }
8954
- const goalsFile = join18(workspaceDir, "goals", "goals.md");
8955
- if (!existsSync18(goalsFile)) {
8956
- writeFileSync9(
9725
+ const goalsFile = join19(workspaceDir, "goals", "goals.md");
9726
+ if (!existsSync20(goalsFile)) {
9727
+ writeFileSync10(
8957
9728
  goalsFile,
8958
9729
  `# Personal Goals
8959
9730
 
@@ -8965,7 +9736,7 @@ Here, I declare the core concepts and principles I want to master.
8965
9736
  );
8966
9737
  }
8967
9738
  }
8968
- var initCommand = new Command8("init").description("Launch the guided interactive onboarding wizard").action(async () => {
9739
+ var initCommand = new Command9("init").description("Launch the guided interactive onboarding wizard").action(async () => {
8969
9740
  printLine();
8970
9741
  console.log(
8971
9742
  "\x1B[1m\x1B[32m ZAM \u2014 The Symbiotic Learning Agent Onboarding\x1B[0m"
@@ -8975,7 +9746,7 @@ var initCommand = new Command8("init").description("Launch the guided interactiv
8975
9746
  );
8976
9747
  printLine();
8977
9748
  console.log("\n\x1B[1m[1/5] Setting up Local Workspace Sandbox\x1B[0m");
8978
- const defaultWorkspace = join18(HOME2, "Documents", "zam");
9749
+ const defaultWorkspace = join19(HOME2, "Documents", "zam");
8979
9750
  const workspacePath = await input4({
8980
9751
  message: "Choose your ZAM workspace directory:",
8981
9752
  default: defaultWorkspace
@@ -9132,7 +9903,7 @@ var initCommand = new Command8("init").description("Launch the guided interactiv
9132
9903
 
9133
9904
  // src/cli/commands/learn.ts
9134
9905
  import { input as input6 } from "@inquirer/prompts";
9135
- import { Command as Command9 } from "commander";
9906
+ import { Command as Command10 } from "commander";
9136
9907
 
9137
9908
  // src/cli/learn-format.ts
9138
9909
  var BLOOM_VERBS3 = {
@@ -9470,7 +10241,7 @@ var STOP_WORDS = /* @__PURE__ */ new Set(["q", ":q", "quit", "stop"]);
9470
10241
  function isExitPrompt(err) {
9471
10242
  return err instanceof Error && err.name === "ExitPromptError";
9472
10243
  }
9473
- var learnCommand = new Command9("learn").description(
10244
+ var learnCommand = new Command10("learn").description(
9474
10245
  "Run a spoiler-free, in-process learning session (recall \u2192 reveal \u2192 self-rate)"
9475
10246
  ).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) => {
9476
10247
  let db;
@@ -9714,8 +10485,8 @@ learnCommand.command("open").description("Open a new terminal window running zam
9714
10485
  });
9715
10486
 
9716
10487
  // src/cli/commands/monitor.ts
9717
- import { Command as Command10 } from "commander";
9718
- var monitorCommand = new Command10("monitor").description(
10488
+ import { Command as Command11 } from "commander";
10489
+ var monitorCommand = new Command11("monitor").description(
9719
10490
  "Shell observation for real-time task monitoring"
9720
10491
  );
9721
10492
  monitorCommand.command("start").description("Output shell hook code to install monitoring").requiredOption("--session <id>", "Session ID to monitor").option(
@@ -9897,8 +10668,8 @@ monitorCommand.command("open").description("Open a new monitored terminal window
9897
10668
  });
9898
10669
 
9899
10670
  // src/cli/commands/observer.ts
9900
- import { Command as Command11 } from "commander";
9901
- var observerCommand = new Command11("observer").description(
10671
+ import { Command as Command12 } from "commander";
10672
+ var observerCommand = new Command12("observer").description(
9902
10673
  "Configure what the UI observer may capture (Layer 2 policy)"
9903
10674
  );
9904
10675
  function applyObserverListChange(current, entry, op) {
@@ -9961,8 +10732,8 @@ observerCommand.command("revoke <process>").description("Remove a process from o
9961
10732
 
9962
10733
  // src/cli/commands/profile.ts
9963
10734
  import { homedir as homedir12 } from "os";
9964
- import { dirname as dirname5, join as join19, resolve as resolve5 } from "path";
9965
- import { Command as Command12 } from "commander";
10735
+ import { dirname as dirname6, join as join20, resolve as resolve7 } from "path";
10736
+ import { Command as Command13 } from "commander";
9966
10737
  var C2 = {
9967
10738
  reset: "\x1B[0m",
9968
10739
  bold: "\x1B[1m",
@@ -9971,7 +10742,7 @@ var C2 = {
9971
10742
  green: "\x1B[32m"
9972
10743
  };
9973
10744
  function defaultPersonalDir() {
9974
- return join19(homedir12(), "Documents", "zam");
10745
+ return join20(homedir12(), "Documents", "zam");
9975
10746
  }
9976
10747
  function render(profile) {
9977
10748
  const sync = profile.syncProvider ? `${C2.green}${profile.syncProvider}${C2.reset} ${C2.dim}(good for cross-device snapshots)${C2.reset}` : `${C2.dim}local folder (use a synced folder or snapshots to move between machines)${C2.reset}`;
@@ -9982,7 +10753,7 @@ function render(profile) {
9982
10753
  console.log(` data dir: ${C2.cyan}${profile.dataDir}${C2.reset}`);
9983
10754
  console.log(` database: ${C2.cyan}${profile.dbPath}${C2.reset}`);
9984
10755
  }
9985
- var profileCommand = new Command12("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) => {
10756
+ var profileCommand = new Command13("profile").description("Show or change this machine's ZAM install profile").option("--mode <mode>", "Set install mode: developer | default").option("--dir <path>", "Set the personal-content folder").option("--json", "Output as JSON").action(async (opts) => {
9986
10757
  if (opts.mode && opts.mode !== "developer" && opts.mode !== "default") {
9987
10758
  console.error(`Invalid --mode: ${opts.mode}. Use developer or default.`);
9988
10759
  process.exit(1);
@@ -9992,7 +10763,7 @@ var profileCommand = new Command12("profile").description("Show or change this m
9992
10763
  if (opts.mode) setInstallMode(opts.mode);
9993
10764
  db = await openDatabaseWithSync({ initialize: true });
9994
10765
  if (opts.dir) {
9995
- await setSetting(db, "personal.workspace_dir", resolve5(opts.dir));
10766
+ await setSetting(db, "personal.workspace_dir", resolve7(opts.dir));
9996
10767
  }
9997
10768
  const personalDir = await getSetting(db, "personal.workspace_dir") || defaultPersonalDir();
9998
10769
  await db.close();
@@ -10002,7 +10773,7 @@ var profileCommand = new Command12("profile").description("Show or change this m
10002
10773
  mode: getInstallMode(),
10003
10774
  personalDir,
10004
10775
  syncProvider: detectSyncProvider(personalDir),
10005
- dataDir: dirname5(dbPath),
10776
+ dataDir: dirname6(dbPath),
10006
10777
  dbPath
10007
10778
  };
10008
10779
  if (opts.json) {
@@ -10019,7 +10790,7 @@ var profileCommand = new Command12("profile").description("Show or change this m
10019
10790
 
10020
10791
  // src/cli/commands/provider.ts
10021
10792
  import { password as password2 } from "@inquirer/prompts";
10022
- import { Command as Command13 } from "commander";
10793
+ import { Command as Command14 } from "commander";
10023
10794
  var VALID_API_FLAVORS = [
10024
10795
  "chat-completions",
10025
10796
  "anthropic-messages"
@@ -10031,6 +10802,9 @@ function upsertProviderRecord(providers, name, patch) {
10031
10802
  if (patch.model !== void 0) merged.model = patch.model;
10032
10803
  if (patch.apiFlavor !== void 0) merged.apiFlavor = patch.apiFlavor;
10033
10804
  if (patch.apiKeyRef !== void 0) merged.apiKeyRef = patch.apiKeyRef;
10805
+ if (patch.label !== void 0) merged.label = patch.label;
10806
+ if (patch.local !== void 0) merged.local = patch.local;
10807
+ if (patch.runner !== void 0) merged.runner = patch.runner;
10034
10808
  return { ...providers, [name]: merged };
10035
10809
  }
10036
10810
  function removeProviderRecord(providers, name) {
@@ -10059,7 +10833,7 @@ function buildProviderListing(providers, hasKey) {
10059
10833
  let keyState;
10060
10834
  if (!rec.apiKeyRef) keyState = "none";
10061
10835
  else keyState = hasKey(rec.apiKeyRef) ? "set" : "missing";
10062
- return {
10836
+ const row = {
10063
10837
  name,
10064
10838
  url: rec.url,
10065
10839
  model: rec.model,
@@ -10067,6 +10841,10 @@ function buildProviderListing(providers, hasKey) {
10067
10841
  apiKeyRef: rec.apiKeyRef,
10068
10842
  keyState
10069
10843
  };
10844
+ if (rec.label !== void 0) row.label = rec.label;
10845
+ if (rec.local !== void 0) row.local = rec.local;
10846
+ if (rec.runner !== void 0) row.runner = rec.runner;
10847
+ return row;
10070
10848
  });
10071
10849
  }
10072
10850
  function findOrphanKeyRefs(storedRefs, providers) {
@@ -10086,19 +10864,53 @@ async function readJson(db, key, fallback) {
10086
10864
  }
10087
10865
  var readProviders = (db) => readJson(db, "llm.providers", {});
10088
10866
  var readRoles = (db) => readJson(db, "llm.roles", {});
10089
- async function writeProviders(db, p) {
10867
+ async function readScopedProviders(db, machine) {
10868
+ if (machine) return getMachineAiConfig().providers ?? {};
10869
+ if (!db)
10870
+ throw new Error("Database is required for shared provider settings.");
10871
+ return readProviders(db);
10872
+ }
10873
+ async function readScopedRoles(db, machine) {
10874
+ if (machine) return getMachineAiConfig().roles ?? {};
10875
+ if (!db)
10876
+ throw new Error("Database is required for shared provider settings.");
10877
+ return readRoles(db);
10878
+ }
10879
+ async function writeScopedProviders(db, machine, p) {
10880
+ if (machine) {
10881
+ const config = getMachineAiConfig();
10882
+ saveMachineAiConfig({ ...config, providers: p });
10883
+ return;
10884
+ }
10885
+ if (!db)
10886
+ throw new Error("Database is required for shared provider settings.");
10090
10887
  await setSetting(db, "llm.providers", JSON.stringify(p));
10091
10888
  }
10092
- async function writeRoles(db, r) {
10889
+ async function writeScopedRoles(db, machine, r) {
10890
+ if (machine) {
10891
+ const config = getMachineAiConfig();
10892
+ saveMachineAiConfig({ ...config, roles: r });
10893
+ return;
10894
+ }
10895
+ if (!db)
10896
+ throw new Error("Database is required for shared provider settings.");
10093
10897
  await setSetting(db, "llm.roles", JSON.stringify(r));
10094
10898
  }
10095
- var providerCommand = new Command13("provider").description(
10899
+ async function withProviderScope(machine, action) {
10900
+ if (machine) {
10901
+ await action(void 0);
10902
+ return;
10903
+ }
10904
+ await withDb(action);
10905
+ }
10906
+ var providerCommand = new Command14("provider").description(
10096
10907
  "Configure role-based AI providers (url/model/flavor/key, per role)"
10097
10908
  );
10098
- providerCommand.command("list").description("Show configured providers, role bindings, and key status").option("--json", "Output as JSON").action(async (opts) => {
10099
- await withDb(async (db) => {
10100
- const providers = await readProviders(db);
10101
- const roles = await readRoles(db);
10909
+ providerCommand.command("list").description("Show configured providers, role bindings, and key status").option("--json", "Output as JSON").option("--machine", "Read machine-local providers from ~/.zam/config.json").action(async (opts) => {
10910
+ const machine = Boolean(opts.machine);
10911
+ await withProviderScope(machine, async (db) => {
10912
+ const providers = await readScopedProviders(db, machine);
10913
+ const roles = await readScopedRoles(db, machine);
10102
10914
  const rows = buildProviderListing(
10103
10915
  providers,
10104
10916
  (ref) => getProviderApiKey(ref) !== null
@@ -10106,11 +10918,23 @@ providerCommand.command("list").description("Show configured providers, role bin
10106
10918
  const orphans = findOrphanKeyRefs(listProviderApiKeyRefs(), providers);
10107
10919
  if (opts.json) {
10108
10920
  console.log(
10109
- JSON.stringify({ providers: rows, roles, orphans }, null, 2)
10921
+ JSON.stringify(
10922
+ {
10923
+ scope: machine ? "machine" : "shared",
10924
+ providers: rows,
10925
+ roles,
10926
+ orphans
10927
+ },
10928
+ null,
10929
+ 2
10930
+ )
10110
10931
  );
10111
10932
  return;
10112
10933
  }
10113
- console.log("Providers (llm.providers):\n");
10934
+ console.log(
10935
+ `Providers (${opts.machine ? "~/.zam/config.json ai.providers" : "llm.providers"}):
10936
+ `
10937
+ );
10114
10938
  if (rows.length === 0) {
10115
10939
  console.log(
10116
10940
  " (none) \u2014 add one: zam provider add <name> --url <url> --model <model>"
@@ -10122,6 +10946,11 @@ providerCommand.command("list").description("Show configured providers, role bin
10122
10946
  console.log(` url: ${row.url ?? "(inherits llm.url)"}`);
10123
10947
  console.log(` model: ${row.model ?? "(inherits llm.model)"}`);
10124
10948
  console.log(` flavor: ${row.apiFlavor}`);
10949
+ if (row.label) console.log(` label: ${row.label}`);
10950
+ if (row.local !== void 0) {
10951
+ console.log(` local: ${row.local ? "yes" : "no"}`);
10952
+ }
10953
+ if (row.runner) console.log(` runner: ${row.runner}`);
10125
10954
  if (row.apiKeyRef) console.log(` key-ref: ${row.apiKeyRef}`);
10126
10955
  }
10127
10956
  }
@@ -10143,7 +10972,10 @@ providerCommand.command("list").description("Show configured providers, role bin
10143
10972
  }
10144
10973
  });
10145
10974
  });
10146
- providerCommand.command("add <name>").description("Add or update a provider record in llm.providers").option("--url <url>", "Endpoint base URL (e.g. https://api.deepseek.com/v1)").option("--model <model>", "Model id (e.g. deepseek-v4-flash)").option(
10975
+ providerCommand.command("add <name>").description("Add or update a provider record in llm.providers").option("--label <label>", "Human-readable provider label").option("--url <url>", "Endpoint base URL (e.g. https://api.deepseek.com/v1)").option("--model <model>", "Model id (e.g. deepseek-v4-flash)").option("--local", "Mark this provider as a local/on-device endpoint").option(
10976
+ "--runner <runner>",
10977
+ "Local runner hint (flm, foundry-local, ollama, ...)"
10978
+ ).option(
10147
10979
  "--flavor <flavor>",
10148
10980
  `Wire protocol: ${VALID_API_FLAVORS.join(" | ")} (default: inferred from URL)`
10149
10981
  ).option(
@@ -10152,7 +10984,7 @@ providerCommand.command("add <name>").description("Add or update a provider reco
10152
10984
  ).option(
10153
10985
  "--key <value>",
10154
10986
  "Store this API key now (prefer `set-key` to keep it out of shell history)"
10155
- ).action(async (name, opts) => {
10987
+ ).option("--machine", "Store this provider in ~/.zam/config.json").action(async (name, opts) => {
10156
10988
  let apiFlavor;
10157
10989
  if (opts.flavor) {
10158
10990
  if (!VALID_API_FLAVORS.includes(opts.flavor)) {
@@ -10164,20 +10996,31 @@ providerCommand.command("add <name>").description("Add or update a provider reco
10164
10996
  apiFlavor = opts.flavor;
10165
10997
  }
10166
10998
  const apiKeyRef = opts.keyRef ?? (opts.key ? name : void 0);
10167
- await withDb(async (db) => {
10168
- const providers = await readProviders(db);
10999
+ const machine = Boolean(opts.machine);
11000
+ await withProviderScope(machine, async (db) => {
11001
+ const providers = await readScopedProviders(db, machine);
10169
11002
  const next = upsertProviderRecord(providers, name, {
11003
+ label: opts.label,
10170
11004
  url: opts.url,
10171
11005
  model: opts.model,
10172
11006
  apiFlavor,
10173
- apiKeyRef
11007
+ apiKeyRef,
11008
+ local: opts.local ? true : void 0,
11009
+ runner: opts.runner
10174
11010
  });
10175
- await writeProviders(db, next);
11011
+ await writeScopedProviders(db, machine, next);
10176
11012
  if (opts.key && apiKeyRef) setProviderApiKey(apiKeyRef, opts.key);
10177
11013
  const rec = next[name];
10178
- console.log(`Provider "${name}" saved:`);
11014
+ console.log(
11015
+ `Provider "${name}" saved (${machine ? "machine-local" : "shared"}):`
11016
+ );
10179
11017
  console.log(` url: ${rec.url ?? "(inherits llm.url)"}`);
10180
11018
  console.log(` model: ${rec.model ?? "(inherits llm.model)"}`);
11019
+ if (rec.label) console.log(` label: ${rec.label}`);
11020
+ if (rec.local !== void 0) {
11021
+ console.log(` local: ${rec.local ? "yes" : "no"}`);
11022
+ }
11023
+ if (rec.runner) console.log(` runner: ${rec.runner}`);
10181
11024
  console.log(
10182
11025
  ` flavor: ${rec.apiFlavor ?? `${rec.url ? inferApiFlavor(rec.url) : "chat-completions"} (inferred)`}`
10183
11026
  );
@@ -10201,9 +11044,10 @@ Bind it to a role: zam provider use recall --primary ${name}`
10201
11044
  );
10202
11045
  });
10203
11046
  });
10204
- providerCommand.command("remove <name>").description("Remove a provider record from llm.providers").action(async (name) => {
10205
- await withDb(async (db) => {
10206
- const providers = await readProviders(db);
11047
+ providerCommand.command("remove <name>").description("Remove a provider record from llm.providers").option("--machine", "Remove from ~/.zam/config.json instead of the DB").action(async (name, opts) => {
11048
+ const machine = Boolean(opts.machine);
11049
+ await withProviderScope(machine, async (db) => {
11050
+ const providers = await readScopedProviders(db, machine);
10207
11051
  const { providers: next, removed } = removeProviderRecord(
10208
11052
  providers,
10209
11053
  name
@@ -10212,9 +11056,14 @@ providerCommand.command("remove <name>").description("Remove a provider record f
10212
11056
  console.log(`No such provider: ${name}`);
10213
11057
  return;
10214
11058
  }
10215
- await writeProviders(db, next);
10216
- console.log(`Removed provider "${name}".`);
10217
- const referencing = rolesReferencing(await readRoles(db), name);
11059
+ await writeScopedProviders(db, machine, next);
11060
+ console.log(
11061
+ `Removed provider "${name}" (${machine ? "machine-local" : "shared"}).`
11062
+ );
11063
+ const referencing = rolesReferencing(
11064
+ await readScopedRoles(db, machine),
11065
+ name
11066
+ );
10218
11067
  if (referencing.length > 0) {
10219
11068
  console.log(
10220
11069
  ` \u26A0 Still referenced by role(s): ${referencing.join(", ")} \u2014 rebind with: zam provider use <role> --primary <name>`
@@ -10222,7 +11071,7 @@ providerCommand.command("remove <name>").description("Remove a provider record f
10222
11071
  }
10223
11072
  });
10224
11073
  });
10225
- providerCommand.command("use <role>").description(`Bind providers to a role (${VALID_ROLES.join(" | ")})`).option("--primary <name>", "Primary provider").option("--fallback <name>", "Fallback provider (optional)").action(async (role, opts) => {
11074
+ providerCommand.command("use <role>").description(`Bind providers to a role (${VALID_ROLES.join(" | ")})`).option("--primary <name>", "Primary provider").option("--fallback <name>", "Fallback provider (optional)").option("--machine", "Bind the role in ~/.zam/config.json").action(async (role, opts) => {
10226
11075
  if (!VALID_ROLES.includes(role)) {
10227
11076
  console.error(`Invalid role: ${role}. Use ${VALID_ROLES.join(", ")}.`);
10228
11077
  process.exit(1);
@@ -10231,19 +11080,23 @@ providerCommand.command("use <role>").description(`Bind providers to a role (${V
10231
11080
  console.error("--primary is required.");
10232
11081
  process.exit(1);
10233
11082
  }
10234
- await withDb(async (db) => {
10235
- const providers = await readProviders(db);
10236
- await writeRoles(
11083
+ const machine = Boolean(opts.machine);
11084
+ await withProviderScope(machine, async (db) => {
11085
+ const providers = await readScopedProviders(db, machine);
11086
+ await writeScopedRoles(
10237
11087
  db,
11088
+ machine,
10238
11089
  bindRoleProviders(
10239
- await readRoles(db),
11090
+ await readScopedRoles(db, machine),
10240
11091
  role,
10241
11092
  opts.primary,
10242
11093
  opts.fallback
10243
11094
  )
10244
11095
  );
10245
11096
  const fb = opts.fallback ? `, fallback: ${opts.fallback}` : "";
10246
- console.log(`Role "${role}" \u2192 primary: ${opts.primary}${fb}`);
11097
+ console.log(
11098
+ `Role "${role}" \u2192 primary: ${opts.primary}${fb} (${machine ? "machine-local" : "shared"})`
11099
+ );
10247
11100
  for (const [label, ref] of [
10248
11101
  ["primary", opts.primary],
10249
11102
  ["fallback", opts.fallback]
@@ -10285,8 +11138,8 @@ providerCommand.command("clear-key <ref>").description("Remove a stored API key"
10285
11138
  });
10286
11139
 
10287
11140
  // src/cli/commands/review.ts
10288
- import { Command as Command14 } from "commander";
10289
- var reviewCommand = new Command14("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) => {
11141
+ import { Command as Command15 } from "commander";
11142
+ 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) => {
10290
11143
  let db;
10291
11144
  try {
10292
11145
  db = await openDatabase();
@@ -10399,10 +11252,10 @@ ${"\u2550".repeat(50)}`);
10399
11252
  });
10400
11253
 
10401
11254
  // src/cli/commands/session.ts
10402
- import { readFileSync as readFileSync11 } from "fs";
11255
+ import { readFileSync as readFileSync12 } from "fs";
10403
11256
  import { input as input7, select as select2 } from "@inquirer/prompts";
10404
- import { Command as Command15 } from "commander";
10405
- var sessionCommand = new Command15("session").description(
11257
+ import { Command as Command16 } from "commander";
11258
+ var sessionCommand = new Command16("session").description(
10406
11259
  "Manage learning sessions"
10407
11260
  );
10408
11261
  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(
@@ -10591,7 +11444,7 @@ function loadPatternFile(path) {
10591
11444
  if (!path) return [];
10592
11445
  let parsed;
10593
11446
  try {
10594
- parsed = JSON.parse(readFileSync11(path, "utf-8"));
11447
+ parsed = JSON.parse(readFileSync12(path, "utf-8"));
10595
11448
  } catch (err) {
10596
11449
  throw new Error(
10597
11450
  `Cannot read synthesis patterns from ${path}: ${err.message}`
@@ -10781,9 +11634,9 @@ sessionCommand.command("end").description("End a session and show summary").requ
10781
11634
  });
10782
11635
 
10783
11636
  // src/cli/commands/settings.ts
10784
- import { existsSync as existsSync19 } from "fs";
10785
- import { Command as Command16 } from "commander";
10786
- var settingsCommand = new Command16("settings").description(
11637
+ import { existsSync as existsSync21 } from "fs";
11638
+ import { Command as Command17 } from "commander";
11639
+ var settingsCommand = new Command17("settings").description(
10787
11640
  "Manage user settings"
10788
11641
  );
10789
11642
  var BOOLEAN_SETTING_KEYS = /* @__PURE__ */ new Set(["llm.enabled", "llm.vision.enabled"]);
@@ -10946,7 +11799,7 @@ settingsCommand.command("repos").description("Show or set Personal, Team, and Or
10946
11799
  console.log("\nValidation:");
10947
11800
  for (const [name, path] of Object.entries(paths)) {
10948
11801
  if (path) {
10949
- const exists = existsSync19(path);
11802
+ const exists = existsSync21(path);
10950
11803
  console.log(
10951
11804
  ` ${name.padEnd(9)}: ${exists ? "\x1B[32m\u2713 Valid folder\x1B[0m" : "\x1B[31m\u2717 Directory does not exist\x1B[0m"}`
10952
11805
  );
@@ -10957,175 +11810,6 @@ settingsCommand.command("repos").description("Show or set Personal, Team, and Or
10957
11810
  });
10958
11811
  });
10959
11812
 
10960
- // src/cli/commands/setup.ts
10961
- import { copyFileSync as copyFileSync2, existsSync as existsSync20, mkdirSync as mkdirSync11, writeFileSync as writeFileSync10 } from "fs";
10962
- import { basename as basename4, dirname as dirname6, join as join20 } from "path";
10963
- import { fileURLToPath as fileURLToPath2 } from "url";
10964
- import { Command as Command17 } from "commander";
10965
- var packageRoot = [
10966
- fileURLToPath2(new URL("../..", import.meta.url)),
10967
- fileURLToPath2(new URL("../../..", import.meta.url))
10968
- ].find((candidate) => existsSync20(join20(candidate, "package.json"))) ?? fileURLToPath2(new URL("../..", import.meta.url));
10969
- var SKILL_PAIRS = [
10970
- {
10971
- from: join20(packageRoot, ".claude", "skills", "zam", "SKILL.md"),
10972
- to: join20(".claude", "skills", "zam", "SKILL.md")
10973
- },
10974
- {
10975
- from: join20(packageRoot, ".agent", "skills", "zam", "SKILL.md"),
10976
- to: join20(".agent", "skills", "zam", "SKILL.md")
10977
- },
10978
- {
10979
- from: join20(packageRoot, ".agents", "skills", "zam", "SKILL.md"),
10980
- to: join20(".agents", "skills", "zam", "SKILL.md")
10981
- }
10982
- ];
10983
- function copySkills(force, cwd = process.cwd()) {
10984
- let anyAction = false;
10985
- for (const { from, to } of SKILL_PAIRS) {
10986
- const dest = join20(cwd, to);
10987
- if (!existsSync20(from)) {
10988
- console.warn(` warn source not found, skipping: ${from}`);
10989
- continue;
10990
- }
10991
- if (existsSync20(dest) && !force) {
10992
- console.log(` skip ${to} (already present \xE2\u20AC\u201D use --force to update)`);
10993
- continue;
10994
- }
10995
- mkdirSync11(dirname6(dest), { recursive: true });
10996
- copyFileSync2(from, dest);
10997
- console.log(` copy ${to}`);
10998
- anyAction = true;
10999
- }
11000
- if (!anyAction && !force) {
11001
- console.log(
11002
- "\nSkill files are already up to date. Run with --force to overwrite."
11003
- );
11004
- }
11005
- }
11006
- function formatDatabaseInitTarget(target) {
11007
- switch (target.kind) {
11008
- case "local":
11009
- return `ZAM database at ${target.location} (local SQLite)`;
11010
- case "turso-remote":
11011
- return `ZAM database via Turso remote at ${target.location}`;
11012
- case "turso-native":
11013
- return `ZAM database via Turso native driver at ${target.location}`;
11014
- case "turso-replica":
11015
- return `ZAM database replica at ${target.location} syncing from ${target.syncUrl}`;
11016
- }
11017
- }
11018
- async function initDatabase(skipInit) {
11019
- if (skipInit) return;
11020
- try {
11021
- const target = getDatabaseTargetInfo();
11022
- const db = await openDatabaseWithSync({ initialize: true });
11023
- await db.close();
11024
- console.log(` init ${formatDatabaseInitTarget(target)}`);
11025
- } catch (err) {
11026
- const msg = err.message;
11027
- if (!msg.includes("already")) {
11028
- console.warn(` warn database init: ${msg}`);
11029
- } else {
11030
- console.log(` skip database already initialized`);
11031
- }
11032
- }
11033
- }
11034
- function writeClaudeMd(skipClaudeMd, cwd = process.cwd()) {
11035
- if (skipClaudeMd) return;
11036
- const dest = join20(cwd, "CLAUDE.md");
11037
- if (existsSync20(dest)) {
11038
- console.log(` skip CLAUDE.md (already present)`);
11039
- return;
11040
- }
11041
- const name = basename4(cwd);
11042
- writeFileSync10(
11043
- dest,
11044
- `# ZAM Personal Kernel \xE2\u20AC\u201D ${name}
11045
-
11046
- This is a ZAM personal instance. ZAM builds lasting skills through spaced
11047
- repetition during real work \xE2\u20AC\u201D not separate study sessions.
11048
-
11049
- ## First time here?
11050
- Run \`/setup\` in Claude Code or Gemini CLI to complete first-time setup.
11051
-
11052
- ## Regular use
11053
- Run \`/zam\` to start a learning session on whatever you are working on.
11054
-
11055
- ## What lives here
11056
- - \`beliefs/\` \xE2\u20AC\u201D your worldview, approved by git commit
11057
- - \`goals/\` \xE2\u20AC\u201D your objectives, decomposed into tasks and learning tokens
11058
-
11059
- ## Fast-changing data
11060
- Learning tokens, cards, and review history live in local SQLite by default.
11061
- Use \`zam connector setup turso\` to store cloud credentials in
11062
- \`~/.zam/credentials.json\` and use a Turso database across machines.
11063
- `,
11064
- "utf8"
11065
- );
11066
- console.log(` write CLAUDE.md`);
11067
- }
11068
- function writeAgentsMd(skipAgentsMd, cwd = process.cwd()) {
11069
- if (skipAgentsMd) return;
11070
- const dest = join20(cwd, "AGENTS.md");
11071
- if (existsSync20(dest)) {
11072
- console.log(` skip AGENTS.md (already present)`);
11073
- return;
11074
- }
11075
- const name = basename4(cwd);
11076
- writeFileSync10(
11077
- dest,
11078
- `# ZAM Personal Kernel - ${name}
11079
-
11080
- This is a ZAM personal instance. ZAM builds lasting skills through spaced
11081
- repetition during real work, not separate study sessions.
11082
-
11083
- ## First time here?
11084
- Run \`zam setup\` from the shell. When this repository includes
11085
- \`.agents/skills/setup/\`, you can instead select \`setup\` through \`/skills\`
11086
- or invoke \`$setup\`.
11087
-
11088
- ## Regular use
11089
- Select the \`zam\` skill through \`/skills\` or invoke \`$zam\` to start a
11090
- learning session on whatever you are working on.
11091
-
11092
- ## What lives here
11093
- - \`beliefs/\` - your worldview, approved by git commit
11094
- - \`goals/\` - your objectives, decomposed into tasks and learning tokens
11095
-
11096
- ## Fast-changing data
11097
- Learning tokens, cards, and review history live in local SQLite by default.
11098
- Use \`zam connector setup turso\` to store cloud credentials in
11099
- \`~/.zam/credentials.json\` and use a Turso database across machines.
11100
-
11101
- ## Codex skills
11102
- Codex discovers repository skills under \`.agents/skills/\`. Run
11103
- \`zam setup --force\` after upgrading \`zam-core\` to refresh them.
11104
- `,
11105
- "utf8"
11106
- );
11107
- console.log(` write AGENTS.md`);
11108
- }
11109
- var setupCommand = new Command17("setup").description(
11110
- "Distribute ZAM skill files into this personal instance and initialize the database"
11111
- ).option(
11112
- "--force",
11113
- "overwrite existing skill files (use after upgrading zam)",
11114
- false
11115
- ).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).action(
11116
- async (opts) => {
11117
- console.log(`Setting up ZAM in ${process.cwd()}
11118
- `);
11119
- copySkills(opts.force);
11120
- await initDatabase(opts.skipInit);
11121
- writeClaudeMd(opts.skipClaudeMd);
11122
- writeAgentsMd(opts.skipAgentsMd);
11123
- console.log(
11124
- "\nDone. Run `zam whoami --set <your-id>` to set your identity. Start the `zam` skill with `/zam` in Claude/Gemini-compatible clients or `$zam` (or `/skills`) in Codex."
11125
- );
11126
- }
11127
- );
11128
-
11129
11813
  // src/cli/commands/skill.ts
11130
11814
  import { Command as Command18 } from "commander";
11131
11815
  var skillCommand = new Command18("skill").description(
@@ -11213,7 +11897,7 @@ skillCommand.command("add").description("Register a new agent skill").requiredOp
11213
11897
  });
11214
11898
 
11215
11899
  // src/cli/commands/snapshot.ts
11216
- import { existsSync as existsSync21, mkdirSync as mkdirSync12, readFileSync as readFileSync12, writeFileSync as writeFileSync11 } from "fs";
11900
+ import { existsSync as existsSync22, mkdirSync as mkdirSync12, readFileSync as readFileSync13, writeFileSync as writeFileSync11 } from "fs";
11217
11901
  import { homedir as homedir13 } from "os";
11218
11902
  import { dirname as dirname7, join as join21 } from "path";
11219
11903
  import { Command as Command19 } from "commander";
@@ -11246,7 +11930,7 @@ var exportCmd = new Command19("export").description("Write a portable SQL-text s
11246
11930
  }
11247
11931
  const out = opts.out ?? join21(personalDir, "snapshots", defaultOutName());
11248
11932
  const dir = dirname7(out);
11249
- if (dir && dir !== "." && !existsSync21(dir)) {
11933
+ if (dir && dir !== "." && !existsSync22(dir)) {
11250
11934
  mkdirSync12(dir, { recursive: true });
11251
11935
  }
11252
11936
  writeFileSync11(out, snapshot, "utf-8");
@@ -11263,11 +11947,11 @@ var exportCmd = new Command19("export").description("Write a portable SQL-text s
11263
11947
  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) => {
11264
11948
  let db;
11265
11949
  try {
11266
- if (!existsSync21(file)) {
11950
+ if (!existsSync22(file)) {
11267
11951
  console.error(`Error: Snapshot file not found: ${file}`);
11268
11952
  process.exit(1);
11269
11953
  }
11270
- const snapshot = readFileSync12(file, "utf-8");
11954
+ const snapshot = readFileSync13(file, "utf-8");
11271
11955
  db = await openDatabaseWithSync({ initialize: true });
11272
11956
  const result = await importSnapshot(db, snapshot, { force: opts.force });
11273
11957
  await db.close();
@@ -11285,11 +11969,11 @@ var importCmd = new Command19("import").description("Restore a snapshot into the
11285
11969
  });
11286
11970
  var verifyCmd = new Command19("verify").description("Check a snapshot's manifest and checksum without importing").argument("<file>", "Snapshot file to verify").action((file) => {
11287
11971
  try {
11288
- if (!existsSync21(file)) {
11972
+ if (!existsSync22(file)) {
11289
11973
  console.error(`Error: Snapshot file not found: ${file}`);
11290
11974
  process.exit(1);
11291
11975
  }
11292
- const manifest = verifySnapshot(readFileSync12(file, "utf-8"));
11976
+ const manifest = verifySnapshot(readFileSync13(file, "utf-8"));
11293
11977
  const { total, nonEmpty } = summarize(manifest.tables);
11294
11978
  console.log(`Valid snapshot (format v${manifest.version})`);
11295
11979
  console.log(` created: ${manifest.createdAt}`);
@@ -11629,7 +12313,7 @@ tokenCommand.command("status").description("Show full status of a token for a us
11629
12313
 
11630
12314
  // src/cli/commands/ui.ts
11631
12315
  import { spawn as spawn3, spawnSync } from "child_process";
11632
- import { existsSync as existsSync22 } from "fs";
12316
+ import { existsSync as existsSync23 } from "fs";
11633
12317
  import { homedir as homedir14 } from "os";
11634
12318
  import { dirname as dirname8, join as join22 } from "path";
11635
12319
  import { fileURLToPath as fileURLToPath3 } from "url";
@@ -11647,7 +12331,7 @@ function findDesktopDir() {
11647
12331
  for (const start of starts) {
11648
12332
  let dir = start;
11649
12333
  for (let i = 0; i < 10; i++) {
11650
- if (existsSync22(join22(dir, "desktop", "src-tauri", "tauri.conf.json"))) {
12334
+ if (existsSync23(join22(dir, "desktop", "src-tauri", "tauri.conf.json"))) {
11651
12335
  return join22(dir, "desktop");
11652
12336
  }
11653
12337
  const parent = dirname8(dir);
@@ -11662,15 +12346,15 @@ function findBuiltApp(desktopDir) {
11662
12346
  if (process.platform === "win32") {
11663
12347
  for (const name of ["ZAM.exe", "zam.exe", "zam-desktop.exe"]) {
11664
12348
  const p = join22(releaseDir, name);
11665
- if (existsSync22(p)) return p;
12349
+ if (existsSync23(p)) return p;
11666
12350
  }
11667
12351
  } else if (process.platform === "darwin") {
11668
12352
  const app = join22(releaseDir, "bundle", "macos", "ZAM.app");
11669
- if (existsSync22(app)) return app;
12353
+ if (existsSync23(app)) return app;
11670
12354
  } else {
11671
12355
  for (const name of ["zam", "ZAM", "zam-desktop"]) {
11672
12356
  const p = join22(releaseDir, name);
11673
- if (existsSync22(p)) return p;
12357
+ if (existsSync23(p)) return p;
11674
12358
  }
11675
12359
  }
11676
12360
  return null;
@@ -11681,7 +12365,7 @@ function findInstalledApp() {
11681
12365
  process.env.ProgramFiles && join22(process.env.ProgramFiles, "ZAM", "ZAM.exe"),
11682
12366
  process.env["ProgramFiles(x86)"] && join22(process.env["ProgramFiles(x86)"], "ZAM", "ZAM.exe")
11683
12367
  ] : process.platform === "darwin" ? ["/Applications/ZAM.app", join22(homedir14(), "Applications", "ZAM.app")] : ["/opt/ZAM/zam", "/usr/bin/zam-desktop"];
11684
- return candidates.find((candidate) => candidate && existsSync22(candidate)) || null;
12368
+ return candidates.find((candidate) => candidate && existsSync23(candidate)) || null;
11685
12369
  }
11686
12370
  function runNpm(args, opts) {
11687
12371
  const res = spawnSync("npm", args, {
@@ -11692,7 +12376,7 @@ function runNpm(args, opts) {
11692
12376
  return res.status ?? 1;
11693
12377
  }
11694
12378
  function ensureDesktopDeps(desktopDir) {
11695
- if (existsSync22(join22(desktopDir, "node_modules"))) return true;
12379
+ if (existsSync23(join22(desktopDir, "node_modules"))) return true;
11696
12380
  console.log(
11697
12381
  `${C3.cyan}Installing desktop dependencies (one-time)...${C3.reset}`
11698
12382
  );
@@ -11724,7 +12408,7 @@ function hasMsvcBuildTools() {
11724
12408
  "Installer",
11725
12409
  "vswhere.exe"
11726
12410
  );
11727
- if (!existsSync22(vswhere)) return false;
12411
+ if (!existsSync23(vswhere)) return false;
11728
12412
  const res = spawnSync(
11729
12413
  vswhere,
11730
12414
  [
@@ -11756,7 +12440,7 @@ function requireMsvcOnWindows() {
11756
12440
  return false;
11757
12441
  }
11758
12442
  function warnIfCliMissing(repoRoot) {
11759
- if (!existsSync22(join22(repoRoot, "dist", "cli", "index.js"))) {
12443
+ if (!existsSync23(join22(repoRoot, "dist", "cli", "index.js"))) {
11760
12444
  console.warn(
11761
12445
  `${C3.yellow}\u26A0 CLI build not found (dist/cli/index.js). The GUI bridge needs it \u2014 run 'npm run build' at the repo root.${C3.reset}`
11762
12446
  );
@@ -11917,7 +12601,7 @@ ${C3.dim}After that, 'zam ui' launches it directly.${C3.reset}`
11917
12601
 
11918
12602
  // src/cli/commands/update.ts
11919
12603
  import { spawnSync as spawnSync2 } from "child_process";
11920
- import { existsSync as existsSync23, readFileSync as readFileSync13, realpathSync } from "fs";
12604
+ import { existsSync as existsSync24, readFileSync as readFileSync14, realpathSync } from "fs";
11921
12605
  import { dirname as dirname9, join as join23 } from "path";
11922
12606
  import { fileURLToPath as fileURLToPath4 } from "url";
11923
12607
  import { confirm as confirm4 } from "@inquirer/prompts";
@@ -11943,7 +12627,7 @@ function currentVersion() {
11943
12627
  for (const up of ["..", "../..", "../../.."]) {
11944
12628
  try {
11945
12629
  const pkg2 = JSON.parse(
11946
- readFileSync13(join23(here, up, "package.json"), "utf-8")
12630
+ readFileSync14(join23(here, up, "package.json"), "utf-8")
11947
12631
  );
11948
12632
  if (pkg2.version) return pkg2.version;
11949
12633
  } catch {
@@ -11954,7 +12638,7 @@ function currentVersion() {
11954
12638
  function versionAt(dir) {
11955
12639
  try {
11956
12640
  const pkg2 = JSON.parse(
11957
- readFileSync13(join23(dir, "package.json"), "utf-8")
12641
+ readFileSync14(join23(dir, "package.json"), "utf-8")
11958
12642
  );
11959
12643
  return pkg2.version ?? "unknown";
11960
12644
  } catch {
@@ -12034,11 +12718,11 @@ function findSourceRepo() {
12034
12718
  let dir = realpathSync(dirname9(fileURLToPath4(import.meta.url)));
12035
12719
  let parent = dirname9(dir);
12036
12720
  while (parent !== dir) {
12037
- if (existsSync23(join23(dir, ".git"))) return dir;
12721
+ if (existsSync24(join23(dir, ".git"))) return dir;
12038
12722
  dir = parent;
12039
12723
  parent = dirname9(dir);
12040
12724
  }
12041
- return existsSync23(join23(dir, ".git")) ? dir : null;
12725
+ return existsSync24(join23(dir, ".git")) ? dir : null;
12042
12726
  }
12043
12727
  function runGit2(cwd, args, capture) {
12044
12728
  const res = spawnSync2("git", args, {
@@ -12215,7 +12899,7 @@ var whoamiCommand = new Command24("whoami").description("Show or set the default
12215
12899
  // src/cli/index.ts
12216
12900
  var __dirname = dirname10(fileURLToPath5(import.meta.url));
12217
12901
  var pkg = JSON.parse(
12218
- readFileSync14(join24(__dirname, "..", "..", "package.json"), "utf-8")
12902
+ readFileSync15(join24(__dirname, "..", "..", "package.json"), "utf-8")
12219
12903
  );
12220
12904
  var program = new Command25();
12221
12905
  program.name("zam").description(