zam-core 0.4.3 → 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,19 +5330,108 @@ async function getLlmConfig(db) {
4953
5330
  locale: await getSetting(db, "system.locale") || "en"
4954
5331
  };
4955
5332
  }
4956
- async function getVisionConfig(db) {
4957
- const base = await getLlmConfig(db);
4958
- const maxFramesStr = await getSetting(db, "llm.vision.max_frames");
4959
- const maxFrames = maxFramesStr ? parseInt(maxFramesStr, 10) : 100;
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;
4960
5391
  return {
4961
- enabled: await getSetting(db, "llm.vision.enabled") === "true",
4962
- url: await getSetting(db, "llm.vision.url") || base.url,
4963
- model: await getSetting(db, "llm.vision.model") || base.model,
4964
- apiKey: await getSetting(db, "llm.vision.api_key") || base.apiKey,
5392
+ enabled: base.enabled,
5393
+ url,
5394
+ model: rec.model || base.model,
5395
+ apiKey: resolveProviderApiKey(rec),
5396
+ apiFlavor: rec.apiFlavor || inferApiFlavor(url),
4965
5397
  locale: base.locale,
4966
- maxFrames: Number.isNaN(maxFrames) ? 100 : maxFrames
5398
+ ...role === "vision" ? { maxFrames: base.maxFrames } : {}
5399
+ };
5400
+ }
5401
+ async function getLegacyRoleConfig(db, role, enabled) {
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
+ }
5419
+ return {
5420
+ enabled,
5421
+ url: base.url,
5422
+ model: base.model,
5423
+ apiKey: base.apiKey,
5424
+ apiFlavor: inferApiFlavor(base.url),
5425
+ locale: base.locale
4967
5426
  };
