zam-core 0.4.3 → 0.5.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/dist/cli/index.js CHANGED
@@ -1,14 +1,14 @@
1
1
  #!/usr/bin/env node
2
2
 
3
3
  // src/cli/index.ts
4
- import { readFileSync as readFileSync14 } from "fs";
5
- import { dirname as dirname10, join as join23 } from "path";
4
+ import { readFileSync as readFileSync15 } from "fs";
5
+ import { dirname as dirname10, join as join24 } from "path";
6
6
  import { fileURLToPath as fileURLToPath5 } from "url";
7
- import { Command as Command24 } from "commander";
7
+ import { Command as Command25 } from "commander";
8
8
 
9
9
  // src/cli/commands/agent.ts
10
- import { existsSync as existsSync11 } from "fs";
11
- import { join as join11 } from "path";
10
+ import { existsSync as existsSync13 } from "fs";
11
+ import { join as join13 } from "path";
12
12
  import { Command } from "commander";
13
13
 
14
14
  // src/kernel/analytics/stats.ts
@@ -203,6 +203,25 @@ function clearADOCredentials(path) {
203
203
  delete creds.ado;
204
204
  saveCredentials(creds, path);
205
205
  }
206
+ function getProviderApiKey(name, path) {
207
+ const key = loadCredentials(path).llmProviders?.[name]?.apiKey;
208
+ return key && key.length > 0 ? key : null;
209
+ }
210
+ function setProviderApiKey(name, apiKey, path) {
211
+ const creds = loadCredentials(path);
212
+ creds.llmProviders = { ...creds.llmProviders, [name]: { apiKey } };
213
+ saveCredentials(creds, path);
214
+ }
215
+ function clearProviderApiKey(name, path) {
216
+ const creds = loadCredentials(path);
217
+ if (creds.llmProviders && name in creds.llmProviders) {
218
+ delete creds.llmProviders[name];
219
+ saveCredentials(creds, path);
220
+ }
221
+ }
222
+ function listProviderApiKeyRefs(path) {
223
+ return Object.keys(loadCredentials(path).llmProviders ?? {});
224
+ }
206
225
 
207
226
  // src/kernel/connectors/azure-devops.ts
