zam-core 0.4.2 → 0.4.4

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/dist/cli/index.js CHANGED
@@ -2,13 +2,13 @@
2
2
 
3
3
  // src/cli/index.ts
4
4
  import { readFileSync as readFileSync14 } from "fs";
5
- import { dirname as dirname10, join as join23 } from "path";
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() {
@@ -4707,13 +4726,11 @@ function runCommand(cmd) {
4707
4726
  return "";
4708
4727
  }
4709
4728
  }
4710
- function detectWindowsAMDIPU() {
4729
+ function detectWindowsNPU() {
4711
4730
  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"`;
4731
+ 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
4732
  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
- );
4733
+ return Boolean(output && output.length > 0);
4717
4734
  }
4718
4735
  function getSystemProfile() {
4719
4736
  const platform = process.platform;
@@ -4725,13 +4742,21 @@ function getSystemProfile() {
4725
4742
  let arch = "unknown";
4726
4743
  if (archStr === "x64") arch = "x64";
4727
4744
  else if (archStr === "arm64") arch = "arm64";
4728
- const hasRyzenNPU = os === "windows" && detectWindowsAMDIPU();
4745
+ const hasNpu = os === "windows" && detectWindowsNPU();
4729
4746
  const hasAppleSilicon = os === "macos" && arch === "arm64";
4730
4747
  let recommendedRunner = "generic";
4731
4748
  let recommendedModel = "qwen3.5:4b";
4732
- if (hasRyzenNPU) {
4733
- recommendedRunner = "fastflowlm";
4734
- recommendedModel = "qwen3.5:4b";
4749
+ if (hasNpu) {
4750
+ const isQualcomm = runCommand(
4751
+ `powershell -NoProfile -Command "Get-CimInstance Win32_PnPEntity | Where-Object { $_.Name -like '*Qualcomm*' } | Select-Object -First 1"`
4752
+ ).length > 0;
4753
+ if (isQualcomm) {
4754
+ recommendedRunner = "generic";
4755
+ recommendedModel = "qwen3.5-4b";
4756
+ } else {
4757
+ recommendedRunner = "fastflowlm";
4758
+ recommendedModel = "qwen3.5:4b";
4759
+ }
4735
4760
  } else if (hasAppleSilicon) {
4736
4761
  recommendedRunner = "ollama";
4737
4762
  recommendedModel = "llama3.2:3b";
@@ -4742,7 +4767,8 @@ function getSystemProfile() {
4742
4767
  return {
4743
4768
  os,
4744
4769
  arch,
4745
- hasRyzenNPU,
4770
+ hasRyzenNPU: hasNpu && !archStr.includes("arm64"),
4771
+ // backwards compatibility flag
4746
4772
  hasAppleSilicon,
4747
4773
  recommendedRunner,
4748
4774
  recommendedModel
@@ -4879,6 +4905,289 @@ function planUpdate(decision) {
4879
4905
  }
4880
4906
  }
4881
4907
 
4908
+ // src/cli/agent-harness.ts
4909
+ import { spawn } from "child_process";
4910
+ import { existsSync as existsSync12 } from "fs";
4911
+ import { homedir as homedir8 } from "os";
4912
+ import { join as join12 } from "path";
4913
+
4914
+ // src/cli/terminal-open.ts
4915
+ import { execFileSync as execFileSync2, execSync as execSync4 } from "child_process";
4916
+ import {
4917
+ accessSync,
4918
+ constants,
4919
+ existsSync as existsSync11,
4920
+ unlinkSync,
4921
+ writeFileSync as writeFileSync6
4922
+ } from "fs";
4923
+ import { tmpdir } from "os";
4924
+ import { basename as basename2, delimiter, extname, isAbsolute, join as join11 } from "path";
4925
+ function isPowerShellShell(shell) {
4926
+ return shell === "pwsh" || shell === "powershell";
4927
+ }
4928
+ function psSingleQuoted2(value) {
4929
+ return `'${value.replace(/'/g, "''")}'`;
4930
+ }
4931
+ function detectShell() {
4932
+ if (process.platform === "win32")
4933
+ return findExecutable("pwsh.exe") ? "pwsh" : "powershell";
4934
+ const shell = process.env.SHELL ?? "";
4935
+ return basename2(shell) === "bash" ? "bash" : "zsh";
4936
+ }
4937
+ function normalizeShell(shell) {
4938
+ if (!shell) return detectShell();
4939
+ const normalized = shell.toLowerCase();
4940
+ if (normalized === "zsh" || normalized === "bash" || normalized === "pwsh" || normalized === "powershell") {
4941
+ return normalized;
4942
+ }
4943
+ throw new Error(
4944
+ `Unsupported shell: ${shell}. Expected zsh, bash, pwsh, or powershell.`
4945
+ );
4946
+ }
4947
+ function selectWindowsExecutable(results, pathext = process.env.PATHEXT ?? ".COM;.EXE;.BAT;.CMD") {
4948
+ if (results.length === 0) return null;
4949
+ const extensions = pathext.split(";").map((ext) => ext.trim().toLowerCase()).filter(Boolean);
4950
+ const runnable = results.find(
4951
+ (result) => extensions.some((ext) => result.toLowerCase().endsWith(ext))
4952
+ );
4953
+ return runnable ?? results[0];
4954
+ }
4955
+ function stripSurroundingQuotes(command) {
4956
+ const trimmed = command.trim();
4957
+ if (trimmed.length < 2) return trimmed;
4958
+ const first = trimmed[0];
4959
+ const last = trimmed[trimmed.length - 1];
4960
+ if (first === '"' && last === '"' || first === "'" && last === "'") {
4961
+ return trimmed.slice(1, -1);
4962
+ }
4963
+ return trimmed;
4964
+ }
4965
+ function executableExists(path) {
4966
+ if (!existsSync11(path)) return false;
4967
+ if (process.platform === "win32") return true;
4968
+ try {
4969
+ accessSync(path, constants.X_OK);
4970
+ return true;
4971
+ } catch {
4972
+ return false;
4973
+ }
4974
+ }
4975
+ function windowsExecutableNames(command) {
4976
+ if (process.platform !== "win32") return [command];
4977
+ if (extname(command)) return [command];
4978
+ const extensions = (process.env.PATHEXT ?? ".COM;.EXE;.BAT;.CMD").split(";").map((ext) => ext.trim()).filter(Boolean);
4979
+ return extensions.map((ext) => `${command}${ext}`);
4980
+ }
4981
+ function hasDirectoryPart(command) {
4982
+ return isAbsolute(command) || command.includes("/") || command.includes("\\");
4983
+ }
4984
+ function findExecutable(command) {
4985
+ const normalized = stripSurroundingQuotes(command);
4986
+ if (!normalized) return null;
4987
+ const matches = [];
4988
+ if (hasDirectoryPart(normalized)) {
4989
+ for (const candidate of windowsExecutableNames(normalized)) {
4990
+ if (executableExists(candidate)) matches.push(candidate);
4991
+ }
4992
+ return process.platform === "win32" ? selectWindowsExecutable(matches) : matches[0] ?? null;
4993
+ }
4994
+ const pathEntries = (process.env.PATH ?? "").split(delimiter).map((entry) => entry.trim()).filter(Boolean);
4995
+ for (const entry of pathEntries) {
4996
+ for (const name of windowsExecutableNames(normalized)) {
4997
+ const candidate = join11(entry, name);
4998
+ if (executableExists(candidate)) matches.push(candidate);
4999
+ }
5000
+ }
5001
+ return process.platform === "win32" ? selectWindowsExecutable(matches) : matches[0] ?? null;
5002
+ }
5003
+ function resolveZamInvocation(shell) {
5004
+ const installed = findExecutable("zam");
5005
+ if (installed) {
5006
+ return isPowerShellShell(shell) ? `& ${psSingleQuoted2(installed)}` : installed;
5007
+ }
5008
+ const projectRoot = join11(import.meta.dirname, "..", "..");
5009
+ const cliSource = join11(projectRoot, "src/cli/index.ts");
5010
+ if (isPowerShellShell(shell)) {
5011
+ return `& npx --prefix ${psSingleQuoted2(projectRoot)} tsx ${psSingleQuoted2(cliSource)}`;
5012
+ }
5013
+ return `npx --prefix ${JSON.stringify(projectRoot)} tsx ${JSON.stringify(cliSource)}`;
5014
+ }
5015
+ function buildShellSetupCommand(dir, shell, command) {
5016
+ if (isPowerShellShell(shell)) {
5017
+ return [`Set-Location -LiteralPath ${psSingleQuoted2(dir)}`, command].join(
5018
+ "; "
5019
+ );
5020
+ }
5021
+ return `cd ${JSON.stringify(dir)} && ${command}`;
5022
+ }
5023
+ function isItermRunning() {
5024
+ try {
5025
+ const result = execSync4(
5026
+ `osascript -e 'tell application "System Events" to (name of processes) contains "iTerm2"' 2>/dev/null`,
5027
+ { encoding: "utf-8" }
5028
+ ).trim();
5029
+ return result === "true";
5030
+ } catch {
5031
+ return false;
5032
+ }
5033
+ }
5034
+ function openMacTerminal(shellSetup, label, dir) {
5035
+ const useIterm = isItermRunning();
5036
+ const escaped = shellSetup.replace(/\\/g, "\\\\").replace(/"/g, '\\"');
5037
+ const appleScript = useIterm ? `tell application "iTerm2"
5038
+ activate
5039
+ set newWindow to (create window with default profile)
5040
+ tell current session of newWindow
5041
+ write text "${escaped}"
5042
+ end tell
5043
+ end tell` : `tell application "Terminal"
5044
+ activate
5045
+ do script "${escaped}"
5046
+ end tell`;
5047
+ const tmpFile = join11(tmpdir(), `zam-terminal-${label}.scpt`);
5048
+ try {
5049
+ writeFileSync6(tmpFile, appleScript);
5050
+ execSync4(`osascript ${JSON.stringify(tmpFile)}`, { stdio: "ignore" });
5051
+ console.log(
5052
+ `Opened ${useIterm ? "iTerm2" : "Terminal.app"} window (${label})`
5053
+ );
5054
+ console.log(` Directory: ${dir}`);
5055
+ } catch (err) {
5056
+ console.error(`Failed to open terminal: ${err.message}`);
5057
+ console.log(`
5058
+ Run this manually in a new terminal:
5059
+ `);
5060
+ console.log(` ${shellSetup}`);
5061
+ } finally {
5062
+ try {
5063
+ unlinkSync(tmpFile);
5064
+ } catch {
5065
+ }
5066
+ }
5067
+ }
5068
+ function openWindowsPowerShell(shellSetup, label, dir, requestedShell) {
5069
+ const requestedExecutable = requestedShell === "powershell" ? "powershell.exe" : "pwsh.exe";
5070
+ const executable = findExecutable(requestedExecutable) ? requestedExecutable : "powershell.exe";
5071
+ const startCommand = [
5072
+ "Start-Process",
5073
+ `-FilePath ${psSingleQuoted2(executable)}`,
5074
+ `-ArgumentList @('-NoExit','-NoProfile','-Command',${psSingleQuoted2(shellSetup)})`
5075
+ ].join(" ");
5076
+ const launcher = findExecutable("pwsh.exe") ? "pwsh.exe" : "powershell.exe";
5077
+ try {
5078
+ execFileSync2(
5079
+ launcher,
5080
+ ["-NoProfile", "-ExecutionPolicy", "Bypass", "-Command", startCommand],
5081
+ {
5082
+ stdio: "ignore"
5083
+ }
5084
+ );
5085
+ console.log(
5086
+ `Opened ${executable === "pwsh.exe" ? "PowerShell" : "Windows PowerShell"} window (${label})`
5087
+ );
5088
+ console.log(` Directory: ${dir}`);
5089
+ } catch (err) {
5090
+ console.error(`Failed to open PowerShell: ${err.message}`);
5091
+ console.log(`
5092
+ Run this manually in a new PowerShell terminal:
5093
+ `);
5094
+ console.log(` ${shellSetup}`);
5095
+ }
5096
+ }
5097
+ function openTerminalWindow(opts) {
5098
+ const { shellSetup, label, dir, shell } = opts;
5099
+ if (process.platform === "darwin" && !isPowerShellShell(shell)) {
5100
+ openMacTerminal(shellSetup, label, dir);
5101
+ return;
5102
+ }
5103
+ if (process.platform === "win32" && isPowerShellShell(shell)) {
5104
+ openWindowsPowerShell(shellSetup, label, dir, shell);
5105
+ return;
5106
+ }
5107
+ console.log(`Run this in a new terminal:
5108
+ `);
5109
+ console.log(` ${shellSetup}
5110
+ `);
5111
+ console.log(
5112
+ `(Automatic terminal opening is only supported on macOS Terminal/iTerm2 and Windows PowerShell for now.)`
5113
+ );
5114
+ }
5115
+
5116
+ // src/cli/agent-harness.ts
5117
+ var AGENT_HARNESSES = [
5118
+ { id: "claude-code", label: "Claude Code", kind: "cli", command: "claude" },
5119
+ { id: "codex", label: "Codex", kind: "cli", command: "codex" },
5120
+ { id: "opencode", label: "opencode", kind: "cli", command: "opencode" },
5121
+ {
5122
+ id: "cursor",
5123
+ label: "Cursor",
5124
+ kind: "app",
5125
+ command: "cursor",
5126
+ candidatePaths: {
5127
+ win32: [
5128
+ join12(homedir8(), "AppData", "Local", "Programs", "cursor", "Cursor.exe")
5129
+ ],
5130
+ darwin: ["/Applications/Cursor.app/Contents/MacOS/Cursor"]
5131
+ }
5132
+ },
5133
+ { id: "copilot", label: "GitHub Copilot", kind: "app", command: "copilot" },
5134
+ {
5135
+ id: "antigravity",
5136
+ label: "Antigravity",
5137
+ kind: "app",
5138
+ command: "antigravity"
5139
+ }
5140
+ ];
5141
+ function getHarness(id) {
5142
+ return AGENT_HARNESSES.find((h) => h.id === id);
5143
+ }
5144
+ function resolveHarnessExecutable(harness, overrideCommand, deps = {}) {
5145
+ const find = deps.find ?? findExecutable;
5146
+ const exists = deps.exists ?? existsSync12;
5147
+ const platform = deps.platform ?? process.platform;
5148
+ const found = find(overrideCommand || harness.command);
5149
+ if (found) return found;
5150
+ if (harness.kind === "app") {
5151
+ for (const candidate of harness.candidatePaths?.[platform] ?? []) {
5152
+ if (exists(candidate)) return candidate;
5153
+ }
5154
+ }
5155
+ return null;
5156
+ }
5157
+ function planHarnessLaunch(harness, opts) {
5158
+ if (harness.kind === "cli") {
5159
+ const invocation = isPowerShellShell(opts.shell) ? `& ${psSingleQuoted2(opts.executable)}` : JSON.stringify(opts.executable);
5160
+ return {
5161
+ kind: "cli",
5162
+ shell: opts.shell,
5163
+ shellSetup: buildShellSetupCommand(
5164
+ opts.workspace,
5165
+ opts.shell,
5166
+ invocation
5167
+ )
5168
+ };
5169
+ }
5170
+ return { kind: "app", executable: opts.executable, args: [opts.workspace] };
5171
+ }
5172
+ function launchHarness(harness, opts) {
5173
+ const plan = planHarnessLaunch(harness, opts);
5174
+ if (plan.kind === "cli") {
5175
+ openTerminalWindow({
5176
+ shellSetup: plan.shellSetup,
5177
+ label: `agent-${harness.id}`,
5178
+ dir: opts.workspace,
5179
+ shell: plan.shell
5180
+ });
5181
+ return;
5182
+ }
5183
+ spawn(plan.executable, plan.args, {
5184
+ detached: true,
5185
+ stdio: "ignore",
5186
+ windowsHide: true
5187
+ }).unref();
5188
+ console.log(`Launched ${harness.label} in ${opts.workspace}`);
5189
+ }
5190
+
4882
5191
  // src/cli/commands/agent.ts
4883
5192
  var C = {
4884
5193
  reset: "\x1B[0m",
@@ -4890,7 +5199,7 @@ var C = {
4890
5199
  };
4891
5200
  var SUPPORTED_AGENTS = ["opencode"];
4892
5201
  function agentsMdPresent(cwd = process.cwd()) {
4893
- return existsSync11(join11(cwd, "AGENTS.md"));
5202
+ return existsSync13(join13(cwd, "AGENTS.md"));
4894
5203
  }
4895
5204
  function printStatus() {
4896
5205
  const installed = hasCommand("opencode");
@@ -4928,19 +5237,87 @@ var installCmd = new Command("install").description("Download and install the de
4928
5237
  console.log(` Start it with: ${C.cyan}opencode${C.reset}`);
4929
5238
  });
4930
5239
  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);
5240
+ var listCmd = new Command("list").description("List known agent harnesses and whether each is detected").action(() => {
5241
+ console.log(`${C.bold}Agent harnesses${C.reset}`);
5242
+ for (const h of AGENT_HARNESSES) {
5243
+ const status = resolveHarnessExecutable(h) ? `${C.green}detected${C.reset}` : `${C.yellow}not found${C.reset}`;
5244
+ console.log(
5245
+ ` ${h.id.padEnd(13)} ${status} ${C.dim}${h.label} (${h.kind})${C.reset}`
5246
+ );
5247
+ }
5248
+ console.log(`
5249
+ ${C.dim}Open one: zam agent open --id <id>${C.reset}`);
5250
+ console.log(
5251
+ ` ${C.dim}Point ZAM at a custom path: zam settings set agent.<id>.command <path>${C.reset}`
5252
+ );
5253
+ });
5254
+ var openCmd = new Command("open").description("Open an agent harness in the workspace, ready to drive ZAM").option(
5255
+ "--id <id>",
5256
+ "Harness id (default: agent.default setting, else first detected)"
5257
+ ).option("--dir <path>", "Workspace directory (defaults to cwd)").option(
5258
+ "--shell <type>",
5259
+ "Shell type: zsh | bash | pwsh | powershell (auto-detected)"
5260
+ ).action(async (opts) => {
5261
+ let shell;
5262
+ try {
5263
+ shell = normalizeShell(opts.shell);
5264
+ } catch (err) {
5265
+ console.error(`Error: ${err.message}`);
5266
+ process.exit(1);
5267
+ }
5268
+ let id = opts.id;
5269
+ let override;
5270
+ let db;
5271
+ try {
5272
+ db = await openDatabase();
5273
+ if (!id) {
5274
+ id = await getSetting(db, "agent.default") || void 0;
5275
+ }
5276
+ if (!id) {
5277
+ id = AGENT_HARNESSES.find((h) => resolveHarnessExecutable(h))?.id;
5278
+ }
5279
+ if (id) {
5280
+ override = await getSetting(db, `agent.${id}.command`) || void 0;
5281
+ }
5282
+ } finally {
5283
+ await db?.close();
5284
+ }
5285
+ if (!id) {
5286
+ console.error(
5287
+ "No agent harness configured or detected. Install one (e.g. zam agent install), then set a default: zam settings set agent.default <id>."
5288
+ );
5289
+ process.exit(1);
5290
+ }
5291
+ const harness = getHarness(id);
5292
+ if (!harness) {
5293
+ console.error(
5294
+ `Unknown harness: ${id}. Known: ${AGENT_HARNESSES.map((h) => h.id).join(", ")}.`
5295
+ );
5296
+ process.exit(1);
5297
+ }
5298
+ const executable = resolveHarnessExecutable(harness, override);
5299
+ if (!executable) {
5300
+ console.error(
5301
+ `${harness.label} was not detected. Install it, or point ZAM at it: zam settings set agent.${harness.id}.command <path>.`
5302
+ );
5303
+ process.exit(1);
5304
+ }
5305
+ const workspace = opts.dir ?? process.cwd();
5306
+ launchHarness(harness, { executable, workspace, shell });
5307
+ });
5308
+ 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
5309
 
4933
5310
  // src/cli/commands/bridge.ts
4934
- import { execFileSync as execFileSync2 } from "child_process";
5311
+ import { execFileSync as execFileSync4 } from "child_process";
4935
5312
  import { randomBytes as randomBytes2 } from "crypto";
4936
5313
  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";
5314
+ import { homedir as homedir10, tmpdir as tmpdir3 } from "os";
5315
+ import { join as join16, resolve as resolve3 } from "path";
5316
+ import { Command as Command3 } from "commander";
4940
5317
 
4941
5318
  // src/cli/llm/client.ts
4942
- import { spawn } from "child_process";
4943
- import { existsSync as existsSync12 } from "fs";
5319
+ import { spawn as spawn2 } from "child_process";
5320
+ import { existsSync as existsSync14 } from "fs";
4944
5321
  var DEFAULT_LLM_URL = "http://localhost:8000/v1";
4945
5322
  var DEFAULT_LLM_MODEL = "qwen3.5:4b";
4946
5323
  var DEFAULT_LLM_API_KEY = "sk-none";
@@ -4953,16 +5330,108 @@ async function getLlmConfig(db) {
4953
5330
  locale: await getSetting(db, "system.locale") || "en"
4954
5331
  };
4955
5332
  }
4956
- async function getVisionConfig(db) {
5333
+ function getCloudModelRecommendation(url) {
5334
+ const lowercase = url.toLowerCase();
5335
+ if (lowercase.includes("openrouter.ai")) {
5336
+ return "openrouter/free";
5337
+ }
5338
+ if (lowercase.includes("openai.com") || lowercase.includes("api.openai")) {
5339
+ return "gpt-5-mini";
5340
+ }
5341
+ if (lowercase.includes("googleapis.com") || lowercase.includes("google")) {
5342
+ return "gemini-3.5-flash";
5343
+ }
5344
+ if (lowercase.includes("deepseek.com")) {
5345
+ return "deepseek-v4-flash";
5346
+ }
5347
+ if (lowercase.includes("mimo")) {
5348
+ return "mimo-v2.5";
5349
+ }
5350
+ return null;
5351
+ }
5352
+ function inferApiFlavor(url) {
5353
+ try {
5354
+ return new URL(url).hostname.toLowerCase().endsWith("anthropic.com") ? "anthropic-messages" : "chat-completions";
5355
+ } catch {
5356
+ return "chat-completions";
5357
+ }
5358
+ }
5359
+ async function readJsonSetting(db, key) {
5360
+ const raw = await getSetting(db, key);
5361
+ if (!raw) return null;
5362
+ try {
5363
+ return JSON.parse(raw);
5364
+ } catch {
5365
+ return null;
5366
+ }
5367
+ }
5368
+ function resolveProviderApiKey(rec) {
5369
+ if (rec.apiKey) return rec.apiKey;
5370
+ if (rec.apiKeyRef) {
5371
+ const key = getProviderApiKey(rec.apiKeyRef);
5372
+ if (key) return key;
5373
+ }
5374
+ return DEFAULT_LLM_API_KEY;
5375
+ }
5376
+ async function getProviderForRole(db, role) {
5377
+ const enabled = role === "vision" ? await getSetting(db, "llm.vision.enabled") === "true" : await getSetting(db, "llm.enabled") === "true";
5378
+ const base = await getLegacyRoleConfig(db, role, enabled);
5379
+ const providers = await readJsonSetting(db, "llm.providers");
5380
+ const roles = await readJsonSetting(db, "llm.roles");
5381
+ const binding = roles?.[role];
5382
+ if (providers && binding?.primary && providers[binding.primary]) {
5383
+ const primary = materializeProvider(providers[binding.primary], base, role);
5384
+ const fallback = binding.fallback && providers[binding.fallback] ? materializeProvider(providers[binding.fallback], base, role) : void 0;
5385
+ return { ...primary, fallback };
5386
+ }
5387
+ return base;
5388
+ }
5389
+ function materializeProvider(rec, base, role) {
5390
+ const url = rec.url || base.url;
5391
+ return {
5392
+ enabled: base.enabled,
5393
+ url,
5394
+ model: rec.model || base.model,
5395
+ apiKey: resolveProviderApiKey(rec),
5396
+ apiFlavor: rec.apiFlavor || inferApiFlavor(url),
5397
+ locale: base.locale,
5398
+ ...role === "vision" ? { maxFrames: base.maxFrames } : {}
5399
+ };
5400
+ }
5401
+ async function getLegacyRoleConfig(db, role, enabled) {
4957
5402
  const base = await getLlmConfig(db);
5403
+ if (role === "vision") {
5404
+ const maxFramesStr = await getSetting(db, "llm.vision.max_frames");
5405
+ const parsed = maxFramesStr ? parseInt(maxFramesStr, 10) : 100;
5406
+ const url = await getSetting(db, "llm.vision.url") || base.url;
5407
+ let model = await getSetting(db, "llm.vision.model");
5408
+ if (!model) model = getCloudModelRecommendation(url) || base.model;
5409
+ return {
5410
+ enabled,
5411
+ url,
5412
+ model,
5413
+ apiKey: await getSetting(db, "llm.vision.api_key") || base.apiKey,
5414
+ apiFlavor: inferApiFlavor(url),
5415
+ locale: base.locale,
5416
+ maxFrames: Number.isNaN(parsed) ? 100 : parsed
5417
+ };
5418
+ }
4958
5419
  return {
4959
- enabled: await getSetting(db, "llm.vision.enabled") === "true",
4960
- url: await getSetting(db, "llm.vision.url") || base.url,
4961
- model: await getSetting(db, "llm.vision.model") || base.model,
4962
- apiKey: await getSetting(db, "llm.vision.api_key") || base.apiKey,
5420
+ enabled,
5421
+ url: base.url,
5422
+ model: base.model,
5423
+ apiKey: base.apiKey,
5424
+ apiFlavor: inferApiFlavor(base.url),
4963
5425
  locale: base.locale
4964
5426
  };
4965
5427
  }
5428
+ function assertChatCompletions(cfg) {
5429
+ if (cfg.apiFlavor !== "chat-completions") {
5430
+ throw new Error(
5431
+ `This role is configured for a "${cfg.apiFlavor}" provider, which is not supported here yet. Use a chat-completions provider for the recall role.`
5432
+ );
5433
+ }
5434
+ }
4966
5435
  var LANGUAGE_NAMES = {
4967
5436
  en: "English",
4968
5437
  de: "German",
@@ -5003,10 +5472,11 @@ async function readChatContent(res, label) {
5003
5472
  return content.trim();
5004
5473
  }
5005
5474
  async function generateQuestionViaLLM(db, input8) {
5006
- const cfg = await getLlmConfig(db);
5475
+ const cfg = await getProviderForRole(db, "recall");
5007
5476
  if (!cfg.enabled) {
5008
5477
  throw new Error("LLM integration is disabled in settings (llm.enabled)");
5009
5478
  }
5479
+ assertChatCompletions(cfg);
5010
5480
  const bloom = input8.bloomLevel >= 1 && input8.bloomLevel <= 5 ? input8.bloomLevel : 1;
5011
5481
  const verb = BLOOM_VERBS2[bloom];
5012
5482
  const langName = LANGUAGE_NAMES[cfg.locale] || "English";
@@ -5046,10 +5516,11 @@ Active-Recall Question:`;
5046
5516
  return readChatContent(res, "LLM request");
5047
5517
  }
5048
5518
  async function evaluateAnswerViaLLM(db, input8) {
5049
- const cfg = await getLlmConfig(db);
5519
+ const cfg = await getProviderForRole(db, "recall");
5050
5520
  if (!cfg.enabled) {
5051
5521
  throw new Error("LLM integration is disabled in settings (llm.enabled)");
5052
5522
  }
5523
+ assertChatCompletions(cfg);
5053
5524
  const langName = LANGUAGE_NAMES[cfg.locale] || "English";
5054
5525
  const ratingPrefix = LOCALIZED_RATING_PREFIX[cfg.locale] || "Suggested rating";
5055
5526
  const systemPrompt = `You are ZAM, an extremely warm, encouraging, and patient skills trainer.
@@ -5099,10 +5570,11 @@ Evaluation:`;
5099
5570
  return readChatContent(res, "LLM evaluation");
5100
5571
  }
5101
5572
  async function translateQuestionViaLLM(db, question) {
5102
- const cfg = await getLlmConfig(db);
5573
+ const cfg = await getProviderForRole(db, "recall");
5103
5574
  if (!cfg.enabled) {
5104
5575
  throw new Error("LLM integration is disabled in settings");
5105
5576
  }
5577
+ assertChatCompletions(cfg);
5106
5578
  const targetLang = LANGUAGE_NAMES[cfg.locale] || "English";
5107
5579
  const systemPrompt = `You are a highly precise translator. Translate the given active-recall question into clear, natural ${targetLang}.
5108
5580
  Output ONLY the raw translation. Do not include any headers, preamble, quotes, or conversational filler.`;
@@ -5156,34 +5628,82 @@ async function getAvailableModels(url, apiKey = DEFAULT_LLM_API_KEY) {
5156
5628
  return [];
5157
5629
  }
5158
5630
  }
5159
- async function checkVisionReadiness(db) {
5160
- const { enabled, url, model, apiKey } = await getVisionConfig(db);
5161
- const visionModelSetting = await getSetting(db, "llm.vision.model");
5162
- const visionModelExplicit = !!visionModelSetting;
5163
- let online = false;
5164
- let availableModels = [];
5165
- let modelAvailable = false;
5166
- if (enabled) {
5167
- online = await isLlmOnline(url);
5168
- if (online) {
5169
- availableModels = await getAvailableModels(url, apiKey);
5170
- modelAvailable = availableModels.length === 0 || availableModels.some(
5171
- (candidate) => candidate.toLowerCase() === model.toLowerCase()
5172
- );
5173
- }
5631
+ function isLocalEndpoint(url) {
5632
+ return url.includes("localhost") || url.includes("127.0.0.1") || url.includes("[::1]") || url.includes("::1");
5633
+ }
5634
+ async function checkProviderEndpoint(endpoint) {
5635
+ const online = await isLlmOnline(endpoint.url);
5636
+ if (!online) {
5637
+ return {
5638
+ endpoint,
5639
+ online: false,
5640
+ availableModels: [],
5641
+ modelAvailable: false
5642
+ };
5174
5643
  }
5644
+ const availableModels = await getAvailableModels(
5645
+ endpoint.url,
5646
+ endpoint.apiKey
5647
+ );
5648
+ const modelAvailable = availableModels.length === 0 || availableModels.some(
5649
+ (candidate) => candidate.toLowerCase() === endpoint.model.toLowerCase()
5650
+ );
5651
+ return { endpoint, online, availableModels, modelAvailable };
5652
+ }
5653
+ function isEndpointUsable(readiness) {
5654
+ return readiness.online && readiness.modelAvailable;
5655
+ }
5656
+ function providerChain(primary) {
5657
+ return [primary, ...primary.fallback ? [primary.fallback] : []];
5658
+ }
5659
+ async function checkProviderChain(primary) {
5660
+ let first;
5661
+ for (const endpoint of providerChain(primary)) {
5662
+ const readiness = await checkProviderEndpoint(endpoint);
5663
+ first ??= readiness;
5664
+ if (isEndpointUsable(readiness)) {
5665
+ return { primary: first, firstUsable: readiness };
5666
+ }
5667
+ }
5668
+ return { primary: first };
5669
+ }
5670
+ async function isVisionProviderModelExplicit(db, active) {
5671
+ if (await getSetting(db, "llm.vision.model")) return true;
5672
+ const providers = await readJsonSetting(db, "llm.providers");
5673
+ const roles = await readJsonSetting(db, "llm.roles");
5674
+ const binding = roles?.vision;
5675
+ if (!providers || !binding) return false;
5676
+ return [binding.primary, binding.fallback].some((id) => {
5677
+ if (!id) return false;
5678
+ const rec = providers[id];
5679
+ return rec?.model === active.model && (rec.url === void 0 || rec.url === active.url);
5680
+ });
5681
+ }
5682
+ async function checkVisionReadiness(db) {
5683
+ const cfg = await getProviderForRole(db, "vision");
5684
+ const chain = cfg.enabled ? await checkProviderChain(cfg) : void 0;
5685
+ const selected = chain?.firstUsable ?? chain?.primary;
5686
+ const active = selected?.endpoint ?? cfg;
5687
+ const online = selected?.online ?? false;
5688
+ const availableModels = selected?.availableModels ?? [];
5689
+ const modelAvailable = selected?.modelAvailable ?? false;
5690
+ const visionModelExplicit = await isVisionProviderModelExplicit(db, active);
5175
5691
  let warning;
5176
- if (enabled && online && modelAvailable && !visionModelExplicit) {
5177
- 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>`;
5692
+ if (cfg.enabled && online && modelAvailable && !visionModelExplicit) {
5693
+ const cloudRec = getCloudModelRecommendation(active.url);
5694
+ if (cloudRec && active.model === cloudRec) {
5695
+ } else {
5696
+ 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>`;
5697
+ }
5178
5698
  }
5179
5699
  return {
5180
- enabled,
5700
+ enabled: cfg.enabled,
5181
5701
  online,
5182
- url,
5183
- model,
5702
+ url: active.url,
5703
+ model: active.model,
5184
5704
  modelAvailable,
5185
5705
  availableModels,
5186
- usable: enabled && online && modelAvailable,
5706
+ usable: cfg.enabled && online && modelAvailable,
5187
5707
  visionModelExplicit,
5188
5708
  warning
5189
5709
  };
@@ -5208,16 +5728,16 @@ function spawnLocalRunner(url, model) {
5208
5728
  const { runner, port } = detectRunner(url, model);
5209
5729
  try {
5210
5730
  if (runner === "fastflowlm") {
5211
- const flmExe = existsSync12("C:\\Program Files\\flm\\flm.exe") ? "C:\\Program Files\\flm\\flm.exe" : "flm";
5731
+ const flmExe = existsSync14("C:\\Program Files\\flm\\flm.exe") ? "C:\\Program Files\\flm\\flm.exe" : "flm";
5212
5732
  if (!hasCommand("flm") && flmExe === "flm") return;
5213
- spawn(flmExe, ["serve", model, "--port", port], {
5733
+ spawn2(flmExe, ["serve", model, "--port", port], {
5214
5734
  detached: true,
5215
5735
  stdio: "ignore",
5216
5736
  windowsHide: true
5217
5737
  }).unref();
5218
5738
  } else if (runner === "ollama" || runner === "generic") {
5219
5739
  if (!hasCommand("ollama")) return;
5220
- spawn("ollama", ["serve"], {
5740
+ spawn2("ollama", ["serve"], {
5221
5741
  detached: true,
5222
5742
  stdio: "ignore",
5223
5743
  windowsHide: true
@@ -5229,7 +5749,7 @@ function spawnLocalRunner(url, model) {
5229
5749
  async function startLocalRunner(url, model, locale) {
5230
5750
  const { runner, port } = detectRunner(url, model);
5231
5751
  if (runner === "fastflowlm") {
5232
- const flmExe = existsSync12("C:\\Program Files\\flm\\flm.exe") ? "C:\\Program Files\\flm\\flm.exe" : "flm";
5752
+ const flmExe = existsSync14("C:\\Program Files\\flm\\flm.exe") ? "C:\\Program Files\\flm\\flm.exe" : "flm";
5233
5753
  if (!hasCommand("flm") && flmExe === "flm") {
5234
5754
  console.warn(
5235
5755
  "\x1B[31m\u2717 FastFlowLM is configured but could not be found on the system.\x1B[0m"
@@ -5242,7 +5762,7 @@ async function startLocalRunner(url, model, locale) {
5242
5762
  `\x1B[36mStarting FastFlowLM serve process: ${flmExe} ${args.join(" ")}\x1B[0m`
5243
5763
  );
5244
5764
  try {
5245
- spawn(flmExe, args, {
5765
+ spawn2(flmExe, args, {
5246
5766
  detached: true,
5247
5767
  stdio: "ignore",
5248
5768
  windowsHide: true
@@ -5263,7 +5783,7 @@ async function startLocalRunner(url, model, locale) {
5263
5783
  }
5264
5784
  console.log("\x1B[36mStarting Ollama serve process: ollama serve\x1B[0m");
5265
5785
  try {
5266
- spawn("ollama", ["serve"], {
5786
+ spawn2("ollama", ["serve"], {
5267
5787
  detached: true,
5268
5788
  stdio: "ignore",
5269
5789
  windowsHide: true
@@ -5286,7 +5806,7 @@ async function startLocalRunner(url, model, locale) {
5286
5806
  let attempts = 0;
5287
5807
  const dotsPerLine = 30;
5288
5808
  while (true) {
5289
- await new Promise((resolve5) => setTimeout(resolve5, 500));
5809
+ await new Promise((resolve6) => setTimeout(resolve6, 500));
5290
5810
  if (await isLlmOnline(url)) {
5291
5811
  if (attempts > 0) process.stdout.write("\n");
5292
5812
  return true;
@@ -5311,12 +5831,18 @@ async function startLocalRunner(url, model, locale) {
5311
5831
  }
5312
5832
  }
5313
5833
  async function ensureLocalLlmRunning(db) {
5314
- const cfg = await getLlmConfig(db);
5834
+ const cfg = await getProviderForRole(db, "recall");
5315
5835
  if (!cfg.enabled) {
5316
5836
  return { usable: false, reason: "disabled" };
5317
5837
  }
5838
+ if (cfg.apiFlavor !== "chat-completions") {
5839
+ console.warn(
5840
+ `\x1B[31m\u2717 Recall is configured for "${cfg.apiFlavor}", but active recall currently supports chat-completions providers only.\x1B[0m`
5841
+ );
5842
+ return { usable: false, reason: "unsupported-provider" };
5843
+ }
5318
5844
  const { url, model, apiKey, locale } = cfg;
5319
- const isLocal = url.includes("localhost") || url.includes("127.0.0.1");
5845
+ const isLocal = isLocalEndpoint(url);
5320
5846
  console.log(`Checking if local LLM server is online at ${url}...`);
5321
5847
  let online = await isLlmOnline(url);
5322
5848
  if (!online && isLocal) {
@@ -5350,8 +5876,9 @@ async function ensureLocalLlmRunning(db) {
5350
5876
  }
5351
5877
  async function ensureLlmReadyHeadless(db, opts = {}) {
5352
5878
  const timeoutMs = opts.timeoutMs ?? 25e3;
5353
- const { enabled, url, model, apiKey } = await getLlmConfig(db);
5354
- if (!enabled) {
5879
+ const cfg = await getProviderForRole(db, "recall");
5880
+ const { url, model, apiKey } = cfg;
5881
+ if (!cfg.enabled) {
5355
5882
  return {
5356
5883
  usable: false,
5357
5884
  reason: "disabled",
@@ -5360,7 +5887,16 @@ async function ensureLlmReadyHeadless(db, opts = {}) {
5360
5887
  availableModels: []
5361
5888
  };
5362
5889
  }
5363
- const isLocal = url.includes("localhost") || url.includes("127.0.0.1");
5890
+ if (cfg.apiFlavor !== "chat-completions") {
5891
+ return {
5892
+ usable: false,
5893
+ reason: "unsupported-provider",
5894
+ online: false,
5895
+ model,
5896
+ availableModels: []
5897
+ };
5898
+ }
5899
+ const isLocal = isLocalEndpoint(url);
5364
5900
  let online = await isLlmOnline(url);
5365
5901
  if (!online && isLocal) {
5366
5902
  spawnLocalRunner(url, model);
@@ -5425,8 +5961,8 @@ async function fetchWithInteractiveTimeout(url, options = {}) {
5425
5961
  const dotsPerLine = 30;
5426
5962
  while (true) {
5427
5963
  let timeoutId;
5428
- const timeoutPromise = new Promise((resolve5) => {
5429
- timeoutId = setTimeout(() => resolve5("timeout"), timeoutMs);
5964
+ const timeoutPromise = new Promise((resolve6) => {
5965
+ timeoutId = setTimeout(() => resolve6("timeout"), timeoutMs);
5430
5966
  });
5431
5967
  const dotsInterval = setInterval(() => {
5432
5968
  process.stdout.write(".");
@@ -5497,10 +6033,10 @@ async function ensureHighQualityQuestion(db, token) {
5497
6033
  }
5498
6034
 
5499
6035
  // src/cli/llm/vision.ts
5500
- import { readFileSync as readFileSync9 } from "fs";
5501
6036
  import { randomBytes } from "crypto";
5502
- import { tmpdir } from "os";
5503
- import { basename as basename2, join as join12 } from "path";
6037
+ import { readFileSync as readFileSync9 } from "fs";
6038
+ import { tmpdir as tmpdir2 } from "os";
6039
+ import { basename as basename3, join as join14 } from "path";
5504
6040
  var LANGUAGE_NAMES2 = {
5505
6041
  en: "English",
5506
6042
  de: "German",
@@ -5527,7 +6063,7 @@ var ACTION_TYPES2 = /* @__PURE__ */ new Set([
5527
6063
  "window-change"
5528
6064
  ]);
5529
6065
  async function observeUiSnapshotViaLLM(db, input8) {
5530
- const cfg = await getVisionConfig(db);
6066
+ const cfg = await getProviderForRole(db, "vision");
5531
6067
  if (!cfg.enabled) {
5532
6068
  throw new Error(
5533
6069
  "Vision observation is disabled in settings (llm.vision.enabled)"
@@ -5536,34 +6072,42 @@ async function observeUiSnapshotViaLLM(db, input8) {
5536
6072
  const imageUrls = [];
5537
6073
  const isVideo = /\.(mp4|mov|m4v|avi|mkv|webm)$/i.test(input8.imagePath);
5538
6074
  if (isVideo) {
5539
- const { mkdirSync: mkdirSync12, readdirSync: readdirSync3, rmSync: rmSync3 } = await import("fs");
5540
- const { execSync: execSync7 } = await import("child_process");
5541
- const tempDir = join12(tmpdir(), `zam-frames-${randomBytes(4).toString("hex")}`);
5542
- mkdirSync12(tempDir, { recursive: true });
6075
+ const { mkdirSync: mkdirSync13, readdirSync: readdirSync3, rmSync: rmSync3 } = await import("fs");
6076
+ const { execSync: execSync6 } = await import("child_process");
6077
+ const tempDir = join14(
6078
+ tmpdir2(),
6079
+ `zam-frames-${randomBytes(4).toString("hex")}`
6080
+ );
6081
+ mkdirSync13(tempDir, { recursive: true });
5543
6082
  try {
5544
- execSync7(
6083
+ execSync6(
5545
6084
  `ffmpeg -i "${input8.imagePath}" -vf "fps=1/3,scale=1280:-1" -vsync vfr "${tempDir}/frame_%03d.png"`,
5546
6085
  { stdio: "ignore" }
5547
6086
  );
5548
6087
  let files = readdirSync3(tempDir).filter((f) => f.endsWith(".png")).sort();
5549
6088
  if (files.length === 0) {
5550
- execSync7(
6089
+ execSync6(
5551
6090
  `ffmpeg -i "${input8.imagePath}" -vframes 1 "${tempDir}/frame_001.png"`,
5552
6091
  { stdio: "ignore" }
5553
6092
  );
5554
6093
  files = readdirSync3(tempDir).filter((f) => f.endsWith(".png")).sort();
5555
6094
  }
6095
+ const maxFrames = cfg.maxFrames ?? 100;
5556
6096
  let sampledFiles = files;
5557
- if (files.length > 12) {
5558
- const step = (files.length - 1) / 11;
5559
- sampledFiles = [];
5560
- for (let i = 0; i < 12; i++) {
5561
- const index = Math.round(i * step);
5562
- sampledFiles.push(files[index]);
6097
+ if (files.length > maxFrames) {
6098
+ if (maxFrames <= 1) {
6099
+ sampledFiles = [files[0]];
6100
+ } else {
6101
+ const step = (files.length - 1) / (maxFrames - 1);
6102
+ sampledFiles = [];
6103
+ for (let i = 0; i < maxFrames; i++) {
6104
+ const index = Math.round(i * step);
6105
+ sampledFiles.push(files[index]);
6106
+ }
5563
6107
  }
5564
6108
  }
5565
6109
  for (const file of sampledFiles) {
5566
- const bytes = readFileSync9(join12(tempDir, file));
6110
+ const bytes = readFileSync9(join14(tempDir, file));
5567
6111
  imageUrls.push(`data:image/png;base64,${bytes.toString("base64")}`);
5568
6112
  }
5569
6113
  } finally {
@@ -5581,42 +6125,92 @@ async function observeUiSnapshotViaLLM(db, input8) {
5581
6125
  if (imageUrls.length === 0) {
5582
6126
  throw new Error("No image data available for vision analysis");
5583
6127
  }
5584
- const model = input8.model ?? cfg.model;
5585
- const content = await requestVisionDraft({
5586
- url: cfg.url,
5587
- apiKey: cfg.apiKey || DEFAULT_LLM_API_KEY,
5588
- model,
5589
- locale: cfg.locale,
5590
- imageUrls,
5591
- input: input8
5592
- });
5593
- let draft;
5594
- try {
5595
- draft = extractDraft(content);
5596
- } catch {
5597
- return uncertainReport(
5598
- input8,
5599
- "Vision model returned output that could not be parsed as JSON."
5600
- );
6128
+ const endpoints = [
6129
+ {
6130
+ url: cfg.url,
6131
+ apiKey: cfg.apiKey || DEFAULT_LLM_API_KEY,
6132
+ model: input8.model ?? cfg.model,
6133
+ apiFlavor: cfg.apiFlavor
6134
+ }
6135
+ ];
6136
+ if (cfg.fallback) {
6137
+ endpoints.push({
6138
+ url: cfg.fallback.url,
6139
+ apiKey: cfg.fallback.apiKey || DEFAULT_LLM_API_KEY,
6140
+ model: cfg.fallback.model,
6141
+ apiFlavor: cfg.fallback.apiFlavor
6142
+ });
5601
6143
  }
5602
- const report = buildReport(input8, draft);
5603
- if (!isUiObservationReport(report)) {
6144
+ let lastRequestError;
6145
+ let sawUnparseableDraft = false;
6146
+ let sawInvalidDraft = false;
6147
+ for (const endpoint of endpoints) {
6148
+ let content;
6149
+ try {
6150
+ content = await requestVisionDraft({
6151
+ ...endpoint,
6152
+ locale: cfg.locale,
6153
+ imageUrls,
6154
+ input: input8
6155
+ });
6156
+ } catch (err) {
6157
+ lastRequestError = err;
6158
+ continue;
6159
+ }
6160
+ let draft;
6161
+ try {
6162
+ draft = extractDraft(content);
6163
+ } catch {
6164
+ sawUnparseableDraft = true;
6165
+ continue;
6166
+ }
6167
+ const report = buildReport(input8, draft);
6168
+ if (isUiObservationReport(report)) {
6169
+ return report;
6170
+ }
6171
+ sawInvalidDraft = true;
6172
+ }
6173
+ if (lastRequestError && !sawUnparseableDraft && !sawInvalidDraft) {
6174
+ throw lastRequestError;
6175
+ }
6176
+ if (sawInvalidDraft) {
5604
6177
  return uncertainReport(
5605
6178
  input8,
5606
6179
  "Vision model returned an invalid UI report draft."
5607
6180
  );
5608
6181
  }
5609
- return report;
6182
+ return uncertainReport(
6183
+ input8,
6184
+ "Vision model returned output that could not be parsed as JSON."
6185
+ );
5610
6186
  }
5611
- async function requestVisionDraft(args) {
5612
- const language = LANGUAGE_NAMES2[args.locale] ?? "English";
5613
- const schema = `{
6187
+ 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.";
6188
+ function visionSchema(language) {
6189
+ return `{
5614
6190
  "kind": "progress | step-completed | error | help-seeking | uncertain",
5615
6191
  "summary": "short factual UI summary in ${language}",
5616
6192
  "actions": [{"type": "click | shortcut | typing | scroll | window-change", "target": "optional UI target", "result": "optional visible result"}],
5617
6193
  "candidateTokens": [{"slug": "optional-kebab-case-skill-token", "confidence": 0.0, "rationale": "why this token may matter"}],
5618
6194
  "confidence": 0.0
5619
6195
  }`;
6196
+ }
6197
+ function visionUserText(args, language) {
6198
+ 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.";
6199
+ return `${intro}
6200
+ Application process: ${args.input.application.processName}
6201
+ Window title: ${args.input.application.windowTitle ?? "(unknown)"}
6202
+
6203
+ Return this JSON draft only:
6204
+ ${visionSchema(language)}`;
6205
+ }
6206
+ async function requestVisionDraft(args) {
6207
+ if (args.apiFlavor === "anthropic-messages") {
6208
+ return requestAnthropicVisionDraft(args);
6209
+ }
6210
+ return requestChatCompletionsVisionDraft(args);
6211
+ }
6212
+ async function requestChatCompletionsVisionDraft(args) {
6213
+ const language = LANGUAGE_NAMES2[args.locale] ?? "English";
5620
6214
  const res = await fetchWithInteractiveTimeout(
5621
6215
  `${args.url}/chat/completions`,
5622
6216
  {
@@ -5628,27 +6222,11 @@ async function requestVisionDraft(args) {
5628
6222
  body: JSON.stringify({
5629
6223
  model: args.model,
5630
6224
  messages: [
5631
- {
5632
- role: "system",
5633
- content: "You are ZAM's UI observer. Return only strict JSON. Do not include markdown, prose, or fields outside the requested schema."
5634
- },
6225
+ { role: "system", content: VISION_SYSTEM_PROMPT },
5635
6226
  {
5636
6227
  role: "user",
5637
6228
  content: [
5638
- {
5639
- type: "text",
5640
- text: args.imageUrls.length > 1 ? `Observe this sequence of Windows/macOS application snapshots showing a task performed over time.
5641
- Application process: ${args.input.application.processName}
5642
- Window title: ${args.input.application.windowTitle ?? "(unknown)"}
5643
-
5644
- Return this JSON draft only:
5645
- ${schema}` : `Observe this Windows/macOS application snapshot for a learning session.
5646
- Application process: ${args.input.application.processName}
5647
- Window title: ${args.input.application.windowTitle ?? "(unknown)"}
5648
-
5649
- Return this JSON draft only:
5650
- ${schema}`
5651
- },
6229
+ { type: "text", text: visionUserText(args, language) },
5652
6230
  ...args.imageUrls.map((url) => ({
5653
6231
  type: "image_url",
5654
6232
  image_url: { url }
@@ -5657,7 +6235,8 @@ ${schema}`
5657
6235
  }
5658
6236
  ],
5659
6237
  temperature: 0,
5660
- max_tokens: args.input.maxTokens ?? 450
6238
+ max_tokens: args.input.maxTokens ?? 450,
6239
+ ...args.url.includes("11434") || args.url.includes("localhost") || args.url.includes("127.0.0.1") ? { options: { num_ctx: 32768 } } : {}
5661
6240
  }),
5662
6241
  locale: args.locale,
5663
6242
  hardTimeoutMs: args.input.hardTimeoutMs ?? 18e4
@@ -5690,6 +6269,64 @@ ${schema}`
5690
6269
  }
5691
6270
  return content.trim();
5692
6271
  }
6272
+ function dataUrlToAnthropicImage(dataUrl) {
6273
+ const match = dataUrl.match(/^data:(image\/[a-zA-Z]+);base64,(.+)$/);
6274
+ if (!match) {
6275
+ throw new Error("Unsupported image data for Anthropic vision request");
6276
+ }
6277
+ return {
6278
+ type: "image",
6279
+ source: { type: "base64", media_type: match[1], data: match[2] }
6280
+ };
6281
+ }
6282
+ async function requestAnthropicVisionDraft(args) {
6283
+ const language = LANGUAGE_NAMES2[args.locale] ?? "English";
6284
+ const base = args.url.replace(/\/+$/, "").replace(/\/v1$/, "");
6285
+ const res = await fetchWithInteractiveTimeout(`${base}/v1/messages`, {
6286
+ method: "POST",
6287
+ headers: {
6288
+ "Content-Type": "application/json",
6289
+ "x-api-key": args.apiKey,
6290
+ "anthropic-version": "2023-06-01"
6291
+ },
6292
+ body: JSON.stringify({
6293
+ model: args.model,
6294
+ max_tokens: args.input.maxTokens ?? 450,
6295
+ system: VISION_SYSTEM_PROMPT,
6296
+ messages: [
6297
+ {
6298
+ role: "user",
6299
+ content: [
6300
+ { type: "text", text: visionUserText(args, language) },
6301
+ ...args.imageUrls.map(dataUrlToAnthropicImage)
6302
+ ]
6303
+ }
6304
+ ]
6305
+ }),
6306
+ locale: args.locale,
6307
+ hardTimeoutMs: args.input.hardTimeoutMs ?? 18e4
6308
+ });
6309
+ if (!res.ok) {
6310
+ const errorText = await res.text().catch(() => "");
6311
+ throw new Error(
6312
+ `Vision LLM request failed: ${res.statusText} (${res.status}) - ${errorText}`
6313
+ );
6314
+ }
6315
+ const data = await res.json();
6316
+ if (data.error !== void 0) {
6317
+ throw new Error(`Vision model failed: ${formatModelError(data.error)}`);
6318
+ }
6319
+ if (data.stop_reason === "refusal") {
6320
+ throw new Error("Vision model refused the request (safety classifier).");
6321
+ }
6322
+ const text = data.content?.find(
6323
+ (b) => b.type === "text" && typeof b.text === "string"
6324
+ )?.text;
6325
+ if (!text) {
6326
+ throw new Error("Empty response from vision model");
6327
+ }
6328
+ return text.trim();
6329
+ }
5693
6330
  function extractDraft(content) {
5694
6331
  const fenced = content.match(/```(?:json)?\s*([\s\S]*?)```/i);
5695
6332
  const candidate = (fenced?.[1] ?? content).trim();
@@ -5718,7 +6355,7 @@ function buildReport(input8, draft) {
5718
6355
  evidence: [
5719
6356
  {
5720
6357
  type: "keyframe",
5721
- ref: input8.evidenceRef ?? basename2(input8.imagePath),
6358
+ ref: input8.evidenceRef ?? basename3(input8.imagePath),
5722
6359
  redacted: input8.redacted ?? false
5723
6360
  }
5724
6361
  ],
@@ -5740,7 +6377,7 @@ function uncertainReport(input8, summary) {
5740
6377
  evidence: [
5741
6378
  {
5742
6379
  type: "keyframe",
5743
- ref: input8.evidenceRef ?? basename2(input8.imagePath),
6380
+ ref: input8.evidenceRef ?? basename3(input8.imagePath),
5744
6381
  redacted: input8.redacted ?? false
5745
6382
  }
5746
6383
  ],
@@ -5850,6 +6487,206 @@ function jsonOut(data) {
5850
6487
  console.log(JSON.stringify(data, null, 2));
5851
6488
  }
5852
6489
 
6490
+ // src/cli/commands/workspace.ts
6491
+ import { execFileSync as execFileSync3 } from "child_process";
6492
+ import { existsSync as existsSync15, mkdirSync as mkdirSync8, writeFileSync as writeFileSync7 } from "fs";
6493
+ import { homedir as homedir9 } from "os";
6494
+ import { join as join15 } from "path";
6495
+ import { confirm, input } from "@inquirer/prompts";
6496
+ import { Command as Command2 } from "commander";
6497
+ function runGit(cwd, args) {
6498
+ try {
6499
+ return execFileSync3("git", args, {
6500
+ cwd,
6501
+ stdio: "pipe",
6502
+ encoding: "utf8"
6503
+ }).trim();
6504
+ } catch (err) {
6505
+ throw new Error(`Git command failed: ${err.message}`);
6506
+ }
6507
+ }
6508
+ function ghRepoCreateArgs(repoName, repoVisibility) {
6509
+ return ["repo", "create", repoName, repoVisibility, "--source=.", "--push"];
6510
+ }
6511
+ function gitRemoteArgs(githubUrl, hasOrigin) {
6512
+ return hasOrigin ? ["remote", "set-url", "origin", githubUrl] : ["remote", "add", "origin", githubUrl];
6513
+ }
6514
+ var workspaceCommand = new Command2("workspace").description(
6515
+ "Manage your ZAM learning workspace"
6516
+ );
6517
+ workspaceCommand.command("publish").description("Publish your local workspace sandbox to GitHub").action(async () => {
6518
+ let db;
6519
+ let workspaceDir = "";
6520
+ try {
6521
+ db = await openDatabase();
6522
+ workspaceDir = await getSetting(db, "personal.workspace_dir") || "";
6523
+ await db.close();
6524
+ } catch {
6525
+ await db?.close();
6526
+ }
6527
+ if (!workspaceDir) {
6528
+ console.error(
6529
+ "\x1B[31m\u2717 No active workspace configured. Please run `zam init` first.\x1B[0m"
6530
+ );
6531
+ process.exit(1);
6532
+ }
6533
+ if (!existsSync15(workspaceDir)) {
6534
+ console.error(
6535
+ `\x1B[31m\u2717 Workspace directory does not exist: ${workspaceDir}\x1B[0m`
6536
+ );
6537
+ process.exit(1);
6538
+ }
6539
+ console.log(`
6540
+ Active workspace: \x1B[36m${workspaceDir}\x1B[0m`);
6541
+ if (!hasCommand("git")) {
6542
+ console.error(
6543
+ "\x1B[31m\u2717 Git command was not found on this system. Please install Git first.\x1B[0m"
6544
+ );
6545
+ process.exit(1);
6546
+ }
6547
+ const gitignorePath = join15(workspaceDir, ".gitignore");
6548
+ if (!existsSync15(gitignorePath)) {
6549
+ writeFileSync7(
6550
+ gitignorePath,
6551
+ "node_modules/\n.agent/\n.agents/\n.claude/\n.gemini/\n.goose/\n*.log\n",
6552
+ "utf8"
6553
+ );
6554
+ }
6555
+ const hasGitRepo = existsSync15(join15(workspaceDir, ".git"));
6556
+ if (!hasGitRepo) {
6557
+ console.log("Initializing local Git repository...");
6558
+ runGit(workspaceDir, ["init", "-b", "main"]);
6559
+ runGit(workspaceDir, ["add", "."]);
6560
+ runGit(workspaceDir, [
6561
+ "commit",
6562
+ "-m",
6563
+ "chore: initial workspace sandbox bootstrap"
6564
+ ]);
6565
+ console.log("\x1B[32m\u2713 Local Git repository initialized.\x1B[0m");
6566
+ } else {
6567
+ console.log("Git repository is already initialized.");
6568
+ }
6569
+ const repoName = await input({
6570
+ message: "Choose a name for your GitHub repository:",
6571
+ default: "zam-personal"
6572
+ });
6573
+ const isPrivate = await confirm({
6574
+ message: "Should the repository be private?",
6575
+ default: true
6576
+ });
6577
+ const repoVisibility = isPrivate ? "--private" : "--public";
6578
+ if (hasCommand("gh")) {
6579
+ console.log("GitHub CLI detected! Automating repository creation...");
6580
+ const proceedGh = await confirm({
6581
+ message: "Would you like ZAM to create the repository using the GitHub CLI?",
6582
+ default: true
6583
+ });
6584
+ if (proceedGh) {
6585
+ try {
6586
+ console.log(`Creating GitHub repository ${repoName}...`);
6587
+ execFileSync3("gh", ghRepoCreateArgs(repoName, repoVisibility), {
6588
+ cwd: workspaceDir,
6589
+ stdio: "inherit"
6590
+ });
6591
+ console.log(
6592
+ "\n\x1B[32m\u2713 Successfully published workspace to GitHub!\x1B[0m"
6593
+ );
6594
+ process.exit(0);
6595
+ } catch (err) {
6596
+ console.warn(
6597
+ `\x1B[33m\u26A0 GitHub CLI creation failed: ${err.message}\x1B[0m`
6598
+ );
6599
+ }
6600
+ }
6601
+ }
6602
+ console.log(
6603
+ "\n\x1B[1mPlease create the repository manually on GitHub:\x1B[0m"
6604
+ );
6605
+ console.log(" 1. Go to https://github.com/new");
6606
+ console.log(` 2. Name it exactly: \x1B[36m${repoName}\x1B[0m`);
6607
+ console.log(
6608
+ ` 3. Choose \x1B[36m${isPrivate ? "Private" : "Public"}\x1B[0m`
6609
+ );
6610
+ console.log(
6611
+ " 4. Do NOT initialize it with README, .gitignore, or license"
6612
+ );
6613
+ console.log(" 5. Click 'Create repository'\n");
6614
+ const githubUrl = await input({
6615
+ message: "Paste the repository Git URL (e.g. git@github.com:user/repo.git):"
6616
+ });
6617
+ if (githubUrl) {
6618
+ try {
6619
+ console.log("Linking remote repository and pushing...");
6620
+ let hasOrigin = false;
6621
+ try {
6622
+ runGit(workspaceDir, ["remote", "get-url", "origin"]);
6623
+ hasOrigin = true;
6624
+ } catch {
6625
+ }
6626
+ runGit(workspaceDir, gitRemoteArgs(githubUrl, hasOrigin));
6627
+ runGit(workspaceDir, ["push", "-u", "origin", "main"]);
6628
+ console.log(
6629
+ "\x1B[32m\u2713 Successfully linked and pushed to GitHub!\x1B[0m"
6630
+ );
6631
+ } catch (err) {
6632
+ console.error(
6633
+ `\x1B[31m\u2717 Push failed: ${err.message}\x1B[0m`
6634
+ );
6635
+ console.log(
6636
+ "You can push manually later using: git push -u origin main"
6637
+ );
6638
+ }
6639
+ }
6640
+ });
6641
+ async function backupDatabaseTo(db, targetDir) {
6642
+ const backupDir = join15(targetDir, "zam-backups");
6643
+ mkdirSync8(backupDir, { recursive: true });
6644
+ const stamp = (/* @__PURE__ */ new Date()).toISOString().replace(/[:.]/g, "-");
6645
+ const dest = join15(backupDir, `zam-${stamp}.db`);
6646
+ await db.exec(`VACUUM INTO '${dest.replace(/'/g, "''")}'`);
6647
+ return dest;
6648
+ }
6649
+ workspaceCommand.command("data-dir").description("Print the ZAM data directory (database, credentials, config)").option("--json", "Output as JSON").action((opts) => {
6650
+ const dir = join15(homedir9(), ".zam");
6651
+ console.log(opts.json ? JSON.stringify({ dataDir: dir }) : dir);
6652
+ });
6653
+ workspaceCommand.command("backup").description("Back up the local ZAM database into your workspace").option(
6654
+ "--dir <path>",
6655
+ "Target directory (default: workspace dir, else ~/Documents/zam)"
6656
+ ).option("--json", "Output as JSON").action(async (opts) => {
6657
+ const target = getDatabaseTargetInfo();
6658
+ if (target.kind !== "local") {
6659
+ 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.`;
6660
+ if (opts.json) {
6661
+ console.log(JSON.stringify({ ok: false, reason }));
6662
+ return;
6663
+ }
6664
+ console.error(`\x1B[33m\u26A0 ${reason}\x1B[0m`);
6665
+ process.exit(1);
6666
+ }
6667
+ let db;
6668
+ try {
6669
+ db = await openDatabase();
6670
+ const workspaceDir = opts.dir || await getSetting(db, "personal.workspace_dir") || join15(homedir9(), "Documents", "zam");
6671
+ const dest = await backupDatabaseTo(db, workspaceDir);
6672
+ if (opts.json) {
6673
+ console.log(JSON.stringify({ ok: true, path: dest }));
6674
+ } else {
6675
+ console.log(`\x1B[32m\u2713 Database backed up to ${dest}\x1B[0m`);
6676
+ }
6677
+ } catch (err) {
6678
+ const reason = err.message;
6679
+ if (opts.json) {
6680
+ console.log(JSON.stringify({ ok: false, reason }));
6681
+ } else {
6682
+ console.error(`\x1B[31m\u2717 Backup failed: ${reason}\x1B[0m`);
6683
+ }
6684
+ process.exit(1);
6685
+ } finally {
6686
+ await db?.close();
6687
+ }
6688
+ });
6689
+
5853
6690
  // src/cli/commands/bridge.ts
5854
6691
  var isServeMode = false;
5855
6692
  function jsonOut2(data) {
@@ -5906,7 +6743,7 @@ function parseTokenUpdates(opts) {
5906
6743
  }
5907
6744
  return updates;
5908
6745
  }
5909
- var bridgeCommand = new Command2("bridge").description(
6746
+ var bridgeCommand = new Command3("bridge").description(
5910
6747
  "Machine-readable JSON protocol for AI integration"
5911
6748
  );
5912
6749
  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) => {
@@ -5934,6 +6771,45 @@ bridgeCommand.command("check-due").description("Check due cards for a user (JSON
5934
6771
  });
5935
6772
  });
5936
6773
  });
6774
+ bridgeCommand.command("backup-db").description("Back up the local database into the workspace (JSON)").option(
6775
+ "--dir <path>",
6776
+ "Target directory (default: workspace dir, else ~/Documents/zam)"
6777
+ ).action(async (opts) => {
6778
+ const target = getDatabaseTargetInfo();
6779
+ if (target.kind !== "local") {
6780
+ jsonOut2({
6781
+ ok: false,
6782
+ reason: "remote",
6783
+ target: target.kind,
6784
+ location: target.location
6785
+ });
6786
+ return;
6787
+ }
6788
+ await withDb2(async (db) => {
6789
+ const workspaceDir = opts.dir || await getSetting(db, "personal.workspace_dir") || join16(homedir10(), "Documents", "zam");
6790
+ const path = await backupDatabaseTo(db, workspaceDir);
6791
+ jsonOut2({ ok: true, path });
6792
+ });
6793
+ });
6794
+ bridgeCommand.command("workspace-info").description("Report the workspace dir, its default, and the data dir (JSON)").action(async () => {
6795
+ await withDb2(async (db) => {
6796
+ const workspaceDir = await getSetting(db, "personal.workspace_dir") || null;
6797
+ jsonOut2({
6798
+ workspaceDir,
6799
+ defaultWorkspaceDir: join16(homedir10(), "Documents", "zam"),
6800
+ dataDir: join16(homedir10(), ".zam")
6801
+ });
6802
+ });
6803
+ });
6804
+ bridgeCommand.command("set-workspace-dir").description("Set the personal workspace directory (JSON)").requiredOption("--dir <path>", "Path to the workspace directory").action(async (opts) => {
6805
+ const raw = String(opts.dir ?? "").trim();
6806
+ if (!raw) jsonError("A non-empty --dir is required");
6807
+ const dir = resolve3(raw);
6808
+ await withDb2(async (db) => {
6809
+ await setSetting(db, "personal.workspace_dir", dir);
6810
+ jsonOut2({ ok: true, workspaceDir: dir });
6811
+ });
6812
+ });
5937
6813
  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) => {
5938
6814
  await withDb2(async (db) => {
5939
6815
  const userId = await resolveUser(opts, db, { json: true });
@@ -6292,7 +7168,7 @@ bridgeCommand.command("discover-skills").description(
6292
7168
  "20"
6293
7169
  ).action(async (opts) => {
6294
7170
  try {
6295
- const monitorDir = join13(homedir8(), ".zam", "monitor");
7171
+ const monitorDir = join16(homedir10(), ".zam", "monitor");
6296
7172
  let files;
6297
7173
  try {
6298
7174
  files = readdirSync2(monitorDir).filter((f) => f.endsWith(".jsonl"));
@@ -6305,7 +7181,7 @@ bridgeCommand.command("discover-skills").description(
6305
7181
  return;
6306
7182
  }
6307
7183
  const limit = Number(opts.limit);
6308
- const sorted = files.map((f) => ({ name: f, path: join13(monitorDir, f) })).sort((a, b) => b.name.localeCompare(a.name)).slice(0, limit);
7184
+ const sorted = files.map((f) => ({ name: f, path: join16(monitorDir, f) })).sort((a, b) => b.name.localeCompare(a.name)).slice(0, limit);
6309
7185
  const sessionCommands = /* @__PURE__ */ new Map();
6310
7186
  for (const file of sorted) {
6311
7187
  const sessionId = file.name.replace(".jsonl", "");
@@ -6417,7 +7293,7 @@ bridgeCommand.command("observe-ui-snapshot").description(
6417
7293
  });
6418
7294
  function resolveWindowsPowerShell() {
6419
7295
  try {
6420
- execFileSync2("where.exe", ["pwsh.exe"], { stdio: "ignore" });
7296
+ execFileSync4("where.exe", ["pwsh.exe"], { stdio: "ignore" });
6421
7297
  return "pwsh";
6422
7298
  } catch {
6423
7299
  return "powershell";
@@ -6432,7 +7308,7 @@ function captureScreenshot(outputPath, hwnd, processName) {
6432
7308
  throw new Error(`Invalid process name format: ${processName}`);
6433
7309
  }
6434
7310
  if (platform === "win32") {
6435
- const stdout = execFileSync2(
7311
+ const stdout = execFileSync4(
6436
7312
  resolveWindowsPowerShell(),
6437
7313
  [
6438
7314
  "-NoProfile",
@@ -6745,13 +7621,13 @@ Write-CaptureResult "fullscreen" $hwndVal $matchedBy
6745
7621
  } else if (platform === "darwin") {
6746
7622
  if (hwnd) {
6747
7623
  const parsedHwnd = hwnd.startsWith("0x") ? parseInt(hwnd, 16) : parseInt(hwnd, 10);
6748
- execFileSync2("screencapture", ["-l", String(parsedHwnd), outputPath], {
7624
+ execFileSync4("screencapture", ["-l", String(parsedHwnd), outputPath], {
6749
7625
  stdio: "pipe"
6750
7626
  });
6751
7627
  return { method: "screencapture-window", target: null };
6752
7628
  } else if (processName) {
6753
7629
  try {
6754
- const windowId = execFileSync2(
7630
+ const windowId = execFileSync4(
6755
7631
  "osascript",
6756
7632
  [
6757
7633
  "-e",
@@ -6760,17 +7636,17 @@ Write-CaptureResult "fullscreen" $hwndVal $matchedBy
6760
7636
  { encoding: "utf8", stdio: ["pipe", "pipe", "ignore"] }
6761
7637
  ).trim();
6762
7638
  if (windowId && /^\d+$/.test(windowId)) {
6763
- execFileSync2("screencapture", ["-l", windowId, outputPath], {
7639
+ execFileSync4("screencapture", ["-l", windowId, outputPath], {
6764
7640
  stdio: "pipe"
6765
7641
  });
6766
7642
  return { method: "screencapture-window", target: null };
6767
7643
  }
6768
7644
  } catch {
6769
7645
  }
6770
- execFileSync2("screencapture", ["-x", outputPath], { stdio: "pipe" });
7646
+ execFileSync4("screencapture", ["-x", outputPath], { stdio: "pipe" });
6771
7647
  return { method: "screencapture-full", target: null };
6772
7648
  } else {
6773
- execFileSync2("screencapture", ["-x", outputPath], { stdio: "pipe" });
7649
+ execFileSync4("screencapture", ["-x", outputPath], { stdio: "pipe" });
6774
7650
  return { method: "screencapture-full", target: null };
6775
7651
  }
6776
7652
  } else {
@@ -6807,7 +7683,7 @@ bridgeCommand.command("capture-ui").description("Capture a screenshot for agent-
6807
7683
  return;
6808
7684
  }
6809
7685
  }
6810
- const outputPath = opts.image ?? opts.output ?? join13(tmpdir2(), `zam-capture-${randomBytes2(4).toString("hex")}.png`);
7686
+ const outputPath = opts.image ?? opts.output ?? join16(tmpdir3(), `zam-capture-${randomBytes2(4).toString("hex")}.png`);
6811
7687
  const captureResult = isProvided ? { method: "provided", target: null } : captureScreenshot(outputPath, opts.hwnd, opts.processName);
6812
7688
  if (!isProvided) {
6813
7689
  const post = decidePostCapture(policy, {
@@ -6851,21 +7727,22 @@ bridgeCommand.command("capture-ui").description("Capture a screenshot for agent-
6851
7727
  });
6852
7728
  });
6853
7729
  });
6854
- 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) => {
7730
+ 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) => {
6855
7731
  const platform = process.platform;
6856
- if (platform !== "darwin") {
7732
+ if (platform !== "darwin" && platform !== "win32") {
6857
7733
  jsonOut2({
6858
7734
  sessionId: opts.session,
6859
7735
  started: false,
6860
- error: "Screen recording is only supported on macOS (darwin)"
7736
+ error: "Screen recording is only supported on macOS (darwin) and Windows (win32)"
6861
7737
  });
6862
7738
  return;
6863
7739
  }
6864
7740
  const sessionId = opts.session;
6865
- const statePath = join13(tmpdir2(), `zam-recording-${sessionId}.json`);
6866
- const outputPath = opts.output ?? join13(tmpdir2(), `zam-recording-${sessionId}.mov`);
6867
- const { existsSync: existsSync22, writeFileSync: writeFileSync12, openSync, closeSync } = await import("fs");
6868
- if (existsSync22(statePath)) {
7741
+ const statePath = join16(tmpdir3(), `zam-recording-${sessionId}.json`);
7742
+ const defaultExt = platform === "win32" ? ".mkv" : ".mov";
7743
+ const outputPath = opts.output ?? join16(tmpdir3(), `zam-recording-${sessionId}${defaultExt}`);
7744
+ const { existsSync: existsSync24, writeFileSync: writeFileSync12, openSync, closeSync } = await import("fs");
7745
+ if (existsSync24(statePath)) {
6869
7746
  jsonOut2({
6870
7747
  sessionId,
6871
7748
  started: false,
@@ -6873,7 +7750,7 @@ bridgeCommand.command("start-recording").description("Start screen recording in
6873
7750
  });
6874
7751
  return;
6875
7752
  }
6876
- const logPath = join13(tmpdir2(), `zam-recording-${sessionId}.log`);
7753
+ const logPath = join16(tmpdir3(), `zam-recording-${sessionId}.log`);
6877
7754
  let logFd;
6878
7755
  try {
6879
7756
  logFd = openSync(logPath, "w");
@@ -6885,26 +7762,34 @@ bridgeCommand.command("start-recording").description("Start screen recording in
6885
7762
  });
6886
7763
  return;
6887
7764
  }
6888
- const { spawn: spawn3 } = await import("child_process");
6889
- const child = spawn3(
6890
- "ffmpeg",
6891
- [
6892
- "-y",
6893
- "-f",
6894
- "avfoundation",
6895
- "-r",
6896
- "5",
6897
- "-i",
6898
- "0",
6899
- "-pix_fmt",
6900
- "yuv420p",
6901
- outputPath
6902
- ],
6903
- {
6904
- detached: true,
6905
- stdio: ["pipe", logFd, logFd]
6906
- }
6907
- );
7765
+ const { spawn: spawn4 } = await import("child_process");
7766
+ const ffmpegArgs = platform === "darwin" ? [
7767
+ "-y",
7768
+ "-f",
7769
+ "avfoundation",
7770
+ "-r",
7771
+ "5",
7772
+ "-i",
7773
+ "0",
7774
+ "-pix_fmt",
7775
+ "yuv420p",
7776
+ outputPath
7777
+ ] : [
7778
+ "-y",
7779
+ "-f",
7780
+ "gdigrab",
7781
+ "-framerate",
7782
+ "5",
7783
+ "-i",
7784
+ "desktop",
7785
+ "-pix_fmt",
7786
+ "yuv420p",
7787
+ outputPath
7788
+ ];
7789
+ const child = spawn4("ffmpeg", ffmpegArgs, {
7790
+ detached: true,
7791
+ stdio: ["pipe", logFd, logFd]
7792
+ });
6908
7793
  try {
6909
7794
  closeSync(logFd);
6910
7795
  } catch {
@@ -6935,21 +7820,21 @@ bridgeCommand.command("start-recording").description("Start screen recording in
6935
7820
  }
6936
7821
  });
6937
7822
  bridgeCommand.command("stop-recording").description(
6938
- "Stop active screen recording and apply idle-frame compression (macOS only) (JSON)"
7823
+ "Stop active screen recording and apply idle-frame compression (JSON)"
6939
7824
  ).requiredOption("--session <id>", "ZAM session ID").action(async (opts) => {
6940
7825
  const platform = process.platform;
6941
- if (platform !== "darwin") {
7826
+ if (platform !== "darwin" && platform !== "win32") {
6942
7827
  jsonOut2({
6943
7828
  sessionId: opts.session,
6944
7829
  stopped: false,
6945
- error: "Screen recording is only supported on macOS (darwin)"
7830
+ error: "Screen recording is only supported on macOS (darwin) and Windows (win32)"
6946
7831
  });
6947
7832
  return;
6948
7833
  }
6949
7834
  const sessionId = opts.session;
6950
- const statePath = join13(tmpdir2(), `zam-recording-${sessionId}.json`);
6951
- const { existsSync: existsSync22, readFileSync: readFileSync15, rmSync: rmSync3 } = await import("fs");
6952
- if (!existsSync22(statePath)) {
7835
+ const statePath = join16(tmpdir3(), `zam-recording-${sessionId}.json`);
7836
+ const { existsSync: existsSync24, readFileSync: readFileSync15, rmSync: rmSync3 } = await import("fs");
7837
+ if (!existsSync24(statePath)) {
6953
7838
  jsonOut2({
6954
7839
  sessionId,
6955
7840
  stopped: false,
@@ -6961,32 +7846,32 @@ bridgeCommand.command("stop-recording").description(
6961
7846
  const { pid, outputPath } = state;
6962
7847
  try {
6963
7848
  process.kill(pid, "SIGINT");
6964
- } catch (e) {
7849
+ } catch (_e) {
6965
7850
  }
6966
7851
  const isProcessRunning = (pId) => {
6967
7852
  try {
6968
7853
  process.kill(pId, 0);
6969
7854
  return true;
6970
- } catch (e) {
7855
+ } catch (_e) {
6971
7856
  return false;
6972
7857
  }
6973
7858
  };
6974
7859
  let attempts = 0;
6975
7860
  while (isProcessRunning(pid) && attempts < 20) {
6976
- await new Promise((resolve5) => setTimeout(resolve5, 250));
7861
+ await new Promise((resolve6) => setTimeout(resolve6, 250));
6977
7862
  attempts++;
6978
7863
  }
6979
7864
  if (isProcessRunning(pid)) {
6980
7865
  try {
6981
7866
  process.kill(pid, "SIGKILL");
6982
- } catch (e) {
7867
+ } catch (_e) {
6983
7868
  }
6984
7869
  }
6985
7870
  try {
6986
7871
  rmSync3(statePath, { force: true });
6987
7872
  } catch {
6988
7873
  }
6989
- if (!existsSync22(outputPath)) {
7874
+ if (!existsSync24(outputPath)) {
6990
7875
  jsonOut2({
6991
7876
  sessionId,
6992
7877
  stopped: false,
@@ -6995,9 +7880,9 @@ bridgeCommand.command("stop-recording").description(
6995
7880
  return;
6996
7881
  }
6997
7882
  const decimatedPath = outputPath.replace(/\.[^.]+$/, "-decimated.mp4");
6998
- const { execSync: execSync7 } = await import("child_process");
7883
+ const { execSync: execSync6 } = await import("child_process");
6999
7884
  try {
7000
- execSync7(
7885
+ execSync6(
7001
7886
  `ffmpeg -y -i "${outputPath}" -vf "mpdecimate,setpts=N/FRAME_RATE/TB" -an -pix_fmt yuv420p "${decimatedPath}"`,
7002
7887
  { stdio: "ignore" }
7003
7888
  );
@@ -7050,11 +7935,13 @@ bridgeCommand.command("sync-observer-policy").description(
7050
7935
  });
7051
7936
  bridgeCommand.command("check-llm").description("Check if LLM is enabled and online (JSON)").action(async () => {
7052
7937
  await withDb2(async (db) => {
7053
- const { enabled, url, model, apiKey } = await getLlmConfig(db);
7938
+ const provider = await getProviderForRole(db, "recall");
7939
+ const { enabled, url, model, apiKey } = provider;
7940
+ const unsupportedProvider = provider.apiFlavor !== "chat-completions";
7054
7941
  let online = false;
7055
7942
  let availableModels = [];
7056
7943
  let modelAvailable = false;
7057
- if (enabled) {
7944
+ if (enabled && !unsupportedProvider) {
7058
7945
  online = await isLlmOnline(url);
7059
7946
  if (online) {
7060
7947
  availableModels = await getAvailableModels(url, apiKey);
@@ -7069,7 +7956,9 @@ bridgeCommand.command("check-llm").description("Check if LLM is enabled and onli
7069
7956
  url,
7070
7957
  model,
7071
7958
  modelAvailable,
7072
- availableModels
7959
+ availableModels,
7960
+ apiFlavor: provider.apiFlavor,
7961
+ unsupportedProvider
7073
7962
  });
7074
7963
  });
7075
7964
  });
@@ -7385,8 +8274,8 @@ bridgeCommand.command("serve").description("Start the persistent JSON-RPC stdin/
7385
8274
  });
7386
8275
 
7387
8276
  // src/cli/commands/card.ts
7388
- import { Command as Command3 } from "commander";
7389
- var cardCommand = new Command3("card").description(
8277
+ import { Command as Command4 } from "commander";
8278
+ var cardCommand = new Command4("card").description(
7390
8279
  "Manage spaced-repetition cards"
7391
8280
  );
7392
8281
  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) => {
@@ -7584,9 +8473,9 @@ cardCommand.command("delete").description("Delete one user's card for a token").
7584
8473
  });
7585
8474
 
7586
8475
  // src/cli/commands/connector.ts
7587
- import { input, password } from "@inquirer/prompts";
7588
- import { Command as Command4 } from "commander";
7589
- var connectorCommand = new Command4("connector").description(
8476
+ import { input as input2, password } from "@inquirer/prompts";
8477
+ import { Command as Command5 } from "commander";
8478
+ var connectorCommand = new Command5("connector").description(
7590
8479
  "Manage external service connectors"
7591
8480
  );
7592
8481
  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(
@@ -7605,10 +8494,10 @@ connectorCommand.command("setup").description("Configure a connector").argument(
7605
8494
  process.exit(1);
7606
8495
  }
7607
8496
  try {
7608
- const orgUrl = await input({
8497
+ const orgUrl = await input2({
7609
8498
  message: "Organization URL (e.g. https://dev.azure.com/myorg):"
7610
8499
  });
7611
- const project = await input({
8500
+ const project = await input2({
7612
8501
  message: "Project name:"
7613
8502
  });
7614
8503
  const pat = await password({
@@ -7699,7 +8588,7 @@ connectorCommand.command("sync").description("Verify the Turso cloud database co
7699
8588
  async function setupTurso(urlArg, tokenArg, mode) {
7700
8589
  let db;
7701
8590
  try {
7702
- const url = urlArg ?? await input({
8591
+ const url = urlArg ?? await input2({
7703
8592
  message: "Turso database URL (e.g. libsql://my-db-user.turso.io):"
7704
8593
  });
7705
8594
  const token = tokenArg ?? await password({
@@ -7728,27 +8617,27 @@ async function setupTurso(urlArg, tokenArg, mode) {
7728
8617
  }
7729
8618
 
7730
8619
  // src/cli/commands/git-sync.ts
7731
- import { execSync as execSync4 } from "child_process";
7732
- import { chmodSync as chmodSync2, existsSync as existsSync13, writeFileSync as writeFileSync6 } from "fs";
7733
- import { join as join14 } from "path";
7734
- import { Command as Command5 } from "commander";
8620
+ import { execSync as execSync5 } from "child_process";
8621
+ import { chmodSync as chmodSync2, existsSync as existsSync16, writeFileSync as writeFileSync8 } from "fs";
8622
+ import { join as join17 } from "path";
8623
+ import { Command as Command6 } from "commander";
7735
8624
  function installHook2() {
7736
- const gitDir = join14(process.cwd(), ".git");
7737
- if (!existsSync13(gitDir)) {
8625
+ const gitDir = join17(process.cwd(), ".git");
8626
+ if (!existsSync16(gitDir)) {
7738
8627
  console.error(
7739
8628
  "Error: Current directory is not the root of a Git repository."
7740
8629
  );
7741
8630
  process.exit(1);
7742
8631
  }
7743
- const hooksDir = join14(gitDir, "hooks");
7744
- const hookPath = join14(hooksDir, "post-commit");
8632
+ const hooksDir = join17(gitDir, "hooks");
8633
+ const hookPath = join17(hooksDir, "post-commit");
7745
8634
  const hookContent = `#!/bin/sh
7746
8635
  # ZAM Spaced Repetition Auto-Stale Hook
7747
8636
  # Triggered automatically on git commits to decay modified concept cards.
7748
8637
  zam git-sync --commit HEAD --quiet
7749
8638
  `;
7750
8639
  try {
7751
- writeFileSync6(hookPath, hookContent, { encoding: "utf-8", flag: "w" });
8640
+ writeFileSync8(hookPath, hookContent, { encoding: "utf-8", flag: "w" });
7752
8641
  try {
7753
8642
  chmodSync2(hookPath, "755");
7754
8643
  } catch (_e) {
@@ -7761,7 +8650,7 @@ zam git-sync --commit HEAD --quiet
7761
8650
  process.exit(1);
7762
8651
  }
7763
8652
  }
7764
- 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) => {
8653
+ var gitSyncCommand = new Command6("git-sync").description("Sync learning cards with recent Git file modifications").option("--commit <hash>", "Git commit hash to check", "HEAD").option("--user <id>", "User ID (default: whoami)").option("--install", "Install git post-commit hook in current repo").option("--quiet", "Suppress verbose output").action(async (opts) => {
7765
8654
  if (opts.install) {
7766
8655
  installHook2();
7767
8656
  return;
@@ -7770,7 +8659,7 @@ var gitSyncCommand = new Command5("git-sync").description("Sync learning cards w
7770
8659
  const userId = await resolveUser(opts, db);
7771
8660
  let changedFiles = [];
7772
8661
  try {
7773
- const output = execSync4(
8662
+ const output = execSync5(
7774
8663
  `git diff-tree --no-commit-id --name-only -r ${opts.commit}`,
7775
8664
  {
7776
8665
  encoding: "utf-8",
@@ -7850,10 +8739,10 @@ ZAM Auto-Stale Complete: Scanned ${changedFiles.length} file(s).`
7850
8739
  });
7851
8740
 
7852
8741
  // src/cli/commands/goal.ts
7853
- import { existsSync as existsSync14, mkdirSync as mkdirSync8 } from "fs";
7854
- import { resolve as resolve3 } from "path";
7855
- import { input as input2 } from "@inquirer/prompts";
7856
- import { Command as Command6 } from "commander";
8742
+ import { existsSync as existsSync17, mkdirSync as mkdirSync9 } from "fs";
8743
+ import { resolve as resolve4 } from "path";
8744
+ import { input as input3 } from "@inquirer/prompts";
8745
+ import { Command as Command7 } from "commander";
7857
8746
  async function resolveGoalsDir() {
7858
8747
  let goalsDir;
7859
8748
  let db;
@@ -7864,9 +8753,9 @@ async function resolveGoalsDir() {
7864
8753
  } finally {
7865
8754
  await db?.close();
7866
8755
  }
7867
- return goalsDir ? resolve3(goalsDir) : resolve3("goals");
8756
+ return goalsDir ? resolve4(goalsDir) : resolve4("goals");
7868
8757
  }
7869
- var goalCommand = new Command6("goal").description(
8758
+ var goalCommand = new Command7("goal").description(
7870
8759
  "Manage learning goals (markdown files)"
7871
8760
  );
7872
8761
  goalCommand.command("list").description("List all goals").option(
@@ -7874,7 +8763,7 @@ goalCommand.command("list").description("List all goals").option(
7874
8763
  "Filter by status (active, completed, paused, abandoned)"
7875
8764
  ).option("--tree", "Show goals as a tree with parent/child relationships").option("--json", "Output as JSON").action(async (opts) => {
7876
8765
  const goalsDir = await resolveGoalsDir();
7877
- if (!existsSync14(goalsDir)) {
8766
+ if (!existsSync17(goalsDir)) {
7878
8767
  console.error(`Goals directory not found: ${goalsDir}`);
7879
8768
  console.error(
7880
8769
  "Set it with: zam settings set personal.goals_dir /path/to/goals"
@@ -7975,8 +8864,8 @@ ${"\u2500".repeat(50)}`);
7975
8864
  });
7976
8865
  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) => {
7977
8866
  const goalsDir = await resolveGoalsDir();
7978
- if (!existsSync14(goalsDir)) {
7979
- mkdirSync8(goalsDir, { recursive: true });
8867
+ if (!existsSync17(goalsDir)) {
8868
+ mkdirSync9(goalsDir, { recursive: true });
7980
8869
  }
7981
8870
  let slug = opts.slug;
7982
8871
  let title = opts.title;
@@ -7985,11 +8874,11 @@ goalCommand.command("create").description("Create a new goal").option("--slug <s
7985
8874
  if (!slug || !title) {
7986
8875
  try {
7987
8876
  if (!title) {
7988
- title = await input2({ message: "Goal title:" });
8877
+ title = await input3({ message: "Goal title:" });
7989
8878
  }
7990
8879
  if (!slug) {
7991
8880
  const suggested = title.toLowerCase().replace(/[^a-z0-9]+/g, "-").replace(/^-|-$/g, "");
7992
- slug = await input2({
8881
+ slug = await input3({
7993
8882
  message: "Goal slug (filename):",
7994
8883
  default: suggested
7995
8884
  });
@@ -8035,22 +8924,22 @@ goalCommand.command("status <slug> <status>").description("Update a goal's statu
8035
8924
  });
8036
8925
 
8037
8926
  // src/cli/commands/init.ts
8038
- import { existsSync as existsSync15, mkdirSync as mkdirSync9, writeFileSync as writeFileSync7 } from "fs";
8039
- import { homedir as homedir9 } from "os";
8040
- import { join as join15 } from "path";
8041
- import { confirm, input as input3 } from "@inquirer/prompts";
8042
- import { Command as Command7 } from "commander";
8043
- var HOME2 = homedir9();
8927
+ import { existsSync as existsSync18, mkdirSync as mkdirSync10, writeFileSync as writeFileSync9 } from "fs";
8928
+ import { homedir as homedir11 } from "os";
8929
+ import { join as join18 } from "path";
8930
+ import { confirm as confirm2, input as input4 } from "@inquirer/prompts";
8931
+ import { Command as Command8 } from "commander";
8932
+ var HOME2 = homedir11();
8044
8933
  function printLine(char = "\u2550", len = 60, color = "\x1B[36m") {
8045
8934
  console.log(`${color}${char.repeat(len)}\x1B[0m`);
8046
8935
  }
8047
8936
  function bootstrapSandboxWorkspace(workspaceDir) {
8048
- mkdirSync9(join15(workspaceDir, "beliefs"), { recursive: true });
8049
- mkdirSync9(join15(workspaceDir, "goals"), { recursive: true });
8050
- mkdirSync9(join15(workspaceDir, "skills"), { recursive: true });
8051
- const worldviewFile = join15(workspaceDir, "beliefs", "worldview.md");
8052
- if (!existsSync15(worldviewFile)) {
8053
- writeFileSync7(
8937
+ mkdirSync10(join18(workspaceDir, "beliefs"), { recursive: true });
8938
+ mkdirSync10(join18(workspaceDir, "goals"), { recursive: true });
8939
+ mkdirSync10(join18(workspaceDir, "skills"), { recursive: true });
8940
+ const worldviewFile = join18(workspaceDir, "beliefs", "worldview.md");
8941
+ if (!existsSync18(worldviewFile)) {
8942
+ writeFileSync9(
8054
8943
  worldviewFile,
8055
8944
  `# Personal Worldview
8056
8945
 
@@ -8062,9 +8951,9 @@ Here, I declare the core concepts and principles I want to master.
8062
8951
  "utf8"
8063
8952
  );
8064
8953
  }
8065
- const goalsFile = join15(workspaceDir, "goals", "goals.md");
8066
- if (!existsSync15(goalsFile)) {
8067
- writeFileSync7(
8954
+ const goalsFile = join18(workspaceDir, "goals", "goals.md");
8955
+ if (!existsSync18(goalsFile)) {
8956
+ writeFileSync9(
8068
8957
  goalsFile,
8069
8958
  `# Personal Goals
8070
8959
 
@@ -8076,7 +8965,7 @@ Here, I declare the core concepts and principles I want to master.
8076
8965
  );
8077
8966
  }
8078
8967
  }
8079
- var initCommand = new Command7("init").description("Launch the guided interactive onboarding wizard").action(async () => {
8968
+ var initCommand = new Command8("init").description("Launch the guided interactive onboarding wizard").action(async () => {
8080
8969
  printLine();
8081
8970
  console.log(
8082
8971
  "\x1B[1m\x1B[32m ZAM \u2014 The Symbiotic Learning Agent Onboarding\x1B[0m"
@@ -8086,8 +8975,8 @@ var initCommand = new Command7("init").description("Launch the guided interactiv
8086
8975
  );
8087
8976
  printLine();
8088
8977
  console.log("\n\x1B[1m[1/5] Setting up Local Workspace Sandbox\x1B[0m");
8089
- const defaultWorkspace = join15(HOME2, "Documents", "zam");
8090
- const workspacePath = await input3({
8978
+ const defaultWorkspace = join18(HOME2, "Documents", "zam");
8979
+ const workspacePath = await input4({
8091
8980
  message: "Choose your ZAM workspace directory:",
8092
8981
  default: defaultWorkspace
8093
8982
  });
@@ -8123,7 +9012,7 @@ var initCommand = new Command7("init").description("Launch the guided interactiv
8123
9012
  \x1B[1mRecommendation:\x1B[0m ZAM suggests installing \x1B[32m${runnerLabel}\x1B[0m with \x1B[36m${profile.recommendedModel}\x1B[0m.`
8124
9013
  );
8125
9014
  console.log("\n\x1B[1m[3/5] Setting up Local LLM Runner\x1B[0m");
8126
- const proceedInstall = await confirm({
9015
+ const proceedInstall = await confirm2({
8127
9016
  message: `Would you like ZAM to install and configure ${runnerLabel} automatically?`,
8128
9017
  default: true
8129
9018
  });
@@ -8191,7 +9080,7 @@ var initCommand = new Command7("init").description("Launch the guided interactiv
8191
9080
  console.log(
8192
9081
  "\n\x1B[1m[5/5] Wiring Developer Agents & Terminal Helpers\x1B[0m"
8193
9082
  );
8194
- const proceedHooks = await confirm({
9083
+ const proceedHooks = await confirm2({
8195
9084
  message: "Distribute ZAM skills and install optional monitored-session shell helpers?",
8196
9085
  default: true
8197
9086
  });
@@ -8242,8 +9131,8 @@ var initCommand = new Command7("init").description("Launch the guided interactiv
8242
9131
  });
8243
9132
 
8244
9133
  // src/cli/commands/learn.ts
8245
- import { input as input5 } from "@inquirer/prompts";
8246
- import { Command as Command8 } from "commander";
9134
+ import { input as input6 } from "@inquirer/prompts";
9135
+ import { Command as Command9 } from "commander";
8247
9136
 
8248
9137
  // src/cli/learn-format.ts
8249
9138
  var BLOOM_VERBS3 = {
@@ -8292,7 +9181,7 @@ function formatReveal(input8) {
8292
9181
  }
8293
9182
 
8294
9183
  // src/cli/review-actions.ts
8295
- import { confirm as confirm2, input as input4, select } from "@inquirer/prompts";
9184
+ import { confirm as confirm3, input as input5, select } from "@inquirer/prompts";
8296
9185
  async function runInteractiveReviewAction(inputData) {
8297
9186
  let currentItem = { ...inputData.item };
8298
9187
  while (true) {
@@ -8407,7 +9296,7 @@ async function runInteractiveReviewAction(inputData) {
8407
9296
  continue;
8408
9297
  }
8409
9298
  if (choice === "deprecate-token") {
8410
- const approved = await confirm2({
9299
+ const approved = await confirm3({
8411
9300
  message: `Deprecate ${currentItem.slug} so it stops appearing in review queues?`,
8412
9301
  default: false
8413
9302
  });
@@ -8438,7 +9327,7 @@ async function runInteractiveReviewAction(inputData) {
8438
9327
  console.log(` Session steps: ${impact.session_steps}`);
8439
9328
  console.log(` Sessions touched: ${impact.sessions_touched}`);
8440
9329
  console.log(` Agent skills updated: ${impact.agent_skills}`);
8441
- const approved = await confirm2({
9330
+ const approved = await confirm3({
8442
9331
  message: "Permanently delete this token and its dependent learning data?",
8443
9332
  default: false
8444
9333
  });
@@ -8463,7 +9352,7 @@ async function runInteractiveReviewAction(inputData) {
8463
9352
  );
8464
9353
  console.log(`Delete your card for ${currentItem.slug}?`);
8465
9354
  console.log(` Review logs removed: ${impact.review_logs}`);
8466
- const approved = await confirm2({
9355
+ const approved = await confirm3({
8467
9356
  message: "Delete only your card for this token?",
8468
9357
  default: false
8469
9358
  });
@@ -8501,21 +9390,21 @@ async function promptTokenEdit(token) {
8501
9390
  }
8502
9391
  switch (field) {
8503
9392
  case "concept": {
8504
- const concept = await input4({
9393
+ const concept = await input5({
8505
9394
  message: "Concept:",
8506
9395
  default: token.concept
8507
9396
  });
8508
9397
  return concept === token.concept ? null : { concept };
8509
9398
  }
8510
9399
  case "domain": {
8511
- const domain = await input4({
9400
+ const domain = await input5({
8512
9401
  message: "Domain (blank allowed):",
8513
9402
  default: token.domain
8514
9403
  });
8515
9404
  return domain === token.domain ? null : { domain };
8516
9405
  }
8517
9406
  case "context": {
8518
- const context = await input4({
9407
+ const context = await input5({
8519
9408
  message: "Context (blank allowed):",
8520
9409
  default: token.context
8521
9410
  });
@@ -8556,14 +9445,14 @@ async function promptTokenEdit(token) {
8556
9445
  return mode === token.symbiosis_mode ? null : { symbiosis_mode: mode };
8557
9446
  }
8558
9447
  case "source_link": {
8559
- const link = await input4({
9448
+ const link = await input5({
8560
9449
  message: "Source link (blank allowed):",
8561
9450
  default: token.source_link ?? ""
8562
9451
  });
8563
9452
  return link === token.source_link ? null : { source_link: link || null };
8564
9453
  }
8565
9454
  case "question": {
8566
- const question = await input4({
9455
+ const question = await input5({
8567
9456
  message: "Recall question (blank allowed):",
8568
9457
  default: token.question ?? ""
8569
9458
  });
@@ -8581,7 +9470,7 @@ var STOP_WORDS = /* @__PURE__ */ new Set(["q", ":q", "quit", "stop"]);
8581
9470
  function isExitPrompt(err) {
8582
9471
  return err instanceof Error && err.name === "ExitPromptError";
8583
9472
  }
8584
- var learnCommand = new Command8("learn").description(
9473
+ var learnCommand = new Command9("learn").description(
8585
9474
  "Run a spoiler-free, in-process learning session (recall \u2192 reveal \u2192 self-rate)"
8586
9475
  ).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) => {
8587
9476
  let db;
@@ -8661,7 +9550,7 @@ ${"\u2500".repeat(50)}`);
8661
9550
  ${prompt.question}`);
8662
9551
  let answer;
8663
9552
  try {
8664
- answer = await input5({
9553
+ answer = await input6({
8665
9554
  message: t(locale, "prompt_answer")
8666
9555
  });
8667
9556
  } catch (err) {
@@ -8781,36 +9670,52 @@ ${"\u2550".repeat(50)}`);
8781
9670
  process.exit(1);
8782
9671
  }
8783
9672
  });
8784
-
8785
- // src/cli/commands/monitor.ts
8786
- import { execFileSync as execFileSync3, execSync as execSync5 } from "child_process";
8787
- import { unlinkSync, writeFileSync as writeFileSync8 } from "fs";
8788
- import { tmpdir as tmpdir3 } from "os";
8789
- import { basename as basename3, join as join16 } from "path";
8790
- import { Command as Command9 } from "commander";
8791
- function isPowerShellShell(shell) {
8792
- return shell === "pwsh" || shell === "powershell";
9673
+ function buildLearnCommand(shell, userId) {
9674
+ const zamInvocation = resolveZamInvocation(shell);
9675
+ const learnArgs = userId ? ` learn --user ${userId}` : " learn";
9676
+ return `${zamInvocation}${learnArgs}`;
8793
9677
  }
8794
- function normalizeShell(shell) {
8795
- if (!shell) return detectShell();
8796
- const normalized = shell.toLowerCase();
8797
- if (normalized === "zsh" || normalized === "bash" || normalized === "pwsh" || normalized === "powershell") {
8798
- return normalized;
9678
+ 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(
9679
+ "--shell <type>",
9680
+ "Shell type: zsh | bash | pwsh | powershell (auto-detected)"
9681
+ ).action(async (opts) => {
9682
+ let shell;
9683
+ try {
9684
+ shell = normalizeShell(opts.shell);
9685
+ } catch (err) {
9686
+ console.error(`Error: ${err.message}`);
9687
+ process.exit(1);
8799
9688
  }
8800
- throw new Error(
8801
- `Unsupported shell: ${shell}. Expected zsh, bash, pwsh, or powershell.`
8802
- );
8803
- }
8804
- function psSingleQuoted2(value) {
8805
- return `'${value.replace(/'/g, "''")}'`;
8806
- }
8807
- function detectShell() {
8808
- if (process.platform === "win32")
8809
- return findExecutable("pwsh.exe") ? "pwsh" : "powershell";
8810
- const shell = process.env.SHELL ?? "";
8811
- return basename3(shell) === "bash" ? "bash" : "zsh";
8812
- }
8813
- var monitorCommand = new Command9("monitor").description(
9689
+ let db;
9690
+ let userId = opts.user;
9691
+ try {
9692
+ db = await openDatabase();
9693
+ if (!userId) {
9694
+ userId = await resolveUser(opts, db);
9695
+ }
9696
+ if (!await getSetting(db, "review_method")) {
9697
+ await setSetting(db, "review_method", "console");
9698
+ }
9699
+ } catch (err) {
9700
+ console.error(`Error: ${err.message}`);
9701
+ process.exit(1);
9702
+ } finally {
9703
+ await db?.close();
9704
+ }
9705
+ const dir = opts.dir ?? process.cwd();
9706
+ const learnCommandLine = buildLearnCommand(shell, userId);
9707
+ const shellSetup = buildShellSetupCommand(dir, shell, learnCommandLine);
9708
+ openTerminalWindow({
9709
+ shellSetup,
9710
+ label: "learn",
9711
+ dir,
9712
+ shell
9713
+ });
9714
+ });
9715
+
9716
+ // src/cli/commands/monitor.ts
9717
+ import { Command as Command10 } from "commander";
9718
+ var monitorCommand = new Command10("monitor").description(
8814
9719
  "Shell observation for real-time task monitoring"
8815
9720
  );
8816
9721
  monitorCommand.command("start").description("Output shell hook code to install monitoring").requiredOption("--session <id>", "Session ID to monitor").option(
@@ -8937,39 +9842,6 @@ monitorCommand.command("status").description("Show monitoring status for a sessi
8937
9842
  console.log(` To: ${result.timeSpan.end}`);
8938
9843
  }
8939
9844
  });
8940
- function selectWindowsExecutable(results, pathext = process.env.PATHEXT ?? ".COM;.EXE;.BAT;.CMD") {
8941
- if (results.length === 0) return null;
8942
- const extensions = pathext.split(";").map((ext) => ext.trim().toLowerCase()).filter(Boolean);
8943
- const runnable = results.find(
8944
- (result) => extensions.some((ext) => result.toLowerCase().endsWith(ext))
8945
- );
8946
- return runnable ?? results[0];
8947
- }
8948
- function findExecutable(command) {
8949
- try {
8950
- const lookup = process.platform === "win32" ? `where.exe ${command}` : `command -v ${command}`;
8951
- const results = execSync5(lookup, { encoding: "utf-8" }).split(/\r?\n/).map((line) => line.trim()).filter(Boolean);
8952
- if (results.length === 0) return null;
8953
- if (process.platform === "win32") {
8954
- return selectWindowsExecutable(results);
8955
- }
8956
- return results[0];
8957
- } catch {
8958
- return null;
8959
- }
8960
- }
8961
- function resolveZamInvocation(shell) {
8962
- const installed = findExecutable("zam");
8963
- if (installed) {
8964
- return isPowerShellShell(shell) ? `& ${psSingleQuoted2(installed)}` : installed;
8965
- }
8966
- const projectRoot = join16(import.meta.dirname, "..", "..", "..");
8967
- const cliSource = join16(projectRoot, "src/cli/index.ts");
8968
- if (isPowerShellShell(shell)) {
8969
- return `& npx --prefix ${psSingleQuoted2(projectRoot)} tsx ${psSingleQuoted2(cliSource)}`;
8970
- }
8971
- return `npx --prefix ${JSON.stringify(projectRoot)} tsx ${JSON.stringify(cliSource)}`;
8972
- }
8973
9845
  function buildMonitorSetupCommand(dir, sessionId, shell) {
8974
9846
  const zamInvocation = resolveZamInvocation(shell);
8975
9847
  if (isPowerShellShell(shell)) {
@@ -8982,17 +9854,6 @@ function buildMonitorSetupCommand(dir, sessionId, shell) {
8982
9854
  }
8983
9855
  return `cd ${JSON.stringify(dir)} && eval "$(${zamInvocation} monitor start --session ${sessionId} --shell ${shell})"`;
8984
9856
  }
8985
- function isItermRunning() {
8986
- try {
8987
- const result = execSync5(
8988
- `osascript -e 'tell application "System Events" to (name of processes) contains "iTerm2"' 2>/dev/null`,
8989
- { encoding: "utf-8" }
8990
- ).trim();
8991
- return result === "true";
8992
- } catch {
8993
- return false;
8994
- }
8995
- }
8996
9857
  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(
8997
9858
  "--shell <type>",
8998
9859
  "Shell type: zsh | bash | pwsh | powershell (auto-detected)"
@@ -9025,89 +9886,19 @@ monitorCommand.command("open").description("Open a new monitored terminal window
9025
9886
  } finally {
9026
9887
  await db?.close();
9027
9888
  }
9028
- const dir = opts.dir ?? process.cwd();
9029
- const shellSetup = buildMonitorSetupCommand(dir, opts.session, shell);
9030
- if (process.platform === "darwin" && !isPowerShellShell(shell)) {
9031
- openMacTerminal(shellSetup, opts.session, dir);
9032
- } else if (process.platform === "win32" && isPowerShellShell(shell)) {
9033
- openWindowsPowerShell(shellSetup, opts.session, dir, shell);
9034
- } else {
9035
- console.log(`Run this in a new terminal:
9036
- `);
9037
- console.log(` ${shellSetup}
9038
- `);
9039
- console.log(
9040
- `(Automatic terminal opening is only supported on macOS Terminal/iTerm2 and Windows PowerShell for now.)`
9041
- );
9042
- }
9043
- });
9044
- function openMacTerminal(shellSetup, sessionId, dir) {
9045
- const useIterm = isItermRunning();
9046
- const escaped = shellSetup.replace(/\\/g, "\\\\").replace(/"/g, '\\"');
9047
- const appleScript = useIterm ? `tell application "iTerm2"
9048
- activate
9049
- set newWindow to (create window with default profile)
9050
- tell current session of newWindow
9051
- write text "${escaped}"
9052
- end tell
9053
- end tell` : `tell application "Terminal"
9054
- activate
9055
- do script "${escaped}"
9056
- end tell`;
9057
- const tmpFile = join16(tmpdir3(), `zam-monitor-${sessionId}.scpt`);
9058
- try {
9059
- writeFileSync8(tmpFile, appleScript);
9060
- execSync5(`osascript ${JSON.stringify(tmpFile)}`, { stdio: "ignore" });
9061
- console.log(
9062
- `Opened ${useIterm ? "iTerm2" : "Terminal.app"} window with monitoring for session ${sessionId}`
9063
- );
9064
- console.log(` Directory: ${dir}`);
9065
- } catch (err) {
9066
- console.error(`Failed to open terminal: ${err.message}`);
9067
- console.log(`
9068
- Run this manually in a new terminal:
9069
- `);
9070
- console.log(` ${shellSetup}`);
9071
- } finally {
9072
- try {
9073
- unlinkSync(tmpFile);
9074
- } catch {
9075
- }
9076
- }
9077
- }
9078
- function openWindowsPowerShell(shellSetup, sessionId, dir, requestedShell) {
9079
- const requestedExecutable = requestedShell === "powershell" ? "powershell.exe" : "pwsh.exe";
9080
- const executable = findExecutable(requestedExecutable) ? requestedExecutable : "powershell.exe";
9081
- const startCommand = [
9082
- "Start-Process",
9083
- `-FilePath ${psSingleQuoted2(executable)}`,
9084
- `-ArgumentList @('-NoExit','-NoProfile','-Command',${psSingleQuoted2(shellSetup)})`
9085
- ].join(" ");
9086
- const launcher = findExecutable("pwsh.exe") ? "pwsh.exe" : "powershell.exe";
9087
- try {
9088
- execFileSync3(
9089
- launcher,
9090
- ["-NoProfile", "-ExecutionPolicy", "Bypass", "-Command", startCommand],
9091
- {
9092
- stdio: "ignore"
9093
- }
9094
- );
9095
- console.log(
9096
- `Opened ${executable === "pwsh.exe" ? "PowerShell" : "Windows PowerShell"} window with monitoring for session ${sessionId}`
9097
- );
9098
- console.log(` Directory: ${dir}`);
9099
- } catch (err) {
9100
- console.error(`Failed to open PowerShell: ${err.message}`);
9101
- console.log(`
9102
- Run this manually in a new PowerShell terminal:
9103
- `);
9104
- console.log(` ${shellSetup}`);
9105
- }
9106
- }
9889
+ const dir = opts.dir ?? process.cwd();
9890
+ const shellSetup = buildMonitorSetupCommand(dir, opts.session, shell);
9891
+ openTerminalWindow({
9892
+ shellSetup,
9893
+ label: `monitor-${opts.session}`,
9894
+ dir,
9895
+ shell
9896
+ });
9897
+ });
9107
9898
 
9108
9899
  // src/cli/commands/observer.ts
9109
- import { Command as Command10 } from "commander";
9110
- var observerCommand = new Command10("observer").description(
9900
+ import { Command as Command11 } from "commander";
9901
+ var observerCommand = new Command11("observer").description(
9111
9902
  "Configure what the UI observer may capture (Layer 2 policy)"
9112
9903
  );
9113
9904
  function applyObserverListChange(current, entry, op) {
@@ -9169,9 +9960,9 @@ observerCommand.command("revoke <process>").description("Remove a process from o
9169
9960
  });
9170
9961
 
9171
9962
  // src/cli/commands/profile.ts
9172
- import { homedir as homedir10 } from "os";
9173
- import { dirname as dirname5, join as join17, resolve as resolve4 } from "path";
9174
- import { Command as Command11 } from "commander";
9963
+ import { homedir as homedir12 } from "os";
9964
+ import { dirname as dirname5, join as join19, resolve as resolve5 } from "path";
9965
+ import { Command as Command12 } from "commander";
9175
9966
  var C2 = {
9176
9967
  reset: "\x1B[0m",
9177
9968
  bold: "\x1B[1m",
@@ -9180,7 +9971,7 @@ var C2 = {
9180
9971
  green: "\x1B[32m"
9181
9972
  };
9182
9973
  function defaultPersonalDir() {
9183
- return join17(homedir10(), "Documents", "zam");
9974
+ return join19(homedir12(), "Documents", "zam");
9184
9975
  }
9185
9976
  function render(profile) {
9186
9977
  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}`;
@@ -9191,7 +9982,7 @@ function render(profile) {
9191
9982
  console.log(` data dir: ${C2.cyan}${profile.dataDir}${C2.reset}`);
9192
9983
  console.log(` database: ${C2.cyan}${profile.dbPath}${C2.reset}`);
9193
9984
  }
9194
- 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) => {
9985
+ var profileCommand = new Command12("profile").description("Show or change this machine's ZAM install profile").option("--mode <mode>", "Set install mode: developer | default").option("--dir <path>", "Set the personal-content folder").option("--json", "Output as JSON").action(async (opts) => {
9195
9986
  if (opts.mode && opts.mode !== "developer" && opts.mode !== "default") {
9196
9987
  console.error(`Invalid --mode: ${opts.mode}. Use developer or default.`);
9197
9988
  process.exit(1);
@@ -9201,7 +9992,7 @@ var profileCommand = new Command11("profile").description("Show or change this m
9201
9992
  if (opts.mode) setInstallMode(opts.mode);
9202
9993
  db = await openDatabaseWithSync({ initialize: true });
9203
9994
  if (opts.dir) {
9204
- await setSetting(db, "personal.workspace_dir", resolve4(opts.dir));
9995
+ await setSetting(db, "personal.workspace_dir", resolve5(opts.dir));
9205
9996
  }
9206
9997
  const personalDir = await getSetting(db, "personal.workspace_dir") || defaultPersonalDir();
9207
9998
  await db.close();
@@ -9226,9 +10017,276 @@ var profileCommand = new Command11("profile").description("Show or change this m
9226
10017
  }
9227
10018
  });
9228
10019
 
10020
+ // src/cli/commands/provider.ts
10021
+ import { password as password2 } from "@inquirer/prompts";
10022
+ import { Command as Command13 } from "commander";
10023
+ var VALID_API_FLAVORS = [
10024
+ "chat-completions",
10025
+ "anthropic-messages"
10026
+ ];
10027
+ var VALID_ROLES = ["vision", "recall", "text"];
10028
+ function upsertProviderRecord(providers, name, patch) {
10029
+ const merged = { ...providers[name] ?? {} };
10030
+ if (patch.url !== void 0) merged.url = patch.url;
10031
+ if (patch.model !== void 0) merged.model = patch.model;
10032
+ if (patch.apiFlavor !== void 0) merged.apiFlavor = patch.apiFlavor;
10033
+ if (patch.apiKeyRef !== void 0) merged.apiKeyRef = patch.apiKeyRef;
10034
+ return { ...providers, [name]: merged };
10035
+ }
10036
+ function removeProviderRecord(providers, name) {
10037
+ if (!(name in providers)) return { providers, removed: false };
10038
+ const next = { ...providers };
10039
+ delete next[name];
10040
+ return { providers: next, removed: true };
10041
+ }
10042
+ function rolesReferencing(roles, name) {
10043
+ return VALID_ROLES.filter((role) => {
10044
+ const binding = roles[role];
10045
+ return binding?.primary === name || binding?.fallback === name;
10046
+ });
10047
+ }
10048
+ function bindRoleProviders(roles, role, primary, fallback) {
10049
+ const binding = { primary };
10050
+ if (fallback) binding.fallback = fallback;
10051
+ return { ...roles, [role]: binding };
10052
+ }
10053
+ function maskSecret(key) {
10054
+ return key.length <= 4 ? "\u2022\u2022\u2022\u2022" : `\u2026${key.slice(-4)}`;
10055
+ }
10056
+ function buildProviderListing(providers, hasKey) {
10057
+ return Object.entries(providers).map(([name, rec]) => {
10058
+ const apiFlavor = rec.apiFlavor ?? (rec.url ? inferApiFlavor(rec.url) : "chat-completions");
10059
+ let keyState;
10060
+ if (!rec.apiKeyRef) keyState = "none";
10061
+ else keyState = hasKey(rec.apiKeyRef) ? "set" : "missing";
10062
+ return {
10063
+ name,
10064
+ url: rec.url,
10065
+ model: rec.model,
10066
+ apiFlavor,
10067
+ apiKeyRef: rec.apiKeyRef,
10068
+ keyState
10069
+ };
10070
+ });
10071
+ }
10072
+ function findOrphanKeyRefs(storedRefs, providers) {
10073
+ const used = new Set(
10074
+ Object.values(providers).map((rec) => rec.apiKeyRef).filter((ref) => !!ref)
10075
+ );
10076
+ return storedRefs.filter((ref) => !used.has(ref));
10077
+ }
10078
+ async function readJson(db, key, fallback) {
10079
+ const raw = await getSetting(db, key);
10080
+ if (!raw) return fallback;
10081
+ try {
10082
+ return JSON.parse(raw);
10083
+ } catch {
10084
+ return fallback;
10085
+ }
10086
+ }
10087
+ var readProviders = (db) => readJson(db, "llm.providers", {});
10088
+ var readRoles = (db) => readJson(db, "llm.roles", {});
10089
+ async function writeProviders(db, p) {
10090
+ await setSetting(db, "llm.providers", JSON.stringify(p));
10091
+ }
10092
+ async function writeRoles(db, r) {
10093
+ await setSetting(db, "llm.roles", JSON.stringify(r));
10094
+ }
10095
+ var providerCommand = new Command13("provider").description(
10096
+ "Configure role-based AI providers (url/model/flavor/key, per role)"
10097
+ );
10098
+ providerCommand.command("list").description("Show configured providers, role bindings, and key status").option("--json", "Output as JSON").action(async (opts) => {
10099
+ await withDb(async (db) => {
10100
+ const providers = await readProviders(db);
10101
+ const roles = await readRoles(db);
10102
+ const rows = buildProviderListing(
10103
+ providers,
10104
+ (ref) => getProviderApiKey(ref) !== null
10105
+ );
10106
+ const orphans = findOrphanKeyRefs(listProviderApiKeyRefs(), providers);
10107
+ if (opts.json) {
10108
+ console.log(
10109
+ JSON.stringify({ providers: rows, roles, orphans }, null, 2)
10110
+ );
10111
+ return;
10112
+ }
10113
+ console.log("Providers (llm.providers):\n");
10114
+ if (rows.length === 0) {
10115
+ console.log(
10116
+ " (none) \u2014 add one: zam provider add <name> --url <url> --model <model>"
10117
+ );
10118
+ } else {
10119
+ for (const row of rows) {
10120
+ 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";
10121
+ console.log(` \x1B[36m${row.name.padEnd(12)}\x1B[0m ${key}`);
10122
+ console.log(` url: ${row.url ?? "(inherits llm.url)"}`);
10123
+ console.log(` model: ${row.model ?? "(inherits llm.model)"}`);
10124
+ console.log(` flavor: ${row.apiFlavor}`);
10125
+ if (row.apiKeyRef) console.log(` key-ref: ${row.apiKeyRef}`);
10126
+ }
10127
+ }
10128
+ console.log("\nRoles (llm.roles):\n");
10129
+ for (const role of VALID_ROLES) {
10130
+ const binding = roles[role];
10131
+ if (!binding?.primary) {
10132
+ console.log(` ${role.padEnd(7)} \x1B[90m(unset)\x1B[0m`);
10133
+ } else {
10134
+ const fb = binding.fallback ? ` \u2192 fallback: ${binding.fallback}` : "";
10135
+ console.log(` ${role.padEnd(7)} primary: ${binding.primary}${fb}`);
10136
+ }
10137
+ }
10138
+ if (orphans.length > 0) {
10139
+ console.log(
10140
+ `
10141
+ \x1B[33mOrphan keys (stored but unreferenced):\x1B[0m ${orphans.join(", ")}`
10142
+ );
10143
+ }
10144
+ });
10145
+ });
10146
+ providerCommand.command("add <name>").description("Add or update a provider record in llm.providers").option("--url <url>", "Endpoint base URL (e.g. https://api.deepseek.com/v1)").option("--model <model>", "Model id (e.g. deepseek-v4-flash)").option(
10147
+ "--flavor <flavor>",
10148
+ `Wire protocol: ${VALID_API_FLAVORS.join(" | ")} (default: inferred from URL)`
10149
+ ).option(
10150
+ "--key-ref <ref>",
10151
+ "Credential reference for the API key (default: <name> when --key is given)"
10152
+ ).option(
10153
+ "--key <value>",
10154
+ "Store this API key now (prefer `set-key` to keep it out of shell history)"
10155
+ ).action(async (name, opts) => {
10156
+ let apiFlavor;
10157
+ if (opts.flavor) {
10158
+ if (!VALID_API_FLAVORS.includes(opts.flavor)) {
10159
+ console.error(
10160
+ `Invalid --flavor: ${opts.flavor}. Use ${VALID_API_FLAVORS.join(" or ")}.`
10161
+ );
10162
+ process.exit(1);
10163
+ }
10164
+ apiFlavor = opts.flavor;
10165
+ }
10166
+ const apiKeyRef = opts.keyRef ?? (opts.key ? name : void 0);
10167
+ await withDb(async (db) => {
10168
+ const providers = await readProviders(db);
10169
+ const next = upsertProviderRecord(providers, name, {
10170
+ url: opts.url,
10171
+ model: opts.model,
10172
+ apiFlavor,
10173
+ apiKeyRef
10174
+ });
10175
+ await writeProviders(db, next);
10176
+ if (opts.key && apiKeyRef) setProviderApiKey(apiKeyRef, opts.key);
10177
+ const rec = next[name];
10178
+ console.log(`Provider "${name}" saved:`);
10179
+ console.log(` url: ${rec.url ?? "(inherits llm.url)"}`);
10180
+ console.log(` model: ${rec.model ?? "(inherits llm.model)"}`);
10181
+ console.log(
10182
+ ` flavor: ${rec.apiFlavor ?? `${rec.url ? inferApiFlavor(rec.url) : "chat-completions"} (inferred)`}`
10183
+ );
10184
+ console.log(` key-ref: ${rec.apiKeyRef ?? "(none \u2014 uses default key)"}`);
10185
+ if (opts.key) {
10186
+ console.log(` key: stored (${maskSecret(opts.key)})`);
10187
+ } else if (rec.apiKeyRef && getProviderApiKey(rec.apiKeyRef) === null) {
10188
+ console.log(
10189
+ `
10190
+ \u26A0 No key stored for "${rec.apiKeyRef}". Run: zam provider set-key ${rec.apiKeyRef}`
10191
+ );
10192
+ }
10193
+ if (!rec.url) {
10194
+ console.log(
10195
+ "\n \u26A0 No --url set; this provider inherits the base llm.url."
10196
+ );
10197
+ }
10198
+ console.log(
10199
+ `
10200
+ Bind it to a role: zam provider use recall --primary ${name}`
10201
+ );
10202
+ });
10203
+ });
10204
+ providerCommand.command("remove <name>").description("Remove a provider record from llm.providers").action(async (name) => {
10205
+ await withDb(async (db) => {
10206
+ const providers = await readProviders(db);
10207
+ const { providers: next, removed } = removeProviderRecord(
10208
+ providers,
10209
+ name
10210
+ );
10211
+ if (!removed) {
10212
+ console.log(`No such provider: ${name}`);
10213
+ return;
10214
+ }
10215
+ await writeProviders(db, next);
10216
+ console.log(`Removed provider "${name}".`);
10217
+ const referencing = rolesReferencing(await readRoles(db), name);
10218
+ if (referencing.length > 0) {
10219
+ console.log(
10220
+ ` \u26A0 Still referenced by role(s): ${referencing.join(", ")} \u2014 rebind with: zam provider use <role> --primary <name>`
10221
+ );
10222
+ }
10223
+ });
10224
+ });
10225
+ providerCommand.command("use <role>").description(`Bind providers to a role (${VALID_ROLES.join(" | ")})`).option("--primary <name>", "Primary provider").option("--fallback <name>", "Fallback provider (optional)").action(async (role, opts) => {
10226
+ if (!VALID_ROLES.includes(role)) {
10227
+ console.error(`Invalid role: ${role}. Use ${VALID_ROLES.join(", ")}.`);
10228
+ process.exit(1);
10229
+ }
10230
+ if (!opts.primary) {
10231
+ console.error("--primary is required.");
10232
+ process.exit(1);
10233
+ }
10234
+ await withDb(async (db) => {
10235
+ const providers = await readProviders(db);
10236
+ await writeRoles(
10237
+ db,
10238
+ bindRoleProviders(
10239
+ await readRoles(db),
10240
+ role,
10241
+ opts.primary,
10242
+ opts.fallback
10243
+ )
10244
+ );
10245
+ const fb = opts.fallback ? `, fallback: ${opts.fallback}` : "";
10246
+ console.log(`Role "${role}" \u2192 primary: ${opts.primary}${fb}`);
10247
+ for (const [label, ref] of [
10248
+ ["primary", opts.primary],
10249
+ ["fallback", opts.fallback]
10250
+ ]) {
10251
+ if (ref && !(ref in providers)) {
10252
+ console.log(
10253
+ ` \u26A0 ${label} "${ref}" is not a defined provider yet. Add it: zam provider add ${ref} --url <url> --model <model>`
10254
+ );
10255
+ }
10256
+ }
10257
+ });
10258
+ });
10259
+ providerCommand.command("set-key <ref>").description(
10260
+ "Store an API key for a provider reference (in credentials.json)"
10261
+ ).option(
10262
+ "--key <value>",
10263
+ "The API key (omit to enter it interactively, hidden)"
10264
+ ).action(async (ref, opts) => {
10265
+ try {
10266
+ const key = opts.key ?? await password2({ message: `API key for "${ref}":` });
10267
+ if (!key || key.trim().length === 0) {
10268
+ console.error("No key provided.");
10269
+ process.exit(1);
10270
+ }
10271
+ setProviderApiKey(ref, key.trim());
10272
+ console.log(`Stored API key for "${ref}" (${maskSecret(key.trim())}).`);
10273
+ } catch (err) {
10274
+ if (err.name === "ExitPromptError") {
10275
+ console.log("\nCancelled.");
10276
+ process.exit(0);
10277
+ }
10278
+ console.error("Error:", err.message);
10279
+ process.exit(1);
10280
+ }
10281
+ });
10282
+ providerCommand.command("clear-key <ref>").description("Remove a stored API key").action((ref) => {
10283
+ clearProviderApiKey(ref);
10284
+ console.log(`Cleared API key for "${ref}".`);
10285
+ });
10286
+
9229
10287
  // src/cli/commands/review.ts
9230
- import { Command as Command12 } from "commander";
9231
- 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) => {
10288
+ import { Command as Command14 } from "commander";
10289
+ var reviewCommand = new Command14("review").description("Start an interactive review session").option("--user <id>", "User ID (default: whoami)").option("--max-new <n>", "Maximum new cards", "10").option("--max-reviews <n>", "Maximum review cards", "50").option("--no-resolve", "Skip resolving source_link into inline context").action(async (opts) => {
9232
10290
  let db;
9233
10291
  try {
9234
10292
  db = await openDatabase();
@@ -9342,9 +10400,9 @@ ${"\u2550".repeat(50)}`);
9342
10400
 
9343
10401
  // src/cli/commands/session.ts
9344
10402
  import { readFileSync as readFileSync11 } from "fs";
9345
- import { input as input6, select as select2 } from "@inquirer/prompts";
9346
- import { Command as Command13 } from "commander";
9347
- var sessionCommand = new Command13("session").description(
10403
+ import { input as input7, select as select2 } from "@inquirer/prompts";
10404
+ import { Command as Command15 } from "commander";
10405
+ var sessionCommand = new Command15("session").description(
9348
10406
  "Manage learning sessions"
9349
10407
  );
9350
10408
  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(
@@ -9521,7 +10579,7 @@ async function selectTask() {
9521
10579
  console.log("No active work items found in Azure DevOps.");
9522
10580
  }
9523
10581
  }
9524
- return input6({ message: "Task description:" });
10582
+ return input7({ message: "Task description:" });
9525
10583
  }
9526
10584
  var RATING_LABELS = {
9527
10585
  1: "Again",
@@ -9723,9 +10781,9 @@ sessionCommand.command("end").description("End a session and show summary").requ
9723
10781
  });
9724
10782
 
9725
10783
  // src/cli/commands/settings.ts
9726
- import { existsSync as existsSync16 } from "fs";
9727
- import { Command as Command14 } from "commander";
9728
- var settingsCommand = new Command14("settings").description(
10784
+ import { existsSync as existsSync19 } from "fs";
10785
+ import { Command as Command16 } from "commander";
10786
+ var settingsCommand = new Command16("settings").description(
9729
10787
  "Manage user settings"
9730
10788
  );
9731
10789
  var BOOLEAN_SETTING_KEYS = /* @__PURE__ */ new Set(["llm.enabled", "llm.vision.enabled"]);
@@ -9888,7 +10946,7 @@ settingsCommand.command("repos").description("Show or set Personal, Team, and Or
9888
10946
  console.log("\nValidation:");
9889
10947
  for (const [name, path] of Object.entries(paths)) {
9890
10948
  if (path) {
9891
- const exists = existsSync16(path);
10949
+ const exists = existsSync19(path);
9892
10950
  console.log(
9893
10951
  ` ${name.padEnd(9)}: ${exists ? "\x1B[32m\u2713 Valid folder\x1B[0m" : "\x1B[31m\u2717 Directory does not exist\x1B[0m"}`
9894
10952
  );
@@ -9900,41 +10958,41 @@ settingsCommand.command("repos").description("Show or set Personal, Team, and Or
9900
10958
  });
9901
10959
 
9902
10960
  // src/cli/commands/setup.ts
9903
- import { copyFileSync as copyFileSync2, existsSync as existsSync17, mkdirSync as mkdirSync10, writeFileSync as writeFileSync9 } from "fs";
9904
- import { basename as basename4, dirname as dirname6, join as join18 } from "path";
10961
+ import { copyFileSync as copyFileSync2, existsSync as existsSync20, mkdirSync as mkdirSync11, writeFileSync as writeFileSync10 } from "fs";
10962
+ import { basename as basename4, dirname as dirname6, join as join20 } from "path";
9905
10963
  import { fileURLToPath as fileURLToPath2 } from "url";
9906
- import { Command as Command15 } from "commander";
10964
+ import { Command as Command17 } from "commander";
9907
10965
  var packageRoot = [
9908
10966
  fileURLToPath2(new URL("../..", import.meta.url)),
9909
10967
  fileURLToPath2(new URL("../../..", import.meta.url))
9910
- ].find((candidate) => existsSync17(join18(candidate, "package.json"))) ?? fileURLToPath2(new URL("../..", import.meta.url));
10968
+ ].find((candidate) => existsSync20(join20(candidate, "package.json"))) ?? fileURLToPath2(new URL("../..", import.meta.url));
9911
10969
  var SKILL_PAIRS = [
9912
10970
  {
9913
- from: join18(packageRoot, ".claude", "skills", "zam", "SKILL.md"),
9914
- to: join18(".claude", "skills", "zam", "SKILL.md")
10971
+ from: join20(packageRoot, ".claude", "skills", "zam", "SKILL.md"),
10972
+ to: join20(".claude", "skills", "zam", "SKILL.md")
9915
10973
  },
9916
10974
  {
9917
- from: join18(packageRoot, ".agent", "skills", "zam", "SKILL.md"),
9918
- to: join18(".agent", "skills", "zam", "SKILL.md")
10975
+ from: join20(packageRoot, ".agent", "skills", "zam", "SKILL.md"),
10976
+ to: join20(".agent", "skills", "zam", "SKILL.md")
9919
10977
  },
9920
10978
  {
9921
- from: join18(packageRoot, ".agents", "skills", "zam", "SKILL.md"),
9922
- to: join18(".agents", "skills", "zam", "SKILL.md")
10979
+ from: join20(packageRoot, ".agents", "skills", "zam", "SKILL.md"),
10980
+ to: join20(".agents", "skills", "zam", "SKILL.md")
9923
10981
  }
9924
10982
  ];
9925
10983
  function copySkills(force, cwd = process.cwd()) {
9926
10984
  let anyAction = false;
9927
10985
  for (const { from, to } of SKILL_PAIRS) {
9928
- const dest = join18(cwd, to);
9929
- if (!existsSync17(from)) {
10986
+ const dest = join20(cwd, to);
10987
+ if (!existsSync20(from)) {
9930
10988
  console.warn(` warn source not found, skipping: ${from}`);
9931
10989
  continue;
9932
10990
  }
9933
- if (existsSync17(dest) && !force) {
10991
+ if (existsSync20(dest) && !force) {
9934
10992
  console.log(` skip ${to} (already present \xE2\u20AC\u201D use --force to update)`);
9935
10993
  continue;
9936
10994
  }
9937
- mkdirSync10(dirname6(dest), { recursive: true });
10995
+ mkdirSync11(dirname6(dest), { recursive: true });
9938
10996
  copyFileSync2(from, dest);
9939
10997
  console.log(` copy ${to}`);
9940
10998
  anyAction = true;
@@ -9975,13 +11033,13 @@ async function initDatabase(skipInit) {
9975
11033
  }
9976
11034
  function writeClaudeMd(skipClaudeMd, cwd = process.cwd()) {
9977
11035
  if (skipClaudeMd) return;
9978
- const dest = join18(cwd, "CLAUDE.md");
9979
- if (existsSync17(dest)) {
11036
+ const dest = join20(cwd, "CLAUDE.md");
11037
+ if (existsSync20(dest)) {
9980
11038
  console.log(` skip CLAUDE.md (already present)`);
9981
11039
  return;
9982
11040
  }
9983
11041
  const name = basename4(cwd);
9984
- writeFileSync9(
11042
+ writeFileSync10(
9985
11043
  dest,
9986
11044
  `# ZAM Personal Kernel \xE2\u20AC\u201D ${name}
9987
11045
 
@@ -10009,13 +11067,13 @@ Use \`zam connector setup turso\` to store cloud credentials in
10009
11067
  }
10010
11068
  function writeAgentsMd(skipAgentsMd, cwd = process.cwd()) {
10011
11069
  if (skipAgentsMd) return;
10012
- const dest = join18(cwd, "AGENTS.md");
10013
- if (existsSync17(dest)) {
11070
+ const dest = join20(cwd, "AGENTS.md");
11071
+ if (existsSync20(dest)) {
10014
11072
  console.log(` skip AGENTS.md (already present)`);
10015
11073
  return;
10016
11074
  }
10017
11075
  const name = basename4(cwd);
10018
- writeFileSync9(
11076
+ writeFileSync10(
10019
11077
  dest,
10020
11078
  `# ZAM Personal Kernel - ${name}
10021
11079
 
@@ -10048,7 +11106,7 @@ Codex discovers repository skills under \`.agents/skills/\`. Run
10048
11106
  );
10049
11107
  console.log(` write AGENTS.md`);
10050
11108
  }
10051
- var setupCommand = new Command15("setup").description(
11109
+ var setupCommand = new Command17("setup").description(
10052
11110
  "Distribute ZAM skill files into this personal instance and initialize the database"
10053
11111
  ).option(
10054
11112
  "--force",
@@ -10069,8 +11127,8 @@ var setupCommand = new Command15("setup").description(
10069
11127
  );
10070
11128
 
10071
11129
  // src/cli/commands/skill.ts
10072
- import { Command as Command16 } from "commander";
10073
- var skillCommand = new Command16("skill").description(
11130
+ import { Command as Command18 } from "commander";
11131
+ var skillCommand = new Command18("skill").description(
10074
11132
  "Manage agent skill entries (task recipes)"
10075
11133
  );
10076
11134
  skillCommand.command("list").description("List all agent skills").option("--json", "Output as JSON").action(async (opts) => {
@@ -10155,10 +11213,10 @@ skillCommand.command("add").description("Register a new agent skill").requiredOp
10155
11213
  });
10156
11214
 
10157
11215
  // src/cli/commands/snapshot.ts
10158
- import { existsSync as existsSync18, mkdirSync as mkdirSync11, readFileSync as readFileSync12, writeFileSync as writeFileSync10 } from "fs";
10159
- import { homedir as homedir11 } from "os";
10160
- import { dirname as dirname7, join as join19 } from "path";
10161
- import { Command as Command17 } from "commander";
11216
+ import { existsSync as existsSync21, mkdirSync as mkdirSync12, readFileSync as readFileSync12, writeFileSync as writeFileSync11 } from "fs";
11217
+ import { homedir as homedir13 } from "os";
11218
+ import { dirname as dirname7, join as join21 } from "path";
11219
+ import { Command as Command19 } from "commander";
10162
11220
  function defaultOutName() {
10163
11221
  const stamp = (/* @__PURE__ */ new Date()).toISOString().replace(/\.\d+Z$/, "").replace(/:/g, "-");
10164
11222
  return `zam-snapshot-${stamp}.sql`;
@@ -10172,12 +11230,12 @@ function summarize(tables) {
10172
11230
  }
10173
11231
  return { total, nonEmpty };
10174
11232
  }
10175
- 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) => {
11233
+ 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) => {
10176
11234
  let db;
10177
11235
  try {
10178
11236
  db = await openDatabaseWithSync({ initialize: true });
10179
11237
  const snapshot = await exportSnapshot(db);
10180
- const personalDir = await getSetting(db, "personal.workspace_dir") || join19(homedir11(), "Documents", "zam");
11238
+ const personalDir = await getSetting(db, "personal.workspace_dir") || join21(homedir13(), "Documents", "zam");
10181
11239
  await db.close();
10182
11240
  db = void 0;
10183
11241
  const manifest = verifySnapshot(snapshot);
@@ -10186,12 +11244,12 @@ var exportCmd = new Command17("export").description("Write a portable SQL-text s
10186
11244
  process.stdout.write(snapshot);
10187
11245
  return;
10188
11246
  }
10189
- const out = opts.out ?? join19(personalDir, "snapshots", defaultOutName());
11247
+ const out = opts.out ?? join21(personalDir, "snapshots", defaultOutName());
10190
11248
  const dir = dirname7(out);
10191
- if (dir && dir !== "." && !existsSync18(dir)) {
10192
- mkdirSync11(dir, { recursive: true });
11249
+ if (dir && dir !== "." && !existsSync21(dir)) {
11250
+ mkdirSync12(dir, { recursive: true });
10193
11251
  }
10194
- writeFileSync10(out, snapshot, "utf-8");
11252
+ writeFileSync11(out, snapshot, "utf-8");
10195
11253
  console.log(`Snapshot written: ${out}`);
10196
11254
  console.log(
10197
11255
  ` ${total} row(s)${nonEmpty.length ? ` \u2014 ${nonEmpty.join(", ")}` : ""}`
@@ -10202,10 +11260,10 @@ var exportCmd = new Command17("export").description("Write a portable SQL-text s
10202
11260
  process.exit(1);
10203
11261
  }
10204
11262
  });
10205
- 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) => {
11263
+ 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) => {
10206
11264
  let db;
10207
11265
  try {
10208
- if (!existsSync18(file)) {
11266
+ if (!existsSync21(file)) {
10209
11267
  console.error(`Error: Snapshot file not found: ${file}`);
10210
11268
  process.exit(1);
10211
11269
  }
@@ -10225,9 +11283,9 @@ var importCmd = new Command17("import").description("Restore a snapshot into the
10225
11283
  process.exit(1);
10226
11284
  }
10227
11285
  });
10228
- var verifyCmd = new Command17("verify").description("Check a snapshot's manifest and checksum without importing").argument("<file>", "Snapshot file to verify").action((file) => {
11286
+ var verifyCmd = new Command19("verify").description("Check a snapshot's manifest and checksum without importing").argument("<file>", "Snapshot file to verify").action((file) => {
10229
11287
  try {
10230
- if (!existsSync18(file)) {
11288
+ if (!existsSync21(file)) {
10231
11289
  console.error(`Error: Snapshot file not found: ${file}`);
10232
11290
  process.exit(1);
10233
11291
  }
@@ -10243,11 +11301,11 @@ var verifyCmd = new Command17("verify").description("Check a snapshot's manifest
10243
11301
  process.exit(1);
10244
11302
  }
10245
11303
  });
10246
- var snapshotCommand = new Command17("snapshot").description("Export, import, or verify a portable database snapshot").addCommand(exportCmd).addCommand(importCmd).addCommand(verifyCmd);
11304
+ var snapshotCommand = new Command19("snapshot").description("Export, import, or verify a portable database snapshot").addCommand(exportCmd).addCommand(importCmd).addCommand(verifyCmd);
10247
11305
 
10248
11306
  // src/cli/commands/stats.ts
10249
- import { Command as Command18 } from "commander";
10250
- 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) => {
11307
+ import { Command as Command20 } from "commander";
11308
+ 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) => {
10251
11309
  await withDb(async (db) => {
10252
11310
  const userId = await resolveUser(opts, db);
10253
11311
  const stats = await getUserStats(db, userId);
@@ -10283,8 +11341,8 @@ var statsCommand = new Command18("stats").description("Show learning dashboard f
10283
11341
  });
10284
11342
 
10285
11343
  // src/cli/commands/token.ts
10286
- import { Command as Command19 } from "commander";
10287
- var tokenCommand = new Command19("token").description(
11344
+ import { Command as Command21 } from "commander";
11345
+ var tokenCommand = new Command21("token").description(
10288
11346
  "Manage knowledge tokens"
10289
11347
  );
10290
11348
  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) => {
@@ -10570,12 +11628,12 @@ tokenCommand.command("status").description("Show full status of a token for a us
10570
11628
  });
10571
11629
 
10572
11630
  // src/cli/commands/ui.ts
10573
- import { spawn as spawn2, spawnSync } from "child_process";
10574
- import { existsSync as existsSync19 } from "fs";
10575
- import { homedir as homedir12 } from "os";
10576
- import { dirname as dirname8, join as join20 } from "path";
11631
+ import { spawn as spawn3, spawnSync } from "child_process";
11632
+ import { existsSync as existsSync22 } from "fs";
11633
+ import { homedir as homedir14 } from "os";
11634
+ import { dirname as dirname8, join as join22 } from "path";
10577
11635
  import { fileURLToPath as fileURLToPath3 } from "url";
10578
- import { Command as Command20 } from "commander";
11636
+ import { Command as Command22 } from "commander";
10579
11637
  var C3 = {
10580
11638
  reset: "\x1B[0m",
10581
11639
  red: "\x1B[31m",
@@ -10589,8 +11647,8 @@ function findDesktopDir() {
10589
11647
  for (const start of starts) {
10590
11648
  let dir = start;
10591
11649
  for (let i = 0; i < 10; i++) {
10592
- if (existsSync19(join20(dir, "desktop", "src-tauri", "tauri.conf.json"))) {
10593
- return join20(dir, "desktop");
11650
+ if (existsSync22(join22(dir, "desktop", "src-tauri", "tauri.conf.json"))) {
11651
+ return join22(dir, "desktop");
10594
11652
  }
10595
11653
  const parent = dirname8(dir);
10596
11654
  if (parent === dir) break;
@@ -10600,30 +11658,30 @@ function findDesktopDir() {
10600
11658
  return null;
10601
11659
  }
10602
11660
  function findBuiltApp(desktopDir) {
10603
- const releaseDir = join20(desktopDir, "src-tauri", "target", "release");
11661
+ const releaseDir = join22(desktopDir, "src-tauri", "target", "release");
10604
11662
  if (process.platform === "win32") {
10605
11663
  for (const name of ["ZAM.exe", "zam.exe", "zam-desktop.exe"]) {
10606
- const p = join20(releaseDir, name);
10607
- if (existsSync19(p)) return p;
11664
+ const p = join22(releaseDir, name);
11665
+ if (existsSync22(p)) return p;
10608
11666
  }
10609
11667
  } else if (process.platform === "darwin") {
10610
- const app = join20(releaseDir, "bundle", "macos", "ZAM.app");
10611
- if (existsSync19(app)) return app;
11668
+ const app = join22(releaseDir, "bundle", "macos", "ZAM.app");
11669
+ if (existsSync22(app)) return app;
10612
11670
  } else {
10613
11671
  for (const name of ["zam", "ZAM", "zam-desktop"]) {
10614
- const p = join20(releaseDir, name);
10615
- if (existsSync19(p)) return p;
11672
+ const p = join22(releaseDir, name);
11673
+ if (existsSync22(p)) return p;
10616
11674
  }
10617
11675
  }
10618
11676
  return null;
10619
11677
  }
10620
11678
  function findInstalledApp() {
10621
11679
  const candidates = process.platform === "win32" ? [
10622
- process.env.LOCALAPPDATA && join20(process.env.LOCALAPPDATA, "Programs", "ZAM", "ZAM.exe"),
10623
- process.env.ProgramFiles && join20(process.env.ProgramFiles, "ZAM", "ZAM.exe"),
10624
- process.env["ProgramFiles(x86)"] && join20(process.env["ProgramFiles(x86)"], "ZAM", "ZAM.exe")
10625
- ] : process.platform === "darwin" ? ["/Applications/ZAM.app", join20(homedir12(), "Applications", "ZAM.app")] : ["/opt/ZAM/zam", "/usr/bin/zam-desktop"];
10626
- return candidates.find((candidate) => candidate && existsSync19(candidate)) || null;
11680
+ process.env.LOCALAPPDATA && join22(process.env.LOCALAPPDATA, "Programs", "ZAM", "ZAM.exe"),
11681
+ process.env.ProgramFiles && join22(process.env.ProgramFiles, "ZAM", "ZAM.exe"),
11682
+ process.env["ProgramFiles(x86)"] && join22(process.env["ProgramFiles(x86)"], "ZAM", "ZAM.exe")
11683
+ ] : process.platform === "darwin" ? ["/Applications/ZAM.app", join22(homedir14(), "Applications", "ZAM.app")] : ["/opt/ZAM/zam", "/usr/bin/zam-desktop"];
11684
+ return candidates.find((candidate) => candidate && existsSync22(candidate)) || null;
10627
11685
  }
10628
11686
  function runNpm(args, opts) {
10629
11687
  const res = spawnSync("npm", args, {
@@ -10634,7 +11692,7 @@ function runNpm(args, opts) {
10634
11692
  return res.status ?? 1;
10635
11693
  }
10636
11694
  function ensureDesktopDeps(desktopDir) {
10637
- if (existsSync19(join20(desktopDir, "node_modules"))) return true;
11695
+ if (existsSync22(join22(desktopDir, "node_modules"))) return true;
10638
11696
  console.log(
10639
11697
  `${C3.cyan}Installing desktop dependencies (one-time)...${C3.reset}`
10640
11698
  );
@@ -10660,13 +11718,13 @@ function requireRust() {
10660
11718
  function hasMsvcBuildTools() {
10661
11719
  if (process.platform !== "win32") return true;
10662
11720
  const pf86 = process.env["ProgramFiles(x86)"] || "C:\\Program Files (x86)";
10663
- const vswhere = join20(
11721
+ const vswhere = join22(
10664
11722
  pf86,
10665
11723
  "Microsoft Visual Studio",
10666
11724
  "Installer",
10667
11725
  "vswhere.exe"
10668
11726
  );
10669
- if (!existsSync19(vswhere)) return false;
11727
+ if (!existsSync22(vswhere)) return false;
10670
11728
  const res = spawnSync(
10671
11729
  vswhere,
10672
11730
  [
@@ -10698,7 +11756,7 @@ function requireMsvcOnWindows() {
10698
11756
  return false;
10699
11757
  }
10700
11758
  function warnIfCliMissing(repoRoot) {
10701
- if (!existsSync19(join20(repoRoot, "dist", "cli", "index.js"))) {
11759
+ if (!existsSync22(join22(repoRoot, "dist", "cli", "index.js"))) {
10702
11760
  console.warn(
10703
11761
  `${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}`
10704
11762
  );
@@ -10707,13 +11765,13 @@ function warnIfCliMissing(repoRoot) {
10707
11765
  function launchApp(appPath, workingDir) {
10708
11766
  console.log(`${C3.green}\u2713 Launching ZAM Desktop...${C3.reset}`);
10709
11767
  if (process.platform === "darwin" && appPath.endsWith(".app")) {
10710
- spawn2("open", [appPath], {
11768
+ spawn3("open", [appPath], {
10711
11769
  cwd: workingDir,
10712
11770
  detached: true,
10713
11771
  stdio: "ignore"
10714
11772
  }).unref();
10715
11773
  } else {
10716
- spawn2(appPath, [], {
11774
+ spawn3(appPath, [], {
10717
11775
  cwd: workingDir,
10718
11776
  detached: true,
10719
11777
  stdio: "ignore",
@@ -10755,13 +11813,13 @@ function createShortcuts(appPath, repoRoot) {
10755
11813
  console.error(`${C3.red}\u2717 Could not create shortcuts.${C3.reset}`);
10756
11814
  }
10757
11815
  }
10758
- 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(
11816
+ 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(
10759
11817
  "--build",
10760
11818
  "Build the native installer for your OS (needs Rust, one-time)"
10761
11819
  ).option("--shortcut", "Create Desktop + Start-menu shortcuts to the GUI").action((opts) => {
10762
11820
  const installedApp = findInstalledApp();
10763
11821
  if (!opts.dev && !opts.build && !opts.shortcut && installedApp) {
10764
- launchApp(installedApp, homedir12());
11822
+ launchApp(installedApp, homedir14());
10765
11823
  return;
10766
11824
  }
10767
11825
  const desktopDir = findDesktopDir();
@@ -10782,7 +11840,7 @@ var uiCommand = new Command20("ui").description("Launch the ZAM Desktop GUI (Act
10782
11840
  );
10783
11841
  const code = runNpm(["run", "tauri", "build"], { cwd: desktopDir });
10784
11842
  if (code === 0) {
10785
- const bundle = join20(
11843
+ const bundle = join22(
10786
11844
  desktopDir,
10787
11845
  "src-tauri",
10788
11846
  "target",
@@ -10859,11 +11917,11 @@ ${C3.dim}After that, 'zam ui' launches it directly.${C3.reset}`
10859
11917
 
10860
11918
  // src/cli/commands/update.ts
10861
11919
  import { spawnSync as spawnSync2 } from "child_process";
10862
- import { existsSync as existsSync20, readFileSync as readFileSync13, realpathSync } from "fs";
10863
- import { dirname as dirname9, join as join21 } from "path";
11920
+ import { existsSync as existsSync23, readFileSync as readFileSync13, realpathSync } from "fs";
11921
+ import { dirname as dirname9, join as join23 } from "path";
10864
11922
  import { fileURLToPath as fileURLToPath4 } from "url";
10865
- import { confirm as confirm3 } from "@inquirer/prompts";
10866
- import { Command as Command21 } from "commander";
11923
+ import { confirm as confirm4 } from "@inquirer/prompts";
11924
+ import { Command as Command23 } from "commander";
10867
11925
  var GITHUB_REPO = "zam-os/zam";
10868
11926
  var CHANNELS = [
10869
11927
  "developer",
@@ -10885,7 +11943,7 @@ function currentVersion() {
10885
11943
  for (const up of ["..", "../..", "../../.."]) {
10886
11944
  try {
10887
11945
  const pkg2 = JSON.parse(
10888
- readFileSync13(join21(here, up, "package.json"), "utf-8")
11946
+ readFileSync13(join23(here, up, "package.json"), "utf-8")
10889
11947
  );
10890
11948
  if (pkg2.version) return pkg2.version;
10891
11949
  } catch {
@@ -10896,7 +11954,7 @@ function currentVersion() {
10896
11954
  function versionAt(dir) {
10897
11955
  try {
10898
11956
  const pkg2 = JSON.parse(
10899
- readFileSync13(join21(dir, "package.json"), "utf-8")
11957
+ readFileSync13(join23(dir, "package.json"), "utf-8")
10900
11958
  );
10901
11959
  return pkg2.version ?? "unknown";
10902
11960
  } catch {
@@ -10941,7 +11999,7 @@ function render2(decision) {
10941
11999
  );
10942
12000
  }
10943
12001
  }
10944
- var checkCmd = new Command21("check").description("Check whether a newer ZAM has been released").option(
12002
+ var checkCmd = new Command23("check").description("Check whether a newer ZAM has been released").option(
10945
12003
  "--latest <version>",
10946
12004
  "Compare against this version instead of fetching"
10947
12005
  ).option("--channel <channel>", "Override the detected install channel").option("--json", "Output as JSON").action(
@@ -10976,13 +12034,13 @@ function findSourceRepo() {
10976
12034
  let dir = realpathSync(dirname9(fileURLToPath4(import.meta.url)));
10977
12035
  let parent = dirname9(dir);
10978
12036
  while (parent !== dir) {
10979
- if (existsSync20(join21(dir, ".git"))) return dir;
12037
+ if (existsSync23(join23(dir, ".git"))) return dir;
10980
12038
  dir = parent;
10981
12039
  parent = dirname9(dir);
10982
12040
  }
10983
- return existsSync20(join21(dir, ".git")) ? dir : null;
12041
+ return existsSync23(join23(dir, ".git")) ? dir : null;
10984
12042
  }
10985
- function runGit(cwd, args, capture) {
12043
+ function runGit2(cwd, args, capture) {
10986
12044
  const res = spawnSync2("git", args, {
10987
12045
  cwd,
10988
12046
  stdio: capture ? ["ignore", "pipe", "pipe"] : "inherit",
@@ -11014,7 +12072,7 @@ function applyDeveloperUpdate(force) {
11014
12072
  );
11015
12073
  process.exit(1);
11016
12074
  }
11017
- const status = runGit(src, ["status", "--porcelain"], true);
12075
+ const status = runGit2(src, ["status", "--porcelain"], true);
11018
12076
  if (status.ok && status.out && !force) {
11019
12077
  console.error(
11020
12078
  `${C4.red}\u2717${C4.reset} The source checkout has uncommitted changes:
@@ -11026,7 +12084,7 @@ Commit or stash them, or re-run with ${C4.cyan}--force${C4.reset}.`
11026
12084
  console.log(
11027
12085
  `${C4.dim}\u2192 git pull --ff-only${C4.reset} ${C4.dim}(${src})${C4.reset}`
11028
12086
  );
11029
- if (!runGit(src, ["pull", "--ff-only"], false).ok) {
12087
+ if (!runGit2(src, ["pull", "--ff-only"], false).ok) {
11030
12088
  console.error(
11031
12089
  `${C4.red}\u2717${C4.reset} git pull failed \u2014 the branch may have diverged or you are offline. Resolve it manually, then retry.`
11032
12090
  );
@@ -11045,7 +12103,7 @@ Commit or stash them, or re-run with ${C4.cyan}--force${C4.reset}.`
11045
12103
  console.log(`${C4.dim}\u2192 zam setup --force${C4.reset}`);
11046
12104
  const setup = spawnSync2(
11047
12105
  process.execPath,
11048
- [join21(src, "dist", "cli", "index.js"), "setup", "--force"],
12106
+ [join23(src, "dist", "cli", "index.js"), "setup", "--force"],
11049
12107
  { cwd: process.cwd(), stdio: "inherit" }
11050
12108
  );
11051
12109
  if (setup.status !== 0) {
@@ -11088,7 +12146,7 @@ ${C4.dim}This install updates through the desktop app's signed updater \u2014 op
11088
12146
  return;
11089
12147
  }
11090
12148
  if (!opts.yes) {
11091
- const ok = await confirm3({
12149
+ const ok = await confirm4({
11092
12150
  message: "Apply this update?",
11093
12151
  default: true
11094
12152
  });
@@ -11108,7 +12166,7 @@ ${C4.dim}This install updates through the desktop app's signed updater \u2014 op
11108
12166
  process.exit(1);
11109
12167
  }
11110
12168
  }
11111
- var updateCommand = new Command21("update").description(
12169
+ var updateCommand = new Command23("update").description(
11112
12170
  "Update ZAM to the latest release (use `update check` to only check)"
11113
12171
  ).option("-y, --yes", "Apply without confirmation").option(
11114
12172
  "--force",
@@ -11118,8 +12176,8 @@ var updateCommand = new Command21("update").description(
11118
12176
  }).addCommand(checkCmd);
11119
12177
 
11120
12178
  // src/cli/commands/whoami.ts
11121
- import { Command as Command22 } from "commander";
11122
- 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) => {
12179
+ import { Command as Command24 } from "commander";
12180
+ 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) => {
11123
12181
  await withDb(async (db) => {
11124
12182
  if (opts.set) {
11125
12183
  await setSetting(db, "user.id", opts.set);
@@ -11154,163 +12212,12 @@ var whoamiCommand = new Command22("whoami").description("Show or set the default
11154
12212
  });
11155
12213
  });
11156
12214
 
11157
- // src/cli/commands/workspace.ts
11158
- import { execSync as execSync6 } from "child_process";
11159
- import { existsSync as existsSync21, writeFileSync as writeFileSync11 } from "fs";
11160
- import { join as join22 } from "path";
11161
- import { confirm as confirm4, input as input7 } from "@inquirer/prompts";
11162
- import { Command as Command23 } from "commander";
11163
- function runGit2(cwd, args) {
11164
- try {
11165
- return execSync6(`git ${args}`, {
11166
- cwd,
11167
- stdio: "pipe",
11168
- encoding: "utf8"
11169
- }).trim();
11170
- } catch (err) {
11171
- throw new Error(`Git command failed: ${err.message}`);
11172
- }
11173
- }
11174
- var workspaceCommand = new Command23("workspace").description(
11175
- "Manage your ZAM learning workspace"
11176
- );
11177
- workspaceCommand.command("publish").description("Publish your local workspace sandbox to GitHub").action(async () => {
11178
- let db;
11179
- let workspaceDir = "";
11180
- try {
11181
- db = await openDatabase();
11182
- workspaceDir = await getSetting(db, "personal.workspace_dir") || "";
11183
- await db.close();
11184
- } catch {
11185
- await db?.close();
11186
- }
11187
- if (!workspaceDir) {
11188
- console.error(
11189
- "\x1B[31m\u2717 No active workspace configured. Please run `zam init` first.\x1B[0m"
11190
- );
11191
- process.exit(1);
11192
- }
11193
- if (!existsSync21(workspaceDir)) {
11194
- console.error(
11195
- `\x1B[31m\u2717 Workspace directory does not exist: ${workspaceDir}\x1B[0m`
11196
- );
11197
- process.exit(1);
11198
- }
11199
- console.log(`
11200
- Active workspace: \x1B[36m${workspaceDir}\x1B[0m`);
11201
- if (!hasCommand("git")) {
11202
- console.error(
11203
- "\x1B[31m\u2717 Git command was not found on this system. Please install Git first.\x1B[0m"
11204
- );
11205
- process.exit(1);
11206
- }
11207
- const gitignorePath = join22(workspaceDir, ".gitignore");
11208
- if (!existsSync21(gitignorePath)) {
11209
- writeFileSync11(
11210
- gitignorePath,
11211
- "node_modules/\n.agent/\n.agents/\n.claude/\n.gemini/\n.goose/\n*.log\n",
11212
- "utf8"
11213
- );
11214
- }
11215
- const hasGitRepo = existsSync21(join22(workspaceDir, ".git"));
11216
- if (!hasGitRepo) {
11217
- console.log("Initializing local Git repository...");
11218
- runGit2(workspaceDir, "init -b main");
11219
- runGit2(workspaceDir, "add .");
11220
- runGit2(
11221
- workspaceDir,
11222
- 'commit -m "chore: initial workspace sandbox bootstrap"'
11223
- );
11224
- console.log("\x1B[32m\u2713 Local Git repository initialized.\x1B[0m");
11225
- } else {
11226
- console.log("Git repository is already initialized.");
11227
- }
11228
- const repoName = await input7({
11229
- message: "Choose a name for your GitHub repository:",
11230
- default: "zam-personal"
11231
- });
11232
- const isPrivate = await confirm4({
11233
- message: "Should the repository be private?",
11234
- default: true
11235
- });
11236
- const repoVisibility = isPrivate ? "--private" : "--public";
11237
- if (hasCommand("gh")) {
11238
- console.log("GitHub CLI detected! Automating repository creation...");
11239
- const proceedGh = await confirm4({
11240
- message: "Would you like ZAM to create the repository using the GitHub CLI?",
11241
- default: true
11242
- });
11243
- if (proceedGh) {
11244
- try {
11245
- console.log(`Creating GitHub repository ${repoName}...`);
11246
- execSync6(
11247
- `gh repo create ${repoName} ${repoVisibility} --source=. --push`,
11248
- {
11249
- cwd: workspaceDir,
11250
- stdio: "inherit"
11251
- }
11252
- );
11253
- console.log(
11254
- "\n\x1B[32m\u2713 Successfully published workspace to GitHub!\x1B[0m"
11255
- );
11256
- process.exit(0);
11257
- } catch (err) {
11258
- console.warn(
11259
- `\x1B[33m\u26A0 GitHub CLI creation failed: ${err.message}\x1B[0m`
11260
- );
11261
- }
11262
- }
11263
- }
11264
- console.log(
11265
- "\n\x1B[1mPlease create the repository manually on GitHub:\x1B[0m"
11266
- );
11267
- console.log(" 1. Go to https://github.com/new");
11268
- console.log(` 2. Name it exactly: \x1B[36m${repoName}\x1B[0m`);
11269
- console.log(
11270
- ` 3. Choose \x1B[36m${isPrivate ? "Private" : "Public"}\x1B[0m`
11271
- );
11272
- console.log(
11273
- " 4. Do NOT initialize it with README, .gitignore, or license"
11274
- );
11275
- console.log(" 5. Click 'Create repository'\n");
11276
- const githubUrl = await input7({
11277
- message: "Paste the repository Git URL (e.g. git@github.com:user/repo.git):"
11278
- });
11279
- if (githubUrl) {
11280
- try {
11281
- console.log("Linking remote repository and pushing...");
11282
- let hasOrigin = false;
11283
- try {
11284
- runGit2(workspaceDir, "remote get-url origin");
11285
- hasOrigin = true;
11286
- } catch {
11287
- }
11288
- if (hasOrigin) {
11289
- runGit2(workspaceDir, `remote set-url origin ${githubUrl}`);
11290
- } else {
11291
- runGit2(workspaceDir, `remote add origin ${githubUrl}`);
11292
- }
11293
- runGit2(workspaceDir, "push -u origin main");
11294
- console.log(
11295
- "\x1B[32m\u2713 Successfully linked and pushed to GitHub!\x1B[0m"
11296
- );
11297
- } catch (err) {
11298
- console.error(
11299
- `\x1B[31m\u2717 Push failed: ${err.message}\x1B[0m`
11300
- );
11301
- console.log(
11302
- "You can push manually later using: git push -u origin main"
11303
- );
11304
- }
11305
- }
11306
- });
11307
-
11308
12215
  // src/cli/index.ts
11309
12216
  var __dirname = dirname10(fileURLToPath5(import.meta.url));
11310
12217
  var pkg = JSON.parse(
11311
- readFileSync14(join23(__dirname, "..", "..", "package.json"), "utf-8")
12218
+ readFileSync14(join24(__dirname, "..", "..", "package.json"), "utf-8")
11312
12219
  );
11313
- var program = new Command24();
12220
+ var program = new Command25();
11314
12221
  program.name("zam").description(
11315
12222
  "The Symbiotic Learning Kernel: Elevating Human Intelligence through AI Collaboration."
11316
12223
  ).version(pkg.version);
@@ -11330,6 +12237,7 @@ program.addCommand(observerCommand);
11330
12237
  program.addCommand(settingsCommand);
11331
12238
  program.addCommand(whoamiCommand);
11332
12239
  program.addCommand(connectorCommand);
12240
+ program.addCommand(providerCommand);
11333
12241
  program.addCommand(snapshotCommand);
11334
12242
  program.addCommand(profileCommand);
11335
12243
  program.addCommand(updateCommand);