zam-core 0.10.7 → 0.10.9

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/app.js CHANGED
@@ -6122,6 +6122,14 @@ function setAgentConnectAutoDone(done, path = defaultConfigPath()) {
6122
6122
  }
6123
6123
  saveInstallConfig(config, path);
6124
6124
  }
6125
+ function getLastRepairedVersion(path = defaultConfigPath()) {
6126
+ return loadInstallConfig(path).lastRepairedVersion;
6127
+ }
6128
+ function setLastRepairedVersion(version, path = defaultConfigPath()) {
6129
+ const config = loadInstallConfig(path);
6130
+ config.lastRepairedVersion = version;
6131
+ saveInstallConfig(config, path);
6132
+ }
6125
6133
  function getConfiguredWorkspaces(path = defaultConfigPath()) {
6126
6134
  return loadInstallConfig(path).workspaces ?? [];
6127
6135
  }
@@ -6844,6 +6852,7 @@ __export(kernel_exports, {
6844
6852
  getInstallMode: () => getInstallMode,
6845
6853
  getKnowledgeContextById: () => getKnowledgeContextById,
6846
6854
  getKnowledgeContextByName: () => getKnowledgeContextByName,
6855
+ getLastRepairedVersion: () => getLastRepairedVersion,
6847
6856
  getMachineAiConfig: () => getMachineAiConfig,
6848
6857
  getMachineAiModels: () => getMachineAiModels,
6849
6858
  getMonitorDir: () => getMonitorDir,
@@ -6939,6 +6948,7 @@ __export(kernel_exports, {
6939
6948
  setAgentConnectAutoDone: () => setAgentConnectAutoDone,
6940
6949
  setInstallChannel: () => setInstallChannel,
6941
6950
  setInstallMode: () => setInstallMode,
6951
+ setLastRepairedVersion: () => setLastRepairedVersion,
6942
6952
  setProviderApiKey: () => setProviderApiKey,
6943
6953
  setSetting: () => setSetting,
6944
6954
  setTursoCredentials: () => setTursoCredentials,
@@ -7015,8 +7025,8 @@ var init_kernel = __esm({
7015
7025
  });
7016
7026
 
7017
7027
  // src/cli/app.ts
7018
- import { readFileSync as readFileSync20 } from "fs";
7019
- import { dirname as dirname14, join as join29 } from "path";
7028
+ import { readFileSync as readFileSync21 } from "fs";
7029
+ import { dirname as dirname14, join as join30 } from "path";
7020
7030
  import { fileURLToPath as fileURLToPath8 } from "url";
7021
7031
  import { Command as Command27 } from "commander";
7022
7032
 
@@ -7435,6 +7445,7 @@ function detectInstalledConnectHarnesses(options = {}) {
7435
7445
  return detected;
7436
7446
  }
7437
7447
  function parseMcpJsonConfig(path, content) {
7448
+ if (content.trim() === "") return {};
7438
7449
  let parsed;
7439
7450
  try {
7440
7451
  parsed = JSON.parse(content);
@@ -7480,17 +7491,21 @@ function connectHarnessMcp(harnessId, opts) {
7480
7491
  if (!existing.mcpServers) {
7481
7492
  existing.mcpServers = {};
7482
7493
  }
7483
- if (opts.zamPath.endsWith(".js")) {
7484
- existing.mcpServers.zam = {
7485
- command: process.execPath,
7486
- args: [opts.zamPath, "mcp"]
7487
- };
7488
- } else {
7489
- existing.mcpServers.zam = {
7490
- command: opts.zamPath,
7491
- args: ["mcp"]
7492
- };
7494
+ const expected = opts.zamPath.endsWith(".js") ? {
7495
+ command: process.execPath,
7496
+ args: [opts.zamPath, "mcp"]
7497
+ } : {
7498
+ command: opts.zamPath,
7499
+ args: ["mcp"]
7500
+ };
7501
+ const current = existing.mcpServers.zam;
7502
+ if (typeof current === "object" && current !== null && !Array.isArray(current)) {
7503
+ const c = current;
7504
+ if (c.command === expected.command && JSON.stringify(c.args) === JSON.stringify(expected.args)) {
7505
+ alreadyConfigured = true;
7506
+ }
7493
7507
  }
7508
+ existing.mcpServers.zam = expected;
7494
7509
  return JSON.stringify(existing, null, 2);
7495
7510
  };
7496
7511
  if (harnessId === "claude-code") {
@@ -7517,25 +7532,7 @@ function connectHarnessMcp(harnessId, opts) {
7517
7532
  } else if (harnessId === "antigravity") {
7518
7533
  targetPath = join12(opts.home, ".gemini", "config", "mcp_config.json");
7519
7534
  hint = "Shared config read by Antigravity CLI and IDE (2.0+); older IDE builds read ~/.gemini/antigravity/mcp_config.json instead. Refresh Installed MCP Servers; the first tool call may still require approval.";
7520
- let existing = {};
7521
- if (exists(targetPath)) {
7522
- existing = parseMcpJsonConfig(targetPath, read(targetPath));
7523
- }
7524
- if (!existing.mcpServers) {
7525
- existing.mcpServers = {};
7526
- }
7527
- if (opts.zamPath.endsWith(".js")) {
7528
- existing.mcpServers.zam = {
7529
- command: process.execPath,
7530
- args: [opts.zamPath, "mcp"]
7531
- };
7532
- } else {
7533
- existing.mcpServers.zam = {
7534
- command: opts.zamPath,
7535
- args: ["mcp"]
7536
- };
7537
- }
7538
- content = JSON.stringify(existing, null, 2);
7535
+ content = mergeMcpServersJson(targetPath);
7539
7536
  } else if (harnessId === "opencode") {
7540
7537
  targetPath = join12(opts.home, ".config", "opencode", "opencode.json");
7541
7538
  hint = "OpenCode will load the enabled 'zam' MCP server on next launch.";
@@ -7549,11 +7546,19 @@ function connectHarnessMcp(harnessId, opts) {
7549
7546
  }
7550
7547
  const servers = mcp ?? {};
7551
7548
  const cmd = opts.zamPath.endsWith(".js") ? [process.execPath, opts.zamPath, "mcp"] : [opts.zamPath, "mcp"];
7552
- servers.zam = {
7549
+ const expected = {
7553
7550
  type: "local",
7554
7551
  command: cmd,
7555
7552
  enabled: true
7556
7553
  };
7554
+ const current = servers.zam;
7555
+ if (typeof current === "object" && current !== null && !Array.isArray(current)) {
7556
+ const c = current;
7557
+ if (c.type === expected.type && c.enabled === expected.enabled && JSON.stringify(c.command) === JSON.stringify(expected.command)) {
7558
+ alreadyConfigured = true;
7559
+ }
7560
+ }
7561
+ servers.zam = expected;
7557
7562
  existing.mcp = servers;
7558
7563
  content = JSON.stringify(existing, null, 2);
7559
7564
  } else if (harnessId === "codex") {
@@ -7664,21 +7669,25 @@ ${zamExtension}
7664
7669
  if (!existing.mcpServers) {
7665
7670
  existing.mcpServers = {};
7666
7671
  }
7667
- if (opts.zamPath.endsWith(".js")) {
7668
- existing.mcpServers.zam = {
7669
- type: "local",
7670
- command: process.execPath,
7671
- args: [opts.zamPath, "mcp"],
7672
- tools: ["*"]
7673
- };
7674
- } else {
7675
- existing.mcpServers.zam = {
7676
- type: "local",
7677
- command: opts.zamPath,
7678
- args: ["mcp"],
7679
- tools: ["*"]
7680
- };
7672
+ const expected = opts.zamPath.endsWith(".js") ? {
7673
+ type: "local",
7674
+ command: process.execPath,
7675
+ args: [opts.zamPath, "mcp"],
7676
+ tools: ["*"]
7677
+ } : {
7678
+ type: "local",
7679
+ command: opts.zamPath,
7680
+ args: ["mcp"],
7681
+ tools: ["*"]
7682
+ };
7683
+ const current = existing.mcpServers.zam;
7684
+ if (typeof current === "object" && current !== null && !Array.isArray(current)) {
7685
+ const c = current;
7686
+ if (c.type === expected.type && c.command === expected.command && JSON.stringify(c.args) === JSON.stringify(expected.args) && JSON.stringify(c.tools) === JSON.stringify(expected.tools)) {
7687
+ alreadyConfigured = true;
7688
+ }
7681
7689
  }
7690
+ existing.mcpServers.zam = expected;
7682
7691
  content = JSON.stringify(existing, null, 2);
7683
7692
  }
7684
7693
  return {
@@ -8310,11 +8319,11 @@ var agentCommand = new Command("agent").description("Provision and inspect the a
8310
8319
 
8311
8320
  // src/cli/commands/bridge.ts
8312
8321
  init_kernel();
8313
- import { execFileSync as execFileSync4 } from "child_process";
8322
+ import { execFileSync as execFileSync5 } from "child_process";
8314
8323
  import { randomBytes as randomBytes2 } from "crypto";
8315
- import { existsSync as existsSync19, readdirSync as readdirSync2, readFileSync as readFileSync16, rmSync as rmSync3 } from "fs";
8316
- import { homedir as homedir13, tmpdir as tmpdir3 } from "os";
8317
- import { join as join22, resolve as resolve5 } from "path";
8324
+ import { existsSync as existsSync21, readdirSync as readdirSync2, readFileSync as readFileSync17, rmSync as rmSync3 } from "fs";
8325
+ import { homedir as homedir14, tmpdir as tmpdir3 } from "os";
8326
+ import { join as join23, resolve as resolve6 } from "path";
8318
8327
  import { Command as Command2 } from "commander";
8319
8328
  import { ulid as ulid10 } from "ulid";
8320
8329
 
@@ -9401,7 +9410,7 @@ async function prepareRecallChain(db, opts) {
9401
9410
  } else {
9402
9411
  spawnLocalRunner(endpoint.url, endpoint.model, endpoint.runner);
9403
9412
  while (Date.now() < deadline) {
9404
- await new Promise((resolve11) => setTimeout(resolve11, 1e3));
9413
+ await new Promise((resolve12) => setTimeout(resolve12, 1e3));
9405
9414
  if (await isLlmOnline(endpoint.url)) {
9406
9415
  online = true;
9407
9416
  break;
@@ -9694,7 +9703,7 @@ async function startLocalRunner(url, model, locale, hint) {
9694
9703
  let attempts = 0;
9695
9704
  const dotsPerLine = 30;
9696
9705
  while (true) {
9697
- await new Promise((resolve11) => setTimeout(resolve11, 500));
9706
+ await new Promise((resolve12) => setTimeout(resolve12, 500));
9698
9707
  if (await isLlmOnline(url)) {
9699
9708
  if (attempts > 0) process.stdout.write("\n");
9700
9709
  return true;
@@ -9783,8 +9792,8 @@ async function fetchWithInteractiveTimeout(url, options = {}) {
9783
9792
  const dotsPerLine = 30;
9784
9793
  while (true) {
9785
9794
  let timeoutId;
9786
- const timeoutPromise = new Promise((resolve11) => {
9787
- timeoutId = setTimeout(() => resolve11("timeout"), timeoutMs);
9795
+ const timeoutPromise = new Promise((resolve12) => {
9796
+ timeoutId = setTimeout(() => resolve12("timeout"), timeoutMs);
9788
9797
  });
9789
9798
  const dotsInterval = setInterval(() => {
9790
9799
  process.stdout.write(".");
@@ -10051,7 +10060,7 @@ async function readWebLink(url) {
10051
10060
  redirect: "manual",
10052
10061
  signal: controller.signal,
10053
10062
  headers: {
10054
- "User-Agent": "ZAM-Content-Studio/0.10.7"
10063
+ "User-Agent": "ZAM-Content-Studio/0.10.9"
10055
10064
  }
10056
10065
  });
10057
10066
  if (res.status >= 300 && res.status < 400) {
@@ -11314,6 +11323,127 @@ async function updateCheck(params) {
11314
11323
  });
11315
11324
  }
11316
11325
 
11326
+ // src/cli/cli-install.ts
11327
+ import { execFileSync as execFileSync4 } from "child_process";
11328
+ import {
11329
+ chmodSync as chmodSync2,
11330
+ existsSync as existsSync18,
11331
+ mkdirSync as mkdirSync13,
11332
+ readFileSync as readFileSync14,
11333
+ writeFileSync as writeFileSync11
11334
+ } from "fs";
11335
+ import { homedir as homedir13 } from "os";
11336
+ import { delimiter as delimiter2, join as join19, resolve as resolve5 } from "path";
11337
+ var BUILT_CLI_PATTERN = /[\\/]dist[\\/]cli[\\/]index\.js$/;
11338
+ function normalizeForCompare(path, platform) {
11339
+ const resolved = resolve5(path);
11340
+ return platform === "win32" ? resolved.toLowerCase() : resolved;
11341
+ }
11342
+ function windowsShimContent(nodePath, cliPath) {
11343
+ return `@echo off\r
11344
+ "${nodePath}" "${cliPath}" %*\r
11345
+ `;
11346
+ }
11347
+ function unixShimContent(nodePath, cliPath) {
11348
+ return `#!/bin/sh
11349
+ exec "${nodePath}" "${cliPath}" "$@"
11350
+ `;
11351
+ }
11352
+ function psQuote(value) {
11353
+ return `'${value.replace(/'/g, "''")}'`;
11354
+ }
11355
+ function ensureWindowsUserPath(binDir) {
11356
+ const script = [
11357
+ "$ErrorActionPreference = 'Stop'",
11358
+ "$key = [Microsoft.Win32.Registry]::CurrentUser.CreateSubKey('Environment', $true)",
11359
+ "$old = [string]$key.GetValue('Path', '', [Microsoft.Win32.RegistryValueOptions]::DoNotExpandEnvironmentNames)",
11360
+ `$target = ${psQuote(binDir)}`,
11361
+ "$expanded = ($old -split ';') | Where-Object { $_ -ne '' } | ForEach-Object { [Environment]::ExpandEnvironmentVariables($_).TrimEnd('\\') }",
11362
+ "if ($expanded -contains $target.TrimEnd('\\')) { 'present'; exit 0 }",
11363
+ "$new = if ($old -and -not $old.EndsWith(';')) { $old + ';' + $target } else { $old + $target }",
11364
+ "$key.SetValue('Path', $new, [Microsoft.Win32.RegistryValueKind]::ExpandString)",
11365
+ `$sig = '[DllImport("user32.dll", SetLastError = true, CharSet = CharSet.Auto)] public static extern IntPtr SendMessageTimeout(IntPtr hWnd, uint Msg, UIntPtr wParam, string lParam, uint fuFlags, uint uTimeout, out UIntPtr lpdwResult);'`,
11366
+ "$type = Add-Type -MemberDefinition $sig -Name 'ZamEnvBroadcast' -Namespace 'ZamInstall' -PassThru",
11367
+ "[UIntPtr]$result = [UIntPtr]::Zero",
11368
+ "$null = $type::SendMessageTimeout([IntPtr]0xffff, 0x001A, [UIntPtr]::Zero, 'Environment', 2, 5000, [ref]$result)",
11369
+ "'updated'"
11370
+ ].join("; ");
11371
+ const output = execFileSync4(
11372
+ "powershell.exe",
11373
+ ["-NoProfile", "-NonInteractive", "-Command", script],
11374
+ { stdio: ["ignore", "pipe", "pipe"], windowsHide: true }
11375
+ ).toString().trim();
11376
+ return output.endsWith("updated");
11377
+ }
11378
+ function ensureUnixUserPath(home, platform) {
11379
+ const profile = join19(home, platform === "darwin" ? ".zprofile" : ".profile");
11380
+ const existing = existsSync18(profile) ? readFileSync14(profile, "utf8") : "";
11381
+ if (existing.includes(".zam/bin")) return false;
11382
+ const block = `
11383
+ # Added by ZAM: keep the zam CLI on PATH
11384
+ export PATH="$HOME/.zam/bin:$PATH"
11385
+ `;
11386
+ writeFileSync11(profile, existing + block, "utf8");
11387
+ return true;
11388
+ }
11389
+ function installCliShim(options = {}) {
11390
+ const home = options.home ?? homedir13();
11391
+ const platform = options.platform ?? process.platform;
11392
+ const env = options.env ?? process.env;
11393
+ const find = options.find ?? findExecutable;
11394
+ const nodePath = resolve5(options.nodePath ?? process.execPath);
11395
+ const cliPath = resolve5(options.cliPath ?? process.argv[1] ?? "");
11396
+ const binDir = join19(home, ".zam", "bin");
11397
+ const shimPath = join19(binDir, platform === "win32" ? "zam.cmd" : "zam");
11398
+ const report = {
11399
+ status: "ok",
11400
+ binDir,
11401
+ shimPath,
11402
+ nodePath,
11403
+ cliPath,
11404
+ onPath: false,
11405
+ pathUpdated: false,
11406
+ needsNewTerminal: false
11407
+ };
11408
+ if (!BUILT_CLI_PATTERN.test(cliPath) || !existsSync18(cliPath)) {
11409
+ report.status = "skipped";
11410
+ report.detail = `Not running from a built CLI entry (${cliPath}); nothing to link.`;
11411
+ return report;
11412
+ }
11413
+ try {
11414
+ const existing = find("zam");
11415
+ if (existing && normalizeForCompare(existing, platform) !== normalizeForCompare(shimPath, platform)) {
11416
+ report.status = "external";
11417
+ report.onPath = true;
11418
+ report.detail = existing;
11419
+ return report;
11420
+ }
11421
+ const content = platform === "win32" ? windowsShimContent(nodePath, cliPath) : unixShimContent(nodePath, cliPath);
11422
+ const existed = existsSync18(shimPath);
11423
+ const changed = !existed || readFileSync14(shimPath, "utf8") !== content;
11424
+ if (changed) {
11425
+ mkdirSync13(binDir, { recursive: true });
11426
+ writeFileSync11(shimPath, content, "utf8");
11427
+ if (platform !== "win32") chmodSync2(shimPath, 493);
11428
+ }
11429
+ report.status = !existed ? "installed" : changed ? "refreshed" : "ok";
11430
+ const processPathHasBin = (env.PATH ?? "").split(delimiter2).map((entry) => normalizeForCompare(entry.trim() || ".", platform)).includes(normalizeForCompare(binDir, platform));
11431
+ if (processPathHasBin) {
11432
+ report.onPath = true;
11433
+ return report;
11434
+ }
11435
+ const ensureUserPath = options.ensureUserPath ?? (platform === "win32" ? ensureWindowsUserPath : () => ensureUnixUserPath(home, platform));
11436
+ report.pathUpdated = ensureUserPath(binDir);
11437
+ report.onPath = true;
11438
+ report.needsNewTerminal = true;
11439
+ return report;
11440
+ } catch (error) {
11441
+ report.status = "error";
11442
+ report.detail = error instanceof Error ? error.message : String(error);
11443
+ return report;
11444
+ }
11445
+ }
11446
+
11317
11447
  // src/cli/curriculum/breadcrumb.ts
11318
11448
  init_kernel();
11319
11449
  var LAST_SELECTION_KEY = "curriculum.lastSelection";
@@ -28000,1054 +28130,1132 @@ function getCurriculumProvider(id) {
28000
28130
  return CURRICULUM_PROVIDERS.find((provider) => provider.id === id);
28001
28131
  }
28002
28132
 
28003
- // src/cli/llm/capability-probe.ts
28133
+ // src/cli/install-repair.ts
28004
28134
  init_kernel();
28005
- var EMBEDDING_MODEL_HINTS = [
28006
- "embed",
28007
- "text-embedding",
28008
- "bge-",
28009
- "gte-",
28010
- "nomic",
28011
- "mxbai"
28012
- ];
28013
- var VISION_MODEL_HINTS = [
28014
- "vision",
28015
- "-vl",
28016
- "vl-",
28017
- "vlm",
28018
- "llava",
28019
- "gpt-4o",
28020
- "gpt-4.1",
28021
- "gpt-5",
28022
- "gemini",
28023
- "pixtral",
28024
- "minicpm-v",
28025
- "internvl",
28026
- "moondream",
28027
- "llama-3.2",
28028
- "llama3.2",
28029
- // Xiaomi MiMo(-VL) is multimodal; the plain "mimo-v*" tag carries no "-vl".
28030
- "mimo"
28135
+ import { existsSync as existsSync20 } from "fs";
28136
+
28137
+ // src/cli/provisioning/index.ts
28138
+ import {
28139
+ existsSync as existsSync19,
28140
+ lstatSync as lstatSync2,
28141
+ mkdirSync as mkdirSync14,
28142
+ readFileSync as readFileSync15,
28143
+ realpathSync,
28144
+ rmSync as rmSync2,
28145
+ symlinkSync,
28146
+ writeFileSync as writeFileSync12
28147
+ } from "fs";
28148
+ import { basename as basename4, dirname as dirname9, join as join20 } from "path";
28149
+ import { fileURLToPath as fileURLToPath5 } from "url";
28150
+ var packageRoot3 = [
28151
+ fileURLToPath5(new URL("../..", import.meta.url)),
28152
+ fileURLToPath5(new URL("../../..", import.meta.url))
28153
+ ].find((candidate) => existsSync19(join20(candidate, "package.json"))) ?? fileURLToPath5(new URL("../..", import.meta.url));
28154
+ var ALL_SETUP_AGENTS = ["claude", "copilot", "codex", "agent"];
28155
+ var SKILL_PAIRS = [
28156
+ {
28157
+ from: join20(packageRoot3, ".claude", "skills", "zam", "SKILL.md"),
28158
+ to: join20(".claude", "skills", "zam", "SKILL.md"),
28159
+ agents: ["claude", "copilot"]
28160
+ },
28161
+ {
28162
+ from: join20(packageRoot3, ".agent", "skills", "zam", "SKILL.md"),
28163
+ to: join20(".agent", "skills", "zam", "SKILL.md"),
28164
+ agents: ["agent"]
28165
+ },
28166
+ {
28167
+ from: join20(packageRoot3, ".agents", "skills", "zam", "SKILL.md"),
28168
+ to: join20(".agents", "skills", "zam", "SKILL.md"),
28169
+ agents: ["codex"]
28170
+ }
28031
28171
  ];
28032
- function matchesAny(id, hints) {
28033
- const lower = id.toLowerCase();
28034
- return hints.some((hint) => lower.includes(hint));
28172
+ function parseSetupAgents(value) {
28173
+ if (!value || value.trim().toLowerCase() === "all") {
28174
+ return new Set(ALL_SETUP_AGENTS);
28175
+ }
28176
+ const aliases = {
28177
+ claude: ["claude"],
28178
+ copilot: ["copilot"],
28179
+ codex: ["codex"],
28180
+ agent: ["agent"],
28181
+ opencode: ["agent"]
28182
+ };
28183
+ const selected = /* @__PURE__ */ new Set();
28184
+ for (const raw of value.split(",")) {
28185
+ const key = raw.trim().toLowerCase();
28186
+ const mapped = aliases[key];
28187
+ if (!mapped) {
28188
+ throw new Error(
28189
+ `Unknown agent "${raw}". Use: all, claude, copilot, codex, agent.`
28190
+ );
28191
+ }
28192
+ for (const item of mapped) selected.add(item);
28193
+ }
28194
+ return selected;
28035
28195
  }
28036
- function catalogHasModel(catalog, model) {
28037
- const lower = model.toLowerCase();
28038
- return catalog.some((id) => id.toLowerCase() === lower);
28196
+ function pathExists(path) {
28197
+ try {
28198
+ lstatSync2(path);
28199
+ return true;
28200
+ } catch {
28201
+ return false;
28202
+ }
28039
28203
  }
28040
- function classifyCapabilities(entry, catalog, catalogKnown, dimProbeEmbedding = false) {
28041
- const detected = emptyCapabilityFlags();
28042
- if (entry.apiFlavor === "anthropic-messages") {
28043
- detected.text = true;
28044
- detected.image = true;
28045
- return detected;
28204
+ function isSymbolicLink(path) {
28205
+ try {
28206
+ return lstatSync2(path).isSymbolicLink();
28207
+ } catch {
28208
+ return false;
28046
28209
  }
28047
- const looksEmbedding = matchesAny(entry.model, EMBEDDING_MODEL_HINTS);
28048
- const looksVision = matchesAny(entry.model, VISION_MODEL_HINTS);
28049
- const inCatalog = catalogHasModel(catalog, entry.model);
28050
- detected.embedding = looksEmbedding || dimProbeEmbedding;
28051
- detected.image = looksVision;
28052
- detected.text = !detected.embedding && (inCatalog || !catalogKnown);
28053
- return detected;
28054
28210
  }
28055
- function resolveApiKey(apiKeyRef) {
28056
- if (!apiKeyRef) return DEFAULT_LLM_API_KEY;
28057
- return getProviderApiKey(apiKeyRef) ?? DEFAULT_LLM_API_KEY;
28211
+ function comparableRealPath(path) {
28212
+ const real = realpathSync(path);
28213
+ return process.platform === "win32" ? real.toLowerCase() : real;
28058
28214
  }
28059
- async function probeModelCapabilities(entry, opts = {}) {
28060
- const apiKey = resolveApiKey(entry.apiKeyRef);
28061
- if (entry.apiFlavor === "anthropic-messages") {
28062
- const online2 = await isLlmOnline(entry.url);
28063
- return {
28064
- reachable: online2,
28065
- catalog: [],
28066
- detected: classifyCapabilities(entry, [], false)
28067
- };
28068
- }
28069
- const online = await isLlmOnline(entry.url);
28070
- if (!online) {
28071
- return { reachable: false, catalog: [], detected: emptyCapabilityFlags() };
28072
- }
28073
- const catalog = await getAvailableModels(entry.url, apiKey);
28074
- const catalogKnown = catalog.length > 0;
28075
- let dimProbeEmbedding = false;
28076
- const looksEmbedding = matchesAny(entry.model, EMBEDDING_MODEL_HINTS);
28077
- if (opts.embeddingDimProbe && !catalogKnown && !looksEmbedding) {
28078
- try {
28079
- const [vector] = await embedTexts(
28080
- { url: entry.url, model: entry.model, apiKey },
28081
- ["capability probe"]
28082
- );
28083
- dimProbeEmbedding = Array.isArray(vector) && vector.length > 0;
28084
- } catch {
28085
- dimProbeEmbedding = false;
28086
- }
28215
+ function pathsResolveToSameDirectory(sourceDir, destinationDir) {
28216
+ try {
28217
+ return comparableRealPath(destinationDir) === comparableRealPath(sourceDir);
28218
+ } catch {
28219
+ return false;
28087
28220
  }
28088
- return {
28089
- reachable: true,
28090
- catalog,
28091
- detected: classifyCapabilities(
28092
- entry,
28093
- catalog,
28094
- catalogKnown,
28095
- dimProbeEmbedding
28096
- )
28097
- };
28098
28221
  }
28099
- function reconcileCapabilities(userSelected, detected) {
28100
- const result = emptyCapabilityFlags();
28101
- for (const key of Object.keys(result)) {
28102
- result[key] = userSelected[key] && detected[key];
28103
- }
28104
- return result;
28222
+ function linkPointsTo(sourceDir, destinationDir) {
28223
+ return isSymbolicLink(destinationDir) && pathsResolveToSameDirectory(sourceDir, destinationDir);
28105
28224
  }
28106
- function validateModelSave(entry, probe, now = () => (/* @__PURE__ */ new Date()).toISOString()) {
28107
- if (!probe.reachable) {
28108
- return {
28109
- ok: false,
28110
- error: "Endpoint is unreachable \u2014 cannot verify capabilities. Bring it online and retry."
28111
- };
28225
+ function isZamSkillCopy(destinationDir) {
28226
+ try {
28227
+ if (!lstatSync2(destinationDir).isDirectory()) return false;
28228
+ const skillFile = join20(destinationDir, "SKILL.md");
28229
+ if (!existsSync19(skillFile)) return false;
28230
+ return /^name:\s*zam\s*$/m.test(readFileSync15(skillFile, "utf8"));
28231
+ } catch {
28232
+ return false;
28112
28233
  }
28113
- return {
28114
- ok: true,
28115
- entry: {
28116
- ...entry,
28117
- capabilities: reconcileCapabilities(entry.capabilities, probe.detected),
28118
- detectedCapabilities: probe.detected,
28119
- probedAt: now()
28120
- }
28121
- };
28122
28234
  }
28123
-
28124
- // src/cli/llm/vision.ts
28125
- init_kernel();
28126
- import { randomBytes } from "crypto";
28127
- import { readFileSync as readFileSync14 } from "fs";
28128
- import { tmpdir as tmpdir2 } from "os";
28129
- import { basename as basename4, join as join19 } from "path";
28130
- var LANGUAGE_NAMES2 = {
28131
- en: "English",
28132
- de: "German",
28133
- es: "Spanish",
28134
- fr: "French",
28135
- pt: "Portuguese",
28136
- zh: "Chinese",
28137
- ja: "Japanese"
28138
- };
28139
- var OBSERVATION_KINDS2 = /* @__PURE__ */ new Set([
28140
- "progress",
28141
- "step-completed",
28142
- "error",
28143
- "help-seeking",
28144
- "uncertain",
28145
- "privacy-pause",
28146
- "heartbeat"
28147
- ]);
28148
- var ACTION_TYPES2 = /* @__PURE__ */ new Set([
28149
- "click",
28150
- "shortcut",
28151
- "typing",
28152
- "scroll",
28153
- "window-change"
28154
- ]);
28155
- async function observeUiSnapshotViaLLM(db, input8) {
28156
- const cfg = await getProviderForRole(db, "vision");
28157
- if (!cfg.enabled) {
28158
- throw new Error(
28159
- "Vision observation is disabled in settings (llm.vision.enabled)"
28160
- );
28235
+ function classifySkillDestination(sourceDir, destinationDir) {
28236
+ if (!existsSync19(sourceDir)) return "source-missing";
28237
+ if (!isSymbolicLink(destinationDir) && pathsResolveToSameDirectory(sourceDir, destinationDir)) {
28238
+ return "source-directory";
28161
28239
  }
28162
- const imageUrls = [];
28163
- const isVideo = /\.(mp4|mov|m4v|avi|mkv|webm)$/i.test(input8.imagePath);
28164
- if (isVideo) {
28165
- const { mkdirSync: mkdirSync18, readdirSync: readdirSync3, rmSync: rmSync4 } = await import("fs");
28166
- const { execSync: execSync6 } = await import("child_process");
28167
- const tempDir = join19(
28168
- tmpdir2(),
28169
- `zam-frames-${randomBytes(4).toString("hex")}`
28170
- );
28171
- mkdirSync18(tempDir, { recursive: true });
28172
- try {
28173
- execSync6(
28174
- `ffmpeg -i "${input8.imagePath}" -vf "fps=1/3,scale=1280:-1" -vsync vfr "${tempDir}/frame_%03d.png"`,
28175
- { stdio: "ignore" }
28176
- );
28177
- let files = readdirSync3(tempDir).filter((f) => f.endsWith(".png")).sort();
28178
- if (files.length === 0) {
28179
- execSync6(
28180
- `ffmpeg -i "${input8.imagePath}" -vframes 1 "${tempDir}/frame_001.png"`,
28181
- { stdio: "ignore" }
28182
- );
28183
- files = readdirSync3(tempDir).filter((f) => f.endsWith(".png")).sort();
28184
- }
28185
- const maxFrames = cfg.maxFrames ?? 100;
28186
- let sampledFiles = files;
28187
- if (files.length > maxFrames) {
28188
- if (maxFrames <= 1) {
28189
- sampledFiles = [files[0]];
28190
- } else {
28191
- const step = (files.length - 1) / (maxFrames - 1);
28192
- sampledFiles = [];
28193
- for (let i = 0; i < maxFrames; i++) {
28194
- const index = Math.round(i * step);
28195
- sampledFiles.push(files[index]);
28196
- }
28197
- }
28198
- }
28199
- for (const file of sampledFiles) {
28200
- const bytes = readFileSync14(join19(tempDir, file));
28201
- imageUrls.push(`data:image/png;base64,${bytes.toString("base64")}`);
28202
- }
28203
- } finally {
28204
- try {
28205
- rmSync4(tempDir, { recursive: true, force: true });
28206
- } catch {
28240
+ if (linkPointsTo(sourceDir, destinationDir)) return "linked";
28241
+ if (!pathExists(destinationDir)) return "missing";
28242
+ if (isSymbolicLink(destinationDir)) return "broken";
28243
+ if (isZamSkillCopy(destinationDir)) return "stale-copy";
28244
+ return "unmanaged";
28245
+ }
28246
+ function wireSkills(cwd = process.cwd(), agents = parseSetupAgents(), opts = {}) {
28247
+ const results = [];
28248
+ const log = (message) => {
28249
+ if (!opts.quiet) console.log(message);
28250
+ };
28251
+ for (const { from, to, agents: pairAgents } of SKILL_PAIRS) {
28252
+ if (!pairAgents.some((agent) => agents.has(agent))) continue;
28253
+ const sourceDir = dirname9(from);
28254
+ const destinationDir = dirname9(join20(cwd, to));
28255
+ const label = dirname9(to);
28256
+ const state = classifySkillDestination(sourceDir, destinationDir);
28257
+ if (state === "source-missing") {
28258
+ if (!opts.quiet) {
28259
+ console.warn(` warn source not found, skipping: ${sourceDir}`);
28207
28260
  }
28261
+ results.push({
28262
+ source: sourceDir,
28263
+ destination: destinationDir,
28264
+ action: "skipped",
28265
+ reason: "source-not-found"
28266
+ });
28267
+ continue;
28208
28268
  }
28209
- } else {
28210
- const imageBytes = readFileSync14(input8.imagePath);
28211
- const ext = input8.imagePath.split(".").pop()?.toLowerCase();
28212
- const mime = ext === "jpg" || ext === "jpeg" ? "image/jpeg" : "image/png";
28213
- imageUrls.push(`data:${mime};base64,${imageBytes.toString("base64")}`);
28214
- }
28215
- if (imageUrls.length === 0) {
28216
- throw new Error("No image data available for vision analysis");
28217
- }
28218
- const endpoints = [
28219
- {
28220
- url: cfg.url,
28221
- apiKey: cfg.apiKey || DEFAULT_LLM_API_KEY,
28222
- model: input8.model ?? cfg.model,
28223
- apiFlavor: cfg.apiFlavor
28269
+ if (state === "source-directory") {
28270
+ log(` skip ${label} (package source)`);
28271
+ results.push({
28272
+ source: sourceDir,
28273
+ destination: destinationDir,
28274
+ action: "skipped",
28275
+ reason: "source-directory"
28276
+ });
28277
+ continue;
28224
28278
  }
28225
- ];
28226
- if (cfg.fallback) {
28227
- endpoints.push({
28228
- url: cfg.fallback.url,
28229
- apiKey: cfg.fallback.apiKey || DEFAULT_LLM_API_KEY,
28230
- model: cfg.fallback.model,
28231
- apiFlavor: cfg.fallback.apiFlavor
28232
- });
28233
- }
28234
- let lastRequestError;
28235
- let sawUnparseableDraft = false;
28236
- let sawInvalidDraft = false;
28237
- for (const endpoint of endpoints) {
28238
- let content;
28239
- try {
28240
- content = await requestVisionDraft({
28241
- ...endpoint,
28242
- locale: cfg.locale,
28243
- imageUrls,
28244
- input: input8
28279
+ if (state === "linked") {
28280
+ log(` skip ${label} (already linked)`);
28281
+ results.push({
28282
+ source: sourceDir,
28283
+ destination: destinationDir,
28284
+ action: "skipped",
28285
+ reason: "already-linked"
28245
28286
  });
28246
- } catch (err) {
28247
- lastRequestError = err;
28248
28287
  continue;
28249
28288
  }
28250
- let draft;
28251
- try {
28252
- draft = extractDraft(content);
28253
- } catch {
28254
- sawUnparseableDraft = true;
28289
+ const destinationExists = state !== "missing";
28290
+ const replaceExisting = destinationExists && (Boolean(opts.force) || state === "broken" || state === "stale-copy");
28291
+ if (destinationExists && !replaceExisting) {
28292
+ if (!opts.quiet) {
28293
+ console.warn(
28294
+ ` warn ${label} already exists and is not managed by ZAM; use --force to replace it`
28295
+ );
28296
+ }
28297
+ results.push({
28298
+ source: sourceDir,
28299
+ destination: destinationDir,
28300
+ action: "skipped",
28301
+ reason: "unmanaged-destination"
28302
+ });
28255
28303
  continue;
28256
28304
  }
28257
- const report = buildReport(input8, draft);
28258
- if (isUiObservationReport(report)) {
28259
- return report;
28305
+ const action = destinationExists ? "relinked" : "linked";
28306
+ if (opts.dryRun) {
28307
+ log(` would ${action === "linked" ? "link" : "relink"} ${label}`);
28308
+ } else {
28309
+ if (destinationExists) {
28310
+ rmSync2(destinationDir, { recursive: true, force: true });
28311
+ }
28312
+ mkdirSync14(dirname9(destinationDir), { recursive: true });
28313
+ symlinkSync(
28314
+ sourceDir,
28315
+ destinationDir,
28316
+ process.platform === "win32" ? "junction" : "dir"
28317
+ );
28318
+ log(` ${action === "linked" ? "link" : "relink"} ${label}`);
28260
28319
  }
28261
- sawInvalidDraft = true;
28262
- }
28263
- if (lastRequestError && !sawUnparseableDraft && !sawInvalidDraft) {
28264
- throw lastRequestError;
28265
- }
28266
- if (sawInvalidDraft) {
28267
- return uncertainReport(
28268
- input8,
28269
- "Vision model returned an invalid UI report draft."
28270
- );
28320
+ results.push({ source: sourceDir, destination: destinationDir, action });
28271
28321
  }
28272
- return uncertainReport(
28273
- input8,
28274
- "Vision model returned output that could not be parsed as JSON."
28275
- );
28276
- }
28277
- 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.";
28278
- function visionSchema(language) {
28279
- return `{
28280
- "kind": "progress | step-completed | error | help-seeking | uncertain",
28281
- "summary": "short factual UI summary in ${language}",
28282
- "actions": [{"type": "click | shortcut | typing | scroll | window-change", "target": "optional UI target", "result": "optional visible result"}],
28283
- "candidateTokens": [{"slug": "optional-kebab-case-skill-token", "confidence": 0.0, "rationale": "why this token may matter"}],
28284
- "confidence": 0.0
28285
- }`;
28322
+ return results;
28286
28323
  }
28287
- function visionUserText(args, language) {
28288
- 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.";
28289
- return `${intro}
28290
- Application process: ${args.input.application.processName}
28291
- Window title: ${args.input.application.windowTitle ?? "(unknown)"}
28292
-
28293
- Return this JSON draft only:
28294
- ${visionSchema(language)}`;
28324
+ function inspectSkillLinks(cwd = process.cwd(), agents = parseSetupAgents()) {
28325
+ const results = [];
28326
+ for (const { from, to, agents: pairAgents } of SKILL_PAIRS) {
28327
+ if (!pairAgents.some((agent) => agents.has(agent))) continue;
28328
+ const sourceDir = dirname9(from);
28329
+ const destinationDir = dirname9(join20(cwd, to));
28330
+ results.push({
28331
+ agents: pairAgents,
28332
+ source: sourceDir,
28333
+ destination: destinationDir,
28334
+ state: classifySkillDestination(sourceDir, destinationDir)
28335
+ });
28336
+ }
28337
+ return results;
28295
28338
  }
28296
- async function requestVisionDraft(args) {
28297
- if (args.apiFlavor === "anthropic-messages") {
28298
- return requestAnthropicVisionDraft(args);
28339
+ function summarizeSkillLinkHealth(inspections) {
28340
+ if (inspections.some((item) => item.state === "unmanaged")) {
28341
+ return "unmanaged";
28299
28342
  }
28300
- return requestChatCompletionsVisionDraft(args);
28343
+ const repairable = ["missing", "broken", "stale-copy"];
28344
+ if (inspections.some((item) => repairable.includes(item.state))) {
28345
+ return "needs-repair";
28346
+ }
28347
+ return "healthy";
28301
28348
  }
28302
- async function requestChatCompletionsVisionDraft(args) {
28303
- const language = LANGUAGE_NAMES2[args.locale] ?? "English";
28304
- const res = await fetchWithInteractiveTimeout(
28305
- `${args.url}/chat/completions`,
28306
- {
28307
- method: "POST",
28308
- headers: {
28309
- "Content-Type": "application/json",
28310
- Authorization: `Bearer ${args.apiKey}`
28311
- },
28312
- body: JSON.stringify({
28313
- model: args.model,
28314
- messages: [
28315
- { role: "system", content: VISION_SYSTEM_PROMPT },
28316
- {
28317
- role: "user",
28318
- content: [
28319
- { type: "text", text: visionUserText(args, language) },
28320
- ...args.imageUrls.map((url) => ({
28321
- type: "image_url",
28322
- image_url: { url }
28323
- }))
28324
- ]
28325
- }
28326
- ],
28327
- temperature: 0,
28328
- max_tokens: args.input.maxTokens ?? DEFAULT_LLM_MAX_TOKENS,
28329
- ...args.url.includes("11434") || args.url.includes("localhost") || args.url.includes("127.0.0.1") ? { options: { num_ctx: 32768 } } : {}
28330
- }),
28331
- locale: args.locale,
28332
- hardTimeoutMs: args.input.hardTimeoutMs ?? 18e4
28349
+ var ZAM_BLOCK_START = "<!-- ZAM:START -->";
28350
+ var ZAM_BLOCK_END = "<!-- ZAM:END -->";
28351
+ function upsertMarkedBlock(dest, blockBody, dryRun) {
28352
+ const block = `${ZAM_BLOCK_START}
28353
+ ${blockBody.trim()}
28354
+ ${ZAM_BLOCK_END}`;
28355
+ if (!existsSync19(dest)) {
28356
+ if (!dryRun) {
28357
+ mkdirSync14(dirname9(dest), { recursive: true });
28358
+ writeFileSync12(dest, `${block}
28359
+ `, "utf8");
28333
28360
  }
28334
- );
28335
- if (!res.ok) {
28336
- const errorText = await res.text().catch(() => "");
28337
- if (errorText.includes("image") && (errorText.includes("not support") || errorText.includes("unsupported"))) {
28338
- throw new Error(
28339
- `Vision model "${args.model}" does not support image input. Set a multimodal model: zam settings set llm.vision.model <model>`
28340
- );
28361
+ return "write";
28362
+ }
28363
+ const existing = readFileSync15(dest, "utf8");
28364
+ if (existing.includes(block)) return "skip";
28365
+ const start = existing.indexOf(ZAM_BLOCK_START);
28366
+ const end = existing.indexOf(ZAM_BLOCK_END);
28367
+ const next = start >= 0 && end > start ? `${existing.slice(0, start)}${block}${existing.slice(end + ZAM_BLOCK_END.length)}` : `${existing.trimEnd()}
28368
+
28369
+ ${block}
28370
+ `;
28371
+ if (!dryRun) writeFileSync12(dest, next, "utf8");
28372
+ return start >= 0 && end > start ? "update" : "write";
28373
+ }
28374
+ function logInstructionAction(action, label, dryRun) {
28375
+ if (action === "skip") {
28376
+ console.log(` skip ${label} (ZAM block already present)`);
28377
+ } else {
28378
+ console.log(` ${dryRun ? "would " : ""}${action} ${label}`);
28379
+ }
28380
+ }
28381
+ function writeClaudeMd(skipClaudeMd, cwd = process.cwd(), opts = {}) {
28382
+ if (skipClaudeMd) return;
28383
+ const dest = join20(cwd, "CLAUDE.md");
28384
+ if (existsSync19(dest)) {
28385
+ if (!opts.updateExisting) {
28386
+ console.log(` skip CLAUDE.md (already present)`);
28387
+ return;
28341
28388
  }
28342
- throw new Error(
28343
- `Vision LLM request failed: ${res.statusText} (${res.status}) - ${errorText}`
28389
+ const action = upsertMarkedBlock(
28390
+ dest,
28391
+ `## ZAM learning sessions
28392
+
28393
+ ZAM is available in this repository. Use the \`zam\` skill in Claude Code to turn real work into an observed learning session with active recall and FSRS scheduling.
28394
+
28395
+ - Skill files live under \`.claude/skills/zam/\`.
28396
+ - Fast-changing review data lives in \`~/.zam/\`, not in this repository.
28397
+ - The skill directory is linked to this ZAM installation and updates with it.`,
28398
+ Boolean(opts.dryRun)
28344
28399
  );
28400
+ logInstructionAction(action, "CLAUDE.md", Boolean(opts.dryRun));
28401
+ return;
28345
28402
  }
28346
- const data = await res.json();
28347
- if (data.error !== void 0) {
28348
- const errorMsg = formatModelError(data.error);
28349
- if (errorMsg.includes("image") && (errorMsg.includes("not support") || errorMsg.includes("unsupported"))) {
28350
- throw new Error(
28351
- `Vision model "${args.model}" does not support image input. Set a multimodal model: zam settings set llm.vision.model <model>`
28352
- );
28403
+ const name = basename4(cwd);
28404
+ const content = `# ZAM Personal Kernel \u2014 ${name}
28405
+
28406
+ This is a ZAM personal instance. ZAM builds lasting skills through spaced
28407
+ repetition during real work \u2014 not separate study sessions.
28408
+
28409
+ ## First time here?
28410
+ Run \`/setup\` in Claude Code or Gemini CLI to complete first-time setup.
28411
+
28412
+ ## Regular use
28413
+ Run \`/zam\` to start a learning session on whatever you are working on.
28414
+
28415
+ ## What lives here
28416
+ - \`beliefs/\` \u2014 your worldview, approved by git commit
28417
+ - \`goals/\` \u2014 your objectives, decomposed into tasks and learning tokens
28418
+
28419
+ ## Fast-changing data
28420
+ Learning tokens, cards, and review history live in local SQLite by default.
28421
+ Use \`zam connector setup turso\` to store cloud credentials in
28422
+ \`~/.zam/credentials.json\` and use a Turso database across machines.
28423
+ `;
28424
+ if (opts.dryRun) {
28425
+ console.log(` would write CLAUDE.md`);
28426
+ } else {
28427
+ writeFileSync12(dest, content, "utf8");
28428
+ console.log(` write CLAUDE.md`);
28429
+ }
28430
+ }
28431
+ function writeAgentsMd(skipAgentsMd, cwd = process.cwd(), opts = {}) {
28432
+ if (skipAgentsMd) return;
28433
+ const dest = join20(cwd, "AGENTS.md");
28434
+ if (existsSync19(dest)) {
28435
+ if (!opts.updateExisting) {
28436
+ console.log(` skip AGENTS.md (already present)`);
28437
+ return;
28353
28438
  }
28354
- throw new Error(`Vision model failed: ${errorMsg}`);
28439
+ const action = upsertMarkedBlock(
28440
+ dest,
28441
+ `## ZAM learning sessions
28442
+
28443
+ ZAM is available in this repository. Select the \`zam\` skill through \`/skills\` or invoke \`$zam\` where supported to turn real work into an observed learning session with active recall and FSRS scheduling.
28444
+
28445
+ - Skill files live under \`.agents/skills/zam/\`.
28446
+ - Fast-changing review data lives in \`~/.zam/\`, not in this repository.
28447
+ - The skill directory is linked to this ZAM installation and updates with it.`,
28448
+ Boolean(opts.dryRun)
28449
+ );
28450
+ logInstructionAction(action, "AGENTS.md", Boolean(opts.dryRun));
28451
+ return;
28355
28452
  }
28356
- const content = data.choices?.[0]?.message?.content;
28357
- if (!content || typeof content !== "string") {
28358
- throw new Error("Empty response from vision model");
28453
+ const name = basename4(cwd);
28454
+ const content = `# ZAM Personal Kernel - ${name}
28455
+
28456
+ This is a ZAM personal instance. ZAM builds lasting skills through spaced
28457
+ repetition during real work, not separate study sessions.
28458
+
28459
+ ## First time here?
28460
+ Run \`zam setup\` from the shell. When this repository includes
28461
+ \`.agents/skills/setup/\`, you can instead select \`setup\` through \`/skills\`
28462
+ or invoke \`$setup\`.
28463
+
28464
+ ## Regular use
28465
+ Select the \`zam\` skill through \`/skills\` or invoke \`$zam\` to start a
28466
+ learning session on whatever you are working on.
28467
+
28468
+ ## What lives here
28469
+ - \`beliefs/\` - your worldview, approved by git commit
28470
+ - \`goals/\` - your objectives, decomposed into tasks and learning tokens
28471
+
28472
+ ## Fast-changing data
28473
+ Learning tokens, cards, and review history live in local SQLite by default.
28474
+ Use \`zam connector setup turso\` to store cloud credentials in
28475
+ \`~/.zam/credentials.json\` and use a Turso database across machines.
28476
+
28477
+ ## Codex skills
28478
+ Codex discovers repository skills under \`.agents/skills/\`. Run
28479
+ \`zam setup --force\` after upgrading \`zam-core\` to refresh them.
28480
+ `;
28481
+ if (opts.dryRun) {
28482
+ console.log(` would write AGENTS.md`);
28483
+ } else {
28484
+ writeFileSync12(dest, content, "utf8");
28485
+ console.log(` write AGENTS.md`);
28359
28486
  }
28360
- return content.trim();
28361
28487
  }
28362
- function dataUrlToAnthropicImage(dataUrl) {
28363
- const match = dataUrl.match(/^data:(image\/[a-zA-Z]+);base64,(.+)$/);
28364
- if (!match) {
28365
- throw new Error("Unsupported image data for Anthropic vision request");
28488
+ function writeCopilotInstructions(cwd = process.cwd(), opts = {}) {
28489
+ const dest = join20(cwd, ".github", "copilot-instructions.md");
28490
+ const action = upsertMarkedBlock(
28491
+ dest,
28492
+ `## ZAM learning sessions
28493
+
28494
+ ZAM is available in this repository through \`.claude/skills/zam/\`. Use the \`zam\` project skill from Copilot-compatible skill selection surfaces to turn real work into an observed learning session with active recall and FSRS scheduling.
28495
+
28496
+ - Fast-changing review data lives in \`~/.zam/\`, not in this repository.
28497
+ - The skill directory is linked to this ZAM installation and updates with it.`,
28498
+ Boolean(opts.dryRun)
28499
+ );
28500
+ logInstructionAction(
28501
+ action,
28502
+ ".github/copilot-instructions.md",
28503
+ Boolean(opts.dryRun)
28504
+ );
28505
+ }
28506
+
28507
+ // src/cli/install-repair.ts
28508
+ function defaultRepairWorkspaces() {
28509
+ const summary = {
28510
+ provisioned: 0,
28511
+ missing: 0,
28512
+ relinked: 0
28513
+ };
28514
+ try {
28515
+ const agents = parseSetupAgents();
28516
+ for (const workspace of getConfiguredWorkspaces()) {
28517
+ if (!existsSync20(workspace.path)) {
28518
+ summary.missing += 1;
28519
+ continue;
28520
+ }
28521
+ const results = wireSkills(workspace.path, agents, { quiet: true });
28522
+ summary.provisioned += 1;
28523
+ summary.relinked += results.filter(
28524
+ (result) => result.action === "linked" || result.action === "relinked"
28525
+ ).length;
28526
+ }
28527
+ } catch (error) {
28528
+ summary.error = error instanceof Error ? error.message : String(error);
28366
28529
  }
28530
+ return summary;
28531
+ }
28532
+ function defaultConnectAgents(deps) {
28533
+ const report = performAgentConnect({}, deps);
28534
+ const companion = report.results.find((result) => result.extension?.kind === "vscode")?.extension?.action ?? null;
28367
28535
  return {
28368
- type: "image",
28369
- source: { type: "base64", media_type: match[1], data: match[2] }
28536
+ success: report.success,
28537
+ detected: report.detected,
28538
+ connected: report.results.filter(
28539
+ (result) => !result.error && (result.alreadyConfigured || result.wrote)
28540
+ ).length,
28541
+ companion,
28542
+ errors: report.results.flatMap(
28543
+ (result) => result.error ? [`${result.label}: ${result.error}`] : []
28544
+ )
28370
28545
  };
28371
28546
  }
28372
- async function requestAnthropicVisionDraft(args) {
28373
- const language = LANGUAGE_NAMES2[args.locale] ?? "English";
28374
- const base = args.url.replace(/\/+$/, "").replace(/\/v1$/, "");
28375
- const res = await fetchWithInteractiveTimeout(`${base}/v1/messages`, {
28376
- method: "POST",
28377
- headers: {
28378
- "Content-Type": "application/json",
28379
- "x-api-key": args.apiKey,
28380
- "anthropic-version": "2023-06-01"
28381
- },
28382
- body: JSON.stringify({
28383
- model: args.model,
28384
- max_tokens: args.input.maxTokens ?? DEFAULT_LLM_MAX_TOKENS,
28385
- system: VISION_SYSTEM_PROMPT,
28386
- messages: [
28387
- {
28388
- role: "user",
28389
- content: [
28390
- { type: "text", text: visionUserText(args, language) },
28391
- ...args.imageUrls.map(dataUrlToAnthropicImage)
28392
- ]
28393
- }
28394
- ]
28395
- }),
28396
- locale: args.locale,
28397
- hardTimeoutMs: args.input.hardTimeoutMs ?? 18e4
28398
- });
28399
- if (!res.ok) {
28400
- const errorText = await res.text().catch(() => "");
28401
- throw new Error(
28402
- `Vision LLM request failed: ${res.statusText} (${res.status}) - ${errorText}`
28403
- );
28404
- }
28405
- const data = await res.json();
28406
- if (data.error !== void 0) {
28407
- throw new Error(`Vision model failed: ${formatModelError(data.error)}`);
28547
+ function performInstallRepair(opts = {}, deps = {}) {
28548
+ const version = (deps.version ?? currentVersion)();
28549
+ const getLastRepaired = deps.getLastRepaired ?? getLastRepairedVersion;
28550
+ if (opts.ifVersionChanged && getLastRepaired() === version) {
28551
+ return {
28552
+ version,
28553
+ skipped: true,
28554
+ cli: null,
28555
+ workspaces: null,
28556
+ agents: null
28557
+ };
28408
28558
  }
28409
- if (data.stop_reason === "refusal") {
28410
- throw new Error("Vision model refused the request (safety classifier).");
28559
+ const cli = (deps.installCli ?? installCliShim)();
28560
+ const workspaces = (deps.repairWorkspaces ?? defaultRepairWorkspaces)();
28561
+ const agentDeps = {};
28562
+ if (cli.status === "installed" || cli.status === "refreshed" || cli.status === "ok") {
28563
+ agentDeps.findZam = () => cli.shimPath;
28411
28564
  }
28412
- const text = data.content?.find(
28413
- (b) => b.type === "text" && typeof b.text === "string"
28414
- )?.text;
28415
- if (!text) {
28416
- throw new Error("Empty response from vision model");
28565
+ let agents;
28566
+ try {
28567
+ agents = (deps.connectAgents ?? defaultConnectAgents)(agentDeps);
28568
+ } catch (error) {
28569
+ agents = {
28570
+ success: false,
28571
+ detected: [],
28572
+ connected: 0,
28573
+ companion: null,
28574
+ errors: [error instanceof Error ? error.message : String(error)]
28575
+ };
28417
28576
  }
28418
- return text.trim();
28577
+ (deps.setLastRepaired ?? setLastRepairedVersion)(version);
28578
+ return { version, skipped: false, cli, workspaces, agents };
28419
28579
  }
28420
- function extractDraft(content) {
28421
- const fenced = content.match(/```(?:json)?\s*([\s\S]*?)```/i);
28422
- const candidate = (fenced?.[1] ?? content).trim();
28423
- try {
28424
- return JSON.parse(candidate);
28425
- } catch {
28426
- const start = candidate.indexOf("{");
28427
- const end = candidate.lastIndexOf("}");
28428
- if (start === -1 || end <= start) throw new Error("no JSON object found");
28429
- return JSON.parse(
28430
- candidate.slice(start, end + 1)
28431
- );
28580
+
28581
+ // src/cli/llm/capability-probe.ts
28582
+ init_kernel();
28583
+ var EMBEDDING_MODEL_HINTS = [
28584
+ "embed",
28585
+ "text-embedding",
28586
+ "bge-",
28587
+ "gte-",
28588
+ "nomic",
28589
+ "mxbai"
28590
+ ];
28591
+ var VISION_MODEL_HINTS = [
28592
+ "vision",
28593
+ "-vl",
28594
+ "vl-",
28595
+ "vlm",
28596
+ "llava",
28597
+ "gpt-4o",
28598
+ "gpt-4.1",
28599
+ "gpt-5",
28600
+ "gemini",
28601
+ "pixtral",
28602
+ "minicpm-v",
28603
+ "internvl",
28604
+ "moondream",
28605
+ "llama-3.2",
28606
+ "llama3.2",
28607
+ // Xiaomi MiMo(-VL) is multimodal; the plain "mimo-v*" tag carries no "-vl".
28608
+ "mimo"
28609
+ ];
28610
+ function matchesAny(id, hints) {
28611
+ const lower = id.toLowerCase();
28612
+ return hints.some((hint) => lower.includes(hint));
28613
+ }
28614
+ function catalogHasModel(catalog, model) {
28615
+ const lower = model.toLowerCase();
28616
+ return catalog.some((id) => id.toLowerCase() === lower);
28617
+ }
28618
+ function classifyCapabilities(entry, catalog, catalogKnown, dimProbeEmbedding = false) {
28619
+ const detected = emptyCapabilityFlags();
28620
+ if (entry.apiFlavor === "anthropic-messages") {
28621
+ detected.text = true;
28622
+ detected.image = true;
28623
+ return detected;
28432
28624
  }
28625
+ const looksEmbedding = matchesAny(entry.model, EMBEDDING_MODEL_HINTS);
28626
+ const looksVision = matchesAny(entry.model, VISION_MODEL_HINTS);
28627
+ const inCatalog = catalogHasModel(catalog, entry.model);
28628
+ detected.embedding = looksEmbedding || dimProbeEmbedding;
28629
+ detected.image = looksVision;
28630
+ detected.text = !detected.embedding && (inCatalog || !catalogKnown);
28631
+ return detected;
28433
28632
  }
28434
- function buildReport(input8, draft) {
28435
- return {
28436
- version: UI_OBSERVATION_PROTOCOL_VERSION,
28437
- sessionId: input8.sessionId,
28438
- sequence: input8.sequence,
28439
- observedFrom: input8.observedFrom,
28440
- observedTo: input8.observedTo,
28441
- kind: parseKind(draft.kind),
28442
- application: input8.application,
28443
- summary: parseSummary(draft.summary),
28444
- actions: parseActions(draft.actions),
28445
- evidence: [
28446
- {
28447
- type: "keyframe",
28448
- ref: input8.evidenceRef ?? basename4(input8.imagePath),
28449
- redacted: input8.redacted ?? false
28450
- }
28451
- ],
28452
- candidateTokens: parseCandidateTokens(draft.candidateTokens),
28453
- confidence: parseConfidence(draft.confidence, 0.35)
28454
- };
28633
+ function resolveApiKey(apiKeyRef) {
28634
+ if (!apiKeyRef) return DEFAULT_LLM_API_KEY;
28635
+ return getProviderApiKey(apiKeyRef) ?? DEFAULT_LLM_API_KEY;
28455
28636
  }
28456
- function uncertainReport(input8, summary) {
28637
+ async function probeModelCapabilities(entry, opts = {}) {
28638
+ const apiKey = resolveApiKey(entry.apiKeyRef);
28639
+ if (entry.apiFlavor === "anthropic-messages") {
28640
+ const online2 = await isLlmOnline(entry.url);
28641
+ return {
28642
+ reachable: online2,
28643
+ catalog: [],
28644
+ detected: classifyCapabilities(entry, [], false)
28645
+ };
28646
+ }
28647
+ const online = await isLlmOnline(entry.url);
28648
+ if (!online) {
28649
+ return { reachable: false, catalog: [], detected: emptyCapabilityFlags() };
28650
+ }
28651
+ const catalog = await getAvailableModels(entry.url, apiKey);
28652
+ const catalogKnown = catalog.length > 0;
28653
+ let dimProbeEmbedding = false;
28654
+ const looksEmbedding = matchesAny(entry.model, EMBEDDING_MODEL_HINTS);
28655
+ if (opts.embeddingDimProbe && !catalogKnown && !looksEmbedding) {
28656
+ try {
28657
+ const [vector] = await embedTexts(
28658
+ { url: entry.url, model: entry.model, apiKey },
28659
+ ["capability probe"]
28660
+ );
28661
+ dimProbeEmbedding = Array.isArray(vector) && vector.length > 0;
28662
+ } catch {
28663
+ dimProbeEmbedding = false;
28664
+ }
28665
+ }
28457
28666
  return {
28458
- version: UI_OBSERVATION_PROTOCOL_VERSION,
28459
- sessionId: input8.sessionId,
28460
- sequence: input8.sequence,
28461
- observedFrom: input8.observedFrom,
28462
- observedTo: input8.observedTo,
28463
- kind: "uncertain",
28464
- application: input8.application,
28465
- summary,
28466
- actions: [],
28467
- evidence: [
28468
- {
28469
- type: "keyframe",
28470
- ref: input8.evidenceRef ?? basename4(input8.imagePath),
28471
- redacted: input8.redacted ?? false
28472
- }
28473
- ],
28474
- candidateTokens: [],
28475
- confidence: 0.1
28667
+ reachable: true,
28668
+ catalog,
28669
+ detected: classifyCapabilities(
28670
+ entry,
28671
+ catalog,
28672
+ catalogKnown,
28673
+ dimProbeEmbedding
28674
+ )
28476
28675
  };
28477
28676
  }
28478
- function parseKind(value) {
28479
- if (typeof value === "string" && OBSERVATION_KINDS2.has(value)) {
28480
- return value;
28677
+ function reconcileCapabilities(userSelected, detected) {
28678
+ const result = emptyCapabilityFlags();
28679
+ for (const key of Object.keys(result)) {
28680
+ result[key] = userSelected[key] && detected[key];
28481
28681
  }
28482
- return "uncertain";
28682
+ return result;
28483
28683
  }
28484
- function parseSummary(value) {
28485
- if (typeof value === "string" && value.trim().length > 0) {
28486
- return value.trim();
28684
+ function validateModelSave(entry, probe, now = () => (/* @__PURE__ */ new Date()).toISOString()) {
28685
+ if (!probe.reachable) {
28686
+ return {
28687
+ ok: false,
28688
+ error: "Endpoint is unreachable \u2014 cannot verify capabilities. Bring it online and retry."
28689
+ };
28487
28690
  }
28488
- return "Vision model did not provide a factual UI summary.";
28489
- }
28490
- function parseActions(value) {
28491
- if (!Array.isArray(value)) return [];
28492
- return value.flatMap((item) => {
28493
- if (!isRecord2(item) || typeof item.type !== "string") return [];
28494
- if (!ACTION_TYPES2.has(item.type)) return [];
28495
- const action = { type: item.type };
28496
- if (typeof item.target === "string" && item.target.trim()) {
28497
- action.target = item.target.trim();
28498
- }
28499
- if (typeof item.result === "string" && item.result.trim()) {
28500
- action.result = item.result.trim();
28501
- }
28502
- return [action];
28503
- });
28504
- }
28505
- function parseCandidateTokens(value) {
28506
- if (!Array.isArray(value)) return [];
28507
- return value.flatMap((item) => {
28508
- if (!isRecord2(item)) return [];
28509
- if (typeof item.slug !== "string" || !/^[A-Za-z0-9._-]+$/.test(item.slug)) {
28510
- return [];
28511
- }
28512
- if (typeof item.rationale !== "string" || !item.rationale.trim()) {
28513
- return [];
28691
+ return {
28692
+ ok: true,
28693
+ entry: {
28694
+ ...entry,
28695
+ capabilities: reconcileCapabilities(entry.capabilities, probe.detected),
28696
+ detectedCapabilities: probe.detected,
28697
+ probedAt: now()
28514
28698
  }
28515
- return [
28516
- {
28517
- slug: item.slug,
28518
- confidence: parseConfidence(item.confidence, 0.2),
28519
- rationale: item.rationale.trim()
28520
- }
28521
- ];
28522
- });
28523
- }
28524
- function parseConfidence(value, fallback) {
28525
- if (typeof value !== "number" || !Number.isFinite(value)) return fallback;
28526
- return Math.max(0, Math.min(1, value));
28527
- }
28528
- function formatModelError(error) {
28529
- if (typeof error === "string") return error;
28530
- if (isRecord2(error) && typeof error.message === "string") {
28531
- return error.message;
28532
- }
28533
- return JSON.stringify(error);
28534
- }
28535
- function isRecord2(value) {
28536
- return typeof value === "object" && value !== null && !Array.isArray(value);
28699
+ };
28537
28700
  }
28538
28701
 
28539
- // src/cli/providers/config.ts
28540
- init_kernel();
28541
-
28542
- // src/cli/commands/shared/db.ts
28702
+ // src/cli/llm/vision.ts
28543
28703
  init_kernel();
28544
- function defaultErrorHandler(message) {
28545
- console.error("Error:", message);
28546
- process.exit(1);
28547
- }
28548
- async function withDb(fn, onError = defaultErrorHandler) {
28549
- let db;
28550
- try {
28551
- db = await openDatabase();
28552
- await fn(db);
28553
- } catch (err) {
28554
- onError(err.message);
28555
- } finally {
28556
- await db?.close();
28704
+ import { randomBytes } from "crypto";
28705
+ import { readFileSync as readFileSync16 } from "fs";
28706
+ import { tmpdir as tmpdir2 } from "os";
28707
+ import { basename as basename5, join as join21 } from "path";
28708
+ var LANGUAGE_NAMES2 = {
28709
+ en: "English",
28710
+ de: "German",
28711
+ es: "Spanish",
28712
+ fr: "French",
28713
+ pt: "Portuguese",
28714
+ zh: "Chinese",
28715
+ ja: "Japanese"
28716
+ };
28717
+ var OBSERVATION_KINDS2 = /* @__PURE__ */ new Set([
28718
+ "progress",
28719
+ "step-completed",
28720
+ "error",
28721
+ "help-seeking",
28722
+ "uncertain",
28723
+ "privacy-pause",
28724
+ "heartbeat"
28725
+ ]);
28726
+ var ACTION_TYPES2 = /* @__PURE__ */ new Set([
28727
+ "click",
28728
+ "shortcut",
28729
+ "typing",
28730
+ "scroll",
28731
+ "window-change"
28732
+ ]);
28733
+ async function observeUiSnapshotViaLLM(db, input8) {
28734
+ const cfg = await getProviderForRole(db, "vision");
28735
+ if (!cfg.enabled) {
28736
+ throw new Error(
28737
+ "Vision observation is disabled in settings (llm.vision.enabled)"
28738
+ );
28557
28739
  }
28558
- }
28559
- function jsonOut(data) {
28560
- console.log(JSON.stringify(data, null, 2));
28561
- }
28562
-
28563
- // src/cli/providers/config.ts
28564
- var VALID_API_FLAVORS = [
28565
- "chat-completions",
28566
- "anthropic-messages"
28567
- ];
28568
- var VALID_ROLES = ["vision", "recall", "text", "embedding"];
28569
- function upsertProviderRecord(providers, name, patch) {
28570
- const merged = { ...providers[name] ?? {} };
28571
- if (patch.url !== void 0) merged.url = patch.url;
28572
- if (patch.model !== void 0) merged.model = patch.model;
28573
- if (patch.apiFlavor !== void 0) merged.apiFlavor = patch.apiFlavor;
28574
- if (patch.apiKeyRef !== void 0) merged.apiKeyRef = patch.apiKeyRef;
28575
- if (patch.label !== void 0) merged.label = patch.label;
28576
- if (patch.local !== void 0) merged.local = patch.local;
28577
- if (patch.runner !== void 0) merged.runner = patch.runner;
28578
- return { ...providers, [name]: merged };
28579
- }
28580
- function removeProviderRecord(providers, name) {
28581
- if (!(name in providers)) return { providers, removed: false };
28582
- const next = { ...providers };
28583
- delete next[name];
28584
- return { providers: next, removed: true };
28585
- }
28586
- function rolesReferencing(roles, name) {
28587
- return VALID_ROLES.filter((role) => {
28588
- const binding = roles[role];
28589
- return binding?.primary === name || binding?.fallback === name;
28590
- });
28591
- }
28592
- function bindRoleProviders(roles, role, primary, fallback) {
28593
- const binding = { primary };
28594
- if (fallback) binding.fallback = fallback;
28595
- return { ...roles, [role]: binding };
28596
- }
28597
- function unbindRole(roles, role) {
28598
- if (!(role in roles)) return roles;
28599
- const next = { ...roles };
28600
- delete next[role];
28601
- return next;
28602
- }
28603
- function maskSecret(key) {
28604
- return key.length <= 4 ? "\u2022\u2022\u2022\u2022" : `\u2026${key.slice(-4)}`;
28605
- }
28606
- function buildProviderListing(providers, hasKey) {
28607
- return Object.entries(providers).map(([name, rec]) => {
28608
- const apiFlavor = rec.apiFlavor ?? (rec.url ? inferApiFlavor(rec.url) : "chat-completions");
28609
- let keyState;
28610
- if (!rec.apiKeyRef) keyState = "none";
28611
- else keyState = hasKey(rec.apiKeyRef) ? "set" : "missing";
28612
- const row = {
28613
- name,
28614
- url: rec.url,
28615
- model: rec.model,
28616
- apiFlavor,
28617
- apiKeyRef: rec.apiKeyRef,
28618
- keyState
28619
- };
28620
- if (rec.label !== void 0) row.label = rec.label;
28621
- if (rec.local !== void 0) row.local = rec.local;
28622
- if (rec.runner !== void 0) row.runner = rec.runner;
28623
- return row;
28624
- });
28625
- }
28626
- function findOrphanKeyRefs(storedRefs, providers) {
28627
- const used = new Set(
28628
- Object.values(providers).map((rec) => rec.apiKeyRef).filter((ref) => !!ref)
28629
- );
28630
- return storedRefs.filter((ref) => !used.has(ref));
28631
- }
28632
- async function readJson(db, key, fallback) {
28633
- const raw = await getSetting(db, key);
28634
- if (!raw) return fallback;
28635
- try {
28636
- return JSON.parse(raw);
28637
- } catch {
28638
- return fallback;
28740
+ const imageUrls = [];
28741
+ const isVideo = /\.(mp4|mov|m4v|avi|mkv|webm)$/i.test(input8.imagePath);
28742
+ if (isVideo) {
28743
+ const { mkdirSync: mkdirSync19, readdirSync: readdirSync3, rmSync: rmSync4 } = await import("fs");
28744
+ const { execSync: execSync6 } = await import("child_process");
28745
+ const tempDir = join21(
28746
+ tmpdir2(),
28747
+ `zam-frames-${randomBytes(4).toString("hex")}`
28748
+ );
28749
+ mkdirSync19(tempDir, { recursive: true });
28750
+ try {
28751
+ execSync6(
28752
+ `ffmpeg -i "${input8.imagePath}" -vf "fps=1/3,scale=1280:-1" -vsync vfr "${tempDir}/frame_%03d.png"`,
28753
+ { stdio: "ignore" }
28754
+ );
28755
+ let files = readdirSync3(tempDir).filter((f) => f.endsWith(".png")).sort();
28756
+ if (files.length === 0) {
28757
+ execSync6(
28758
+ `ffmpeg -i "${input8.imagePath}" -vframes 1 "${tempDir}/frame_001.png"`,
28759
+ { stdio: "ignore" }
28760
+ );
28761
+ files = readdirSync3(tempDir).filter((f) => f.endsWith(".png")).sort();
28762
+ }
28763
+ const maxFrames = cfg.maxFrames ?? 100;
28764
+ let sampledFiles = files;
28765
+ if (files.length > maxFrames) {
28766
+ if (maxFrames <= 1) {
28767
+ sampledFiles = [files[0]];
28768
+ } else {
28769
+ const step = (files.length - 1) / (maxFrames - 1);
28770
+ sampledFiles = [];
28771
+ for (let i = 0; i < maxFrames; i++) {
28772
+ const index = Math.round(i * step);
28773
+ sampledFiles.push(files[index]);
28774
+ }
28775
+ }
28776
+ }
28777
+ for (const file of sampledFiles) {
28778
+ const bytes = readFileSync16(join21(tempDir, file));
28779
+ imageUrls.push(`data:image/png;base64,${bytes.toString("base64")}`);
28780
+ }
28781
+ } finally {
28782
+ try {
28783
+ rmSync4(tempDir, { recursive: true, force: true });
28784
+ } catch {
28785
+ }
28786
+ }
28787
+ } else {
28788
+ const imageBytes = readFileSync16(input8.imagePath);
28789
+ const ext = input8.imagePath.split(".").pop()?.toLowerCase();
28790
+ const mime = ext === "jpg" || ext === "jpeg" ? "image/jpeg" : "image/png";
28791
+ imageUrls.push(`data:${mime};base64,${imageBytes.toString("base64")}`);
28792
+ }
28793
+ if (imageUrls.length === 0) {
28794
+ throw new Error("No image data available for vision analysis");
28795
+ }
28796
+ const endpoints = [
28797
+ {
28798
+ url: cfg.url,
28799
+ apiKey: cfg.apiKey || DEFAULT_LLM_API_KEY,
28800
+ model: input8.model ?? cfg.model,
28801
+ apiFlavor: cfg.apiFlavor
28802
+ }
28803
+ ];
28804
+ if (cfg.fallback) {
28805
+ endpoints.push({
28806
+ url: cfg.fallback.url,
28807
+ apiKey: cfg.fallback.apiKey || DEFAULT_LLM_API_KEY,
28808
+ model: cfg.fallback.model,
28809
+ apiFlavor: cfg.fallback.apiFlavor
28810
+ });
28811
+ }
28812
+ let lastRequestError;
28813
+ let sawUnparseableDraft = false;
28814
+ let sawInvalidDraft = false;
28815
+ for (const endpoint of endpoints) {
28816
+ let content;
28817
+ try {
28818
+ content = await requestVisionDraft({
28819
+ ...endpoint,
28820
+ locale: cfg.locale,
28821
+ imageUrls,
28822
+ input: input8
28823
+ });
28824
+ } catch (err) {
28825
+ lastRequestError = err;
28826
+ continue;
28827
+ }
28828
+ let draft;
28829
+ try {
28830
+ draft = extractDraft(content);
28831
+ } catch {
28832
+ sawUnparseableDraft = true;
28833
+ continue;
28834
+ }
28835
+ const report = buildReport(input8, draft);
28836
+ if (isUiObservationReport(report)) {
28837
+ return report;
28838
+ }
28839
+ sawInvalidDraft = true;
28639
28840
  }
28640
- }
28641
- var readProviders = (db) => readJson(db, "llm.providers", {});
28642
- var readRoles = (db) => readJson(db, "llm.roles", {});
28643
- async function readScopedProviders(db, machine) {
28644
- if (machine) return getMachineAiConfig().providers ?? {};
28645
- if (!db)
28646
- throw new Error("Database is required for shared provider settings.");
28647
- return readProviders(db);
28648
- }
28649
- async function readScopedRoles(db, machine) {
28650
- if (machine) return getMachineAiConfig().roles ?? {};
28651
- if (!db)
28652
- throw new Error("Database is required for shared provider settings.");
28653
- return readRoles(db);
28654
- }
28655
- async function writeScopedProviders(db, machine, p) {
28656
- if (machine) {
28657
- const config = getMachineAiConfig();
28658
- saveMachineAiConfig({ ...config, providers: p });
28659
- return;
28841
+ if (lastRequestError && !sawUnparseableDraft && !sawInvalidDraft) {
28842
+ throw lastRequestError;
28660
28843
  }
28661
- if (!db)
28662
- throw new Error("Database is required for shared provider settings.");
28663
- await setSetting(db, "llm.providers", JSON.stringify(p));
28664
- }
28665
- async function writeScopedRoles(db, machine, r) {
28666
- if (machine) {
28667
- const config = getMachineAiConfig();
28668
- saveMachineAiConfig({ ...config, roles: r });
28669
- return;
28844
+ if (sawInvalidDraft) {
28845
+ return uncertainReport(
28846
+ input8,
28847
+ "Vision model returned an invalid UI report draft."
28848
+ );
28670
28849
  }
28671
- if (!db)
28672
- throw new Error("Database is required for shared provider settings.");
28673
- await setSetting(db, "llm.roles", JSON.stringify(r));
28850
+ return uncertainReport(
28851
+ input8,
28852
+ "Vision model returned output that could not be parsed as JSON."
28853
+ );
28674
28854
  }
28675
- async function withProviderScope(machine, action) {
28676
- if (machine) {
28677
- await action(void 0);
28678
- return;
28679
- }
28680
- await withDb(action);
28855
+ 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.";
28856
+ function visionSchema(language) {
28857
+ return `{
28858
+ "kind": "progress | step-completed | error | help-seeking | uncertain",
28859
+ "summary": "short factual UI summary in ${language}",
28860
+ "actions": [{"type": "click | shortcut | typing | scroll | window-change", "target": "optional UI target", "result": "optional visible result"}],
28861
+ "candidateTokens": [{"slug": "optional-kebab-case-skill-token", "confidence": 0.0, "rationale": "why this token may matter"}],
28862
+ "confidence": 0.0
28863
+ }`;
28681
28864
  }
28865
+ function visionUserText(args, language) {
28866
+ 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.";
28867
+ return `${intro}
28868
+ Application process: ${args.input.application.processName}
28869
+ Window title: ${args.input.application.windowTitle ?? "(unknown)"}
28682
28870
 
28683
- // src/cli/provisioning/index.ts
28684
- import {
28685
- existsSync as existsSync18,
28686
- lstatSync as lstatSync2,
28687
- mkdirSync as mkdirSync13,
28688
- readFileSync as readFileSync15,
28689
- realpathSync,
28690
- rmSync as rmSync2,
28691
- symlinkSync,
28692
- writeFileSync as writeFileSync11
28693
- } from "fs";
28694
- import { basename as basename5, dirname as dirname9, join as join20 } from "path";
28695
- import { fileURLToPath as fileURLToPath5 } from "url";
28696
- var packageRoot3 = [
28697
- fileURLToPath5(new URL("../..", import.meta.url)),
28698
- fileURLToPath5(new URL("../../..", import.meta.url))
28699
- ].find((candidate) => existsSync18(join20(candidate, "package.json"))) ?? fileURLToPath5(new URL("../..", import.meta.url));
28700
- var ALL_SETUP_AGENTS = ["claude", "copilot", "codex", "agent"];
28701
- var SKILL_PAIRS = [
28702
- {
28703
- from: join20(packageRoot3, ".claude", "skills", "zam", "SKILL.md"),
28704
- to: join20(".claude", "skills", "zam", "SKILL.md"),
28705
- agents: ["claude", "copilot"]
28706
- },
28707
- {
28708
- from: join20(packageRoot3, ".agent", "skills", "zam", "SKILL.md"),
28709
- to: join20(".agent", "skills", "zam", "SKILL.md"),
28710
- agents: ["agent"]
28711
- },
28712
- {
28713
- from: join20(packageRoot3, ".agents", "skills", "zam", "SKILL.md"),
28714
- to: join20(".agents", "skills", "zam", "SKILL.md"),
28715
- agents: ["codex"]
28871
+ Return this JSON draft only:
28872
+ ${visionSchema(language)}`;
28873
+ }
28874
+ async function requestVisionDraft(args) {
28875
+ if (args.apiFlavor === "anthropic-messages") {
28876
+ return requestAnthropicVisionDraft(args);
28716
28877
  }
28717
- ];
28718
- function parseSetupAgents(value) {
28719
- if (!value || value.trim().toLowerCase() === "all") {
28720
- return new Set(ALL_SETUP_AGENTS);
28878
+ return requestChatCompletionsVisionDraft(args);
28879
+ }
28880
+ async function requestChatCompletionsVisionDraft(args) {
28881
+ const language = LANGUAGE_NAMES2[args.locale] ?? "English";
28882
+ const res = await fetchWithInteractiveTimeout(
28883
+ `${args.url}/chat/completions`,
28884
+ {
28885
+ method: "POST",
28886
+ headers: {
28887
+ "Content-Type": "application/json",
28888
+ Authorization: `Bearer ${args.apiKey}`
28889
+ },
28890
+ body: JSON.stringify({
28891
+ model: args.model,
28892
+ messages: [
28893
+ { role: "system", content: VISION_SYSTEM_PROMPT },
28894
+ {
28895
+ role: "user",
28896
+ content: [
28897
+ { type: "text", text: visionUserText(args, language) },
28898
+ ...args.imageUrls.map((url) => ({
28899
+ type: "image_url",
28900
+ image_url: { url }
28901
+ }))
28902
+ ]
28903
+ }
28904
+ ],
28905
+ temperature: 0,
28906
+ max_tokens: args.input.maxTokens ?? DEFAULT_LLM_MAX_TOKENS,
28907
+ ...args.url.includes("11434") || args.url.includes("localhost") || args.url.includes("127.0.0.1") ? { options: { num_ctx: 32768 } } : {}
28908
+ }),
28909
+ locale: args.locale,
28910
+ hardTimeoutMs: args.input.hardTimeoutMs ?? 18e4
28911
+ }
28912
+ );
28913
+ if (!res.ok) {
28914
+ const errorText = await res.text().catch(() => "");
28915
+ if (errorText.includes("image") && (errorText.includes("not support") || errorText.includes("unsupported"))) {
28916
+ throw new Error(
28917
+ `Vision model "${args.model}" does not support image input. Set a multimodal model: zam settings set llm.vision.model <model>`
28918
+ );
28919
+ }
28920
+ throw new Error(
28921
+ `Vision LLM request failed: ${res.statusText} (${res.status}) - ${errorText}`
28922
+ );
28721
28923
  }
28722
- const aliases = {
28723
- claude: ["claude"],
28724
- copilot: ["copilot"],
28725
- codex: ["codex"],
28726
- agent: ["agent"],
28727
- opencode: ["agent"]
28728
- };
28729
- const selected = /* @__PURE__ */ new Set();
28730
- for (const raw of value.split(",")) {
28731
- const key = raw.trim().toLowerCase();
28732
- const mapped = aliases[key];
28733
- if (!mapped) {
28924
+ const data = await res.json();
28925
+ if (data.error !== void 0) {
28926
+ const errorMsg = formatModelError(data.error);
28927
+ if (errorMsg.includes("image") && (errorMsg.includes("not support") || errorMsg.includes("unsupported"))) {
28734
28928
  throw new Error(
28735
- `Unknown agent "${raw}". Use: all, claude, copilot, codex, agent.`
28929
+ `Vision model "${args.model}" does not support image input. Set a multimodal model: zam settings set llm.vision.model <model>`
28736
28930
  );
28737
28931
  }
28738
- for (const item of mapped) selected.add(item);
28932
+ throw new Error(`Vision model failed: ${errorMsg}`);
28739
28933
  }
28740
- return selected;
28934
+ const content = data.choices?.[0]?.message?.content;
28935
+ if (!content || typeof content !== "string") {
28936
+ throw new Error("Empty response from vision model");
28937
+ }
28938
+ return content.trim();
28741
28939
  }
28742
- function pathExists(path) {
28743
- try {
28744
- lstatSync2(path);
28745
- return true;
28746
- } catch {
28747
- return false;
28940
+ function dataUrlToAnthropicImage(dataUrl) {
28941
+ const match = dataUrl.match(/^data:(image\/[a-zA-Z]+);base64,(.+)$/);
28942
+ if (!match) {
28943
+ throw new Error("Unsupported image data for Anthropic vision request");
28944
+ }
28945
+ return {
28946
+ type: "image",
28947
+ source: { type: "base64", media_type: match[1], data: match[2] }
28948
+ };
28949
+ }
28950
+ async function requestAnthropicVisionDraft(args) {
28951
+ const language = LANGUAGE_NAMES2[args.locale] ?? "English";
28952
+ const base = args.url.replace(/\/+$/, "").replace(/\/v1$/, "");
28953
+ const res = await fetchWithInteractiveTimeout(`${base}/v1/messages`, {
28954
+ method: "POST",
28955
+ headers: {
28956
+ "Content-Type": "application/json",
28957
+ "x-api-key": args.apiKey,
28958
+ "anthropic-version": "2023-06-01"
28959
+ },
28960
+ body: JSON.stringify({
28961
+ model: args.model,
28962
+ max_tokens: args.input.maxTokens ?? DEFAULT_LLM_MAX_TOKENS,
28963
+ system: VISION_SYSTEM_PROMPT,
28964
+ messages: [
28965
+ {
28966
+ role: "user",
28967
+ content: [
28968
+ { type: "text", text: visionUserText(args, language) },
28969
+ ...args.imageUrls.map(dataUrlToAnthropicImage)
28970
+ ]
28971
+ }
28972
+ ]
28973
+ }),
28974
+ locale: args.locale,
28975
+ hardTimeoutMs: args.input.hardTimeoutMs ?? 18e4
28976
+ });
28977
+ if (!res.ok) {
28978
+ const errorText = await res.text().catch(() => "");
28979
+ throw new Error(
28980
+ `Vision LLM request failed: ${res.statusText} (${res.status}) - ${errorText}`
28981
+ );
28982
+ }
28983
+ const data = await res.json();
28984
+ if (data.error !== void 0) {
28985
+ throw new Error(`Vision model failed: ${formatModelError(data.error)}`);
28986
+ }
28987
+ if (data.stop_reason === "refusal") {
28988
+ throw new Error("Vision model refused the request (safety classifier).");
28989
+ }
28990
+ const text = data.content?.find(
28991
+ (b) => b.type === "text" && typeof b.text === "string"
28992
+ )?.text;
28993
+ if (!text) {
28994
+ throw new Error("Empty response from vision model");
28748
28995
  }
28996
+ return text.trim();
28749
28997
  }
28750
- function isSymbolicLink(path) {
28998
+ function extractDraft(content) {
28999
+ const fenced = content.match(/```(?:json)?\s*([\s\S]*?)```/i);
29000
+ const candidate = (fenced?.[1] ?? content).trim();
28751
29001
  try {
28752
- return lstatSync2(path).isSymbolicLink();
29002
+ return JSON.parse(candidate);
28753
29003
  } catch {
28754
- return false;
29004
+ const start = candidate.indexOf("{");
29005
+ const end = candidate.lastIndexOf("}");
29006
+ if (start === -1 || end <= start) throw new Error("no JSON object found");
29007
+ return JSON.parse(
29008
+ candidate.slice(start, end + 1)
29009
+ );
28755
29010
  }
28756
29011
  }
28757
- function comparableRealPath(path) {
28758
- const real = realpathSync(path);
28759
- return process.platform === "win32" ? real.toLowerCase() : real;
28760
- }
28761
- function pathsResolveToSameDirectory(sourceDir, destinationDir) {
28762
- try {
28763
- return comparableRealPath(destinationDir) === comparableRealPath(sourceDir);
28764
- } catch {
28765
- return false;
28766
- }
29012
+ function buildReport(input8, draft) {
29013
+ return {
29014
+ version: UI_OBSERVATION_PROTOCOL_VERSION,
29015
+ sessionId: input8.sessionId,
29016
+ sequence: input8.sequence,
29017
+ observedFrom: input8.observedFrom,
29018
+ observedTo: input8.observedTo,
29019
+ kind: parseKind(draft.kind),
29020
+ application: input8.application,
29021
+ summary: parseSummary(draft.summary),
29022
+ actions: parseActions(draft.actions),
29023
+ evidence: [
29024
+ {
29025
+ type: "keyframe",
29026
+ ref: input8.evidenceRef ?? basename5(input8.imagePath),
29027
+ redacted: input8.redacted ?? false
29028
+ }
29029
+ ],
29030
+ candidateTokens: parseCandidateTokens(draft.candidateTokens),
29031
+ confidence: parseConfidence(draft.confidence, 0.35)
29032
+ };
28767
29033
  }
28768
- function linkPointsTo(sourceDir, destinationDir) {
28769
- return isSymbolicLink(destinationDir) && pathsResolveToSameDirectory(sourceDir, destinationDir);
29034
+ function uncertainReport(input8, summary) {
29035
+ return {
29036
+ version: UI_OBSERVATION_PROTOCOL_VERSION,
29037
+ sessionId: input8.sessionId,
29038
+ sequence: input8.sequence,
29039
+ observedFrom: input8.observedFrom,
29040
+ observedTo: input8.observedTo,
29041
+ kind: "uncertain",
29042
+ application: input8.application,
29043
+ summary,
29044
+ actions: [],
29045
+ evidence: [
29046
+ {
29047
+ type: "keyframe",
29048
+ ref: input8.evidenceRef ?? basename5(input8.imagePath),
29049
+ redacted: input8.redacted ?? false
29050
+ }
29051
+ ],
29052
+ candidateTokens: [],
29053
+ confidence: 0.1
29054
+ };
28770
29055
  }
28771
- function isZamSkillCopy(destinationDir) {
28772
- try {
28773
- if (!lstatSync2(destinationDir).isDirectory()) return false;
28774
- const skillFile = join20(destinationDir, "SKILL.md");
28775
- if (!existsSync18(skillFile)) return false;
28776
- return /^name:\s*zam\s*$/m.test(readFileSync15(skillFile, "utf8"));
28777
- } catch {
28778
- return false;
29056
+ function parseKind(value) {
29057
+ if (typeof value === "string" && OBSERVATION_KINDS2.has(value)) {
29058
+ return value;
28779
29059
  }
29060
+ return "uncertain";
28780
29061
  }
28781
- function classifySkillDestination(sourceDir, destinationDir) {
28782
- if (!existsSync18(sourceDir)) return "source-missing";
28783
- if (!isSymbolicLink(destinationDir) && pathsResolveToSameDirectory(sourceDir, destinationDir)) {
28784
- return "source-directory";
29062
+ function parseSummary(value) {
29063
+ if (typeof value === "string" && value.trim().length > 0) {
29064
+ return value.trim();
28785
29065
  }
28786
- if (linkPointsTo(sourceDir, destinationDir)) return "linked";
28787
- if (!pathExists(destinationDir)) return "missing";
28788
- if (isSymbolicLink(destinationDir)) return "broken";
28789
- if (isZamSkillCopy(destinationDir)) return "stale-copy";
28790
- return "unmanaged";
29066
+ return "Vision model did not provide a factual UI summary.";
28791
29067
  }
28792
- function wireSkills(cwd = process.cwd(), agents = parseSetupAgents(), opts = {}) {
28793
- const results = [];
28794
- const log = (message) => {
28795
- if (!opts.quiet) console.log(message);
28796
- };
28797
- for (const { from, to, agents: pairAgents } of SKILL_PAIRS) {
28798
- if (!pairAgents.some((agent) => agents.has(agent))) continue;
28799
- const sourceDir = dirname9(from);
28800
- const destinationDir = dirname9(join20(cwd, to));
28801
- const label = dirname9(to);
28802
- const state = classifySkillDestination(sourceDir, destinationDir);
28803
- if (state === "source-missing") {
28804
- if (!opts.quiet) {
28805
- console.warn(` warn source not found, skipping: ${sourceDir}`);
28806
- }
28807
- results.push({
28808
- source: sourceDir,
28809
- destination: destinationDir,
28810
- action: "skipped",
28811
- reason: "source-not-found"
28812
- });
28813
- continue;
29068
+ function parseActions(value) {
29069
+ if (!Array.isArray(value)) return [];
29070
+ return value.flatMap((item) => {
29071
+ if (!isRecord2(item) || typeof item.type !== "string") return [];
29072
+ if (!ACTION_TYPES2.has(item.type)) return [];
29073
+ const action = { type: item.type };
29074
+ if (typeof item.target === "string" && item.target.trim()) {
29075
+ action.target = item.target.trim();
28814
29076
  }
28815
- if (state === "source-directory") {
28816
- log(` skip ${label} (package source)`);
28817
- results.push({
28818
- source: sourceDir,
28819
- destination: destinationDir,
28820
- action: "skipped",
28821
- reason: "source-directory"
28822
- });
28823
- continue;
29077
+ if (typeof item.result === "string" && item.result.trim()) {
29078
+ action.result = item.result.trim();
28824
29079
  }
28825
- if (state === "linked") {
28826
- log(` skip ${label} (already linked)`);
28827
- results.push({
28828
- source: sourceDir,
28829
- destination: destinationDir,
28830
- action: "skipped",
28831
- reason: "already-linked"
28832
- });
28833
- continue;
29080
+ return [action];
29081
+ });
29082
+ }
29083
+ function parseCandidateTokens(value) {
29084
+ if (!Array.isArray(value)) return [];
29085
+ return value.flatMap((item) => {
29086
+ if (!isRecord2(item)) return [];
29087
+ if (typeof item.slug !== "string" || !/^[A-Za-z0-9._-]+$/.test(item.slug)) {
29088
+ return [];
28834
29089
  }
28835
- const destinationExists = state !== "missing";
28836
- const replaceExisting = destinationExists && (Boolean(opts.force) || state === "broken" || state === "stale-copy");
28837
- if (destinationExists && !replaceExisting) {
28838
- if (!opts.quiet) {
28839
- console.warn(
28840
- ` warn ${label} already exists and is not managed by ZAM; use --force to replace it`
28841
- );
28842
- }
28843
- results.push({
28844
- source: sourceDir,
28845
- destination: destinationDir,
28846
- action: "skipped",
28847
- reason: "unmanaged-destination"
28848
- });
28849
- continue;
29090
+ if (typeof item.rationale !== "string" || !item.rationale.trim()) {
29091
+ return [];
28850
29092
  }
28851
- const action = destinationExists ? "relinked" : "linked";
28852
- if (opts.dryRun) {
28853
- log(` would ${action === "linked" ? "link" : "relink"} ${label}`);
28854
- } else {
28855
- if (destinationExists) {
28856
- rmSync2(destinationDir, { recursive: true, force: true });
29093
+ return [
29094
+ {
29095
+ slug: item.slug,
29096
+ confidence: parseConfidence(item.confidence, 0.2),
29097
+ rationale: item.rationale.trim()
28857
29098
  }
28858
- mkdirSync13(dirname9(destinationDir), { recursive: true });
28859
- symlinkSync(
28860
- sourceDir,
28861
- destinationDir,
28862
- process.platform === "win32" ? "junction" : "dir"
28863
- );
28864
- log(` ${action === "linked" ? "link" : "relink"} ${label}`);
28865
- }
28866
- results.push({ source: sourceDir, destination: destinationDir, action });
29099
+ ];
29100
+ });
29101
+ }
29102
+ function parseConfidence(value, fallback) {
29103
+ if (typeof value !== "number" || !Number.isFinite(value)) return fallback;
29104
+ return Math.max(0, Math.min(1, value));
29105
+ }
29106
+ function formatModelError(error) {
29107
+ if (typeof error === "string") return error;
29108
+ if (isRecord2(error) && typeof error.message === "string") {
29109
+ return error.message;
28867
29110
  }
28868
- return results;
29111
+ return JSON.stringify(error);
29112
+ }
29113
+ function isRecord2(value) {
29114
+ return typeof value === "object" && value !== null && !Array.isArray(value);
29115
+ }
29116
+
29117
+ // src/cli/providers/config.ts
29118
+ init_kernel();
29119
+
29120
+ // src/cli/commands/shared/db.ts
29121
+ init_kernel();
29122
+ function defaultErrorHandler(message) {
29123
+ console.error("Error:", message);
29124
+ process.exit(1);
29125
+ }
29126
+ async function withDb(fn, onError = defaultErrorHandler) {
29127
+ let db;
29128
+ try {
29129
+ db = await openDatabase();
29130
+ await fn(db);
29131
+ } catch (err) {
29132
+ onError(err.message);
29133
+ } finally {
29134
+ await db?.close();
29135
+ }
29136
+ }
29137
+ function jsonOut(data) {
29138
+ console.log(JSON.stringify(data, null, 2));
29139
+ }
29140
+
29141
+ // src/cli/providers/config.ts
29142
+ var VALID_API_FLAVORS = [
29143
+ "chat-completions",
29144
+ "anthropic-messages"
29145
+ ];
29146
+ var VALID_ROLES = ["vision", "recall", "text", "embedding"];
29147
+ function upsertProviderRecord(providers, name, patch) {
29148
+ const merged = { ...providers[name] ?? {} };
29149
+ if (patch.url !== void 0) merged.url = patch.url;
29150
+ if (patch.model !== void 0) merged.model = patch.model;
29151
+ if (patch.apiFlavor !== void 0) merged.apiFlavor = patch.apiFlavor;
29152
+ if (patch.apiKeyRef !== void 0) merged.apiKeyRef = patch.apiKeyRef;
29153
+ if (patch.label !== void 0) merged.label = patch.label;
29154
+ if (patch.local !== void 0) merged.local = patch.local;
29155
+ if (patch.runner !== void 0) merged.runner = patch.runner;
29156
+ return { ...providers, [name]: merged };
29157
+ }
29158
+ function removeProviderRecord(providers, name) {
29159
+ if (!(name in providers)) return { providers, removed: false };
29160
+ const next = { ...providers };
29161
+ delete next[name];
29162
+ return { providers: next, removed: true };
29163
+ }
29164
+ function rolesReferencing(roles, name) {
29165
+ return VALID_ROLES.filter((role) => {
29166
+ const binding = roles[role];
29167
+ return binding?.primary === name || binding?.fallback === name;
29168
+ });
29169
+ }
29170
+ function bindRoleProviders(roles, role, primary, fallback) {
29171
+ const binding = { primary };
29172
+ if (fallback) binding.fallback = fallback;
29173
+ return { ...roles, [role]: binding };
28869
29174
  }
28870
- function inspectSkillLinks(cwd = process.cwd(), agents = parseSetupAgents()) {
28871
- const results = [];
28872
- for (const { from, to, agents: pairAgents } of SKILL_PAIRS) {
28873
- if (!pairAgents.some((agent) => agents.has(agent))) continue;
28874
- const sourceDir = dirname9(from);
28875
- const destinationDir = dirname9(join20(cwd, to));
28876
- results.push({
28877
- agents: pairAgents,
28878
- source: sourceDir,
28879
- destination: destinationDir,
28880
- state: classifySkillDestination(sourceDir, destinationDir)
28881
- });
28882
- }
28883
- return results;
29175
+ function unbindRole(roles, role) {
29176
+ if (!(role in roles)) return roles;
29177
+ const next = { ...roles };
29178
+ delete next[role];
29179
+ return next;
28884
29180
  }
28885
- function summarizeSkillLinkHealth(inspections) {
28886
- if (inspections.some((item) => item.state === "unmanaged")) {
28887
- return "unmanaged";
28888
- }
28889
- const repairable = ["missing", "broken", "stale-copy"];
28890
- if (inspections.some((item) => repairable.includes(item.state))) {
28891
- return "needs-repair";
28892
- }
28893
- return "healthy";
29181
+ function maskSecret(key) {
29182
+ return key.length <= 4 ? "\u2022\u2022\u2022\u2022" : `\u2026${key.slice(-4)}`;
28894
29183
  }
28895
- var ZAM_BLOCK_START = "<!-- ZAM:START -->";
28896
- var ZAM_BLOCK_END = "<!-- ZAM:END -->";
28897
- function upsertMarkedBlock(dest, blockBody, dryRun) {
28898
- const block = `${ZAM_BLOCK_START}
28899
- ${blockBody.trim()}
28900
- ${ZAM_BLOCK_END}`;
28901
- if (!existsSync18(dest)) {
28902
- if (!dryRun) {
28903
- mkdirSync13(dirname9(dest), { recursive: true });
28904
- writeFileSync11(dest, `${block}
28905
- `, "utf8");
28906
- }
28907
- return "write";
28908
- }
28909
- const existing = readFileSync15(dest, "utf8");
28910
- if (existing.includes(block)) return "skip";
28911
- const start = existing.indexOf(ZAM_BLOCK_START);
28912
- const end = existing.indexOf(ZAM_BLOCK_END);
28913
- const next = start >= 0 && end > start ? `${existing.slice(0, start)}${block}${existing.slice(end + ZAM_BLOCK_END.length)}` : `${existing.trimEnd()}
28914
-
28915
- ${block}
28916
- `;
28917
- if (!dryRun) writeFileSync11(dest, next, "utf8");
28918
- return start >= 0 && end > start ? "update" : "write";
29184
+ function buildProviderListing(providers, hasKey) {
29185
+ return Object.entries(providers).map(([name, rec]) => {
29186
+ const apiFlavor = rec.apiFlavor ?? (rec.url ? inferApiFlavor(rec.url) : "chat-completions");
29187
+ let keyState;
29188
+ if (!rec.apiKeyRef) keyState = "none";
29189
+ else keyState = hasKey(rec.apiKeyRef) ? "set" : "missing";
29190
+ const row = {
29191
+ name,
29192
+ url: rec.url,
29193
+ model: rec.model,
29194
+ apiFlavor,
29195
+ apiKeyRef: rec.apiKeyRef,
29196
+ keyState
29197
+ };
29198
+ if (rec.label !== void 0) row.label = rec.label;
29199
+ if (rec.local !== void 0) row.local = rec.local;
29200
+ if (rec.runner !== void 0) row.runner = rec.runner;
29201
+ return row;
29202
+ });
28919
29203
  }
28920
- function logInstructionAction(action, label, dryRun) {
28921
- if (action === "skip") {
28922
- console.log(` skip ${label} (ZAM block already present)`);
28923
- } else {
28924
- console.log(` ${dryRun ? "would " : ""}${action} ${label}`);
29204
+ function findOrphanKeyRefs(storedRefs, providers) {
29205
+ const used = new Set(
29206
+ Object.values(providers).map((rec) => rec.apiKeyRef).filter((ref) => !!ref)
29207
+ );
29208
+ return storedRefs.filter((ref) => !used.has(ref));
29209
+ }
29210
+ async function readJson(db, key, fallback) {
29211
+ const raw = await getSetting(db, key);
29212
+ if (!raw) return fallback;
29213
+ try {
29214
+ return JSON.parse(raw);
29215
+ } catch {
29216
+ return fallback;
28925
29217
  }
28926
29218
  }
28927
- function writeClaudeMd(skipClaudeMd, cwd = process.cwd(), opts = {}) {
28928
- if (skipClaudeMd) return;
28929
- const dest = join20(cwd, "CLAUDE.md");
28930
- if (existsSync18(dest)) {
28931
- if (!opts.updateExisting) {
28932
- console.log(` skip CLAUDE.md (already present)`);
28933
- return;
28934
- }
28935
- const action = upsertMarkedBlock(
28936
- dest,
28937
- `## ZAM learning sessions
28938
-
28939
- ZAM is available in this repository. Use the \`zam\` skill in Claude Code to turn real work into an observed learning session with active recall and FSRS scheduling.
28940
-
28941
- - Skill files live under \`.claude/skills/zam/\`.
28942
- - Fast-changing review data lives in \`~/.zam/\`, not in this repository.
28943
- - The skill directory is linked to this ZAM installation and updates with it.`,
28944
- Boolean(opts.dryRun)
28945
- );
28946
- logInstructionAction(action, "CLAUDE.md", Boolean(opts.dryRun));
29219
+ var readProviders = (db) => readJson(db, "llm.providers", {});
29220
+ var readRoles = (db) => readJson(db, "llm.roles", {});
29221
+ async function readScopedProviders(db, machine) {
29222
+ if (machine) return getMachineAiConfig().providers ?? {};
29223
+ if (!db)
29224
+ throw new Error("Database is required for shared provider settings.");
29225
+ return readProviders(db);
29226
+ }
29227
+ async function readScopedRoles(db, machine) {
29228
+ if (machine) return getMachineAiConfig().roles ?? {};
29229
+ if (!db)
29230
+ throw new Error("Database is required for shared provider settings.");
29231
+ return readRoles(db);
29232
+ }
29233
+ async function writeScopedProviders(db, machine, p) {
29234
+ if (machine) {
29235
+ const config = getMachineAiConfig();
29236
+ saveMachineAiConfig({ ...config, providers: p });
28947
29237
  return;
28948
29238
  }
28949
- const name = basename5(cwd);
28950
- const content = `# ZAM Personal Kernel \u2014 ${name}
28951
-
28952
- This is a ZAM personal instance. ZAM builds lasting skills through spaced
28953
- repetition during real work \u2014 not separate study sessions.
28954
-
28955
- ## First time here?
28956
- Run \`/setup\` in Claude Code or Gemini CLI to complete first-time setup.
28957
-
28958
- ## Regular use
28959
- Run \`/zam\` to start a learning session on whatever you are working on.
28960
-
28961
- ## What lives here
28962
- - \`beliefs/\` \u2014 your worldview, approved by git commit
28963
- - \`goals/\` \u2014 your objectives, decomposed into tasks and learning tokens
28964
-
28965
- ## Fast-changing data
28966
- Learning tokens, cards, and review history live in local SQLite by default.
28967
- Use \`zam connector setup turso\` to store cloud credentials in
28968
- \`~/.zam/credentials.json\` and use a Turso database across machines.
28969
- `;
28970
- if (opts.dryRun) {
28971
- console.log(` would write CLAUDE.md`);
28972
- } else {
28973
- writeFileSync11(dest, content, "utf8");
28974
- console.log(` write CLAUDE.md`);
28975
- }
29239
+ if (!db)
29240
+ throw new Error("Database is required for shared provider settings.");
29241
+ await setSetting(db, "llm.providers", JSON.stringify(p));
28976
29242
  }
28977
- function writeAgentsMd(skipAgentsMd, cwd = process.cwd(), opts = {}) {
28978
- if (skipAgentsMd) return;
28979
- const dest = join20(cwd, "AGENTS.md");
28980
- if (existsSync18(dest)) {
28981
- if (!opts.updateExisting) {
28982
- console.log(` skip AGENTS.md (already present)`);
28983
- return;
28984
- }
28985
- const action = upsertMarkedBlock(
28986
- dest,
28987
- `## ZAM learning sessions
28988
-
28989
- ZAM is available in this repository. Select the \`zam\` skill through \`/skills\` or invoke \`$zam\` where supported to turn real work into an observed learning session with active recall and FSRS scheduling.
28990
-
28991
- - Skill files live under \`.agents/skills/zam/\`.
28992
- - Fast-changing review data lives in \`~/.zam/\`, not in this repository.
28993
- - The skill directory is linked to this ZAM installation and updates with it.`,
28994
- Boolean(opts.dryRun)
28995
- );
28996
- logInstructionAction(action, "AGENTS.md", Boolean(opts.dryRun));
29243
+ async function writeScopedRoles(db, machine, r) {
29244
+ if (machine) {
29245
+ const config = getMachineAiConfig();
29246
+ saveMachineAiConfig({ ...config, roles: r });
28997
29247
  return;
28998
29248
  }
28999
- const name = basename5(cwd);
29000
- const content = `# ZAM Personal Kernel - ${name}
29001
-
29002
- This is a ZAM personal instance. ZAM builds lasting skills through spaced
29003
- repetition during real work, not separate study sessions.
29004
-
29005
- ## First time here?
29006
- Run \`zam setup\` from the shell. When this repository includes
29007
- \`.agents/skills/setup/\`, you can instead select \`setup\` through \`/skills\`
29008
- or invoke \`$setup\`.
29009
-
29010
- ## Regular use
29011
- Select the \`zam\` skill through \`/skills\` or invoke \`$zam\` to start a
29012
- learning session on whatever you are working on.
29013
-
29014
- ## What lives here
29015
- - \`beliefs/\` - your worldview, approved by git commit
29016
- - \`goals/\` - your objectives, decomposed into tasks and learning tokens
29017
-
29018
- ## Fast-changing data
29019
- Learning tokens, cards, and review history live in local SQLite by default.
29020
- Use \`zam connector setup turso\` to store cloud credentials in
29021
- \`~/.zam/credentials.json\` and use a Turso database across machines.
29022
-
29023
- ## Codex skills
29024
- Codex discovers repository skills under \`.agents/skills/\`. Run
29025
- \`zam setup --force\` after upgrading \`zam-core\` to refresh them.
29026
- `;
29027
- if (opts.dryRun) {
29028
- console.log(` would write AGENTS.md`);
29029
- } else {
29030
- writeFileSync11(dest, content, "utf8");
29031
- console.log(` write AGENTS.md`);
29032
- }
29249
+ if (!db)
29250
+ throw new Error("Database is required for shared provider settings.");
29251
+ await setSetting(db, "llm.roles", JSON.stringify(r));
29033
29252
  }
29034
- function writeCopilotInstructions(cwd = process.cwd(), opts = {}) {
29035
- const dest = join20(cwd, ".github", "copilot-instructions.md");
29036
- const action = upsertMarkedBlock(
29037
- dest,
29038
- `## ZAM learning sessions
29039
-
29040
- ZAM is available in this repository through \`.claude/skills/zam/\`. Use the \`zam\` project skill from Copilot-compatible skill selection surfaces to turn real work into an observed learning session with active recall and FSRS scheduling.
29041
-
29042
- - Fast-changing review data lives in \`~/.zam/\`, not in this repository.
29043
- - The skill directory is linked to this ZAM installation and updates with it.`,
29044
- Boolean(opts.dryRun)
29045
- );
29046
- logInstructionAction(
29047
- action,
29048
- ".github/copilot-instructions.md",
29049
- Boolean(opts.dryRun)
29050
- );
29253
+ async function withProviderScope(machine, action) {
29254
+ if (machine) {
29255
+ await action(void 0);
29256
+ return;
29257
+ }
29258
+ await withDb(action);
29051
29259
  }
29052
29260
 
29053
29261
  // src/cli/users/identity.ts
@@ -29073,13 +29281,13 @@ async function resolveUser(opts, db, resolveOpts) {
29073
29281
  }
29074
29282
 
29075
29283
  // src/cli/workspaces/backup.ts
29076
- import { mkdirSync as mkdirSync14 } from "fs";
29077
- import { join as join21 } from "path";
29284
+ import { mkdirSync as mkdirSync15 } from "fs";
29285
+ import { join as join22 } from "path";
29078
29286
  async function backupDatabaseTo(db, targetDir) {
29079
- const backupDir = join21(targetDir, "zam-backups");
29080
- mkdirSync14(backupDir, { recursive: true });
29287
+ const backupDir = join22(targetDir, "zam-backups");
29288
+ mkdirSync15(backupDir, { recursive: true });
29081
29289
  const stamp = (/* @__PURE__ */ new Date()).toISOString().replace(/[:.]/g, "-");
29082
- const dest = join21(backupDir, `zam-${stamp}.db`);
29290
+ const dest = join22(backupDir, `zam-${stamp}.db`);
29083
29291
  await db.exec(`VACUUM INTO '${dest.replace(/'/g, "''")}'`);
29084
29292
  return dest;
29085
29293
  }
@@ -29210,20 +29418,20 @@ bridgeCommand.command("workspace-info").description("Report the workspace dir, i
29210
29418
  activeWorkspace,
29211
29419
  workspaceDir: activeWorkspace.path,
29212
29420
  defaultWorkspaceDir: defaultWorkspaceDir(),
29213
- dataDir: join22(homedir13(), ".zam")
29421
+ dataDir: join23(homedir14(), ".zam")
29214
29422
  });
29215
29423
  });
29216
29424
  });
29217
29425
  function provisionConfiguredWorkspaces() {
29218
29426
  return getConfiguredWorkspaces().flatMap(
29219
- (workspace) => existsSync19(workspace.path) ? wireSkills(workspace.path, parseSetupAgents(), { quiet: true }) : []
29427
+ (workspace) => existsSync21(workspace.path) ? wireSkills(workspace.path, parseSetupAgents(), { quiet: true }) : []
29220
29428
  );
29221
29429
  }
29222
29430
  function buildWorkspaceLinkHealth(workspaces) {
29223
29431
  const agents = parseSetupAgents();
29224
29432
  const map = {};
29225
29433
  for (const workspace of workspaces) {
29226
- if (!existsSync19(workspace.path)) continue;
29434
+ if (!existsSync21(workspace.path)) continue;
29227
29435
  const links = inspectSkillLinks(workspace.path, agents);
29228
29436
  map[workspace.id] = {
29229
29437
  health: summarizeSkillLinkHealth(links),
@@ -29253,7 +29461,7 @@ bridgeCommand.command("workspace-list").description("List configured ZAM workspa
29253
29461
  activeWorkspace,
29254
29462
  workspaceDir: activeWorkspace.path,
29255
29463
  defaultWorkspaceDir: defaultWorkspaceDir(),
29256
- dataDir: join22(homedir13(), ".zam"),
29464
+ dataDir: join23(homedir14(), ".zam"),
29257
29465
  linkHealth: buildWorkspaceLinkHealth(workspaces)
29258
29466
  });
29259
29467
  });
@@ -29268,7 +29476,7 @@ bridgeCommand.command("workspace-repair-links").description(
29268
29476
  if (!id) jsonError("A non-empty --id is required");
29269
29477
  const workspace = getConfiguredWorkspaces().find((item) => item.id === id);
29270
29478
  if (!workspace) jsonError(`Workspace "${id}" is not configured`);
29271
- if (!existsSync19(workspace.path)) {
29479
+ if (!existsSync21(workspace.path)) {
29272
29480
  jsonError(`Workspace path does not exist: ${workspace.path}`);
29273
29481
  }
29274
29482
  const agents = parseSetupAgents(opts.agents);
@@ -29299,8 +29507,8 @@ function parseBridgeWorkspaceKind(value) {
29299
29507
  bridgeCommand.command("workspace-add").description("Register an existing directory as a ZAM workspace (JSON)").requiredOption("--path <dir>", "Existing workspace/repository directory").option("--id <id>", "Workspace id").option("--label <label>", "Human-readable label").option("--kind <kind>", "Workspace kind", "custom").action(async (opts) => {
29300
29508
  const raw = String(opts.path ?? "").trim();
29301
29509
  if (!raw) jsonError("A non-empty --path is required");
29302
- const path = resolve5(raw);
29303
- if (!existsSync19(path)) jsonError(`Workspace path does not exist: ${path}`);
29510
+ const path = resolve6(raw);
29511
+ if (!existsSync21(path)) jsonError(`Workspace path does not exist: ${path}`);
29304
29512
  const id = opts.id ? String(opts.id).trim() : void 0;
29305
29513
  if (opts.id && !id) jsonError("A non-empty --id is required");
29306
29514
  const kind = parseBridgeWorkspaceKind(opts.kind);
@@ -29344,8 +29552,8 @@ bridgeCommand.command("workspace-remove").description("Unregister a ZAM workspac
29344
29552
  bridgeCommand.command("set-workspace-dir").description("Set the personal workspace directory (JSON)").requiredOption("--dir <path>", "Path to the workspace directory").action(async (opts) => {
29345
29553
  const raw = String(opts.dir ?? "").trim();
29346
29554
  if (!raw) jsonError("A non-empty --dir is required");
29347
- const dir = resolve5(raw);
29348
- if (!existsSync19(dir)) jsonError(`Workspace path does not exist: ${dir}`);
29555
+ const dir = resolve6(raw);
29556
+ if (!existsSync21(dir)) jsonError(`Workspace path does not exist: ${dir}`);
29349
29557
  const skillLinks = wireSkills(dir, parseSetupAgents(), { quiet: true });
29350
29558
  await withDb2(async (db) => {
29351
29559
  const workspace = await activateWorkspacePath(db, dir);
@@ -29408,7 +29616,7 @@ bridgeCommand.command("agent-open").description("Launch an agent harness in the
29408
29616
  jsonError(`Workspace is not configured: ${opts.workspace}`);
29409
29617
  }
29410
29618
  const activeWorkspace = await ensureActiveWorkspace(db);
29411
- const workspace = opts.dir ? existsSync19(opts.dir) ? opts.dir : homedir13() : existingWorkspaceDirOrHome(configuredWorkspace ?? activeWorkspace);
29619
+ const workspace = opts.dir ? existsSync21(opts.dir) ? opts.dir : homedir14() : existingWorkspaceDirOrHome(configuredWorkspace ?? activeWorkspace);
29412
29620
  launchHarness(harness, {
29413
29621
  executable,
29414
29622
  workspace,
@@ -29780,7 +29988,7 @@ bridgeCommand.command("discover-skills").description(
29780
29988
  "20"
29781
29989
  ).action(async (opts) => {
29782
29990
  try {
29783
- const monitorDir = join22(homedir13(), ".zam", "monitor");
29991
+ const monitorDir = join23(homedir14(), ".zam", "monitor");
29784
29992
  let files;
29785
29993
  try {
29786
29994
  files = readdirSync2(monitorDir).filter((f) => f.endsWith(".jsonl"));
@@ -29793,7 +30001,7 @@ bridgeCommand.command("discover-skills").description(
29793
30001
  return;
29794
30002
  }
29795
30003
  const limit = Number(opts.limit);
29796
- const sorted = files.map((f) => ({ name: f, path: join22(monitorDir, f) })).sort((a, b) => b.name.localeCompare(a.name)).slice(0, limit);
30004
+ const sorted = files.map((f) => ({ name: f, path: join23(monitorDir, f) })).sort((a, b) => b.name.localeCompare(a.name)).slice(0, limit);
29797
30005
  const sessionCommands = /* @__PURE__ */ new Map();
29798
30006
  for (const file of sorted) {
29799
30007
  const sessionId = file.name.replace(".jsonl", "");
@@ -29905,7 +30113,7 @@ bridgeCommand.command("observe-ui-snapshot").description(
29905
30113
  });
29906
30114
  function resolveWindowsPowerShell() {
29907
30115
  try {
29908
- execFileSync4("where.exe", ["pwsh.exe"], { stdio: "ignore" });
30116
+ execFileSync5("where.exe", ["pwsh.exe"], { stdio: "ignore" });
29909
30117
  return "pwsh";
29910
30118
  } catch {
29911
30119
  return "powershell";
@@ -29920,7 +30128,7 @@ function captureScreenshot(outputPath, hwnd, processName) {
29920
30128
  throw new Error(`Invalid process name format: ${processName}`);
29921
30129
  }
29922
30130
  if (platform === "win32") {
29923
- const stdout = execFileSync4(
30131
+ const stdout = execFileSync5(
29924
30132
  resolveWindowsPowerShell(),
29925
30133
  [
29926
30134
  "-NoProfile",
@@ -30233,13 +30441,13 @@ Write-CaptureResult "fullscreen" $hwndVal $matchedBy
30233
30441
  } else if (platform === "darwin") {
30234
30442
  if (hwnd) {
30235
30443
  const parsedHwnd = hwnd.startsWith("0x") ? parseInt(hwnd, 16) : parseInt(hwnd, 10);
30236
- execFileSync4("screencapture", ["-l", String(parsedHwnd), outputPath], {
30444
+ execFileSync5("screencapture", ["-l", String(parsedHwnd), outputPath], {
30237
30445
  stdio: "pipe"
30238
30446
  });
30239
30447
  return { method: "screencapture-window", target: null };
30240
30448
  } else if (processName) {
30241
30449
  try {
30242
- const windowId = execFileSync4(
30450
+ const windowId = execFileSync5(
30243
30451
  "osascript",
30244
30452
  [
30245
30453
  "-e",
@@ -30248,17 +30456,17 @@ Write-CaptureResult "fullscreen" $hwndVal $matchedBy
30248
30456
  { encoding: "utf8", stdio: ["pipe", "pipe", "ignore"] }
30249
30457
  ).trim();
30250
30458
  if (windowId && /^\d+$/.test(windowId)) {
30251
- execFileSync4("screencapture", ["-l", windowId, outputPath], {
30459
+ execFileSync5("screencapture", ["-l", windowId, outputPath], {
30252
30460
  stdio: "pipe"
30253
30461
  });
30254
30462
  return { method: "screencapture-window", target: null };
30255
30463
  }
30256
30464
  } catch {
30257
30465
  }
30258
- execFileSync4("screencapture", ["-x", outputPath], { stdio: "pipe" });
30466
+ execFileSync5("screencapture", ["-x", outputPath], { stdio: "pipe" });
30259
30467
  return { method: "screencapture-full", target: null };
30260
30468
  } else {
30261
- execFileSync4("screencapture", ["-x", outputPath], { stdio: "pipe" });
30469
+ execFileSync5("screencapture", ["-x", outputPath], { stdio: "pipe" });
30262
30470
  return { method: "screencapture-full", target: null };
30263
30471
  }
30264
30472
  } else {
@@ -30295,7 +30503,7 @@ bridgeCommand.command("capture-ui").description("Capture a screenshot for agent-
30295
30503
  return;
30296
30504
  }
30297
30505
  }
30298
- const outputPath = opts.image ?? opts.output ?? join22(tmpdir3(), `zam-capture-${randomBytes2(4).toString("hex")}.png`);
30506
+ const outputPath = opts.image ?? opts.output ?? join23(tmpdir3(), `zam-capture-${randomBytes2(4).toString("hex")}.png`);
30299
30507
  const captureResult = isProvided ? { method: "provided", target: null } : captureScreenshot(outputPath, opts.hwnd, opts.processName);
30300
30508
  if (!isProvided) {
30301
30509
  const post = decidePostCapture(policy, {
@@ -30323,7 +30531,7 @@ bridgeCommand.command("capture-ui").description("Capture a screenshot for agent-
30323
30531
  return;
30324
30532
  }
30325
30533
  }
30326
- const imageBytes = readFileSync16(outputPath);
30534
+ const imageBytes = readFileSync17(outputPath);
30327
30535
  const base64 = imageBytes.toString("base64");
30328
30536
  jsonOut2({
30329
30537
  sessionId: opts.session ?? null,
@@ -30350,11 +30558,11 @@ bridgeCommand.command("start-recording").description("Start screen recording in
30350
30558
  return;
30351
30559
  }
30352
30560
  const sessionId = opts.session;
30353
- const statePath = join22(tmpdir3(), `zam-recording-${sessionId}.json`);
30561
+ const statePath = join23(tmpdir3(), `zam-recording-${sessionId}.json`);
30354
30562
  const defaultExt = platform === "win32" ? ".mkv" : ".mov";
30355
- const outputPath = opts.output ?? join22(tmpdir3(), `zam-recording-${sessionId}${defaultExt}`);
30356
- const { existsSync: existsSync28, writeFileSync: writeFileSync16, openSync, closeSync } = await import("fs");
30357
- if (existsSync28(statePath)) {
30563
+ const outputPath = opts.output ?? join23(tmpdir3(), `zam-recording-${sessionId}${defaultExt}`);
30564
+ const { existsSync: existsSync30, writeFileSync: writeFileSync17, openSync, closeSync } = await import("fs");
30565
+ if (existsSync30(statePath)) {
30358
30566
  jsonOut2({
30359
30567
  sessionId,
30360
30568
  started: false,
@@ -30362,7 +30570,7 @@ bridgeCommand.command("start-recording").description("Start screen recording in
30362
30570
  });
30363
30571
  return;
30364
30572
  }
30365
- const logPath = join22(tmpdir3(), `zam-recording-${sessionId}.log`);
30573
+ const logPath = join23(tmpdir3(), `zam-recording-${sessionId}.log`);
30366
30574
  let logFd;
30367
30575
  try {
30368
30576
  logFd = openSync(logPath, "w");
@@ -30408,7 +30616,7 @@ bridgeCommand.command("start-recording").description("Start screen recording in
30408
30616
  }
30409
30617
  child.unref();
30410
30618
  if (child.pid) {
30411
- writeFileSync16(
30619
+ writeFileSync17(
30412
30620
  statePath,
30413
30621
  JSON.stringify({
30414
30622
  pid: child.pid,
@@ -30444,9 +30652,9 @@ bridgeCommand.command("stop-recording").description(
30444
30652
  return;
30445
30653
  }
30446
30654
  const sessionId = opts.session;
30447
- const statePath = join22(tmpdir3(), `zam-recording-${sessionId}.json`);
30448
- const { existsSync: existsSync28, readFileSync: readFileSync21, rmSync: rmSync4 } = await import("fs");
30449
- if (!existsSync28(statePath)) {
30655
+ const statePath = join23(tmpdir3(), `zam-recording-${sessionId}.json`);
30656
+ const { existsSync: existsSync30, readFileSync: readFileSync22, rmSync: rmSync4 } = await import("fs");
30657
+ if (!existsSync30(statePath)) {
30450
30658
  jsonOut2({
30451
30659
  sessionId,
30452
30660
  stopped: false,
@@ -30454,7 +30662,7 @@ bridgeCommand.command("stop-recording").description(
30454
30662
  });
30455
30663
  return;
30456
30664
  }
30457
- const state = JSON.parse(readFileSync21(statePath, "utf8"));
30665
+ const state = JSON.parse(readFileSync22(statePath, "utf8"));
30458
30666
  const { pid, outputPath } = state;
30459
30667
  try {
30460
30668
  process.kill(pid, "SIGINT");
@@ -30470,7 +30678,7 @@ bridgeCommand.command("stop-recording").description(
30470
30678
  };
30471
30679
  let attempts = 0;
30472
30680
  while (isProcessRunning(pid) && attempts < 20) {
30473
- await new Promise((resolve11) => setTimeout(resolve11, 250));
30681
+ await new Promise((resolve12) => setTimeout(resolve12, 250));
30474
30682
  attempts++;
30475
30683
  }
30476
30684
  if (isProcessRunning(pid)) {
@@ -30483,7 +30691,7 @@ bridgeCommand.command("stop-recording").description(
30483
30691
  rmSync4(statePath, { force: true });
30484
30692
  } catch {
30485
30693
  }
30486
- if (!existsSync28(outputPath)) {
30694
+ if (!existsSync30(outputPath)) {
30487
30695
  jsonOut2({
30488
30696
  sessionId,
30489
30697
  stopped: false,
@@ -30913,7 +31121,7 @@ bridgeCommand.command("cloud-model-hint").description("Suggest a cloud model for
30913
31121
  });
30914
31122
  bridgeCommand.command("local-llm-hints").description("Detect installed local LLM servers and suggest defaults (JSON)").action(() => {
30915
31123
  const profile = getSystemProfile();
30916
- const flmInstalled = hasCommand("flm") || existsSync19("C:\\Program Files\\flm\\flm.exe");
31124
+ const flmInstalled = hasCommand("flm") || existsSync21("C:\\Program Files\\flm\\flm.exe");
30917
31125
  const ollamaInstalled = isOllamaInstalled();
30918
31126
  const runners = [
30919
31127
  { id: "flm", label: "FastFlowLM", installed: flmInstalled },
@@ -31145,13 +31353,15 @@ bridgeCommand.command("desktop-bootstrap").description("Initialize first-run des
31145
31353
  const userId = await ensureDefaultUser(db, opts.user);
31146
31354
  const { enabled, url, model, locale } = await getLlmConfig(db);
31147
31355
  const { workspaceDir, activeWorkspaceId, skillLinks } = await ensureDesktopWorkspace(db);
31356
+ const cli = installCliShim();
31148
31357
  jsonOut2({
31149
31358
  userId,
31150
31359
  locale,
31151
31360
  llm: { enabled, url, model },
31152
31361
  activeWorkspaceId,
31153
31362
  workspaceDir,
31154
- skillLinks
31363
+ skillLinks,
31364
+ cli
31155
31365
  });
31156
31366
  });
31157
31367
  });
@@ -31168,6 +31378,22 @@ bridgeCommand.command("get-settings").description("Get active ZAM settings (JSON
31168
31378
  });
31169
31379
  });
31170
31380
  });
31381
+ bridgeCommand.command("install-repair").description(
31382
+ "Verify and repair this machine's ZAM installation: CLI shim + PATH, workspace skill links, agent configs and companion extensions (JSON)"
31383
+ ).option(
31384
+ "--if-version-changed",
31385
+ "Only run when the app version differs from the last repaired one; reports skipped:true otherwise"
31386
+ ).action((opts) => {
31387
+ try {
31388
+ jsonOut2(
31389
+ performInstallRepair({
31390
+ ifVersionChanged: Boolean(opts.ifVersionChanged)
31391
+ })
31392
+ );
31393
+ } catch (err) {
31394
+ jsonError(err.message);
31395
+ }
31396
+ });
31171
31397
  bridgeCommand.command("agent-harness-status").description(
31172
31398
  "Detect installed agent harnesses and their ZAM MCP configuration state (JSON)"
31173
31399
  ).action(() => {
@@ -34100,28 +34326,28 @@ var doctorCommand = new Command5("doctor").description(
34100
34326
  // src/cli/commands/git-sync.ts
34101
34327
  init_kernel();
34102
34328
  import { execSync as execSync5 } from "child_process";
34103
- import { chmodSync as chmodSync2, existsSync as existsSync20, writeFileSync as writeFileSync12 } from "fs";
34104
- import { join as join23 } from "path";
34329
+ import { chmodSync as chmodSync3, existsSync as existsSync22, writeFileSync as writeFileSync13 } from "fs";
34330
+ import { join as join24 } from "path";
34105
34331
  import { Command as Command6 } from "commander";
34106
34332
  function installHook2() {
34107
- const gitDir = join23(process.cwd(), ".git");
34108
- if (!existsSync20(gitDir)) {
34333
+ const gitDir = join24(process.cwd(), ".git");
34334
+ if (!existsSync22(gitDir)) {
34109
34335
  console.error(
34110
34336
  "Error: Current directory is not the root of a Git repository."
34111
34337
  );
34112
34338
  process.exit(1);
34113
34339
  }
34114
- const hooksDir = join23(gitDir, "hooks");
34115
- const hookPath = join23(hooksDir, "post-commit");
34340
+ const hooksDir = join24(gitDir, "hooks");
34341
+ const hookPath = join24(hooksDir, "post-commit");
34116
34342
  const hookContent = `#!/bin/sh
34117
34343
  # ZAM Spaced Repetition Auto-Stale Hook
34118
34344
  # Triggered automatically on git commits to decay modified concept cards.
34119
34345
  zam git-sync --commit HEAD --quiet
34120
34346
  `;
34121
34347
  try {
34122
- writeFileSync12(hookPath, hookContent, { encoding: "utf-8", flag: "w" });
34348
+ writeFileSync13(hookPath, hookContent, { encoding: "utf-8", flag: "w" });
34123
34349
  try {
34124
- chmodSync2(hookPath, "755");
34350
+ chmodSync3(hookPath, "755");
34125
34351
  } catch (_e) {
34126
34352
  }
34127
34353
  console.log(
@@ -34222,8 +34448,8 @@ ZAM Auto-Stale Complete: Scanned ${changedFiles.length} file(s).`
34222
34448
 
34223
34449
  // src/cli/commands/goal.ts
34224
34450
  init_kernel();
34225
- import { existsSync as existsSync21, mkdirSync as mkdirSync15 } from "fs";
34226
- import { resolve as resolve6 } from "path";
34451
+ import { existsSync as existsSync23, mkdirSync as mkdirSync16 } from "fs";
34452
+ import { resolve as resolve7 } from "path";
34227
34453
  import { input as input2 } from "@inquirer/prompts";
34228
34454
  import { Command as Command7 } from "commander";
34229
34455
  async function resolveGoalsDir() {
@@ -34236,7 +34462,7 @@ async function resolveGoalsDir() {
34236
34462
  } finally {
34237
34463
  await db?.close();
34238
34464
  }
34239
- return goalsDir ? resolve6(goalsDir) : resolve6("goals");
34465
+ return goalsDir ? resolve7(goalsDir) : resolve7("goals");
34240
34466
  }
34241
34467
  var goalCommand = new Command7("goal").description(
34242
34468
  "Manage learning goals (markdown files)"
@@ -34246,7 +34472,7 @@ goalCommand.command("list").description("List all goals").option(
34246
34472
  "Filter by status (active, completed, paused, abandoned)"
34247
34473
  ).option("--tree", "Show goals as a tree with parent/child relationships").option("--json", "Output as JSON").action(async (opts) => {
34248
34474
  const goalsDir = await resolveGoalsDir();
34249
- if (!existsSync21(goalsDir)) {
34475
+ if (!existsSync23(goalsDir)) {
34250
34476
  console.error(`Goals directory not found: ${goalsDir}`);
34251
34477
  console.error(
34252
34478
  "Set it with: zam settings set personal.goals_dir /path/to/goals"
@@ -34347,8 +34573,8 @@ ${"\u2500".repeat(50)}`);
34347
34573
  });
34348
34574
  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) => {
34349
34575
  const goalsDir = await resolveGoalsDir();
34350
- if (!existsSync21(goalsDir)) {
34351
- mkdirSync15(goalsDir, { recursive: true });
34576
+ if (!existsSync23(goalsDir)) {
34577
+ mkdirSync16(goalsDir, { recursive: true });
34352
34578
  }
34353
34579
  let slug = opts.slug;
34354
34580
  let title = opts.title;
@@ -34408,22 +34634,22 @@ goalCommand.command("status <slug> <status>").description("Update a goal's statu
34408
34634
 
34409
34635
  // src/cli/commands/init.ts
34410
34636
  init_kernel();
34411
- import { existsSync as existsSync22, mkdirSync as mkdirSync16, writeFileSync as writeFileSync13 } from "fs";
34412
- import { homedir as homedir14 } from "os";
34413
- import { join as join24, resolve as resolve7 } from "path";
34637
+ import { existsSync as existsSync24, mkdirSync as mkdirSync17, writeFileSync as writeFileSync14 } from "fs";
34638
+ import { homedir as homedir15 } from "os";
34639
+ import { join as join25, resolve as resolve8 } from "path";
34414
34640
  import { confirm, input as input3 } from "@inquirer/prompts";
34415
34641
  import { Command as Command8 } from "commander";
34416
- var HOME2 = homedir14();
34642
+ var HOME2 = homedir15();
34417
34643
  function printLine(char = "\u2550", len = 60, color = "\x1B[36m") {
34418
34644
  console.log(`${color}${char.repeat(len)}\x1B[0m`);
34419
34645
  }
34420
34646
  function bootstrapSandboxWorkspace(workspaceDir) {
34421
- mkdirSync16(join24(workspaceDir, "beliefs"), { recursive: true });
34422
- mkdirSync16(join24(workspaceDir, "goals"), { recursive: true });
34423
- mkdirSync16(join24(workspaceDir, "skills"), { recursive: true });
34424
- const worldviewFile = join24(workspaceDir, "beliefs", "worldview.md");
34425
- if (!existsSync22(worldviewFile)) {
34426
- writeFileSync13(
34647
+ mkdirSync17(join25(workspaceDir, "beliefs"), { recursive: true });
34648
+ mkdirSync17(join25(workspaceDir, "goals"), { recursive: true });
34649
+ mkdirSync17(join25(workspaceDir, "skills"), { recursive: true });
34650
+ const worldviewFile = join25(workspaceDir, "beliefs", "worldview.md");
34651
+ if (!existsSync24(worldviewFile)) {
34652
+ writeFileSync14(
34427
34653
  worldviewFile,
34428
34654
  `# Personal Worldview
34429
34655
 
@@ -34435,9 +34661,9 @@ Here, I declare the core concepts and principles I want to master.
34435
34661
  "utf8"
34436
34662
  );
34437
34663
  }
34438
- const goalsFile = join24(workspaceDir, "goals", "goals.md");
34439
- if (!existsSync22(goalsFile)) {
34440
- writeFileSync13(
34664
+ const goalsFile = join25(workspaceDir, "goals", "goals.md");
34665
+ if (!existsSync24(goalsFile)) {
34666
+ writeFileSync14(
34441
34667
  goalsFile,
34442
34668
  `# Personal Goals
34443
34669
 
@@ -34459,8 +34685,8 @@ var initCommand = new Command8("init").description("Launch the guided interactiv
34459
34685
  );
34460
34686
  printLine();
34461
34687
  console.log("\n\x1B[1m[1/5] Setting up Local Workspace Sandbox\x1B[0m");
34462
- const defaultWorkspace = join24(HOME2, "Documents", "zam");
34463
- const workspacePath = resolve7(
34688
+ const defaultWorkspace = join25(HOME2, "Documents", "zam");
34689
+ const workspacePath = resolve8(
34464
34690
  await input3({
34465
34691
  message: "Choose your ZAM workspace directory:",
34466
34692
  default: defaultWorkspace
@@ -35661,7 +35887,7 @@ observerCommand.command("revoke <process>").description("Remove a process from o
35661
35887
 
35662
35888
  // src/cli/commands/profile.ts
35663
35889
  init_kernel();
35664
- import { dirname as dirname10, resolve as resolve8 } from "path";
35890
+ import { dirname as dirname10, resolve as resolve9 } from "path";
35665
35891
  import { Command as Command13 } from "commander";
35666
35892
  var C2 = {
35667
35893
  reset: "\x1B[0m",
@@ -35689,7 +35915,7 @@ var profileCommand = new Command13("profile").description("Show or change this m
35689
35915
  if (opts.mode) setInstallMode(opts.mode);
35690
35916
  db = await openDatabaseWithSync({ initialize: true });
35691
35917
  if (opts.dir) {
35692
- await activateWorkspacePath(db, resolve8(opts.dir), {
35918
+ await activateWorkspacePath(db, resolve9(opts.dir), {
35693
35919
  kind: "personal",
35694
35920
  label: "Personal"
35695
35921
  });
@@ -36091,7 +36317,7 @@ ${"\u2550".repeat(50)}`);
36091
36317
 
36092
36318
  // src/cli/commands/session.ts
36093
36319
  init_kernel();
36094
- import { readFileSync as readFileSync17 } from "fs";
36320
+ import { readFileSync as readFileSync18 } from "fs";
36095
36321
  import { input as input6, select as select2 } from "@inquirer/prompts";
36096
36322
  import { Command as Command16 } from "commander";
36097
36323
  var sessionCommand = new Command16("session").description(
@@ -36282,7 +36508,7 @@ function loadPatternFile(path) {
36282
36508
  if (!path) return [];
36283
36509
  let parsed;
36284
36510
  try {
36285
- parsed = JSON.parse(readFileSync17(path, "utf-8"));
36511
+ parsed = JSON.parse(readFileSync18(path, "utf-8"));
36286
36512
  } catch (err) {
36287
36513
  throw new Error(
36288
36514
  `Cannot read synthesis patterns from ${path}: ${err.message}`
@@ -36473,7 +36699,7 @@ sessionCommand.command("end").description("End a session and show summary").requ
36473
36699
 
36474
36700
  // src/cli/commands/settings.ts
36475
36701
  init_kernel();
36476
- import { existsSync as existsSync23 } from "fs";
36702
+ import { existsSync as existsSync25 } from "fs";
36477
36703
  import { Command as Command17 } from "commander";
36478
36704
  var settingsCommand = new Command17("settings").description(
36479
36705
  "Manage user settings"
@@ -36638,7 +36864,7 @@ settingsCommand.command("repos").description("Show or set Personal, Team, and Or
36638
36864
  console.log("\nValidation:");
36639
36865
  for (const [name, path] of Object.entries(paths)) {
36640
36866
  if (path) {
36641
- const exists = existsSync23(path);
36867
+ const exists = existsSync25(path);
36642
36868
  console.log(
36643
36869
  ` ${name.padEnd(9)}: ${exists ? "\x1B[32m\u2713 Valid folder\x1B[0m" : "\x1B[31m\u2717 Directory does not exist\x1B[0m"}`
36644
36870
  );
@@ -36651,7 +36877,7 @@ settingsCommand.command("repos").description("Show or set Personal, Team, and Or
36651
36877
 
36652
36878
  // src/cli/commands/setup.ts
36653
36879
  init_kernel();
36654
- import { resolve as resolve9 } from "path";
36880
+ import { resolve as resolve10 } from "path";
36655
36881
  import { Command as Command18 } from "commander";
36656
36882
  function formatDatabaseInitTarget(target) {
36657
36883
  switch (target.kind) {
@@ -36751,7 +36977,7 @@ var setupCommand = new Command18("setup").description(
36751
36977
  console.error(`Error: ${err.message}`);
36752
36978
  process.exit(1);
36753
36979
  }
36754
- const target = resolve9(opts.target ?? process.cwd());
36980
+ const target = resolve10(opts.target ?? process.cwd());
36755
36981
  const updateExistingInstructions = Boolean(opts.target) || opts.force;
36756
36982
  console.log(
36757
36983
  `Setting up ZAM in ${target}${opts.dryRun ? " (dry run)" : ""}
@@ -36876,8 +37102,8 @@ skillCommand.command("add").description("Register a new agent skill").requiredOp
36876
37102
 
36877
37103
  // src/cli/commands/snapshot.ts
36878
37104
  init_kernel();
36879
- import { existsSync as existsSync24, mkdirSync as mkdirSync17, readFileSync as readFileSync18, writeFileSync as writeFileSync14 } from "fs";
36880
- import { dirname as dirname11, join as join25 } from "path";
37105
+ import { existsSync as existsSync26, mkdirSync as mkdirSync18, readFileSync as readFileSync19, writeFileSync as writeFileSync15 } from "fs";
37106
+ import { dirname as dirname11, join as join26 } from "path";
36881
37107
  import { Command as Command20 } from "commander";
36882
37108
  function defaultOutName() {
36883
37109
  const stamp = (/* @__PURE__ */ new Date()).toISOString().replace(/\.\d+Z$/, "").replace(/:/g, "-");
@@ -36906,12 +37132,12 @@ var exportCmd = new Command20("export").description("Write a portable SQL-text s
36906
37132
  process.stdout.write(snapshot);
36907
37133
  return;
36908
37134
  }
36909
- const out = opts.out ?? join25(personalDir, "snapshots", defaultOutName());
37135
+ const out = opts.out ?? join26(personalDir, "snapshots", defaultOutName());
36910
37136
  const dir = dirname11(out);
36911
- if (dir && dir !== "." && !existsSync24(dir)) {
36912
- mkdirSync17(dir, { recursive: true });
37137
+ if (dir && dir !== "." && !existsSync26(dir)) {
37138
+ mkdirSync18(dir, { recursive: true });
36913
37139
  }
36914
- writeFileSync14(out, snapshot, "utf-8");
37140
+ writeFileSync15(out, snapshot, "utf-8");
36915
37141
  console.log(`Snapshot written: ${out}`);
36916
37142
  console.log(
36917
37143
  ` ${total} row(s)${nonEmpty.length ? ` \u2014 ${nonEmpty.join(", ")}` : ""}`
@@ -36925,11 +37151,11 @@ var exportCmd = new Command20("export").description("Write a portable SQL-text s
36925
37151
  var importCmd = new Command20("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) => {
36926
37152
  let db;
36927
37153
  try {
36928
- if (!existsSync24(file)) {
37154
+ if (!existsSync26(file)) {
36929
37155
  console.error(`Error: Snapshot file not found: ${file}`);
36930
37156
  process.exit(1);
36931
37157
  }
36932
- const snapshot = readFileSync18(file, "utf-8");
37158
+ const snapshot = readFileSync19(file, "utf-8");
36933
37159
  db = await openDatabaseWithSync({ initialize: true });
36934
37160
  const result = await importSnapshot(db, snapshot, { force: opts.force });
36935
37161
  await db.close();
@@ -36947,11 +37173,11 @@ var importCmd = new Command20("import").description("Restore a snapshot into the
36947
37173
  });
36948
37174
  var verifyCmd = new Command20("verify").description("Check a snapshot's manifest and checksum without importing").argument("<file>", "Snapshot file to verify").action((file) => {
36949
37175
  try {
36950
- if (!existsSync24(file)) {
37176
+ if (!existsSync26(file)) {
36951
37177
  console.error(`Error: Snapshot file not found: ${file}`);
36952
37178
  process.exit(1);
36953
37179
  }
36954
- const manifest = verifySnapshot(readFileSync18(file, "utf-8"));
37180
+ const manifest = verifySnapshot(readFileSync19(file, "utf-8"));
36955
37181
  const { total, nonEmpty } = summarize(manifest.tables);
36956
37182
  console.log(`Valid snapshot (format v${manifest.version})`);
36957
37183
  console.log(` created: ${manifest.createdAt}`);
@@ -37500,9 +37726,9 @@ tokenCommand.command("reembed").description("Backfill or refresh semantic-search
37500
37726
  // src/cli/commands/ui.ts
37501
37727
  init_kernel();
37502
37728
  import { spawn as spawn3, spawnSync } from "child_process";
37503
- import { existsSync as existsSync25 } from "fs";
37504
- import { homedir as homedir15 } from "os";
37505
- import { dirname as dirname12, join as join26 } from "path";
37729
+ import { existsSync as existsSync27 } from "fs";
37730
+ import { homedir as homedir16 } from "os";
37731
+ import { dirname as dirname12, join as join27 } from "path";
37506
37732
  import { fileURLToPath as fileURLToPath6 } from "url";
37507
37733
  import { Command as Command23 } from "commander";
37508
37734
  var C3 = {
@@ -37518,8 +37744,8 @@ function findDesktopDir() {
37518
37744
  for (const start of starts) {
37519
37745
  let dir = start;
37520
37746
  for (let i = 0; i < 10; i++) {
37521
- if (existsSync25(join26(dir, "desktop", "src-tauri", "tauri.conf.json"))) {
37522
- return join26(dir, "desktop");
37747
+ if (existsSync27(join27(dir, "desktop", "src-tauri", "tauri.conf.json"))) {
37748
+ return join27(dir, "desktop");
37523
37749
  }
37524
37750
  const parent = dirname12(dir);
37525
37751
  if (parent === dir) break;
@@ -37529,30 +37755,30 @@ function findDesktopDir() {
37529
37755
  return null;
37530
37756
  }
37531
37757
  function findBuiltApp(desktopDir) {
37532
- const releaseDir = join26(desktopDir, "src-tauri", "target", "release");
37758
+ const releaseDir = join27(desktopDir, "src-tauri", "target", "release");
37533
37759
  if (process.platform === "win32") {
37534
37760
  for (const name of ["ZAM.exe", "zam.exe", "zam-desktop.exe"]) {
37535
- const p = join26(releaseDir, name);
37536
- if (existsSync25(p)) return p;
37761
+ const p = join27(releaseDir, name);
37762
+ if (existsSync27(p)) return p;
37537
37763
  }
37538
37764
  } else if (process.platform === "darwin") {
37539
- const app = join26(releaseDir, "bundle", "macos", "ZAM.app");
37540
- if (existsSync25(app)) return app;
37765
+ const app = join27(releaseDir, "bundle", "macos", "ZAM.app");
37766
+ if (existsSync27(app)) return app;
37541
37767
  } else {
37542
37768
  for (const name of ["zam", "ZAM", "zam-desktop"]) {
37543
- const p = join26(releaseDir, name);
37544
- if (existsSync25(p)) return p;
37769
+ const p = join27(releaseDir, name);
37770
+ if (existsSync27(p)) return p;
37545
37771
  }
37546
37772
  }
37547
37773
  return null;
37548
37774
  }
37549
37775
  function findInstalledApp() {
37550
37776
  const candidates = process.platform === "win32" ? [
37551
- process.env.LOCALAPPDATA && join26(process.env.LOCALAPPDATA, "Programs", "ZAM", "ZAM.exe"),
37552
- process.env.ProgramFiles && join26(process.env.ProgramFiles, "ZAM", "ZAM.exe"),
37553
- process.env["ProgramFiles(x86)"] && join26(process.env["ProgramFiles(x86)"], "ZAM", "ZAM.exe")
37554
- ] : process.platform === "darwin" ? ["/Applications/ZAM.app", join26(homedir15(), "Applications", "ZAM.app")] : ["/opt/ZAM/zam", "/usr/bin/zam-desktop"];
37555
- return candidates.find((candidate) => candidate && existsSync25(candidate)) || null;
37777
+ process.env.LOCALAPPDATA && join27(process.env.LOCALAPPDATA, "Programs", "ZAM", "ZAM.exe"),
37778
+ process.env.ProgramFiles && join27(process.env.ProgramFiles, "ZAM", "ZAM.exe"),
37779
+ process.env["ProgramFiles(x86)"] && join27(process.env["ProgramFiles(x86)"], "ZAM", "ZAM.exe")
37780
+ ] : process.platform === "darwin" ? ["/Applications/ZAM.app", join27(homedir16(), "Applications", "ZAM.app")] : ["/opt/ZAM/zam", "/usr/bin/zam-desktop"];
37781
+ return candidates.find((candidate) => candidate && existsSync27(candidate)) || null;
37556
37782
  }
37557
37783
  function runNpm(args, opts) {
37558
37784
  const res = spawnSync("npm", args, {
@@ -37563,7 +37789,7 @@ function runNpm(args, opts) {
37563
37789
  return res.status ?? 1;
37564
37790
  }
37565
37791
  function ensureDesktopDeps(desktopDir) {
37566
- if (existsSync25(join26(desktopDir, "node_modules"))) return true;
37792
+ if (existsSync27(join27(desktopDir, "node_modules"))) return true;
37567
37793
  console.log(
37568
37794
  `${C3.cyan}Installing desktop dependencies (one-time)...${C3.reset}`
37569
37795
  );
@@ -37589,13 +37815,13 @@ function requireRust() {
37589
37815
  function hasMsvcBuildTools() {
37590
37816
  if (process.platform !== "win32") return true;
37591
37817
  const pf86 = process.env["ProgramFiles(x86)"] || "C:\\Program Files (x86)";
37592
- const vswhere = join26(
37818
+ const vswhere = join27(
37593
37819
  pf86,
37594
37820
  "Microsoft Visual Studio",
37595
37821
  "Installer",
37596
37822
  "vswhere.exe"
37597
37823
  );
37598
- if (!existsSync25(vswhere)) return false;
37824
+ if (!existsSync27(vswhere)) return false;
37599
37825
  const res = spawnSync(
37600
37826
  vswhere,
37601
37827
  [
@@ -37627,7 +37853,7 @@ function requireMsvcOnWindows() {
37627
37853
  return false;
37628
37854
  }
37629
37855
  function warnIfCliMissing(repoRoot) {
37630
- if (!existsSync25(join26(repoRoot, "dist", "cli", "index.js"))) {
37856
+ if (!existsSync27(join27(repoRoot, "dist", "cli", "index.js"))) {
37631
37857
  console.warn(
37632
37858
  `${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}`
37633
37859
  );
@@ -37690,7 +37916,7 @@ var uiCommand = new Command23("ui").description("Launch the ZAM Desktop GUI (Act
37690
37916
  ).option("--shortcut", "Create Desktop + Start-menu shortcuts to the GUI").action((opts) => {
37691
37917
  const installedApp = findInstalledApp();
37692
37918
  if (!opts.dev && !opts.build && !opts.shortcut && installedApp) {
37693
- launchApp(installedApp, homedir15());
37919
+ launchApp(installedApp, homedir16());
37694
37920
  return;
37695
37921
  }
37696
37922
  const desktopDir = findDesktopDir();
@@ -37711,7 +37937,7 @@ var uiCommand = new Command23("ui").description("Launch the ZAM Desktop GUI (Act
37711
37937
  );
37712
37938
  const code = runNpm(["run", "tauri", "build"], { cwd: desktopDir });
37713
37939
  if (code === 0) {
37714
- const bundle = join26(
37940
+ const bundle = join27(
37715
37941
  desktopDir,
37716
37942
  "src-tauri",
37717
37943
  "target",
@@ -37789,8 +38015,8 @@ ${C3.dim}After that, 'zam ui' launches it directly.${C3.reset}`
37789
38015
  // src/cli/commands/update.ts
37790
38016
  init_kernel();
37791
38017
  import { spawnSync as spawnSync2 } from "child_process";
37792
- import { existsSync as existsSync26, readFileSync as readFileSync19, realpathSync as realpathSync2 } from "fs";
37793
- import { dirname as dirname13, join as join27 } from "path";
38018
+ import { existsSync as existsSync28, readFileSync as readFileSync20, realpathSync as realpathSync2 } from "fs";
38019
+ import { dirname as dirname13, join as join28 } from "path";
37794
38020
  import { fileURLToPath as fileURLToPath7 } from "url";
37795
38021
  import { confirm as confirm3 } from "@inquirer/prompts";
37796
38022
  import { Command as Command24 } from "commander";
@@ -37812,7 +38038,7 @@ var C4 = {
37812
38038
  function versionAt(dir) {
37813
38039
  try {
37814
38040
  const pkg2 = JSON.parse(
37815
- readFileSync19(join27(dir, "package.json"), "utf-8")
38041
+ readFileSync20(join28(dir, "package.json"), "utf-8")
37816
38042
  );
37817
38043
  return pkg2.version ?? "unknown";
37818
38044
  } catch {
@@ -37873,11 +38099,11 @@ function findSourceRepo() {
37873
38099
  let dir = realpathSync2(dirname13(fileURLToPath7(import.meta.url)));
37874
38100
  let parent = dirname13(dir);
37875
38101
  while (parent !== dir) {
37876
- if (existsSync26(join27(dir, ".git"))) return dir;
38102
+ if (existsSync28(join28(dir, ".git"))) return dir;
37877
38103
  dir = parent;
37878
38104
  parent = dirname13(dir);
37879
38105
  }
37880
- return existsSync26(join27(dir, ".git")) ? dir : null;
38106
+ return existsSync28(join28(dir, ".git")) ? dir : null;
37881
38107
  }
37882
38108
  function runGit(cwd, args, capture) {
37883
38109
  const res = spawnSync2("git", args, {
@@ -37898,7 +38124,7 @@ function runNpm2(args, cwd) {
37898
38124
  function smokeTestBuild(src) {
37899
38125
  const res = spawnSync2(
37900
38126
  process.execPath,
37901
- [join27(src, "dist", "cli", "index.js"), "--version"],
38127
+ [join28(src, "dist", "cli", "index.js"), "--version"],
37902
38128
  {
37903
38129
  cwd: src,
37904
38130
  encoding: "utf8",
@@ -37978,7 +38204,7 @@ Fix it manually in ${src} (try \`npm ci && npm run build\`, and check your Node
37978
38204
  console.log(`${C4.dim}\u2192 zam setup --force${C4.reset}`);
37979
38205
  const setup = spawnSync2(
37980
38206
  process.execPath,
37981
- [join27(src, "dist", "cli", "index.js"), "setup", "--force"],
38207
+ [join28(src, "dist", "cli", "index.js"), "setup", "--force"],
37982
38208
  { cwd: process.cwd(), stdio: "inherit" }
37983
38209
  );
37984
38210
  if (setup.status !== 0) {
@@ -38090,15 +38316,15 @@ var whoamiCommand = new Command25("whoami").description("Show or set the default
38090
38316
 
38091
38317
  // src/cli/commands/workspace.ts
38092
38318
  init_kernel();
38093
- import { execFileSync as execFileSync5 } from "child_process";
38094
- import { existsSync as existsSync27, writeFileSync as writeFileSync15 } from "fs";
38095
- import { homedir as homedir16 } from "os";
38096
- import { join as join28, resolve as resolve10 } from "path";
38319
+ import { execFileSync as execFileSync6 } from "child_process";
38320
+ import { existsSync as existsSync29, writeFileSync as writeFileSync16 } from "fs";
38321
+ import { homedir as homedir17 } from "os";
38322
+ import { join as join29, resolve as resolve11 } from "path";
38097
38323
  import { confirm as confirm4, input as input7 } from "@inquirer/prompts";
38098
38324
  import { Command as Command26 } from "commander";
38099
38325
  function runGit2(cwd, args) {
38100
38326
  try {
38101
- return execFileSync5("git", args, {
38327
+ return execFileSync6("git", args, {
38102
38328
  cwd,
38103
38329
  stdio: "pipe",
38104
38330
  encoding: "utf8"
@@ -38177,7 +38403,7 @@ workspaceCommand.command("list").description("List configured ZAM workspaces").o
38177
38403
  const workspaces = getConfiguredWorkspaces();
38178
38404
  const agents = parseSetupAgents();
38179
38405
  const linkHealth = Object.fromEntries(
38180
- workspaces.filter((workspace) => existsSync27(workspace.path)).map((workspace) => [
38406
+ workspaces.filter((workspace) => existsSync29(workspace.path)).map((workspace) => [
38181
38407
  workspace.id,
38182
38408
  summarizeSkillLinkHealth(inspectSkillLinks(workspace.path, agents))
38183
38409
  ])
@@ -38219,8 +38445,8 @@ workspaceCommand.command("add <id>").description("Register an existing directory
38219
38445
  `Source-control provider (${WORKSPACE_SOURCE_CONTROLS.join(" | ")})`
38220
38446
  ).option("--scopes <list>", "Comma-separated knowledge scopes").option("--default-agent <id>", "Default agent harness for this workspace").action((id, opts) => {
38221
38447
  try {
38222
- const path = resolve10(String(opts.path));
38223
- if (!existsSync27(path)) {
38448
+ const path = resolve11(String(opts.path));
38449
+ if (!existsSync29(path)) {
38224
38450
  console.error(`Workspace path does not exist: ${path}`);
38225
38451
  process.exit(1);
38226
38452
  }
@@ -38314,7 +38540,7 @@ workspaceCommand.command("publish").description("Publish your local workspace sa
38314
38540
  );
38315
38541
  process.exit(1);
38316
38542
  }
38317
- if (!existsSync27(workspaceDir)) {
38543
+ if (!existsSync29(workspaceDir)) {
38318
38544
  console.error(
38319
38545
  `\x1B[31m\u2717 Workspace directory does not exist: ${workspaceDir}\x1B[0m`
38320
38546
  );
@@ -38328,15 +38554,15 @@ Active workspace: \x1B[36m${workspaceDir}\x1B[0m`);
38328
38554
  );
38329
38555
  process.exit(1);
38330
38556
  }
38331
- const gitignorePath = join28(workspaceDir, ".gitignore");
38332
- if (!existsSync27(gitignorePath)) {
38333
- writeFileSync15(
38557
+ const gitignorePath = join29(workspaceDir, ".gitignore");
38558
+ if (!existsSync29(gitignorePath)) {
38559
+ writeFileSync16(
38334
38560
  gitignorePath,
38335
38561
  "node_modules/\n.agent/\n.agents/\n.claude/\n.gemini/\n.goose/\n*.log\n",
38336
38562
  "utf8"
38337
38563
  );
38338
38564
  }
38339
- const hasGitRepo = existsSync27(join28(workspaceDir, ".git"));
38565
+ const hasGitRepo = existsSync29(join29(workspaceDir, ".git"));
38340
38566
  if (!hasGitRepo) {
38341
38567
  console.log("Initializing local Git repository...");
38342
38568
  runGit2(workspaceDir, ["init", "-b", "main"]);
@@ -38368,7 +38594,7 @@ Active workspace: \x1B[36m${workspaceDir}\x1B[0m`);
38368
38594
  if (proceedGh) {
38369
38595
  try {
38370
38596
  console.log(`Creating GitHub repository ${repoName}...`);
38371
- execFileSync5("gh", ghRepoCreateArgs(repoName, repoVisibility), {
38597
+ execFileSync6("gh", ghRepoCreateArgs(repoName, repoVisibility), {
38372
38598
  cwd: workspaceDir,
38373
38599
  stdio: "inherit"
38374
38600
  });
@@ -38423,7 +38649,7 @@ Active workspace: \x1B[36m${workspaceDir}\x1B[0m`);
38423
38649
  }
38424
38650
  });
38425
38651
  workspaceCommand.command("data-dir").description("Print the ZAM data directory (database, credentials, config)").option("--json", "Output as JSON").action((opts) => {
38426
- const dir = join28(homedir16(), ".zam");
38652
+ const dir = join29(homedir17(), ".zam");
38427
38653
  console.log(opts.json ? JSON.stringify({ dataDir: dir }) : dir);
38428
38654
  });
38429
38655
  workspaceCommand.command("backup").description("Back up the local ZAM database into your workspace").option(
@@ -38466,7 +38692,7 @@ workspaceCommand.command("backup").description("Back up the local ZAM database i
38466
38692
  // src/cli/app.ts
38467
38693
  var __dirname = dirname14(fileURLToPath8(import.meta.url));
38468
38694
  var pkg = JSON.parse(
38469
- readFileSync20(join29(__dirname, "..", "..", "package.json"), "utf-8")
38695
+ readFileSync21(join30(__dirname, "..", "..", "package.json"), "utf-8")
38470
38696
  );
38471
38697
  var program = new Command27();
38472
38698
  program.name("zam").description(