208
227
  function loadADOConfig() {
@@ -4403,8 +4422,10 @@ function t(locale, key, params = {}) {
4403
4422
  import { existsSync as existsSync8, mkdirSync as mkdirSync7, readFileSync as readFileSync8, writeFileSync as writeFileSync5 } from "fs";
4404
4423
  import { homedir as homedir6 } from "os";
4405
4424
  import { dirname as dirname4, join as join9 } from "path";
4406
- var DEFAULT_CONFIG_PATH = join9(homedir6(), ".zam", "config.json");
4407
- 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()) {
4408
4429
  if (!existsSync8(path)) return {};
4409
4430
  try {
4410
4431
  return JSON.parse(readFileSync8(path, "utf-8"));
@@ -4412,25 +4433,50 @@ function loadInstallConfig(path = DEFAULT_CONFIG_PATH) {
4412
4433
  return {};
4413
4434
  }
4414
4435
  }
4415
- function saveInstallConfig(config, path = DEFAULT_CONFIG_PATH) {
4436
+ function saveInstallConfig(config, path = defaultConfigPath()) {
4416
4437
  const dir = dirname4(path);
4417
4438
  if (!existsSync8(dir)) mkdirSync7(dir, { recursive: true });
4418
4439
  writeFileSync5(path, `${JSON.stringify(config, null, 2)}
4419
4440
  `, "utf-8");
4420
4441
  }
4421
- function getInstallMode(path = DEFAULT_CONFIG_PATH) {
4442
+ function getInstallMode(path = defaultConfigPath()) {
4422
4443
  return loadInstallConfig(path).mode ?? "developer";
4423
4444
  }
4424
- function setInstallMode(mode, path = DEFAULT_CONFIG_PATH) {
4445
+ function setInstallMode(mode, path = defaultConfigPath()) {
4425
4446
  const config = loadInstallConfig(path);
4426
4447
  config.mode = mode;
4427
4448
  saveInstallConfig(config, path);
4428
4449
  }
4429
- function getInstallChannel(path = DEFAULT_CONFIG_PATH) {
4450
+ function getInstallChannel(path = defaultConfigPath()) {
4430
4451
  const config = loadInstallConfig(path);
4431
4452
  if (config.channel) return config.channel;
4432
4453
  return (config.mode ?? "developer") === "developer" ? "developer" : "direct";
4433
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
+ }
4434
4480
  function detectSyncProvider(dir) {
4435
4481
  const p = dir.toLowerCase();
4436
4482
  if (p.includes("onedrive")) return "OneDrive";
@@ -4707,13 +4753,11 @@ function runCommand(cmd) {
4707
4753
  return "";
4708
4754
  }
4709
4755
  }
4710
- function detectWindowsAMDIPU() {
4756
+ function detectWindowsNPU() {
4711
4757
  if (process.platform !== "win32") return false;
4712
- const cmd = `powershell -NoProfile -Command "Get-CimInstance Win32_PnPEntity | Where-Object { $_.Name -like '*AMD IPU*' -or $_.Name -like '*AMD NPU*' -or $_.Name -like '*NPU Compute*' -or $_.Name -like '*Ryzen AI*' -or $_.HardwareID -like '*VEN_1022&DEV_1502*' -or $_.HardwareID -like '*VEN_1022&DEV_17F0*' } | Select-Object -First 1 -ExpandProperty Name"`;
4758
+ const cmd = `powershell -NoProfile -Command "Get-CimInstance Win32_PnPEntity | Where-Object { $_.PNPClass -eq 'ComputeAccelerator' -or $_.Name -like '*AMD IPU*' -or $_.Name -like '*AMD NPU*' -or $_.Name -like '*Qualcomm*NPU*' -or $_.Name -like '*Hexagon*NPU*' -or $_.Name -like '*Intel*AI Boost*' -or $_.Name -like '*NPU Compute*' -or $_.Name -like '*Ryzen AI*' } | Select-Object -First 1 -ExpandProperty Name"`;
4713
4759
  const output = runCommand(cmd);
4714
- return Boolean(
4715
- output && (output.toLowerCase().includes("amd") || output.toLowerCase().includes("ipu") || output.toLowerCase().includes("npu") || output.toLowerCase().includes("ryzen"))
4716
- );
4760
+ return Boolean(output && output.length > 0);
4717
4761
  }
4718
4762
  function getSystemProfile() {
4719
4763
  const platform = process.platform;
@@ -4725,13 +4769,21 @@ function getSystemProfile() {
4725
4769
  let arch = "unknown";
4726
4770
  if (archStr === "x64") arch = "x64";
4727
4771
  else if (archStr === "arm64") arch = "arm64";
4728
- const hasRyzenNPU = os === "windows" && detectWindowsAMDIPU();
4772
+ const hasNpu = os === "windows" && detectWindowsNPU();
4729
4773
  const hasAppleSilicon = os === "macos" && arch === "arm64";
4730
4774
  let recommendedRunner = "generic";
4731
4775
  let recommendedModel = "qwen3.5:4b";
4732
- if (hasRyzenNPU) {
4733
- recommendedRunner = "fastflowlm";
4734
- recommendedModel = "qwen3.5:4b";
4776
+ if (hasNpu) {
4777
+ const isQualcomm = runCommand(
4778
+ `powershell -NoProfile -Command "Get-CimInstance Win32_PnPEntity | Where-Object { $_.Name -like '*Qualcomm*' } | Select-Object -First 1"`
4779
+ ).length > 0;
4780
+ if (isQualcomm) {
4781
+ recommendedRunner = "generic";
4782
+ recommendedModel = "qwen3.5-4b";
4783
+ } else {
4784
+ recommendedRunner = "fastflowlm";
4785
+ recommendedModel = "qwen3.5:4b";
4786
+ }
4735
4787
  } else if (hasAppleSilicon) {
4736
4788
  recommendedRunner = "ollama";
4737
4789
  recommendedModel = "llama3.2:3b";
@@ -4742,7 +4794,8 @@ function getSystemProfile() {
4742
4794
  return {
4743
4795
  os,
4744
4796
  arch,
4745
- hasRyzenNPU,
4797
+ hasRyzenNPU: hasNpu && !archStr.includes("arm64"),
4798
+ // backwards compatibility flag
4746
4799
  hasAppleSilicon,
4747
4800
  recommendedRunner,
4748
4801
  recommendedModel
@@ -4879,6 +4932,304 @@ function planUpdate(decision) {
4879
4932
  }
4880
4933
  }
4881
4934
 
4935
+ // src/cli/agent-harness.ts
4936
+ import { spawn } from "child_process";
4937
+ import { existsSync as existsSync12 } from "fs";
4938
+ import { homedir as homedir8 } from "os";
4939
+ import { join as join12 } from "path";
4940
+
4941
+ // src/cli/terminal-open.ts
4942
+ import { execFileSync as execFileSync2, execSync as execSync4 } from "child_process";
4943
+ import {
4944
+ accessSync,
4945
+ constants,
4946
+ existsSync as existsSync11,
4947
+ unlinkSync,
4948
+ writeFileSync as writeFileSync6
4949
+ } from "fs";
4950
+ import { tmpdir } from "os";
4951
+ import { basename as basename2, delimiter, extname, isAbsolute, join as join11 } from "path";
4952
+ function isPowerShellShell(shell) {
4953
+ return shell === "pwsh" || shell === "powershell";
4954
+ }
4955
+ function psSingleQuoted2(value) {
4956
+ return `'${value.replace(/'/g, "''")}'`;
4957
+ }
4958
+ function detectShell() {
4959
+ if (process.platform === "win32")
4960
+ return findExecutable("pwsh.exe") ? "pwsh" : "powershell";
4961
+ const shell = process.env.SHELL ?? "";
4962
+ return basename2(shell) === "bash" ? "bash" : "zsh";
4963
+ }
4964
+ function normalizeShell(shell) {
4965
+ if (!shell) return detectShell();
4966
+ const normalized = shell.toLowerCase();
4967
+ if (normalized === "zsh" || normalized === "bash" || normalized === "pwsh" || normalized === "powershell") {
4968
+ return normalized;
4969
+ }
4970
+ throw new Error(
4971
+ `Unsupported shell: ${shell}. Expected zsh, bash, pwsh, or powershell.`
4972
+ );
4973
+ }
4974
+ function selectWindowsExecutable(results, pathext = process.env.PATHEXT ?? ".COM;.EXE;.BAT;.CMD") {
4975
+ if (results.length === 0) return null;
4976
+ const extensions = pathext.split(";").map((ext) => ext.trim().toLowerCase()).filter(Boolean);
4977
+ const runnable = results.find(
4978
+ (result) => extensions.some((ext) => result.toLowerCase().endsWith(ext))
4979
+ );
4980
+ return runnable ?? results[0];
4981
+ }
4982
+ function stripSurroundingQuotes(command) {
4983
+ const trimmed = command.trim();
4984
+ if (trimmed.length < 2) return trimmed;
4985
+ const first = trimmed[0];
4986
+ const last = trimmed[trimmed.length - 1];
4987
+ if (first === '"' && last === '"' || first === "'" && last === "'") {
4988
+ return trimmed.slice(1, -1);
4989
+ }
4990
+ return trimmed;
4991
+ }
4992
+ function executableExists(path) {
4993
+ if (!existsSync11(path)) return false;
4994
+ if (process.platform === "win32") return true;
4995
+ try {
4996
+ accessSync(path, constants.X_OK);
4997
+ return true;
4998
+ } catch {
4999
+ return false;
5000
+ }
5001
+ }
5002
+ function windowsExecutableNames(command) {
5003
+ if (process.platform !== "win32") return [command];
5004
+ if (extname(command)) return [command];
5005
+ const extensions = (process.env.PATHEXT ?? ".COM;.EXE;.BAT;.CMD").split(";").map((ext) => ext.trim()).filter(Boolean);
5006
+ return extensions.map((ext) => `${command}${ext}`);
5007
+ }
5008
+ function hasDirectoryPart(command) {
5009
+ return isAbsolute(command) || command.includes("/") || command.includes("\\");
5010
+ }
5011
+ function findExecutable(command) {
5012
+ const normalized = stripSurroundingQuotes(command);
5013
+ if (!normalized) return null;
5014
+ const matches = [];
5015
+ if (hasDirectoryPart(normalized)) {
5016
+ for (const candidate of windowsExecutableNames(normalized)) {
5017
+ if (executableExists(candidate)) matches.push(candidate);
5018
+ }
5019
+ return process.platform === "win32" ? selectWindowsExecutable(matches) : matches[0] ?? null;
5020
+ }
5021
+ const pathEntries = (process.env.PATH ?? "").split(delimiter).map((entry) => entry.trim()).filter(Boolean);
5022
+ for (const entry of pathEntries) {
5023
+ for (const name of windowsExecutableNames(normalized)) {
5024
+ const candidate = join11(entry, name);
5025
+ if (executableExists(candidate)) matches.push(candidate);
5026
+ }
5027
+ }
5028
+ return process.platform === "win32" ? selectWindowsExecutable(matches) : matches[0] ?? null;
5029
+ }
5030
+ function resolveZamInvocation(shell) {
5031
+ const installed = findExecutable("zam");
5032
+ if (installed) {
5033
+ return isPowerShellShell(shell) ? `& ${psSingleQuoted2(installed)}` : installed;
5034
+ }
5035
+ const projectRoot = join11(import.meta.dirname, "..", "..");
5036
+ const cliSource = join11(projectRoot, "src/cli/index.ts");
5037
+ if (isPowerShellShell(shell)) {
5038
+ return `& npx --prefix ${psSingleQuoted2(projectRoot)} tsx ${psSingleQuoted2(cliSource)}`;
5039
+ }
5040
+ return `npx --prefix ${JSON.stringify(projectRoot)} tsx ${JSON.stringify(cliSource)}`;
5041
+ }
5042
+ function buildShellSetupCommand(dir, shell, command) {
5043
+ if (isPowerShellShell(shell)) {
5044
+ return [`Set-Location -LiteralPath ${psSingleQuoted2(dir)}`, command].join(
5045
+ "; "
5046
+ );
5047
+ }
5048
+ return `cd ${JSON.stringify(dir)} && ${command}`;
5049
+ }
5050
+ function isItermRunning() {
5051
+ try {
5052
+ const result = execSync4(
5053
+ `osascript -e 'tell application "System Events" to (name of processes) contains "iTerm2"' 2>/dev/null`,
5054
+ { encoding: "utf-8" }
5055
+ ).trim();
5056
+ return result === "true";
5057
+ } catch {
5058
+ return false;
5059
+ }
5060
+ }
5061
+ function openMacTerminal(shellSetup, label, dir, silent) {
5062
+ const useIterm = isItermRunning();
5063
+ const escaped = shellSetup.replace(/\\/g, "\\\\").replace(/"/g, '\\"');
5064
+ const appleScript = useIterm ? `tell application "iTerm2"
5065
+ activate
5066
+ set newWindow to (create window with default profile)
5067
+ tell current session of newWindow
5068
+ write text "${escaped}"
5069
+ end tell
5070
+ end tell` : `tell application "Terminal"
5071
+ activate
5072
+ do script "${escaped}"
5073
+ end tell`;
5074
+ const tmpFile = join11(tmpdir(), `zam-terminal-${label}.scpt`);
5075
+ try {
5076
+ writeFileSync6(tmpFile, appleScript);
5077
+ execSync4(`osascript ${JSON.stringify(tmpFile)}`, { stdio: "ignore" });
5078
+ if (!silent) {
5079
+ console.log(
5080
+ `Opened ${useIterm ? "iTerm2" : "Terminal.app"} window (${label})`
5081
+ );
5082
+ console.log(` Directory: ${dir}`);
5083
+ }
5084
+ } catch (err) {
5085
+ if (!silent) {
5086
+ console.error(`Failed to open terminal: ${err.message}`);
5087
+ console.log(`
5088
+ Run this manually in a new terminal:
5089
+ `);
5090
+ console.log(` ${shellSetup}`);
5091
+ }
5092
+ } finally {
5093
+ try {
5094
+ unlinkSync(tmpFile);
5095
+ } catch {
5096
+ }
5097
+ }
5098
+ }
5099
+ function openWindowsPowerShell(shellSetup, label, dir, requestedShell, silent) {
5100
+ const requestedExecutable = requestedShell === "powershell" ? "powershell.exe" : "pwsh.exe";
5101
+ const executable = findExecutable(requestedExecutable) ? requestedExecutable : "powershell.exe";
5102
+ const startCommand = [
5103
+ "Start-Process",
5104
+ `-FilePath ${psSingleQuoted2(executable)}`,
5105
+ `-ArgumentList @('-NoExit','-NoProfile','-Command',${psSingleQuoted2(shellSetup)})`
5106
+ ].join(" ");
5107
+ const launcher = findExecutable("pwsh.exe") ? "pwsh.exe" : "powershell.exe";
5108
+ try {
5109
+ execFileSync2(
5110
+ launcher,
5111
+ ["-NoProfile", "-ExecutionPolicy", "Bypass", "-Command", startCommand],
5112
+ {
5113
+ stdio: "ignore"
5114
+ }
5115
+ );
5116
+ if (!silent) {
5117
+ console.log(
5118
+ `Opened ${executable === "pwsh.exe" ? "PowerShell" : "Windows PowerShell"} window (${label})`
5119
+ );
5120
+ console.log(` Directory: ${dir}`);
5121
+ }
5122
+ } catch (err) {
5123
+ if (!silent) {
5124
+ console.error(`Failed to open PowerShell: ${err.message}`);
5125
+ console.log(`
5126
+ Run this manually in a new PowerShell terminal:
5127
+ `);
5128
+ console.log(` ${shellSetup}`);
5129
+ }
5130
+ }
5131
+ }
5132
+ function openTerminalWindow(opts) {
5133
+ const { shellSetup, label, dir, shell, silent = false } = opts;
5134
+ const platform = opts.platform ?? process.platform;
5135
+ if (platform === "darwin" && !isPowerShellShell(shell)) {
5136
+ openMacTerminal(shellSetup, label, dir, silent);
5137
+ return;
5138
+ }
5139
+ if (platform === "win32" && isPowerShellShell(shell)) {
5140
+ openWindowsPowerShell(shellSetup, label, dir, shell, silent);
5141
+ return;
5142
+ }
5143
+ if (!silent) {
5144
+ console.log(`Run this in a new terminal:
5145
+ `);
5146
+ console.log(` ${shellSetup}
5147
+ `);
5148
+ console.log(
5149
+ `(Automatic terminal opening is only supported on macOS Terminal/iTerm2 and Windows PowerShell for now.)`
5150
+ );
5151
+ }
5152
+ }
5153
+
5154
+ // src/cli/agent-harness.ts
5155
+ var AGENT_HARNESSES = [
5156
+ { id: "claude-code", label: "Claude Code", kind: "cli", command: "claude" },
5157
+ { id: "codex", label: "Codex", kind: "cli", command: "codex" },
5158
+ { id: "opencode", label: "opencode", kind: "cli", command: "opencode" },
5159
+ {
5160
+ id: "cursor",
5161
+ label: "Cursor",
5162
+ kind: "app",
5163
+ command: "cursor",
5164
+ candidatePaths: {
5165
+ win32: [
5166
+ join12(homedir8(), "AppData", "Local", "Programs", "cursor", "Cursor.exe")
5167
+ ],
5168
+ darwin: ["/Applications/Cursor.app/Contents/MacOS/Cursor"]
5169
+ }
5170
+ },
5171
+ { id: "copilot", label: "GitHub Copilot", kind: "app", command: "copilot" },
5172
+ {
5173
+ id: "antigravity",
5174
+ label: "Antigravity",
5175
+ kind: "app",
5176
+ command: "antigravity"
5177
+ }
5178
+ ];
5179
+ function getHarness(id) {
5180
+ return AGENT_HARNESSES.find((h) => h.id === id);
5181
+ }
5182
+ function resolveHarnessExecutable(harness, overrideCommand, deps = {}) {
5183
+ const find = deps.find ?? findExecutable;
5184
+ const exists = deps.exists ?? existsSync12;
5185
+ const platform = deps.platform ?? process.platform;
5186
+ const found = find(overrideCommand || harness.command);
5187
+ if (found) return found;
5188
+ if (harness.kind === "app") {
5189
+ for (const candidate of harness.candidatePaths?.[platform] ?? []) {
5190
+ if (exists(candidate)) return candidate;
5191
+ }
5192
+ }
5193
+ return null;
5194
+ }
5195
+ function planHarnessLaunch(harness, opts) {
5196
+ if (harness.kind === "cli") {
5197
+ const invocation = isPowerShellShell(opts.shell) ? `& ${psSingleQuoted2(opts.executable)}` : JSON.stringify(opts.executable);
5198
+ return {
5199
+ kind: "cli",
5200
+ shell: opts.shell,
5201
+ shellSetup: buildShellSetupCommand(
5202
+ opts.workspace,
5203
+ opts.shell,
5204
+ invocation
5205
+ )
5206
+ };
5207
+ }
5208
+ return { kind: "app", executable: opts.executable, args: [opts.workspace] };
5209
+ }
5210
+ function launchHarness(harness, opts) {
5211
+ const plan = planHarnessLaunch(harness, opts);
5212
+ if (plan.kind === "cli") {
5213
+ openTerminalWindow({
5214
+ shellSetup: plan.shellSetup,
5215
+ label: `agent-${harness.id}`,
5216
+ dir: opts.workspace,
5217
+ shell: plan.shell,
5218
+ silent: opts.silent,
5219
+ platform: opts.platform
5220
+ });
5221
+ return;
5222
+ }
5223
+ spawn(plan.executable, plan.args, {
5224
+ detached: true,
5225
+ stdio: "ignore",
5226
+ windowsHide: true
5227
+ }).unref();
5228
+ if (!opts.silent) {
5229
+ console.log(`Launched ${harness.label} in ${opts.workspace}`);
5230
+ }
5231
+ }
5232
+
4882
5233
  // src/cli/commands/agent.ts
4883
5234
  var C = {
4884
5235
  reset: "\x1B[0m",
@@ -4890,7 +5241,7 @@ var C = {
4890
5241
  };
4891
5242
  var SUPPORTED_AGENTS = ["opencode"];
4892
5243
  function agentsMdPresent(cwd = process.cwd()) {
4893
- return existsSync11(join11(cwd, "AGENTS.md"));
5244
+ return existsSync13(join13(cwd, "AGENTS.md"));
4894
5245
  }
4895
5246
  function printStatus() {
4896
5247
  const installed = hasCommand("opencode");
@@ -4928,19 +5279,87 @@ var installCmd = new Command("install").description("Download and install the de
4928
5279
  console.log(` Start it with: ${C.cyan}opencode${C.reset}`);
4929
5280
  });
4930
5281
  var statusCmd = new Command("status").description("Show whether the agent is installed and wired to ZAM").action(printStatus);
4931
- var agentCommand = new Command("agent").description("Provision and inspect the agent that drives ZAM sessions").addCommand(installCmd).addCommand(statusCmd).action(printStatus);
5282
+ var listCmd = new Command("list").description("List known agent harnesses and whether each is detected").action(() => {
5283
+ console.log(`${C.bold}Agent harnesses${C.reset}`);
5284
+ for (const h of AGENT_HARNESSES) {
5285
+ const status = resolveHarnessExecutable(h) ? `${C.green}detected${C.reset}` : `${C.yellow}not found${C.reset}`;
5286
+ console.log(
5287
+ ` ${h.id.padEnd(13)} ${status} ${C.dim}${h.label} (${h.kind})${C.reset}`
5288
+ );
5289
+ }
5290
+ console.log(`
5291
+ ${C.dim}Open one: zam agent open --id <id>${C.reset}`);
5292
+ console.log(
5293
+ ` ${C.dim}Point ZAM at a custom path: zam settings set agent.<id>.command <path>${C.reset}`
5294
+ );
5295
+ });
5296
+ var openCmd = new Command("open").description("Open an agent harness in the workspace, ready to drive ZAM").option(
5297
+ "--id <id>",
5298
+ "Harness id (default: agent.default setting, else first detected)"
5299
+ ).option("--dir <path>", "Workspace directory (defaults to cwd)").option(
5300
+ "--shell <type>",
5301
+ "Shell type: zsh | bash | pwsh | powershell (auto-detected)"
5302
+ ).action(async (opts) => {
5303
+ let shell;
5304
+ try {
5305
+ shell = normalizeShell(opts.shell);
5306
+ } catch (err) {
5307
+ console.error(`Error: ${err.message}`);
5308
+ process.exit(1);
5309
+ }
5310
+ let id = opts.id;
5311
+ let override;
5312
+ let db;
5313
+ try {
5314
+ db = await openDatabase();
5315
+ if (!id) {
5316
+ id = await getSetting(db, "agent.default") || void 0;
5317
+ }
5318
+ if (!id) {
5319
+ id = AGENT_HARNESSES.find((h) => resolveHarnessExecutable(h))?.id;
5320
+ }
5321
+ if (id) {
5322
+ override = await getSetting(db, `agent.${id}.command`) || void 0;
5323
+ }
5324
+ } finally {
5325
+ await db?.close();
5326
+ }
5327
+ if (!id) {
5328
+ console.error(
5329
+ "No agent harness configured or detected. Install one (e.g. zam agent install), then set a default: zam settings set agent.default <id>."
5330
+ );
5331
+ process.exit(1);
5332
+ }
5333
+ const harness = getHarness(id);
5334
+ if (!harness) {
5335
+ console.error(
5336
+ `Unknown harness: ${id}. Known: ${AGENT_HARNESSES.map((h) => h.id).join(", ")}.`
5337
+ );
5338
+ process.exit(1);
5339
+ }
5340
+ const executable = resolveHarnessExecutable(harness, override);
5341
+ if (!executable) {
5342
+ console.error(
5343
+ `${harness.label} was not detected. Install it, or point ZAM at it: zam settings set agent.${harness.id}.command <path>.`
5344
+ );
5345
+ process.exit(1);
5346
+ }
5347
+ const workspace = opts.dir ?? process.cwd();
5348
+ launchHarness(harness, { executable, workspace, shell });
5349
+ });
5350
+ var agentCommand = new Command("agent").description("Provision and inspect the agent that drives ZAM sessions").addCommand(installCmd).addCommand(statusCmd).addCommand(openCmd).addCommand(listCmd).action(printStatus);
4932
5351
 
4933
5352
  // src/cli/commands/bridge.ts
4934
- import { execFileSync as execFileSync2 } from "child_process";
5353
+ import { execFileSync as execFileSync4 } from "child_process";
4935
5354
  import { randomBytes as randomBytes2 } from "crypto";
4936
- import { readdirSync as readdirSync2, readFileSync as readFileSync10, rmSync as rmSync2 } from "fs";
4937
- import { homedir as homedir8, tmpdir as tmpdir2 } from "os";
4938
- import { join as join13 } from "path";
4939
- import { Command as Command2 } from "commander";
5355
+ import { existsSync as existsSync17, readdirSync as readdirSync2, readFileSync as readFileSync11, rmSync as rmSync2 } from "fs";
5356
+ import { homedir as homedir10, tmpdir as tmpdir3 } from "os";
5357
+ import { basename as basename5, join as join17, resolve as resolve5 } from "path";
5358
+ import { Command as Command4 } from "commander";
4940
5359
 
4941
5360
  // src/cli/llm/client.ts
4942
- import { spawn } from "child_process";
4943
- import { existsSync as existsSync12 } from "fs";
5361
+ import { spawn as spawn2 } from "child_process";
5362
+ import { existsSync as existsSync14 } from "fs";
4944
5363
  var DEFAULT_LLM_URL = "http://localhost:8000/v1";
4945
5364
  var DEFAULT_LLM_MODEL = "qwen3.5:4b";
4946
5365
  var DEFAULT_LLM_API_KEY = "sk-none";
@@ -4953,22 +5372,149 @@ async function getLlmConfig(db) {
4953
5372
  locale: await getSetting(db, "system.locale") || "en"
4954
5373
  };
4955
5374
  }
4956
- async function getVisionConfig(db) {
4957
- const base = await getLlmConfig(db);
4958
- const maxFramesStr = await getSetting(db, "llm.vision.max_frames");
4959
- const maxFrames = maxFramesStr ? parseInt(maxFramesStr, 10) : 100;
5375
+ function getCloudModelRecommendation(url) {
5376
+ const lowercase = url.toLowerCase();
5377
+ if (lowercase.includes("openrouter.ai")) {
5378
+ return "openrouter/free";
5379
+ }
5380
+ if (lowercase.includes("openai.com") || lowercase.includes("api.openai")) {
5381
+ return "gpt-5-mini";
5382
+ }
5383
+ if (lowercase.includes("googleapis.com") || lowercase.includes("google")) {
5384
+ return "gemini-3.5-flash";
5385
+ }
5386
+ if (lowercase.includes("deepseek.com")) {
5387
+ return "deepseek-v4-flash";
5388
+ }
5389
+ if (lowercase.includes("mimo")) {
5390
+ return "mimo-v2.5";
5391
+ }
5392
+ return null;
5393
+ }
5394
+ function inferApiFlavor(url) {
5395
+ try {
5396
+ return new URL(url).hostname.toLowerCase().endsWith("anthropic.com") ? "anthropic-messages" : "chat-completions";
5397
+ } catch {
5398
+ return "chat-completions";
5399
+ }
5400
+ }
5401
+ async function readJsonSetting(db, key) {
5402
+ const raw = await getSetting(db, key);
5403
+ if (!raw) return null;
5404
+ try {
5405
+ return JSON.parse(raw);
5406
+ } catch {
5407
+ return null;
5408
+ }
5409
+ }
5410
+ function resolveProviderApiKey(rec) {
5411
+ if (rec.apiKey) return rec.apiKey;
5412
+ if (rec.apiKeyRef) {
5413
+ const key = getProviderApiKey(rec.apiKeyRef);
5414
+ if (key) return key;
5415
+ }
5416
+ return DEFAULT_LLM_API_KEY;
5417
+ }
5418
+ async function getProviderForRole(db, role) {
5419
+ const enabled = role === "vision" ? await getSetting(db, "llm.vision.enabled") === "true" : await getSetting(db, "llm.enabled") === "true";
5420
+ const base = await getLegacyRoleConfig(db, role, enabled);
5421
+ const providers = await readJsonSetting(db, "llm.providers");
5422
+ const roles = await readJsonSetting(db, "llm.roles");
5423
+ const binding = roles?.[role];
5424
+ let resolved = base;
5425
+ if (providers && binding?.primary && providers[binding.primary]) {
5426
+ const primary = materializeProvider(
5427
+ providers[binding.primary],
5428
+ base,
5429
+ role,
5430
+ {
5431
+ providerName: binding.primary,
5432
+ source: "shared"
5433
+ }
5434
+ );
5435
+ const fallback = binding.fallback && providers[binding.fallback] ? materializeProvider(providers[binding.fallback], base, role, {
5436
+ providerName: binding.fallback,
5437
+ source: "shared"
5438
+ }) : void 0;
5439
+ resolved = { ...primary, fallback };
5440
+ }
5441
+ const machine = getMachineAiConfig();
5442
+ const machineProviders = machine.providers;
5443
+ const machineBinding = machine.roles?.[role];
5444
+ if (machineProviders && machineBinding?.primary && machineProviders[machineBinding.primary]) {
5445
+ const primary = materializeProvider(
5446
+ machineProviders[machineBinding.primary],
5447
+ resolved,
5448
+ role,
5449
+ { providerName: machineBinding.primary, source: "machine" }
5450
+ );
5451
+ const fallback = machineBinding.fallback && machineProviders[machineBinding.fallback] ? materializeProvider(
5452
+ machineProviders[machineBinding.fallback],
5453
+ resolved,
5454
+ role,
5455
+ { providerName: machineBinding.fallback, source: "machine" }
5456
+ ) : void 0;
5457
+ return { ...primary, fallback };
5458
+ }
5459
+ return resolved;
5460
+ }
5461
+ function materializeProvider(rec, base, role, meta) {
5462
+ const url = rec.url || base.url;
4960
5463
  return {
4961
- enabled: await getSetting(db, "llm.vision.enabled") === "true",
4962
- url: await getSetting(db, "llm.vision.url") || base.url,
4963
- model: await getSetting(db, "llm.vision.model") || base.model,
4964
- apiKey: await getSetting(db, "llm.vision.api_key") || base.apiKey,
5464
+ enabled: base.enabled,
5465
+ url,
5466
+ model: rec.model || base.model,
5467
+ apiKey: resolveProviderApiKey(rec),
5468
+ apiFlavor: rec.apiFlavor || inferApiFlavor(url),
4965
5469
  locale: base.locale,
4966
- maxFrames: Number.isNaN(maxFrames) ? 100 : maxFrames
5470
+ providerName: meta.providerName,
5471
+ label: rec.label,
5472
+ source: meta.source,
5473
+ local: rec.local ?? isLocalEndpoint(url),
5474
+ ...role === "vision" ? { maxFrames: base.maxFrames } : {}
4967
5475
  };
4968
5476
  }
4969
- var LANGUAGE_NAMES = {
4970
- en: "English",
4971
- de: "German",
5477
+ async function getLegacyRoleConfig(db, role, enabled) {
5478
+ const base = await getLlmConfig(db);
5479
+ if (role === "vision") {
5480
+ const maxFramesStr = await getSetting(db, "llm.vision.max_frames");
5481
+ const parsed = maxFramesStr ? parseInt(maxFramesStr, 10) : 100;
5482
+ const url = await getSetting(db, "llm.vision.url") || base.url;
5483
+ let model = await getSetting(db, "llm.vision.model");
5484
+ if (!model) model = getCloudModelRecommendation(url) || base.model;
5485
+ return {
5486
+ enabled,
5487
+ url,
5488
+ model,
5489
+ apiKey: await getSetting(db, "llm.vision.api_key") || base.apiKey,
5490
+ apiFlavor: inferApiFlavor(url),
5491
+ locale: base.locale,
5492
+ source: "legacy",
5493
+ local: isLocalEndpoint(url),
5494
+ maxFrames: Number.isNaN(parsed) ? 100 : parsed
5495
+ };
5496
+ }
5497
+ return {
5498
+ enabled,
5499
+ url: base.url,
5500
+ model: base.model,
5501
+ apiKey: base.apiKey,
5502
+ apiFlavor: inferApiFlavor(base.url),
5503
+ locale: base.locale,
5504
+ source: "legacy",
5505
+ local: isLocalEndpoint(base.url)
5506
+ };
5507
+ }
5508
+ function assertChatCompletions(cfg) {
5509
+ if (cfg.apiFlavor !== "chat-completions") {
5510
+ throw new Error(
5511
+ `This role is configured for a "${cfg.apiFlavor}" provider, which is not supported here yet. Use a chat-completions provider for the recall role.`
5512
+ );
5513
+ }
5514
+ }
5515
+ var LANGUAGE_NAMES = {
5516
+ en: "English",
5517
+ de: "German",
4972
5518
  es: "Spanish",
4973
5519
  fr: "French",
4974
5520
  pt: "Portuguese",
@@ -5006,10 +5552,11 @@ async function readChatContent(res, label) {
5006
5552
  return content.trim();
5007
5553
  }
5008
5554
  async function generateQuestionViaLLM(db, input8) {
5009
- const cfg = await getLlmConfig(db);
5555
+ const cfg = await getProviderForRole(db, "recall");
5010
5556
  if (!cfg.enabled) {
5011
5557
  throw new Error("LLM integration is disabled in settings (llm.enabled)");
5012
5558
  }
5559
+ assertChatCompletions(cfg);
5013
5560
  const bloom = input8.bloomLevel >= 1 && input8.bloomLevel <= 5 ? input8.bloomLevel : 1;
5014
5561
  const verb = BLOOM_VERBS2[bloom];
5015
5562
  const langName = LANGUAGE_NAMES[cfg.locale] || "English";
@@ -5049,10 +5596,11 @@ Active-Recall Question:`;
5049
5596
  return readChatContent(res, "LLM request");
5050
5597
  }
5051
5598
  async function evaluateAnswerViaLLM(db, input8) {
5052
- const cfg = await getLlmConfig(db);
5599
+ const cfg = await getProviderForRole(db, "recall");
5053
5600
  if (!cfg.enabled) {
5054
5601
  throw new Error("LLM integration is disabled in settings (llm.enabled)");
5055
5602
  }
5603
+ assertChatCompletions(cfg);
5056
5604
  const langName = LANGUAGE_NAMES[cfg.locale] || "English";
5057
5605
  const ratingPrefix = LOCALIZED_RATING_PREFIX[cfg.locale] || "Suggested rating";
5058
5606
  const systemPrompt = `You are ZAM, an extremely warm, encouraging, and patient skills trainer.
@@ -5102,10 +5650,11 @@ Evaluation:`;
5102
5650
  return readChatContent(res, "LLM evaluation");
5103
5651
  }
5104
5652
  async function translateQuestionViaLLM(db, question) {
5105
- const cfg = await getLlmConfig(db);
5653
+ const cfg = await getProviderForRole(db, "recall");
5106
5654
  if (!cfg.enabled) {
5107
5655
  throw new Error("LLM integration is disabled in settings");
5108
5656
  }
5657
+ assertChatCompletions(cfg);
5109
5658
  const targetLang = LANGUAGE_NAMES[cfg.locale] || "English";
5110
5659
  const systemPrompt = `You are a highly precise translator. Translate the given active-recall question into clear, natural ${targetLang}.
5111
5660
  Output ONLY the raw translation. Do not include any headers, preamble, quotes, or conversational filler.`;
@@ -5159,38 +5708,174 @@ async function getAvailableModels(url, apiKey = DEFAULT_LLM_API_KEY) {
5159
5708
  return [];
5160
5709
  }
5161
5710
  }
5162
- async function checkVisionReadiness(db) {
5163
- const { enabled, url, model, apiKey } = await getVisionConfig(db);
5164
- const visionModelSetting = await getSetting(db, "llm.vision.model");
5165
- const visionModelExplicit = !!visionModelSetting;
5166
- let online = false;
5167
- let availableModels = [];
5168
- let modelAvailable = false;
5169
- if (enabled) {
5170
- online = await isLlmOnline(url);
5171
- if (online) {
5172
- availableModels = await getAvailableModels(url, apiKey);
5173
- modelAvailable = availableModels.length === 0 || availableModels.some(
5174
- (candidate) => candidate.toLowerCase() === model.toLowerCase()
5175
- );
5176
- }
5711
+ function isLocalEndpoint(url) {
5712
+ return url.includes("localhost") || url.includes("127.0.0.1") || url.includes("[::1]") || url.includes("::1");
5713
+ }
5714
+ async function checkProviderEndpoint(endpoint) {
5715
+ const online = await isLlmOnline(endpoint.url);
5716
+ if (!online) {
5717
+ return {
5718
+ endpoint,
5719
+ online: false,
5720
+ availableModels: [],
5721
+ modelAvailable: false
5722
+ };
5177
5723
  }
5724
+ const availableModels = await getAvailableModels(
5725
+ endpoint.url,
5726
+ endpoint.apiKey
5727
+ );
5728
+ const modelAvailable = availableModels.length === 0 || availableModels.some(
5729
+ (candidate) => candidate.toLowerCase() === endpoint.model.toLowerCase()
5730
+ );
5731
+ return { endpoint, online, availableModels, modelAvailable };
5732
+ }
5733
+ function isEndpointUsable(readiness) {
5734
+ return readiness.online && readiness.modelAvailable;
5735
+ }
5736
+ function providerChain(primary) {
5737
+ return [primary, ...primary.fallback ? [primary.fallback] : []];
5738
+ }
5739
+ async function checkProviderChain(primary) {
5740
+ let first;
5741
+ for (const endpoint of providerChain(primary)) {
5742
+ const readiness = await checkProviderEndpoint(endpoint);
5743
+ first ??= readiness;
5744
+ if (isEndpointUsable(readiness)) {
5745
+ return { primary: first, firstUsable: readiness };
5746
+ }
5747
+ }
5748
+ return { primary: first };
5749
+ }
5750
+ async function isVisionProviderModelExplicit(db, active) {
5751
+ if (await getSetting(db, "llm.vision.model")) return true;
5752
+ const providers = await readJsonSetting(db, "llm.providers");
5753
+ const roles = await readJsonSetting(db, "llm.roles");
5754
+ const binding = roles?.vision;
5755
+ const sharedExplicit = [binding?.primary, binding?.fallback].some((id) => {
5756
+ if (!id) return false;
5757
+ const rec = providers?.[id];
5758
+ return rec?.model === active.model && (rec.url === void 0 || rec.url === active.url);
5759
+ });
5760
+ if (sharedExplicit) return true;
5761
+ const machine = getMachineAiConfig();
5762
+ const machineBinding = machine.roles?.vision;
5763
+ return [machineBinding?.primary, machineBinding?.fallback].some((id) => {
5764
+ if (!id) return false;
5765
+ const rec = machine.providers?.[id];
5766
+ return rec?.model === active.model && (rec.url === void 0 || rec.url === active.url);
5767
+ });
5768
+ }
5769
+ async function checkVisionReadiness(db) {
5770
+ const cfg = await getProviderForRole(db, "vision");
5771
+ const chain = cfg.enabled ? await checkProviderChain(cfg) : void 0;
5772
+ const selected = chain?.firstUsable ?? chain?.primary;
5773
+ const active = selected?.endpoint ?? cfg;
5774
+ const online = selected?.online ?? false;
5775
+ const availableModels = selected?.availableModels ?? [];
5776
+ const modelAvailable = selected?.modelAvailable ?? false;
5777
+ const visionModelExplicit = await isVisionProviderModelExplicit(db, active);
5178
5778
  let warning;
5179
- if (enabled && online && modelAvailable && !visionModelExplicit) {
5180
- warning = `No explicit vision model configured (llm.vision.model). Falling back to base model "${model}", which may not support image input. Set a multimodal model: zam settings set llm.vision.model <model>`;
5779
+ if (cfg.enabled && online && modelAvailable && !visionModelExplicit) {
5780
+ const cloudRec = getCloudModelRecommendation(active.url);
5781
+ if (cloudRec && active.model === cloudRec) {
5782
+ } else {
5783
+ warning = `No explicit vision model configured (llm.vision.model). Falling back to base model "${active.model}", which may not support image input. Set a multimodal model: zam settings set llm.vision.model <model>`;
5784
+ }
5181
5785
  }
5182
5786
  return {
5183
- enabled,
5787
+ enabled: cfg.enabled,
5184
5788
  online,
5185
- url,
5186
- model,
5789
+ url: active.url,
5790
+ model: active.model,
5187
5791
  modelAvailable,
5188
5792
  availableModels,
5189
- usable: enabled && online && modelAvailable,
5793
+ usable: cfg.enabled && online && modelAvailable,
5190
5794
  visionModelExplicit,
5191
5795
  warning
5192
5796
  };
5193
5797
  }
5798
+ function summarizeFallback(provider) {
5799
+ if (!provider) return void 0;
5800
+ return {
5801
+ providerName: provider.providerName,
5802
+ label: provider.label,
5803
+ source: provider.source,
5804
+ url: provider.url,
5805
+ model: provider.model,
5806
+ apiFlavor: provider.apiFlavor,
5807
+ local: provider.local
5808
+ };
5809
+ }
5810
+ async function getProviderRoleStatus(db, role) {
5811
+ const cfg = await getProviderForRole(db, role);
5812
+ const unsupportedProvider = role !== "vision" && cfg.apiFlavor !== "chat-completions";
5813
+ if (!cfg.enabled) {
5814
+ return {
5815
+ role,
5816
+ enabled: false,
5817
+ providerName: cfg.providerName,
5818
+ label: cfg.label,
5819
+ source: cfg.source,
5820
+ url: cfg.url,
5821
+ model: cfg.model,
5822
+ apiFlavor: cfg.apiFlavor,
5823
+ local: cfg.local,
5824
+ online: false,
5825
+ modelAvailable: false,
5826
+ availableModels: [],
5827
+ usable: false,
5828
+ reason: "disabled",
5829
+ fallback: summarizeFallback(cfg.fallback)
5830
+ };
5831
+ }
5832
+ if (unsupportedProvider) {
5833
+ return {
5834
+ role,
5835
+ enabled: true,
5836
+ providerName: cfg.providerName,
5837
+ label: cfg.label,
5838
+ source: cfg.source,
5839
+ url: cfg.url,
5840
+ model: cfg.model,
5841
+ apiFlavor: cfg.apiFlavor,
5842
+ local: cfg.local,
5843
+ online: false,
5844
+ modelAvailable: false,
5845
+ availableModels: [],
5846
+ usable: false,
5847
+ reason: "unsupported-provider",
5848
+ fallback: summarizeFallback(cfg.fallback)
5849
+ };
5850
+ }
5851
+ let selected;
5852
+ if (role === "vision") {
5853
+ const chain = await checkProviderChain(cfg);
5854
+ selected = chain.firstUsable ?? chain.primary;
5855
+ } else {
5856
+ selected = await checkProviderEndpoint(cfg);
5857
+ }
5858
+ const active = selected.endpoint;
5859
+ const usable = selected.online && selected.modelAvailable;
5860
+ const reason = usable ? void 0 : selected.online ? "model-not-found" : "offline";
5861
+ return {
5862
+ role,
5863
+ enabled: true,
5864
+ providerName: active.providerName,
5865
+ label: active.label,
5866
+ source: active.source,
5867
+ url: active.url,
5868
+ model: active.model,
5869
+ apiFlavor: active.apiFlavor,
5870
+ local: active.local,
5871
+ online: selected.online,
5872
+ modelAvailable: selected.modelAvailable,
5873
+ availableModels: selected.availableModels,
5874
+ usable,
5875
+ reason,
5876
+ fallback: summarizeFallback(cfg.fallback)
5877
+ };
5878
+ }
5194
5879
  function detectRunner(url, model) {
5195
5880
  let runner = "unknown";
5196
5881
  let port = "8000";
@@ -5211,16 +5896,16 @@ function spawnLocalRunner(url, model) {
5211
5896
  const { runner, port } = detectRunner(url, model);
5212
5897
  try {
5213
5898
  if (runner === "fastflowlm") {
5214
- const flmExe = existsSync12("C:\\Program Files\\flm\\flm.exe") ? "C:\\Program Files\\flm\\flm.exe" : "flm";
5899
+ const flmExe = existsSync14("C:\\Program Files\\flm\\flm.exe") ? "C:\\Program Files\\flm\\flm.exe" : "flm";
5215
5900
  if (!hasCommand("flm") && flmExe === "flm") return;
5216
- spawn(flmExe, ["serve", model, "--port", port], {
5901
+ spawn2(flmExe, ["serve", model, "--port", port], {
5217
5902
  detached: true,
5218
5903
  stdio: "ignore",
5219
5904
  windowsHide: true
5220
5905
  }).unref();
5221
5906
  } else if (runner === "ollama" || runner === "generic") {
5222
5907
  if (!hasCommand("ollama")) return;
5223
- spawn("ollama", ["serve"], {
5908
+ spawn2("ollama", ["serve"], {
5224
5909
  detached: true,
5225
5910
  stdio: "ignore",
5226
5911
  windowsHide: true
@@ -5232,7 +5917,7 @@ function spawnLocalRunner(url, model) {
5232
5917
  async function startLocalRunner(url, model, locale) {
5233
5918
  const { runner, port } = detectRunner(url, model);
5234
5919
  if (runner === "fastflowlm") {
5235
- const flmExe = existsSync12("C:\\Program Files\\flm\\flm.exe") ? "C:\\Program Files\\flm\\flm.exe" : "flm";
5920
+ const flmExe = existsSync14("C:\\Program Files\\flm\\flm.exe") ? "C:\\Program Files\\flm\\flm.exe" : "flm";
5236
5921
  if (!hasCommand("flm") && flmExe === "flm") {
5237
5922
  console.warn(
5238
5923
  "\x1B[31m\u2717 FastFlowLM is configured but could not be found on the system.\x1B[0m"
@@ -5245,7 +5930,7 @@ async function startLocalRunner(url, model, locale) {
5245
5930
  `\x1B[36mStarting FastFlowLM serve process: ${flmExe} ${args.join(" ")}\x1B[0m`
5246
5931
  );
5247
5932
  try {
5248
- spawn(flmExe, args, {
5933
+ spawn2(flmExe, args, {
5249
5934
  detached: true,
5250
5935
  stdio: "ignore",
5251
5936
  windowsHide: true
@@ -5266,7 +5951,7 @@ async function startLocalRunner(url, model, locale) {
5266
5951
  }
5267
5952
  console.log("\x1B[36mStarting Ollama serve process: ollama serve\x1B[0m");
5268
5953
  try {
5269
- spawn("ollama", ["serve"], {
5954
+ spawn2("ollama", ["serve"], {
5270
5955
  detached: true,
5271
5956
  stdio: "ignore",
5272
5957
  windowsHide: true
@@ -5289,7 +5974,7 @@ async function startLocalRunner(url, model, locale) {
5289
5974
  let attempts = 0;
5290
5975
  const dotsPerLine = 30;
5291
5976
  while (true) {
5292
- await new Promise((resolve5) => setTimeout(resolve5, 500));
5977
+ await new Promise((resolve8) => setTimeout(resolve8, 500));
5293
5978
  if (await isLlmOnline(url)) {
5294
5979
  if (attempts > 0) process.stdout.write("\n");
5295
5980
  return true;
@@ -5314,12 +5999,18 @@ async function startLocalRunner(url, model, locale) {
5314
5999
  }
5315
6000
  }
5316
6001
  async function ensureLocalLlmRunning(db) {
5317
- const cfg = await getLlmConfig(db);
6002
+ const cfg = await getProviderForRole(db, "recall");
5318
6003
  if (!cfg.enabled) {
5319
6004
  return { usable: false, reason: "disabled" };
5320
6005
  }
6006
+ if (cfg.apiFlavor !== "chat-completions") {
6007
+ console.warn(
6008
+ `\x1B[31m\u2717 Recall is configured for "${cfg.apiFlavor}", but active recall currently supports chat-completions providers only.\x1B[0m`
6009
+ );
6010
+ return { usable: false, reason: "unsupported-provider" };
6011
+ }
5321
6012
  const { url, model, apiKey, locale } = cfg;
5322
- const isLocal = url.includes("localhost") || url.includes("127.0.0.1");
6013
+ const isLocal = isLocalEndpoint(url);
5323
6014
  console.log(`Checking if local LLM server is online at ${url}...`);
5324
6015
  let online = await isLlmOnline(url);
5325
6016
  if (!online && isLocal) {
@@ -5353,8 +6044,9 @@ async function ensureLocalLlmRunning(db) {
5353
6044
  }
5354
6045
  async function ensureLlmReadyHeadless(db, opts = {}) {
5355
6046
  const timeoutMs = opts.timeoutMs ?? 25e3;
5356
- const { enabled, url, model, apiKey } = await getLlmConfig(db);
5357
- if (!enabled) {
6047
+ const cfg = await getProviderForRole(db, "recall");
6048
+ const { url, model, apiKey } = cfg;
6049
+ if (!cfg.enabled) {
5358
6050
  return {
5359
6051
  usable: false,
5360
6052
  reason: "disabled",
@@ -5363,7 +6055,16 @@ async function ensureLlmReadyHeadless(db, opts = {}) {
5363
6055
  availableModels: []
5364
6056
  };
5365
6057
  }
5366
- const isLocal = url.includes("localhost") || url.includes("127.0.0.1");
6058
+ if (cfg.apiFlavor !== "chat-completions") {
6059
+ return {
6060
+ usable: false,
6061
+ reason: "unsupported-provider",
6062
+ online: false,
6063
+ model,
6064
+ availableModels: []
6065
+ };
6066
+ }
6067
+ const isLocal = isLocalEndpoint(url);
5367
6068
  let online = await isLlmOnline(url);
5368
6069
  if (!online && isLocal) {
5369
6070
  spawnLocalRunner(url, model);
@@ -5428,8 +6129,8 @@ async function fetchWithInteractiveTimeout(url, options = {}) {
5428
6129
  const dotsPerLine = 30;
5429
6130
  while (true) {
5430
6131
  let timeoutId;
5431
- const timeoutPromise = new Promise((resolve5) => {
5432
- timeoutId = setTimeout(() => resolve5("timeout"), timeoutMs);
6132
+ const timeoutPromise = new Promise((resolve8) => {
6133
+ timeoutId = setTimeout(() => resolve8("timeout"), timeoutMs);
5433
6134
  });
5434
6135
  const dotsInterval = setInterval(() => {
5435
6136
  process.stdout.write(".");
@@ -5502,8 +6203,8 @@ async function ensureHighQualityQuestion(db, token) {
5502
6203
  // src/cli/llm/vision.ts
5503
6204
  import { randomBytes } from "crypto";
5504
6205
  import { readFileSync as readFileSync9 } from "fs";
5505
- import { tmpdir } from "os";
5506
- import { basename as basename2, join as join12 } from "path";
6206
+ import { tmpdir as tmpdir2 } from "os";
6207
+ import { basename as basename3, join as join14 } from "path";
5507
6208
  var LANGUAGE_NAMES2 = {
5508
6209
  en: "English",
5509
6210
  de: "German",
@@ -5530,7 +6231,7 @@ var ACTION_TYPES2 = /* @__PURE__ */ new Set([
5530
6231
  "window-change"
5531
6232
  ]);
5532
6233
  async function observeUiSnapshotViaLLM(db, input8) {
5533
- const cfg = await getVisionConfig(db);
6234
+ const cfg = await getProviderForRole(db, "vision");
5534
6235
  if (!cfg.enabled) {
5535
6236
  throw new Error(
5536
6237
  "Vision observation is disabled in settings (llm.vision.enabled)"
@@ -5539,21 +6240,21 @@ async function observeUiSnapshotViaLLM(db, input8) {
5539
6240
  const imageUrls = [];
5540
6241
  const isVideo = /\.(mp4|mov|m4v|avi|mkv|webm)$/i.test(input8.imagePath);
5541
6242
  if (isVideo) {
5542
- const { mkdirSync: mkdirSync12, readdirSync: readdirSync3, rmSync: rmSync3 } = await import("fs");
5543
- const { execSync: execSync7 } = await import("child_process");
5544
- const tempDir = join12(
5545
- tmpdir(),
6243
+ const { mkdirSync: mkdirSync13, readdirSync: readdirSync3, rmSync: rmSync3 } = await import("fs");
6244
+ const { execSync: execSync6 } = await import("child_process");
6245
+ const tempDir = join14(
6246
+ tmpdir2(),
5546
6247
  `zam-frames-${randomBytes(4).toString("hex")}`
5547
6248
  );
5548
- mkdirSync12(tempDir, { recursive: true });
6249
+ mkdirSync13(tempDir, { recursive: true });
5549
6250
  try {
5550
- execSync7(
6251
+ execSync6(
5551
6252
  `ffmpeg -i "${input8.imagePath}" -vf "fps=1/3,scale=1280:-1" -vsync vfr "${tempDir}/frame_%03d.png"`,
5552
6253
  { stdio: "ignore" }
5553
6254
  );
5554
6255
  let files = readdirSync3(tempDir).filter((f) => f.endsWith(".png")).sort();
5555
6256
  if (files.length === 0) {
5556
- execSync7(
6257
+ execSync6(
5557
6258
  `ffmpeg -i "${input8.imagePath}" -vframes 1 "${tempDir}/frame_001.png"`,
5558
6259
  { stdio: "ignore" }
5559
6260
  );
@@ -5574,7 +6275,7 @@ async function observeUiSnapshotViaLLM(db, input8) {
5574
6275
  }
5575
6276
  }
5576
6277
  for (const file of sampledFiles) {
5577
- const bytes = readFileSync9(join12(tempDir, file));
6278
+ const bytes = readFileSync9(join14(tempDir, file));
5578
6279
  imageUrls.push(`data:image/png;base64,${bytes.toString("base64")}`);
5579
6280
  }
5580
6281
  } finally {
@@ -5592,42 +6293,92 @@ async function observeUiSnapshotViaLLM(db, input8) {
5592
6293
  if (imageUrls.length === 0) {
5593
6294
  throw new Error("No image data available for vision analysis");
5594
6295
  }
5595
- const model = input8.model ?? cfg.model;
5596
- const content = await requestVisionDraft({
5597
- url: cfg.url,
5598
- apiKey: cfg.apiKey || DEFAULT_LLM_API_KEY,
5599
- model,
5600
- locale: cfg.locale,
5601
- imageUrls,
5602
- input: input8
5603
- });
5604
- let draft;
5605
- try {
5606
- draft = extractDraft(content);
5607
- } catch {
5608
- return uncertainReport(
5609
- input8,
5610
- "Vision model returned output that could not be parsed as JSON."
5611
- );
6296
+ const endpoints = [
6297
+ {
6298
+ url: cfg.url,
6299
+ apiKey: cfg.apiKey || DEFAULT_LLM_API_KEY,
6300
+ model: input8.model ?? cfg.model,
6301
+ apiFlavor: cfg.apiFlavor
6302
+ }
6303
+ ];
6304
+ if (cfg.fallback) {
6305
+ endpoints.push({
6306
+ url: cfg.fallback.url,
6307
+ apiKey: cfg.fallback.apiKey || DEFAULT_LLM_API_KEY,
6308
+ model: cfg.fallback.model,
6309
+ apiFlavor: cfg.fallback.apiFlavor
6310
+ });
5612
6311
  }
5613
- const report = buildReport(input8, draft);
5614
- if (!isUiObservationReport(report)) {
6312
+ let lastRequestError;
6313
+ let sawUnparseableDraft = false;
6314
+ let sawInvalidDraft = false;
6315
+ for (const endpoint of endpoints) {
6316
+ let content;
6317
+ try {
6318
+ content = await requestVisionDraft({
6319
+ ...endpoint,
6320
+ locale: cfg.locale,
6321
+ imageUrls,
6322
+ input: input8
6323
+ });
6324
+ } catch (err) {
6325
+ lastRequestError = err;
6326
+ continue;
6327
+ }
6328
+ let draft;
6329
+ try {
6330
+ draft = extractDraft(content);
6331
+ } catch {
6332
+ sawUnparseableDraft = true;
6333
+ continue;
6334
+ }
6335
+ const report = buildReport(input8, draft);
6336
+ if (isUiObservationReport(report)) {
6337
+ return report;
6338
+ }
6339
+ sawInvalidDraft = true;
6340
+ }
6341
+ if (lastRequestError && !sawUnparseableDraft && !sawInvalidDraft) {
6342
+ throw lastRequestError;
6343
+ }
6344
+ if (sawInvalidDraft) {
5615
6345
  return uncertainReport(
5616
6346
  input8,
5617
6347
  "Vision model returned an invalid UI report draft."
5618
6348
  );
5619
6349
  }
5620
- return report;
6350
+ return uncertainReport(
6351
+ input8,
6352
+ "Vision model returned output that could not be parsed as JSON."
6353
+ );
5621
6354
  }
5622
- async function requestVisionDraft(args) {
5623
- const language = LANGUAGE_NAMES2[args.locale] ?? "English";
5624
- const schema = `{
6355
+ var VISION_SYSTEM_PROMPT = "You are ZAM's UI observer. Return only strict JSON. Do not include markdown, prose, or fields outside the requested schema.";
6356
+ function visionSchema(language) {
6357
+ return `{
5625
6358
  "kind": "progress | step-completed | error | help-seeking | uncertain",
5626
6359
  "summary": "short factual UI summary in ${language}",
5627
6360
  "actions": [{"type": "click | shortcut | typing | scroll | window-change", "target": "optional UI target", "result": "optional visible result"}],
5628
6361
  "candidateTokens": [{"slug": "optional-kebab-case-skill-token", "confidence": 0.0, "rationale": "why this token may matter"}],
5629
6362
  "confidence": 0.0
5630
6363
  }`;
6364
+ }
6365
+ function visionUserText(args, language) {
6366
+ const intro = args.imageUrls.length > 1 ? "Observe this sequence of Windows/macOS application snapshots showing a task performed over time." : "Observe this Windows/macOS application snapshot for a learning session.";
6367
+ return `${intro}
6368
+ Application process: ${args.input.application.processName}
6369
+ Window title: ${args.input.application.windowTitle ?? "(unknown)"}
6370
+
6371
+ Return this JSON draft only:
6372
+ ${visionSchema(language)}`;
6373
+ }
6374
+ async function requestVisionDraft(args) {
6375
+ if (args.apiFlavor === "anthropic-messages") {
6376
+ return requestAnthropicVisionDraft(args);
6377
+ }
6378
+ return requestChatCompletionsVisionDraft(args);
6379
+ }
6380
+ async function requestChatCompletionsVisionDraft(args) {
6381
+ const language = LANGUAGE_NAMES2[args.locale] ?? "English";
5631
6382
  const res = await fetchWithInteractiveTimeout(
5632
6383
  `${args.url}/chat/completions`,
5633
6384
  {
@@ -5639,27 +6390,11 @@ async function requestVisionDraft(args) {
5639
6390
  body: JSON.stringify({
5640
6391
  model: args.model,
5641
6392
  messages: [
5642
- {
5643
- role: "system",
5644
- content: "You are ZAM's UI observer. Return only strict JSON. Do not include markdown, prose, or fields outside the requested schema."
5645
- },
6393
+ { role: "system", content: VISION_SYSTEM_PROMPT },
5646
6394
  {
5647
6395
  role: "user",
5648
6396
  content: [
5649
- {
5650
- type: "text",
5651
- text: args.imageUrls.length > 1 ? `Observe this sequence of Windows/macOS application snapshots showing a task performed over time.
5652
- Application process: ${args.input.application.processName}
5653
- Window title: ${args.input.application.windowTitle ?? "(unknown)"}
5654
-
5655
- Return this JSON draft only:
5656
- ${schema}` : `Observe this Windows/macOS application snapshot for a learning session.
5657
- Application process: ${args.input.application.processName}
5658
- Window title: ${args.input.application.windowTitle ?? "(unknown)"}
5659
-
5660
- Return this JSON draft only:
5661
- ${schema}`
5662
- },
6397
+ { type: "text", text: visionUserText(args, language) },
5663
6398
  ...args.imageUrls.map((url) => ({
5664
6399
  type: "image_url",
5665
6400
  image_url: { url }
@@ -5702,6 +6437,64 @@ ${schema}`
5702
6437
  }
5703
6438
  return content.trim();
5704
6439
  }
6440
+ function dataUrlToAnthropicImage(dataUrl) {
6441
+ const match = dataUrl.match(/^data:(image\/[a-zA-Z]+);base64,(.+)$/);
6442
+ if (!match) {
6443
+ throw new Error("Unsupported image data for Anthropic vision request");
6444
+ }
6445
+ return {
6446
+ type: "image",
6447
+ source: { type: "base64", media_type: match[1], data: match[2] }
6448
+ };
6449
+ }
6450
+ async function requestAnthropicVisionDraft(args) {
6451
+ const language = LANGUAGE_NAMES2[args.locale] ?? "English";
6452
+ const base = args.url.replace(/\/+$/, "").replace(/\/v1$/, "");
6453
+ const res = await fetchWithInteractiveTimeout(`${base}/v1/messages`, {
6454
+ method: "POST",
6455
+ headers: {
6456
+ "Content-Type": "application/json",
6457
+ "x-api-key": args.apiKey,
6458
+ "anthropic-version": "2023-06-01"
6459
+ },
6460
+ body: JSON.stringify({
6461
+ model: args.model,
6462
+ max_tokens: args.input.maxTokens ?? 450,
6463
+ system: VISION_SYSTEM_PROMPT,
6464
+ messages: [
6465
+ {
6466
+ role: "user",
6467
+ content: [
6468
+ { type: "text", text: visionUserText(args, language) },
6469
+ ...args.imageUrls.map(dataUrlToAnthropicImage)
6470
+ ]
6471
+ }
6472
+ ]
6473
+ }),
6474
+ locale: args.locale,
6475
+ hardTimeoutMs: args.input.hardTimeoutMs ?? 18e4
6476
+ });
6477
+ if (!res.ok) {
6478
+ const errorText = await res.text().catch(() => "");
6479
+ throw new Error(
6480
+ `Vision LLM request failed: ${res.statusText} (${res.status}) - ${errorText}`
6481
+ );
6482
+ }
6483
+ const data = await res.json();
6484
+ if (data.error !== void 0) {
6485
+ throw new Error(`Vision model failed: ${formatModelError(data.error)}`);
6486
+ }
6487
+ if (data.stop_reason === "refusal") {
6488
+ throw new Error("Vision model refused the request (safety classifier).");
6489
+ }
6490
+ const text = data.content?.find(
6491
+ (b) => b.type === "text" && typeof b.text === "string"
6492
+ )?.text;
6493
+ if (!text) {
6494
+ throw new Error("Empty response from vision model");
6495
+ }
6496
+ return text.trim();
6497
+ }
5705
6498
  function extractDraft(content) {
5706
6499
  const fenced = content.match(/```(?:json)?\s*([\s\S]*?)```/i);
5707
6500
  const candidate = (fenced?.[1] ?? content).trim();
@@ -5730,7 +6523,7 @@ function buildReport(input8, draft) {
5730
6523
  evidence: [
5731
6524
  {
5732
6525
  type: "keyframe",
5733
- ref: input8.evidenceRef ?? basename2(input8.imagePath),
6526
+ ref: input8.evidenceRef ?? basename3(input8.imagePath),
5734
6527
  redacted: input8.redacted ?? false
5735
6528
  }
5736
6529
  ],
@@ -5752,7 +6545,7 @@ function uncertainReport(input8, summary) {
5752
6545
  evidence: [
5753
6546
  {
5754
6547
  type: "keyframe",
5755
- ref: input8.evidenceRef ?? basename2(input8.imagePath),
6548
+ ref: input8.evidenceRef ?? basename3(input8.imagePath),
5756
6549
  redacted: input8.redacted ?? false
5757
6550
  }
5758
6551
  ],
@@ -5862,50 +6655,728 @@ function jsonOut(data) {
5862
6655
  console.log(JSON.stringify(data, null, 2));
5863
6656
  }
5864
6657
 
5865
- // src/cli/commands/bridge.ts
5866
- var isServeMode = false;
5867
- function jsonOut2(data) {
5868
- console.log(JSON.stringify(data, null, 2));
6658
+ // src/cli/commands/workspace.ts
6659
+ import { execFileSync as execFileSync3 } from "child_process";
6660
+ import { existsSync as existsSync16, mkdirSync as mkdirSync9, writeFileSync as writeFileSync8 } from "fs";
6661
+ import { homedir as homedir9 } from "os";
6662
+ import { join as join16, resolve as resolve4 } from "path";
6663
+ import { confirm, input } from "@inquirer/prompts";
6664
+ import { Command as Command3 } from "commander";
6665
+
6666
+ // src/cli/commands/setup.ts
6667
+ import {
6668
+ copyFileSync as copyFileSync2,
6669
+ existsSync as existsSync15,
6670
+ mkdirSync as mkdirSync8,
6671
+ readFileSync as readFileSync10,
6672
+ writeFileSync as writeFileSync7
6673
+ } from "fs";
6674
+ import { basename as basename4, dirname as dirname5, join as join15, resolve as resolve3 } from "path";
6675
+ import { fileURLToPath as fileURLToPath2 } from "url";
6676
+ import { Command as Command2 } from "commander";
6677
+ var packageRoot = [
6678
+ fileURLToPath2(new URL("../..", import.meta.url)),
6679
+ fileURLToPath2(new URL("../../..", import.meta.url))
6680
+ ].find((candidate) => existsSync15(join15(candidate, "package.json"))) ?? fileURLToPath2(new URL("../..", import.meta.url));
6681
+ var ALL_SETUP_AGENTS = ["claude", "copilot", "codex", "agent"];
6682
+ var SKILL_PAIRS = [
6683
+ {
6684
+ from: join15(packageRoot, ".claude", "skills", "zam", "SKILL.md"),
6685
+ to: join15(".claude", "skills", "zam", "SKILL.md"),
6686
+ agents: ["claude"]
6687
+ },
6688
+ {
6689
+ from: join15(packageRoot, ".agent", "skills", "zam", "SKILL.md"),
6690
+ to: join15(".agent", "skills", "zam", "SKILL.md"),
6691
+ agents: ["agent"]
6692
+ },
6693
+ {
6694
+ from: join15(packageRoot, ".agents", "skills", "zam", "SKILL.md"),
6695
+ to: join15(".agents", "skills", "zam", "SKILL.md"),
6696
+ agents: ["copilot", "codex"]
6697
+ }
6698
+ ];
6699
+ function parseSetupAgents(value) {
6700
+ if (!value || value.trim().toLowerCase() === "all") {
6701
+ return new Set(ALL_SETUP_AGENTS);
6702
+ }
6703
+ const aliases = {
6704
+ claude: ["claude"],
6705
+ copilot: ["copilot"],
6706
+ codex: ["codex"],
6707
+ agent: ["agent"],
6708
+ opencode: ["agent"]
6709
+ };
6710
+ const selected = /* @__PURE__ */ new Set();
6711
+ for (const raw of value.split(",")) {
6712
+ const key = raw.trim().toLowerCase();
6713
+ const mapped = aliases[key];
6714
+ if (!mapped) {
6715
+ throw new Error(
6716
+ `Unknown agent "${raw}". Use: all, claude, copilot, codex, agent.`
6717
+ );
6718
+ }
6719
+ for (const item of mapped) selected.add(item);
6720
+ }
6721
+ return selected;
5869
6722
  }
5870
- function jsonError(message) {
5871
- if (isServeMode) {
5872
- throw new Error(JSON.stringify({ error: message }));
6723
+ function copySkills(force, cwd = process.cwd(), agents = parseSetupAgents(), dryRun = false) {
6724
+ let anyAction = false;
6725
+ for (const { from, to, agents: pairAgents } of SKILL_PAIRS) {
6726
+ if (!pairAgents.some((agent) => agents.has(agent))) continue;
6727
+ const dest = join15(cwd, to);
6728
+ if (!existsSync15(from)) {
6729
+ console.warn(` warn source not found, skipping: ${from}`);
6730
+ continue;
6731
+ }
6732
+ if (existsSync15(dest) && !force) {
6733
+ console.log(` skip ${to} (already present \xE2\u20AC\u201D use --force to update)`);
6734
+ continue;
6735
+ }
6736
+ if (dryRun) {
6737
+ console.log(` would copy ${to}`);
6738
+ anyAction = true;
6739
+ continue;
6740
+ }
6741
+ mkdirSync8(dirname5(dest), { recursive: true });
6742
+ copyFileSync2(from, dest);
6743
+ console.log(` copy ${to}`);
6744
+ anyAction = true;
6745
+ }
6746
+ if (!anyAction && !force) {
6747
+ console.log(
6748
+ "\nSkill files are already up to date. Run with --force to overwrite."
6749
+ );
5873
6750
  }
5874
- console.log(JSON.stringify({ error: message }, null, 2));
5875
- process.exit(1);
5876
6751
  }
5877
- function parseNonNegativeIntegerOption(name, value) {
5878
- const parsed = Number(value);
5879
- if (!Number.isInteger(parsed) || parsed < 0) {
5880
- jsonError(`${name} must be a non-negative integer`);
6752
+ function formatDatabaseInitTarget(target) {
6753
+ switch (target.kind) {
6754
+ case "local":
6755
+ return `ZAM database at ${target.location} (local SQLite)`;
6756
+ case "turso-remote":
6757
+ return `ZAM database via Turso remote at ${target.location}`;
6758
+ case "turso-native":
6759
+ return `ZAM database via Turso native driver at ${target.location}`;
6760
+ case "turso-replica":
6761
+ return `ZAM database replica at ${target.location} syncing from ${target.syncUrl}`;
5881
6762
  }
5882
- return parsed;
5883
6763
  }
5884
- async function withDb2(fn) {
5885
- await withDb(fn, jsonError);
6764
+ async function initDatabase(skipInit) {
6765
+ if (skipInit) return;
6766
+ try {
6767
+ const target = getDatabaseTargetInfo();
6768
+ const db = await openDatabaseWithSync({ initialize: true });
6769
+ await db.close();
6770
+ console.log(` init ${formatDatabaseInitTarget(target)}`);
6771
+ } catch (err) {
6772
+ const msg = err.message;
6773
+ if (!msg.includes("already")) {
6774
+ console.warn(` warn database init: ${msg}`);
6775
+ } else {
6776
+ console.log(` skip database already initialized`);
6777
+ }
6778
+ }
5886
6779
  }
5887
- async function getReviewTarget2(db, cardId, userId) {
5888
- const target = await db.prepare(
5889
- `SELECT c.id AS card_id, c.token_id, c.user_id, t.slug
5890
- FROM cards c
5891
- JOIN tokens t ON t.id = c.token_id
5892
- WHERE c.id = ?`
5893
- ).get(cardId);
5894
- if (!target) {
5895
- jsonError(`Card not found: ${cardId}`);
6780
+ var ZAM_BLOCK_START = "<!-- ZAM:START -->";
6781
+ var ZAM_BLOCK_END = "<!-- ZAM:END -->";
6782
+ function upsertMarkedBlock(dest, blockBody, dryRun) {
6783
+ const block = `${ZAM_BLOCK_START}
6784
+ ${blockBody.trim()}
6785
+ ${ZAM_BLOCK_END}`;
6786
+ if (!existsSync15(dest)) {
6787
+ if (!dryRun) {
6788
+ mkdirSync8(dirname5(dest), { recursive: true });
6789
+ writeFileSync7(dest, `${block}
6790
+ `, "utf8");
6791
+ }
6792
+ return "write";
5896
6793
  }
5897
- if (target.user_id !== userId) {
5898
- jsonError(`Card ${cardId} does not belong to user ${userId}`);
6794
+ const existing = readFileSync10(dest, "utf8");
6795
+ if (existing.includes(block)) return "skip";
6796
+ const start = existing.indexOf(ZAM_BLOCK_START);
6797
+ const end = existing.indexOf(ZAM_BLOCK_END);
6798
+ const next = start >= 0 && end > start ? `${existing.slice(0, start)}${block}${existing.slice(end + ZAM_BLOCK_END.length)}` : `${existing.trimEnd()}
6799
+
6800
+ ${block}
6801
+ `;
6802
+ if (!dryRun) writeFileSync7(dest, next, "utf8");
6803
+ return start >= 0 && end > start ? "update" : "write";
6804
+ }
6805
+ function logInstructionAction(action, label, dryRun) {
6806
+ if (action === "skip") {
6807
+ console.log(` skip ${label} (ZAM block already present)`);
6808
+ } else {
6809
+ console.log(` ${dryRun ? "would " : ""}${action} ${label}`);
5899
6810
  }
5900
- return target;
5901
6811
  }
5902
- function parseTokenUpdates(opts) {
5903
- const updates = {};
5904
- if (opts.concept !== void 0) updates.concept = opts.concept;
5905
- if (opts.domain !== void 0) updates.domain = opts.domain;
5906
- if (opts.bloom !== void 0)
5907
- updates.bloom_level = Number(opts.bloom);
5908
- if (opts.context !== void 0) updates.context = opts.context;
6812
+ function writeClaudeMd(skipClaudeMd, cwd = process.cwd(), opts = {}) {
6813
+ if (skipClaudeMd) return;
6814
+ const dest = join15(cwd, "CLAUDE.md");
6815
+ if (existsSync15(dest)) {
6816
+ if (!opts.updateExisting) {
6817
+ console.log(` skip CLAUDE.md (already present)`);
6818
+ return;
6819
+ }
6820
+ const action = upsertMarkedBlock(
6821
+ dest,
6822
+ `## ZAM learning sessions
6823
+
6824
+ ZAM is available in this repository. Use the \`zam\` skill in Claude Code to turn real work into an observed learning session with active recall and FSRS scheduling.
6825
+
6826
+ - Skill files live under \`.claude/skills/zam/\`.
6827
+ - Fast-changing review data lives in \`~/.zam/\`, not in this repository.
6828
+ - Run \`zam setup --target . --agents claude --force\` after upgrading ZAM to refresh the skill.`,
6829
+ Boolean(opts.dryRun)
6830
+ );
6831
+ logInstructionAction(action, "CLAUDE.md", Boolean(opts.dryRun));
6832
+ return;
6833
+ }
6834
+ const name = basename4(cwd);
6835
+ const content = `# ZAM Personal Kernel \xE2\u20AC\u201D ${name}
6836
+
6837
+ This is a ZAM personal instance. ZAM builds lasting skills through spaced
6838
+ repetition during real work \xE2\u20AC\u201D not separate study sessions.
6839
+
6840
+ ## First time here?
6841
+ Run \`/setup\` in Claude Code or Gemini CLI to complete first-time setup.
6842
+
6843
+ ## Regular use
6844
+ Run \`/zam\` to start a learning session on whatever you are working on.
6845
+
6846
+ ## What lives here
6847
+ - \`beliefs/\` \xE2\u20AC\u201D your worldview, approved by git commit
6848
+ - \`goals/\` \xE2\u20AC\u201D your objectives, decomposed into tasks and learning tokens
6849
+
6850
+ ## Fast-changing data
6851
+ Learning tokens, cards, and review history live in local SQLite by default.
6852
+ Use \`zam connector setup turso\` to store cloud credentials in
6853
+ \`~/.zam/credentials.json\` and use a Turso database across machines.
6854
+ `;
6855
+ if (opts.dryRun) {
6856
+ console.log(` would write CLAUDE.md`);
6857
+ } else {
6858
+ writeFileSync7(dest, content, "utf8");
6859
+ console.log(` write CLAUDE.md`);
6860
+ }
6861
+ }
6862
+ function writeAgentsMd(skipAgentsMd, cwd = process.cwd(), opts = {}) {
6863
+ if (skipAgentsMd) return;
6864
+ const dest = join15(cwd, "AGENTS.md");
6865
+ if (existsSync15(dest)) {
6866
+ if (!opts.updateExisting) {
6867
+ console.log(` skip AGENTS.md (already present)`);
6868
+ return;
6869
+ }
6870
+ const action = upsertMarkedBlock(
6871
+ dest,
6872
+ `## ZAM learning sessions
6873
+
6874
+ ZAM is available in this repository. Select the \`zam\` skill through \`/skills\` or invoke \`$zam\` where supported to turn real work into an observed learning session with active recall and FSRS scheduling.
6875
+
6876
+ - Skill files live under \`.agents/skills/zam/\`.
6877
+ - Fast-changing review data lives in \`~/.zam/\`, not in this repository.
6878
+ - Run \`zam setup --target . --agents copilot,codex --force\` after upgrading ZAM to refresh the skill.`,
6879
+ Boolean(opts.dryRun)
6880
+ );
6881
+ logInstructionAction(action, "AGENTS.md", Boolean(opts.dryRun));
6882
+ return;
6883
+ }
6884
+ const name = basename4(cwd);
6885
+ const content = `# ZAM Personal Kernel - ${name}
6886
+
6887
+ This is a ZAM personal instance. ZAM builds lasting skills through spaced
6888
+ repetition during real work, not separate study sessions.
6889
+
6890
+ ## First time here?
6891
+ Run \`zam setup\` from the shell. When this repository includes
6892
+ \`.agents/skills/setup/\`, you can instead select \`setup\` through \`/skills\`
6893
+ or invoke \`$setup\`.
6894
+
6895
+ ## Regular use
6896
+ Select the \`zam\` skill through \`/skills\` or invoke \`$zam\` to start a
6897
+ learning session on whatever you are working on.
6898
+
6899
+ ## What lives here
6900
+ - \`beliefs/\` - your worldview, approved by git commit
6901
+ - \`goals/\` - your objectives, decomposed into tasks and learning tokens
6902
+
6903
+ ## Fast-changing data
6904
+ Learning tokens, cards, and review history live in local SQLite by default.
6905
+ Use \`zam connector setup turso\` to store cloud credentials in
6906
+ \`~/.zam/credentials.json\` and use a Turso database across machines.
6907
+
6908
+ ## Codex skills
6909
+ Codex discovers repository skills under \`.agents/skills/\`. Run
6910
+ \`zam setup --force\` after upgrading \`zam-core\` to refresh them.
6911
+ `;
6912
+ if (opts.dryRun) {
6913
+ console.log(` would write AGENTS.md`);
6914
+ } else {
6915
+ writeFileSync7(dest, content, "utf8");
6916
+ console.log(` write AGENTS.md`);
6917
+ }
6918
+ }
6919
+ function writeCopilotInstructions(cwd = process.cwd(), opts = {}) {
6920
+ const dest = join15(cwd, ".github", "copilot-instructions.md");
6921
+ const action = upsertMarkedBlock(
6922
+ dest,
6923
+ `## ZAM learning sessions
6924
+
6925
+ ZAM is available in this repository through \`.agents/skills/zam/\`. Use the \`zam\` skill from Copilot-compatible skill selection surfaces to turn real work into an observed learning session with active recall and FSRS scheduling.
6926
+
6927
+ - Fast-changing review data lives in \`~/.zam/\`, not in this repository.
6928
+ - Run \`zam setup --target . --agents copilot --force\` after upgrading ZAM to refresh the skill.`,
6929
+ Boolean(opts.dryRun)
6930
+ );
6931
+ logInstructionAction(
6932
+ action,
6933
+ ".github/copilot-instructions.md",
6934
+ Boolean(opts.dryRun)
6935
+ );
6936
+ }
6937
+ var setupCommand = new Command2("setup").description(
6938
+ "Distribute ZAM skill files into this personal instance and initialize the database"
6939
+ ).option(
6940
+ "--force",
6941
+ "overwrite existing skill files (use after upgrading zam)",
6942
+ false
6943
+ ).option("--skip-init", "skip database initialization", false).option("--skip-claude-md", "skip CLAUDE.md generation", false).option("--skip-agents-md", "skip AGENTS.md generation", false).option("--target <path>", "repository/workspace directory to set up").option(
6944
+ "--agents <list>",
6945
+ "comma-separated agents to wire: all, claude, copilot, codex, agent"
6946
+ ).option(
6947
+ "--dry-run",
6948
+ "show what would be written without changing files",
6949
+ false
6950
+ ).action(
6951
+ async (opts) => {
6952
+ let agents;
6953
+ try {
6954
+ agents = parseSetupAgents(opts.agents);
6955
+ } catch (err) {
6956
+ console.error(`Error: ${err.message}`);
6957
+ process.exit(1);
6958
+ }
6959
+ const target = resolve3(opts.target ?? process.cwd());
6960
+ const updateExistingInstructions = Boolean(opts.target) || opts.force;
6961
+ console.log(
6962
+ `Setting up ZAM in ${target}${opts.dryRun ? " (dry run)" : ""}
6963
+ `
6964
+ );
6965
+ copySkills(opts.force, target, agents, opts.dryRun);
6966
+ await initDatabase(opts.skipInit || opts.dryRun);
6967
+ if (agents.has("claude")) {
6968
+ writeClaudeMd(opts.skipClaudeMd, target, {
6969
+ dryRun: opts.dryRun,
6970
+ updateExisting: updateExistingInstructions
6971
+ });
6972
+ }
6973
+ if (agents.has("copilot") || agents.has("codex") || agents.has("agent")) {
6974
+ writeAgentsMd(opts.skipAgentsMd, target, {
6975
+ dryRun: opts.dryRun,
6976
+ updateExisting: updateExistingInstructions
6977
+ });
6978
+ }
6979
+ if (agents.has("copilot") && (opts.target || opts.agents)) {
6980
+ writeCopilotInstructions(target, {
6981
+ dryRun: opts.dryRun,
6982
+ updateExisting: updateExistingInstructions
6983
+ });
6984
+ }
6985
+ console.log(
6986
+ "\nDone. Run `zam whoami --set <your-id>` to set your identity. Start the `zam` skill with `/zam` in Claude/Gemini-compatible clients or `$zam` (or `/skills`) in Codex."
6987
+ );
6988
+ }
6989
+ );
6990
+
6991
+ // src/cli/commands/workspace.ts
6992
+ function runGit(cwd, args) {
6993
+ try {
6994
+ return execFileSync3("git", args, {
6995
+ cwd,
6996
+ stdio: "pipe",
6997
+ encoding: "utf8"
6998
+ }).trim();
6999
+ } catch (err) {
7000
+ throw new Error(`Git command failed: ${err.message}`);
7001
+ }
7002
+ }
7003
+ function ghRepoCreateArgs(repoName, repoVisibility) {
7004
+ return ["repo", "create", repoName, repoVisibility, "--source=.", "--push"];
7005
+ }
7006
+ function gitRemoteArgs(githubUrl, hasOrigin) {
7007
+ return hasOrigin ? ["remote", "set-url", "origin", githubUrl] : ["remote", "add", "origin", githubUrl];
7008
+ }
7009
+ var workspaceCommand = new Command3("workspace").description(
7010
+ "Manage your ZAM learning workspace"
7011
+ );
7012
+ var WORKSPACE_KINDS = [
7013
+ "personal",
7014
+ "team",
7015
+ "family",
7016
+ "community",
7017
+ "organization",
7018
+ "custom"
7019
+ ];
7020
+ var WORKSPACE_SOURCE_CONTROLS = [
7021
+ "github",
7022
+ "azure-devops",
7023
+ "git",
7024
+ "none"
7025
+ ];
7026
+ function parseWorkspaceKind(value) {
7027
+ const kind = (value ?? "custom").toLowerCase();
7028
+ if (WORKSPACE_KINDS.includes(kind)) {
7029
+ return kind;
7030
+ }
7031
+ throw new Error(
7032
+ `Invalid workspace kind: ${value}. Use ${WORKSPACE_KINDS.join(", ")}.`
7033
+ );
7034
+ }
7035
+ function parseWorkspaceSourceControl(value) {
7036
+ if (!value) return void 0;
7037
+ const source = value.toLowerCase();
7038
+ if (WORKSPACE_SOURCE_CONTROLS.includes(source)) {
7039
+ return source;
7040
+ }
7041
+ throw new Error(
7042
+ `Invalid source control: ${value}. Use ${WORKSPACE_SOURCE_CONTROLS.join(", ")}.`
7043
+ );
7044
+ }
7045
+ function parseScopes(value) {
7046
+ if (!value) return void 0;
7047
+ const scopes = value.split(",").map((item) => item.trim()).filter(Boolean);
7048
+ return scopes.length > 0 ? scopes : void 0;
7049
+ }
7050
+ function requireWorkspace(id) {
7051
+ const workspace = getConfiguredWorkspaces().find((item) => item.id === id);
7052
+ if (!workspace) {
7053
+ throw new Error(
7054
+ `Workspace "${id}" is not configured. Add it with: zam workspace add ${id} --path <dir>`
7055
+ );
7056
+ }
7057
+ return workspace;
7058
+ }
7059
+ workspaceCommand.command("list").description("List configured ZAM workspaces").option("--json", "Output as JSON").action((opts) => {
7060
+ const workspaces = getConfiguredWorkspaces();
7061
+ if (opts.json) {
7062
+ console.log(JSON.stringify({ workspaces }, null, 2));
7063
+ return;
7064
+ }
7065
+ console.log("Configured ZAM workspaces:\n");
7066
+ if (workspaces.length === 0) {
7067
+ console.log(
7068
+ " (none) \u2014 add one: zam workspace add personal --path <dir>"
7069
+ );
7070
+ return;
7071
+ }
7072
+ for (const workspace of workspaces) {
7073
+ const label = workspace.label ? ` (${workspace.label})` : "";
7074
+ console.log(` ${workspace.id}${label}`);
7075
+ console.log(` kind: ${workspace.kind}`);
7076
+ console.log(` path: ${workspace.path}`);
7077
+ if (workspace.sourceControl) {
7078
+ console.log(` source: ${workspace.sourceControl}`);
7079
+ }
7080
+ if (workspace.knowledgeScopes?.length) {
7081
+ console.log(` scopes: ${workspace.knowledgeScopes.join(", ")}`);
7082
+ }
7083
+ }
7084
+ });
7085
+ workspaceCommand.command("add <id>").description("Register an existing directory as a ZAM workspace").requiredOption("--path <dir>", "Existing workspace/repository directory").option(
7086
+ "--kind <kind>",
7087
+ `Workspace kind (${WORKSPACE_KINDS.join(" | ")})`,
7088
+ "custom"
7089
+ ).option("--label <label>", "Human-readable label").option(
7090
+ "--source-control <provider>",
7091
+ `Source-control provider (${WORKSPACE_SOURCE_CONTROLS.join(" | ")})`
7092
+ ).option("--scopes <list>", "Comma-separated knowledge scopes").option("--default-agent <id>", "Default agent harness for this workspace").action((id, opts) => {
7093
+ try {
7094
+ const path = resolve4(String(opts.path));
7095
+ if (!existsSync16(path)) {
7096
+ console.error(`Workspace path does not exist: ${path}`);
7097
+ process.exit(1);
7098
+ }
7099
+ const workspace = {
7100
+ id,
7101
+ kind: parseWorkspaceKind(opts.kind),
7102
+ path,
7103
+ ...opts.label ? { label: opts.label } : {},
7104
+ ...opts.sourceControl ? { sourceControl: parseWorkspaceSourceControl(opts.sourceControl) } : {},
7105
+ ...parseScopes(opts.scopes) ? { knowledgeScopes: parseScopes(opts.scopes) } : {},
7106
+ ...opts.defaultAgent ? { defaultAgent: opts.defaultAgent } : {}
7107
+ };
7108
+ upsertConfiguredWorkspace(workspace);
7109
+ console.log(`Registered workspace "${id}" at ${path}.`);
7110
+ } catch (err) {
7111
+ console.error(`Error: ${err.message}`);
7112
+ process.exit(1);
7113
+ }
7114
+ });
7115
+ workspaceCommand.command("setup <id>").description("Install ZAM skills into a configured workspace").option(
7116
+ "--agents <list>",
7117
+ "comma-separated agents to wire: all, claude, copilot, codex, agent"
7118
+ ).option(
7119
+ "--force",
7120
+ "overwrite existing skill files and refresh ZAM blocks",
7121
+ false
7122
+ ).option(
7123
+ "--dry-run",
7124
+ "show what would be written without changing files",
7125
+ false
7126
+ ).action((id, opts) => {
7127
+ try {
7128
+ const workspace = requireWorkspace(id);
7129
+ const agents = parseSetupAgents(opts.agents);
7130
+ console.log(
7131
+ `Setting up workspace "${workspace.id}" in ${workspace.path}${opts.dryRun ? " (dry run)" : ""}
7132
+ `
7133
+ );
7134
+ copySkills(
7135
+ Boolean(opts.force),
7136
+ workspace.path,
7137
+ agents,
7138
+ Boolean(opts.dryRun)
7139
+ );
7140
+ if (agents.has("claude")) {
7141
+ writeClaudeMd(false, workspace.path, {
7142
+ dryRun: Boolean(opts.dryRun),
7143
+ updateExisting: true
7144
+ });
7145
+ }
7146
+ if (agents.has("copilot") || agents.has("codex") || agents.has("agent")) {
7147
+ writeAgentsMd(false, workspace.path, {
7148
+ dryRun: Boolean(opts.dryRun),
7149
+ updateExisting: true
7150
+ });
7151
+ }
7152
+ if (agents.has("copilot")) {
7153
+ writeCopilotInstructions(workspace.path, {
7154
+ dryRun: Boolean(opts.dryRun),
7155
+ updateExisting: true
7156
+ });
7157
+ }
7158
+ } catch (err) {
7159
+ console.error(`Error: ${err.message}`);
7160
+ process.exit(1);
7161
+ }
7162
+ });
7163
+ workspaceCommand.command("publish").description("Publish your local workspace sandbox to GitHub").action(async () => {
7164
+ let db;
7165
+ let workspaceDir = "";
7166
+ try {
7167
+ db = await openDatabase();
7168
+ workspaceDir = await getSetting(db, "personal.workspace_dir") || "";
7169
+ await db.close();
7170
+ } catch {
7171
+ await db?.close();
7172
+ }
7173
+ if (!workspaceDir) {
7174
+ console.error(
7175
+ "\x1B[31m\u2717 No active workspace configured. Please run `zam init` first.\x1B[0m"
7176
+ );
7177
+ process.exit(1);
7178
+ }
7179
+ if (!existsSync16(workspaceDir)) {
7180
+ console.error(
7181
+ `\x1B[31m\u2717 Workspace directory does not exist: ${workspaceDir}\x1B[0m`
7182
+ );
7183
+ process.exit(1);
7184
+ }
7185
+ console.log(`
7186
+ Active workspace: \x1B[36m${workspaceDir}\x1B[0m`);
7187
+ if (!hasCommand("git")) {
7188
+ console.error(
7189
+ "\x1B[31m\u2717 Git command was not found on this system. Please install Git first.\x1B[0m"
7190
+ );
7191
+ process.exit(1);
7192
+ }
7193
+ const gitignorePath = join16(workspaceDir, ".gitignore");
7194
+ if (!existsSync16(gitignorePath)) {
7195
+ writeFileSync8(
7196
+ gitignorePath,
7197
+ "node_modules/\n.agent/\n.agents/\n.claude/\n.gemini/\n.goose/\n*.log\n",
7198
+ "utf8"
7199
+ );
7200
+ }
7201
+ const hasGitRepo = existsSync16(join16(workspaceDir, ".git"));
7202
+ if (!hasGitRepo) {
7203
+ console.log("Initializing local Git repository...");
7204
+ runGit(workspaceDir, ["init", "-b", "main"]);
7205
+ runGit(workspaceDir, ["add", "."]);
7206
+ runGit(workspaceDir, [
7207
+ "commit",
7208
+ "-m",
7209
+ "chore: initial workspace sandbox bootstrap"
7210
+ ]);
7211
+ console.log("\x1B[32m\u2713 Local Git repository initialized.\x1B[0m");
7212
+ } else {
7213
+ console.log("Git repository is already initialized.");
7214
+ }
7215
+ const repoName = await input({
7216
+ message: "Choose a name for your GitHub repository:",
7217
+ default: "zam-personal"
7218
+ });
7219
+ const isPrivate = await confirm({
7220
+ message: "Should the repository be private?",
7221
+ default: true
7222
+ });
7223
+ const repoVisibility = isPrivate ? "--private" : "--public";
7224
+ if (hasCommand("gh")) {
7225
+ console.log("GitHub CLI detected! Automating repository creation...");
7226
+ const proceedGh = await confirm({
7227
+ message: "Would you like ZAM to create the repository using the GitHub CLI?",
7228
+ default: true
7229
+ });
7230
+ if (proceedGh) {
7231
+ try {
7232
+ console.log(`Creating GitHub repository ${repoName}...`);
7233
+ execFileSync3("gh", ghRepoCreateArgs(repoName, repoVisibility), {
7234
+ cwd: workspaceDir,
7235
+ stdio: "inherit"
7236
+ });
7237
+ console.log(
7238
+ "\n\x1B[32m\u2713 Successfully published workspace to GitHub!\x1B[0m"
7239
+ );
7240
+ process.exit(0);
7241
+ } catch (err) {
7242
+ console.warn(
7243
+ `\x1B[33m\u26A0 GitHub CLI creation failed: ${err.message}\x1B[0m`
7244
+ );
7245
+ }
7246
+ }
7247
+ }
7248
+ console.log(
7249
+ "\n\x1B[1mPlease create the repository manually on GitHub:\x1B[0m"
7250
+ );
7251
+ console.log(" 1. Go to https://github.com/new");
7252
+ console.log(` 2. Name it exactly: \x1B[36m${repoName}\x1B[0m`);
7253
+ console.log(
7254
+ ` 3. Choose \x1B[36m${isPrivate ? "Private" : "Public"}\x1B[0m`
7255
+ );
7256
+ console.log(
7257
+ " 4. Do NOT initialize it with README, .gitignore, or license"
7258
+ );
7259
+ console.log(" 5. Click 'Create repository'\n");
7260
+ const githubUrl = await input({
7261
+ message: "Paste the repository Git URL (e.g. git@github.com:user/repo.git):"
7262
+ });
7263
+ if (githubUrl) {
7264
+ try {
7265
+ console.log("Linking remote repository and pushing...");
7266
+ let hasOrigin = false;
7267
+ try {
7268
+ runGit(workspaceDir, ["remote", "get-url", "origin"]);
7269
+ hasOrigin = true;
7270
+ } catch {
7271
+ }
7272
+ runGit(workspaceDir, gitRemoteArgs(githubUrl, hasOrigin));
7273
+ runGit(workspaceDir, ["push", "-u", "origin", "main"]);
7274
+ console.log(
7275
+ "\x1B[32m\u2713 Successfully linked and pushed to GitHub!\x1B[0m"
7276
+ );
7277
+ } catch (err) {
7278
+ console.error(
7279
+ `\x1B[31m\u2717 Push failed: ${err.message}\x1B[0m`
7280
+ );
7281
+ console.log(
7282
+ "You can push manually later using: git push -u origin main"
7283
+ );
7284
+ }
7285
+ }
7286
+ });
7287
+ async function backupDatabaseTo(db, targetDir) {
7288
+ const backupDir = join16(targetDir, "zam-backups");
7289
+ mkdirSync9(backupDir, { recursive: true });
7290
+ const stamp = (/* @__PURE__ */ new Date()).toISOString().replace(/[:.]/g, "-");
7291
+ const dest = join16(backupDir, `zam-${stamp}.db`);
7292
+ await db.exec(`VACUUM INTO '${dest.replace(/'/g, "''")}'`);
7293
+ return dest;
7294
+ }
7295
+ workspaceCommand.command("data-dir").description("Print the ZAM data directory (database, credentials, config)").option("--json", "Output as JSON").action((opts) => {
7296
+ const dir = join16(homedir9(), ".zam");
7297
+ console.log(opts.json ? JSON.stringify({ dataDir: dir }) : dir);
7298
+ });
7299
+ workspaceCommand.command("backup").description("Back up the local ZAM database into your workspace").option(
7300
+ "--dir <path>",
7301
+ "Target directory (default: workspace dir, else ~/Documents/zam)"
7302
+ ).option("--json", "Output as JSON").action(async (opts) => {
7303
+ const target = getDatabaseTargetInfo();
7304
+ if (target.kind !== "local") {
7305
+ const reason = `The database is ${target.kind} (${target.location}); file backup applies only to a local database \u2014 your Turso remote is already the cloud backup.`;
7306
+ if (opts.json) {
7307
+ console.log(JSON.stringify({ ok: false, reason }));
7308
+ return;
7309
+ }
7310
+ console.error(`\x1B[33m\u26A0 ${reason}\x1B[0m`);
7311
+ process.exit(1);
7312
+ }
7313
+ let db;
7314
+ try {
7315
+ db = await openDatabase();
7316
+ const workspaceDir = opts.dir || await getSetting(db, "personal.workspace_dir") || join16(homedir9(), "Documents", "zam");
7317
+ const dest = await backupDatabaseTo(db, workspaceDir);
7318
+ if (opts.json) {
7319
+ console.log(JSON.stringify({ ok: true, path: dest }));
7320
+ } else {
7321
+ console.log(`\x1B[32m\u2713 Database backed up to ${dest}\x1B[0m`);
7322
+ }
7323
+ } catch (err) {
7324
+ const reason = err.message;
7325
+ if (opts.json) {
7326
+ console.log(JSON.stringify({ ok: false, reason }));
7327
+ } else {
7328
+ console.error(`\x1B[31m\u2717 Backup failed: ${reason}\x1B[0m`);
7329
+ }
7330
+ process.exit(1);
7331
+ } finally {
7332
+ await db?.close();
7333
+ }
7334
+ });
7335
+
7336
+ // src/cli/commands/bridge.ts
7337
+ var isServeMode = false;
7338
+ function jsonOut2(data) {
7339
+ console.log(JSON.stringify(data, null, 2));
7340
+ }
7341
+ function jsonError(message) {
7342
+ if (isServeMode) {
7343
+ throw new Error(JSON.stringify({ error: message }));
7344
+ }
7345
+ console.log(JSON.stringify({ error: message }, null, 2));
7346
+ process.exit(1);
7347
+ }
7348
+ function parseNonNegativeIntegerOption(name, value) {
7349
+ const parsed = Number(value);
7350
+ if (!Number.isInteger(parsed) || parsed < 0) {
7351
+ jsonError(`${name} must be a non-negative integer`);
7352
+ }
7353
+ return parsed;
7354
+ }
7355
+ async function withDb2(fn) {
7356
+ await withDb(fn, jsonError);
7357
+ }
7358
+ async function getReviewTarget2(db, cardId, userId) {
7359
+ const target = await db.prepare(
7360
+ `SELECT c.id AS card_id, c.token_id, c.user_id, t.slug
7361
+ FROM cards c
7362
+ JOIN tokens t ON t.id = c.token_id
7363
+ WHERE c.id = ?`
7364
+ ).get(cardId);
7365
+ if (!target) {
7366
+ jsonError(`Card not found: ${cardId}`);
7367
+ }
7368
+ if (target.user_id !== userId) {
7369
+ jsonError(`Card ${cardId} does not belong to user ${userId}`);
7370
+ }
7371
+ return target;
7372
+ }
7373
+ function parseTokenUpdates(opts) {
7374
+ const updates = {};
7375
+ if (opts.concept !== void 0) updates.concept = opts.concept;
7376
+ if (opts.domain !== void 0) updates.domain = opts.domain;
7377
+ if (opts.bloom !== void 0)
7378
+ updates.bloom_level = Number(opts.bloom);
7379
+ if (opts.context !== void 0) updates.context = opts.context;
5909
7380
  if (opts.sourceLink !== void 0) {
5910
7381
  updates.source_link = opts.sourceLink === "" ? null : opts.sourceLink;
5911
7382
  }
@@ -5918,7 +7389,7 @@ function parseTokenUpdates(opts) {
5918
7389
  }
5919
7390
  return updates;
5920
7391
  }
5921
- var bridgeCommand = new Command2("bridge").description(
7392
+ var bridgeCommand = new Command4("bridge").description(
5922
7393
  "Machine-readable JSON protocol for AI integration"
5923
7394
  );
5924
7395
  bridgeCommand.command("check-due").description("Check due cards for a user (JSON)").option("--user <id>", "User ID (default: whoami)").option("--domain <domain>", "Filter by knowledge domain").action(async (opts) => {
@@ -5946,6 +7417,160 @@ bridgeCommand.command("check-due").description("Check due cards for a user (JSON
5946
7417
  });
5947
7418
  });
5948
7419
  });
7420
+ bridgeCommand.command("backup-db").description("Back up the local database into the workspace (JSON)").option(
7421
+ "--dir <path>",
7422
+ "Target directory (default: workspace dir, else ~/Documents/zam)"
7423
+ ).action(async (opts) => {
7424
+ const target = getDatabaseTargetInfo();
7425
+ if (target.kind !== "local") {
7426
+ jsonOut2({
7427
+ ok: false,
7428
+ reason: "remote",
7429
+ target: target.kind,
7430
+ location: target.location
7431
+ });
7432
+ return;
7433
+ }
7434
+ await withDb2(async (db) => {
7435
+ const workspaceDir = opts.dir || await getSetting(db, "personal.workspace_dir") || join17(homedir10(), "Documents", "zam");
7436
+ const path = await backupDatabaseTo(db, workspaceDir);
7437
+ jsonOut2({ ok: true, path });
7438
+ });
7439
+ });
7440
+ bridgeCommand.command("workspace-info").description("Report the workspace dir, its default, and the data dir (JSON)").action(async () => {
7441
+ await withDb2(async (db) => {
7442
+ const workspaceDir = await getSetting(db, "personal.workspace_dir") || null;
7443
+ jsonOut2({
7444
+ workspaceDir,
7445
+ defaultWorkspaceDir: join17(homedir10(), "Documents", "zam"),
7446
+ dataDir: join17(homedir10(), ".zam")
7447
+ });
7448
+ });
7449
+ });
7450
+ bridgeCommand.command("workspace-list").description("List configured ZAM workspaces (JSON)").action(async () => {
7451
+ await withDb2(async (db) => {
7452
+ const workspaceDir = await getSetting(db, "personal.workspace_dir") || null;
7453
+ jsonOut2({
7454
+ workspaces: getConfiguredWorkspaces(),
7455
+ workspaceDir,
7456
+ defaultWorkspaceDir: join17(homedir10(), "Documents", "zam"),
7457
+ dataDir: join17(homedir10(), ".zam")
7458
+ });
7459
+ });
7460
+ });
7461
+ function workspaceIdFromPath(dir) {
7462
+ const base = basename5(dir).toLowerCase().replace(/[^a-z0-9]+/g, "-").replace(/^-+|-+$/g, "");
7463
+ const prefix = base || "workspace";
7464
+ const existing = new Set(getConfiguredWorkspaces().map((item) => item.id));
7465
+ if (!existing.has(prefix)) return prefix;
7466
+ let index = 2;
7467
+ while (existing.has(`${prefix}-${index}`)) index++;
7468
+ return `${prefix}-${index}`;
7469
+ }
7470
+ function parseBridgeWorkspaceKind(value) {
7471
+ const kind = (value || "custom").toLowerCase();
7472
+ if (kind === "personal" || kind === "team" || kind === "family" || kind === "community" || kind === "organization" || kind === "custom") {
7473
+ return kind;
7474
+ }
7475
+ jsonError(`Invalid workspace kind: ${value}`);
7476
+ }
7477
+ bridgeCommand.command("workspace-add").description("Register an existing directory as a ZAM workspace (JSON)").requiredOption("--path <dir>", "Existing workspace/repository directory").option("--id <id>", "Workspace id").option("--label <label>", "Human-readable label").option("--kind <kind>", "Workspace kind", "custom").action(async (opts) => {
7478
+ const raw = String(opts.path ?? "").trim();
7479
+ if (!raw) jsonError("A non-empty --path is required");
7480
+ const path = resolve5(raw);
7481
+ if (!existsSync17(path)) jsonError(`Workspace path does not exist: ${path}`);
7482
+ const id = String(opts.id || workspaceIdFromPath(path)).trim();
7483
+ if (!id) jsonError("A non-empty --id is required");
7484
+ const workspace = {
7485
+ id,
7486
+ label: opts.label || basename5(path) || id,
7487
+ kind: parseBridgeWorkspaceKind(opts.kind),
7488
+ path
7489
+ };
7490
+ await withDb2(async (db) => {
7491
+ await setSetting(db, "personal.workspace_dir", path);
7492
+ upsertConfiguredWorkspace(workspace);
7493
+ jsonOut2({
7494
+ ok: true,
7495
+ workspace,
7496
+ workspaces: getConfiguredWorkspaces(),
7497
+ workspaceDir: path
7498
+ });
7499
+ });
7500
+ });
7501
+ bridgeCommand.command("set-workspace-dir").description("Set the personal workspace directory (JSON)").requiredOption("--dir <path>", "Path to the workspace directory").action(async (opts) => {
7502
+ const raw = String(opts.dir ?? "").trim();
7503
+ if (!raw) jsonError("A non-empty --dir is required");
7504
+ const dir = resolve5(raw);
7505
+ await withDb2(async (db) => {
7506
+ await setSetting(db, "personal.workspace_dir", dir);
7507
+ jsonOut2({ ok: true, workspaceDir: dir });
7508
+ });
7509
+ });
7510
+ bridgeCommand.command("agent-list").description("List agent harnesses with detection state + the default (JSON)").action(async () => {
7511
+ await withDb2(async (db) => {
7512
+ const configuredDefault = await getSetting(db, "agent.default") || null;
7513
+ const harnesses = await Promise.all(
7514
+ AGENT_HARNESSES.map(async (h) => {
7515
+ const override = await getSetting(db, `agent.${h.id}.command`) || void 0;
7516
+ return {
7517
+ id: h.id,
7518
+ label: h.label,
7519
+ kind: h.kind,
7520
+ detected: resolveHarnessExecutable(h, override) !== null
7521
+ };
7522
+ })
7523
+ );
7524
+ const fallbackDefault = harnesses.find((h) => h.detected)?.id ?? null;
7525
+ jsonOut2({ harnesses, default: configuredDefault ?? fallbackDefault });
7526
+ });
7527
+ });
7528
+ bridgeCommand.command("agent-open").description("Launch an agent harness in the workspace (JSON)").option(
7529
+ "--id <id>",
7530
+ "Harness id (default: agent.default setting, else first detected)"
7531
+ ).option("--workspace <id>", "Configured workspace id to open").option("--dir <path>", "Explicit workspace directory to open").action(async (opts) => {
7532
+ await withDb2(async (db) => {
7533
+ let id = opts.id || await getSetting(db, "agent.default") || void 0;
7534
+ if (!id) {
7535
+ id = AGENT_HARNESSES.find((h) => resolveHarnessExecutable(h))?.id;
7536
+ }
7537
+ if (!id) {
7538
+ jsonError(
7539
+ "No agent harness configured or detected. Install one (Claude Code, Codex, opencode) or set agent.default."
7540
+ );
7541
+ }
7542
+ const harness = getHarness(id);
7543
+ if (!harness) {
7544
+ jsonError(`Unknown harness: ${id}`);
7545
+ }
7546
+ const override = await getSetting(db, `agent.${harness.id}.command`) || void 0;
7547
+ const executable = resolveHarnessExecutable(harness, override);
7548
+ if (!executable) {
7549
+ jsonError(
7550
+ `${harness.label} was not detected. Set its path: zam settings set agent.${harness.id}.command <path>`
7551
+ );
7552
+ }
7553
+ const configuredWorkspace = opts.workspace ? getConfiguredWorkspaces().find((item) => item.id === opts.workspace) : void 0;
7554
+ if (opts.workspace && !configuredWorkspace) {
7555
+ jsonError(`Workspace is not configured: ${opts.workspace}`);
7556
+ }
7557
+ let workspace = opts.dir || configuredWorkspace?.path || await getSetting(db, "personal.workspace_dir") || join17(homedir10(), "Documents", "zam");
7558
+ if (!existsSync17(workspace)) workspace = homedir10();
7559
+ launchHarness(harness, {
7560
+ executable,
7561
+ workspace,
7562
+ shell: normalizeShell(void 0),
7563
+ silent: true
7564
+ });
7565
+ jsonOut2({
7566
+ ok: true,
7567
+ id: harness.id,
7568
+ label: harness.label,
7569
+ kind: harness.kind,
7570
+ workspace
7571
+ });
7572
+ });
7573
+ });
5949
7574
  bridgeCommand.command("get-review").description("Get next review card with prompt (JSON)").option("--user <id>", "User ID (default: whoami)").option("--no-resolve", "Skip resolving the token's source_link into context").action(async (opts) => {
5950
7575
  await withDb2(async (db) => {
5951
7576
  const userId = await resolveUser(opts, db, { json: true });
@@ -6304,7 +7929,7 @@ bridgeCommand.command("discover-skills").description(
6304
7929
  "20"
6305
7930
  ).action(async (opts) => {
6306
7931
  try {
6307
- const monitorDir = join13(homedir8(), ".zam", "monitor");
7932
+ const monitorDir = join17(homedir10(), ".zam", "monitor");
6308
7933
  let files;
6309
7934
  try {
6310
7935
  files = readdirSync2(monitorDir).filter((f) => f.endsWith(".jsonl"));
@@ -6317,7 +7942,7 @@ bridgeCommand.command("discover-skills").description(
6317
7942
  return;
6318
7943
  }
6319
7944
  const limit = Number(opts.limit);
6320
- const sorted = files.map((f) => ({ name: f, path: join13(monitorDir, f) })).sort((a, b) => b.name.localeCompare(a.name)).slice(0, limit);
7945
+ const sorted = files.map((f) => ({ name: f, path: join17(monitorDir, f) })).sort((a, b) => b.name.localeCompare(a.name)).slice(0, limit);
6321
7946
  const sessionCommands = /* @__PURE__ */ new Map();
6322
7947
  for (const file of sorted) {
6323
7948
  const sessionId = file.name.replace(".jsonl", "");
@@ -6429,7 +8054,7 @@ bridgeCommand.command("observe-ui-snapshot").description(
6429
8054
  });
6430
8055
  function resolveWindowsPowerShell() {
6431
8056
  try {
6432
- execFileSync2("where.exe", ["pwsh.exe"], { stdio: "ignore" });
8057
+ execFileSync4("where.exe", ["pwsh.exe"], { stdio: "ignore" });
6433
8058
  return "pwsh";
6434
8059
  } catch {
6435
8060
  return "powershell";
@@ -6444,7 +8069,7 @@ function captureScreenshot(outputPath, hwnd, processName) {
6444
8069
  throw new Error(`Invalid process name format: ${processName}`);
6445
8070
  }
6446
8071
  if (platform === "win32") {
6447
- const stdout = execFileSync2(
8072
+ const stdout = execFileSync4(
6448
8073
  resolveWindowsPowerShell(),
6449
8074
  [
6450
8075
  "-NoProfile",
@@ -6757,13 +8382,13 @@ Write-CaptureResult "fullscreen" $hwndVal $matchedBy
6757
8382
  } else if (platform === "darwin") {
6758
8383
  if (hwnd) {
6759
8384
  const parsedHwnd = hwnd.startsWith("0x") ? parseInt(hwnd, 16) : parseInt(hwnd, 10);
6760
- execFileSync2("screencapture", ["-l", String(parsedHwnd), outputPath], {
8385
+ execFileSync4("screencapture", ["-l", String(parsedHwnd), outputPath], {
6761
8386
  stdio: "pipe"
6762
8387
  });
6763
8388
  return { method: "screencapture-window", target: null };
6764
8389
  } else if (processName) {
6765
8390
  try {
6766
- const windowId = execFileSync2(
8391
+ const windowId = execFileSync4(
6767
8392
  "osascript",
6768
8393
  [
6769
8394
  "-e",
@@ -6772,17 +8397,17 @@ Write-CaptureResult "fullscreen" $hwndVal $matchedBy
6772
8397
  { encoding: "utf8", stdio: ["pipe", "pipe", "ignore"] }
6773
8398
  ).trim();
6774
8399
  if (windowId && /^\d+$/.test(windowId)) {
6775
- execFileSync2("screencapture", ["-l", windowId, outputPath], {
8400
+ execFileSync4("screencapture", ["-l", windowId, outputPath], {
6776
8401
  stdio: "pipe"
6777
8402
  });
6778
8403
  return { method: "screencapture-window", target: null };
6779
8404
  }
6780
8405
  } catch {
6781
8406
  }
6782
- execFileSync2("screencapture", ["-x", outputPath], { stdio: "pipe" });
8407
+ execFileSync4("screencapture", ["-x", outputPath], { stdio: "pipe" });
6783
8408
  return { method: "screencapture-full", target: null };
6784
8409
  } else {
6785
- execFileSync2("screencapture", ["-x", outputPath], { stdio: "pipe" });
8410
+ execFileSync4("screencapture", ["-x", outputPath], { stdio: "pipe" });
6786
8411
  return { method: "screencapture-full", target: null };
6787
8412
  }
6788
8413
  } else {
@@ -6819,7 +8444,7 @@ bridgeCommand.command("capture-ui").description("Capture a screenshot for agent-
6819
8444
  return;
6820
8445
  }
6821
8446
  }
6822
- const outputPath = opts.image ?? opts.output ?? join13(tmpdir2(), `zam-capture-${randomBytes2(4).toString("hex")}.png`);
8447
+ const outputPath = opts.image ?? opts.output ?? join17(tmpdir3(), `zam-capture-${randomBytes2(4).toString("hex")}.png`);
6823
8448
  const captureResult = isProvided ? { method: "provided", target: null } : captureScreenshot(outputPath, opts.hwnd, opts.processName);
6824
8449
  if (!isProvided) {
6825
8450
  const post = decidePostCapture(policy, {
@@ -6847,7 +8472,7 @@ bridgeCommand.command("capture-ui").description("Capture a screenshot for agent-
6847
8472
  return;
6848
8473
  }
6849
8474
  }
6850
- const imageBytes = readFileSync10(outputPath);
8475
+ const imageBytes = readFileSync11(outputPath);
6851
8476
  const base64 = imageBytes.toString("base64");
6852
8477
  jsonOut2({
6853
8478
  sessionId: opts.session ?? null,
@@ -6863,21 +8488,22 @@ bridgeCommand.command("capture-ui").description("Capture a screenshot for agent-
6863
8488
  });
6864
8489
  });
6865
8490
  });
6866
- bridgeCommand.command("start-recording").description("Start screen recording in the background (macOS only) (JSON)").requiredOption("--session <id>", "ZAM session ID").option("--output <path>", "Video output path").action(async (opts) => {
8491
+ bridgeCommand.command("start-recording").description("Start screen recording in the background (JSON)").requiredOption("--session <id>", "ZAM session ID").option("--output <path>", "Video output path").action(async (opts) => {
6867
8492
  const platform = process.platform;
6868
- if (platform !== "darwin") {
8493
+ if (platform !== "darwin" && platform !== "win32") {
6869
8494
  jsonOut2({
6870
8495
  sessionId: opts.session,
6871
8496
  started: false,
6872
- error: "Screen recording is only supported on macOS (darwin)"
8497
+ error: "Screen recording is only supported on macOS (darwin) and Windows (win32)"
6873
8498
  });
6874
8499
  return;
6875
8500
  }
6876
8501
  const sessionId = opts.session;
6877
- const statePath = join13(tmpdir2(), `zam-recording-${sessionId}.json`);
6878
- const outputPath = opts.output ?? join13(tmpdir2(), `zam-recording-${sessionId}.mov`);
6879
- const { existsSync: existsSync22, writeFileSync: writeFileSync12, openSync, closeSync } = await import("fs");
6880
- if (existsSync22(statePath)) {
8502
+ const statePath = join17(tmpdir3(), `zam-recording-${sessionId}.json`);
8503
+ const defaultExt = platform === "win32" ? ".mkv" : ".mov";
8504
+ const outputPath = opts.output ?? join17(tmpdir3(), `zam-recording-${sessionId}${defaultExt}`);
8505
+ const { existsSync: existsSync25, writeFileSync: writeFileSync12, openSync, closeSync } = await import("fs");
8506
+ if (existsSync25(statePath)) {
6881
8507
  jsonOut2({
6882
8508
  sessionId,
6883
8509
  started: false,
@@ -6885,7 +8511,7 @@ bridgeCommand.command("start-recording").description("Start screen recording in
6885
8511
  });
6886
8512
  return;
6887
8513
  }
6888
- const logPath = join13(tmpdir2(), `zam-recording-${sessionId}.log`);
8514
+ const logPath = join17(tmpdir3(), `zam-recording-${sessionId}.log`);
6889
8515
  let logFd;
6890
8516
  try {
6891
8517
  logFd = openSync(logPath, "w");
@@ -6897,26 +8523,34 @@ bridgeCommand.command("start-recording").description("Start screen recording in
6897
8523
  });
6898
8524
  return;
6899
8525
  }
6900
- const { spawn: spawn3 } = await import("child_process");
6901
- const child = spawn3(
6902
- "ffmpeg",
6903
- [
6904
- "-y",
6905
- "-f",
6906
- "avfoundation",
6907
- "-r",
6908
- "5",
6909
- "-i",
6910
- "0",
6911
- "-pix_fmt",
6912
- "yuv420p",
6913
- outputPath
6914
- ],
6915
- {
6916
- detached: true,
6917
- stdio: ["pipe", logFd, logFd]
6918
- }
6919
- );
8526
+ const { spawn: spawn4 } = await import("child_process");
8527
+ const ffmpegArgs = platform === "darwin" ? [
8528
+ "-y",
8529
+ "-f",
8530
+ "avfoundation",
8531
+ "-r",
8532
+ "5",
8533
+ "-i",
8534
+ "0",
8535
+ "-pix_fmt",
8536
+ "yuv420p",
8537
+ outputPath
8538
+ ] : [
8539
+ "-y",
8540
+ "-f",
8541
+ "gdigrab",
8542
+ "-framerate",
8543
+ "5",
8544
+ "-i",
8545
+ "desktop",
8546
+ "-pix_fmt",
8547
+ "yuv420p",
8548
+ outputPath
8549
+ ];
8550
+ const child = spawn4("ffmpeg", ffmpegArgs, {
8551
+ detached: true,
8552
+ stdio: ["pipe", logFd, logFd]
8553
+ });
6920
8554
  try {
6921
8555
  closeSync(logFd);
6922
8556
  } catch {
@@ -6947,21 +8581,21 @@ bridgeCommand.command("start-recording").description("Start screen recording in
6947
8581
  }
6948
8582
  });
6949
8583
  bridgeCommand.command("stop-recording").description(
6950
- "Stop active screen recording and apply idle-frame compression (macOS only) (JSON)"
8584
+ "Stop active screen recording and apply idle-frame compression (JSON)"
6951
8585
  ).requiredOption("--session <id>", "ZAM session ID").action(async (opts) => {
6952
8586
  const platform = process.platform;
6953
- if (platform !== "darwin") {
8587
+ if (platform !== "darwin" && platform !== "win32") {
6954
8588
  jsonOut2({
6955
8589
  sessionId: opts.session,
6956
8590
  stopped: false,
6957
- error: "Screen recording is only supported on macOS (darwin)"
8591
+ error: "Screen recording is only supported on macOS (darwin) and Windows (win32)"
6958
8592
  });
6959
8593
  return;
6960
8594
  }
6961
8595
  const sessionId = opts.session;
6962
- const statePath = join13(tmpdir2(), `zam-recording-${sessionId}.json`);
6963
- const { existsSync: existsSync22, readFileSync: readFileSync15, rmSync: rmSync3 } = await import("fs");
6964
- if (!existsSync22(statePath)) {
8596
+ const statePath = join17(tmpdir3(), `zam-recording-${sessionId}.json`);
8597
+ const { existsSync: existsSync25, readFileSync: readFileSync16, rmSync: rmSync3 } = await import("fs");
8598
+ if (!existsSync25(statePath)) {
6965
8599
  jsonOut2({
6966
8600
  sessionId,
6967
8601
  stopped: false,
@@ -6969,7 +8603,7 @@ bridgeCommand.command("stop-recording").description(
6969
8603
  });
6970
8604
  return;
6971
8605
  }
6972
- const state = JSON.parse(readFileSync15(statePath, "utf8"));
8606
+ const state = JSON.parse(readFileSync16(statePath, "utf8"));
6973
8607
  const { pid, outputPath } = state;
6974
8608
  try {
6975
8609
  process.kill(pid, "SIGINT");
@@ -6985,7 +8619,7 @@ bridgeCommand.command("stop-recording").description(
6985
8619
  };
6986
8620
  let attempts = 0;
6987
8621
  while (isProcessRunning(pid) && attempts < 20) {
6988
- await new Promise((resolve5) => setTimeout(resolve5, 250));
8622
+ await new Promise((resolve8) => setTimeout(resolve8, 250));
6989
8623
  attempts++;
6990
8624
  }
6991
8625
  if (isProcessRunning(pid)) {
@@ -6998,7 +8632,7 @@ bridgeCommand.command("stop-recording").description(
6998
8632
  rmSync3(statePath, { force: true });
6999
8633
  } catch {
7000
8634
  }
7001
- if (!existsSync22(outputPath)) {
8635
+ if (!existsSync25(outputPath)) {
7002
8636
  jsonOut2({
7003
8637
  sessionId,
7004
8638
  stopped: false,
@@ -7007,9 +8641,9 @@ bridgeCommand.command("stop-recording").description(
7007
8641
  return;
7008
8642
  }
7009
8643
  const decimatedPath = outputPath.replace(/\.[^.]+$/, "-decimated.mp4");
7010
- const { execSync: execSync7 } = await import("child_process");
8644
+ const { execSync: execSync6 } = await import("child_process");
7011
8645
  try {
7012
- execSync7(
8646
+ execSync6(
7013
8647
  `ffmpeg -y -i "${outputPath}" -vf "mpdecimate,setpts=N/FRAME_RATE/TB" -an -pix_fmt yuv420p "${decimatedPath}"`,
7014
8648
  { stdio: "ignore" }
7015
8649
  );
@@ -7062,11 +8696,13 @@ bridgeCommand.command("sync-observer-policy").description(
7062
8696
  });
7063
8697
  bridgeCommand.command("check-llm").description("Check if LLM is enabled and online (JSON)").action(async () => {
7064
8698
  await withDb2(async (db) => {
7065
- const { enabled, url, model, apiKey } = await getLlmConfig(db);
8699
+ const provider = await getProviderForRole(db, "recall");
8700
+ const { enabled, url, model, apiKey } = provider;
8701
+ const unsupportedProvider = provider.apiFlavor !== "chat-completions";
7066
8702
  let online = false;
7067
8703
  let availableModels = [];
7068
8704
  let modelAvailable = false;
7069
- if (enabled) {
8705
+ if (enabled && !unsupportedProvider) {
7070
8706
  online = await isLlmOnline(url);
7071
8707
  if (online) {
7072
8708
  availableModels = await getAvailableModels(url, apiKey);
@@ -7081,10 +8717,22 @@ bridgeCommand.command("check-llm").description("Check if LLM is enabled and onli
7081
8717
  url,
7082
8718
  model,
7083
8719
  modelAvailable,
7084
- availableModels
8720
+ availableModels,
8721
+ apiFlavor: provider.apiFlavor,
8722
+ unsupportedProvider
7085
8723
  });
7086
8724
  });
7087
8725
  });
8726
+ bridgeCommand.command("provider-status").description("Show secret-safe provider status for LLM roles (JSON)").action(async () => {
8727
+ await withDb2(async (db) => {
8728
+ const [recall, vision, text] = await Promise.all([
8729
+ getProviderRoleStatus(db, "recall"),
8730
+ getProviderRoleStatus(db, "vision"),
8731
+ getProviderRoleStatus(db, "text")
8732
+ ]);
8733
+ jsonOut2({ roles: { recall, vision, text } });
8734
+ });
8735
+ });
7088
8736
  bridgeCommand.command("check-vision").description(
7089
8737
  "Check if UI observer vision analysis is enabled and ready (JSON)"
7090
8738
  ).action(async () => {
@@ -7397,8 +9045,8 @@ bridgeCommand.command("serve").description("Start the persistent JSON-RPC stdin/
7397
9045
  });
7398
9046
 
7399
9047
  // src/cli/commands/card.ts
7400
- import { Command as Command3 } from "commander";
7401
- var cardCommand = new Command3("card").description(
9048
+ import { Command as Command5 } from "commander";
9049
+ var cardCommand = new Command5("card").description(
7402
9050
  "Manage spaced-repetition cards"
7403
9051
  );
7404
9052
  cardCommand.command("due").description("Show due tokens for a user").option("--user <id>", "User ID (default: whoami)").option("--domain <domain>", "Filter by knowledge domain").option("--json", "Output as JSON").option("--summary", "Show only counts per domain (no slugs or concepts)").action(async (opts) => {
@@ -7596,9 +9244,9 @@ cardCommand.command("delete").description("Delete one user's card for a token").
7596
9244
  });
7597
9245
 
7598
9246
  // src/cli/commands/connector.ts
7599
- import { input, password } from "@inquirer/prompts";
7600
- import { Command as Command4 } from "commander";
7601
- var connectorCommand = new Command4("connector").description(
9247
+ import { input as input2, password } from "@inquirer/prompts";
9248
+ import { Command as Command6 } from "commander";
9249
+ var connectorCommand = new Command6("connector").description(
7602
9250
  "Manage external service connectors"
7603
9251
  );
7604
9252
  connectorCommand.command("setup").description("Configure a connector").argument("<type>", "Connector type (ado, turso)").option("--url <url>", "Turso database URL (non-interactive)").option("--token <token>", "Turso auth token (non-interactive)").option(
@@ -7617,10 +9265,10 @@ connectorCommand.command("setup").description("Configure a connector").argument(
7617
9265
  process.exit(1);
7618
9266
  }
7619
9267
  try {
7620
- const orgUrl = await input({
9268
+ const orgUrl = await input2({
7621
9269
  message: "Organization URL (e.g. https://dev.azure.com/myorg):"
7622
9270
  });
7623
- const project = await input({
9271
+ const project = await input2({
7624
9272
  message: "Project name:"
7625
9273
  });
7626
9274
  const pat = await password({
@@ -7711,7 +9359,7 @@ connectorCommand.command("sync").description("Verify the Turso cloud database co
7711
9359
  async function setupTurso(urlArg, tokenArg, mode) {
7712
9360
  let db;
7713
9361
  try {
7714
- const url = urlArg ?? await input({
9362
+ const url = urlArg ?? await input2({
7715
9363
  message: "Turso database URL (e.g. libsql://my-db-user.turso.io):"
7716
9364
  });
7717
9365
  const token = tokenArg ?? await password({
@@ -7740,27 +9388,27 @@ async function setupTurso(urlArg, tokenArg, mode) {
7740
9388
  }
7741
9389
 
7742
9390
  // src/cli/commands/git-sync.ts
7743
- import { execSync as execSync4 } from "child_process";
7744
- import { chmodSync as chmodSync2, existsSync as existsSync13, writeFileSync as writeFileSync6 } from "fs";
7745
- import { join as join14 } from "path";
7746
- import { Command as Command5 } from "commander";
9391
+ import { execSync as execSync5 } from "child_process";
9392
+ import { chmodSync as chmodSync2, existsSync as existsSync18, writeFileSync as writeFileSync9 } from "fs";
9393
+ import { join as join18 } from "path";
9394
+ import { Command as Command7 } from "commander";
7747
9395
  function installHook2() {
7748
- const gitDir = join14(process.cwd(), ".git");
7749
- if (!existsSync13(gitDir)) {
9396
+ const gitDir = join18(process.cwd(), ".git");
9397
+ if (!existsSync18(gitDir)) {
7750
9398
  console.error(
7751
9399
  "Error: Current directory is not the root of a Git repository."
7752
9400
  );
7753
9401
  process.exit(1);
7754
9402
  }
7755
- const hooksDir = join14(gitDir, "hooks");
7756
- const hookPath = join14(hooksDir, "post-commit");
9403
+ const hooksDir = join18(gitDir, "hooks");
9404
+ const hookPath = join18(hooksDir, "post-commit");
7757
9405
  const hookContent = `#!/bin/sh
7758
9406
  # ZAM Spaced Repetition Auto-Stale Hook
7759
9407
  # Triggered automatically on git commits to decay modified concept cards.
7760
9408
  zam git-sync --commit HEAD --quiet
7761
9409
  `;
7762
9410
  try {
7763
- writeFileSync6(hookPath, hookContent, { encoding: "utf-8", flag: "w" });
9411
+ writeFileSync9(hookPath, hookContent, { encoding: "utf-8", flag: "w" });
7764
9412
  try {
7765
9413
  chmodSync2(hookPath, "755");
7766
9414
  } catch (_e) {
@@ -7773,7 +9421,7 @@ zam git-sync --commit HEAD --quiet
7773
9421
  process.exit(1);
7774
9422
  }
7775
9423
  }
7776
- var gitSyncCommand = new Command5("git-sync").description("Sync learning cards with recent Git file modifications").option("--commit <hash>", "Git commit hash to check", "HEAD").option("--user <id>", "User ID (default: whoami)").option("--install", "Install git post-commit hook in current repo").option("--quiet", "Suppress verbose output").action(async (opts) => {
9424
+ var gitSyncCommand = new Command7("git-sync").description("Sync learning cards with recent Git file modifications").option("--commit <hash>", "Git commit hash to check", "HEAD").option("--user <id>", "User ID (default: whoami)").option("--install", "Install git post-commit hook in current repo").option("--quiet", "Suppress verbose output").action(async (opts) => {
7777
9425
  if (opts.install) {
7778
9426
  installHook2();
7779
9427
  return;
@@ -7782,7 +9430,7 @@ var gitSyncCommand = new Command5("git-sync").description("Sync learning cards w
7782
9430
  const userId = await resolveUser(opts, db);
7783
9431
  let changedFiles = [];
7784
9432
  try {
7785
- const output = execSync4(
9433
+ const output = execSync5(
7786
9434
  `git diff-tree --no-commit-id --name-only -r ${opts.commit}`,
7787
9435
  {
7788
9436
  encoding: "utf-8",
@@ -7862,10 +9510,10 @@ ZAM Auto-Stale Complete: Scanned ${changedFiles.length} file(s).`
7862
9510
  });
7863
9511
 
7864
9512
  // src/cli/commands/goal.ts
7865
- import { existsSync as existsSync14, mkdirSync as mkdirSync8 } from "fs";
7866
- import { resolve as resolve3 } from "path";
7867
- import { input as input2 } from "@inquirer/prompts";
7868
- import { Command as Command6 } from "commander";
9513
+ import { existsSync as existsSync19, mkdirSync as mkdirSync10 } from "fs";
9514
+ import { resolve as resolve6 } from "path";
9515
+ import { input as input3 } from "@inquirer/prompts";
9516
+ import { Command as Command8 } from "commander";
7869
9517
  async function resolveGoalsDir() {
7870
9518
  let goalsDir;
7871
9519
  let db;
@@ -7876,9 +9524,9 @@ async function resolveGoalsDir() {
7876
9524
  } finally {
7877
9525
  await db?.close();
7878
9526
  }
7879
- return goalsDir ? resolve3(goalsDir) : resolve3("goals");
9527
+ return goalsDir ? resolve6(goalsDir) : resolve6("goals");
7880
9528
  }
7881
- var goalCommand = new Command6("goal").description(
9529
+ var goalCommand = new Command8("goal").description(
7882
9530
  "Manage learning goals (markdown files)"
7883
9531
  );
7884
9532
  goalCommand.command("list").description("List all goals").option(
@@ -7886,7 +9534,7 @@ goalCommand.command("list").description("List all goals").option(
7886
9534
  "Filter by status (active, completed, paused, abandoned)"
7887
9535
  ).option("--tree", "Show goals as a tree with parent/child relationships").option("--json", "Output as JSON").action(async (opts) => {
7888
9536
  const goalsDir = await resolveGoalsDir();
7889
- if (!existsSync14(goalsDir)) {
9537
+ if (!existsSync19(goalsDir)) {
7890
9538
  console.error(`Goals directory not found: ${goalsDir}`);
7891
9539
  console.error(
7892
9540
  "Set it with: zam settings set personal.goals_dir /path/to/goals"
@@ -7987,8 +9635,8 @@ ${"\u2500".repeat(50)}`);
7987
9635
  });
7988
9636
  goalCommand.command("create").description("Create a new goal").option("--slug <slug>", "Goal slug (used as filename)").option("--title <title>", "Goal title").option("--parent <slug>", "Parent goal slug").option("--description <text>", "Goal description").option("--json", "Output as JSON").action(async (opts) => {
7989
9637
  const goalsDir = await resolveGoalsDir();
7990
- if (!existsSync14(goalsDir)) {
7991
- mkdirSync8(goalsDir, { recursive: true });
9638
+ if (!existsSync19(goalsDir)) {
9639
+ mkdirSync10(goalsDir, { recursive: true });
7992
9640
  }
7993
9641
  let slug = opts.slug;
7994
9642
  let title = opts.title;
@@ -7997,11 +9645,11 @@ goalCommand.command("create").description("Create a new goal").option("--slug <s
7997
9645
  if (!slug || !title) {
7998
9646
  try {
7999
9647
  if (!title) {
8000
- title = await input2({ message: "Goal title:" });
9648
+ title = await input3({ message: "Goal title:" });
8001
9649
  }
8002
9650
  if (!slug) {
8003
9651
  const suggested = title.toLowerCase().replace(/[^a-z0-9]+/g, "-").replace(/^-|-$/g, "");
8004
- slug = await input2({
9652
+ slug = await input3({
8005
9653
  message: "Goal slug (filename):",
8006
9654
  default: suggested
8007
9655
  });
@@ -8047,22 +9695,22 @@ goalCommand.command("status <slug> <status>").description("Update a goal's statu
8047
9695
  });
8048
9696
 
8049
9697
  // src/cli/commands/init.ts
8050
- import { existsSync as existsSync15, mkdirSync as mkdirSync9, writeFileSync as writeFileSync7 } from "fs";
8051
- import { homedir as homedir9 } from "os";
8052
- import { join as join15 } from "path";
8053
- import { confirm, input as input3 } from "@inquirer/prompts";
8054
- import { Command as Command7 } from "commander";
8055
- var HOME2 = homedir9();
9698
+ import { existsSync as existsSync20, mkdirSync as mkdirSync11, writeFileSync as writeFileSync10 } from "fs";
9699
+ import { homedir as homedir11 } from "os";
9700
+ import { join as join19 } from "path";
9701
+ import { confirm as confirm2, input as input4 } from "@inquirer/prompts";
9702
+ import { Command as Command9 } from "commander";
9703
+ var HOME2 = homedir11();
8056
9704
  function printLine(char = "\u2550", len = 60, color = "\x1B[36m") {
8057
9705
  console.log(`${color}${char.repeat(len)}\x1B[0m`);
8058
9706
  }
8059
9707
  function bootstrapSandboxWorkspace(workspaceDir) {
8060
- mkdirSync9(join15(workspaceDir, "beliefs"), { recursive: true });
8061
- mkdirSync9(join15(workspaceDir, "goals"), { recursive: true });
8062
- mkdirSync9(join15(workspaceDir, "skills"), { recursive: true });
8063
- const worldviewFile = join15(workspaceDir, "beliefs", "worldview.md");
8064
- if (!existsSync15(worldviewFile)) {
8065
- writeFileSync7(
9708
+ mkdirSync11(join19(workspaceDir, "beliefs"), { recursive: true });
9709
+ mkdirSync11(join19(workspaceDir, "goals"), { recursive: true });
9710
+ mkdirSync11(join19(workspaceDir, "skills"), { recursive: true });
9711
+ const worldviewFile = join19(workspaceDir, "beliefs", "worldview.md");
9712
+ if (!existsSync20(worldviewFile)) {
9713
+ writeFileSync10(
8066
9714
  worldviewFile,
8067
9715
  `# Personal Worldview
8068
9716
 
@@ -8074,9 +9722,9 @@ Here, I declare the core concepts and principles I want to master.
8074
9722
  "utf8"
8075
9723
  );
8076
9724
  }
8077
- const goalsFile = join15(workspaceDir, "goals", "goals.md");
8078
- if (!existsSync15(goalsFile)) {
8079
- writeFileSync7(
9725
+ const goalsFile = join19(workspaceDir, "goals", "goals.md");
9726
+ if (!existsSync20(goalsFile)) {
9727
+ writeFileSync10(
8080
9728
  goalsFile,
8081
9729
  `# Personal Goals
8082
9730
 
@@ -8088,7 +9736,7 @@ Here, I declare the core concepts and principles I want to master.
8088
9736
  );
8089
9737
  }
8090
9738
  }
8091
- var initCommand = new Command7("init").description("Launch the guided interactive onboarding wizard").action(async () => {
9739
+ var initCommand = new Command9("init").description("Launch the guided interactive onboarding wizard").action(async () => {
8092
9740
  printLine();
8093
9741
  console.log(
8094
9742
  "\x1B[1m\x1B[32m ZAM \u2014 The Symbiotic Learning Agent Onboarding\x1B[0m"
@@ -8098,8 +9746,8 @@ var initCommand = new Command7("init").description("Launch the guided interactiv
8098
9746
  );
8099
9747
  printLine();
8100
9748
  console.log("\n\x1B[1m[1/5] Setting up Local Workspace Sandbox\x1B[0m");
8101
- const defaultWorkspace = join15(HOME2, "Documents", "zam");
8102
- const workspacePath = await input3({
9749
+ const defaultWorkspace = join19(HOME2, "Documents", "zam");
9750
+ const workspacePath = await input4({
8103
9751
  message: "Choose your ZAM workspace directory:",
8104
9752
  default: defaultWorkspace
8105
9753
  });
@@ -8135,7 +9783,7 @@ var initCommand = new Command7("init").description("Launch the guided interactiv
8135
9783
  \x1B[1mRecommendation:\x1B[0m ZAM suggests installing \x1B[32m${runnerLabel}\x1B[0m with \x1B[36m${profile.recommendedModel}\x1B[0m.`
8136
9784
  );
8137
9785
  console.log("\n\x1B[1m[3/5] Setting up Local LLM Runner\x1B[0m");
8138
- const proceedInstall = await confirm({
9786
+ const proceedInstall = await confirm2({
8139
9787
  message: `Would you like ZAM to install and configure ${runnerLabel} automatically?`,
8140
9788
  default: true
8141
9789
  });
@@ -8203,7 +9851,7 @@ var initCommand = new Command7("init").description("Launch the guided interactiv
8203
9851
  console.log(
8204
9852
  "\n\x1B[1m[5/5] Wiring Developer Agents & Terminal Helpers\x1B[0m"
8205
9853
  );
8206
- const proceedHooks = await confirm({
9854
+ const proceedHooks = await confirm2({
8207
9855
  message: "Distribute ZAM skills and install optional monitored-session shell helpers?",
8208
9856
  default: true
8209
9857
  });
@@ -8254,8 +9902,8 @@ var initCommand = new Command7("init").description("Launch the guided interactiv
8254
9902
  });
8255
9903
 
8256
9904
  // src/cli/commands/learn.ts
8257
- import { input as input5 } from "@inquirer/prompts";
8258
- import { Command as Command8 } from "commander";
9905
+ import { input as input6 } from "@inquirer/prompts";
9906
+ import { Command as Command10 } from "commander";
8259
9907
 
8260
9908
  // src/cli/learn-format.ts
8261
9909
  var BLOOM_VERBS3 = {
@@ -8304,7 +9952,7 @@ function formatReveal(input8) {
8304
9952
  }
8305
9953
 
8306
9954
  // src/cli/review-actions.ts
8307
- import { confirm as confirm2, input as input4, select } from "@inquirer/prompts";
9955
+ import { confirm as confirm3, input as input5, select } from "@inquirer/prompts";
8308
9956
  async function runInteractiveReviewAction(inputData) {
8309
9957
  let currentItem = { ...inputData.item };
8310
9958
  while (true) {
@@ -8419,7 +10067,7 @@ async function runInteractiveReviewAction(inputData) {
8419
10067
  continue;
8420
10068
  }
8421
10069
  if (choice === "deprecate-token") {
8422
- const approved = await confirm2({
10070
+ const approved = await confirm3({
8423
10071
  message: `Deprecate ${currentItem.slug} so it stops appearing in review queues?`,
8424
10072
  default: false
8425
10073
  });
@@ -8450,7 +10098,7 @@ async function runInteractiveReviewAction(inputData) {
8450
10098
  console.log(` Session steps: ${impact.session_steps}`);
8451
10099
  console.log(` Sessions touched: ${impact.sessions_touched}`);
8452
10100
  console.log(` Agent skills updated: ${impact.agent_skills}`);
8453
- const approved = await confirm2({
10101
+ const approved = await confirm3({
8454
10102
  message: "Permanently delete this token and its dependent learning data?",
8455
10103
  default: false
8456
10104
  });
@@ -8475,7 +10123,7 @@ async function runInteractiveReviewAction(inputData) {
8475
10123
  );
8476
10124
  console.log(`Delete your card for ${currentItem.slug}?`);
8477
10125
  console.log(` Review logs removed: ${impact.review_logs}`);
8478
- const approved = await confirm2({
10126
+ const approved = await confirm3({
8479
10127
  message: "Delete only your card for this token?",
8480
10128
  default: false
8481
10129
  });
@@ -8513,21 +10161,21 @@ async function promptTokenEdit(token) {
8513
10161
  }
8514
10162
  switch (field) {
8515
10163
  case "concept": {
8516
- const concept = await input4({
10164
+ const concept = await input5({
8517
10165
  message: "Concept:",
8518
10166
  default: token.concept
8519
10167
  });
8520
10168
  return concept === token.concept ? null : { concept };
8521
10169
  }
8522
10170
  case "domain": {
8523
- const domain = await input4({
10171
+ const domain = await input5({
8524
10172
  message: "Domain (blank allowed):",
8525
10173
  default: token.domain
8526
10174
  });
8527
10175
  return domain === token.domain ? null : { domain };
8528
10176
  }
8529
10177
  case "context": {
8530
- const context = await input4({
10178
+ const context = await input5({
8531
10179
  message: "Context (blank allowed):",
8532
10180
  default: token.context
8533
10181
  });
@@ -8568,14 +10216,14 @@ async function promptTokenEdit(token) {
8568
10216
  return mode === token.symbiosis_mode ? null : { symbiosis_mode: mode };
8569
10217
  }
8570
10218
  case "source_link": {
8571
- const link = await input4({
10219
+ const link = await input5({
8572
10220
  message: "Source link (blank allowed):",
8573
10221
  default: token.source_link ?? ""
8574
10222
  });
8575
10223
  return link === token.source_link ? null : { source_link: link || null };
8576
10224
  }
8577
10225
  case "question": {
8578
- const question = await input4({
10226
+ const question = await input5({
8579
10227
  message: "Recall question (blank allowed):",
8580
10228
  default: token.question ?? ""
8581
10229
  });
@@ -8593,7 +10241,7 @@ var STOP_WORDS = /* @__PURE__ */ new Set(["q", ":q", "quit", "stop"]);
8593
10241
  function isExitPrompt(err) {
8594
10242
  return err instanceof Error && err.name === "ExitPromptError";
8595
10243
  }
8596
- var learnCommand = new Command8("learn").description(
10244
+ var learnCommand = new Command10("learn").description(
8597
10245
  "Run a spoiler-free, in-process learning session (recall \u2192 reveal \u2192 self-rate)"
8598
10246
  ).option("--user <id>", "User ID (default: whoami)").option("--max-new <n>", "Maximum new cards", "10").option("--max-reviews <n>", "Maximum review cards", "50").option("--no-resolve", "Skip resolving source_link into the revealed answer").action(async (opts) => {
8599
10247
  let db;
@@ -8673,7 +10321,7 @@ ${"\u2500".repeat(50)}`);
8673
10321
  ${prompt.question}`);
8674
10322
  let answer;
8675
10323
  try {
8676
- answer = await input5({
10324
+ answer = await input6({
8677
10325
  message: t(locale, "prompt_answer")
8678
10326
  });
8679
10327
  } catch (err) {
@@ -8793,36 +10441,52 @@ ${"\u2550".repeat(50)}`);
8793
10441
  process.exit(1);
8794
10442
  }
8795
10443
  });
8796
-
8797
- // src/cli/commands/monitor.ts
8798
- import { execFileSync as execFileSync3, execSync as execSync5 } from "child_process";
8799
- import { unlinkSync, writeFileSync as writeFileSync8 } from "fs";
8800
- import { tmpdir as tmpdir3 } from "os";
8801
- import { basename as basename3, join as join16 } from "path";
8802
- import { Command as Command9 } from "commander";
8803
- function isPowerShellShell(shell) {
8804
- return shell === "pwsh" || shell === "powershell";
10444
+ function buildLearnCommand(shell, userId) {
10445
+ const zamInvocation = resolveZamInvocation(shell);
10446
+ const learnArgs = userId ? ` learn --user ${userId}` : " learn";
10447
+ return `${zamInvocation}${learnArgs}`;
8805
10448
  }
8806
- function normalizeShell(shell) {
8807
- if (!shell) return detectShell();
8808
- const normalized = shell.toLowerCase();
8809
- if (normalized === "zsh" || normalized === "bash" || normalized === "pwsh" || normalized === "powershell") {
8810
- return normalized;
10449
+ learnCommand.command("open").description("Open a new terminal window running zam learn (Active Recall)").option("--user <id>", "User ID (default: whoami)").option("--dir <path>", "Working directory (defaults to cwd)").option(
10450
+ "--shell <type>",
10451
+ "Shell type: zsh | bash | pwsh | powershell (auto-detected)"
10452
+ ).action(async (opts) => {
10453
+ let shell;
10454
+ try {
10455
+ shell = normalizeShell(opts.shell);
10456
+ } catch (err) {
10457
+ console.error(`Error: ${err.message}`);
10458
+ process.exit(1);
8811
10459
  }
8812
- throw new Error(
8813
- `Unsupported shell: ${shell}. Expected zsh, bash, pwsh, or powershell.`
8814
- );
8815
- }
8816
- function psSingleQuoted2(value) {
8817
- return `'${value.replace(/'/g, "''")}'`;
8818
- }
8819
- function detectShell() {
8820
- if (process.platform === "win32")
8821
- return findExecutable("pwsh.exe") ? "pwsh" : "powershell";
8822
- const shell = process.env.SHELL ?? "";
8823
- return basename3(shell) === "bash" ? "bash" : "zsh";
8824
- }
8825
- var monitorCommand = new Command9("monitor").description(
10460
+ let db;
10461
+ let userId = opts.user;
10462
+ try {
10463
+ db = await openDatabase();
10464
+ if (!userId) {
10465
+ userId = await resolveUser(opts, db);
10466
+ }
10467
+ if (!await getSetting(db, "review_method")) {
10468
+ await setSetting(db, "review_method", "console");
10469
+ }
10470
+ } catch (err) {
10471
+ console.error(`Error: ${err.message}`);
10472
+ process.exit(1);
10473
+ } finally {
10474
+ await db?.close();
10475
+ }
10476
+ const dir = opts.dir ?? process.cwd();
10477
+ const learnCommandLine = buildLearnCommand(shell, userId);
10478
+ const shellSetup = buildShellSetupCommand(dir, shell, learnCommandLine);
10479
+ openTerminalWindow({
10480
+ shellSetup,
10481
+ label: "learn",
10482
+ dir,
10483
+ shell
10484
+ });
10485
+ });
10486
+
10487
+ // src/cli/commands/monitor.ts
10488
+ import { Command as Command11 } from "commander";
10489
+ var monitorCommand = new Command11("monitor").description(
8826
10490
  "Shell observation for real-time task monitoring"
8827
10491
  );
8828
10492
  monitorCommand.command("start").description("Output shell hook code to install monitoring").requiredOption("--session <id>", "Session ID to monitor").option(
@@ -8949,39 +10613,6 @@ monitorCommand.command("status").description("Show monitoring status for a sessi
8949
10613
  console.log(` To: ${result.timeSpan.end}`);
8950
10614
  }
8951
10615
  });
8952
- function selectWindowsExecutable(results, pathext = process.env.PATHEXT ?? ".COM;.EXE;.BAT;.CMD") {
8953
- if (results.length === 0) return null;
8954
- const extensions = pathext.split(";").map((ext) => ext.trim().toLowerCase()).filter(Boolean);
8955
- const runnable = results.find(
8956
- (result) => extensions.some((ext) => result.toLowerCase().endsWith(ext))
8957
- );
8958
- return runnable ?? results[0];
8959
- }
8960
- function findExecutable(command) {
8961
- try {
8962
- const lookup = process.platform === "win32" ? `where.exe ${command}` : `command -v ${command}`;
8963
- const results = execSync5(lookup, { encoding: "utf-8" }).split(/\r?\n/).map((line) => line.trim()).filter(Boolean);
8964
- if (results.length === 0) return null;
8965
- if (process.platform === "win32") {
8966
- return selectWindowsExecutable(results);
8967
- }
8968
- return results[0];
8969
- } catch {
8970
- return null;
8971
- }
8972
- }
8973
- function resolveZamInvocation(shell) {
8974
- const installed = findExecutable("zam");
8975
- if (installed) {
8976
- return isPowerShellShell(shell) ? `& ${psSingleQuoted2(installed)}` : installed;
8977
- }
8978
- const projectRoot = join16(import.meta.dirname, "..", "..", "..");
8979
- const cliSource = join16(projectRoot, "src/cli/index.ts");
8980
- if (isPowerShellShell(shell)) {
8981
- return `& npx --prefix ${psSingleQuoted2(projectRoot)} tsx ${psSingleQuoted2(cliSource)}`;
8982
- }
8983
- return `npx --prefix ${JSON.stringify(projectRoot)} tsx ${JSON.stringify(cliSource)}`;
8984
- }
8985
10616
  function buildMonitorSetupCommand(dir, sessionId, shell) {
8986
10617
  const zamInvocation = resolveZamInvocation(shell);
8987
10618
  if (isPowerShellShell(shell)) {
@@ -8994,17 +10625,6 @@ function buildMonitorSetupCommand(dir, sessionId, shell) {
8994
10625
  }
8995
10626
  return `cd ${JSON.stringify(dir)} && eval "$(${zamInvocation} monitor start --session ${sessionId} --shell ${shell})"`;
8996
10627
  }
8997
- function isItermRunning() {
8998
- try {
8999
- const result = execSync5(
9000
- `osascript -e 'tell application "System Events" to (name of processes) contains "iTerm2"' 2>/dev/null`,
9001
- { encoding: "utf-8" }
9002
- ).trim();
9003
- return result === "true";
9004
- } catch {
9005
- return false;
9006
- }
9007
- }
9008
10628
  monitorCommand.command("open").description("Open a new monitored terminal window for a session").requiredOption("--session <id>", "Session ID to monitor").option("--dir <path>", "Working directory (defaults to cwd)").option(
9009
10629
  "--shell <type>",
9010
10630
  "Shell type: zsh | bash | pwsh | powershell (auto-detected)"
@@ -9019,107 +10639,37 @@ monitorCommand.command("open").description("Open a new monitored terminal window
9019
10639
  let db;
9020
10640
  try {
9021
10641
  db = await openDatabase();
9022
- const session = await db.prepare("SELECT id, completed_at FROM sessions WHERE id = ?").get(opts.session);
9023
- if (!session) {
9024
- console.error(`Error: Session not found: ${opts.session}`);
9025
- process.exit(1);
9026
- }
9027
- if (session.completed_at) {
9028
- console.error(`Error: Session already completed: ${opts.session}`);
9029
- process.exit(1);
9030
- }
9031
- if (!await getSetting(db, "monitor_method")) {
9032
- await setSetting(db, "monitor_method", "terminal");
9033
- }
9034
- } catch (err) {
9035
- console.error(`Error: ${err.message}`);
9036
- process.exit(1);
9037
- } finally {
9038
- await db?.close();
9039
- }
9040
- const dir = opts.dir ?? process.cwd();
9041
- const shellSetup = buildMonitorSetupCommand(dir, opts.session, shell);
9042
- if (process.platform === "darwin" && !isPowerShellShell(shell)) {
9043
- openMacTerminal(shellSetup, opts.session, dir);
9044
- } else if (process.platform === "win32" && isPowerShellShell(shell)) {
9045
- openWindowsPowerShell(shellSetup, opts.session, dir, shell);
9046
- } else {
9047
- console.log(`Run this in a new terminal:
9048
- `);
9049
- console.log(` ${shellSetup}
9050
- `);
9051
- console.log(
9052
- `(Automatic terminal opening is only supported on macOS Terminal/iTerm2 and Windows PowerShell for now.)`
9053
- );
9054
- }
9055
- });
9056
- function openMacTerminal(shellSetup, sessionId, dir) {
9057
- const useIterm = isItermRunning();
9058
- const escaped = shellSetup.replace(/\\/g, "\\\\").replace(/"/g, '\\"');
9059
- const appleScript = useIterm ? `tell application "iTerm2"
9060
- activate
9061
- set newWindow to (create window with default profile)
9062
- tell current session of newWindow
9063
- write text "${escaped}"
9064
- end tell
9065
- end tell` : `tell application "Terminal"
9066
- activate
9067
- do script "${escaped}"
9068
- end tell`;
9069
- const tmpFile = join16(tmpdir3(), `zam-monitor-${sessionId}.scpt`);
9070
- try {
9071
- writeFileSync8(tmpFile, appleScript);
9072
- execSync5(`osascript ${JSON.stringify(tmpFile)}`, { stdio: "ignore" });
9073
- console.log(
9074
- `Opened ${useIterm ? "iTerm2" : "Terminal.app"} window with monitoring for session ${sessionId}`
9075
- );
9076
- console.log(` Directory: ${dir}`);
9077
- } catch (err) {
9078
- console.error(`Failed to open terminal: ${err.message}`);
9079
- console.log(`
9080
- Run this manually in a new terminal:
9081
- `);
9082
- console.log(` ${shellSetup}`);
9083
- } finally {
9084
- try {
9085
- unlinkSync(tmpFile);
9086
- } catch {
9087
- }
9088
- }
9089
- }
9090
- function openWindowsPowerShell(shellSetup, sessionId, dir, requestedShell) {
9091
- const requestedExecutable = requestedShell === "powershell" ? "powershell.exe" : "pwsh.exe";
9092
- const executable = findExecutable(requestedExecutable) ? requestedExecutable : "powershell.exe";
9093
- const startCommand = [
9094
- "Start-Process",
9095
- `-FilePath ${psSingleQuoted2(executable)}`,
9096
- `-ArgumentList @('-NoExit','-NoProfile','-Command',${psSingleQuoted2(shellSetup)})`
9097
- ].join(" ");
9098
- const launcher = findExecutable("pwsh.exe") ? "pwsh.exe" : "powershell.exe";
9099
- try {
9100
- execFileSync3(
9101
- launcher,
9102
- ["-NoProfile", "-ExecutionPolicy", "Bypass", "-Command", startCommand],
9103
- {
9104
- stdio: "ignore"
9105
- }
9106
- );
9107
- console.log(
9108
- `Opened ${executable === "pwsh.exe" ? "PowerShell" : "Windows PowerShell"} window with monitoring for session ${sessionId}`
9109
- );
9110
- console.log(` Directory: ${dir}`);
10642
+ const session = await db.prepare("SELECT id, completed_at FROM sessions WHERE id = ?").get(opts.session);
10643
+ if (!session) {
10644
+ console.error(`Error: Session not found: ${opts.session}`);
10645
+ process.exit(1);
10646
+ }
10647
+ if (session.completed_at) {
10648
+ console.error(`Error: Session already completed: ${opts.session}`);
10649
+ process.exit(1);
10650
+ }
10651
+ if (!await getSetting(db, "monitor_method")) {
10652
+ await setSetting(db, "monitor_method", "terminal");
10653
+ }
9111
10654
  } catch (err) {
9112
- console.error(`Failed to open PowerShell: ${err.message}`);
9113
- console.log(`
9114
- Run this manually in a new PowerShell terminal:
9115
- `);
9116
- console.log(` ${shellSetup}`);
10655
+ console.error(`Error: ${err.message}`);
10656
+ process.exit(1);
10657
+ } finally {
10658
+ await db?.close();
9117
10659
  }
9118
- }
10660
+ const dir = opts.dir ?? process.cwd();
10661
+ const shellSetup = buildMonitorSetupCommand(dir, opts.session, shell);
10662
+ openTerminalWindow({
10663
+ shellSetup,
10664
+ label: `monitor-${opts.session}`,
10665
+ dir,
10666
+ shell
10667
+ });
10668
+ });
9119
10669
 
9120
10670
  // src/cli/commands/observer.ts
9121
- import { Command as Command10 } from "commander";
9122
- var observerCommand = new Command10("observer").description(
10671
+ import { Command as Command12 } from "commander";
10672
+ var observerCommand = new Command12("observer").description(
9123
10673
  "Configure what the UI observer may capture (Layer 2 policy)"
9124
10674
  );
9125
10675
  function applyObserverListChange(current, entry, op) {
@@ -9181,9 +10731,9 @@ observerCommand.command("revoke <process>").description("Remove a process from o
9181
10731
  });
9182
10732
 
9183
10733
  // src/cli/commands/profile.ts
9184
- import { homedir as homedir10 } from "os";
9185
- import { dirname as dirname5, join as join17, resolve as resolve4 } from "path";
9186
- import { Command as Command11 } from "commander";
10734
+ import { homedir as homedir12 } from "os";
10735
+ import { dirname as dirname6, join as join20, resolve as resolve7 } from "path";
10736
+ import { Command as Command13 } from "commander";
9187
10737
  var C2 = {
9188
10738
  reset: "\x1B[0m",
9189
10739
  bold: "\x1B[1m",
@@ -9192,7 +10742,7 @@ var C2 = {
9192
10742
  green: "\x1B[32m"
9193
10743
  };
9194
10744
  function defaultPersonalDir() {
9195
- return join17(homedir10(), "Documents", "zam");
10745
+ return join20(homedir12(), "Documents", "zam");
9196
10746
  }
9197
10747
  function render(profile) {
9198
10748
  const sync = profile.syncProvider ? `${C2.green}${profile.syncProvider}${C2.reset} ${C2.dim}(good for cross-device snapshots)${C2.reset}` : `${C2.dim}local folder (use a synced folder or snapshots to move between machines)${C2.reset}`;
@@ -9203,7 +10753,7 @@ function render(profile) {
9203
10753
  console.log(` data dir: ${C2.cyan}${profile.dataDir}${C2.reset}`);
9204
10754
  console.log(` database: ${C2.cyan}${profile.dbPath}${C2.reset}`);
9205
10755
  }
9206
- var profileCommand = new Command11("profile").description("Show or change this machine's ZAM install profile").option("--mode <mode>", "Set install mode: developer | default").option("--dir <path>", "Set the personal-content folder").option("--json", "Output as JSON").action(async (opts) => {
10756
+ var profileCommand = new Command13("profile").description("Show or change this machine's ZAM install profile").option("--mode <mode>", "Set install mode: developer | default").option("--dir <path>", "Set the personal-content folder").option("--json", "Output as JSON").action(async (opts) => {
9207
10757
  if (opts.mode && opts.mode !== "developer" && opts.mode !== "default") {
9208
10758
  console.error(`Invalid --mode: ${opts.mode}. Use developer or default.`);
9209
10759
  process.exit(1);
@@ -9213,7 +10763,7 @@ var profileCommand = new Command11("profile").description("Show or change this m
9213
10763
  if (opts.mode) setInstallMode(opts.mode);
9214
10764
  db = await openDatabaseWithSync({ initialize: true });
9215
10765
  if (opts.dir) {
9216
- await setSetting(db, "personal.workspace_dir", resolve4(opts.dir));
10766
+ await setSetting(db, "personal.workspace_dir", resolve7(opts.dir));
9217
10767
  }
9218
10768
  const personalDir = await getSetting(db, "personal.workspace_dir") || defaultPersonalDir();
9219
10769
  await db.close();
@@ -9223,7 +10773,7 @@ var profileCommand = new Command11("profile").description("Show or change this m
9223
10773
  mode: getInstallMode(),
9224
10774
  personalDir,
9225
10775
  syncProvider: detectSyncProvider(personalDir),
9226
- dataDir: dirname5(dbPath),
10776
+ dataDir: dirname6(dbPath),
9227
10777
  dbPath
9228
10778
  };
9229
10779
  if (opts.json) {
@@ -9238,9 +10788,358 @@ var profileCommand = new Command11("profile").description("Show or change this m
9238
10788
  }
9239
10789
  });
9240
10790
 
10791
+ // src/cli/commands/provider.ts
10792
+ import { password as password2 } from "@inquirer/prompts";
10793
+ import { Command as Command14 } from "commander";
10794
+ var VALID_API_FLAVORS = [
10795
+ "chat-completions",
10796
+ "anthropic-messages"
10797
+ ];
10798
+ var VALID_ROLES = ["vision", "recall", "text"];
10799
+ function upsertProviderRecord(providers, name, patch) {
10800
+ const merged = { ...providers[name] ?? {} };
10801
+ if (patch.url !== void 0) merged.url = patch.url;
10802
+ if (patch.model !== void 0) merged.model = patch.model;
10803
+ if (patch.apiFlavor !== void 0) merged.apiFlavor = patch.apiFlavor;
10804
+ if (patch.apiKeyRef !== void 0) merged.apiKeyRef = patch.apiKeyRef;
10805
+ if (patch.label !== void 0) merged.label = patch.label;
10806
+ if (patch.local !== void 0) merged.local = patch.local;
10807
+ if (patch.runner !== void 0) merged.runner = patch.runner;
10808
+ return { ...providers, [name]: merged };
10809
+ }
10810
+ function removeProviderRecord(providers, name) {
10811
+ if (!(name in providers)) return { providers, removed: false };
10812
+ const next = { ...providers };
10813
+ delete next[name];
10814
+ return { providers: next, removed: true };
10815
+ }
10816
+ function rolesReferencing(roles, name) {
10817
+ return VALID_ROLES.filter((role) => {
10818
+ const binding = roles[role];
10819
+ return binding?.primary === name || binding?.fallback === name;
10820
+ });
10821
+ }
10822
+ function bindRoleProviders(roles, role, primary, fallback) {
10823
+ const binding = { primary };
10824
+ if (fallback) binding.fallback = fallback;
10825
+ return { ...roles, [role]: binding };
10826
+ }
10827
+ function maskSecret(key) {
10828
+ return key.length <= 4 ? "\u2022\u2022\u2022\u2022" : `\u2026${key.slice(-4)}`;
10829
+ }
10830
+ function buildProviderListing(providers, hasKey) {
10831
+ return Object.entries(providers).map(([name, rec]) => {
10832
+ const apiFlavor = rec.apiFlavor ?? (rec.url ? inferApiFlavor(rec.url) : "chat-completions");
10833
+ let keyState;
10834
+ if (!rec.apiKeyRef) keyState = "none";
10835
+ else keyState = hasKey(rec.apiKeyRef) ? "set" : "missing";
10836
+ const row = {
10837
+ name,
10838
+ url: rec.url,
10839
+ model: rec.model,
10840
+ apiFlavor,
10841
+ apiKeyRef: rec.apiKeyRef,
10842
+ keyState
10843
+ };
10844
+ if (rec.label !== void 0) row.label = rec.label;
10845
+ if (rec.local !== void 0) row.local = rec.local;
10846
+ if (rec.runner !== void 0) row.runner = rec.runner;
10847
+ return row;
10848
+ });
10849
+ }
10850
+ function findOrphanKeyRefs(storedRefs, providers) {
10851
+ const used = new Set(
10852
+ Object.values(providers).map((rec) => rec.apiKeyRef).filter((ref) => !!ref)
10853
+ );
10854
+ return storedRefs.filter((ref) => !used.has(ref));
10855
+ }
10856
+ async function readJson(db, key, fallback) {
10857
+ const raw = await getSetting(db, key);
10858
+ if (!raw) return fallback;
10859
+ try {
10860
+ return JSON.parse(raw);
10861
+ } catch {
10862
+ return fallback;
10863
+ }
10864
+ }
10865
+ var readProviders = (db) => readJson(db, "llm.providers", {});
10866
+ var readRoles = (db) => readJson(db, "llm.roles", {});
10867
+ async function readScopedProviders(db, machine) {
10868
+ if (machine) return getMachineAiConfig().providers ?? {};
10869
+ if (!db)
10870
+ throw new Error("Database is required for shared provider settings.");
10871
+ return readProviders(db);
10872
+ }
10873
+ async function readScopedRoles(db, machine) {
10874
+ if (machine) return getMachineAiConfig().roles ?? {};
10875
+ if (!db)
10876
+ throw new Error("Database is required for shared provider settings.");
10877
+ return readRoles(db);
10878
+ }
10879
+ async function writeScopedProviders(db, machine, p) {
10880
+ if (machine) {
10881
+ const config = getMachineAiConfig();
10882
+ saveMachineAiConfig({ ...config, providers: p });
10883
+ return;
10884
+ }
10885
+ if (!db)
10886
+ throw new Error("Database is required for shared provider settings.");
10887
+ await setSetting(db, "llm.providers", JSON.stringify(p));
10888
+ }
10889
+ async function writeScopedRoles(db, machine, r) {
10890
+ if (machine) {
10891
+ const config = getMachineAiConfig();
10892
+ saveMachineAiConfig({ ...config, roles: r });
10893
+ return;
10894
+ }
10895
+ if (!db)
10896
+ throw new Error("Database is required for shared provider settings.");
10897
+ await setSetting(db, "llm.roles", JSON.stringify(r));
10898
+ }
10899
+ async function withProviderScope(machine, action) {
10900
+ if (machine) {
10901
+ await action(void 0);
10902
+ return;
10903
+ }
10904
+ await withDb(action);
10905
+ }
10906
+ var providerCommand = new Command14("provider").description(
10907
+ "Configure role-based AI providers (url/model/flavor/key, per role)"
10908
+ );
10909
+ providerCommand.command("list").description("Show configured providers, role bindings, and key status").option("--json", "Output as JSON").option("--machine", "Read machine-local providers from ~/.zam/config.json").action(async (opts) => {
10910
+ const machine = Boolean(opts.machine);
10911
+ await withProviderScope(machine, async (db) => {
10912
+ const providers = await readScopedProviders(db, machine);
10913
+ const roles = await readScopedRoles(db, machine);
10914
+ const rows = buildProviderListing(
10915
+ providers,
10916
+ (ref) => getProviderApiKey(ref) !== null
10917
+ );
10918
+ const orphans = findOrphanKeyRefs(listProviderApiKeyRefs(), providers);
10919
+ if (opts.json) {
10920
+ console.log(
10921
+ JSON.stringify(
10922
+ {
10923
+ scope: machine ? "machine" : "shared",
10924
+ providers: rows,
10925
+ roles,
10926
+ orphans
10927
+ },
10928
+ null,
10929
+ 2
10930
+ )
10931
+ );
10932
+ return;
10933
+ }
10934
+ console.log(
10935
+ `Providers (${opts.machine ? "~/.zam/config.json ai.providers" : "llm.providers"}):
10936
+ `
10937
+ );
10938
+ if (rows.length === 0) {
10939
+ console.log(
10940
+ " (none) \u2014 add one: zam provider add <name> --url <url> --model <model>"
10941
+ );
10942
+ } else {
10943
+ for (const row of rows) {
10944
+ const key = row.keyState === "set" ? "\x1B[32mkey \u2713\x1B[0m" : row.keyState === "missing" ? `\x1B[31mkey \u2717 (set: zam provider set-key ${row.apiKeyRef})\x1B[0m` : "\x1B[90mno key\x1B[0m";
10945
+ console.log(` \x1B[36m${row.name.padEnd(12)}\x1B[0m ${key}`);
10946
+ console.log(` url: ${row.url ?? "(inherits llm.url)"}`);
10947
+ console.log(` model: ${row.model ?? "(inherits llm.model)"}`);
10948
+ console.log(` flavor: ${row.apiFlavor}`);
10949
+ if (row.label) console.log(` label: ${row.label}`);
10950
+ if (row.local !== void 0) {
10951
+ console.log(` local: ${row.local ? "yes" : "no"}`);
10952
+ }
10953
+ if (row.runner) console.log(` runner: ${row.runner}`);
10954
+ if (row.apiKeyRef) console.log(` key-ref: ${row.apiKeyRef}`);
10955
+ }
10956
+ }
10957
+ console.log("\nRoles (llm.roles):\n");
10958
+ for (const role of VALID_ROLES) {
10959
+ const binding = roles[role];
10960
+ if (!binding?.primary) {
10961
+ console.log(` ${role.padEnd(7)} \x1B[90m(unset)\x1B[0m`);
10962
+ } else {
10963
+ const fb = binding.fallback ? ` \u2192 fallback: ${binding.fallback}` : "";
10964
+ console.log(` ${role.padEnd(7)} primary: ${binding.primary}${fb}`);
10965
+ }
10966
+ }
10967
+ if (orphans.length > 0) {
10968
+ console.log(
10969
+ `
10970
+ \x1B[33mOrphan keys (stored but unreferenced):\x1B[0m ${orphans.join(", ")}`
10971
+ );
10972
+ }
10973
+ });
10974
+ });
10975
+ providerCommand.command("add <name>").description("Add or update a provider record in llm.providers").option("--label <label>", "Human-readable provider label").option("--url <url>", "Endpoint base URL (e.g. https://api.deepseek.com/v1)").option("--model <model>", "Model id (e.g. deepseek-v4-flash)").option("--local", "Mark this provider as a local/on-device endpoint").option(
10976
+ "--runner <runner>",
10977
+ "Local runner hint (flm, foundry-local, ollama, ...)"
10978
+ ).option(
10979
+ "--flavor <flavor>",
10980
+ `Wire protocol: ${VALID_API_FLAVORS.join(" | ")} (default: inferred from URL)`
10981
+ ).option(
10982
+ "--key-ref <ref>",
10983
+ "Credential reference for the API key (default: <name> when --key is given)"
10984
+ ).option(
10985
+ "--key <value>",
10986
+ "Store this API key now (prefer `set-key` to keep it out of shell history)"
10987
+ ).option("--machine", "Store this provider in ~/.zam/config.json").action(async (name, opts) => {
10988
+ let apiFlavor;
10989
+ if (opts.flavor) {
10990
+ if (!VALID_API_FLAVORS.includes(opts.flavor)) {
10991
+ console.error(
10992
+ `Invalid --flavor: ${opts.flavor}. Use ${VALID_API_FLAVORS.join(" or ")}.`
10993
+ );
10994
+ process.exit(1);
10995
+ }
10996
+ apiFlavor = opts.flavor;
10997
+ }
10998
+ const apiKeyRef = opts.keyRef ?? (opts.key ? name : void 0);
10999
+ const machine = Boolean(opts.machine);
11000
+ await withProviderScope(machine, async (db) => {
11001
+ const providers = await readScopedProviders(db, machine);
11002
+ const next = upsertProviderRecord(providers, name, {
11003
+ label: opts.label,
11004
+ url: opts.url,
11005
+ model: opts.model,
11006
+ apiFlavor,
11007
+ apiKeyRef,
11008
+ local: opts.local ? true : void 0,
11009
+ runner: opts.runner
11010
+ });
11011
+ await writeScopedProviders(db, machine, next);
11012
+ if (opts.key && apiKeyRef) setProviderApiKey(apiKeyRef, opts.key);
11013
+ const rec = next[name];
11014
+ console.log(
11015
+ `Provider "${name}" saved (${machine ? "machine-local" : "shared"}):`
11016
+ );
11017
+ console.log(` url: ${rec.url ?? "(inherits llm.url)"}`);
11018
+ console.log(` model: ${rec.model ?? "(inherits llm.model)"}`);
11019
+ if (rec.label) console.log(` label: ${rec.label}`);
11020
+ if (rec.local !== void 0) {
11021
+ console.log(` local: ${rec.local ? "yes" : "no"}`);
11022
+ }
11023
+ if (rec.runner) console.log(` runner: ${rec.runner}`);
11024
+ console.log(
11025
+ ` flavor: ${rec.apiFlavor ?? `${rec.url ? inferApiFlavor(rec.url) : "chat-completions"} (inferred)`}`
11026
+ );
11027
+ console.log(` key-ref: ${rec.apiKeyRef ?? "(none \u2014 uses default key)"}`);
11028
+ if (opts.key) {
11029
+ console.log(` key: stored (${maskSecret(opts.key)})`);
11030
+ } else if (rec.apiKeyRef && getProviderApiKey(rec.apiKeyRef) === null) {
11031
+ console.log(
11032
+ `
11033
+ \u26A0 No key stored for "${rec.apiKeyRef}". Run: zam provider set-key ${rec.apiKeyRef}`
11034
+ );
11035
+ }
11036
+ if (!rec.url) {
11037
+ console.log(
11038
+ "\n \u26A0 No --url set; this provider inherits the base llm.url."
11039
+ );
11040
+ }
11041
+ console.log(
11042
+ `
11043
+ Bind it to a role: zam provider use recall --primary ${name}`
11044
+ );
11045
+ });
11046
+ });
11047
+ providerCommand.command("remove <name>").description("Remove a provider record from llm.providers").option("--machine", "Remove from ~/.zam/config.json instead of the DB").action(async (name, opts) => {
11048
+ const machine = Boolean(opts.machine);
11049
+ await withProviderScope(machine, async (db) => {
11050
+ const providers = await readScopedProviders(db, machine);
11051
+ const { providers: next, removed } = removeProviderRecord(
11052
+ providers,
11053
+ name
11054
+ );
11055
+ if (!removed) {
11056
+ console.log(`No such provider: ${name}`);
11057
+ return;
11058
+ }
11059
+ await writeScopedProviders(db, machine, next);
11060
+ console.log(
11061
+ `Removed provider "${name}" (${machine ? "machine-local" : "shared"}).`
11062
+ );
11063
+ const referencing = rolesReferencing(
11064
+ await readScopedRoles(db, machine),
11065
+ name
11066
+ );
11067
+ if (referencing.length > 0) {
11068
+ console.log(
11069
+ ` \u26A0 Still referenced by role(s): ${referencing.join(", ")} \u2014 rebind with: zam provider use <role> --primary <name>`
11070
+ );
11071
+ }
11072
+ });
11073
+ });
11074
+ providerCommand.command("use <role>").description(`Bind providers to a role (${VALID_ROLES.join(" | ")})`).option("--primary <name>", "Primary provider").option("--fallback <name>", "Fallback provider (optional)").option("--machine", "Bind the role in ~/.zam/config.json").action(async (role, opts) => {
11075
+ if (!VALID_ROLES.includes(role)) {
11076
+ console.error(`Invalid role: ${role}. Use ${VALID_ROLES.join(", ")}.`);
11077
+ process.exit(1);
11078
+ }
11079
+ if (!opts.primary) {
11080
+ console.error("--primary is required.");
11081
+ process.exit(1);
11082
+ }
11083
+ const machine = Boolean(opts.machine);
11084
+ await withProviderScope(machine, async (db) => {
11085
+ const providers = await readScopedProviders(db, machine);
11086
+ await writeScopedRoles(
11087
+ db,
11088
+ machine,
11089
+ bindRoleProviders(
11090
+ await readScopedRoles(db, machine),
11091
+ role,
11092
+ opts.primary,
11093
+ opts.fallback
11094
+ )
11095
+ );
11096
+ const fb = opts.fallback ? `, fallback: ${opts.fallback}` : "";
11097
+ console.log(
11098
+ `Role "${role}" \u2192 primary: ${opts.primary}${fb} (${machine ? "machine-local" : "shared"})`
11099
+ );
11100
+ for (const [label, ref] of [
11101
+ ["primary", opts.primary],
11102
+ ["fallback", opts.fallback]
11103
+ ]) {
11104
+ if (ref && !(ref in providers)) {
11105
+ console.log(
11106
+ ` \u26A0 ${label} "${ref}" is not a defined provider yet. Add it: zam provider add ${ref} --url <url> --model <model>`
11107
+ );
11108
+ }
11109
+ }
11110
+ });
11111
+ });
11112
+ providerCommand.command("set-key <ref>").description(
11113
+ "Store an API key for a provider reference (in credentials.json)"
11114
+ ).option(
11115
+ "--key <value>",
11116
+ "The API key (omit to enter it interactively, hidden)"
11117
+ ).action(async (ref, opts) => {
11118
+ try {
11119
+ const key = opts.key ?? await password2({ message: `API key for "${ref}":` });
11120
+ if (!key || key.trim().length === 0) {
11121
+ console.error("No key provided.");
11122
+ process.exit(1);
11123
+ }
11124
+ setProviderApiKey(ref, key.trim());
11125
+ console.log(`Stored API key for "${ref}" (${maskSecret(key.trim())}).`);
11126
+ } catch (err) {
11127
+ if (err.name === "ExitPromptError") {
11128
+ console.log("\nCancelled.");
11129
+ process.exit(0);
11130
+ }
11131
+ console.error("Error:", err.message);
11132
+ process.exit(1);
11133
+ }
11134
+ });
11135
+ providerCommand.command("clear-key <ref>").description("Remove a stored API key").action((ref) => {
11136
+ clearProviderApiKey(ref);
11137
+ console.log(`Cleared API key for "${ref}".`);
11138
+ });
11139
+
9241
11140
  // src/cli/commands/review.ts
9242
- import { Command as Command12 } from "commander";
9243
- var reviewCommand = new Command12("review").description("Start an interactive review session").option("--user <id>", "User ID (default: whoami)").option("--max-new <n>", "Maximum new cards", "10").option("--max-reviews <n>", "Maximum review cards", "50").option("--no-resolve", "Skip resolving source_link into inline context").action(async (opts) => {
11141
+ import { Command as Command15 } from "commander";
11142
+ var reviewCommand = new Command15("review").description("Start an interactive review session").option("--user <id>", "User ID (default: whoami)").option("--max-new <n>", "Maximum new cards", "10").option("--max-reviews <n>", "Maximum review cards", "50").option("--no-resolve", "Skip resolving source_link into inline context").action(async (opts) => {
9244
11143
  let db;
9245
11144
  try {
9246
11145
  db = await openDatabase();
@@ -9353,10 +11252,10 @@ ${"\u2550".repeat(50)}`);
9353
11252
  });
9354
11253
 
9355
11254
  // src/cli/commands/session.ts
9356
- import { readFileSync as readFileSync11 } from "fs";
9357
- import { input as input6, select as select2 } from "@inquirer/prompts";
9358
- import { Command as Command13 } from "commander";
9359
- var sessionCommand = new Command13("session").description(
11255
+ import { readFileSync as readFileSync12 } from "fs";
11256
+ import { input as input7, select as select2 } from "@inquirer/prompts";
11257
+ import { Command as Command16 } from "commander";
11258
+ var sessionCommand = new Command16("session").description(
9360
11259
  "Manage learning sessions"
9361
11260
  );
9362
11261
  sessionCommand.command("start").description("Start a new learning session (review \u2192 task)").option("--user <id>", "User ID (default: whoami)").option("--task <description>", "Task description (interactive if omitted)").option(
@@ -9533,7 +11432,7 @@ async function selectTask() {
9533
11432
  console.log("No active work items found in Azure DevOps.");
9534
11433
  }
9535
11434
  }
9536
- return input6({ message: "Task description:" });
11435
+ return input7({ message: "Task description:" });
9537
11436
  }
9538
11437
  var RATING_LABELS = {
9539
11438
  1: "Again",
@@ -9545,7 +11444,7 @@ function loadPatternFile(path) {
9545
11444
  if (!path) return [];
9546
11445
  let parsed;
9547
11446
  try {
9548
- parsed = JSON.parse(readFileSync11(path, "utf-8"));
11447
+ parsed = JSON.parse(readFileSync12(path, "utf-8"));
9549
11448
  } catch (err) {
9550
11449
  throw new Error(
9551
11450
  `Cannot read synthesis patterns from ${path}: ${err.message}`
@@ -9735,9 +11634,9 @@ sessionCommand.command("end").description("End a session and show summary").requ
9735
11634
  });
9736
11635
 
9737
11636
  // src/cli/commands/settings.ts
9738
- import { existsSync as existsSync16 } from "fs";
9739
- import { Command as Command14 } from "commander";
9740
- var settingsCommand = new Command14("settings").description(
11637
+ import { existsSync as existsSync21 } from "fs";
11638
+ import { Command as Command17 } from "commander";
11639
+ var settingsCommand = new Command17("settings").description(
9741
11640
  "Manage user settings"
9742
11641
  );
9743
11642
  var BOOLEAN_SETTING_KEYS = /* @__PURE__ */ new Set(["llm.enabled", "llm.vision.enabled"]);
@@ -9860,229 +11759,60 @@ settingsCommand.command("locale [lang]").description(
9860
11759
  );
9861
11760
  process.exit(1);
9862
11761
  }
9863
- await setSetting(db, "system.locale", lower);
9864
- console.log(`Language set to: \x1B[32m${lower}\x1B[0m`);
9865
- });
9866
- });
9867
- settingsCommand.command("repos").description("Show or set Personal, Team, and Organization repository paths").option("--personal <path>", "Set the Personal Repository path").option("--team <path>", "Set the Team Repository path").option("--org <path>", "Set the Organization Repository path").action(async (opts) => {
9868
- await withDb(async (db) => {
9869
- let changed = false;
9870
- if (opts.personal !== void 0) {
9871
- await setSetting(db, "repo.personal", opts.personal);
9872
- console.log(`Set repo.personal = ${opts.personal}`);
9873
- changed = true;
9874
- }
9875
- if (opts.team !== void 0) {
9876
- await setSetting(db, "repo.team", opts.team);
9877
- console.log(`Set repo.team = ${opts.team}`);
9878
- changed = true;
9879
- }
9880
- if (opts.org !== void 0) {
9881
- await setSetting(db, "repo.org", opts.org);
9882
- console.log(`Set repo.org = ${opts.org}`);
9883
- changed = true;
9884
- }
9885
- if (changed) {
9886
- console.log("\nUpdated Repository Settings:\n");
9887
- } else {
9888
- console.log("Repository Settings:\n");
9889
- }
9890
- const paths = await getRepoPaths(db);
9891
- console.log(
9892
- `Personal Repo: ${paths.personal ? `\x1B[32m${paths.personal}\x1B[0m` : "\x1B[31mNot Configured\x1B[0m"}`
9893
- );
9894
- console.log(
9895
- `Team Repo: ${paths.team ? `\x1B[32m${paths.team}\x1B[0m` : "\x1B[31mNot Configured\x1B[0m"}`
9896
- );
9897
- console.log(
9898
- `Org Repo: ${paths.org ? `\x1B[32m${paths.org}\x1B[0m` : "\x1B[31mNot Configured\x1B[0m"}`
9899
- );
9900
- console.log("\nValidation:");
9901
- for (const [name, path] of Object.entries(paths)) {
9902
- if (path) {
9903
- const exists = existsSync16(path);
9904
- console.log(
9905
- ` ${name.padEnd(9)}: ${exists ? "\x1B[32m\u2713 Valid folder\x1B[0m" : "\x1B[31m\u2717 Directory does not exist\x1B[0m"}`
9906
- );
9907
- } else {
9908
- console.log(` ${name.padEnd(9)}: \x1B[33m- Not Set\x1B[0m`);
9909
- }
9910
- }
9911
- });
9912
- });
9913
-
9914
- // src/cli/commands/setup.ts
9915
- import { copyFileSync as copyFileSync2, existsSync as existsSync17, mkdirSync as mkdirSync10, writeFileSync as writeFileSync9 } from "fs";
9916
- import { basename as basename4, dirname as dirname6, join as join18 } from "path";
9917
- import { fileURLToPath as fileURLToPath2 } from "url";
9918
- import { Command as Command15 } from "commander";
9919
- var packageRoot = [
9920
- fileURLToPath2(new URL("../..", import.meta.url)),
9921
- fileURLToPath2(new URL("../../..", import.meta.url))
9922
- ].find((candidate) => existsSync17(join18(candidate, "package.json"))) ?? fileURLToPath2(new URL("../..", import.meta.url));
9923
- var SKILL_PAIRS = [
9924
- {
9925
- from: join18(packageRoot, ".claude", "skills", "zam", "SKILL.md"),
9926
- to: join18(".claude", "skills", "zam", "SKILL.md")
9927
- },
9928
- {
9929
- from: join18(packageRoot, ".agent", "skills", "zam", "SKILL.md"),
9930
- to: join18(".agent", "skills", "zam", "SKILL.md")
9931
- },
9932
- {
9933
- from: join18(packageRoot, ".agents", "skills", "zam", "SKILL.md"),
9934
- to: join18(".agents", "skills", "zam", "SKILL.md")
9935
- }
9936
- ];
9937
- function copySkills(force, cwd = process.cwd()) {
9938
- let anyAction = false;
9939
- for (const { from, to } of SKILL_PAIRS) {
9940
- const dest = join18(cwd, to);
9941
- if (!existsSync17(from)) {
9942
- console.warn(` warn source not found, skipping: ${from}`);
9943
- continue;
9944
- }
9945
- if (existsSync17(dest) && !force) {
9946
- console.log(` skip ${to} (already present \xE2\u20AC\u201D use --force to update)`);
9947
- continue;
9948
- }
9949
- mkdirSync10(dirname6(dest), { recursive: true });
9950
- copyFileSync2(from, dest);
9951
- console.log(` copy ${to}`);
9952
- anyAction = true;
9953
- }
9954
- if (!anyAction && !force) {
9955
- console.log(
9956
- "\nSkill files are already up to date. Run with --force to overwrite."
9957
- );
9958
- }
9959
- }
9960
- function formatDatabaseInitTarget(target) {
9961
- switch (target.kind) {
9962
- case "local":
9963
- return `ZAM database at ${target.location} (local SQLite)`;
9964
- case "turso-remote":
9965
- return `ZAM database via Turso remote at ${target.location}`;
9966
- case "turso-native":
9967
- return `ZAM database via Turso native driver at ${target.location}`;
9968
- case "turso-replica":
9969
- return `ZAM database replica at ${target.location} syncing from ${target.syncUrl}`;
9970
- }
9971
- }
9972
- async function initDatabase(skipInit) {
9973
- if (skipInit) return;
9974
- try {
9975
- const target = getDatabaseTargetInfo();
9976
- const db = await openDatabaseWithSync({ initialize: true });
9977
- await db.close();
9978
- console.log(` init ${formatDatabaseInitTarget(target)}`);
9979
- } catch (err) {
9980
- const msg = err.message;
9981
- if (!msg.includes("already")) {
9982
- console.warn(` warn database init: ${msg}`);
11762
+ await setSetting(db, "system.locale", lower);
11763
+ console.log(`Language set to: \x1B[32m${lower}\x1B[0m`);
11764
+ });
11765
+ });
11766
+ settingsCommand.command("repos").description("Show or set Personal, Team, and Organization repository paths").option("--personal <path>", "Set the Personal Repository path").option("--team <path>", "Set the Team Repository path").option("--org <path>", "Set the Organization Repository path").action(async (opts) => {
11767
+ await withDb(async (db) => {
11768
+ let changed = false;
11769
+ if (opts.personal !== void 0) {
11770
+ await setSetting(db, "repo.personal", opts.personal);
11771
+ console.log(`Set repo.personal = ${opts.personal}`);
11772
+ changed = true;
11773
+ }
11774
+ if (opts.team !== void 0) {
11775
+ await setSetting(db, "repo.team", opts.team);
11776
+ console.log(`Set repo.team = ${opts.team}`);
11777
+ changed = true;
11778
+ }
11779
+ if (opts.org !== void 0) {
11780
+ await setSetting(db, "repo.org", opts.org);
11781
+ console.log(`Set repo.org = ${opts.org}`);
11782
+ changed = true;
11783
+ }
11784
+ if (changed) {
11785
+ console.log("\nUpdated Repository Settings:\n");
9983
11786
  } else {
9984
- console.log(` skip database already initialized`);
11787
+ console.log("Repository Settings:\n");
9985
11788
  }
9986
- }
9987
- }
9988
- function writeClaudeMd(skipClaudeMd, cwd = process.cwd()) {
9989
- if (skipClaudeMd) return;
9990
- const dest = join18(cwd, "CLAUDE.md");
9991
- if (existsSync17(dest)) {
9992
- console.log(` skip CLAUDE.md (already present)`);
9993
- return;
9994
- }
9995
- const name = basename4(cwd);
9996
- writeFileSync9(
9997
- dest,
9998
- `# ZAM Personal Kernel \xE2\u20AC\u201D ${name}
9999
-
10000
- This is a ZAM personal instance. ZAM builds lasting skills through spaced
10001
- repetition during real work \xE2\u20AC\u201D not separate study sessions.
10002
-
10003
- ## First time here?
10004
- Run \`/setup\` in Claude Code or Gemini CLI to complete first-time setup.
10005
-
10006
- ## Regular use
10007
- Run \`/zam\` to start a learning session on whatever you are working on.
10008
-
10009
- ## What lives here
10010
- - \`beliefs/\` \xE2\u20AC\u201D your worldview, approved by git commit
10011
- - \`goals/\` \xE2\u20AC\u201D your objectives, decomposed into tasks and learning tokens
10012
-
10013
- ## Fast-changing data
10014
- Learning tokens, cards, and review history live in local SQLite by default.
10015
- Use \`zam connector setup turso\` to store cloud credentials in
10016
- \`~/.zam/credentials.json\` and use a Turso database across machines.
10017
- `,
10018
- "utf8"
10019
- );
10020
- console.log(` write CLAUDE.md`);
10021
- }
10022
- function writeAgentsMd(skipAgentsMd, cwd = process.cwd()) {
10023
- if (skipAgentsMd) return;
10024
- const dest = join18(cwd, "AGENTS.md");
10025
- if (existsSync17(dest)) {
10026
- console.log(` skip AGENTS.md (already present)`);
10027
- return;
10028
- }
10029
- const name = basename4(cwd);
10030
- writeFileSync9(
10031
- dest,
10032
- `# ZAM Personal Kernel - ${name}
10033
-
10034
- This is a ZAM personal instance. ZAM builds lasting skills through spaced
10035
- repetition during real work, not separate study sessions.
10036
-
10037
- ## First time here?
10038
- Run \`zam setup\` from the shell. When this repository includes
10039
- \`.agents/skills/setup/\`, you can instead select \`setup\` through \`/skills\`
10040
- or invoke \`$setup\`.
10041
-
10042
- ## Regular use
10043
- Select the \`zam\` skill through \`/skills\` or invoke \`$zam\` to start a
10044
- learning session on whatever you are working on.
10045
-
10046
- ## What lives here
10047
- - \`beliefs/\` - your worldview, approved by git commit
10048
- - \`goals/\` - your objectives, decomposed into tasks and learning tokens
10049
-
10050
- ## Fast-changing data
10051
- Learning tokens, cards, and review history live in local SQLite by default.
10052
- Use \`zam connector setup turso\` to store cloud credentials in
10053
- \`~/.zam/credentials.json\` and use a Turso database across machines.
10054
-
10055
- ## Codex skills
10056
- Codex discovers repository skills under \`.agents/skills/\`. Run
10057
- \`zam setup --force\` after upgrading \`zam-core\` to refresh them.
10058
- `,
10059
- "utf8"
10060
- );
10061
- console.log(` write AGENTS.md`);
10062
- }
10063
- var setupCommand = new Command15("setup").description(
10064
- "Distribute ZAM skill files into this personal instance and initialize the database"
10065
- ).option(
10066
- "--force",
10067
- "overwrite existing skill files (use after upgrading zam)",
10068
- false
10069
- ).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(
10070
- async (opts) => {
10071
- console.log(`Setting up ZAM in ${process.cwd()}
10072
- `);
10073
- copySkills(opts.force);
10074
- await initDatabase(opts.skipInit);
10075
- writeClaudeMd(opts.skipClaudeMd);
10076
- writeAgentsMd(opts.skipAgentsMd);
11789
+ const paths = await getRepoPaths(db);
10077
11790
  console.log(
10078
- "\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."
11791
+ `Personal Repo: ${paths.personal ? `\x1B[32m${paths.personal}\x1B[0m` : "\x1B[31mNot Configured\x1B[0m"}`
10079
11792
  );
10080
- }
10081
- );
11793
+ console.log(
11794
+ `Team Repo: ${paths.team ? `\x1B[32m${paths.team}\x1B[0m` : "\x1B[31mNot Configured\x1B[0m"}`
11795
+ );
11796
+ console.log(
11797
+ `Org Repo: ${paths.org ? `\x1B[32m${paths.org}\x1B[0m` : "\x1B[31mNot Configured\x1B[0m"}`
11798
+ );
11799
+ console.log("\nValidation:");
11800
+ for (const [name, path] of Object.entries(paths)) {
11801
+ if (path) {
11802
+ const exists = existsSync21(path);
11803
+ console.log(
11804
+ ` ${name.padEnd(9)}: ${exists ? "\x1B[32m\u2713 Valid folder\x1B[0m" : "\x1B[31m\u2717 Directory does not exist\x1B[0m"}`
11805
+ );
11806
+ } else {
11807
+ console.log(` ${name.padEnd(9)}: \x1B[33m- Not Set\x1B[0m`);
11808
+ }
11809
+ }
11810
+ });
11811
+ });
10082
11812
 
10083
11813
  // src/cli/commands/skill.ts
10084
- import { Command as Command16 } from "commander";
10085
- var skillCommand = new Command16("skill").description(
11814
+ import { Command as Command18 } from "commander";
11815
+ var skillCommand = new Command18("skill").description(
10086
11816
  "Manage agent skill entries (task recipes)"
10087
11817
  );
10088
11818
  skillCommand.command("list").description("List all agent skills").option("--json", "Output as JSON").action(async (opts) => {
@@ -10167,10 +11897,10 @@ skillCommand.command("add").description("Register a new agent skill").requiredOp
10167
11897
  });
10168
11898
 
10169
11899
  // src/cli/commands/snapshot.ts
10170
- import { existsSync as existsSync18, mkdirSync as mkdirSync11, readFileSync as readFileSync12, writeFileSync as writeFileSync10 } from "fs";
10171
- import { homedir as homedir11 } from "os";
10172
- import { dirname as dirname7, join as join19 } from "path";
10173
- import { Command as Command17 } from "commander";
11900
+ import { existsSync as existsSync22, mkdirSync as mkdirSync12, readFileSync as readFileSync13, writeFileSync as writeFileSync11 } from "fs";
11901
+ import { homedir as homedir13 } from "os";
11902
+ import { dirname as dirname7, join as join21 } from "path";
11903
+ import { Command as Command19 } from "commander";
10174
11904
  function defaultOutName() {
10175
11905
  const stamp = (/* @__PURE__ */ new Date()).toISOString().replace(/\.\d+Z$/, "").replace(/:/g, "-");
10176
11906
  return `zam-snapshot-${stamp}.sql`;
@@ -10184,12 +11914,12 @@ function summarize(tables) {
10184
11914
  }
10185
11915
  return { total, nonEmpty };
10186
11916
  }
10187
- var exportCmd = new Command17("export").description("Write a portable SQL-text snapshot of the database").option("--out <file>", "Output file (use - for stdout)").action(async (opts) => {
11917
+ var exportCmd = new Command19("export").description("Write a portable SQL-text snapshot of the database").option("--out <file>", "Output file (use - for stdout)").action(async (opts) => {
10188
11918
  let db;
10189
11919
  try {
10190
11920
  db = await openDatabaseWithSync({ initialize: true });
10191
11921
  const snapshot = await exportSnapshot(db);
10192
- const personalDir = await getSetting(db, "personal.workspace_dir") || join19(homedir11(), "Documents", "zam");
11922
+ const personalDir = await getSetting(db, "personal.workspace_dir") || join21(homedir13(), "Documents", "zam");
10193
11923
  await db.close();
10194
11924
  db = void 0;
10195
11925
  const manifest = verifySnapshot(snapshot);
@@ -10198,12 +11928,12 @@ var exportCmd = new Command17("export").description("Write a portable SQL-text s
10198
11928
  process.stdout.write(snapshot);
10199
11929
  return;
10200
11930
  }
10201
- const out = opts.out ?? join19(personalDir, "snapshots", defaultOutName());
11931
+ const out = opts.out ?? join21(personalDir, "snapshots", defaultOutName());
10202
11932
  const dir = dirname7(out);
10203
- if (dir && dir !== "." && !existsSync18(dir)) {
10204
- mkdirSync11(dir, { recursive: true });
11933
+ if (dir && dir !== "." && !existsSync22(dir)) {
11934
+ mkdirSync12(dir, { recursive: true });
10205
11935
  }
10206
- writeFileSync10(out, snapshot, "utf-8");
11936
+ writeFileSync11(out, snapshot, "utf-8");
10207
11937
  console.log(`Snapshot written: ${out}`);
10208
11938
  console.log(
10209
11939
  ` ${total} row(s)${nonEmpty.length ? ` \u2014 ${nonEmpty.join(", ")}` : ""}`
@@ -10214,14 +11944,14 @@ var exportCmd = new Command17("export").description("Write a portable SQL-text s
10214
11944
  process.exit(1);
10215
11945
  }
10216
11946
  });
10217
- var importCmd = new Command17("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) => {
11947
+ var importCmd = new Command19("import").description("Restore a snapshot into the database").argument("<file>", "Snapshot file to restore").option("--force", "Overwrite a non-empty database", false).action(async (file, opts) => {
10218
11948
  let db;
10219
11949
  try {
10220
- if (!existsSync18(file)) {
11950
+ if (!existsSync22(file)) {
10221
11951
  console.error(`Error: Snapshot file not found: ${file}`);
10222
11952
  process.exit(1);
10223
11953
  }
10224
- const snapshot = readFileSync12(file, "utf-8");
11954
+ const snapshot = readFileSync13(file, "utf-8");
10225
11955
  db = await openDatabaseWithSync({ initialize: true });
10226
11956
  const result = await importSnapshot(db, snapshot, { force: opts.force });
10227
11957
  await db.close();
@@ -10237,13 +11967,13 @@ var importCmd = new Command17("import").description("Restore a snapshot into the
10237
11967
  process.exit(1);
10238
11968
  }
10239
11969
  });
10240
- var verifyCmd = new Command17("verify").description("Check a snapshot's manifest and checksum without importing").argument("<file>", "Snapshot file to verify").action((file) => {
11970
+ var verifyCmd = new Command19("verify").description("Check a snapshot's manifest and checksum without importing").argument("<file>", "Snapshot file to verify").action((file) => {
10241
11971
  try {
10242
- if (!existsSync18(file)) {
11972
+ if (!existsSync22(file)) {
10243
11973
  console.error(`Error: Snapshot file not found: ${file}`);
10244
11974
  process.exit(1);
10245
11975
  }
10246
- const manifest = verifySnapshot(readFileSync12(file, "utf-8"));
11976
+ const manifest = verifySnapshot(readFileSync13(file, "utf-8"));
10247
11977
  const { total, nonEmpty } = summarize(manifest.tables);
10248
11978
  console.log(`Valid snapshot (format v${manifest.version})`);
10249
11979
  console.log(` created: ${manifest.createdAt}`);
@@ -10255,11 +11985,11 @@ var verifyCmd = new Command17("verify").description("Check a snapshot's manifest
10255
11985
  process.exit(1);
10256
11986
  }
10257
11987
  });
10258
- var snapshotCommand = new Command17("snapshot").description("Export, import, or verify a portable database snapshot").addCommand(exportCmd).addCommand(importCmd).addCommand(verifyCmd);
11988
+ var snapshotCommand = new Command19("snapshot").description("Export, import, or verify a portable database snapshot").addCommand(exportCmd).addCommand(importCmd).addCommand(verifyCmd);
10259
11989
 
10260
11990
  // src/cli/commands/stats.ts
10261
- import { Command as Command18 } from "commander";
10262
- var statsCommand = new Command18("stats").description("Show learning dashboard for a user").option("--user <id>", "User ID (default: whoami)").option("--json", "Output as JSON").action(async (opts) => {
11991
+ import { Command as Command20 } from "commander";
11992
+ var statsCommand = new Command20("stats").description("Show learning dashboard for a user").option("--user <id>", "User ID (default: whoami)").option("--json", "Output as JSON").action(async (opts) => {
10263
11993
  await withDb(async (db) => {
10264
11994
  const userId = await resolveUser(opts, db);
10265
11995
  const stats = await getUserStats(db, userId);
@@ -10295,8 +12025,8 @@ var statsCommand = new Command18("stats").description("Show learning dashboard f
10295
12025
  });
10296
12026
 
10297
12027
  // src/cli/commands/token.ts
10298
- import { Command as Command19 } from "commander";
10299
- var tokenCommand = new Command19("token").description(
12028
+ import { Command as Command21 } from "commander";
12029
+ var tokenCommand = new Command21("token").description(
10300
12030
  "Manage knowledge tokens"
10301
12031
  );
10302
12032
  tokenCommand.command("register").description("Register a new knowledge token").requiredOption("--slug <slug>", "Unique token slug").requiredOption("--concept <concept>", "Concept description").option("--domain <domain>", "Knowledge domain", "").option("--bloom <level>", "Bloom taxonomy level (1-5)", "1").option("--source-link <link>", "Source file path or reference URL", "").option("--question <question>", "Specific question prompt for recall", "").option("--json", "Output as JSON").option("--quiet", "Suppress output (exit code only)").action(async (opts) => {
@@ -10582,12 +12312,12 @@ tokenCommand.command("status").description("Show full status of a token for a us
10582
12312
  });
10583
12313
 
10584
12314
  // src/cli/commands/ui.ts
10585
- import { spawn as spawn2, spawnSync } from "child_process";
10586
- import { existsSync as existsSync19 } from "fs";
10587
- import { homedir as homedir12 } from "os";
10588
- import { dirname as dirname8, join as join20 } from "path";
12315
+ import { spawn as spawn3, spawnSync } from "child_process";
12316
+ import { existsSync as existsSync23 } from "fs";
12317
+ import { homedir as homedir14 } from "os";
12318
+ import { dirname as dirname8, join as join22 } from "path";
10589
12319
  import { fileURLToPath as fileURLToPath3 } from "url";
10590
- import { Command as Command20 } from "commander";
12320
+ import { Command as Command22 } from "commander";
10591
12321
  var C3 = {
10592
12322
  reset: "\x1B[0m",
10593
12323
  red: "\x1B[31m",
@@ -10601,8 +12331,8 @@ function findDesktopDir() {
10601
12331
  for (const start of starts) {
10602
12332
  let dir = start;
10603
12333
  for (let i = 0; i < 10; i++) {
10604
- if (existsSync19(join20(dir, "desktop", "src-tauri", "tauri.conf.json"))) {
10605
- return join20(dir, "desktop");
12334
+ if (existsSync23(join22(dir, "desktop", "src-tauri", "tauri.conf.json"))) {
12335
+ return join22(dir, "desktop");
10606
12336
  }
10607
12337
  const parent = dirname8(dir);
10608
12338
  if (parent === dir) break;
@@ -10612,30 +12342,30 @@ function findDesktopDir() {
10612
12342
  return null;
10613
12343
  }
10614
12344
  function findBuiltApp(desktopDir) {
10615
- const releaseDir = join20(desktopDir, "src-tauri", "target", "release");
12345
+ const releaseDir = join22(desktopDir, "src-tauri", "target", "release");
10616
12346
  if (process.platform === "win32") {
10617
12347
  for (const name of ["ZAM.exe", "zam.exe", "zam-desktop.exe"]) {
10618
- const p = join20(releaseDir, name);
10619
- if (existsSync19(p)) return p;
12348
+ const p = join22(releaseDir, name);
12349
+ if (existsSync23(p)) return p;
10620
12350
  }
10621
12351
  } else if (process.platform === "darwin") {
10622
- const app = join20(releaseDir, "bundle", "macos", "ZAM.app");
10623
- if (existsSync19(app)) return app;
12352
+ const app = join22(releaseDir, "bundle", "macos", "ZAM.app");
12353
+ if (existsSync23(app)) return app;
10624
12354
  } else {
10625
12355
  for (const name of ["zam", "ZAM", "zam-desktop"]) {
10626
- const p = join20(releaseDir, name);
10627
- if (existsSync19(p)) return p;
12356
+ const p = join22(releaseDir, name);
12357
+ if (existsSync23(p)) return p;
10628
12358
  }
10629
12359
  }
10630
12360
  return null;
10631
12361
  }
10632
12362
  function findInstalledApp() {
10633
12363
  const candidates = process.platform === "win32" ? [
10634
- process.env.LOCALAPPDATA && join20(process.env.LOCALAPPDATA, "Programs", "ZAM", "ZAM.exe"),
10635
- process.env.ProgramFiles && join20(process.env.ProgramFiles, "ZAM", "ZAM.exe"),
10636
- process.env["ProgramFiles(x86)"] && join20(process.env["ProgramFiles(x86)"], "ZAM", "ZAM.exe")
10637
- ] : process.platform === "darwin" ? ["/Applications/ZAM.app", join20(homedir12(), "Applications", "ZAM.app")] : ["/opt/ZAM/zam", "/usr/bin/zam-desktop"];
10638
- return candidates.find((candidate) => candidate && existsSync19(candidate)) || null;
12364
+ process.env.LOCALAPPDATA && join22(process.env.LOCALAPPDATA, "Programs", "ZAM", "ZAM.exe"),
12365
+ process.env.ProgramFiles && join22(process.env.ProgramFiles, "ZAM", "ZAM.exe"),
12366
+ process.env["ProgramFiles(x86)"] && join22(process.env["ProgramFiles(x86)"], "ZAM", "ZAM.exe")
12367
+ ] : process.platform === "darwin" ? ["/Applications/ZAM.app", join22(homedir14(), "Applications", "ZAM.app")] : ["/opt/ZAM/zam", "/usr/bin/zam-desktop"];
12368
+ return candidates.find((candidate) => candidate && existsSync23(candidate)) || null;
10639
12369
  }
10640
12370
  function runNpm(args, opts) {
10641
12371
  const res = spawnSync("npm", args, {
@@ -10646,7 +12376,7 @@ function runNpm(args, opts) {
10646
12376
  return res.status ?? 1;
10647
12377
  }
10648
12378
  function ensureDesktopDeps(desktopDir) {
10649
- if (existsSync19(join20(desktopDir, "node_modules"))) return true;
12379
+ if (existsSync23(join22(desktopDir, "node_modules"))) return true;
10650
12380
  console.log(
10651
12381
  `${C3.cyan}Installing desktop dependencies (one-time)...${C3.reset}`
10652
12382
  );
@@ -10672,13 +12402,13 @@ function requireRust() {
10672
12402
  function hasMsvcBuildTools() {
10673
12403
  if (process.platform !== "win32") return true;
10674
12404
  const pf86 = process.env["ProgramFiles(x86)"] || "C:\\Program Files (x86)";
10675
- const vswhere = join20(
12405
+ const vswhere = join22(
10676
12406
  pf86,
10677
12407
  "Microsoft Visual Studio",
10678
12408
  "Installer",
10679
12409
  "vswhere.exe"
10680
12410
  );
10681
- if (!existsSync19(vswhere)) return false;
12411
+ if (!existsSync23(vswhere)) return false;
10682
12412
  const res = spawnSync(
10683
12413
  vswhere,
10684
12414
  [
@@ -10710,7 +12440,7 @@ function requireMsvcOnWindows() {
10710
12440
  return false;
10711
12441
  }
10712
12442
  function warnIfCliMissing(repoRoot) {
10713
- if (!existsSync19(join20(repoRoot, "dist", "cli", "index.js"))) {
12443
+ if (!existsSync23(join22(repoRoot, "dist", "cli", "index.js"))) {
10714
12444
  console.warn(
10715
12445
  `${C3.yellow}\u26A0 CLI build not found (dist/cli/index.js). The GUI bridge needs it \u2014 run 'npm run build' at the repo root.${C3.reset}`
10716
12446
  );
@@ -10719,13 +12449,13 @@ function warnIfCliMissing(repoRoot) {
10719
12449
  function launchApp(appPath, workingDir) {
10720
12450
  console.log(`${C3.green}\u2713 Launching ZAM Desktop...${C3.reset}`);
10721
12451
  if (process.platform === "darwin" && appPath.endsWith(".app")) {
10722
- spawn2("open", [appPath], {
12452
+ spawn3("open", [appPath], {
10723
12453
  cwd: workingDir,
10724
12454
  detached: true,
10725
12455
  stdio: "ignore"
10726
12456
  }).unref();
10727
12457
  } else {
10728
- spawn2(appPath, [], {
12458
+ spawn3(appPath, [], {
10729
12459
  cwd: workingDir,
10730
12460
  detached: true,
10731
12461
  stdio: "ignore",
@@ -10767,13 +12497,13 @@ function createShortcuts(appPath, repoRoot) {
10767
12497
  console.error(`${C3.red}\u2717 Could not create shortcuts.${C3.reset}`);
10768
12498
  }
10769
12499
  }
10770
- var uiCommand = new Command20("ui").description("Launch the ZAM Desktop GUI (Active-Recall Studio)").option("--dev", "Run in hot-reload development mode (needs Rust)").option(
12500
+ var uiCommand = new Command22("ui").description("Launch the ZAM Desktop GUI (Active-Recall Studio)").option("--dev", "Run in hot-reload development mode (needs Rust)").option(
10771
12501
  "--build",
10772
12502
  "Build the native installer for your OS (needs Rust, one-time)"
10773
12503
  ).option("--shortcut", "Create Desktop + Start-menu shortcuts to the GUI").action((opts) => {
10774
12504
  const installedApp = findInstalledApp();
10775
12505
  if (!opts.dev && !opts.build && !opts.shortcut && installedApp) {
10776
- launchApp(installedApp, homedir12());
12506
+ launchApp(installedApp, homedir14());
10777
12507
  return;
10778
12508
  }
10779
12509
  const desktopDir = findDesktopDir();
@@ -10794,7 +12524,7 @@ var uiCommand = new Command20("ui").description("Launch the ZAM Desktop GUI (Act
10794
12524
  );
10795
12525
  const code = runNpm(["run", "tauri", "build"], { cwd: desktopDir });
10796
12526
  if (code === 0) {
10797
- const bundle = join20(
12527
+ const bundle = join22(
10798
12528
  desktopDir,
10799
12529
  "src-tauri",
10800
12530
  "target",
@@ -10871,11 +12601,11 @@ ${C3.dim}After that, 'zam ui' launches it directly.${C3.reset}`
10871
12601
 
10872
12602
  // src/cli/commands/update.ts
10873
12603
  import { spawnSync as spawnSync2 } from "child_process";
10874
- import { existsSync as existsSync20, readFileSync as readFileSync13, realpathSync } from "fs";
10875
- import { dirname as dirname9, join as join21 } from "path";
12604
+ import { existsSync as existsSync24, readFileSync as readFileSync14, realpathSync } from "fs";
12605
+ import { dirname as dirname9, join as join23 } from "path";
10876
12606
  import { fileURLToPath as fileURLToPath4 } from "url";
10877
- import { confirm as confirm3 } from "@inquirer/prompts";
10878
- import { Command as Command21 } from "commander";
12607
+ import { confirm as confirm4 } from "@inquirer/prompts";
12608
+ import { Command as Command23 } from "commander";
10879
12609
  var GITHUB_REPO = "zam-os/zam";
10880
12610
  var CHANNELS = [
10881
12611
  "developer",
@@ -10897,7 +12627,7 @@ function currentVersion() {
10897
12627
  for (const up of ["..", "../..", "../../.."]) {
10898
12628
  try {
10899
12629
  const pkg2 = JSON.parse(
10900
- readFileSync13(join21(here, up, "package.json"), "utf-8")
12630
+ readFileSync14(join23(here, up, "package.json"), "utf-8")
10901
12631
  );
10902
12632
  if (pkg2.version) return pkg2.version;
10903
12633
  } catch {
@@ -10908,7 +12638,7 @@ function currentVersion() {
10908
12638
  function versionAt(dir) {
10909
12639
  try {
10910
12640
  const pkg2 = JSON.parse(
10911
- readFileSync13(join21(dir, "package.json"), "utf-8")
12641
+ readFileSync14(join23(dir, "package.json"), "utf-8")
10912
12642
  );
10913
12643
  return pkg2.version ?? "unknown";
10914
12644
  } catch {
@@ -10953,7 +12683,7 @@ function render2(decision) {
10953
12683
  );
10954
12684
  }
10955
12685
  }
10956
- var checkCmd = new Command21("check").description("Check whether a newer ZAM has been released").option(
12686
+ var checkCmd = new Command23("check").description("Check whether a newer ZAM has been released").option(
10957
12687
  "--latest <version>",
10958
12688
  "Compare against this version instead of fetching"
10959
12689
  ).option("--channel <channel>", "Override the detected install channel").option("--json", "Output as JSON").action(
@@ -10988,13 +12718,13 @@ function findSourceRepo() {
10988
12718
  let dir = realpathSync(dirname9(fileURLToPath4(import.meta.url)));
10989
12719
  let parent = dirname9(dir);
10990
12720
  while (parent !== dir) {
10991
- if (existsSync20(join21(dir, ".git"))) return dir;
12721
+ if (existsSync24(join23(dir, ".git"))) return dir;
10992
12722
  dir = parent;
10993
12723
  parent = dirname9(dir);
10994
12724
  }
10995
- return existsSync20(join21(dir, ".git")) ? dir : null;
12725
+ return existsSync24(join23(dir, ".git")) ? dir : null;
10996
12726
  }
10997
- function runGit(cwd, args, capture) {
12727
+ function runGit2(cwd, args, capture) {
10998
12728
  const res = spawnSync2("git", args, {
10999
12729
  cwd,
11000
12730
  stdio: capture ? ["ignore", "pipe", "pipe"] : "inherit",
@@ -11026,7 +12756,7 @@ function applyDeveloperUpdate(force) {
11026
12756
  );
11027
12757
  process.exit(1);
11028
12758
  }
11029
- const status = runGit(src, ["status", "--porcelain"], true);
12759
+ const status = runGit2(src, ["status", "--porcelain"], true);
11030
12760
  if (status.ok && status.out && !force) {
11031
12761
  console.error(
11032
12762
  `${C4.red}\u2717${C4.reset} The source checkout has uncommitted changes:
@@ -11038,7 +12768,7 @@ Commit or stash them, or re-run with ${C4.cyan}--force${C4.reset}.`
11038
12768
  console.log(
11039
12769
  `${C4.dim}\u2192 git pull --ff-only${C4.reset} ${C4.dim}(${src})${C4.reset}`
11040
12770
  );
11041
- if (!runGit(src, ["pull", "--ff-only"], false).ok) {
12771
+ if (!runGit2(src, ["pull", "--ff-only"], false).ok) {
11042
12772
  console.error(
11043
12773
  `${C4.red}\u2717${C4.reset} git pull failed \u2014 the branch may have diverged or you are offline. Resolve it manually, then retry.`
11044
12774
  );
@@ -11057,7 +12787,7 @@ Commit or stash them, or re-run with ${C4.cyan}--force${C4.reset}.`
11057
12787
  console.log(`${C4.dim}\u2192 zam setup --force${C4.reset}`);
11058
12788
  const setup = spawnSync2(
11059
12789
  process.execPath,
11060
- [join21(src, "dist", "cli", "index.js"), "setup", "--force"],
12790
+ [join23(src, "dist", "cli", "index.js"), "setup", "--force"],
11061
12791
  { cwd: process.cwd(), stdio: "inherit" }
11062
12792
  );
11063
12793
  if (setup.status !== 0) {
@@ -11100,7 +12830,7 @@ ${C4.dim}This install updates through the desktop app's signed updater \u2014 op
11100
12830
  return;
11101
12831
  }
11102
12832
  if (!opts.yes) {
11103
- const ok = await confirm3({
12833
+ const ok = await confirm4({
11104
12834
  message: "Apply this update?",
11105
12835
  default: true
11106
12836
  });
@@ -11120,7 +12850,7 @@ ${C4.dim}This install updates through the desktop app's signed updater \u2014 op
11120
12850
  process.exit(1);
11121
12851
  }
11122
12852
  }
11123
- var updateCommand = new Command21("update").description(
12853
+ var updateCommand = new Command23("update").description(
11124
12854
  "Update ZAM to the latest release (use `update check` to only check)"
11125
12855
  ).option("-y, --yes", "Apply without confirmation").option(
11126
12856
  "--force",
@@ -11130,8 +12860,8 @@ var updateCommand = new Command21("update").description(
11130
12860
  }).addCommand(checkCmd);
11131
12861
 
11132
12862
  // src/cli/commands/whoami.ts
11133
- import { Command as Command22 } from "commander";
11134
- var whoamiCommand = new Command22("whoami").description("Show or set the default user identity").option("--set <id>", "Set the default user ID").option("--clear", "Remove the default user ID").option("--json", "Output as JSON").action(async (opts) => {
12863
+ import { Command as Command24 } from "commander";
12864
+ var whoamiCommand = new Command24("whoami").description("Show or set the default user identity").option("--set <id>", "Set the default user ID").option("--clear", "Remove the default user ID").option("--json", "Output as JSON").action(async (opts) => {
11135
12865
  await withDb(async (db) => {
11136
12866
  if (opts.set) {
11137
12867
  await setSetting(db, "user.id", opts.set);
@@ -11166,163 +12896,12 @@ var whoamiCommand = new Command22("whoami").description("Show or set the default
11166
12896
  });
11167
12897
  });
11168
12898
 
11169
- // src/cli/commands/workspace.ts
11170
- import { execSync as execSync6 } from "child_process";
11171
- import { existsSync as existsSync21, writeFileSync as writeFileSync11 } from "fs";
11172
- import { join as join22 } from "path";
11173
- import { confirm as confirm4, input as input7 } from "@inquirer/prompts";
11174
- import { Command as Command23 } from "commander";
11175
- function runGit2(cwd, args) {
11176
- try {
11177
- return execSync6(`git ${args}`, {
11178
- cwd,
11179
- stdio: "pipe",
11180
- encoding: "utf8"
11181
- }).trim();
11182
- } catch (err) {
11183
- throw new Error(`Git command failed: ${err.message}`);
11184
- }
11185
- }
11186
- var workspaceCommand = new Command23("workspace").description(
11187
- "Manage your ZAM learning workspace"
11188
- );
11189
- workspaceCommand.command("publish").description("Publish your local workspace sandbox to GitHub").action(async () => {
11190
- let db;
11191
- let workspaceDir = "";
11192
- try {
11193
- db = await openDatabase();
11194
- workspaceDir = await getSetting(db, "personal.workspace_dir") || "";
11195
- await db.close();
11196
- } catch {
11197
- await db?.close();
11198
- }
11199
- if (!workspaceDir) {
11200
- console.error(
11201
- "\x1B[31m\u2717 No active workspace configured. Please run `zam init` first.\x1B[0m"
11202
- );
11203
- process.exit(1);
11204
- }
11205
- if (!existsSync21(workspaceDir)) {
11206
- console.error(
11207
- `\x1B[31m\u2717 Workspace directory does not exist: ${workspaceDir}\x1B[0m`
11208
- );
11209
- process.exit(1);
11210
- }
11211
- console.log(`
11212
- Active workspace: \x1B[36m${workspaceDir}\x1B[0m`);
11213
- if (!hasCommand("git")) {
11214
- console.error(
11215
- "\x1B[31m\u2717 Git command was not found on this system. Please install Git first.\x1B[0m"
11216
- );
11217
- process.exit(1);
11218
- }
11219
- const gitignorePath = join22(workspaceDir, ".gitignore");
11220
- if (!existsSync21(gitignorePath)) {
11221
- writeFileSync11(
11222
- gitignorePath,
11223
- "node_modules/\n.agent/\n.agents/\n.claude/\n.gemini/\n.goose/\n*.log\n",
11224
- "utf8"
11225
- );
11226
- }
11227
- const hasGitRepo = existsSync21(join22(workspaceDir, ".git"));
11228
- if (!hasGitRepo) {
11229
- console.log("Initializing local Git repository...");
11230
- runGit2(workspaceDir, "init -b main");
11231
- runGit2(workspaceDir, "add .");
11232
- runGit2(
11233
- workspaceDir,
11234
- 'commit -m "chore: initial workspace sandbox bootstrap"'
11235
- );
11236
- console.log("\x1B[32m\u2713 Local Git repository initialized.\x1B[0m");
11237
- } else {
11238
- console.log("Git repository is already initialized.");
11239
- }
11240
- const repoName = await input7({
11241
- message: "Choose a name for your GitHub repository:",
11242
- default: "zam-personal"
11243
- });
11244
- const isPrivate = await confirm4({
11245
- message: "Should the repository be private?",
11246
- default: true
11247
- });
11248
- const repoVisibility = isPrivate ? "--private" : "--public";
11249
- if (hasCommand("gh")) {
11250
- console.log("GitHub CLI detected! Automating repository creation...");
11251
- const proceedGh = await confirm4({
11252
- message: "Would you like ZAM to create the repository using the GitHub CLI?",
11253
- default: true
11254
- });
11255
- if (proceedGh) {
11256
- try {
11257
- console.log(`Creating GitHub repository ${repoName}...`);
11258
- execSync6(
11259
- `gh repo create ${repoName} ${repoVisibility} --source=. --push`,
11260
- {
11261
- cwd: workspaceDir,
11262
- stdio: "inherit"
11263
- }
11264
- );
11265
- console.log(
11266
- "\n\x1B[32m\u2713 Successfully published workspace to GitHub!\x1B[0m"
11267
- );
11268
- process.exit(0);
11269
- } catch (err) {
11270
- console.warn(
11271
- `\x1B[33m\u26A0 GitHub CLI creation failed: ${err.message}\x1B[0m`
11272
- );
11273
- }
11274
- }
11275
- }
11276
- console.log(
11277
- "\n\x1B[1mPlease create the repository manually on GitHub:\x1B[0m"
11278
- );
11279
- console.log(" 1. Go to https://github.com/new");
11280
- console.log(` 2. Name it exactly: \x1B[36m${repoName}\x1B[0m`);
11281
- console.log(
11282
- ` 3. Choose \x1B[36m${isPrivate ? "Private" : "Public"}\x1B[0m`
11283
- );
11284
- console.log(
11285
- " 4. Do NOT initialize it with README, .gitignore, or license"
11286
- );
11287
- console.log(" 5. Click 'Create repository'\n");
11288
- const githubUrl = await input7({
11289
- message: "Paste the repository Git URL (e.g. git@github.com:user/repo.git):"
11290
- });
11291
- if (githubUrl) {
11292
- try {
11293
- console.log("Linking remote repository and pushing...");
11294
- let hasOrigin = false;
11295
- try {
11296
- runGit2(workspaceDir, "remote get-url origin");
11297
- hasOrigin = true;
11298
- } catch {
11299
- }
11300
- if (hasOrigin) {
11301
- runGit2(workspaceDir, `remote set-url origin ${githubUrl}`);
11302
- } else {
11303
- runGit2(workspaceDir, `remote add origin ${githubUrl}`);
11304
- }
11305
- runGit2(workspaceDir, "push -u origin main");
11306
- console.log(
11307
- "\x1B[32m\u2713 Successfully linked and pushed to GitHub!\x1B[0m"
11308
- );
11309
- } catch (err) {
11310
- console.error(
11311
- `\x1B[31m\u2717 Push failed: ${err.message}\x1B[0m`
11312
- );
11313
- console.log(
11314
- "You can push manually later using: git push -u origin main"
11315
- );
11316
- }
11317
- }
11318
- });
11319
-
11320
12899
  // src/cli/index.ts
11321
12900
  var __dirname = dirname10(fileURLToPath5(import.meta.url));
11322
12901
  var pkg = JSON.parse(
11323
- readFileSync14(join23(__dirname, "..", "..", "package.json"), "utf-8")
12902
+ readFileSync15(join24(__dirname, "..", "..", "package.json"), "utf-8")
11324
12903
  );
11325
- var program = new Command24();
12904
+ var program = new Command25();
11326
12905
  program.name("zam").description(
11327
12906
  "The Symbiotic Learning Kernel: Elevating Human Intelligence through AI Collaboration."
11328
12907
  ).version(pkg.version);
@@ -11342,6 +12921,7 @@ program.addCommand(observerCommand);
11342
12921
  program.addCommand(settingsCommand);
11343
12922
  program.addCommand(whoamiCommand);
11344
12923
  program.addCommand(connectorCommand);
12924
+ program.addCommand(providerCommand);
11345
12925
  program.addCommand(snapshotCommand);
11346
12926
  program.addCommand(profileCommand);
11347
12927
  program.addCommand(updateCommand);