4968
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
+ }
4969
5435
  var LANGUAGE_NAMES = {
4970
5436
  en: "English",
4971
5437
  de: "German",
@@ -5006,10 +5472,11 @@ async function readChatContent(res, label) {
5006
5472
  return content.trim();
5007
5473
  }
5008
5474
  async function generateQuestionViaLLM(db, input8) {
5009
- const cfg = await getLlmConfig(db);
5475
+ const cfg = await getProviderForRole(db, "recall");
5010
5476
  if (!cfg.enabled) {
5011
5477
  throw new Error("LLM integration is disabled in settings (llm.enabled)");
5012
5478
  }
5479
+ assertChatCompletions(cfg);
5013
5480
  const bloom = input8.bloomLevel >= 1 && input8.bloomLevel <= 5 ? input8.bloomLevel : 1;
5014
5481
  const verb = BLOOM_VERBS2[bloom];
5015
5482
  const langName = LANGUAGE_NAMES[cfg.locale] || "English";
@@ -5049,10 +5516,11 @@ Active-Recall Question:`;
5049
5516
  return readChatContent(res, "LLM request");
5050
5517
  }
5051
5518
  async function evaluateAnswerViaLLM(db, input8) {
5052
- const cfg = await getLlmConfig(db);
5519
+ const cfg = await getProviderForRole(db, "recall");
5053
5520
  if (!cfg.enabled) {
5054
5521
  throw new Error("LLM integration is disabled in settings (llm.enabled)");
5055
5522
  }
5523
+ assertChatCompletions(cfg);
5056
5524
  const langName = LANGUAGE_NAMES[cfg.locale] || "English";
5057
5525
  const ratingPrefix = LOCALIZED_RATING_PREFIX[cfg.locale] || "Suggested rating";
5058
5526
  const systemPrompt = `You are ZAM, an extremely warm, encouraging, and patient skills trainer.
@@ -5102,10 +5570,11 @@ Evaluation:`;
5102
5570
  return readChatContent(res, "LLM evaluation");
5103
5571
  }
5104
5572
  async function translateQuestionViaLLM(db, question) {
5105
- const cfg = await getLlmConfig(db);
5573
+ const cfg = await getProviderForRole(db, "recall");
5106
5574
  if (!cfg.enabled) {
5107
5575
  throw new Error("LLM integration is disabled in settings");
5108
5576
  }
5577
+ assertChatCompletions(cfg);
5109
5578
  const targetLang = LANGUAGE_NAMES[cfg.locale] || "English";
5110
5579
  const systemPrompt = `You are a highly precise translator. Translate the given active-recall question into clear, natural ${targetLang}.
5111
5580
  Output ONLY the raw translation. Do not include any headers, preamble, quotes, or conversational filler.`;
@@ -5159,34 +5628,82 @@ async function getAvailableModels(url, apiKey = DEFAULT_LLM_API_KEY) {
5159
5628
  return [];
5160
5629
  }
5161
5630
  }
5162
- async function checkVisionReadiness(db) {
5163
- const { enabled, url, model, apiKey } = await getVisionConfig(db);
5164
- const visionModelSetting = await getSetting(db, "llm.vision.model");
5165
- const visionModelExplicit = !!visionModelSetting;
5166
- let online = false;
5167
- let availableModels = [];
5168
- let modelAvailable = false;
5169
- if (enabled) {
5170
- online = await isLlmOnline(url);
5171
- if (online) {
5172
- availableModels = await getAvailableModels(url, apiKey);
5173
- modelAvailable = availableModels.length === 0 || availableModels.some(
5174
- (candidate) => candidate.toLowerCase() === model.toLowerCase()
5175
- );
5176
- }
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
+ };
5177
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);
5178
5691
  let warning;
5179
- if (enabled && online && modelAvailable && !visionModelExplicit) {
5180
- warning = `No explicit vision model configured (llm.vision.model). Falling back to base model "${model}", which may not support image input. Set a multimodal model: zam settings set llm.vision.model <model>`;
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
+ }
5181
5698
  }
5182
5699
  return {
5183
- enabled,
5700
+ enabled: cfg.enabled,
5184
5701
  online,
5185
- url,
5186
- model,
5702
+ url: active.url,
5703
+ model: active.model,
5187
5704
  modelAvailable,
5188
5705
  availableModels,
5189
- usable: enabled && online && modelAvailable,
5706
+ usable: cfg.enabled && online && modelAvailable,
5190
5707
  visionModelExplicit,
5191
5708
  warning
5192
5709
  };
@@ -5211,16 +5728,16 @@ function spawnLocalRunner(url, model) {
5211
5728
  const { runner, port } = detectRunner(url, model);
5212
5729
  try {
5213
5730
  if (runner === "fastflowlm") {
5214
- 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";
5215
5732
  if (!hasCommand("flm") && flmExe === "flm") return;
5216
- spawn(flmExe, ["serve", model, "--port", port], {
5733
+ spawn2(flmExe, ["serve", model, "--port", port], {
5217
5734
  detached: true,
5218
5735
  stdio: "ignore",
5219
5736
  windowsHide: true
5220
5737
  }).unref();
5221
5738
  } else if (runner === "ollama" || runner === "generic") {
5222
5739
  if (!hasCommand("ollama")) return;
5223
- spawn("ollama", ["serve"], {
5740
+ spawn2("ollama", ["serve"], {
5224
5741
  detached: true,
5225
5742
  stdio: "ignore",
5226
5743
  windowsHide: true
@@ -5232,7 +5749,7 @@ function spawnLocalRunner(url, model) {
5232
5749
  async function startLocalRunner(url, model, locale) {
5233
5750
  const { runner, port } = detectRunner(url, model);
5234
5751
  if (runner === "fastflowlm") {
5235
- 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";
5236
5753
  if (!hasCommand("flm") && flmExe === "flm") {
5237
5754
  console.warn(
5238
5755
  "\x1B[31m\u2717 FastFlowLM is configured but could not be found on the system.\x1B[0m"
@@ -5245,7 +5762,7 @@ async function startLocalRunner(url, model, locale) {
5245
5762
  `\x1B[36mStarting FastFlowLM serve process: ${flmExe} ${args.join(" ")}\x1B[0m`
5246
5763
  );
5247
5764
  try {
5248
- spawn(flmExe, args, {
5765
+ spawn2(flmExe, args, {
5249
5766
  detached: true,
5250
5767
  stdio: "ignore",
5251
5768
  windowsHide: true
@@ -5266,7 +5783,7 @@ async function startLocalRunner(url, model, locale) {
5266
5783
  }
5267
5784
  console.log("\x1B[36mStarting Ollama serve process: ollama serve\x1B[0m");
5268
5785
  try {
5269
- spawn("ollama", ["serve"], {
5786
+ spawn2("ollama", ["serve"], {
5270
5787
  detached: true,
5271
5788
  stdio: "ignore",
5272
5789
  windowsHide: true
@@ -5289,7 +5806,7 @@ async function startLocalRunner(url, model, locale) {
5289
5806
  let attempts = 0;
5290
5807
  const dotsPerLine = 30;
5291
5808
  while (true) {
5292
- await new Promise((resolve5) => setTimeout(resolve5, 500));
5809
+ await new Promise((resolve6) => setTimeout(resolve6, 500));
5293
5810
  if (await isLlmOnline(url)) {
5294
5811
  if (attempts > 0) process.stdout.write("\n");
5295
5812
  return true;
@@ -5314,12 +5831,18 @@ async function startLocalRunner(url, model, locale) {
5314
5831
  }
5315
5832
  }
5316
5833
  async function ensureLocalLlmRunning(db) {
5317
- const cfg = await getLlmConfig(db);
5834
+ const cfg = await getProviderForRole(db, "recall");
5318
5835
  if (!cfg.enabled) {
5319
5836
  return { usable: false, reason: "disabled" };
5320
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
+ }
5321
5844
  const { url, model, apiKey, locale } = cfg;
5322
- const isLocal = url.includes("localhost") || url.includes("127.0.0.1");
5845
+ const isLocal = isLocalEndpoint(url);
5323
5846
  console.log(`Checking if local LLM server is online at ${url}...`);
5324
5847
  let online = await isLlmOnline(url);
5325
5848
  if (!online && isLocal) {
@@ -5353,8 +5876,9 @@ async function ensureLocalLlmRunning(db) {
5353
5876
  }
5354
5877
  async function ensureLlmReadyHeadless(db, opts = {}) {
5355
5878
  const timeoutMs = opts.timeoutMs ?? 25e3;
5356
- const { enabled, url, model, apiKey } = await getLlmConfig(db);
5357
- if (!enabled) {
5879
+ const cfg = await getProviderForRole(db, "recall");
5880
+ const { url, model, apiKey } = cfg;
5881
+ if (!cfg.enabled) {
5358
5882
  return {
5359
5883
  usable: false,
5360
5884
  reason: "disabled",
@@ -5363,7 +5887,16 @@ async function ensureLlmReadyHeadless(db, opts = {}) {
5363
5887
  availableModels: []
5364
5888
  };
5365
5889
  }
5366
- 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);
5367
5900
  let online = await isLlmOnline(url);
5368
5901
  if (!online && isLocal) {
5369
5902
  spawnLocalRunner(url, model);
@@ -5428,8 +5961,8 @@ async function fetchWithInteractiveTimeout(url, options = {}) {
5428
5961
  const dotsPerLine = 30;
5429
5962
  while (true) {
5430
5963
  let timeoutId;
5431
- const timeoutPromise = new Promise((resolve5) => {
5432
- timeoutId = setTimeout(() => resolve5("timeout"), timeoutMs);
5964
+ const timeoutPromise = new Promise((resolve6) => {
5965
+ timeoutId = setTimeout(() => resolve6("timeout"), timeoutMs);
5433
5966
  });
5434
5967
  const dotsInterval = setInterval(() => {
5435
5968
  process.stdout.write(".");
@@ -5502,8 +6035,8 @@ async function ensureHighQualityQuestion(db, token) {
5502
6035
  // src/cli/llm/vision.ts
5503
6036
  import { randomBytes } from "crypto";
5504
6037
  import { readFileSync as readFileSync9 } from "fs";
5505
- import { tmpdir } from "os";
5506
- import { basename as basename2, join as join12 } from "path";
6038
+ import { tmpdir as tmpdir2 } from "os";
6039
+ import { basename as basename3, join as join14 } from "path";
5507
6040
  var LANGUAGE_NAMES2 = {
5508
6041
  en: "English",
5509
6042
  de: "German",
@@ -5530,7 +6063,7 @@ var ACTION_TYPES2 = /* @__PURE__ */ new Set([
5530
6063
  "window-change"
5531
6064
  ]);
5532
6065
  async function observeUiSnapshotViaLLM(db, input8) {
5533
- const cfg = await getVisionConfig(db);
6066
+ const cfg = await getProviderForRole(db, "vision");
5534
6067
  if (!cfg.enabled) {
5535
6068
  throw new Error(
5536
6069
  "Vision observation is disabled in settings (llm.vision.enabled)"
@@ -5539,21 +6072,21 @@ async function observeUiSnapshotViaLLM(db, input8) {
5539
6072
  const imageUrls = [];
5540
6073
  const isVideo = /\.(mp4|mov|m4v|avi|mkv|webm)$/i.test(input8.imagePath);
5541
6074
  if (isVideo) {
5542
- const { mkdirSync: mkdirSync12, readdirSync: readdirSync3, rmSync: rmSync3 } = await import("fs");
5543
- const { execSync: execSync7 } = await import("child_process");
5544
- const tempDir = join12(
5545
- tmpdir(),
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(),
5546
6079
  `zam-frames-${randomBytes(4).toString("hex")}`
5547
6080
  );
5548
- mkdirSync12(tempDir, { recursive: true });
6081
+ mkdirSync13(tempDir, { recursive: true });
5549
6082
  try {
5550
- execSync7(
6083
+ execSync6(
5551
6084
  `ffmpeg -i "${input8.imagePath}" -vf "fps=1/3,scale=1280:-1" -vsync vfr "${tempDir}/frame_%03d.png"`,
5552
6085
  { stdio: "ignore" }
5553
6086
  );
5554
6087
  let files = readdirSync3(tempDir).filter((f) => f.endsWith(".png")).sort();
5555
6088
  if (files.length === 0) {
5556
- execSync7(
6089
+ execSync6(
5557
6090
  `ffmpeg -i "${input8.imagePath}" -vframes 1 "${tempDir}/frame_001.png"`,
5558
6091
  { stdio: "ignore" }
5559
6092
  );
@@ -5574,7 +6107,7 @@ async function observeUiSnapshotViaLLM(db, input8) {
5574
6107
  }
5575
6108
  }
5576
6109
  for (const file of sampledFiles) {
5577
- const bytes = readFileSync9(join12(tempDir, file));
6110
+ const bytes = readFileSync9(join14(tempDir, file));
5578
6111
  imageUrls.push(`data:image/png;base64,${bytes.toString("base64")}`);
5579
6112
  }
5580
6113
  } finally {
@@ -5592,42 +6125,92 @@ async function observeUiSnapshotViaLLM(db, input8) {
5592
6125
  if (imageUrls.length === 0) {
5593
6126
  throw new Error("No image data available for vision analysis");
5594
6127
  }
5595
- const model = input8.model ?? cfg.model;
5596
- const content = await requestVisionDraft({
5597
- url: cfg.url,
5598
- apiKey: cfg.apiKey || DEFAULT_LLM_API_KEY,
5599
- model,
5600
- locale: cfg.locale,
5601
- imageUrls,
5602
- input: input8
5603
- });
5604
- let draft;
5605
- try {
5606
- draft = extractDraft(content);
5607
- } catch {
5608
- return uncertainReport(
5609
- input8,
5610
- "Vision model returned output that could not be parsed as JSON."
5611
- );
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
+ });
5612
6143
  }
5613
- const report = buildReport(input8, draft);
5614
- 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) {
5615
6177
  return uncertainReport(
5616
6178
  input8,
5617
6179
  "Vision model returned an invalid UI report draft."
5618
6180
  );
5619
6181
  }
5620
- return report;
6182
+ return uncertainReport(
6183
+ input8,
6184
+ "Vision model returned output that could not be parsed as JSON."
6185
+ );
5621
6186
  }
5622
- async function requestVisionDraft(args) {
5623
- const language = LANGUAGE_NAMES2[args.locale] ?? "English";
5624
- 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 `{
5625
6190
  "kind": "progress | step-completed | error | help-seeking | uncertain",
5626
6191
  "summary": "short factual UI summary in ${language}",
5627
6192
  "actions": [{"type": "click | shortcut | typing | scroll | window-change", "target": "optional UI target", "result": "optional visible result"}],
5628
6193
  "candidateTokens": [{"slug": "optional-kebab-case-skill-token", "confidence": 0.0, "rationale": "why this token may matter"}],
5629
6194
  "confidence": 0.0
5630
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";
5631
6214
  const res = await fetchWithInteractiveTimeout(
5632
6215
  `${args.url}/chat/completions`,
5633
6216
  {
@@ -5639,27 +6222,11 @@ async function requestVisionDraft(args) {
5639
6222
  body: JSON.stringify({
5640
6223
  model: args.model,
5641
6224
  messages: [
5642
- {
5643
- role: "system",
5644
- content: "You are ZAM's UI observer. Return only strict JSON. Do not include markdown, prose, or fields outside the requested schema."
5645
- },
6225
+ { role: "system", content: VISION_SYSTEM_PROMPT },
5646
6226
  {
5647
6227
  role: "user",
5648
6228
  content: [
5649
- {
5650
- type: "text",
5651
- text: args.imageUrls.length > 1 ? `Observe this sequence of Windows/macOS application snapshots showing a task performed over time.
5652
- Application process: ${args.input.application.processName}
5653
- Window title: ${args.input.application.windowTitle ?? "(unknown)"}
5654
-
5655
- Return this JSON draft only:
5656
- ${schema}` : `Observe this Windows/macOS application snapshot for a learning session.
5657
- Application process: ${args.input.application.processName}
5658
- Window title: ${args.input.application.windowTitle ?? "(unknown)"}
5659
-
5660
- Return this JSON draft only:
5661
- ${schema}`
5662
- },
6229
+ { type: "text", text: visionUserText(args, language) },
5663
6230
  ...args.imageUrls.map((url) => ({
5664
6231
  type: "image_url",
5665
6232
  image_url: { url }
@@ -5702,6 +6269,64 @@ ${schema}`
5702
6269
  }
5703
6270
  return content.trim();
5704
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
+ }
5705
6330
  function extractDraft(content) {
5706
6331
  const fenced = content.match(/```(?:json)?\s*([\s\S]*?)```/i);
5707
6332
  const candidate = (fenced?.[1] ?? content).trim();
@@ -5730,7 +6355,7 @@ function buildReport(input8, draft) {
5730
6355
  evidence: [
5731
6356
  {
5732
6357
  type: "keyframe",
5733
- ref: input8.evidenceRef ?? basename2(input8.imagePath),
6358
+ ref: input8.evidenceRef ?? basename3(input8.imagePath),
5734
6359
  redacted: input8.redacted ?? false
5735
6360
  }
5736
6361
  ],
@@ -5752,7 +6377,7 @@ function uncertainReport(input8, summary) {
5752
6377
  evidence: [
5753
6378
  {
5754
6379
  type: "keyframe",
5755
- ref: input8.evidenceRef ?? basename2(input8.imagePath),
6380
+ ref: input8.evidenceRef ?? basename3(input8.imagePath),
5756
6381
  redacted: input8.redacted ?? false
5757
6382
  }
5758
6383
  ],
@@ -5862,6 +6487,206 @@ function jsonOut(data) {
5862
6487
  console.log(JSON.stringify(data, null, 2));
5863
6488
  }
5864
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
+
5865
6690
  // src/cli/commands/bridge.ts
5866
6691
  var isServeMode = false;
5867
6692
  function jsonOut2(data) {
@@ -5918,7 +6743,7 @@ function parseTokenUpdates(opts) {
5918
6743
  }
5919
6744
  return updates;
5920
6745
  }
5921
- var bridgeCommand = new Command2("bridge").description(
6746
+ var bridgeCommand = new Command3("bridge").description(
5922
6747
  "Machine-readable JSON protocol for AI integration"
5923
6748
  );
5924
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) => {
@@ -5946,6 +6771,45 @@ bridgeCommand.command("check-due").description("Check due cards for a user (JSON
5946
6771
  });
5947
6772
  });
5948
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
+ });
5949
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) => {
5950
6814
  await withDb2(async (db) => {
5951
6815
  const userId = await resolveUser(opts, db, { json: true });
@@ -6304,7 +7168,7 @@ bridgeCommand.command("discover-skills").description(
6304
7168
  "20"
6305
7169
  ).action(async (opts) => {
6306
7170
  try {
6307
- const monitorDir = join13(homedir8(), ".zam", "monitor");
7171
+ const monitorDir = join16(homedir10(), ".zam", "monitor");
6308
7172
  let files;
6309
7173
  try {
6310
7174
  files = readdirSync2(monitorDir).filter((f) => f.endsWith(".jsonl"));
@@ -6317,7 +7181,7 @@ bridgeCommand.command("discover-skills").description(
6317
7181
  return;
6318
7182
  }
6319
7183
  const limit = Number(opts.limit);
6320
- const sorted = files.map((f) => ({ name: f, path: join13(monitorDir, f) })).sort((a, b) => b.name.localeCompare(a.name)).slice(0, limit);
7184
+ const sorted = files.map((f) => ({ name: f, path: join16(monitorDir, f) })).sort((a, b) => b.name.localeCompare(a.name)).slice(0, limit);
6321
7185
  const sessionCommands = /* @__PURE__ */ new Map();
6322
7186
  for (const file of sorted) {
6323
7187
  const sessionId = file.name.replace(".jsonl", "");
@@ -6429,7 +7293,7 @@ bridgeCommand.command("observe-ui-snapshot").description(
6429
7293
  });
6430
7294
  function resolveWindowsPowerShell() {
6431
7295
  try {
6432
- execFileSync2("where.exe", ["pwsh.exe"], { stdio: "ignore" });
7296
+ execFileSync4("where.exe", ["pwsh.exe"], { stdio: "ignore" });
6433
7297
  return "pwsh";
6434
7298
  } catch {
6435
7299
  return "powershell";
@@ -6444,7 +7308,7 @@ function captureScreenshot(outputPath, hwnd, processName) {
6444
7308
  throw new Error(`Invalid process name format: ${processName}`);
6445
7309
  }
6446
7310
  if (platform === "win32") {
6447
- const stdout = execFileSync2(
7311
+ const stdout = execFileSync4(
6448
7312
  resolveWindowsPowerShell(),
6449
7313
  [
6450
7314
  "-NoProfile",
@@ -6757,13 +7621,13 @@ Write-CaptureResult "fullscreen" $hwndVal $matchedBy
6757
7621
  } else if (platform === "darwin") {
6758
7622
  if (hwnd) {
6759
7623
  const parsedHwnd = hwnd.startsWith("0x") ? parseInt(hwnd, 16) : parseInt(hwnd, 10);
6760
- execFileSync2("screencapture", ["-l", String(parsedHwnd), outputPath], {
7624
+ execFileSync4("screencapture", ["-l", String(parsedHwnd), outputPath], {
6761
7625
  stdio: "pipe"
6762
7626
  });
6763
7627
  return { method: "screencapture-window", target: null };
6764
7628
  } else if (processName) {
6765
7629
  try {
6766
- const windowId = execFileSync2(
7630
+ const windowId = execFileSync4(
6767
7631
  "osascript",
6768
7632
  [
6769
7633
  "-e",
@@ -6772,17 +7636,17 @@ Write-CaptureResult "fullscreen" $hwndVal $matchedBy
6772
7636
  { encoding: "utf8", stdio: ["pipe", "pipe", "ignore"] }
6773
7637
  ).trim();
6774
7638
  if (windowId && /^\d+$/.test(windowId)) {
6775
- execFileSync2("screencapture", ["-l", windowId, outputPath], {
7639
+ execFileSync4("screencapture", ["-l", windowId, outputPath], {
6776
7640
  stdio: "pipe"
6777
7641
  });
6778
7642
  return { method: "screencapture-window", target: null };
6779
7643
  }
6780
7644
  } catch {
6781
7645
  }
6782
- execFileSync2("screencapture", ["-x", outputPath], { stdio: "pipe" });
7646
+ execFileSync4("screencapture", ["-x", outputPath], { stdio: "pipe" });
6783
7647
  return { method: "screencapture-full", target: null };
6784
7648
  } else {
6785
- execFileSync2("screencapture", ["-x", outputPath], { stdio: "pipe" });
7649
+ execFileSync4("screencapture", ["-x", outputPath], { stdio: "pipe" });
6786
7650
  return { method: "screencapture-full", target: null };
6787
7651
  }
6788
7652
  } else {
@@ -6819,7 +7683,7 @@ bridgeCommand.command("capture-ui").description("Capture a screenshot for agent-
6819
7683
  return;
6820
7684
  }
6821
7685
  }
6822
- 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`);
6823
7687
  const captureResult = isProvided ? { method: "provided", target: null } : captureScreenshot(outputPath, opts.hwnd, opts.processName);
6824
7688
  if (!isProvided) {
6825
7689
  const post = decidePostCapture(policy, {
@@ -6863,21 +7727,22 @@ bridgeCommand.command("capture-ui").description("Capture a screenshot for agent-
6863
7727
  });
6864
7728
  });
6865
7729
  });
6866
- bridgeCommand.command("start-recording").description("Start screen recording in the background (macOS only) (JSON)").requiredOption("--session <id>", "ZAM session ID").option("--output <path>", "Video output path").action(async (opts) => {
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) => {
6867
7731
  const platform = process.platform;
6868
- if (platform !== "darwin") {
7732
+ if (platform !== "darwin" && platform !== "win32") {
6869
7733
  jsonOut2({
6870
7734
  sessionId: opts.session,
6871
7735
  started: false,
6872
- error: "Screen recording is only supported on macOS (darwin)"
7736
+ error: "Screen recording is only supported on macOS (darwin) and Windows (win32)"
6873
7737
  });
6874
7738
  return;
6875
7739
  }
6876
7740
  const sessionId = opts.session;
6877
- const statePath = join13(tmpdir2(), `zam-recording-${sessionId}.json`);
6878
- const outputPath = opts.output ?? join13(tmpdir2(), `zam-recording-${sessionId}.mov`);
6879
- const { existsSync: existsSync22, writeFileSync: writeFileSync12, openSync, closeSync } = await import("fs");
6880
- if (existsSync22(statePath)) {
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)) {
6881
7746
  jsonOut2({
6882
7747
  sessionId,
6883
7748
  started: false,
@@ -6885,7 +7750,7 @@ bridgeCommand.command("start-recording").description("Start screen recording in
6885
7750
  });
6886
7751
  return;
6887
7752
  }
6888
- const logPath = join13(tmpdir2(), `zam-recording-${sessionId}.log`);
7753
+ const logPath = join16(tmpdir3(), `zam-recording-${sessionId}.log`);
6889
7754
  let logFd;
6890
7755
  try {
6891
7756
  logFd = openSync(logPath, "w");
@@ -6897,26 +7762,34 @@ bridgeCommand.command("start-recording").description("Start screen recording in
6897
7762
  });
6898
7763
  return;
6899
7764
  }
6900
- const { spawn: spawn3 } = await import("child_process");
6901
- const child = spawn3(
6902
- "ffmpeg",
6903
- [
6904
- "-y",
6905
- "-f",
6906
- "avfoundation",
6907
- "-r",
6908
- "5",
6909
- "-i",
6910
- "0",
6911
- "-pix_fmt",
6912
- "yuv420p",
6913
- outputPath
6914
- ],
6915
- {
6916
- detached: true,
6917
- stdio: ["pipe", logFd, logFd]
6918
- }
6919
- );
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
+ });
6920
7793
  try {
6921
7794
  closeSync(logFd);
6922
7795
  } catch {
@@ -6947,21 +7820,21 @@ bridgeCommand.command("start-recording").description("Start screen recording in
6947
7820
  }
6948
7821
  });
6949
7822
  bridgeCommand.command("stop-recording").description(
6950
- "Stop active screen recording and apply idle-frame compression (macOS only) (JSON)"
7823
+ "Stop active screen recording and apply idle-frame compression (JSON)"
6951
7824
  ).requiredOption("--session <id>", "ZAM session ID").action(async (opts) => {
6952
7825
  const platform = process.platform;
6953
- if (platform !== "darwin") {
7826
+ if (platform !== "darwin" && platform !== "win32") {
6954
7827
  jsonOut2({
6955
7828
  sessionId: opts.session,
6956
7829
  stopped: false,
6957
- error: "Screen recording is only supported on macOS (darwin)"
7830
+ error: "Screen recording is only supported on macOS (darwin) and Windows (win32)"
6958
7831
  });
6959
7832
  return;
6960
7833
  }
6961
7834
  const sessionId = opts.session;
6962
- const statePath = join13(tmpdir2(), `zam-recording-${sessionId}.json`);
6963
- const { existsSync: existsSync22, readFileSync: readFileSync15, rmSync: rmSync3 } = await import("fs");
6964
- if (!existsSync22(statePath)) {
7835
+ const statePath = join16(tmpdir3(), `zam-recording-${sessionId}.json`);
7836
+ const { existsSync: existsSync24, readFileSync: readFileSync15, rmSync: rmSync3 } = await import("fs");
7837
+ if (!existsSync24(statePath)) {
6965
7838
  jsonOut2({
6966
7839
  sessionId,
6967
7840
  stopped: false,
@@ -6985,7 +7858,7 @@ bridgeCommand.command("stop-recording").description(
6985
7858
  };
6986
7859
  let attempts = 0;
6987
7860
  while (isProcessRunning(pid) && attempts < 20) {
6988
- await new Promise((resolve5) => setTimeout(resolve5, 250));
7861
+ await new Promise((resolve6) => setTimeout(resolve6, 250));
6989
7862
  attempts++;
6990
7863
  }
6991
7864
  if (isProcessRunning(pid)) {
@@ -6998,7 +7871,7 @@ bridgeCommand.command("stop-recording").description(
6998
7871
  rmSync3(statePath, { force: true });
6999
7872
  } catch {
7000
7873
  }
7001
- if (!existsSync22(outputPath)) {
7874
+ if (!existsSync24(outputPath)) {
7002
7875
  jsonOut2({
7003
7876
  sessionId,
7004
7877
  stopped: false,
@@ -7007,9 +7880,9 @@ bridgeCommand.command("stop-recording").description(
7007
7880
  return;
7008
7881
  }
7009
7882
  const decimatedPath = outputPath.replace(/\.[^.]+$/, "-decimated.mp4");
7010
- const { execSync: execSync7 } = await import("child_process");
7883
+ const { execSync: execSync6 } = await import("child_process");
7011
7884
  try {
7012
- execSync7(
7885
+ execSync6(
7013
7886
  `ffmpeg -y -i "${outputPath}" -vf "mpdecimate,setpts=N/FRAME_RATE/TB" -an -pix_fmt yuv420p "${decimatedPath}"`,
7014
7887
  { stdio: "ignore" }
7015
7888
  );
@@ -7062,11 +7935,13 @@ bridgeCommand.command("sync-observer-policy").description(
7062
7935
  });
7063
7936
  bridgeCommand.command("check-llm").description("Check if LLM is enabled and online (JSON)").action(async () => {
7064
7937
  await withDb2(async (db) => {
7065
- 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";
7066
7941
  let online = false;
7067
7942
  let availableModels = [];
7068
7943
  let modelAvailable = false;
7069
- if (enabled) {
7944
+ if (enabled && !unsupportedProvider) {
7070
7945
  online = await isLlmOnline(url);
7071
7946
  if (online) {
7072
7947
  availableModels = await getAvailableModels(url, apiKey);
@@ -7081,7 +7956,9 @@ bridgeCommand.command("check-llm").description("Check if LLM is enabled and onli
7081
7956
  url,
7082
7957
  model,
7083
7958
  modelAvailable,
7084
- availableModels
7959
+ availableModels,
7960
+ apiFlavor: provider.apiFlavor,
7961
+ unsupportedProvider
7085
7962
  });
7086
7963
  });
7087
7964
  });
@@ -7397,8 +8274,8 @@ bridgeCommand.command("serve").description("Start the persistent JSON-RPC stdin/
7397
8274
  });
7398
8275
 
7399
8276
  // src/cli/commands/card.ts
7400
- import { Command as Command3 } from "commander";
7401
- var cardCommand = new Command3("card").description(
8277
+ import { Command as Command4 } from "commander";
8278
+ var cardCommand = new Command4("card").description(
7402
8279
  "Manage spaced-repetition cards"
7403
8280
  );
7404
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) => {
@@ -7596,9 +8473,9 @@ cardCommand.command("delete").description("Delete one user's card for a token").
7596
8473
  });
7597
8474
 
7598
8475
  // src/cli/commands/connector.ts
7599
- import { input, password } from "@inquirer/prompts";
7600
- import { Command as Command4 } from "commander";
7601
- var connectorCommand = new Command4("connector").description(
8476
+ import { input as input2, password } from "@inquirer/prompts";
8477
+ import { Command as Command5 } from "commander";
8478
+ var connectorCommand = new Command5("connector").description(
7602
8479
  "Manage external service connectors"
7603
8480
  );
7604
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(
@@ -7617,10 +8494,10 @@ connectorCommand.command("setup").description("Configure a connector").argument(
7617
8494
  process.exit(1);
7618
8495
  }
7619
8496
  try {
7620
- const orgUrl = await input({
8497
+ const orgUrl = await input2({
7621
8498
  message: "Organization URL (e.g. https://dev.azure.com/myorg):"
7622
8499
  });
7623
- const project = await input({
8500
+ const project = await input2({
7624
8501
  message: "Project name:"
7625
8502
  });
7626
8503
  const pat = await password({
@@ -7711,7 +8588,7 @@ connectorCommand.command("sync").description("Verify the Turso cloud database co
7711
8588
  async function setupTurso(urlArg, tokenArg, mode) {
7712
8589
  let db;
7713
8590
  try {
7714
- const url = urlArg ?? await input({
8591
+ const url = urlArg ?? await input2({
7715
8592
  message: "Turso database URL (e.g. libsql://my-db-user.turso.io):"
7716
8593
  });
7717
8594
  const token = tokenArg ?? await password({
@@ -7740,27 +8617,27 @@ async function setupTurso(urlArg, tokenArg, mode) {
7740
8617
  }
7741
8618
 
7742
8619
  // src/cli/commands/git-sync.ts
7743
- import { execSync as execSync4 } from "child_process";
7744
- import { chmodSync as chmodSync2, existsSync as existsSync13, writeFileSync as writeFileSync6 } from "fs";
7745
- import { join as join14 } from "path";
7746
- import { Command as Command5 } from "commander";
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";
7747
8624
  function installHook2() {
7748
- const gitDir = join14(process.cwd(), ".git");
7749
- if (!existsSync13(gitDir)) {
8625
+ const gitDir = join17(process.cwd(), ".git");
8626
+ if (!existsSync16(gitDir)) {
7750
8627
  console.error(
7751
8628
  "Error: Current directory is not the root of a Git repository."
7752
8629
  );
7753
8630
  process.exit(1);
7754
8631
  }
7755
- const hooksDir = join14(gitDir, "hooks");
7756
- const hookPath = join14(hooksDir, "post-commit");
8632
+ const hooksDir = join17(gitDir, "hooks");
8633
+ const hookPath = join17(hooksDir, "post-commit");
7757
8634
  const hookContent = `#!/bin/sh
7758
8635
  # ZAM Spaced Repetition Auto-Stale Hook
7759
8636
  # Triggered automatically on git commits to decay modified concept cards.
7760
8637
  zam git-sync --commit HEAD --quiet
7761
8638
  `;
7762
8639
  try {
7763
- writeFileSync6(hookPath, hookContent, { encoding: "utf-8", flag: "w" });
8640
+ writeFileSync8(hookPath, hookContent, { encoding: "utf-8", flag: "w" });
7764
8641
  try {
7765
8642
  chmodSync2(hookPath, "755");
7766
8643
  } catch (_e) {
@@ -7773,7 +8650,7 @@ zam git-sync --commit HEAD --quiet
7773
8650
  process.exit(1);
7774
8651
  }
7775
8652
  }
7776
- var gitSyncCommand = new Command5("git-sync").description("Sync learning cards with recent Git file modifications").option("--commit <hash>", "Git commit hash to check", "HEAD").option("--user <id>", "User ID (default: whoami)").option("--install", "Install git post-commit hook in current repo").option("--quiet", "Suppress verbose output").action(async (opts) => {
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) => {
7777
8654
  if (opts.install) {
7778
8655
  installHook2();
7779
8656
  return;
@@ -7782,7 +8659,7 @@ var gitSyncCommand = new Command5("git-sync").description("Sync learning cards w
7782
8659
  const userId = await resolveUser(opts, db);
7783
8660
  let changedFiles = [];
7784
8661
  try {
7785
- const output = execSync4(
8662
+ const output = execSync5(
7786
8663
  `git diff-tree --no-commit-id --name-only -r ${opts.commit}`,
7787
8664
  {
7788
8665
  encoding: "utf-8",
@@ -7862,10 +8739,10 @@ ZAM Auto-Stale Complete: Scanned ${changedFiles.length} file(s).`
7862
8739
  });
7863
8740
 
7864
8741
  // src/cli/commands/goal.ts
7865
- import { existsSync as existsSync14, mkdirSync as mkdirSync8 } from "fs";
7866
- import { resolve as resolve3 } from "path";
7867
- import { input as input2 } from "@inquirer/prompts";
7868
- import { Command as Command6 } from "commander";
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";
7869
8746
  async function resolveGoalsDir() {
7870
8747
  let goalsDir;
7871
8748
  let db;
@@ -7876,9 +8753,9 @@ async function resolveGoalsDir() {
7876
8753
  } finally {
7877
8754
  await db?.close();
7878
8755
  }
7879
- return goalsDir ? resolve3(goalsDir) : resolve3("goals");
8756
+ return goalsDir ? resolve4(goalsDir) : resolve4("goals");
7880
8757
  }
7881
- var goalCommand = new Command6("goal").description(
8758
+ var goalCommand = new Command7("goal").description(
7882
8759
  "Manage learning goals (markdown files)"
7883
8760
  );
7884
8761
  goalCommand.command("list").description("List all goals").option(
@@ -7886,7 +8763,7 @@ goalCommand.command("list").description("List all goals").option(
7886
8763
  "Filter by status (active, completed, paused, abandoned)"
7887
8764
  ).option("--tree", "Show goals as a tree with parent/child relationships").option("--json", "Output as JSON").action(async (opts) => {
7888
8765
  const goalsDir = await resolveGoalsDir();
7889
- if (!existsSync14(goalsDir)) {
8766
+ if (!existsSync17(goalsDir)) {
7890
8767
  console.error(`Goals directory not found: ${goalsDir}`);
7891
8768
  console.error(
7892
8769
  "Set it with: zam settings set personal.goals_dir /path/to/goals"
@@ -7987,8 +8864,8 @@ ${"\u2500".repeat(50)}`);
7987
8864
  });
7988
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) => {
7989
8866
  const goalsDir = await resolveGoalsDir();
7990
- if (!existsSync14(goalsDir)) {
7991
- mkdirSync8(goalsDir, { recursive: true });
8867
+ if (!existsSync17(goalsDir)) {
8868
+ mkdirSync9(goalsDir, { recursive: true });
7992
8869
  }
7993
8870
  let slug = opts.slug;
7994
8871
  let title = opts.title;
@@ -7997,11 +8874,11 @@ goalCommand.command("create").description("Create a new goal").option("--slug <s
7997
8874
  if (!slug || !title) {
7998
8875
  try {
7999
8876
  if (!title) {
8000
- title = await input2({ message: "Goal title:" });
8877
+ title = await input3({ message: "Goal title:" });
8001
8878
  }
8002
8879
  if (!slug) {
8003
8880
  const suggested = title.toLowerCase().replace(/[^a-z0-9]+/g, "-").replace(/^-|-$/g, "");
8004
- slug = await input2({
8881
+ slug = await input3({
8005
8882
  message: "Goal slug (filename):",
8006
8883
  default: suggested
8007
8884
  });
@@ -8047,22 +8924,22 @@ goalCommand.command("status <slug> <status>").description("Update a goal's statu
8047
8924
  });
8048
8925
 
8049
8926
  // src/cli/commands/init.ts
8050
- import { existsSync as existsSync15, mkdirSync as mkdirSync9, writeFileSync as writeFileSync7 } from "fs";
8051
- import { homedir as homedir9 } from "os";
8052
- import { join as join15 } from "path";
8053
- import { confirm, input as input3 } from "@inquirer/prompts";
8054
- import { Command as Command7 } from "commander";
8055
- var HOME2 = homedir9();
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();
8056
8933
  function printLine(char = "\u2550", len = 60, color = "\x1B[36m") {
8057
8934
  console.log(`${color}${char.repeat(len)}\x1B[0m`);
8058
8935
  }
8059
8936
  function bootstrapSandboxWorkspace(workspaceDir) {
8060
- mkdirSync9(join15(workspaceDir, "beliefs"), { recursive: true });
8061
- mkdirSync9(join15(workspaceDir, "goals"), { recursive: true });
8062
- mkdirSync9(join15(workspaceDir, "skills"), { recursive: true });
8063
- const worldviewFile = join15(workspaceDir, "beliefs", "worldview.md");
8064
- if (!existsSync15(worldviewFile)) {
8065
- writeFileSync7(
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(
8066
8943
  worldviewFile,
8067
8944
  `# Personal Worldview
8068
8945
 
@@ -8074,9 +8951,9 @@ Here, I declare the core concepts and principles I want to master.
8074
8951
  "utf8"
8075
8952
  );
8076
8953
  }
8077
- const goalsFile = join15(workspaceDir, "goals", "goals.md");
8078
- if (!existsSync15(goalsFile)) {
8079
- writeFileSync7(
8954
+ const goalsFile = join18(workspaceDir, "goals", "goals.md");
8955
+ if (!existsSync18(goalsFile)) {
8956
+ writeFileSync9(
8080
8957
  goalsFile,
8081
8958
  `# Personal Goals
8082
8959
 
@@ -8088,7 +8965,7 @@ Here, I declare the core concepts and principles I want to master.
8088
8965
  );
8089
8966
  }
8090
8967
  }
8091
- 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 () => {
8092
8969
  printLine();
8093
8970
  console.log(
8094
8971
  "\x1B[1m\x1B[32m ZAM \u2014 The Symbiotic Learning Agent Onboarding\x1B[0m"
@@ -8098,8 +8975,8 @@ var initCommand = new Command7("init").description("Launch the guided interactiv
8098
8975
  );
8099
8976
  printLine();
8100
8977
  console.log("\n\x1B[1m[1/5] Setting up Local Workspace Sandbox\x1B[0m");
8101
- const defaultWorkspace = join15(HOME2, "Documents", "zam");
8102
- const workspacePath = await input3({
8978
+ const defaultWorkspace = join18(HOME2, "Documents", "zam");
8979
+ const workspacePath = await input4({
8103
8980
  message: "Choose your ZAM workspace directory:",
8104
8981
  default: defaultWorkspace
8105
8982
  });
@@ -8135,7 +9012,7 @@ var initCommand = new Command7("init").description("Launch the guided interactiv
8135
9012
  \x1B[1mRecommendation:\x1B[0m ZAM suggests installing \x1B[32m${runnerLabel}\x1B[0m with \x1B[36m${profile.recommendedModel}\x1B[0m.`
8136
9013
  );
8137
9014
  console.log("\n\x1B[1m[3/5] Setting up Local LLM Runner\x1B[0m");
8138
- const proceedInstall = await confirm({
9015
+ const proceedInstall = await confirm2({
8139
9016
  message: `Would you like ZAM to install and configure ${runnerLabel} automatically?`,
8140
9017
  default: true
8141
9018
  });
@@ -8203,7 +9080,7 @@ var initCommand = new Command7("init").description("Launch the guided interactiv
8203
9080
  console.log(
8204
9081
  "\n\x1B[1m[5/5] Wiring Developer Agents & Terminal Helpers\x1B[0m"
8205
9082
  );
8206
- const proceedHooks = await confirm({
9083
+ const proceedHooks = await confirm2({
8207
9084
  message: "Distribute ZAM skills and install optional monitored-session shell helpers?",
8208
9085
  default: true
8209
9086
  });
@@ -8254,8 +9131,8 @@ var initCommand = new Command7("init").description("Launch the guided interactiv
8254
9131
  });
8255
9132
 
8256
9133
  // src/cli/commands/learn.ts
8257
- import { input as input5 } from "@inquirer/prompts";
8258
- import { Command as Command8 } from "commander";
9134
+ import { input as input6 } from "@inquirer/prompts";
9135
+ import { Command as Command9 } from "commander";
8259
9136
 
8260
9137
  // src/cli/learn-format.ts
8261
9138
  var BLOOM_VERBS3 = {
@@ -8304,7 +9181,7 @@ function formatReveal(input8) {
8304
9181
  }
8305
9182
 
8306
9183
  // src/cli/review-actions.ts
8307
- import { confirm as confirm2, input as input4, select } from "@inquirer/prompts";
9184
+ import { confirm as confirm3, input as input5, select } from "@inquirer/prompts";
8308
9185
  async function runInteractiveReviewAction(inputData) {
8309
9186
  let currentItem = { ...inputData.item };
8310
9187
  while (true) {
@@ -8419,7 +9296,7 @@ async function runInteractiveReviewAction(inputData) {
8419
9296
  continue;
8420
9297
  }
8421
9298
  if (choice === "deprecate-token") {
8422
- const approved = await confirm2({
9299
+ const approved = await confirm3({
8423
9300
  message: `Deprecate ${currentItem.slug} so it stops appearing in review queues?`,
8424
9301
  default: false
8425
9302
  });
@@ -8450,7 +9327,7 @@ async function runInteractiveReviewAction(inputData) {
8450
9327
  console.log(` Session steps: ${impact.session_steps}`);
8451
9328
  console.log(` Sessions touched: ${impact.sessions_touched}`);
8452
9329
  console.log(` Agent skills updated: ${impact.agent_skills}`);
8453
- const approved = await confirm2({
9330
+ const approved = await confirm3({
8454
9331
  message: "Permanently delete this token and its dependent learning data?",
8455
9332
  default: false
8456
9333
  });
@@ -8475,7 +9352,7 @@ async function runInteractiveReviewAction(inputData) {
8475
9352
  );
8476
9353
  console.log(`Delete your card for ${currentItem.slug}?`);
8477
9354
  console.log(` Review logs removed: ${impact.review_logs}`);
8478
- const approved = await confirm2({
9355
+ const approved = await confirm3({
8479
9356
  message: "Delete only your card for this token?",
8480
9357
  default: false
8481
9358
  });
@@ -8513,21 +9390,21 @@ async function promptTokenEdit(token) {
8513
9390
  }
8514
9391
  switch (field) {
8515
9392
  case "concept": {
8516
- const concept = await input4({
9393
+ const concept = await input5({
8517
9394
  message: "Concept:",
8518
9395
  default: token.concept
8519
9396
  });
8520
9397
  return concept === token.concept ? null : { concept };
8521
9398
  }
8522
9399
  case "domain": {
8523
- const domain = await input4({
9400
+ const domain = await input5({
8524
9401
  message: "Domain (blank allowed):",
8525
9402
  default: token.domain
8526
9403
  });
8527
9404
  return domain === token.domain ? null : { domain };
8528
9405
  }
8529
9406
  case "context": {
8530
- const context = await input4({
9407
+ const context = await input5({
8531
9408
  message: "Context (blank allowed):",
8532
9409
  default: token.context
8533
9410
  });
@@ -8568,14 +9445,14 @@ async function promptTokenEdit(token) {
8568
9445
  return mode === token.symbiosis_mode ? null : { symbiosis_mode: mode };
8569
9446
  }
8570
9447
  case "source_link": {
8571
- const link = await input4({
9448
+ const link = await input5({
8572
9449
  message: "Source link (blank allowed):",
8573
9450
  default: token.source_link ?? ""
8574
9451
  });
8575
9452
  return link === token.source_link ? null : { source_link: link || null };
8576
9453
  }
8577
9454
  case "question": {
8578
- const question = await input4({
9455
+ const question = await input5({
8579
9456
  message: "Recall question (blank allowed):",
8580
9457
  default: token.question ?? ""
8581
9458
  });
@@ -8593,7 +9470,7 @@ var STOP_WORDS = /* @__PURE__ */ new Set(["q", ":q", "quit", "stop"]);
8593
9470
  function isExitPrompt(err) {
8594
9471
  return err instanceof Error && err.name === "ExitPromptError";
8595
9472
  }
8596
- var learnCommand = new Command8("learn").description(
9473
+ var learnCommand = new Command9("learn").description(
8597
9474
  "Run a spoiler-free, in-process learning session (recall \u2192 reveal \u2192 self-rate)"
8598
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) => {
8599
9476
  let db;
@@ -8673,7 +9550,7 @@ ${"\u2500".repeat(50)}`);
8673
9550
  ${prompt.question}`);
8674
9551
  let answer;
8675
9552
  try {
8676
- answer = await input5({
9553
+ answer = await input6({
8677
9554
  message: t(locale, "prompt_answer")
8678
9555
  });
8679
9556
  } catch (err) {
@@ -8793,36 +9670,52 @@ ${"\u2550".repeat(50)}`);
8793
9670
  process.exit(1);
8794
9671
  }
8795
9672
  });
8796
-
8797
- // src/cli/commands/monitor.ts
8798
- import { execFileSync as execFileSync3, execSync as execSync5 } from "child_process";
8799
- import { unlinkSync, writeFileSync as writeFileSync8 } from "fs";
8800
- import { tmpdir as tmpdir3 } from "os";
8801
- import { basename as basename3, join as join16 } from "path";
8802
- import { Command as Command9 } from "commander";
8803
- function isPowerShellShell(shell) {
8804
- return shell === "pwsh" || shell === "powershell";
9673
+ function buildLearnCommand(shell, userId) {
9674
+ const zamInvocation = resolveZamInvocation(shell);
9675
+ const learnArgs = userId ? ` learn --user ${userId}` : " learn";
9676
+ return `${zamInvocation}${learnArgs}`;
8805
9677
  }
8806
- function normalizeShell(shell) {
8807
- if (!shell) return detectShell();
8808
- const normalized = shell.toLowerCase();
8809
- if (normalized === "zsh" || normalized === "bash" || normalized === "pwsh" || normalized === "powershell") {
8810
- return normalized;
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);
8811
9688
  }
8812
- throw new Error(
8813
- `Unsupported shell: ${shell}. Expected zsh, bash, pwsh, or powershell.`
8814
- );
8815
- }
8816
- function psSingleQuoted2(value) {
8817
- return `'${value.replace(/'/g, "''")}'`;
8818
- }
8819
- function detectShell() {
8820
- if (process.platform === "win32")
8821
- return findExecutable("pwsh.exe") ? "pwsh" : "powershell";
8822
- const shell = process.env.SHELL ?? "";
8823
- return basename3(shell) === "bash" ? "bash" : "zsh";
8824
- }
8825
- var monitorCommand = new Command9("monitor").description(
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(
8826
9719
  "Shell observation for real-time task monitoring"
8827
9720
  );
8828
9721
  monitorCommand.command("start").description("Output shell hook code to install monitoring").requiredOption("--session <id>", "Session ID to monitor").option(
@@ -8949,39 +9842,6 @@ monitorCommand.command("status").description("Show monitoring status for a sessi
8949
9842
  console.log(` To: ${result.timeSpan.end}`);
8950
9843
  }
8951
9844
  });
8952
- function selectWindowsExecutable(results, pathext = process.env.PATHEXT ?? ".COM;.EXE;.BAT;.CMD") {
8953
- if (results.length === 0) return null;
8954
- const extensions = pathext.split(";").map((ext) => ext.trim().toLowerCase()).filter(Boolean);
8955
- const runnable = results.find(
8956
- (result) => extensions.some((ext) => result.toLowerCase().endsWith(ext))
8957
- );
8958
- return runnable ?? results[0];
8959
- }
8960
- function findExecutable(command) {
8961
- try {
8962
- const lookup = process.platform === "win32" ? `where.exe ${command}` : `command -v ${command}`;
8963
- const results = execSync5(lookup, { encoding: "utf-8" }).split(/\r?\n/).map((line) => line.trim()).filter(Boolean);
8964
- if (results.length === 0) return null;
8965
- if (process.platform === "win32") {
8966
- return selectWindowsExecutable(results);
8967
- }
8968
- return results[0];
8969
- } catch {
8970
- return null;
8971
- }
8972
- }
8973
- function resolveZamInvocation(shell) {
8974
- const installed = findExecutable("zam");
8975
- if (installed) {
8976
- return isPowerShellShell(shell) ? `& ${psSingleQuoted2(installed)}` : installed;
8977
- }
8978
- const projectRoot = join16(import.meta.dirname, "..", "..", "..");
8979
- const cliSource = join16(projectRoot, "src/cli/index.ts");
8980
- if (isPowerShellShell(shell)) {
8981
- return `& npx --prefix ${psSingleQuoted2(projectRoot)} tsx ${psSingleQuoted2(cliSource)}`;
8982
- }
8983
- return `npx --prefix ${JSON.stringify(projectRoot)} tsx ${JSON.stringify(cliSource)}`;
8984
- }
8985
9845
  function buildMonitorSetupCommand(dir, sessionId, shell) {
8986
9846
  const zamInvocation = resolveZamInvocation(shell);
8987
9847
  if (isPowerShellShell(shell)) {
@@ -8994,17 +9854,6 @@ function buildMonitorSetupCommand(dir, sessionId, shell) {
8994
9854
  }
8995
9855
  return `cd ${JSON.stringify(dir)} && eval "$(${zamInvocation} monitor start --session ${sessionId} --shell ${shell})"`;
8996
9856
  }
8997
- function isItermRunning() {
8998
- try {
8999
- const result = execSync5(
9000
- `osascript -e 'tell application "System Events" to (name of processes) contains "iTerm2"' 2>/dev/null`,
9001
- { encoding: "utf-8" }
9002
- ).trim();
9003
- return result === "true";
9004
- } catch {
9005
- return false;
9006
- }
9007
- }
9008
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(
9009
9858
  "--shell <type>",
9010
9859
  "Shell type: zsh | bash | pwsh | powershell (auto-detected)"
@@ -9037,89 +9886,19 @@ monitorCommand.command("open").description("Open a new monitored terminal window
9037
9886
  } finally {
9038
9887
  await db?.close();
9039
9888
  }
9040
- const dir = opts.dir ?? process.cwd();
9041
- const shellSetup = buildMonitorSetupCommand(dir, opts.session, shell);
9042
- if (process.platform === "darwin" && !isPowerShellShell(shell)) {
9043
- openMacTerminal(shellSetup, opts.session, dir);
9044
- } else if (process.platform === "win32" && isPowerShellShell(shell)) {
9045
- openWindowsPowerShell(shellSetup, opts.session, dir, shell);
9046
- } else {
9047
- console.log(`Run this in a new terminal:
9048
- `);
9049
- console.log(` ${shellSetup}
9050
- `);
9051
- console.log(
9052
- `(Automatic terminal opening is only supported on macOS Terminal/iTerm2 and Windows PowerShell for now.)`
9053
- );
9054
- }
9055
- });
9056
- function openMacTerminal(shellSetup, sessionId, dir) {
9057
- const useIterm = isItermRunning();
9058
- const escaped = shellSetup.replace(/\\/g, "\\\\").replace(/"/g, '\\"');
9059
- const appleScript = useIterm ? `tell application "iTerm2"
9060
- activate
9061
- set newWindow to (create window with default profile)
9062
- tell current session of newWindow
9063
- write text "${escaped}"
9064
- end tell
9065
- end tell` : `tell application "Terminal"
9066
- activate
9067
- do script "${escaped}"
9068
- end tell`;
9069
- const tmpFile = join16(tmpdir3(), `zam-monitor-${sessionId}.scpt`);
9070
- try {
9071
- writeFileSync8(tmpFile, appleScript);
9072
- execSync5(`osascript ${JSON.stringify(tmpFile)}`, { stdio: "ignore" });
9073
- console.log(
9074
- `Opened ${useIterm ? "iTerm2" : "Terminal.app"} window with monitoring for session ${sessionId}`
9075
- );
9076
- console.log(` Directory: ${dir}`);
9077
- } catch (err) {
9078
- console.error(`Failed to open terminal: ${err.message}`);
9079
- console.log(`
9080
- Run this manually in a new terminal:
9081
- `);
9082
- console.log(` ${shellSetup}`);
9083
- } finally {
9084
- try {
9085
- unlinkSync(tmpFile);
9086
- } catch {
9087
- }
9088
- }
9089
- }
9090
- function openWindowsPowerShell(shellSetup, sessionId, dir, requestedShell) {
9091
- const requestedExecutable = requestedShell === "powershell" ? "powershell.exe" : "pwsh.exe";
9092
- const executable = findExecutable(requestedExecutable) ? requestedExecutable : "powershell.exe";
9093
- const startCommand = [
9094
- "Start-Process",
9095
- `-FilePath ${psSingleQuoted2(executable)}`,
9096
- `-ArgumentList @('-NoExit','-NoProfile','-Command',${psSingleQuoted2(shellSetup)})`
9097
- ].join(" ");
9098
- const launcher = findExecutable("pwsh.exe") ? "pwsh.exe" : "powershell.exe";
9099
- try {
9100
- execFileSync3(
9101
- launcher,
9102
- ["-NoProfile", "-ExecutionPolicy", "Bypass", "-Command", startCommand],
9103
- {
9104
- stdio: "ignore"
9105
- }
9106
- );
9107
- console.log(
9108
- `Opened ${executable === "pwsh.exe" ? "PowerShell" : "Windows PowerShell"} window with monitoring for session ${sessionId}`
9109
- );
9110
- console.log(` Directory: ${dir}`);
9111
- } catch (err) {
9112
- console.error(`Failed to open PowerShell: ${err.message}`);
9113
- console.log(`
9114
- Run this manually in a new PowerShell terminal:
9115
- `);
9116
- console.log(` ${shellSetup}`);
9117
- }
9118
- }
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
+ });
9119
9898
 
9120
9899
  // src/cli/commands/observer.ts
9121
- import { Command as Command10 } from "commander";
9122
- var observerCommand = new Command10("observer").description(
9900
+ import { Command as Command11 } from "commander";
9901
+ var observerCommand = new Command11("observer").description(
9123
9902
  "Configure what the UI observer may capture (Layer 2 policy)"
9124
9903
  );
9125
9904
  function applyObserverListChange(current, entry, op) {
@@ -9181,9 +9960,9 @@ observerCommand.command("revoke <process>").description("Remove a process from o
9181
9960
  });
9182
9961
 
9183
9962
  // src/cli/commands/profile.ts
9184
- import { homedir as homedir10 } from "os";
9185
- import { dirname as dirname5, join as join17, resolve as resolve4 } from "path";
9186
- import { Command as Command11 } from "commander";
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";
9187
9966
  var C2 = {
9188
9967
  reset: "\x1B[0m",
9189
9968
  bold: "\x1B[1m",
@@ -9192,7 +9971,7 @@ var C2 = {
9192
9971
  green: "\x1B[32m"
9193
9972
  };
9194
9973
  function defaultPersonalDir() {
9195
- return join17(homedir10(), "Documents", "zam");
9974
+ return join19(homedir12(), "Documents", "zam");
9196
9975
  }
9197
9976
  function render(profile) {
9198
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}`;
@@ -9203,7 +9982,7 @@ function render(profile) {
9203
9982
  console.log(` data dir: ${C2.cyan}${profile.dataDir}${C2.reset}`);
9204
9983
  console.log(` database: ${C2.cyan}${profile.dbPath}${C2.reset}`);
9205
9984
  }
9206
- var profileCommand = new Command11("profile").description("Show or change this machine's ZAM install profile").option("--mode <mode>", "Set install mode: developer | default").option("--dir <path>", "Set the personal-content folder").option("--json", "Output as JSON").action(async (opts) => {
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) => {
9207
9986
  if (opts.mode && opts.mode !== "developer" && opts.mode !== "default") {
9208
9987
  console.error(`Invalid --mode: ${opts.mode}. Use developer or default.`);
9209
9988
  process.exit(1);
@@ -9213,7 +9992,7 @@ var profileCommand = new Command11("profile").description("Show or change this m
9213
9992
  if (opts.mode) setInstallMode(opts.mode);
9214
9993
  db = await openDatabaseWithSync({ initialize: true });
9215
9994
  if (opts.dir) {
9216
- await setSetting(db, "personal.workspace_dir", resolve4(opts.dir));
9995
+ await setSetting(db, "personal.workspace_dir", resolve5(opts.dir));
9217
9996
  }
9218
9997
  const personalDir = await getSetting(db, "personal.workspace_dir") || defaultPersonalDir();
9219
9998
  await db.close();
@@ -9238,9 +10017,276 @@ var profileCommand = new Command11("profile").description("Show or change this m
9238
10017
  }
9239
10018
  });
9240
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
+
9241
10287
  // src/cli/commands/review.ts
9242
- import { Command as Command12 } from "commander";
9243
- var reviewCommand = new Command12("review").description("Start an interactive review session").option("--user <id>", "User ID (default: whoami)").option("--max-new <n>", "Maximum new cards", "10").option("--max-reviews <n>", "Maximum review cards", "50").option("--no-resolve", "Skip resolving source_link into inline context").action(async (opts) => {
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) => {
9244
10290
  let db;
9245
10291
  try {
9246
10292
  db = await openDatabase();
@@ -9354,9 +10400,9 @@ ${"\u2550".repeat(50)}`);
9354
10400
 
9355
10401
  // src/cli/commands/session.ts
9356
10402
  import { readFileSync as readFileSync11 } from "fs";
9357
- import { input as input6, select as select2 } from "@inquirer/prompts";
9358
- import { Command as Command13 } from "commander";
9359
- var sessionCommand = new Command13("session").description(
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(
9360
10406
  "Manage learning sessions"
9361
10407
  );
9362
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(
@@ -9533,7 +10579,7 @@ async function selectTask() {
9533
10579
  console.log("No active work items found in Azure DevOps.");
9534
10580
  }
9535
10581
  }
9536
- return input6({ message: "Task description:" });
10582
+ return input7({ message: "Task description:" });
9537
10583
  }
9538
10584
  var RATING_LABELS = {
9539
10585
  1: "Again",
@@ -9735,9 +10781,9 @@ sessionCommand.command("end").description("End a session and show summary").requ
9735
10781
  });
9736
10782
 
9737
10783
  // src/cli/commands/settings.ts
9738
- import { existsSync as existsSync16 } from "fs";
9739
- import { Command as Command14 } from "commander";
9740
- var settingsCommand = new Command14("settings").description(
10784
+ import { existsSync as existsSync19 } from "fs";
10785
+ import { Command as Command16 } from "commander";
10786
+ var settingsCommand = new Command16("settings").description(
9741
10787
  "Manage user settings"
9742
10788
  );
9743
10789
  var BOOLEAN_SETTING_KEYS = /* @__PURE__ */ new Set(["llm.enabled", "llm.vision.enabled"]);
@@ -9900,7 +10946,7 @@ settingsCommand.command("repos").description("Show or set Personal, Team, and Or
9900
10946
  console.log("\nValidation:");
9901
10947
  for (const [name, path] of Object.entries(paths)) {
9902
10948
  if (path) {
9903
- const exists = existsSync16(path);
10949
+ const exists = existsSync19(path);
9904
10950
  console.log(
9905
10951
  ` ${name.padEnd(9)}: ${exists ? "\x1B[32m\u2713 Valid folder\x1B[0m" : "\x1B[31m\u2717 Directory does not exist\x1B[0m"}`
9906
10952
  );
@@ -9912,41 +10958,41 @@ settingsCommand.command("repos").description("Show or set Personal, Team, and Or
9912
10958
  });
9913
10959
 
9914
10960
  // src/cli/commands/setup.ts
9915
- import { copyFileSync as copyFileSync2, existsSync as existsSync17, mkdirSync as mkdirSync10, writeFileSync as writeFileSync9 } from "fs";
9916
- import { basename as basename4, dirname as dirname6, join as join18 } from "path";
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";
9917
10963
  import { fileURLToPath as fileURLToPath2 } from "url";
9918
- import { Command as Command15 } from "commander";
10964
+ import { Command as Command17 } from "commander";
9919
10965
  var packageRoot = [
9920
10966
  fileURLToPath2(new URL("../..", import.meta.url)),
9921
10967
  fileURLToPath2(new URL("../../..", import.meta.url))
9922
- ].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));
9923
10969
  var SKILL_PAIRS = [
9924
10970
  {
9925
- from: join18(packageRoot, ".claude", "skills", "zam", "SKILL.md"),
9926
- to: join18(".claude", "skills", "zam", "SKILL.md")
10971
+ from: join20(packageRoot, ".claude", "skills", "zam", "SKILL.md"),
10972
+ to: join20(".claude", "skills", "zam", "SKILL.md")
9927
10973
  },
9928
10974
  {
9929
- from: join18(packageRoot, ".agent", "skills", "zam", "SKILL.md"),
9930
- to: join18(".agent", "skills", "zam", "SKILL.md")
10975
+ from: join20(packageRoot, ".agent", "skills", "zam", "SKILL.md"),
10976
+ to: join20(".agent", "skills", "zam", "SKILL.md")
9931
10977
  },
9932
10978
  {
9933
- from: join18(packageRoot, ".agents", "skills", "zam", "SKILL.md"),
9934
- to: join18(".agents", "skills", "zam", "SKILL.md")
10979
+ from: join20(packageRoot, ".agents", "skills", "zam", "SKILL.md"),
10980
+ to: join20(".agents", "skills", "zam", "SKILL.md")
9935
10981
  }
9936
10982
  ];
9937
10983
  function copySkills(force, cwd = process.cwd()) {
9938
10984
  let anyAction = false;
9939
10985
  for (const { from, to } of SKILL_PAIRS) {
9940
- const dest = join18(cwd, to);
9941
- if (!existsSync17(from)) {
10986
+ const dest = join20(cwd, to);
10987
+ if (!existsSync20(from)) {
9942
10988
  console.warn(` warn source not found, skipping: ${from}`);
9943
10989
  continue;
9944
10990
  }
9945
- if (existsSync17(dest) && !force) {
10991
+ if (existsSync20(dest) && !force) {
9946
10992
  console.log(` skip ${to} (already present \xE2\u20AC\u201D use --force to update)`);
9947
10993
  continue;
9948
10994
  }
9949
- mkdirSync10(dirname6(dest), { recursive: true });
10995
+ mkdirSync11(dirname6(dest), { recursive: true });
9950
10996
  copyFileSync2(from, dest);
9951
10997
  console.log(` copy ${to}`);
9952
10998
  anyAction = true;
@@ -9987,13 +11033,13 @@ async function initDatabase(skipInit) {
9987
11033
  }
9988
11034
  function writeClaudeMd(skipClaudeMd, cwd = process.cwd()) {
9989
11035
  if (skipClaudeMd) return;
9990
- const dest = join18(cwd, "CLAUDE.md");
9991
- if (existsSync17(dest)) {
11036
+ const dest = join20(cwd, "CLAUDE.md");
11037
+ if (existsSync20(dest)) {
9992
11038
  console.log(` skip CLAUDE.md (already present)`);
9993
11039
  return;
9994
11040
  }
9995
11041
  const name = basename4(cwd);
9996
- writeFileSync9(
11042
+ writeFileSync10(
9997
11043
  dest,
9998
11044
  `# ZAM Personal Kernel \xE2\u20AC\u201D ${name}
9999
11045
 
@@ -10021,13 +11067,13 @@ Use \`zam connector setup turso\` to store cloud credentials in
10021
11067
  }
10022
11068
  function writeAgentsMd(skipAgentsMd, cwd = process.cwd()) {
10023
11069
  if (skipAgentsMd) return;
10024
- const dest = join18(cwd, "AGENTS.md");
10025
- if (existsSync17(dest)) {
11070
+ const dest = join20(cwd, "AGENTS.md");
11071
+ if (existsSync20(dest)) {
10026
11072
  console.log(` skip AGENTS.md (already present)`);
10027
11073
  return;
10028
11074
  }
10029
11075
  const name = basename4(cwd);
10030
- writeFileSync9(
11076
+ writeFileSync10(
10031
11077
  dest,
10032
11078
  `# ZAM Personal Kernel - ${name}
10033
11079
 
@@ -10060,7 +11106,7 @@ Codex discovers repository skills under \`.agents/skills/\`. Run
10060
11106
  );
10061
11107
  console.log(` write AGENTS.md`);
10062
11108
  }
10063
- var setupCommand = new Command15("setup").description(
11109
+ var setupCommand = new Command17("setup").description(
10064
11110
  "Distribute ZAM skill files into this personal instance and initialize the database"
10065
11111
  ).option(
10066
11112
  "--force",
@@ -10081,8 +11127,8 @@ var setupCommand = new Command15("setup").description(
10081
11127
  );
10082
11128
 
10083
11129
  // src/cli/commands/skill.ts
10084
- import { Command as Command16 } from "commander";
10085
- var skillCommand = new Command16("skill").description(
11130
+ import { Command as Command18 } from "commander";
11131
+ var skillCommand = new Command18("skill").description(
10086
11132
  "Manage agent skill entries (task recipes)"
10087
11133
  );
10088
11134
  skillCommand.command("list").description("List all agent skills").option("--json", "Output as JSON").action(async (opts) => {
@@ -10167,10 +11213,10 @@ skillCommand.command("add").description("Register a new agent skill").requiredOp
10167
11213
  });
10168
11214
 
10169
11215
  // src/cli/commands/snapshot.ts
10170
- import { existsSync as existsSync18, mkdirSync as mkdirSync11, readFileSync as readFileSync12, writeFileSync as writeFileSync10 } from "fs";
10171
- import { homedir as homedir11 } from "os";
10172
- import { dirname as dirname7, join as join19 } from "path";
10173
- import { Command as Command17 } from "commander";
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";
10174
11220
  function defaultOutName() {
10175
11221
  const stamp = (/* @__PURE__ */ new Date()).toISOString().replace(/\.\d+Z$/, "").replace(/:/g, "-");
10176
11222
  return `zam-snapshot-${stamp}.sql`;
@@ -10184,12 +11230,12 @@ function summarize(tables) {
10184
11230
  }
10185
11231
  return { total, nonEmpty };
10186
11232
  }
10187
- var exportCmd = new Command17("export").description("Write a portable SQL-text snapshot of the database").option("--out <file>", "Output file (use - for stdout)").action(async (opts) => {
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) => {
10188
11234
  let db;
10189
11235
  try {
10190
11236
  db = await openDatabaseWithSync({ initialize: true });
10191
11237
  const snapshot = await exportSnapshot(db);
10192
- 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");
10193
11239
  await db.close();
10194
11240
  db = void 0;
10195
11241
  const manifest = verifySnapshot(snapshot);
@@ -10198,12 +11244,12 @@ var exportCmd = new Command17("export").description("Write a portable SQL-text s
10198
11244
  process.stdout.write(snapshot);
10199
11245
  return;
10200
11246
  }
10201
- const out = opts.out ?? join19(personalDir, "snapshots", defaultOutName());
11247
+ const out = opts.out ?? join21(personalDir, "snapshots", defaultOutName());
10202
11248
  const dir = dirname7(out);
10203
- if (dir && dir !== "." && !existsSync18(dir)) {
10204
- mkdirSync11(dir, { recursive: true });
11249
+ if (dir && dir !== "." && !existsSync21(dir)) {
11250
+ mkdirSync12(dir, { recursive: true });
10205
11251
  }
10206
- writeFileSync10(out, snapshot, "utf-8");
11252
+ writeFileSync11(out, snapshot, "utf-8");
10207
11253
  console.log(`Snapshot written: ${out}`);
10208
11254
  console.log(
10209
11255
  ` ${total} row(s)${nonEmpty.length ? ` \u2014 ${nonEmpty.join(", ")}` : ""}`
@@ -10214,10 +11260,10 @@ var exportCmd = new Command17("export").description("Write a portable SQL-text s
10214
11260
  process.exit(1);
10215
11261
  }
10216
11262
  });
10217
- var importCmd = new Command17("import").description("Restore a snapshot into the database").argument("<file>", "Snapshot file to restore").option("--force", "Overwrite a non-empty database", false).action(async (file, opts) => {
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) => {
10218
11264
  let db;
10219
11265
  try {
10220
- if (!existsSync18(file)) {
11266
+ if (!existsSync21(file)) {
10221
11267
  console.error(`Error: Snapshot file not found: ${file}`);
10222
11268
  process.exit(1);
10223
11269
  }
@@ -10237,9 +11283,9 @@ var importCmd = new Command17("import").description("Restore a snapshot into the
10237
11283
  process.exit(1);
10238
11284
  }
10239
11285
  });
10240
- 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) => {
10241
11287
  try {
10242
- if (!existsSync18(file)) {
11288
+ if (!existsSync21(file)) {
10243
11289
  console.error(`Error: Snapshot file not found: ${file}`);
10244
11290
  process.exit(1);
10245
11291
  }
@@ -10255,11 +11301,11 @@ var verifyCmd = new Command17("verify").description("Check a snapshot's manifest
10255
11301
  process.exit(1);
10256
11302
  }
10257
11303
  });
10258
- 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);
10259
11305
 
10260
11306
  // src/cli/commands/stats.ts
10261
- import { Command as Command18 } from "commander";
10262
- var statsCommand = new Command18("stats").description("Show learning dashboard for a user").option("--user <id>", "User ID (default: whoami)").option("--json", "Output as JSON").action(async (opts) => {
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) => {
10263
11309
  await withDb(async (db) => {
10264
11310
  const userId = await resolveUser(opts, db);
10265
11311
  const stats = await getUserStats(db, userId);
@@ -10295,8 +11341,8 @@ var statsCommand = new Command18("stats").description("Show learning dashboard f
10295
11341
  });
10296
11342
 
10297
11343
  // src/cli/commands/token.ts
10298
- import { Command as Command19 } from "commander";
10299
- var tokenCommand = new Command19("token").description(
11344
+ import { Command as Command21 } from "commander";
11345
+ var tokenCommand = new Command21("token").description(
10300
11346
  "Manage knowledge tokens"
10301
11347
  );
10302
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) => {
@@ -10582,12 +11628,12 @@ tokenCommand.command("status").description("Show full status of a token for a us
10582
11628
  });
10583
11629
 
10584
11630
  // src/cli/commands/ui.ts
10585
- import { spawn as spawn2, spawnSync } from "child_process";
10586
- import { existsSync as existsSync19 } from "fs";
10587
- import { homedir as homedir12 } from "os";
10588
- import { dirname as dirname8, join as join20 } from "path";
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";
10589
11635
  import { fileURLToPath as fileURLToPath3 } from "url";
10590
- import { Command as Command20 } from "commander";
11636
+ import { Command as Command22 } from "commander";
10591
11637
  var C3 = {
10592
11638
  reset: "\x1B[0m",
10593
11639
  red: "\x1B[31m",
@@ -10601,8 +11647,8 @@ function findDesktopDir() {
10601
11647
  for (const start of starts) {
10602
11648
  let dir = start;
10603
11649
  for (let i = 0; i < 10; i++) {
10604
- if (existsSync19(join20(dir, "desktop", "src-tauri", "tauri.conf.json"))) {
10605
- return join20(dir, "desktop");
11650
+ if (existsSync22(join22(dir, "desktop", "src-tauri", "tauri.conf.json"))) {
11651
+ return join22(dir, "desktop");
10606
11652
  }
10607
11653
  const parent = dirname8(dir);
10608
11654
  if (parent === dir) break;
@@ -10612,30 +11658,30 @@ function findDesktopDir() {
10612
11658
  return null;
10613
11659
  }
10614
11660
  function findBuiltApp(desktopDir) {
10615
- const releaseDir = join20(desktopDir, "src-tauri", "target", "release");
11661
+ const releaseDir = join22(desktopDir, "src-tauri", "target", "release");
10616
11662
  if (process.platform === "win32") {
10617
11663
  for (const name of ["ZAM.exe", "zam.exe", "zam-desktop.exe"]) {
10618
- const p = join20(releaseDir, name);
10619
- if (existsSync19(p)) return p;
11664
+ const p = join22(releaseDir, name);
11665
+ if (existsSync22(p)) return p;
10620
11666
  }
10621
11667
  } else if (process.platform === "darwin") {
10622
- const app = join20(releaseDir, "bundle", "macos", "ZAM.app");
10623
- if (existsSync19(app)) return app;
11668
+ const app = join22(releaseDir, "bundle", "macos", "ZAM.app");
11669
+ if (existsSync22(app)) return app;
10624
11670
  } else {
10625
11671
  for (const name of ["zam", "ZAM", "zam-desktop"]) {
10626
- const p = join20(releaseDir, name);
10627
- if (existsSync19(p)) return p;
11672
+ const p = join22(releaseDir, name);
11673
+ if (existsSync22(p)) return p;
10628
11674
  }
10629
11675
  }
10630
11676
  return null;
10631
11677
  }
10632
11678
  function findInstalledApp() {
10633
11679
  const candidates = process.platform === "win32" ? [
10634
- process.env.LOCALAPPDATA && join20(process.env.LOCALAPPDATA, "Programs", "ZAM", "ZAM.exe"),
10635
- process.env.ProgramFiles && join20(process.env.ProgramFiles, "ZAM", "ZAM.exe"),
10636
- process.env["ProgramFiles(x86)"] && join20(process.env["ProgramFiles(x86)"], "ZAM", "ZAM.exe")
10637
- ] : process.platform === "darwin" ? ["/Applications/ZAM.app", join20(homedir12(), "Applications", "ZAM.app")] : ["/opt/ZAM/zam", "/usr/bin/zam-desktop"];
10638
- return candidates.find((candidate) => candidate && existsSync19(candidate)) || null;
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;
10639
11685
  }
10640
11686
  function runNpm(args, opts) {
10641
11687
  const res = spawnSync("npm", args, {
@@ -10646,7 +11692,7 @@ function runNpm(args, opts) {
10646
11692
  return res.status ?? 1;
10647
11693
  }
10648
11694
  function ensureDesktopDeps(desktopDir) {
10649
- if (existsSync19(join20(desktopDir, "node_modules"))) return true;
11695
+ if (existsSync22(join22(desktopDir, "node_modules"))) return true;
10650
11696
  console.log(
10651
11697
  `${C3.cyan}Installing desktop dependencies (one-time)...${C3.reset}`
10652
11698
  );
@@ -10672,13 +11718,13 @@ function requireRust() {
10672
11718
  function hasMsvcBuildTools() {
10673
11719
  if (process.platform !== "win32") return true;
10674
11720
  const pf86 = process.env["ProgramFiles(x86)"] || "C:\\Program Files (x86)";
10675
- const vswhere = join20(
11721
+ const vswhere = join22(
10676
11722
  pf86,
10677
11723
  "Microsoft Visual Studio",
10678
11724
  "Installer",
10679
11725
  "vswhere.exe"
10680
11726
  );
10681
- if (!existsSync19(vswhere)) return false;
11727
+ if (!existsSync22(vswhere)) return false;
10682
11728
  const res = spawnSync(
10683
11729
  vswhere,
10684
11730
  [
@@ -10710,7 +11756,7 @@ function requireMsvcOnWindows() {
10710
11756
  return false;
10711
11757
  }
10712
11758
  function warnIfCliMissing(repoRoot) {
10713
- if (!existsSync19(join20(repoRoot, "dist", "cli", "index.js"))) {
11759
+ if (!existsSync22(join22(repoRoot, "dist", "cli", "index.js"))) {
10714
11760
  console.warn(
10715
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}`
10716
11762
  );
@@ -10719,13 +11765,13 @@ function warnIfCliMissing(repoRoot) {
10719
11765
  function launchApp(appPath, workingDir) {
10720
11766
  console.log(`${C3.green}\u2713 Launching ZAM Desktop...${C3.reset}`);
10721
11767
  if (process.platform === "darwin" && appPath.endsWith(".app")) {
10722
- spawn2("open", [appPath], {
11768
+ spawn3("open", [appPath], {
10723
11769
  cwd: workingDir,
10724
11770
  detached: true,
10725
11771
  stdio: "ignore"
10726
11772
  }).unref();
10727
11773
  } else {
10728
- spawn2(appPath, [], {
11774
+ spawn3(appPath, [], {
10729
11775
  cwd: workingDir,
10730
11776
  detached: true,
10731
11777
  stdio: "ignore",
@@ -10767,13 +11813,13 @@ function createShortcuts(appPath, repoRoot) {
10767
11813
  console.error(`${C3.red}\u2717 Could not create shortcuts.${C3.reset}`);
10768
11814
  }
10769
11815
  }
10770
- var uiCommand = new Command20("ui").description("Launch the ZAM Desktop GUI (Active-Recall Studio)").option("--dev", "Run in hot-reload development mode (needs Rust)").option(
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(
10771
11817
  "--build",
10772
11818
  "Build the native installer for your OS (needs Rust, one-time)"
10773
11819
  ).option("--shortcut", "Create Desktop + Start-menu shortcuts to the GUI").action((opts) => {
10774
11820
  const installedApp = findInstalledApp();
10775
11821
  if (!opts.dev && !opts.build && !opts.shortcut && installedApp) {
10776
- launchApp(installedApp, homedir12());
11822
+ launchApp(installedApp, homedir14());
10777
11823
  return;
10778
11824
  }
10779
11825
  const desktopDir = findDesktopDir();
@@ -10794,7 +11840,7 @@ var uiCommand = new Command20("ui").description("Launch the ZAM Desktop GUI (Act
10794
11840
  );
10795
11841
  const code = runNpm(["run", "tauri", "build"], { cwd: desktopDir });
10796
11842
  if (code === 0) {
10797
- const bundle = join20(
11843
+ const bundle = join22(
10798
11844
  desktopDir,
10799
11845
  "src-tauri",
10800
11846
  "target",
@@ -10871,11 +11917,11 @@ ${C3.dim}After that, 'zam ui' launches it directly.${C3.reset}`
10871
11917
 
10872
11918
  // src/cli/commands/update.ts
10873
11919
  import { spawnSync as spawnSync2 } from "child_process";
10874
- import { existsSync as existsSync20, readFileSync as readFileSync13, realpathSync } from "fs";
10875
- import { dirname as dirname9, join as join21 } from "path";
11920
+ import { existsSync as existsSync23, readFileSync as readFileSync13, realpathSync } from "fs";
11921
+ import { dirname as dirname9, join as join23 } from "path";
10876
11922
  import { fileURLToPath as fileURLToPath4 } from "url";
10877
- import { confirm as confirm3 } from "@inquirer/prompts";
10878
- import { Command as Command21 } from "commander";
11923
+ import { confirm as confirm4 } from "@inquirer/prompts";
11924
+ import { Command as Command23 } from "commander";
10879
11925
  var GITHUB_REPO = "zam-os/zam";
10880
11926
  var CHANNELS = [
10881
11927
  "developer",
@@ -10897,7 +11943,7 @@ function currentVersion() {
10897
11943
  for (const up of ["..", "../..", "../../.."]) {
10898
11944
  try {
10899
11945
  const pkg2 = JSON.parse(
10900
- readFileSync13(join21(here, up, "package.json"), "utf-8")
11946
+ readFileSync13(join23(here, up, "package.json"), "utf-8")
10901
11947
  );
10902
11948
  if (pkg2.version) return pkg2.version;
10903
11949
  } catch {
@@ -10908,7 +11954,7 @@ function currentVersion() {
10908
11954
  function versionAt(dir) {
10909
11955
  try {
10910
11956
  const pkg2 = JSON.parse(
10911
- readFileSync13(join21(dir, "package.json"), "utf-8")
11957
+ readFileSync13(join23(dir, "package.json"), "utf-8")
10912
11958
  );
10913
11959
  return pkg2.version ?? "unknown";
10914
11960
  } catch {
@@ -10953,7 +11999,7 @@ function render2(decision) {
10953
11999
  );
10954
12000
  }
10955
12001
  }
10956
- 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(
10957
12003
  "--latest <version>",
10958
12004
  "Compare against this version instead of fetching"
10959
12005
  ).option("--channel <channel>", "Override the detected install channel").option("--json", "Output as JSON").action(
@@ -10988,13 +12034,13 @@ function findSourceRepo() {
10988
12034
  let dir = realpathSync(dirname9(fileURLToPath4(import.meta.url)));
10989
12035
  let parent = dirname9(dir);
10990
12036
  while (parent !== dir) {
10991
- if (existsSync20(join21(dir, ".git"))) return dir;
12037
+ if (existsSync23(join23(dir, ".git"))) return dir;
10992
12038
  dir = parent;
10993
12039
  parent = dirname9(dir);
10994
12040
  }
10995
- return existsSync20(join21(dir, ".git")) ? dir : null;
12041
+ return existsSync23(join23(dir, ".git")) ? dir : null;
10996
12042
  }
10997
- function runGit(cwd, args, capture) {
12043
+ function runGit2(cwd, args, capture) {
10998
12044
  const res = spawnSync2("git", args, {
10999
12045
  cwd,
11000
12046
  stdio: capture ? ["ignore", "pipe", "pipe"] : "inherit",
@@ -11026,7 +12072,7 @@ function applyDeveloperUpdate(force) {
11026
12072
  );
11027
12073
  process.exit(1);
11028
12074
  }
11029
- const status = runGit(src, ["status", "--porcelain"], true);
12075
+ const status = runGit2(src, ["status", "--porcelain"], true);
11030
12076
  if (status.ok && status.out && !force) {
11031
12077
  console.error(
11032
12078
  `${C4.red}\u2717${C4.reset} The source checkout has uncommitted changes:
@@ -11038,7 +12084,7 @@ Commit or stash them, or re-run with ${C4.cyan}--force${C4.reset}.`
11038
12084
  console.log(
11039
12085
  `${C4.dim}\u2192 git pull --ff-only${C4.reset} ${C4.dim}(${src})${C4.reset}`
11040
12086
  );
11041
- if (!runGit(src, ["pull", "--ff-only"], false).ok) {
12087
+ if (!runGit2(src, ["pull", "--ff-only"], false).ok) {
11042
12088
  console.error(
11043
12089
  `${C4.red}\u2717${C4.reset} git pull failed \u2014 the branch may have diverged or you are offline. Resolve it manually, then retry.`
11044
12090
  );
@@ -11057,7 +12103,7 @@ Commit or stash them, or re-run with ${C4.cyan}--force${C4.reset}.`
11057
12103
  console.log(`${C4.dim}\u2192 zam setup --force${C4.reset}`);
11058
12104
  const setup = spawnSync2(
11059
12105
  process.execPath,
11060
- [join21(src, "dist", "cli", "index.js"), "setup", "--force"],
12106
+ [join23(src, "dist", "cli", "index.js"), "setup", "--force"],
11061
12107
  { cwd: process.cwd(), stdio: "inherit" }
11062
12108
  );
11063
12109
  if (setup.status !== 0) {
@@ -11100,7 +12146,7 @@ ${C4.dim}This install updates through the desktop app's signed updater \u2014 op
11100
12146
  return;
11101
12147
  }
11102
12148
  if (!opts.yes) {
11103
- const ok = await confirm3({
12149
+ const ok = await confirm4({
11104
12150
  message: "Apply this update?",
11105
12151
  default: true
11106
12152
  });
@@ -11120,7 +12166,7 @@ ${C4.dim}This install updates through the desktop app's signed updater \u2014 op
11120
12166
  process.exit(1);
11121
12167
  }
11122
12168
  }
11123
- var updateCommand = new Command21("update").description(
12169
+ var updateCommand = new Command23("update").description(
11124
12170
  "Update ZAM to the latest release (use `update check` to only check)"
11125
12171
  ).option("-y, --yes", "Apply without confirmation").option(
11126
12172
  "--force",
@@ -11130,8 +12176,8 @@ var updateCommand = new Command21("update").description(
11130
12176
  }).addCommand(checkCmd);
11131
12177
 
11132
12178
  // src/cli/commands/whoami.ts
11133
- import { Command as Command22 } from "commander";
11134
- var whoamiCommand = new Command22("whoami").description("Show or set the default user identity").option("--set <id>", "Set the default user ID").option("--clear", "Remove the default user ID").option("--json", "Output as JSON").action(async (opts) => {
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) => {
11135
12181
  await withDb(async (db) => {
11136
12182
  if (opts.set) {
11137
12183
  await setSetting(db, "user.id", opts.set);
@@ -11166,163 +12212,12 @@ var whoamiCommand = new Command22("whoami").description("Show or set the default
11166
12212
  });
11167
12213
  });
11168
12214
 
11169
- // src/cli/commands/workspace.ts
11170
- import { execSync as execSync6 } from "child_process";
11171
- import { existsSync as existsSync21, writeFileSync as writeFileSync11 } from "fs";
11172
- import { join as join22 } from "path";
11173
- import { confirm as confirm4, input as input7 } from "@inquirer/prompts";
11174
- import { Command as Command23 } from "commander";
11175
- function runGit2(cwd, args) {
11176
- try {
11177
- return execSync6(`git ${args}`, {
11178
- cwd,
11179
- stdio: "pipe",
11180
- encoding: "utf8"
11181
- }).trim();
11182
- } catch (err) {
11183
- throw new Error(`Git command failed: ${err.message}`);
11184
- }
11185
- }
11186
- var workspaceCommand = new Command23("workspace").description(
11187
- "Manage your ZAM learning workspace"
11188
- );
11189
- workspaceCommand.command("publish").description("Publish your local workspace sandbox to GitHub").action(async () => {
11190
- let db;
11191
- let workspaceDir = "";
11192
- try {
11193
- db = await openDatabase();
11194
- workspaceDir = await getSetting(db, "personal.workspace_dir") || "";
11195
- await db.close();
11196
- } catch {
11197
- await db?.close();
11198
- }
11199
- if (!workspaceDir) {
11200
- console.error(
11201
- "\x1B[31m\u2717 No active workspace configured. Please run `zam init` first.\x1B[0m"
11202
- );
11203
- process.exit(1);
11204
- }
11205
- if (!existsSync21(workspaceDir)) {
11206
- console.error(
11207
- `\x1B[31m\u2717 Workspace directory does not exist: ${workspaceDir}\x1B[0m`
11208
- );
11209
- process.exit(1);
11210
- }
11211
- console.log(`
11212
- Active workspace: \x1B[36m${workspaceDir}\x1B[0m`);
11213
- if (!hasCommand("git")) {
11214
- console.error(
11215
- "\x1B[31m\u2717 Git command was not found on this system. Please install Git first.\x1B[0m"
11216
- );
11217
- process.exit(1);
11218
- }
11219
- const gitignorePath = join22(workspaceDir, ".gitignore");
11220
- if (!existsSync21(gitignorePath)) {
11221
- writeFileSync11(
11222
- gitignorePath,
11223
- "node_modules/\n.agent/\n.agents/\n.claude/\n.gemini/\n.goose/\n*.log\n",
11224
- "utf8"
11225
- );
11226
- }
11227
- const hasGitRepo = existsSync21(join22(workspaceDir, ".git"));
11228
- if (!hasGitRepo) {
11229
- console.log("Initializing local Git repository...");
11230
- runGit2(workspaceDir, "init -b main");
11231
- runGit2(workspaceDir, "add .");
11232
- runGit2(
11233
- workspaceDir,
11234
- 'commit -m "chore: initial workspace sandbox bootstrap"'
11235
- );
11236
- console.log("\x1B[32m\u2713 Local Git repository initialized.\x1B[0m");
11237
- } else {
11238
- console.log("Git repository is already initialized.");
11239
- }
11240
- const repoName = await input7({
11241
- message: "Choose a name for your GitHub repository:",
11242
- default: "zam-personal"
11243
- });
11244
- const isPrivate = await confirm4({
11245
- message: "Should the repository be private?",
11246
- default: true
11247
- });
11248
- const repoVisibility = isPrivate ? "--private" : "--public";
11249
- if (hasCommand("gh")) {
11250
- console.log("GitHub CLI detected! Automating repository creation...");
11251
- const proceedGh = await confirm4({
11252
- message: "Would you like ZAM to create the repository using the GitHub CLI?",
11253
- default: true
11254
- });
11255
- if (proceedGh) {
11256
- try {
11257
- console.log(`Creating GitHub repository ${repoName}...`);
11258
- execSync6(
11259
- `gh repo create ${repoName} ${repoVisibility} --source=. --push`,
11260
- {
11261
- cwd: workspaceDir,
11262
- stdio: "inherit"
11263
- }
11264
- );
11265
- console.log(
11266
- "\n\x1B[32m\u2713 Successfully published workspace to GitHub!\x1B[0m"
11267
- );
11268
- process.exit(0);
11269
- } catch (err) {
11270
- console.warn(
11271
- `\x1B[33m\u26A0 GitHub CLI creation failed: ${err.message}\x1B[0m`
11272
- );
11273
- }
11274
- }
11275
- }
11276
- console.log(
11277
- "\n\x1B[1mPlease create the repository manually on GitHub:\x1B[0m"
11278
- );
11279
- console.log(" 1. Go to https://github.com/new");
11280
- console.log(` 2. Name it exactly: \x1B[36m${repoName}\x1B[0m`);
11281
- console.log(
11282
- ` 3. Choose \x1B[36m${isPrivate ? "Private" : "Public"}\x1B[0m`
11283
- );
11284
- console.log(
11285
- " 4. Do NOT initialize it with README, .gitignore, or license"
11286
- );
11287
- console.log(" 5. Click 'Create repository'\n");
11288
- const githubUrl = await input7({
11289
- message: "Paste the repository Git URL (e.g. git@github.com:user/repo.git):"
11290
- });
11291
- if (githubUrl) {
11292
- try {
11293
- console.log("Linking remote repository and pushing...");
11294
- let hasOrigin = false;
11295
- try {
11296
- runGit2(workspaceDir, "remote get-url origin");
11297
- hasOrigin = true;
11298
- } catch {
11299
- }
11300
- if (hasOrigin) {
11301
- runGit2(workspaceDir, `remote set-url origin ${githubUrl}`);
11302
- } else {
11303
- runGit2(workspaceDir, `remote add origin ${githubUrl}`);
11304
- }
11305
- runGit2(workspaceDir, "push -u origin main");
11306
- console.log(
11307
- "\x1B[32m\u2713 Successfully linked and pushed to GitHub!\x1B[0m"
11308
- );
11309
- } catch (err) {
11310
- console.error(
11311
- `\x1B[31m\u2717 Push failed: ${err.message}\x1B[0m`
11312
- );
11313
- console.log(
11314
- "You can push manually later using: git push -u origin main"
11315
- );
11316
- }
11317
- }
11318
- });
11319
-
11320
12215
  // src/cli/index.ts
11321
12216
  var __dirname = dirname10(fileURLToPath5(import.meta.url));
11322
12217
  var pkg = JSON.parse(
11323
- readFileSync14(join23(__dirname, "..", "..", "package.json"), "utf-8")
12218
+ readFileSync14(join24(__dirname, "..", "..", "package.json"), "utf-8")
11324
12219
  );
11325
- var program = new Command24();
12220
+ var program = new Command25();
11326
12221
  program.name("zam").description(
11327
12222
  "The Symbiotic Learning Kernel: Elevating Human Intelligence through AI Collaboration."
11328
12223
  ).version(pkg.version);
@@ -11342,6 +12237,7 @@ program.addCommand(observerCommand);
11342
12237
  program.addCommand(settingsCommand);
11343
12238
  program.addCommand(whoamiCommand);
11344
12239
  program.addCommand(connectorCommand);
12240
+ program.addCommand(providerCommand);
11345
12241
  program.addCommand(snapshotCommand);
11346
12242
  program.addCommand(profileCommand);
11347
12243
  program.addCommand(updateCommand);