zam-core 0.4.4 → 0.5.1

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,57 @@ 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
+ }
4480
+ function removeConfiguredWorkspace(id, path = defaultConfigPath()) {
4481
+ const next = getConfiguredWorkspaces(path).filter(
4482
+ (workspace) => workspace.id !== id
4483
+ );
4484
+ saveConfiguredWorkspaces(next, path);
4485
+ return next;
4486
+ }
4453
4487
  function detectSyncProvider(dir) {
4454
4488
  const p = dir.toLowerCase();
4455
4489
  if (p.includes("onedrive")) return "OneDrive";
@@ -5031,7 +5065,7 @@ function isItermRunning() {
5031
5065
  return false;
5032
5066
  }
5033
5067
  }
5034
- function openMacTerminal(shellSetup, label, dir) {
5068
+ function openMacTerminal(shellSetup, label, dir, silent) {
5035
5069
  const useIterm = isItermRunning();
5036
5070
  const escaped = shellSetup.replace(/\\/g, "\\\\").replace(/"/g, '\\"');
5037
5071
  const appleScript = useIterm ? `tell application "iTerm2"
@@ -5048,16 +5082,20 @@ end tell`;
5048
5082
  try {
5049
5083
  writeFileSync6(tmpFile, appleScript);
5050
5084
  execSync4(`osascript ${JSON.stringify(tmpFile)}`, { stdio: "ignore" });
5051
- console.log(
5052
- `Opened ${useIterm ? "iTerm2" : "Terminal.app"} window (${label})`
5053
- );
5054
- console.log(` Directory: ${dir}`);
5085
+ if (!silent) {
5086
+ console.log(
5087
+ `Opened ${useIterm ? "iTerm2" : "Terminal.app"} window (${label})`
5088
+ );
5089
+ console.log(` Directory: ${dir}`);
5090
+ }
5055
5091
  } catch (err) {
5056
- console.error(`Failed to open terminal: ${err.message}`);
5057
- console.log(`
5092
+ if (!silent) {
5093
+ console.error(`Failed to open terminal: ${err.message}`);
5094
+ console.log(`
5058
5095
  Run this manually in a new terminal:
5059
5096
  `);
5060
- console.log(` ${shellSetup}`);
5097
+ console.log(` ${shellSetup}`);
5098
+ }
5061
5099
  } finally {
5062
5100
  try {
5063
5101
  unlinkSync(tmpFile);
@@ -5065,7 +5103,7 @@ Run this manually in a new terminal:
5065
5103
  }
5066
5104
  }
5067
5105
  }
5068
- function openWindowsPowerShell(shellSetup, label, dir, requestedShell) {
5106
+ function openWindowsPowerShell(shellSetup, label, dir, requestedShell, silent) {
5069
5107
  const requestedExecutable = requestedShell === "powershell" ? "powershell.exe" : "pwsh.exe";
5070
5108
  const executable = findExecutable(requestedExecutable) ? requestedExecutable : "powershell.exe";
5071
5109
  const startCommand = [
@@ -5082,35 +5120,42 @@ function openWindowsPowerShell(shellSetup, label, dir, requestedShell) {
5082
5120
  stdio: "ignore"
5083
5121
  }
5084
5122
  );
5085
- console.log(
5086
- `Opened ${executable === "pwsh.exe" ? "PowerShell" : "Windows PowerShell"} window (${label})`
5087
- );
5088
- console.log(` Directory: ${dir}`);
5123
+ if (!silent) {
5124
+ console.log(
5125
+ `Opened ${executable === "pwsh.exe" ? "PowerShell" : "Windows PowerShell"} window (${label})`
5126
+ );
5127
+ console.log(` Directory: ${dir}`);
5128
+ }
5089
5129
  } catch (err) {
5090
- console.error(`Failed to open PowerShell: ${err.message}`);
5091
- console.log(`
5130
+ if (!silent) {
5131
+ console.error(`Failed to open PowerShell: ${err.message}`);
5132
+ console.log(`
5092
5133
  Run this manually in a new PowerShell terminal:
5093
5134
  `);
5094
- console.log(` ${shellSetup}`);
5135
+ console.log(` ${shellSetup}`);
5136
+ }
5095
5137
  }
5096
5138
  }
5097
5139
  function openTerminalWindow(opts) {
5098
- const { shellSetup, label, dir, shell } = opts;
5099
- if (process.platform === "darwin" && !isPowerShellShell(shell)) {
5100
- openMacTerminal(shellSetup, label, dir);
5140
+ const { shellSetup, label, dir, shell, silent = false } = opts;
5141
+ const platform = opts.platform ?? process.platform;
5142
+ if (platform === "darwin" && !isPowerShellShell(shell)) {
5143
+ openMacTerminal(shellSetup, label, dir, silent);
5101
5144
  return;
5102
5145
  }
5103
- if (process.platform === "win32" && isPowerShellShell(shell)) {
5104
- openWindowsPowerShell(shellSetup, label, dir, shell);
5146
+ if (platform === "win32" && isPowerShellShell(shell)) {
5147
+ openWindowsPowerShell(shellSetup, label, dir, shell, silent);
5105
5148
  return;
5106
5149
  }
5107
- console.log(`Run this in a new terminal:
5150
+ if (!silent) {
5151
+ console.log(`Run this in a new terminal:
5108
5152
  `);
5109
- console.log(` ${shellSetup}
5153
+ console.log(` ${shellSetup}
5110
5154
  `);
5111
- console.log(
5112
- `(Automatic terminal opening is only supported on macOS Terminal/iTerm2 and Windows PowerShell for now.)`
5113
- );
5155
+ console.log(
5156
+ `(Automatic terminal opening is only supported on macOS Terminal/iTerm2 and Windows PowerShell for now.)`
5157
+ );
5158
+ }
5114
5159
  }
5115
5160
 
5116
5161
  // src/cli/agent-harness.ts
@@ -5176,7 +5221,9 @@ function launchHarness(harness, opts) {
5176
5221
  shellSetup: plan.shellSetup,
5177
5222
  label: `agent-${harness.id}`,
5178
5223
  dir: opts.workspace,
5179
- shell: plan.shell
5224
+ shell: plan.shell,
5225
+ silent: opts.silent,
5226
+ platform: opts.platform
5180
5227
  });
5181
5228
  return;
5182
5229
  }
@@ -5185,7 +5232,9 @@ function launchHarness(harness, opts) {
5185
5232
  stdio: "ignore",
5186
5233
  windowsHide: true
5187
5234
  }).unref();
5188
- console.log(`Launched ${harness.label} in ${opts.workspace}`);
5235
+ if (!opts.silent) {
5236
+ console.log(`Launched ${harness.label} in ${opts.workspace}`);
5237
+ }
5189
5238
  }
5190
5239
 
5191
5240
  // src/cli/commands/agent.ts
@@ -5310,10 +5359,16 @@ var agentCommand = new Command("agent").description("Provision and inspect the a
5310
5359
  // src/cli/commands/bridge.ts
5311
5360
  import { execFileSync as execFileSync4 } from "child_process";
5312
5361
  import { randomBytes as randomBytes2 } from "crypto";
5313
- import { readdirSync as readdirSync2, readFileSync as readFileSync10, rmSync as rmSync2 } from "fs";
5362
+ import {
5363
+ existsSync as existsSync17,
5364
+ mkdirSync as mkdirSync10,
5365
+ readdirSync as readdirSync3,
5366
+ readFileSync as readFileSync11,
5367
+ rmSync as rmSync3
5368
+ } from "fs";
5314
5369
  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";
5370
+ import { basename as basename5, join as join17, resolve as resolve5 } from "path";
5371
+ import { Command as Command4 } from "commander";
5317
5372
 
5318
5373
  // src/cli/llm/client.ts
5319
5374
  import { spawn as spawn2 } from "child_process";
@@ -5379,14 +5434,44 @@ async function getProviderForRole(db, role) {
5379
5434
  const providers = await readJsonSetting(db, "llm.providers");
5380
5435
  const roles = await readJsonSetting(db, "llm.roles");
5381
5436
  const binding = roles?.[role];
5437
+ let resolved = base;
5382
5438
  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;
5439
+ const primary = materializeProvider(
5440
+ providers[binding.primary],
5441
+ base,
5442
+ role,
5443
+ {
5444
+ providerName: binding.primary,
5445
+ source: "shared"
5446
+ }
5447
+ );
5448
+ const fallback = binding.fallback && providers[binding.fallback] ? materializeProvider(providers[binding.fallback], base, role, {
5449
+ providerName: binding.fallback,
5450
+ source: "shared"
5451
+ }) : void 0;
5452
+ resolved = { ...primary, fallback };
5453
+ }
5454
+ const machine = getMachineAiConfig();
5455
+ const machineProviders = machine.providers;
5456
+ const machineBinding = machine.roles?.[role];
5457
+ if (machineProviders && machineBinding?.primary && machineProviders[machineBinding.primary]) {
5458
+ const primary = materializeProvider(
5459
+ machineProviders[machineBinding.primary],
5460
+ resolved,
5461
+ role,
5462
+ { providerName: machineBinding.primary, source: "machine" }
5463
+ );
5464
+ const fallback = machineBinding.fallback && machineProviders[machineBinding.fallback] ? materializeProvider(
5465
+ machineProviders[machineBinding.fallback],
5466
+ resolved,
5467
+ role,
5468
+ { providerName: machineBinding.fallback, source: "machine" }
5469
+ ) : void 0;
5385
5470
  return { ...primary, fallback };
5386
5471
  }
5387
- return base;
5472
+ return resolved;
5388
5473
  }
5389
- function materializeProvider(rec, base, role) {
5474
+ function materializeProvider(rec, base, role, meta) {
5390
5475
  const url = rec.url || base.url;
5391
5476
  return {
5392
5477
  enabled: base.enabled,
@@ -5395,6 +5480,10 @@ function materializeProvider(rec, base, role) {
5395
5480
  apiKey: resolveProviderApiKey(rec),
5396
5481
  apiFlavor: rec.apiFlavor || inferApiFlavor(url),
5397
5482
  locale: base.locale,
5483
+ providerName: meta.providerName,
5484
+ label: rec.label,
5485
+ source: meta.source,
5486
+ local: rec.local ?? isLocalEndpoint(url),
5398
5487
  ...role === "vision" ? { maxFrames: base.maxFrames } : {}
5399
5488
  };
5400
5489
  }
@@ -5413,6 +5502,8 @@ async function getLegacyRoleConfig(db, role, enabled) {
5413
5502
  apiKey: await getSetting(db, "llm.vision.api_key") || base.apiKey,
5414
5503
  apiFlavor: inferApiFlavor(url),
5415
5504
  locale: base.locale,
5505
+ source: "legacy",
5506
+ local: isLocalEndpoint(url),
5416
5507
  maxFrames: Number.isNaN(parsed) ? 100 : parsed
5417
5508
  };
5418
5509
  }
@@ -5422,7 +5513,9 @@ async function getLegacyRoleConfig(db, role, enabled) {
5422
5513
  model: base.model,
5423
5514
  apiKey: base.apiKey,
5424
5515
  apiFlavor: inferApiFlavor(base.url),
5425
- locale: base.locale
5516
+ locale: base.locale,
5517
+ source: "legacy",
5518
+ local: isLocalEndpoint(base.url)
5426
5519
  };
5427
5520
  }
5428
5521
  function assertChatCompletions(cfg) {
@@ -5672,10 +5765,17 @@ async function isVisionProviderModelExplicit(db, active) {
5672
5765
  const providers = await readJsonSetting(db, "llm.providers");
5673
5766
  const roles = await readJsonSetting(db, "llm.roles");
5674
5767
  const binding = roles?.vision;
5675
- if (!providers || !binding) return false;
5676
- return [binding.primary, binding.fallback].some((id) => {
5768
+ const sharedExplicit = [binding?.primary, binding?.fallback].some((id) => {
5769
+ if (!id) return false;
5770
+ const rec = providers?.[id];
5771
+ return rec?.model === active.model && (rec.url === void 0 || rec.url === active.url);
5772
+ });
5773
+ if (sharedExplicit) return true;
5774
+ const machine = getMachineAiConfig();
5775
+ const machineBinding = machine.roles?.vision;
5776
+ return [machineBinding?.primary, machineBinding?.fallback].some((id) => {
5677
5777
  if (!id) return false;
5678
- const rec = providers[id];
5778
+ const rec = machine.providers?.[id];
5679
5779
  return rec?.model === active.model && (rec.url === void 0 || rec.url === active.url);
5680
5780
  });
5681
5781
  }
@@ -5708,6 +5808,87 @@ async function checkVisionReadiness(db) {
5708
5808
  warning
5709
5809
  };
5710
5810
  }
5811
+ function summarizeFallback(provider) {
5812
+ if (!provider) return void 0;
5813
+ return {
5814
+ providerName: provider.providerName,
5815
+ label: provider.label,
5816
+ source: provider.source,
5817
+ url: provider.url,
5818
+ model: provider.model,
5819
+ apiFlavor: provider.apiFlavor,
5820
+ local: provider.local
5821
+ };
5822
+ }
5823
+ async function getProviderRoleStatus(db, role) {
5824
+ const cfg = await getProviderForRole(db, role);
5825
+ const unsupportedProvider = role !== "vision" && cfg.apiFlavor !== "chat-completions";
5826
+ if (!cfg.enabled) {
5827
+ return {
5828
+ role,
5829
+ enabled: false,
5830
+ providerName: cfg.providerName,
5831
+ label: cfg.label,
5832
+ source: cfg.source,
5833
+ url: cfg.url,
5834
+ model: cfg.model,
5835
+ apiFlavor: cfg.apiFlavor,
5836
+ local: cfg.local,
5837
+ online: false,
5838
+ modelAvailable: false,
5839
+ availableModels: [],
5840
+ usable: false,
5841
+ reason: "disabled",
5842
+ fallback: summarizeFallback(cfg.fallback)
5843
+ };
5844
+ }
5845
+ if (unsupportedProvider) {
5846
+ return {
5847
+ role,
5848
+ enabled: true,
5849
+ providerName: cfg.providerName,
5850
+ label: cfg.label,
5851
+ source: cfg.source,
5852
+ url: cfg.url,
5853
+ model: cfg.model,
5854
+ apiFlavor: cfg.apiFlavor,
5855
+ local: cfg.local,
5856
+ online: false,
5857
+ modelAvailable: false,
5858
+ availableModels: [],
5859
+ usable: false,
5860
+ reason: "unsupported-provider",
5861
+ fallback: summarizeFallback(cfg.fallback)
5862
+ };
5863
+ }
5864
+ let selected;
5865
+ if (role === "vision") {
5866
+ const chain = await checkProviderChain(cfg);
5867
+ selected = chain.firstUsable ?? chain.primary;
5868
+ } else {
5869
+ selected = await checkProviderEndpoint(cfg);
5870
+ }
5871
+ const active = selected.endpoint;
5872
+ const usable = selected.online && selected.modelAvailable;
5873
+ const reason = usable ? void 0 : selected.online ? "model-not-found" : "offline";
5874
+ return {
5875
+ role,
5876
+ enabled: true,
5877
+ providerName: active.providerName,
5878
+ label: active.label,
5879
+ source: active.source,
5880
+ url: active.url,
5881
+ model: active.model,
5882
+ apiFlavor: active.apiFlavor,
5883
+ local: active.local,
5884
+ online: selected.online,
5885
+ modelAvailable: selected.modelAvailable,
5886
+ availableModels: selected.availableModels,
5887
+ usable,
5888
+ reason,
5889
+ fallback: summarizeFallback(cfg.fallback)
5890
+ };
5891
+ }
5711
5892
  function detectRunner(url, model) {
5712
5893
  let runner = "unknown";
5713
5894
  let port = "8000";
@@ -5806,7 +5987,7 @@ async function startLocalRunner(url, model, locale) {
5806
5987
  let attempts = 0;
5807
5988
  const dotsPerLine = 30;
5808
5989
  while (true) {
5809
- await new Promise((resolve6) => setTimeout(resolve6, 500));
5990
+ await new Promise((resolve9) => setTimeout(resolve9, 500));
5810
5991
  if (await isLlmOnline(url)) {
5811
5992
  if (attempts > 0) process.stdout.write("\n");
5812
5993
  return true;
@@ -5961,8 +6142,8 @@ async function fetchWithInteractiveTimeout(url, options = {}) {
5961
6142
  const dotsPerLine = 30;
5962
6143
  while (true) {
5963
6144
  let timeoutId;
5964
- const timeoutPromise = new Promise((resolve6) => {
5965
- timeoutId = setTimeout(() => resolve6("timeout"), timeoutMs);
6145
+ const timeoutPromise = new Promise((resolve9) => {
6146
+ timeoutId = setTimeout(() => resolve9("timeout"), timeoutMs);
5966
6147
  });
5967
6148
  const dotsInterval = setInterval(() => {
5968
6149
  process.stdout.write(".");
@@ -6072,25 +6253,25 @@ async function observeUiSnapshotViaLLM(db, input8) {
6072
6253
  const imageUrls = [];
6073
6254
  const isVideo = /\.(mp4|mov|m4v|avi|mkv|webm)$/i.test(input8.imagePath);
6074
6255
  if (isVideo) {
6075
- const { mkdirSync: mkdirSync13, readdirSync: readdirSync3, rmSync: rmSync3 } = await import("fs");
6256
+ const { mkdirSync: mkdirSync14, readdirSync: readdirSync4, rmSync: rmSync4 } = await import("fs");
6076
6257
  const { execSync: execSync6 } = await import("child_process");
6077
6258
  const tempDir = join14(
6078
6259
  tmpdir2(),
6079
6260
  `zam-frames-${randomBytes(4).toString("hex")}`
6080
6261
  );
6081
- mkdirSync13(tempDir, { recursive: true });
6262
+ mkdirSync14(tempDir, { recursive: true });
6082
6263
  try {
6083
6264
  execSync6(
6084
6265
  `ffmpeg -i "${input8.imagePath}" -vf "fps=1/3,scale=1280:-1" -vsync vfr "${tempDir}/frame_%03d.png"`,
6085
6266
  { stdio: "ignore" }
6086
6267
  );
6087
- let files = readdirSync3(tempDir).filter((f) => f.endsWith(".png")).sort();
6268
+ let files = readdirSync4(tempDir).filter((f) => f.endsWith(".png")).sort();
6088
6269
  if (files.length === 0) {
6089
6270
  execSync6(
6090
6271
  `ffmpeg -i "${input8.imagePath}" -vframes 1 "${tempDir}/frame_001.png"`,
6091
6272
  { stdio: "ignore" }
6092
6273
  );
6093
- files = readdirSync3(tempDir).filter((f) => f.endsWith(".png")).sort();
6274
+ files = readdirSync4(tempDir).filter((f) => f.endsWith(".png")).sort();
6094
6275
  }
6095
6276
  const maxFrames = cfg.maxFrames ?? 100;
6096
6277
  let sampledFiles = files;
@@ -6112,7 +6293,7 @@ async function observeUiSnapshotViaLLM(db, input8) {
6112
6293
  }
6113
6294
  } finally {
6114
6295
  try {
6115
- rmSync3(tempDir, { recursive: true, force: true });
6296
+ rmSync4(tempDir, { recursive: true, force: true });
6116
6297
  } catch {
6117
6298
  }
6118
6299
  }
@@ -6467,6 +6648,419 @@ async function resolveUser(opts, db, resolveOpts) {
6467
6648
  process.exit(1);
6468
6649
  }
6469
6650
 
6651
+ // src/cli/commands/setup.ts
6652
+ import {
6653
+ existsSync as existsSync15,
6654
+ lstatSync,
6655
+ mkdirSync as mkdirSync8,
6656
+ readdirSync as readdirSync2,
6657
+ readFileSync as readFileSync10,
6658
+ realpathSync,
6659
+ rmSync as rmSync2,
6660
+ symlinkSync,
6661
+ writeFileSync as writeFileSync7
6662
+ } from "fs";
6663
+ import { basename as basename4, dirname as dirname5, join as join15, resolve as resolve3 } from "path";
6664
+ import { fileURLToPath as fileURLToPath2 } from "url";
6665
+ import { Command as Command2 } from "commander";
6666
+ var packageRoot = [
6667
+ fileURLToPath2(new URL("../..", import.meta.url)),
6668
+ fileURLToPath2(new URL("../../..", import.meta.url))
6669
+ ].find((candidate) => existsSync15(join15(candidate, "package.json"))) ?? fileURLToPath2(new URL("../..", import.meta.url));
6670
+ var ALL_SETUP_AGENTS = ["claude", "copilot", "codex", "agent"];
6671
+ var SKILL_PAIRS = [
6672
+ {
6673
+ from: join15(packageRoot, ".claude", "skills", "zam", "SKILL.md"),
6674
+ to: join15(".claude", "skills", "zam", "SKILL.md"),
6675
+ agents: ["claude", "copilot"]
6676
+ },
6677
+ {
6678
+ from: join15(packageRoot, ".agent", "skills", "zam", "SKILL.md"),
6679
+ to: join15(".agent", "skills", "zam", "SKILL.md"),
6680
+ agents: ["agent"]
6681
+ },
6682
+ {
6683
+ from: join15(packageRoot, ".agents", "skills", "zam", "SKILL.md"),
6684
+ to: join15(".agents", "skills", "zam", "SKILL.md"),
6685
+ agents: ["codex"]
6686
+ }
6687
+ ];
6688
+ function parseSetupAgents(value) {
6689
+ if (!value || value.trim().toLowerCase() === "all") {
6690
+ return new Set(ALL_SETUP_AGENTS);
6691
+ }
6692
+ const aliases = {
6693
+ claude: ["claude"],
6694
+ copilot: ["copilot"],
6695
+ codex: ["codex"],
6696
+ agent: ["agent"],
6697
+ opencode: ["agent"]
6698
+ };
6699
+ const selected = /* @__PURE__ */ new Set();
6700
+ for (const raw of value.split(",")) {
6701
+ const key = raw.trim().toLowerCase();
6702
+ const mapped = aliases[key];
6703
+ if (!mapped) {
6704
+ throw new Error(
6705
+ `Unknown agent "${raw}". Use: all, claude, copilot, codex, agent.`
6706
+ );
6707
+ }
6708
+ for (const item of mapped) selected.add(item);
6709
+ }
6710
+ return selected;
6711
+ }
6712
+ function pathExists(path) {
6713
+ try {
6714
+ lstatSync(path);
6715
+ return true;
6716
+ } catch {
6717
+ return false;
6718
+ }
6719
+ }
6720
+ function comparableRealPath(path) {
6721
+ const real = realpathSync(path);
6722
+ return process.platform === "win32" ? real.toLowerCase() : real;
6723
+ }
6724
+ function pathsResolveToSameDirectory(sourceDir, destinationDir) {
6725
+ try {
6726
+ return comparableRealPath(destinationDir) === comparableRealPath(sourceDir);
6727
+ } catch {
6728
+ return false;
6729
+ }
6730
+ }
6731
+ function linkPointsTo(sourceDir, destinationDir) {
6732
+ try {
6733
+ return lstatSync(destinationDir).isSymbolicLink() && pathsResolveToSameDirectory(sourceDir, destinationDir);
6734
+ } catch {
6735
+ return false;
6736
+ }
6737
+ }
6738
+ function isReplaceableCopiedSkill(destinationDir) {
6739
+ try {
6740
+ if (!lstatSync(destinationDir).isDirectory()) return false;
6741
+ const entries = readdirSync2(destinationDir);
6742
+ if (entries.length !== 1 || entries[0] !== "SKILL.md") return false;
6743
+ return /^name:\s*zam\s*$/m.test(
6744
+ readFileSync10(join15(destinationDir, "SKILL.md"), "utf8")
6745
+ );
6746
+ } catch {
6747
+ return false;
6748
+ }
6749
+ }
6750
+ function wireSkills(cwd = process.cwd(), agents = parseSetupAgents(), opts = {}) {
6751
+ const results = [];
6752
+ const log = (message) => {
6753
+ if (!opts.quiet) console.log(message);
6754
+ };
6755
+ for (const { from, to, agents: pairAgents } of SKILL_PAIRS) {
6756
+ if (!pairAgents.some((agent) => agents.has(agent))) continue;
6757
+ const sourceDir = dirname5(from);
6758
+ const destinationDir = dirname5(join15(cwd, to));
6759
+ if (!existsSync15(sourceDir)) {
6760
+ if (!opts.quiet) {
6761
+ console.warn(` warn source not found, skipping: ${sourceDir}`);
6762
+ }
6763
+ results.push({
6764
+ source: sourceDir,
6765
+ destination: destinationDir,
6766
+ action: "skipped",
6767
+ reason: "source-not-found"
6768
+ });
6769
+ continue;
6770
+ }
6771
+ if (pathsResolveToSameDirectory(sourceDir, destinationDir) && !lstatSync(destinationDir).isSymbolicLink()) {
6772
+ log(` skip ${dirname5(to)} (package source)`);
6773
+ results.push({
6774
+ source: sourceDir,
6775
+ destination: destinationDir,
6776
+ action: "skipped",
6777
+ reason: "source-directory"
6778
+ });
6779
+ continue;
6780
+ }
6781
+ if (linkPointsTo(sourceDir, destinationDir)) {
6782
+ log(` skip ${dirname5(to)} (already linked)`);
6783
+ results.push({
6784
+ source: sourceDir,
6785
+ destination: destinationDir,
6786
+ action: "skipped",
6787
+ reason: "already-linked"
6788
+ });
6789
+ continue;
6790
+ }
6791
+ const destinationExists = pathExists(destinationDir);
6792
+ const replaceExisting = destinationExists && (Boolean(opts.force) || isReplaceableCopiedSkill(destinationDir));
6793
+ if (destinationExists && !replaceExisting) {
6794
+ if (!opts.quiet) {
6795
+ console.warn(
6796
+ ` warn ${dirname5(to)} already exists and is not managed by ZAM; use --force to replace it`
6797
+ );
6798
+ }
6799
+ results.push({
6800
+ source: sourceDir,
6801
+ destination: destinationDir,
6802
+ action: "skipped",
6803
+ reason: "unmanaged-destination"
6804
+ });
6805
+ continue;
6806
+ }
6807
+ const action = destinationExists ? "relinked" : "linked";
6808
+ if (opts.dryRun) {
6809
+ log(` would ${action === "linked" ? "link" : "relink"} ${dirname5(to)}`);
6810
+ } else {
6811
+ if (destinationExists) {
6812
+ rmSync2(destinationDir, { recursive: true, force: true });
6813
+ }
6814
+ mkdirSync8(dirname5(destinationDir), { recursive: true });
6815
+ symlinkSync(
6816
+ sourceDir,
6817
+ destinationDir,
6818
+ process.platform === "win32" ? "junction" : "dir"
6819
+ );
6820
+ log(` ${action === "linked" ? "link" : "relink"} ${dirname5(to)}`);
6821
+ }
6822
+ results.push({ source: sourceDir, destination: destinationDir, action });
6823
+ }
6824
+ return results;
6825
+ }
6826
+ function formatDatabaseInitTarget(target) {
6827
+ switch (target.kind) {
6828
+ case "local":
6829
+ return `ZAM database at ${target.location} (local SQLite)`;
6830
+ case "turso-remote":
6831
+ return `ZAM database via Turso remote at ${target.location}`;
6832
+ case "turso-native":
6833
+ return `ZAM database via Turso native driver at ${target.location}`;
6834
+ case "turso-replica":
6835
+ return `ZAM database replica at ${target.location} syncing from ${target.syncUrl}`;
6836
+ }
6837
+ }
6838
+ async function initDatabase(skipInit) {
6839
+ if (skipInit) return;
6840
+ try {
6841
+ const target = getDatabaseTargetInfo();
6842
+ const db = await openDatabaseWithSync({ initialize: true });
6843
+ await db.close();
6844
+ console.log(` init ${formatDatabaseInitTarget(target)}`);
6845
+ } catch (err) {
6846
+ const msg = err.message;
6847
+ if (!msg.includes("already")) {
6848
+ console.warn(` warn database init: ${msg}`);
6849
+ } else {
6850
+ console.log(` skip database already initialized`);
6851
+ }
6852
+ }
6853
+ }
6854
+ var ZAM_BLOCK_START = "<!-- ZAM:START -->";
6855
+ var ZAM_BLOCK_END = "<!-- ZAM:END -->";
6856
+ function upsertMarkedBlock(dest, blockBody, dryRun) {
6857
+ const block = `${ZAM_BLOCK_START}
6858
+ ${blockBody.trim()}
6859
+ ${ZAM_BLOCK_END}`;
6860
+ if (!existsSync15(dest)) {
6861
+ if (!dryRun) {
6862
+ mkdirSync8(dirname5(dest), { recursive: true });
6863
+ writeFileSync7(dest, `${block}
6864
+ `, "utf8");
6865
+ }
6866
+ return "write";
6867
+ }
6868
+ const existing = readFileSync10(dest, "utf8");
6869
+ if (existing.includes(block)) return "skip";
6870
+ const start = existing.indexOf(ZAM_BLOCK_START);
6871
+ const end = existing.indexOf(ZAM_BLOCK_END);
6872
+ const next = start >= 0 && end > start ? `${existing.slice(0, start)}${block}${existing.slice(end + ZAM_BLOCK_END.length)}` : `${existing.trimEnd()}
6873
+
6874
+ ${block}
6875
+ `;
6876
+ if (!dryRun) writeFileSync7(dest, next, "utf8");
6877
+ return start >= 0 && end > start ? "update" : "write";
6878
+ }
6879
+ function logInstructionAction(action, label, dryRun) {
6880
+ if (action === "skip") {
6881
+ console.log(` skip ${label} (ZAM block already present)`);
6882
+ } else {
6883
+ console.log(` ${dryRun ? "would " : ""}${action} ${label}`);
6884
+ }
6885
+ }
6886
+ function writeClaudeMd(skipClaudeMd, cwd = process.cwd(), opts = {}) {
6887
+ if (skipClaudeMd) return;
6888
+ const dest = join15(cwd, "CLAUDE.md");
6889
+ if (existsSync15(dest)) {
6890
+ if (!opts.updateExisting) {
6891
+ console.log(` skip CLAUDE.md (already present)`);
6892
+ return;
6893
+ }
6894
+ const action = upsertMarkedBlock(
6895
+ dest,
6896
+ `## ZAM learning sessions
6897
+
6898
+ 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.
6899
+
6900
+ - Skill files live under \`.claude/skills/zam/\`.
6901
+ - Fast-changing review data lives in \`~/.zam/\`, not in this repository.
6902
+ - The skill directory is linked to this ZAM installation and updates with it.`,
6903
+ Boolean(opts.dryRun)
6904
+ );
6905
+ logInstructionAction(action, "CLAUDE.md", Boolean(opts.dryRun));
6906
+ return;
6907
+ }
6908
+ const name = basename4(cwd);
6909
+ const content = `# ZAM Personal Kernel \xE2\u20AC\u201D ${name}
6910
+
6911
+ This is a ZAM personal instance. ZAM builds lasting skills through spaced
6912
+ repetition during real work \xE2\u20AC\u201D not separate study sessions.
6913
+
6914
+ ## First time here?
6915
+ Run \`/setup\` in Claude Code or Gemini CLI to complete first-time setup.
6916
+
6917
+ ## Regular use
6918
+ Run \`/zam\` to start a learning session on whatever you are working on.
6919
+
6920
+ ## What lives here
6921
+ - \`beliefs/\` \xE2\u20AC\u201D your worldview, approved by git commit
6922
+ - \`goals/\` \xE2\u20AC\u201D your objectives, decomposed into tasks and learning tokens
6923
+
6924
+ ## Fast-changing data
6925
+ Learning tokens, cards, and review history live in local SQLite by default.
6926
+ Use \`zam connector setup turso\` to store cloud credentials in
6927
+ \`~/.zam/credentials.json\` and use a Turso database across machines.
6928
+ `;
6929
+ if (opts.dryRun) {
6930
+ console.log(` would write CLAUDE.md`);
6931
+ } else {
6932
+ writeFileSync7(dest, content, "utf8");
6933
+ console.log(` write CLAUDE.md`);
6934
+ }
6935
+ }
6936
+ function writeAgentsMd(skipAgentsMd, cwd = process.cwd(), opts = {}) {
6937
+ if (skipAgentsMd) return;
6938
+ const dest = join15(cwd, "AGENTS.md");
6939
+ if (existsSync15(dest)) {
6940
+ if (!opts.updateExisting) {
6941
+ console.log(` skip AGENTS.md (already present)`);
6942
+ return;
6943
+ }
6944
+ const action = upsertMarkedBlock(
6945
+ dest,
6946
+ `## ZAM learning sessions
6947
+
6948
+ 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.
6949
+
6950
+ - Skill files live under \`.agents/skills/zam/\`.
6951
+ - Fast-changing review data lives in \`~/.zam/\`, not in this repository.
6952
+ - The skill directory is linked to this ZAM installation and updates with it.`,
6953
+ Boolean(opts.dryRun)
6954
+ );
6955
+ logInstructionAction(action, "AGENTS.md", Boolean(opts.dryRun));
6956
+ return;
6957
+ }
6958
+ const name = basename4(cwd);
6959
+ const content = `# ZAM Personal Kernel - ${name}
6960
+
6961
+ This is a ZAM personal instance. ZAM builds lasting skills through spaced
6962
+ repetition during real work, not separate study sessions.
6963
+
6964
+ ## First time here?
6965
+ Run \`zam setup\` from the shell. When this repository includes
6966
+ \`.agents/skills/setup/\`, you can instead select \`setup\` through \`/skills\`
6967
+ or invoke \`$setup\`.
6968
+
6969
+ ## Regular use
6970
+ Select the \`zam\` skill through \`/skills\` or invoke \`$zam\` to start a
6971
+ learning session on whatever you are working on.
6972
+
6973
+ ## What lives here
6974
+ - \`beliefs/\` - your worldview, approved by git commit
6975
+ - \`goals/\` - your objectives, decomposed into tasks and learning tokens
6976
+
6977
+ ## Fast-changing data
6978
+ Learning tokens, cards, and review history live in local SQLite by default.
6979
+ Use \`zam connector setup turso\` to store cloud credentials in
6980
+ \`~/.zam/credentials.json\` and use a Turso database across machines.
6981
+
6982
+ ## Codex skills
6983
+ Codex discovers repository skills under \`.agents/skills/\`. Run
6984
+ \`zam setup --force\` after upgrading \`zam-core\` to refresh them.
6985
+ `;
6986
+ if (opts.dryRun) {
6987
+ console.log(` would write AGENTS.md`);
6988
+ } else {
6989
+ writeFileSync7(dest, content, "utf8");
6990
+ console.log(` write AGENTS.md`);
6991
+ }
6992
+ }
6993
+ function writeCopilotInstructions(cwd = process.cwd(), opts = {}) {
6994
+ const dest = join15(cwd, ".github", "copilot-instructions.md");
6995
+ const action = upsertMarkedBlock(
6996
+ dest,
6997
+ `## ZAM learning sessions
6998
+
6999
+ ZAM is available in this repository through \`.claude/skills/zam/\`. Use the \`zam\` project skill from Copilot-compatible skill selection surfaces to turn real work into an observed learning session with active recall and FSRS scheduling.
7000
+
7001
+ - Fast-changing review data lives in \`~/.zam/\`, not in this repository.
7002
+ - The skill directory is linked to this ZAM installation and updates with it.`,
7003
+ Boolean(opts.dryRun)
7004
+ );
7005
+ logInstructionAction(
7006
+ action,
7007
+ ".github/copilot-instructions.md",
7008
+ Boolean(opts.dryRun)
7009
+ );
7010
+ }
7011
+ var setupCommand = new Command2("setup").description(
7012
+ "Link ZAM skill directories into this workspace and initialize the database"
7013
+ ).option("--force", "replace an unmanaged existing ZAM skill directory", false).option("--skip-init", "skip database initialization", false).option("--skip-claude-md", "skip CLAUDE.md generation", false).option("--skip-agents-md", "skip AGENTS.md generation", false).option("--target <path>", "repository/workspace directory to set up").option(
7014
+ "--agents <list>",
7015
+ "comma-separated agents to wire: all, claude, copilot, codex, agent"
7016
+ ).option(
7017
+ "--dry-run",
7018
+ "show what would be written without changing files",
7019
+ false
7020
+ ).action(
7021
+ async (opts) => {
7022
+ let agents;
7023
+ try {
7024
+ agents = parseSetupAgents(opts.agents);
7025
+ } catch (err) {
7026
+ console.error(`Error: ${err.message}`);
7027
+ process.exit(1);
7028
+ }
7029
+ const target = resolve3(opts.target ?? process.cwd());
7030
+ const updateExistingInstructions = Boolean(opts.target) || opts.force;
7031
+ console.log(
7032
+ `Setting up ZAM in ${target}${opts.dryRun ? " (dry run)" : ""}
7033
+ `
7034
+ );
7035
+ wireSkills(target, agents, {
7036
+ force: opts.force,
7037
+ dryRun: opts.dryRun
7038
+ });
7039
+ await initDatabase(opts.skipInit || opts.dryRun);
7040
+ if (agents.has("claude")) {
7041
+ writeClaudeMd(opts.skipClaudeMd, target, {
7042
+ dryRun: opts.dryRun,
7043
+ updateExisting: updateExistingInstructions
7044
+ });
7045
+ }
7046
+ if (agents.has("codex") || agents.has("agent")) {
7047
+ writeAgentsMd(opts.skipAgentsMd, target, {
7048
+ dryRun: opts.dryRun,
7049
+ updateExisting: updateExistingInstructions
7050
+ });
7051
+ }
7052
+ if (agents.has("copilot") && (opts.target || opts.agents)) {
7053
+ writeCopilotInstructions(target, {
7054
+ dryRun: opts.dryRun,
7055
+ updateExisting: updateExistingInstructions
7056
+ });
7057
+ }
7058
+ console.log(
7059
+ "\nDone. Run `zam whoami --set <your-id>` to set your identity. Start the `zam` skill with `/zam` in Claude/Copilot/Gemini-compatible clients or `$zam` (or `/skills`) in Codex."
7060
+ );
7061
+ }
7062
+ );
7063
+
6470
7064
  // src/cli/commands/shared/db.ts
6471
7065
  function defaultErrorHandler(message) {
6472
7066
  console.error("Error:", message);
@@ -6489,11 +7083,11 @@ function jsonOut(data) {
6489
7083
 
6490
7084
  // src/cli/commands/workspace.ts
6491
7085
  import { execFileSync as execFileSync3 } from "child_process";
6492
- import { existsSync as existsSync15, mkdirSync as mkdirSync8, writeFileSync as writeFileSync7 } from "fs";
7086
+ import { existsSync as existsSync16, mkdirSync as mkdirSync9, writeFileSync as writeFileSync8 } from "fs";
6493
7087
  import { homedir as homedir9 } from "os";
6494
- import { join as join15 } from "path";
7088
+ import { join as join16, resolve as resolve4 } from "path";
6495
7089
  import { confirm, input } from "@inquirer/prompts";
6496
- import { Command as Command2 } from "commander";
7090
+ import { Command as Command3 } from "commander";
6497
7091
  function runGit(cwd, args) {
6498
7092
  try {
6499
7093
  return execFileSync3("git", args, {
@@ -6502,18 +7096,179 @@ function runGit(cwd, args) {
6502
7096
  encoding: "utf8"
6503
7097
  }).trim();
6504
7098
  } catch (err) {
6505
- throw new Error(`Git command failed: ${err.message}`);
7099
+ throw new Error(`Git command failed: ${err.message}`);
7100
+ }
7101
+ }
7102
+ function ghRepoCreateArgs(repoName, repoVisibility) {
7103
+ return ["repo", "create", repoName, repoVisibility, "--source=.", "--push"];
7104
+ }
7105
+ function gitRemoteArgs(githubUrl, hasOrigin) {
7106
+ return hasOrigin ? ["remote", "set-url", "origin", githubUrl] : ["remote", "add", "origin", githubUrl];
7107
+ }
7108
+ var workspaceCommand = new Command3("workspace").description(
7109
+ "Manage your ZAM learning workspace"
7110
+ );
7111
+ var WORKSPACE_KINDS = [
7112
+ "personal",
7113
+ "team",
7114
+ "family",
7115
+ "community",
7116
+ "organization",
7117
+ "custom"
7118
+ ];
7119
+ var WORKSPACE_SOURCE_CONTROLS = [
7120
+ "github",
7121
+ "azure-devops",
7122
+ "git",
7123
+ "none"
7124
+ ];
7125
+ function parseWorkspaceKind(value) {
7126
+ const kind = (value ?? "custom").toLowerCase();
7127
+ if (WORKSPACE_KINDS.includes(kind)) {
7128
+ return kind;
7129
+ }
7130
+ throw new Error(
7131
+ `Invalid workspace kind: ${value}. Use ${WORKSPACE_KINDS.join(", ")}.`
7132
+ );
7133
+ }
7134
+ function parseWorkspaceSourceControl(value) {
7135
+ if (!value) return void 0;
7136
+ const source = value.toLowerCase();
7137
+ if (WORKSPACE_SOURCE_CONTROLS.includes(source)) {
7138
+ return source;
7139
+ }
7140
+ throw new Error(
7141
+ `Invalid source control: ${value}. Use ${WORKSPACE_SOURCE_CONTROLS.join(", ")}.`
7142
+ );
7143
+ }
7144
+ function parseScopes(value) {
7145
+ if (!value) return void 0;
7146
+ const scopes = value.split(",").map((item) => item.trim()).filter(Boolean);
7147
+ return scopes.length > 0 ? scopes : void 0;
7148
+ }
7149
+ function requireWorkspace(id) {
7150
+ const workspace = getConfiguredWorkspaces().find((item) => item.id === id);
7151
+ if (!workspace) {
7152
+ throw new Error(
7153
+ `Workspace "${id}" is not configured. Add it with: zam workspace add ${id} --path <dir>`
7154
+ );
7155
+ }
7156
+ return workspace;
7157
+ }
7158
+ workspaceCommand.command("list").description("List configured ZAM workspaces").option("--json", "Output as JSON").action((opts) => {
7159
+ const workspaces = getConfiguredWorkspaces();
7160
+ if (opts.json) {
7161
+ console.log(JSON.stringify({ workspaces }, null, 2));
7162
+ return;
7163
+ }
7164
+ console.log("Configured ZAM workspaces:\n");
7165
+ if (workspaces.length === 0) {
7166
+ console.log(
7167
+ " (none) \u2014 add one: zam workspace add personal --path <dir>"
7168
+ );
7169
+ return;
7170
+ }
7171
+ for (const workspace of workspaces) {
7172
+ const label = workspace.label ? ` (${workspace.label})` : "";
7173
+ console.log(` ${workspace.id}${label}`);
7174
+ console.log(` kind: ${workspace.kind}`);
7175
+ console.log(` path: ${workspace.path}`);
7176
+ if (workspace.sourceControl) {
7177
+ console.log(` source: ${workspace.sourceControl}`);
7178
+ }
7179
+ if (workspace.knowledgeScopes?.length) {
7180
+ console.log(` scopes: ${workspace.knowledgeScopes.join(", ")}`);
7181
+ }
7182
+ }
7183
+ });
7184
+ workspaceCommand.command("add <id>").description("Register an existing directory as a ZAM workspace").requiredOption("--path <dir>", "Existing workspace/repository directory").option(
7185
+ "--kind <kind>",
7186
+ `Workspace kind (${WORKSPACE_KINDS.join(" | ")})`,
7187
+ "custom"
7188
+ ).option("--label <label>", "Human-readable label").option(
7189
+ "--source-control <provider>",
7190
+ `Source-control provider (${WORKSPACE_SOURCE_CONTROLS.join(" | ")})`
7191
+ ).option("--scopes <list>", "Comma-separated knowledge scopes").option("--default-agent <id>", "Default agent harness for this workspace").action((id, opts) => {
7192
+ try {
7193
+ const path = resolve4(String(opts.path));
7194
+ if (!existsSync16(path)) {
7195
+ console.error(`Workspace path does not exist: ${path}`);
7196
+ process.exit(1);
7197
+ }
7198
+ const workspace = {
7199
+ id,
7200
+ kind: parseWorkspaceKind(opts.kind),
7201
+ path,
7202
+ ...opts.label ? { label: opts.label } : {},
7203
+ ...opts.sourceControl ? { sourceControl: parseWorkspaceSourceControl(opts.sourceControl) } : {},
7204
+ ...parseScopes(opts.scopes) ? { knowledgeScopes: parseScopes(opts.scopes) } : {},
7205
+ ...opts.defaultAgent ? { defaultAgent: opts.defaultAgent } : {}
7206
+ };
7207
+ wireSkills(path, parseSetupAgents());
7208
+ upsertConfiguredWorkspace(workspace);
7209
+ console.log(`Registered and linked workspace "${id}" at ${path}.`);
7210
+ } catch (err) {
7211
+ console.error(`Error: ${err.message}`);
7212
+ process.exit(1);
7213
+ }
7214
+ });
7215
+ workspaceCommand.command("remove <id>").description("Unregister a ZAM workspace without deleting its files").action((id) => {
7216
+ const existing = getConfiguredWorkspaces().find((item) => item.id === id);
7217
+ if (!existing) {
7218
+ console.error(`Workspace "${id}" is not configured.`);
7219
+ process.exit(1);
7220
+ }
7221
+ removeConfiguredWorkspace(id);
7222
+ console.log(
7223
+ `Unregistered workspace "${id}". Files in ${existing.path} were not changed.`
7224
+ );
7225
+ });
7226
+ workspaceCommand.command("setup <id>").description("Install ZAM skills into a configured workspace").option(
7227
+ "--agents <list>",
7228
+ "comma-separated agents to wire: all, claude, copilot, codex, agent"
7229
+ ).option(
7230
+ "--force",
7231
+ "overwrite existing skill files and refresh ZAM blocks",
7232
+ false
7233
+ ).option(
7234
+ "--dry-run",
7235
+ "show what would be written without changing files",
7236
+ false
7237
+ ).action((id, opts) => {
7238
+ try {
7239
+ const workspace = requireWorkspace(id);
7240
+ const agents = parseSetupAgents(opts.agents);
7241
+ console.log(
7242
+ `Setting up workspace "${workspace.id}" in ${workspace.path}${opts.dryRun ? " (dry run)" : ""}
7243
+ `
7244
+ );
7245
+ wireSkills(workspace.path, agents, {
7246
+ force: Boolean(opts.force),
7247
+ dryRun: Boolean(opts.dryRun)
7248
+ });
7249
+ if (agents.has("claude")) {
7250
+ writeClaudeMd(false, workspace.path, {
7251
+ dryRun: Boolean(opts.dryRun),
7252
+ updateExisting: true
7253
+ });
7254
+ }
7255
+ if (agents.has("codex") || agents.has("agent")) {
7256
+ writeAgentsMd(false, workspace.path, {
7257
+ dryRun: Boolean(opts.dryRun),
7258
+ updateExisting: true
7259
+ });
7260
+ }
7261
+ if (agents.has("copilot")) {
7262
+ writeCopilotInstructions(workspace.path, {
7263
+ dryRun: Boolean(opts.dryRun),
7264
+ updateExisting: true
7265
+ });
7266
+ }
7267
+ } catch (err) {
7268
+ console.error(`Error: ${err.message}`);
7269
+ process.exit(1);
6506
7270
  }
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
- );
7271
+ });
6517
7272
  workspaceCommand.command("publish").description("Publish your local workspace sandbox to GitHub").action(async () => {
6518
7273
  let db;
6519
7274
  let workspaceDir = "";
@@ -6530,7 +7285,7 @@ workspaceCommand.command("publish").description("Publish your local workspace sa
6530
7285
  );
6531
7286
  process.exit(1);
6532
7287
  }
6533
- if (!existsSync15(workspaceDir)) {
7288
+ if (!existsSync16(workspaceDir)) {
6534
7289
  console.error(
6535
7290
  `\x1B[31m\u2717 Workspace directory does not exist: ${workspaceDir}\x1B[0m`
6536
7291
  );
@@ -6544,15 +7299,15 @@ Active workspace: \x1B[36m${workspaceDir}\x1B[0m`);
6544
7299
  );
6545
7300
  process.exit(1);
6546
7301
  }
6547
- const gitignorePath = join15(workspaceDir, ".gitignore");
6548
- if (!existsSync15(gitignorePath)) {
6549
- writeFileSync7(
7302
+ const gitignorePath = join16(workspaceDir, ".gitignore");
7303
+ if (!existsSync16(gitignorePath)) {
7304
+ writeFileSync8(
6550
7305
  gitignorePath,
6551
7306
  "node_modules/\n.agent/\n.agents/\n.claude/\n.gemini/\n.goose/\n*.log\n",
6552
7307
  "utf8"
6553
7308
  );
6554
7309
  }
6555
- const hasGitRepo = existsSync15(join15(workspaceDir, ".git"));
7310
+ const hasGitRepo = existsSync16(join16(workspaceDir, ".git"));
6556
7311
  if (!hasGitRepo) {
6557
7312
  console.log("Initializing local Git repository...");
6558
7313
  runGit(workspaceDir, ["init", "-b", "main"]);
@@ -6639,15 +7394,15 @@ Active workspace: \x1B[36m${workspaceDir}\x1B[0m`);
6639
7394
  }
6640
7395
  });
6641
7396
  async function backupDatabaseTo(db, targetDir) {
6642
- const backupDir = join15(targetDir, "zam-backups");
6643
- mkdirSync8(backupDir, { recursive: true });
7397
+ const backupDir = join16(targetDir, "zam-backups");
7398
+ mkdirSync9(backupDir, { recursive: true });
6644
7399
  const stamp = (/* @__PURE__ */ new Date()).toISOString().replace(/[:.]/g, "-");
6645
- const dest = join15(backupDir, `zam-${stamp}.db`);
7400
+ const dest = join16(backupDir, `zam-${stamp}.db`);
6646
7401
  await db.exec(`VACUUM INTO '${dest.replace(/'/g, "''")}'`);
6647
7402
  return dest;
6648
7403
  }
6649
7404
  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");
7405
+ const dir = join16(homedir9(), ".zam");
6651
7406
  console.log(opts.json ? JSON.stringify({ dataDir: dir }) : dir);
6652
7407
  });
6653
7408
  workspaceCommand.command("backup").description("Back up the local ZAM database into your workspace").option(
@@ -6667,7 +7422,7 @@ workspaceCommand.command("backup").description("Back up the local ZAM database i
6667
7422
  let db;
6668
7423
  try {
6669
7424
  db = await openDatabase();
6670
- const workspaceDir = opts.dir || await getSetting(db, "personal.workspace_dir") || join15(homedir9(), "Documents", "zam");
7425
+ const workspaceDir = opts.dir || await getSetting(db, "personal.workspace_dir") || join16(homedir9(), "Documents", "zam");
6671
7426
  const dest = await backupDatabaseTo(db, workspaceDir);
6672
7427
  if (opts.json) {
6673
7428
  console.log(JSON.stringify({ ok: true, path: dest }));
@@ -6743,7 +7498,7 @@ function parseTokenUpdates(opts) {
6743
7498
  }
6744
7499
  return updates;
6745
7500
  }
6746
- var bridgeCommand = new Command3("bridge").description(
7501
+ var bridgeCommand = new Command4("bridge").description(
6747
7502
  "Machine-readable JSON protocol for AI integration"
6748
7503
  );
6749
7504
  bridgeCommand.command("check-due").description("Check due cards for a user (JSON)").option("--user <id>", "User ID (default: whoami)").option("--domain <domain>", "Filter by knowledge domain").action(async (opts) => {
@@ -6786,7 +7541,7 @@ bridgeCommand.command("backup-db").description("Back up the local database into
6786
7541
  return;
6787
7542
  }
6788
7543
  await withDb2(async (db) => {
6789
- const workspaceDir = opts.dir || await getSetting(db, "personal.workspace_dir") || join16(homedir10(), "Documents", "zam");
7544
+ const workspaceDir = opts.dir || await getSetting(db, "personal.workspace_dir") || join17(homedir10(), "Documents", "zam");
6790
7545
  const path = await backupDatabaseTo(db, workspaceDir);
6791
7546
  jsonOut2({ ok: true, path });
6792
7547
  });
@@ -6796,18 +7551,199 @@ bridgeCommand.command("workspace-info").description("Report the workspace dir, i
6796
7551
  const workspaceDir = await getSetting(db, "personal.workspace_dir") || null;
6797
7552
  jsonOut2({
6798
7553
  workspaceDir,
6799
- defaultWorkspaceDir: join16(homedir10(), "Documents", "zam"),
6800
- dataDir: join16(homedir10(), ".zam")
7554
+ defaultWorkspaceDir: join17(homedir10(), "Documents", "zam"),
7555
+ dataDir: join17(homedir10(), ".zam")
7556
+ });
7557
+ });
7558
+ });
7559
+ function sameWorkspacePath(left, right) {
7560
+ const normalize = (value) => {
7561
+ const path = resolve5(value);
7562
+ return process.platform === "win32" ? path.toLowerCase() : path;
7563
+ };
7564
+ return normalize(left) === normalize(right);
7565
+ }
7566
+ async function ensureDesktopWorkspace(db) {
7567
+ const configured = getConfiguredWorkspaces();
7568
+ const savedWorkspaceDir = await getSetting(db, "personal.workspace_dir");
7569
+ const workspaceDir = savedWorkspaceDir || configured.find((workspace) => workspace.kind === "personal")?.path || join17(homedir10(), "Documents", "zam");
7570
+ mkdirSync10(workspaceDir, { recursive: true });
7571
+ if (!savedWorkspaceDir) {
7572
+ await setSetting(db, "personal.workspace_dir", workspaceDir);
7573
+ }
7574
+ if (!configured.some(
7575
+ (workspace) => sameWorkspacePath(workspace.path, workspaceDir)
7576
+ )) {
7577
+ const id = configured.some((workspace) => workspace.id === "personal") ? workspaceIdFromPath(workspaceDir) : "personal";
7578
+ upsertConfiguredWorkspace({
7579
+ id,
7580
+ label: basename5(workspaceDir) || "ZAM",
7581
+ kind: "personal",
7582
+ path: workspaceDir
7583
+ });
7584
+ }
7585
+ const skillLinks = getConfiguredWorkspaces().flatMap(
7586
+ (workspace) => existsSync17(workspace.path) ? wireSkills(workspace.path, parseSetupAgents(), { quiet: true }) : []
7587
+ );
7588
+ return { workspaceDir, skillLinks };
7589
+ }
7590
+ bridgeCommand.command("workspace-list").description("List configured ZAM workspaces (JSON)").action(async () => {
7591
+ await withDb2(async (db) => {
7592
+ const workspaceDir = await getSetting(db, "personal.workspace_dir") || null;
7593
+ jsonOut2({
7594
+ workspaces: getConfiguredWorkspaces(),
7595
+ workspaceDir,
7596
+ defaultWorkspaceDir: join17(homedir10(), "Documents", "zam"),
7597
+ dataDir: join17(homedir10(), ".zam")
7598
+ });
7599
+ });
7600
+ });
7601
+ function workspaceIdFromPath(dir) {
7602
+ const base = basename5(dir).toLowerCase().replace(/[^a-z0-9]+/g, "-").replace(/^-+|-+$/g, "");
7603
+ const prefix = base || "workspace";
7604
+ const existing = new Set(getConfiguredWorkspaces().map((item) => item.id));
7605
+ if (!existing.has(prefix)) return prefix;
7606
+ let index = 2;
7607
+ while (existing.has(`${prefix}-${index}`)) index++;
7608
+ return `${prefix}-${index}`;
7609
+ }
7610
+ function parseBridgeWorkspaceKind(value) {
7611
+ const kind = (value || "custom").toLowerCase();
7612
+ if (kind === "personal" || kind === "team" || kind === "family" || kind === "community" || kind === "organization" || kind === "custom") {
7613
+ return kind;
7614
+ }
7615
+ jsonError(`Invalid workspace kind: ${value}`);
7616
+ }
7617
+ bridgeCommand.command("workspace-add").description("Register an existing directory as a ZAM workspace (JSON)").requiredOption("--path <dir>", "Existing workspace/repository directory").option("--id <id>", "Workspace id").option("--label <label>", "Human-readable label").option("--kind <kind>", "Workspace kind", "custom").action(async (opts) => {
7618
+ const raw = String(opts.path ?? "").trim();
7619
+ if (!raw) jsonError("A non-empty --path is required");
7620
+ const path = resolve5(raw);
7621
+ if (!existsSync17(path)) jsonError(`Workspace path does not exist: ${path}`);
7622
+ const id = String(opts.id || workspaceIdFromPath(path)).trim();
7623
+ if (!id) jsonError("A non-empty --id is required");
7624
+ const workspace = {
7625
+ id,
7626
+ label: opts.label || basename5(path) || id,
7627
+ kind: parseBridgeWorkspaceKind(opts.kind),
7628
+ path
7629
+ };
7630
+ const skillLinks = wireSkills(path, parseSetupAgents(), { quiet: true });
7631
+ await withDb2(async (db) => {
7632
+ await setSetting(db, "personal.workspace_dir", path);
7633
+ upsertConfiguredWorkspace(workspace);
7634
+ jsonOut2({
7635
+ ok: true,
7636
+ workspace,
7637
+ workspaces: getConfiguredWorkspaces(),
7638
+ workspaceDir: path,
7639
+ skillLinks
7640
+ });
7641
+ });
7642
+ });
7643
+ bridgeCommand.command("workspace-remove").description("Unregister a ZAM workspace without deleting its files (JSON)").requiredOption("--id <id>", "Workspace id").action(async (opts) => {
7644
+ const id = String(opts.id ?? "").trim();
7645
+ if (!id) jsonError("A non-empty --id is required");
7646
+ const workspace = getConfiguredWorkspaces().find((item) => item.id === id);
7647
+ if (!workspace) jsonError(`Workspace "${id}" is not configured`);
7648
+ await withDb2(async (db) => {
7649
+ const activeWorkspaceDir = await getSetting(db, "personal.workspace_dir") || null;
7650
+ const remaining = removeConfiguredWorkspace(id);
7651
+ let workspaceDir = activeWorkspaceDir;
7652
+ let skillLinks = [];
7653
+ if (activeWorkspaceDir && sameWorkspacePath(activeWorkspaceDir, workspace.path)) {
7654
+ if (remaining.length > 0) {
7655
+ workspaceDir = remaining[0].path;
7656
+ await setSetting(db, "personal.workspace_dir", workspaceDir);
7657
+ } else {
7658
+ workspaceDir = join17(homedir10(), "Documents", "zam");
7659
+ await setSetting(db, "personal.workspace_dir", workspaceDir);
7660
+ const ensured = await ensureDesktopWorkspace(db);
7661
+ workspaceDir = ensured.workspaceDir;
7662
+ skillLinks = ensured.skillLinks;
7663
+ }
7664
+ }
7665
+ jsonOut2({
7666
+ ok: true,
7667
+ removed: workspace,
7668
+ workspaces: getConfiguredWorkspaces(),
7669
+ workspaceDir,
7670
+ skillLinks
6801
7671
  });
6802
7672
  });
6803
7673
  });
6804
7674
  bridgeCommand.command("set-workspace-dir").description("Set the personal workspace directory (JSON)").requiredOption("--dir <path>", "Path to the workspace directory").action(async (opts) => {
6805
7675
  const raw = String(opts.dir ?? "").trim();
6806
7676
  if (!raw) jsonError("A non-empty --dir is required");
6807
- const dir = resolve3(raw);
7677
+ const dir = resolve5(raw);
7678
+ if (!existsSync17(dir)) jsonError(`Workspace path does not exist: ${dir}`);
7679
+ const skillLinks = wireSkills(dir, parseSetupAgents(), { quiet: true });
6808
7680
  await withDb2(async (db) => {
6809
7681
  await setSetting(db, "personal.workspace_dir", dir);
6810
- jsonOut2({ ok: true, workspaceDir: dir });
7682
+ jsonOut2({ ok: true, workspaceDir: dir, skillLinks });
7683
+ });
7684
+ });
7685
+ bridgeCommand.command("agent-list").description("List agent harnesses with detection state + the default (JSON)").action(async () => {
7686
+ await withDb2(async (db) => {
7687
+ const configuredDefault = await getSetting(db, "agent.default") || null;
7688
+ const harnesses = await Promise.all(
7689
+ AGENT_HARNESSES.map(async (h) => {
7690
+ const override = await getSetting(db, `agent.${h.id}.command`) || void 0;
7691
+ return {
7692
+ id: h.id,
7693
+ label: h.label,
7694
+ kind: h.kind,
7695
+ detected: resolveHarnessExecutable(h, override) !== null
7696
+ };
7697
+ })
7698
+ );
7699
+ const fallbackDefault = harnesses.find((h) => h.detected)?.id ?? null;
7700
+ jsonOut2({ harnesses, default: configuredDefault ?? fallbackDefault });
7701
+ });
7702
+ });
7703
+ bridgeCommand.command("agent-open").description("Launch an agent harness in the workspace (JSON)").option(
7704
+ "--id <id>",
7705
+ "Harness id (default: agent.default setting, else first detected)"
7706
+ ).option("--workspace <id>", "Configured workspace id to open").option("--dir <path>", "Explicit workspace directory to open").action(async (opts) => {
7707
+ await withDb2(async (db) => {
7708
+ let id = opts.id || await getSetting(db, "agent.default") || void 0;
7709
+ if (!id) {
7710
+ id = AGENT_HARNESSES.find((h) => resolveHarnessExecutable(h))?.id;
7711
+ }
7712
+ if (!id) {
7713
+ jsonError(
7714
+ "No agent harness configured or detected. Install one (Claude Code, Codex, opencode) or set agent.default."
7715
+ );
7716
+ }
7717
+ const harness = getHarness(id);
7718
+ if (!harness) {
7719
+ jsonError(`Unknown harness: ${id}`);
7720
+ }
7721
+ const override = await getSetting(db, `agent.${harness.id}.command`) || void 0;
7722
+ const executable = resolveHarnessExecutable(harness, override);
7723
+ if (!executable) {
7724
+ jsonError(
7725
+ `${harness.label} was not detected. Set its path: zam settings set agent.${harness.id}.command <path>`
7726
+ );
7727
+ }
7728
+ const configuredWorkspace = opts.workspace ? getConfiguredWorkspaces().find((item) => item.id === opts.workspace) : void 0;
7729
+ if (opts.workspace && !configuredWorkspace) {
7730
+ jsonError(`Workspace is not configured: ${opts.workspace}`);
7731
+ }
7732
+ let workspace = opts.dir || configuredWorkspace?.path || await getSetting(db, "personal.workspace_dir") || join17(homedir10(), "Documents", "zam");
7733
+ if (!existsSync17(workspace)) workspace = homedir10();
7734
+ launchHarness(harness, {
7735
+ executable,
7736
+ workspace,
7737
+ shell: normalizeShell(void 0),
7738
+ silent: true
7739
+ });
7740
+ jsonOut2({
7741
+ ok: true,
7742
+ id: harness.id,
7743
+ label: harness.label,
7744
+ kind: harness.kind,
7745
+ workspace
7746
+ });
6811
7747
  });
6812
7748
  });
6813
7749
  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) => {
@@ -7168,10 +8104,10 @@ bridgeCommand.command("discover-skills").description(
7168
8104
  "20"
7169
8105
  ).action(async (opts) => {
7170
8106
  try {
7171
- const monitorDir = join16(homedir10(), ".zam", "monitor");
8107
+ const monitorDir = join17(homedir10(), ".zam", "monitor");
7172
8108
  let files;
7173
8109
  try {
7174
- files = readdirSync2(monitorDir).filter((f) => f.endsWith(".jsonl"));
8110
+ files = readdirSync3(monitorDir).filter((f) => f.endsWith(".jsonl"));
7175
8111
  } catch {
7176
8112
  jsonOut2({ proposals: [], message: "No monitor logs found." });
7177
8113
  return;
@@ -7181,7 +8117,7 @@ bridgeCommand.command("discover-skills").description(
7181
8117
  return;
7182
8118
  }
7183
8119
  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);
8120
+ const sorted = files.map((f) => ({ name: f, path: join17(monitorDir, f) })).sort((a, b) => b.name.localeCompare(a.name)).slice(0, limit);
7185
8121
  const sessionCommands = /* @__PURE__ */ new Map();
7186
8122
  for (const file of sorted) {
7187
8123
  const sessionId = file.name.replace(".jsonl", "");
@@ -7683,7 +8619,7 @@ bridgeCommand.command("capture-ui").description("Capture a screenshot for agent-
7683
8619
  return;
7684
8620
  }
7685
8621
  }
7686
- const outputPath = opts.image ?? opts.output ?? join16(tmpdir3(), `zam-capture-${randomBytes2(4).toString("hex")}.png`);
8622
+ const outputPath = opts.image ?? opts.output ?? join17(tmpdir3(), `zam-capture-${randomBytes2(4).toString("hex")}.png`);
7687
8623
  const captureResult = isProvided ? { method: "provided", target: null } : captureScreenshot(outputPath, opts.hwnd, opts.processName);
7688
8624
  if (!isProvided) {
7689
8625
  const post = decidePostCapture(policy, {
@@ -7694,7 +8630,7 @@ bridgeCommand.command("capture-ui").description("Capture a screenshot for agent-
7694
8630
  if (!post.allowed) {
7695
8631
  if (!opts.output) {
7696
8632
  try {
7697
- rmSync2(outputPath, { force: true });
8633
+ rmSync3(outputPath, { force: true });
7698
8634
  } catch {
7699
8635
  }
7700
8636
  }
@@ -7711,7 +8647,7 @@ bridgeCommand.command("capture-ui").description("Capture a screenshot for agent-
7711
8647
  return;
7712
8648
  }
7713
8649
  }
7714
- const imageBytes = readFileSync10(outputPath);
8650
+ const imageBytes = readFileSync11(outputPath);
7715
8651
  const base64 = imageBytes.toString("base64");
7716
8652
  jsonOut2({
7717
8653
  sessionId: opts.session ?? null,
@@ -7738,11 +8674,11 @@ bridgeCommand.command("start-recording").description("Start screen recording in
7738
8674
  return;
7739
8675
  }
7740
8676
  const sessionId = opts.session;
7741
- const statePath = join16(tmpdir3(), `zam-recording-${sessionId}.json`);
8677
+ const statePath = join17(tmpdir3(), `zam-recording-${sessionId}.json`);
7742
8678
  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)) {
8679
+ const outputPath = opts.output ?? join17(tmpdir3(), `zam-recording-${sessionId}${defaultExt}`);
8680
+ const { existsSync: existsSync25, writeFileSync: writeFileSync12, openSync, closeSync } = await import("fs");
8681
+ if (existsSync25(statePath)) {
7746
8682
  jsonOut2({
7747
8683
  sessionId,
7748
8684
  started: false,
@@ -7750,7 +8686,7 @@ bridgeCommand.command("start-recording").description("Start screen recording in
7750
8686
  });
7751
8687
  return;
7752
8688
  }
7753
- const logPath = join16(tmpdir3(), `zam-recording-${sessionId}.log`);
8689
+ const logPath = join17(tmpdir3(), `zam-recording-${sessionId}.log`);
7754
8690
  let logFd;
7755
8691
  try {
7756
8692
  logFd = openSync(logPath, "w");
@@ -7832,9 +8768,9 @@ bridgeCommand.command("stop-recording").description(
7832
8768
  return;
7833
8769
  }
7834
8770
  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)) {
8771
+ const statePath = join17(tmpdir3(), `zam-recording-${sessionId}.json`);
8772
+ const { existsSync: existsSync25, readFileSync: readFileSync16, rmSync: rmSync4 } = await import("fs");
8773
+ if (!existsSync25(statePath)) {
7838
8774
  jsonOut2({
7839
8775
  sessionId,
7840
8776
  stopped: false,
@@ -7842,7 +8778,7 @@ bridgeCommand.command("stop-recording").description(
7842
8778
  });
7843
8779
  return;
7844
8780
  }
7845
- const state = JSON.parse(readFileSync15(statePath, "utf8"));
8781
+ const state = JSON.parse(readFileSync16(statePath, "utf8"));
7846
8782
  const { pid, outputPath } = state;
7847
8783
  try {
7848
8784
  process.kill(pid, "SIGINT");
@@ -7858,7 +8794,7 @@ bridgeCommand.command("stop-recording").description(
7858
8794
  };
7859
8795
  let attempts = 0;
7860
8796
  while (isProcessRunning(pid) && attempts < 20) {
7861
- await new Promise((resolve6) => setTimeout(resolve6, 250));
8797
+ await new Promise((resolve9) => setTimeout(resolve9, 250));
7862
8798
  attempts++;
7863
8799
  }
7864
8800
  if (isProcessRunning(pid)) {
@@ -7868,10 +8804,10 @@ bridgeCommand.command("stop-recording").description(
7868
8804
  }
7869
8805
  }
7870
8806
  try {
7871
- rmSync3(statePath, { force: true });
8807
+ rmSync4(statePath, { force: true });
7872
8808
  } catch {
7873
8809
  }
7874
- if (!existsSync24(outputPath)) {
8810
+ if (!existsSync25(outputPath)) {
7875
8811
  jsonOut2({
7876
8812
  sessionId,
7877
8813
  stopped: false,
@@ -7897,7 +8833,7 @@ bridgeCommand.command("stop-recording").description(
7897
8833
  return;
7898
8834
  }
7899
8835
  try {
7900
- rmSync3(outputPath, { force: true });
8836
+ rmSync4(outputPath, { force: true });
7901
8837
  } catch {
7902
8838
  }
7903
8839
  jsonOut2({
@@ -7962,6 +8898,16 @@ bridgeCommand.command("check-llm").description("Check if LLM is enabled and onli
7962
8898
  });
7963
8899
  });
7964
8900
  });
8901
+ bridgeCommand.command("provider-status").description("Show secret-safe provider status for LLM roles (JSON)").action(async () => {
8902
+ await withDb2(async (db) => {
8903
+ const [recall, vision, text] = await Promise.all([
8904
+ getProviderRoleStatus(db, "recall"),
8905
+ getProviderRoleStatus(db, "vision"),
8906
+ getProviderRoleStatus(db, "text")
8907
+ ]);
8908
+ jsonOut2({ roles: { recall, vision, text } });
8909
+ });
8910
+ });
7965
8911
  bridgeCommand.command("check-vision").description(
7966
8912
  "Check if UI observer vision analysis is enabled and ready (JSON)"
7967
8913
  ).action(async () => {
@@ -8052,10 +8998,13 @@ bridgeCommand.command("desktop-bootstrap").description("Initialize first-run des
8052
8998
  await withDb2(async (db) => {
8053
8999
  const userId = await ensureDefaultUser(db, opts.user);
8054
9000
  const { enabled, url, model, locale } = await getLlmConfig(db);
9001
+ const { workspaceDir, skillLinks } = await ensureDesktopWorkspace(db);
8055
9002
  jsonOut2({
8056
9003
  userId,
8057
9004
  locale,
8058
- llm: { enabled, url, model }
9005
+ llm: { enabled, url, model },
9006
+ workspaceDir,
9007
+ skillLinks
8059
9008
  });
8060
9009
  });
8061
9010
  });
@@ -8274,8 +9223,8 @@ bridgeCommand.command("serve").description("Start the persistent JSON-RPC stdin/
8274
9223
  });
8275
9224
 
8276
9225
  // src/cli/commands/card.ts
8277
- import { Command as Command4 } from "commander";
8278
- var cardCommand = new Command4("card").description(
9226
+ import { Command as Command5 } from "commander";
9227
+ var cardCommand = new Command5("card").description(
8279
9228
  "Manage spaced-repetition cards"
8280
9229
  );
8281
9230
  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 +9423,8 @@ cardCommand.command("delete").description("Delete one user's card for a token").
8474
9423
 
8475
9424
  // src/cli/commands/connector.ts
8476
9425
  import { input as input2, password } from "@inquirer/prompts";
8477
- import { Command as Command5 } from "commander";
8478
- var connectorCommand = new Command5("connector").description(
9426
+ import { Command as Command6 } from "commander";
9427
+ var connectorCommand = new Command6("connector").description(
8479
9428
  "Manage external service connectors"
8480
9429
  );
8481
9430
  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 +9567,26 @@ async function setupTurso(urlArg, tokenArg, mode) {
8618
9567
 
8619
9568
  // src/cli/commands/git-sync.ts
8620
9569
  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";
9570
+ import { chmodSync as chmodSync2, existsSync as existsSync18, writeFileSync as writeFileSync9 } from "fs";
9571
+ import { join as join18 } from "path";
9572
+ import { Command as Command7 } from "commander";
8624
9573
  function installHook2() {
8625
- const gitDir = join17(process.cwd(), ".git");
8626
- if (!existsSync16(gitDir)) {
9574
+ const gitDir = join18(process.cwd(), ".git");
9575
+ if (!existsSync18(gitDir)) {
8627
9576
  console.error(
8628
9577
  "Error: Current directory is not the root of a Git repository."
8629
9578
  );
8630
9579
  process.exit(1);
8631
9580
  }
8632
- const hooksDir = join17(gitDir, "hooks");
8633
- const hookPath = join17(hooksDir, "post-commit");
9581
+ const hooksDir = join18(gitDir, "hooks");
9582
+ const hookPath = join18(hooksDir, "post-commit");
8634
9583
  const hookContent = `#!/bin/sh
8635
9584
  # ZAM Spaced Repetition Auto-Stale Hook
8636
9585
  # Triggered automatically on git commits to decay modified concept cards.
8637
9586
  zam git-sync --commit HEAD --quiet
8638
9587
  `;
8639
9588
  try {
8640
- writeFileSync8(hookPath, hookContent, { encoding: "utf-8", flag: "w" });
9589
+ writeFileSync9(hookPath, hookContent, { encoding: "utf-8", flag: "w" });
8641
9590
  try {
8642
9591
  chmodSync2(hookPath, "755");
8643
9592
  } catch (_e) {
@@ -8650,7 +9599,7 @@ zam git-sync --commit HEAD --quiet
8650
9599
  process.exit(1);
8651
9600
  }
8652
9601
  }
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) => {
9602
+ var gitSyncCommand = new Command7("git-sync").description("Sync learning cards with recent Git file modifications").option("--commit <hash>", "Git commit hash to check", "HEAD").option("--user <id>", "User ID (default: whoami)").option("--install", "Install git post-commit hook in current repo").option("--quiet", "Suppress verbose output").action(async (opts) => {
8654
9603
  if (opts.install) {
8655
9604
  installHook2();
8656
9605
  return;
@@ -8739,10 +9688,10 @@ ZAM Auto-Stale Complete: Scanned ${changedFiles.length} file(s).`
8739
9688
  });
8740
9689
 
8741
9690
  // src/cli/commands/goal.ts
8742
- import { existsSync as existsSync17, mkdirSync as mkdirSync9 } from "fs";
8743
- import { resolve as resolve4 } from "path";
9691
+ import { existsSync as existsSync19, mkdirSync as mkdirSync11 } from "fs";
9692
+ import { resolve as resolve6 } from "path";
8744
9693
  import { input as input3 } from "@inquirer/prompts";
8745
- import { Command as Command7 } from "commander";
9694
+ import { Command as Command8 } from "commander";
8746
9695
  async function resolveGoalsDir() {
8747
9696
  let goalsDir;
8748
9697
  let db;
@@ -8753,9 +9702,9 @@ async function resolveGoalsDir() {
8753
9702
  } finally {
8754
9703
  await db?.close();
8755
9704
  }
8756
- return goalsDir ? resolve4(goalsDir) : resolve4("goals");
9705
+ return goalsDir ? resolve6(goalsDir) : resolve6("goals");
8757
9706
  }
8758
- var goalCommand = new Command7("goal").description(
9707
+ var goalCommand = new Command8("goal").description(
8759
9708
  "Manage learning goals (markdown files)"
8760
9709
  );
8761
9710
  goalCommand.command("list").description("List all goals").option(
@@ -8763,7 +9712,7 @@ goalCommand.command("list").description("List all goals").option(
8763
9712
  "Filter by status (active, completed, paused, abandoned)"
8764
9713
  ).option("--tree", "Show goals as a tree with parent/child relationships").option("--json", "Output as JSON").action(async (opts) => {
8765
9714
  const goalsDir = await resolveGoalsDir();
8766
- if (!existsSync17(goalsDir)) {
9715
+ if (!existsSync19(goalsDir)) {
8767
9716
  console.error(`Goals directory not found: ${goalsDir}`);
8768
9717
  console.error(
8769
9718
  "Set it with: zam settings set personal.goals_dir /path/to/goals"
@@ -8864,8 +9813,8 @@ ${"\u2500".repeat(50)}`);
8864
9813
  });
8865
9814
  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
9815
  const goalsDir = await resolveGoalsDir();
8867
- if (!existsSync17(goalsDir)) {
8868
- mkdirSync9(goalsDir, { recursive: true });
9816
+ if (!existsSync19(goalsDir)) {
9817
+ mkdirSync11(goalsDir, { recursive: true });
8869
9818
  }
8870
9819
  let slug = opts.slug;
8871
9820
  let title = opts.title;
@@ -8924,22 +9873,22 @@ goalCommand.command("status <slug> <status>").description("Update a goal's statu
8924
9873
  });
8925
9874
 
8926
9875
  // src/cli/commands/init.ts
8927
- import { existsSync as existsSync18, mkdirSync as mkdirSync10, writeFileSync as writeFileSync9 } from "fs";
9876
+ import { existsSync as existsSync20, mkdirSync as mkdirSync12, writeFileSync as writeFileSync10 } from "fs";
8928
9877
  import { homedir as homedir11 } from "os";
8929
- import { join as join18 } from "path";
9878
+ import { join as join19, resolve as resolve7 } from "path";
8930
9879
  import { confirm as confirm2, input as input4 } from "@inquirer/prompts";
8931
- import { Command as Command8 } from "commander";
9880
+ import { Command as Command9 } from "commander";
8932
9881
  var HOME2 = homedir11();
8933
9882
  function printLine(char = "\u2550", len = 60, color = "\x1B[36m") {
8934
9883
  console.log(`${color}${char.repeat(len)}\x1B[0m`);
8935
9884
  }
8936
9885
  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(
9886
+ mkdirSync12(join19(workspaceDir, "beliefs"), { recursive: true });
9887
+ mkdirSync12(join19(workspaceDir, "goals"), { recursive: true });
9888
+ mkdirSync12(join19(workspaceDir, "skills"), { recursive: true });
9889
+ const worldviewFile = join19(workspaceDir, "beliefs", "worldview.md");
9890
+ if (!existsSync20(worldviewFile)) {
9891
+ writeFileSync10(
8943
9892
  worldviewFile,
8944
9893
  `# Personal Worldview
8945
9894
 
@@ -8951,9 +9900,9 @@ Here, I declare the core concepts and principles I want to master.
8951
9900
  "utf8"
8952
9901
  );
8953
9902
  }
8954
- const goalsFile = join18(workspaceDir, "goals", "goals.md");
8955
- if (!existsSync18(goalsFile)) {
8956
- writeFileSync9(
9903
+ const goalsFile = join19(workspaceDir, "goals", "goals.md");
9904
+ if (!existsSync20(goalsFile)) {
9905
+ writeFileSync10(
8957
9906
  goalsFile,
8958
9907
  `# Personal Goals
8959
9908
 
@@ -8965,7 +9914,7 @@ Here, I declare the core concepts and principles I want to master.
8965
9914
  );
8966
9915
  }
8967
9916
  }
8968
- var initCommand = new Command8("init").description("Launch the guided interactive onboarding wizard").action(async () => {
9917
+ var initCommand = new Command9("init").description("Launch the guided interactive onboarding wizard").action(async () => {
8969
9918
  printLine();
8970
9919
  console.log(
8971
9920
  "\x1B[1m\x1B[32m ZAM \u2014 The Symbiotic Learning Agent Onboarding\x1B[0m"
@@ -8975,13 +9924,22 @@ var initCommand = new Command8("init").description("Launch the guided interactiv
8975
9924
  );
8976
9925
  printLine();
8977
9926
  console.log("\n\x1B[1m[1/5] Setting up Local Workspace Sandbox\x1B[0m");
8978
- const defaultWorkspace = join18(HOME2, "Documents", "zam");
8979
- const workspacePath = await input4({
8980
- message: "Choose your ZAM workspace directory:",
8981
- default: defaultWorkspace
8982
- });
9927
+ const defaultWorkspace = join19(HOME2, "Documents", "zam");
9928
+ const workspacePath = resolve7(
9929
+ await input4({
9930
+ message: "Choose your ZAM workspace directory:",
9931
+ default: defaultWorkspace
9932
+ })
9933
+ );
8983
9934
  try {
8984
9935
  bootstrapSandboxWorkspace(workspacePath);
9936
+ wireSkills(workspacePath, parseSetupAgents());
9937
+ upsertConfiguredWorkspace({
9938
+ id: "personal",
9939
+ label: "Personal",
9940
+ kind: "personal",
9941
+ path: workspacePath
9942
+ });
8985
9943
  console.log(
8986
9944
  `\x1B[32m\u2713 Local Sandbox created at: ${workspacePath}\x1B[0m`
8987
9945
  );
@@ -9132,7 +10090,7 @@ var initCommand = new Command8("init").description("Launch the guided interactiv
9132
10090
 
9133
10091
  // src/cli/commands/learn.ts
9134
10092
  import { input as input6 } from "@inquirer/prompts";
9135
- import { Command as Command9 } from "commander";
10093
+ import { Command as Command10 } from "commander";
9136
10094
 
9137
10095
  // src/cli/learn-format.ts
9138
10096
  var BLOOM_VERBS3 = {
@@ -9470,7 +10428,7 @@ var STOP_WORDS = /* @__PURE__ */ new Set(["q", ":q", "quit", "stop"]);
9470
10428
  function isExitPrompt(err) {
9471
10429
  return err instanceof Error && err.name === "ExitPromptError";
9472
10430
  }
9473
- var learnCommand = new Command9("learn").description(
10431
+ var learnCommand = new Command10("learn").description(
9474
10432
  "Run a spoiler-free, in-process learning session (recall \u2192 reveal \u2192 self-rate)"
9475
10433
  ).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
10434
  let db;
@@ -9714,8 +10672,8 @@ learnCommand.command("open").description("Open a new terminal window running zam
9714
10672
  });
9715
10673
 
9716
10674
  // src/cli/commands/monitor.ts
9717
- import { Command as Command10 } from "commander";
9718
- var monitorCommand = new Command10("monitor").description(
10675
+ import { Command as Command11 } from "commander";
10676
+ var monitorCommand = new Command11("monitor").description(
9719
10677
  "Shell observation for real-time task monitoring"
9720
10678
  );
9721
10679
  monitorCommand.command("start").description("Output shell hook code to install monitoring").requiredOption("--session <id>", "Session ID to monitor").option(
@@ -9897,8 +10855,8 @@ monitorCommand.command("open").description("Open a new monitored terminal window
9897
10855
  });
9898
10856
 
9899
10857
  // src/cli/commands/observer.ts
9900
- import { Command as Command11 } from "commander";
9901
- var observerCommand = new Command11("observer").description(
10858
+ import { Command as Command12 } from "commander";
10859
+ var observerCommand = new Command12("observer").description(
9902
10860
  "Configure what the UI observer may capture (Layer 2 policy)"
9903
10861
  );
9904
10862
  function applyObserverListChange(current, entry, op) {
@@ -9961,8 +10919,8 @@ observerCommand.command("revoke <process>").description("Remove a process from o
9961
10919
 
9962
10920
  // src/cli/commands/profile.ts
9963
10921
  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";
10922
+ import { dirname as dirname6, join as join20, resolve as resolve8 } from "path";
10923
+ import { Command as Command13 } from "commander";
9966
10924
  var C2 = {
9967
10925
  reset: "\x1B[0m",
9968
10926
  bold: "\x1B[1m",
@@ -9971,7 +10929,7 @@ var C2 = {
9971
10929
  green: "\x1B[32m"
9972
10930
  };
9973
10931
  function defaultPersonalDir() {
9974
- return join19(homedir12(), "Documents", "zam");
10932
+ return join20(homedir12(), "Documents", "zam");
9975
10933
  }
9976
10934
  function render(profile) {
9977
10935
  const sync = profile.syncProvider ? `${C2.green}${profile.syncProvider}${C2.reset} ${C2.dim}(good for cross-device snapshots)${C2.reset}` : `${C2.dim}local folder (use a synced folder or snapshots to move between machines)${C2.reset}`;
@@ -9982,7 +10940,7 @@ function render(profile) {
9982
10940
  console.log(` data dir: ${C2.cyan}${profile.dataDir}${C2.reset}`);
9983
10941
  console.log(` database: ${C2.cyan}${profile.dbPath}${C2.reset}`);
9984
10942
  }
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) => {
10943
+ var profileCommand = new Command13("profile").description("Show or change this machine's ZAM install profile").option("--mode <mode>", "Set install mode: developer | default").option("--dir <path>", "Set the personal-content folder").option("--json", "Output as JSON").action(async (opts) => {
9986
10944
  if (opts.mode && opts.mode !== "developer" && opts.mode !== "default") {
9987
10945
  console.error(`Invalid --mode: ${opts.mode}. Use developer or default.`);
9988
10946
  process.exit(1);
@@ -9992,7 +10950,7 @@ var profileCommand = new Command12("profile").description("Show or change this m
9992
10950
  if (opts.mode) setInstallMode(opts.mode);
9993
10951
  db = await openDatabaseWithSync({ initialize: true });
9994
10952
  if (opts.dir) {
9995
- await setSetting(db, "personal.workspace_dir", resolve5(opts.dir));
10953
+ await setSetting(db, "personal.workspace_dir", resolve8(opts.dir));
9996
10954
  }
9997
10955
  const personalDir = await getSetting(db, "personal.workspace_dir") || defaultPersonalDir();
9998
10956
  await db.close();
@@ -10002,7 +10960,7 @@ var profileCommand = new Command12("profile").description("Show or change this m
10002
10960
  mode: getInstallMode(),
10003
10961
  personalDir,
10004
10962
  syncProvider: detectSyncProvider(personalDir),
10005
- dataDir: dirname5(dbPath),
10963
+ dataDir: dirname6(dbPath),
10006
10964
  dbPath
10007
10965
  };
10008
10966
  if (opts.json) {
@@ -10019,7 +10977,7 @@ var profileCommand = new Command12("profile").description("Show or change this m
10019
10977
 
10020
10978
  // src/cli/commands/provider.ts
10021
10979
  import { password as password2 } from "@inquirer/prompts";
10022
- import { Command as Command13 } from "commander";
10980
+ import { Command as Command14 } from "commander";
10023
10981
  var VALID_API_FLAVORS = [
10024
10982
  "chat-completions",
10025
10983
  "anthropic-messages"
@@ -10031,6 +10989,9 @@ function upsertProviderRecord(providers, name, patch) {
10031
10989
  if (patch.model !== void 0) merged.model = patch.model;
10032
10990
  if (patch.apiFlavor !== void 0) merged.apiFlavor = patch.apiFlavor;
10033
10991
  if (patch.apiKeyRef !== void 0) merged.apiKeyRef = patch.apiKeyRef;
10992
+ if (patch.label !== void 0) merged.label = patch.label;
10993
+ if (patch.local !== void 0) merged.local = patch.local;
10994
+ if (patch.runner !== void 0) merged.runner = patch.runner;
10034
10995
  return { ...providers, [name]: merged };
10035
10996
  }
10036
10997
  function removeProviderRecord(providers, name) {
@@ -10059,7 +11020,7 @@ function buildProviderListing(providers, hasKey) {
10059
11020
  let keyState;
10060
11021
  if (!rec.apiKeyRef) keyState = "none";
10061
11022
  else keyState = hasKey(rec.apiKeyRef) ? "set" : "missing";
10062
- return {
11023
+ const row = {
10063
11024
  name,
10064
11025
  url: rec.url,
10065
11026
  model: rec.model,
@@ -10067,6 +11028,10 @@ function buildProviderListing(providers, hasKey) {
10067
11028
  apiKeyRef: rec.apiKeyRef,
10068
11029
  keyState
10069
11030
  };
11031
+ if (rec.label !== void 0) row.label = rec.label;
11032
+ if (rec.local !== void 0) row.local = rec.local;
11033
+ if (rec.runner !== void 0) row.runner = rec.runner;
11034
+ return row;
10070
11035
  });
10071
11036
  }
10072
11037
  function findOrphanKeyRefs(storedRefs, providers) {
@@ -10086,19 +11051,53 @@ async function readJson(db, key, fallback) {
10086
11051
  }
10087
11052
  var readProviders = (db) => readJson(db, "llm.providers", {});
10088
11053
  var readRoles = (db) => readJson(db, "llm.roles", {});
10089
- async function writeProviders(db, p) {
11054
+ async function readScopedProviders(db, machine) {
11055
+ if (machine) return getMachineAiConfig().providers ?? {};
11056
+ if (!db)
11057
+ throw new Error("Database is required for shared provider settings.");
11058
+ return readProviders(db);
11059
+ }
11060
+ async function readScopedRoles(db, machine) {
11061
+ if (machine) return getMachineAiConfig().roles ?? {};
11062
+ if (!db)
11063
+ throw new Error("Database is required for shared provider settings.");
11064
+ return readRoles(db);
11065
+ }
11066
+ async function writeScopedProviders(db, machine, p) {
11067
+ if (machine) {
11068
+ const config = getMachineAiConfig();
11069
+ saveMachineAiConfig({ ...config, providers: p });
11070
+ return;
11071
+ }
11072
+ if (!db)
11073
+ throw new Error("Database is required for shared provider settings.");
10090
11074
  await setSetting(db, "llm.providers", JSON.stringify(p));
10091
11075
  }
10092
- async function writeRoles(db, r) {
11076
+ async function writeScopedRoles(db, machine, r) {
11077
+ if (machine) {
11078
+ const config = getMachineAiConfig();
11079
+ saveMachineAiConfig({ ...config, roles: r });
11080
+ return;
11081
+ }
11082
+ if (!db)
11083
+ throw new Error("Database is required for shared provider settings.");
10093
11084
  await setSetting(db, "llm.roles", JSON.stringify(r));
10094
11085
  }
10095
- var providerCommand = new Command13("provider").description(
11086
+ async function withProviderScope(machine, action) {
11087
+ if (machine) {
11088
+ await action(void 0);
11089
+ return;
11090
+ }
11091
+ await withDb(action);
11092
+ }
11093
+ var providerCommand = new Command14("provider").description(
10096
11094
  "Configure role-based AI providers (url/model/flavor/key, per role)"
10097
11095
  );
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);
11096
+ providerCommand.command("list").description("Show configured providers, role bindings, and key status").option("--json", "Output as JSON").option("--machine", "Read machine-local providers from ~/.zam/config.json").action(async (opts) => {
11097
+ const machine = Boolean(opts.machine);
11098
+ await withProviderScope(machine, async (db) => {
11099
+ const providers = await readScopedProviders(db, machine);
11100
+ const roles = await readScopedRoles(db, machine);
10102
11101
  const rows = buildProviderListing(
10103
11102
  providers,
10104
11103
  (ref) => getProviderApiKey(ref) !== null
@@ -10106,11 +11105,23 @@ providerCommand.command("list").description("Show configured providers, role bin
10106
11105
  const orphans = findOrphanKeyRefs(listProviderApiKeyRefs(), providers);
10107
11106
  if (opts.json) {
10108
11107
  console.log(
10109
- JSON.stringify({ providers: rows, roles, orphans }, null, 2)
11108
+ JSON.stringify(
11109
+ {
11110
+ scope: machine ? "machine" : "shared",
11111
+ providers: rows,
11112
+ roles,
11113
+ orphans
11114
+ },
11115
+ null,
11116
+ 2
11117
+ )
10110
11118
  );
10111
11119
  return;
10112
11120
  }
10113
- console.log("Providers (llm.providers):\n");
11121
+ console.log(
11122
+ `Providers (${opts.machine ? "~/.zam/config.json ai.providers" : "llm.providers"}):
11123
+ `
11124
+ );
10114
11125
  if (rows.length === 0) {
10115
11126
  console.log(
10116
11127
  " (none) \u2014 add one: zam provider add <name> --url <url> --model <model>"
@@ -10122,6 +11133,11 @@ providerCommand.command("list").description("Show configured providers, role bin
10122
11133
  console.log(` url: ${row.url ?? "(inherits llm.url)"}`);
10123
11134
  console.log(` model: ${row.model ?? "(inherits llm.model)"}`);
10124
11135
  console.log(` flavor: ${row.apiFlavor}`);
11136
+ if (row.label) console.log(` label: ${row.label}`);
11137
+ if (row.local !== void 0) {
11138
+ console.log(` local: ${row.local ? "yes" : "no"}`);
11139
+ }
11140
+ if (row.runner) console.log(` runner: ${row.runner}`);
10125
11141
  if (row.apiKeyRef) console.log(` key-ref: ${row.apiKeyRef}`);
10126
11142
  }
10127
11143
  }
@@ -10143,7 +11159,10 @@ providerCommand.command("list").description("Show configured providers, role bin
10143
11159
  }
10144
11160
  });
10145
11161
  });
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(
11162
+ providerCommand.command("add <name>").description("Add or update a provider record in llm.providers").option("--label <label>", "Human-readable provider label").option("--url <url>", "Endpoint base URL (e.g. https://api.deepseek.com/v1)").option("--model <model>", "Model id (e.g. deepseek-v4-flash)").option("--local", "Mark this provider as a local/on-device endpoint").option(
11163
+ "--runner <runner>",
11164
+ "Local runner hint (flm, foundry-local, ollama, ...)"
11165
+ ).option(
10147
11166
  "--flavor <flavor>",
10148
11167
  `Wire protocol: ${VALID_API_FLAVORS.join(" | ")} (default: inferred from URL)`
10149
11168
  ).option(
@@ -10152,7 +11171,7 @@ providerCommand.command("add <name>").description("Add or update a provider reco
10152
11171
  ).option(
10153
11172
  "--key <value>",
10154
11173
  "Store this API key now (prefer `set-key` to keep it out of shell history)"
10155
- ).action(async (name, opts) => {
11174
+ ).option("--machine", "Store this provider in ~/.zam/config.json").action(async (name, opts) => {
10156
11175
  let apiFlavor;
10157
11176
  if (opts.flavor) {
10158
11177
  if (!VALID_API_FLAVORS.includes(opts.flavor)) {
@@ -10164,20 +11183,31 @@ providerCommand.command("add <name>").description("Add or update a provider reco
10164
11183
  apiFlavor = opts.flavor;
10165
11184
  }
10166
11185
  const apiKeyRef = opts.keyRef ?? (opts.key ? name : void 0);
10167
- await withDb(async (db) => {
10168
- const providers = await readProviders(db);
11186
+ const machine = Boolean(opts.machine);
11187
+ await withProviderScope(machine, async (db) => {
11188
+ const providers = await readScopedProviders(db, machine);
10169
11189
  const next = upsertProviderRecord(providers, name, {
11190
+ label: opts.label,
10170
11191
  url: opts.url,
10171
11192
  model: opts.model,
10172
11193
  apiFlavor,
10173
- apiKeyRef
11194
+ apiKeyRef,
11195
+ local: opts.local ? true : void 0,
11196
+ runner: opts.runner
10174
11197
  });
10175
- await writeProviders(db, next);
11198
+ await writeScopedProviders(db, machine, next);
10176
11199
  if (opts.key && apiKeyRef) setProviderApiKey(apiKeyRef, opts.key);
10177
11200
  const rec = next[name];
10178
- console.log(`Provider "${name}" saved:`);
11201
+ console.log(
11202
+ `Provider "${name}" saved (${machine ? "machine-local" : "shared"}):`
11203
+ );
10179
11204
  console.log(` url: ${rec.url ?? "(inherits llm.url)"}`);
10180
11205
  console.log(` model: ${rec.model ?? "(inherits llm.model)"}`);
11206
+ if (rec.label) console.log(` label: ${rec.label}`);
11207
+ if (rec.local !== void 0) {
11208
+ console.log(` local: ${rec.local ? "yes" : "no"}`);
11209
+ }
11210
+ if (rec.runner) console.log(` runner: ${rec.runner}`);
10181
11211
  console.log(
10182
11212
  ` flavor: ${rec.apiFlavor ?? `${rec.url ? inferApiFlavor(rec.url) : "chat-completions"} (inferred)`}`
10183
11213
  );
@@ -10201,9 +11231,10 @@ Bind it to a role: zam provider use recall --primary ${name}`
10201
11231
  );
10202
11232
  });
10203
11233
  });
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);
11234
+ providerCommand.command("remove <name>").description("Remove a provider record from llm.providers").option("--machine", "Remove from ~/.zam/config.json instead of the DB").action(async (name, opts) => {
11235
+ const machine = Boolean(opts.machine);
11236
+ await withProviderScope(machine, async (db) => {
11237
+ const providers = await readScopedProviders(db, machine);
10207
11238
  const { providers: next, removed } = removeProviderRecord(
10208
11239
  providers,
10209
11240
  name
@@ -10212,9 +11243,14 @@ providerCommand.command("remove <name>").description("Remove a provider record f
10212
11243
  console.log(`No such provider: ${name}`);
10213
11244
  return;
10214
11245
  }
10215
- await writeProviders(db, next);
10216
- console.log(`Removed provider "${name}".`);
10217
- const referencing = rolesReferencing(await readRoles(db), name);
11246
+ await writeScopedProviders(db, machine, next);
11247
+ console.log(
11248
+ `Removed provider "${name}" (${machine ? "machine-local" : "shared"}).`
11249
+ );
11250
+ const referencing = rolesReferencing(
11251
+ await readScopedRoles(db, machine),
11252
+ name
11253
+ );
10218
11254
  if (referencing.length > 0) {
10219
11255
  console.log(
10220
11256
  ` \u26A0 Still referenced by role(s): ${referencing.join(", ")} \u2014 rebind with: zam provider use <role> --primary <name>`
@@ -10222,7 +11258,7 @@ providerCommand.command("remove <name>").description("Remove a provider record f
10222
11258
  }
10223
11259
  });
10224
11260
  });
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) => {
11261
+ providerCommand.command("use <role>").description(`Bind providers to a role (${VALID_ROLES.join(" | ")})`).option("--primary <name>", "Primary provider").option("--fallback <name>", "Fallback provider (optional)").option("--machine", "Bind the role in ~/.zam/config.json").action(async (role, opts) => {
10226
11262
  if (!VALID_ROLES.includes(role)) {
10227
11263
  console.error(`Invalid role: ${role}. Use ${VALID_ROLES.join(", ")}.`);
10228
11264
  process.exit(1);
@@ -10231,19 +11267,23 @@ providerCommand.command("use <role>").description(`Bind providers to a role (${V
10231
11267
  console.error("--primary is required.");
10232
11268
  process.exit(1);
10233
11269
  }
10234
- await withDb(async (db) => {
10235
- const providers = await readProviders(db);
10236
- await writeRoles(
11270
+ const machine = Boolean(opts.machine);
11271
+ await withProviderScope(machine, async (db) => {
11272
+ const providers = await readScopedProviders(db, machine);
11273
+ await writeScopedRoles(
10237
11274
  db,
11275
+ machine,
10238
11276
  bindRoleProviders(
10239
- await readRoles(db),
11277
+ await readScopedRoles(db, machine),
10240
11278
  role,
10241
11279
  opts.primary,
10242
11280
  opts.fallback
10243
11281
  )
10244
11282
  );
10245
11283
  const fb = opts.fallback ? `, fallback: ${opts.fallback}` : "";
10246
- console.log(`Role "${role}" \u2192 primary: ${opts.primary}${fb}`);
11284
+ console.log(
11285
+ `Role "${role}" \u2192 primary: ${opts.primary}${fb} (${machine ? "machine-local" : "shared"})`
11286
+ );
10247
11287
  for (const [label, ref] of [
10248
11288
  ["primary", opts.primary],
10249
11289
  ["fallback", opts.fallback]
@@ -10285,8 +11325,8 @@ providerCommand.command("clear-key <ref>").description("Remove a stored API key"
10285
11325
  });
10286
11326
 
10287
11327
  // 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) => {
11328
+ import { Command as Command15 } from "commander";
11329
+ var reviewCommand = new Command15("review").description("Start an interactive review session").option("--user <id>", "User ID (default: whoami)").option("--max-new <n>", "Maximum new cards", "10").option("--max-reviews <n>", "Maximum review cards", "50").option("--no-resolve", "Skip resolving source_link into inline context").action(async (opts) => {
10290
11330
  let db;
10291
11331
  try {
10292
11332
  db = await openDatabase();
@@ -10399,10 +11439,10 @@ ${"\u2550".repeat(50)}`);
10399
11439
  });
10400
11440
 
10401
11441
  // src/cli/commands/session.ts
10402
- import { readFileSync as readFileSync11 } from "fs";
11442
+ import { readFileSync as readFileSync12 } from "fs";
10403
11443
  import { input as input7, select as select2 } from "@inquirer/prompts";
10404
- import { Command as Command15 } from "commander";
10405
- var sessionCommand = new Command15("session").description(
11444
+ import { Command as Command16 } from "commander";
11445
+ var sessionCommand = new Command16("session").description(
10406
11446
  "Manage learning sessions"
10407
11447
  );
10408
11448
  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 +11631,7 @@ function loadPatternFile(path) {
10591
11631
  if (!path) return [];
10592
11632
  let parsed;
10593
11633
  try {
10594
- parsed = JSON.parse(readFileSync11(path, "utf-8"));
11634
+ parsed = JSON.parse(readFileSync12(path, "utf-8"));
10595
11635
  } catch (err) {
10596
11636
  throw new Error(
10597
11637
  `Cannot read synthesis patterns from ${path}: ${err.message}`
@@ -10781,9 +11821,9 @@ sessionCommand.command("end").description("End a session and show summary").requ
10781
11821
  });
10782
11822
 
10783
11823
  // 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(
11824
+ import { existsSync as existsSync21 } from "fs";
11825
+ import { Command as Command17 } from "commander";
11826
+ var settingsCommand = new Command17("settings").description(
10787
11827
  "Manage user settings"
10788
11828
  );
10789
11829
  var BOOLEAN_SETTING_KEYS = /* @__PURE__ */ new Set(["llm.enabled", "llm.vision.enabled"]);
@@ -10946,7 +11986,7 @@ settingsCommand.command("repos").description("Show or set Personal, Team, and Or
10946
11986
  console.log("\nValidation:");
10947
11987
  for (const [name, path] of Object.entries(paths)) {
10948
11988
  if (path) {
10949
- const exists = existsSync19(path);
11989
+ const exists = existsSync21(path);
10950
11990
  console.log(
10951
11991
  ` ${name.padEnd(9)}: ${exists ? "\x1B[32m\u2713 Valid folder\x1B[0m" : "\x1B[31m\u2717 Directory does not exist\x1B[0m"}`
10952
11992
  );
@@ -10957,175 +11997,6 @@ settingsCommand.command("repos").description("Show or set Personal, Team, and Or
10957
11997
  });
10958
11998
  });
10959
11999
 
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
12000
  // src/cli/commands/skill.ts
11130
12001
  import { Command as Command18 } from "commander";
11131
12002
  var skillCommand = new Command18("skill").description(
@@ -11213,7 +12084,7 @@ skillCommand.command("add").description("Register a new agent skill").requiredOp
11213
12084
  });
11214
12085
 
11215
12086
  // src/cli/commands/snapshot.ts
11216
- import { existsSync as existsSync21, mkdirSync as mkdirSync12, readFileSync as readFileSync12, writeFileSync as writeFileSync11 } from "fs";
12087
+ import { existsSync as existsSync22, mkdirSync as mkdirSync13, readFileSync as readFileSync13, writeFileSync as writeFileSync11 } from "fs";
11217
12088
  import { homedir as homedir13 } from "os";
11218
12089
  import { dirname as dirname7, join as join21 } from "path";
11219
12090
  import { Command as Command19 } from "commander";
@@ -11246,8 +12117,8 @@ var exportCmd = new Command19("export").description("Write a portable SQL-text s
11246
12117
  }
11247
12118
  const out = opts.out ?? join21(personalDir, "snapshots", defaultOutName());
11248
12119
  const dir = dirname7(out);
11249
- if (dir && dir !== "." && !existsSync21(dir)) {
11250
- mkdirSync12(dir, { recursive: true });
12120
+ if (dir && dir !== "." && !existsSync22(dir)) {
12121
+ mkdirSync13(dir, { recursive: true });
11251
12122
  }
11252
12123
  writeFileSync11(out, snapshot, "utf-8");
11253
12124
  console.log(`Snapshot written: ${out}`);
@@ -11263,11 +12134,11 @@ var exportCmd = new Command19("export").description("Write a portable SQL-text s
11263
12134
  var importCmd = new Command19("import").description("Restore a snapshot into the database").argument("<file>", "Snapshot file to restore").option("--force", "Overwrite a non-empty database", false).action(async (file, opts) => {
11264
12135
  let db;
11265
12136
  try {
11266
- if (!existsSync21(file)) {
12137
+ if (!existsSync22(file)) {
11267
12138
  console.error(`Error: Snapshot file not found: ${file}`);
11268
12139
  process.exit(1);
11269
12140
  }
11270
- const snapshot = readFileSync12(file, "utf-8");
12141
+ const snapshot = readFileSync13(file, "utf-8");
11271
12142
  db = await openDatabaseWithSync({ initialize: true });
11272
12143
  const result = await importSnapshot(db, snapshot, { force: opts.force });
11273
12144
  await db.close();
@@ -11285,11 +12156,11 @@ var importCmd = new Command19("import").description("Restore a snapshot into the
11285
12156
  });
11286
12157
  var verifyCmd = new Command19("verify").description("Check a snapshot's manifest and checksum without importing").argument("<file>", "Snapshot file to verify").action((file) => {
11287
12158
  try {
11288
- if (!existsSync21(file)) {
12159
+ if (!existsSync22(file)) {
11289
12160
  console.error(`Error: Snapshot file not found: ${file}`);
11290
12161
  process.exit(1);
11291
12162
  }
11292
- const manifest = verifySnapshot(readFileSync12(file, "utf-8"));
12163
+ const manifest = verifySnapshot(readFileSync13(file, "utf-8"));
11293
12164
  const { total, nonEmpty } = summarize(manifest.tables);
11294
12165
  console.log(`Valid snapshot (format v${manifest.version})`);
11295
12166
  console.log(` created: ${manifest.createdAt}`);
@@ -11629,7 +12500,7 @@ tokenCommand.command("status").description("Show full status of a token for a us
11629
12500
 
11630
12501
  // src/cli/commands/ui.ts
11631
12502
  import { spawn as spawn3, spawnSync } from "child_process";
11632
- import { existsSync as existsSync22 } from "fs";
12503
+ import { existsSync as existsSync23 } from "fs";
11633
12504
  import { homedir as homedir14 } from "os";
11634
12505
  import { dirname as dirname8, join as join22 } from "path";
11635
12506
  import { fileURLToPath as fileURLToPath3 } from "url";
@@ -11647,7 +12518,7 @@ function findDesktopDir() {
11647
12518
  for (const start of starts) {
11648
12519
  let dir = start;
11649
12520
  for (let i = 0; i < 10; i++) {
11650
- if (existsSync22(join22(dir, "desktop", "src-tauri", "tauri.conf.json"))) {
12521
+ if (existsSync23(join22(dir, "desktop", "src-tauri", "tauri.conf.json"))) {
11651
12522
  return join22(dir, "desktop");
11652
12523
  }
11653
12524
  const parent = dirname8(dir);
@@ -11662,15 +12533,15 @@ function findBuiltApp(desktopDir) {
11662
12533
  if (process.platform === "win32") {
11663
12534
  for (const name of ["ZAM.exe", "zam.exe", "zam-desktop.exe"]) {
11664
12535
  const p = join22(releaseDir, name);
11665
- if (existsSync22(p)) return p;
12536
+ if (existsSync23(p)) return p;
11666
12537
  }
11667
12538
  } else if (process.platform === "darwin") {
11668
12539
  const app = join22(releaseDir, "bundle", "macos", "ZAM.app");
11669
- if (existsSync22(app)) return app;
12540
+ if (existsSync23(app)) return app;
11670
12541
  } else {
11671
12542
  for (const name of ["zam", "ZAM", "zam-desktop"]) {
11672
12543
  const p = join22(releaseDir, name);
11673
- if (existsSync22(p)) return p;
12544
+ if (existsSync23(p)) return p;
11674
12545
  }
11675
12546
  }
11676
12547
  return null;
@@ -11681,7 +12552,7 @@ function findInstalledApp() {
11681
12552
  process.env.ProgramFiles && join22(process.env.ProgramFiles, "ZAM", "ZAM.exe"),
11682
12553
  process.env["ProgramFiles(x86)"] && join22(process.env["ProgramFiles(x86)"], "ZAM", "ZAM.exe")
11683
12554
  ] : 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;
12555
+ return candidates.find((candidate) => candidate && existsSync23(candidate)) || null;
11685
12556
  }
11686
12557
  function runNpm(args, opts) {
11687
12558
  const res = spawnSync("npm", args, {
@@ -11692,7 +12563,7 @@ function runNpm(args, opts) {
11692
12563
  return res.status ?? 1;
11693
12564
  }
11694
12565
  function ensureDesktopDeps(desktopDir) {
11695
- if (existsSync22(join22(desktopDir, "node_modules"))) return true;
12566
+ if (existsSync23(join22(desktopDir, "node_modules"))) return true;
11696
12567
  console.log(
11697
12568
  `${C3.cyan}Installing desktop dependencies (one-time)...${C3.reset}`
11698
12569
  );
@@ -11724,7 +12595,7 @@ function hasMsvcBuildTools() {
11724
12595
  "Installer",
11725
12596
  "vswhere.exe"
11726
12597
  );
11727
- if (!existsSync22(vswhere)) return false;
12598
+ if (!existsSync23(vswhere)) return false;
11728
12599
  const res = spawnSync(
11729
12600
  vswhere,
11730
12601
  [
@@ -11756,7 +12627,7 @@ function requireMsvcOnWindows() {
11756
12627
  return false;
11757
12628
  }
11758
12629
  function warnIfCliMissing(repoRoot) {
11759
- if (!existsSync22(join22(repoRoot, "dist", "cli", "index.js"))) {
12630
+ if (!existsSync23(join22(repoRoot, "dist", "cli", "index.js"))) {
11760
12631
  console.warn(
11761
12632
  `${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
12633
  );
@@ -11917,7 +12788,7 @@ ${C3.dim}After that, 'zam ui' launches it directly.${C3.reset}`
11917
12788
 
11918
12789
  // src/cli/commands/update.ts
11919
12790
  import { spawnSync as spawnSync2 } from "child_process";
11920
- import { existsSync as existsSync23, readFileSync as readFileSync13, realpathSync } from "fs";
12791
+ import { existsSync as existsSync24, readFileSync as readFileSync14, realpathSync as realpathSync2 } from "fs";
11921
12792
  import { dirname as dirname9, join as join23 } from "path";
11922
12793
  import { fileURLToPath as fileURLToPath4 } from "url";
11923
12794
  import { confirm as confirm4 } from "@inquirer/prompts";
@@ -11943,7 +12814,7 @@ function currentVersion() {
11943
12814
  for (const up of ["..", "../..", "../../.."]) {
11944
12815
  try {
11945
12816
  const pkg2 = JSON.parse(
11946
- readFileSync13(join23(here, up, "package.json"), "utf-8")
12817
+ readFileSync14(join23(here, up, "package.json"), "utf-8")
11947
12818
  );
11948
12819
  if (pkg2.version) return pkg2.version;
11949
12820
  } catch {
@@ -11954,7 +12825,7 @@ function currentVersion() {
11954
12825
  function versionAt(dir) {
11955
12826
  try {
11956
12827
  const pkg2 = JSON.parse(
11957
- readFileSync13(join23(dir, "package.json"), "utf-8")
12828
+ readFileSync14(join23(dir, "package.json"), "utf-8")
11958
12829
  );
11959
12830
  return pkg2.version ?? "unknown";
11960
12831
  } catch {
@@ -12031,14 +12902,14 @@ var checkCmd = new Command23("check").description("Check whether a newer ZAM has
12031
12902
  }
12032
12903
  );
12033
12904
  function findSourceRepo() {
12034
- let dir = realpathSync(dirname9(fileURLToPath4(import.meta.url)));
12905
+ let dir = realpathSync2(dirname9(fileURLToPath4(import.meta.url)));
12035
12906
  let parent = dirname9(dir);
12036
12907
  while (parent !== dir) {
12037
- if (existsSync23(join23(dir, ".git"))) return dir;
12908
+ if (existsSync24(join23(dir, ".git"))) return dir;
12038
12909
  dir = parent;
12039
12910
  parent = dirname9(dir);
12040
12911
  }
12041
- return existsSync23(join23(dir, ".git")) ? dir : null;
12912
+ return existsSync24(join23(dir, ".git")) ? dir : null;
12042
12913
  }
12043
12914
  function runGit2(cwd, args, capture) {
12044
12915
  const res = spawnSync2("git", args, {
@@ -12215,7 +13086,7 @@ var whoamiCommand = new Command24("whoami").description("Show or set the default
12215
13086
  // src/cli/index.ts
12216
13087
  var __dirname = dirname10(fileURLToPath5(import.meta.url));
12217
13088
  var pkg = JSON.parse(
12218
- readFileSync14(join24(__dirname, "..", "..", "package.json"), "utf-8")
13089
+ readFileSync15(join24(__dirname, "..", "..", "package.json"), "utf-8")
12219
13090
  );
12220
13091
  var program = new Command25();
12221
13092
  program.name("zam").description(