zam-core 0.10.8 → 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);
@@ -8308,11 +8319,11 @@ var agentCommand = new Command("agent").description("Provision and inspect the a
8308
8319
 
8309
8320
  // src/cli/commands/bridge.ts
8310
8321
  init_kernel();
8311
- import { execFileSync as execFileSync4 } from "child_process";
8322
+ import { execFileSync as execFileSync5 } from "child_process";
8312
8323
  import { randomBytes as randomBytes2 } from "crypto";
8313
- import { existsSync as existsSync19, readdirSync as readdirSync2, readFileSync as readFileSync16, rmSync as rmSync3 } from "fs";
8314
- import { homedir as homedir13, tmpdir as tmpdir3 } from "os";
8315
- 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";
8316
8327
  import { Command as Command2 } from "commander";
8317
8328
  import { ulid as ulid10 } from "ulid";
8318
8329
 
@@ -9399,7 +9410,7 @@ async function prepareRecallChain(db, opts) {
9399
9410
  } else {
9400
9411
  spawnLocalRunner(endpoint.url, endpoint.model, endpoint.runner);
9401
9412
  while (Date.now() < deadline) {
9402
- await new Promise((resolve11) => setTimeout(resolve11, 1e3));
9413
+ await new Promise((resolve12) => setTimeout(resolve12, 1e3));
9403
9414
  if (await isLlmOnline(endpoint.url)) {
9404
9415
  online = true;
9405
9416
  break;
@@ -9692,7 +9703,7 @@ async function startLocalRunner(url, model, locale, hint) {
9692
9703
  let attempts = 0;
9693
9704
  const dotsPerLine = 30;
9694
9705
  while (true) {
9695
- await new Promise((resolve11) => setTimeout(resolve11, 500));
9706
+ await new Promise((resolve12) => setTimeout(resolve12, 500));
9696
9707
  if (await isLlmOnline(url)) {
9697
9708
  if (attempts > 0) process.stdout.write("\n");
9698
9709
  return true;
@@ -9781,8 +9792,8 @@ async function fetchWithInteractiveTimeout(url, options = {}) {
9781
9792
  const dotsPerLine = 30;
9782
9793
  while (true) {
9783
9794
  let timeoutId;
9784
- const timeoutPromise = new Promise((resolve11) => {
9785
- timeoutId = setTimeout(() => resolve11("timeout"), timeoutMs);
9795
+ const timeoutPromise = new Promise((resolve12) => {
9796
+ timeoutId = setTimeout(() => resolve12("timeout"), timeoutMs);
9786
9797
  });
9787
9798
  const dotsInterval = setInterval(() => {
9788
9799
  process.stdout.write(".");
@@ -10049,7 +10060,7 @@ async function readWebLink(url) {
10049
10060
  redirect: "manual",
10050
10061
  signal: controller.signal,
10051
10062
  headers: {
10052
- "User-Agent": "ZAM-Content-Studio/0.10.8"
10063
+ "User-Agent": "ZAM-Content-Studio/0.10.9"
10053
10064
  }
10054
10065
  });
10055
10066
  if (res.status >= 300 && res.status < 400) {
@@ -11312,6 +11323,127 @@ async function updateCheck(params) {
11312
11323
  });
11313
11324
  }
11314
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
+
11315
11447
  // src/cli/curriculum/breadcrumb.ts
11316
11448
  init_kernel();
11317
11449
  var LAST_SELECTION_KEY = "curriculum.lastSelection";
@@ -27998,1054 +28130,1132 @@ function getCurriculumProvider(id) {
27998
28130
  return CURRICULUM_PROVIDERS.find((provider) => provider.id === id);
27999
28131
  }
28000
28132
 
28001
- // src/cli/llm/capability-probe.ts
28133
+ // src/cli/install-repair.ts
28002
28134
  init_kernel();
28003
- var EMBEDDING_MODEL_HINTS = [
28004
- "embed",
28005
- "text-embedding",
28006
- "bge-",
28007
- "gte-",
28008
- "nomic",
28009
- "mxbai"
28010
- ];
28011
- var VISION_MODEL_HINTS = [
28012
- "vision",
28013
- "-vl",
28014
- "vl-",
28015
- "vlm",
28016
- "llava",
28017
- "gpt-4o",
28018
- "gpt-4.1",
28019
- "gpt-5",
28020
- "gemini",
28021
- "pixtral",
28022
- "minicpm-v",
28023
- "internvl",
28024
- "moondream",
28025
- "llama-3.2",
28026
- "llama3.2",
28027
- // Xiaomi MiMo(-VL) is multimodal; the plain "mimo-v*" tag carries no "-vl".
28028
- "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
+ }
28029
28171
  ];
28030
- function matchesAny(id, hints) {
28031
- const lower = id.toLowerCase();
28032
- 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;
28033
28195
  }
28034
- function catalogHasModel(catalog, model) {
28035
- const lower = model.toLowerCase();
28036
- 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
+ }
28037
28203
  }
28038
- function classifyCapabilities(entry, catalog, catalogKnown, dimProbeEmbedding = false) {
28039
- const detected = emptyCapabilityFlags();
28040
- if (entry.apiFlavor === "anthropic-messages") {
28041
- detected.text = true;
28042
- detected.image = true;
28043
- return detected;
28204
+ function isSymbolicLink(path) {
28205
+ try {
28206
+ return lstatSync2(path).isSymbolicLink();
28207
+ } catch {
28208
+ return false;
28044
28209
  }
28045
- const looksEmbedding = matchesAny(entry.model, EMBEDDING_MODEL_HINTS);
28046
- const looksVision = matchesAny(entry.model, VISION_MODEL_HINTS);
28047
- const inCatalog = catalogHasModel(catalog, entry.model);
28048
- detected.embedding = looksEmbedding || dimProbeEmbedding;
28049
- detected.image = looksVision;
28050
- detected.text = !detected.embedding && (inCatalog || !catalogKnown);
28051
- return detected;
28052
28210
  }
28053
- function resolveApiKey(apiKeyRef) {
28054
- if (!apiKeyRef) return DEFAULT_LLM_API_KEY;
28055
- return getProviderApiKey(apiKeyRef) ?? DEFAULT_LLM_API_KEY;
28211
+ function comparableRealPath(path) {
28212
+ const real = realpathSync(path);
28213
+ return process.platform === "win32" ? real.toLowerCase() : real;
28056
28214
  }
28057
- async function probeModelCapabilities(entry, opts = {}) {
28058
- const apiKey = resolveApiKey(entry.apiKeyRef);
28059
- if (entry.apiFlavor === "anthropic-messages") {
28060
- const online2 = await isLlmOnline(entry.url);
28061
- return {
28062
- reachable: online2,
28063
- catalog: [],
28064
- detected: classifyCapabilities(entry, [], false)
28065
- };
28066
- }
28067
- const online = await isLlmOnline(entry.url);
28068
- if (!online) {
28069
- return { reachable: false, catalog: [], detected: emptyCapabilityFlags() };
28070
- }
28071
- const catalog = await getAvailableModels(entry.url, apiKey);
28072
- const catalogKnown = catalog.length > 0;
28073
- let dimProbeEmbedding = false;
28074
- const looksEmbedding = matchesAny(entry.model, EMBEDDING_MODEL_HINTS);
28075
- if (opts.embeddingDimProbe && !catalogKnown && !looksEmbedding) {
28076
- try {
28077
- const [vector] = await embedTexts(
28078
- { url: entry.url, model: entry.model, apiKey },
28079
- ["capability probe"]
28080
- );
28081
- dimProbeEmbedding = Array.isArray(vector) && vector.length > 0;
28082
- } catch {
28083
- dimProbeEmbedding = false;
28084
- }
28215
+ function pathsResolveToSameDirectory(sourceDir, destinationDir) {
28216
+ try {
28217
+ return comparableRealPath(destinationDir) === comparableRealPath(sourceDir);
28218
+ } catch {
28219
+ return false;
28085
28220
  }
28086
- return {
28087
- reachable: true,
28088
- catalog,
28089
- detected: classifyCapabilities(
28090
- entry,
28091
- catalog,
28092
- catalogKnown,
28093
- dimProbeEmbedding
28094
- )
28095
- };
28096
28221
  }
28097
- function reconcileCapabilities(userSelected, detected) {
28098
- const result = emptyCapabilityFlags();
28099
- for (const key of Object.keys(result)) {
28100
- result[key] = userSelected[key] && detected[key];
28222
+ function linkPointsTo(sourceDir, destinationDir) {
28223
+ return isSymbolicLink(destinationDir) && pathsResolveToSameDirectory(sourceDir, destinationDir);
28224
+ }
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;
28101
28233
  }
28102
- return result;
28103
28234
  }
28104
- function validateModelSave(entry, probe, now = () => (/* @__PURE__ */ new Date()).toISOString()) {
28105
- if (!probe.reachable) {
28106
- return {
28107
- ok: false,
28108
- error: "Endpoint is unreachable \u2014 cannot verify capabilities. Bring it online and retry."
28109
- };
28235
+ function classifySkillDestination(sourceDir, destinationDir) {
28236
+ if (!existsSync19(sourceDir)) return "source-missing";
28237
+ if (!isSymbolicLink(destinationDir) && pathsResolveToSameDirectory(sourceDir, destinationDir)) {
28238
+ return "source-directory";
28110
28239
  }
28111
- return {
28112
- ok: true,
28113
- entry: {
28114
- ...entry,
28115
- capabilities: reconcileCapabilities(entry.capabilities, probe.detected),
28116
- detectedCapabilities: probe.detected,
28117
- probedAt: now()
28118
- }
28119
- };
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";
28120
28245
  }
28121
-
28122
- // src/cli/llm/vision.ts
28123
- init_kernel();
28124
- import { randomBytes } from "crypto";
28125
- import { readFileSync as readFileSync14 } from "fs";
28126
- import { tmpdir as tmpdir2 } from "os";
28127
- import { basename as basename4, join as join19 } from "path";
28128
- var LANGUAGE_NAMES2 = {
28129
- en: "English",
28130
- de: "German",
28131
- es: "Spanish",
28132
- fr: "French",
28133
- pt: "Portuguese",
28134
- zh: "Chinese",
28135
- ja: "Japanese"
28136
- };
28137
- var OBSERVATION_KINDS2 = /* @__PURE__ */ new Set([
28138
- "progress",
28139
- "step-completed",
28140
- "error",
28141
- "help-seeking",
28142
- "uncertain",
28143
- "privacy-pause",
28144
- "heartbeat"
28145
- ]);
28146
- var ACTION_TYPES2 = /* @__PURE__ */ new Set([
28147
- "click",
28148
- "shortcut",
28149
- "typing",
28150
- "scroll",
28151
- "window-change"
28152
- ]);
28153
- async function observeUiSnapshotViaLLM(db, input8) {
28154
- const cfg = await getProviderForRole(db, "vision");
28155
- if (!cfg.enabled) {
28156
- throw new Error(
28157
- "Vision observation is disabled in settings (llm.vision.enabled)"
28158
- );
28159
- }
28160
- const imageUrls = [];
28161
- const isVideo = /\.(mp4|mov|m4v|avi|mkv|webm)$/i.test(input8.imagePath);
28162
- if (isVideo) {
28163
- const { mkdirSync: mkdirSync18, readdirSync: readdirSync3, rmSync: rmSync4 } = await import("fs");
28164
- const { execSync: execSync6 } = await import("child_process");
28165
- const tempDir = join19(
28166
- tmpdir2(),
28167
- `zam-frames-${randomBytes(4).toString("hex")}`
28168
- );
28169
- mkdirSync18(tempDir, { recursive: true });
28170
- try {
28171
- execSync6(
28172
- `ffmpeg -i "${input8.imagePath}" -vf "fps=1/3,scale=1280:-1" -vsync vfr "${tempDir}/frame_%03d.png"`,
28173
- { stdio: "ignore" }
28174
- );
28175
- let files = readdirSync3(tempDir).filter((f) => f.endsWith(".png")).sort();
28176
- if (files.length === 0) {
28177
- execSync6(
28178
- `ffmpeg -i "${input8.imagePath}" -vframes 1 "${tempDir}/frame_001.png"`,
28179
- { stdio: "ignore" }
28180
- );
28181
- files = readdirSync3(tempDir).filter((f) => f.endsWith(".png")).sort();
28182
- }
28183
- const maxFrames = cfg.maxFrames ?? 100;
28184
- let sampledFiles = files;
28185
- if (files.length > maxFrames) {
28186
- if (maxFrames <= 1) {
28187
- sampledFiles = [files[0]];
28188
- } else {
28189
- const step = (files.length - 1) / (maxFrames - 1);
28190
- sampledFiles = [];
28191
- for (let i = 0; i < maxFrames; i++) {
28192
- const index = Math.round(i * step);
28193
- sampledFiles.push(files[index]);
28194
- }
28195
- }
28196
- }
28197
- for (const file of sampledFiles) {
28198
- const bytes = readFileSync14(join19(tempDir, file));
28199
- imageUrls.push(`data:image/png;base64,${bytes.toString("base64")}`);
28200
- }
28201
- } finally {
28202
- try {
28203
- rmSync4(tempDir, { recursive: true, force: true });
28204
- } catch {
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}`);
28205
28260
  }
28261
+ results.push({
28262
+ source: sourceDir,
28263
+ destination: destinationDir,
28264
+ action: "skipped",
28265
+ reason: "source-not-found"
28266
+ });
28267
+ continue;
28206
28268
  }
28207
- } else {
28208
- const imageBytes = readFileSync14(input8.imagePath);
28209
- const ext = input8.imagePath.split(".").pop()?.toLowerCase();
28210
- const mime = ext === "jpg" || ext === "jpeg" ? "image/jpeg" : "image/png";
28211
- imageUrls.push(`data:${mime};base64,${imageBytes.toString("base64")}`);
28212
- }
28213
- if (imageUrls.length === 0) {
28214
- throw new Error("No image data available for vision analysis");
28215
- }
28216
- const endpoints = [
28217
- {
28218
- url: cfg.url,
28219
- apiKey: cfg.apiKey || DEFAULT_LLM_API_KEY,
28220
- model: input8.model ?? cfg.model,
28221
- 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;
28222
28278
  }
28223
- ];
28224
- if (cfg.fallback) {
28225
- endpoints.push({
28226
- url: cfg.fallback.url,
28227
- apiKey: cfg.fallback.apiKey || DEFAULT_LLM_API_KEY,
28228
- model: cfg.fallback.model,
28229
- apiFlavor: cfg.fallback.apiFlavor
28230
- });
28231
- }
28232
- let lastRequestError;
28233
- let sawUnparseableDraft = false;
28234
- let sawInvalidDraft = false;
28235
- for (const endpoint of endpoints) {
28236
- let content;
28237
- try {
28238
- content = await requestVisionDraft({
28239
- ...endpoint,
28240
- locale: cfg.locale,
28241
- imageUrls,
28242
- 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"
28243
28286
  });
28244
- } catch (err) {
28245
- lastRequestError = err;
28246
28287
  continue;
28247
28288
  }
28248
- let draft;
28249
- try {
28250
- draft = extractDraft(content);
28251
- } catch {
28252
- 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
+ });
28253
28303
  continue;
28254
28304
  }
28255
- const report = buildReport(input8, draft);
28256
- if (isUiObservationReport(report)) {
28257
- 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}`);
28258
28319
  }
28259
- sawInvalidDraft = true;
28260
- }
28261
- if (lastRequestError && !sawUnparseableDraft && !sawInvalidDraft) {
28262
- throw lastRequestError;
28320
+ results.push({ source: sourceDir, destination: destinationDir, action });
28263
28321
  }
28264
- if (sawInvalidDraft) {
28265
- return uncertainReport(
28266
- input8,
28267
- "Vision model returned an invalid UI report draft."
28268
- );
28322
+ return results;
28323
+ }
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
+ });
28269
28336
  }
28270
- return uncertainReport(
28271
- input8,
28272
- "Vision model returned output that could not be parsed as JSON."
28273
- );
28337
+ return results;
28274
28338
  }
28275
- 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.";
28276
- function visionSchema(language) {
28277
- return `{
28278
- "kind": "progress | step-completed | error | help-seeking | uncertain",
28279
- "summary": "short factual UI summary in ${language}",
28280
- "actions": [{"type": "click | shortcut | typing | scroll | window-change", "target": "optional UI target", "result": "optional visible result"}],
28281
- "candidateTokens": [{"slug": "optional-kebab-case-skill-token", "confidence": 0.0, "rationale": "why this token may matter"}],
28282
- "confidence": 0.0
28283
- }`;
28339
+ function summarizeSkillLinkHealth(inspections) {
28340
+ if (inspections.some((item) => item.state === "unmanaged")) {
28341
+ return "unmanaged";
28342
+ }
28343
+ const repairable = ["missing", "broken", "stale-copy"];
28344
+ if (inspections.some((item) => repairable.includes(item.state))) {
28345
+ return "needs-repair";
28346
+ }
28347
+ return "healthy";
28284
28348
  }
28285
- function visionUserText(args, language) {
28286
- 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.";
28287
- return `${intro}
28288
- Application process: ${args.input.application.processName}
28289
- Window title: ${args.input.application.windowTitle ?? "(unknown)"}
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");
28360
+ }
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()}
28290
28368
 
28291
- Return this JSON draft only:
28292
- ${visionSchema(language)}`;
28369
+ ${block}
28370
+ `;
28371
+ if (!dryRun) writeFileSync12(dest, next, "utf8");
28372
+ return start >= 0 && end > start ? "update" : "write";
28293
28373
  }
28294
- async function requestVisionDraft(args) {
28295
- if (args.apiFlavor === "anthropic-messages") {
28296
- return requestAnthropicVisionDraft(args);
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}`);
28297
28379
  }
28298
- return requestChatCompletionsVisionDraft(args);
28299
28380
  }
28300
- async function requestChatCompletionsVisionDraft(args) {
28301
- const language = LANGUAGE_NAMES2[args.locale] ?? "English";
28302
- const res = await fetchWithInteractiveTimeout(
28303
- `${args.url}/chat/completions`,
28304
- {
28305
- method: "POST",
28306
- headers: {
28307
- "Content-Type": "application/json",
28308
- Authorization: `Bearer ${args.apiKey}`
28309
- },
28310
- body: JSON.stringify({
28311
- model: args.model,
28312
- messages: [
28313
- { role: "system", content: VISION_SYSTEM_PROMPT },
28314
- {
28315
- role: "user",
28316
- content: [
28317
- { type: "text", text: visionUserText(args, language) },
28318
- ...args.imageUrls.map((url) => ({
28319
- type: "image_url",
28320
- image_url: { url }
28321
- }))
28322
- ]
28323
- }
28324
- ],
28325
- temperature: 0,
28326
- max_tokens: args.input.maxTokens ?? DEFAULT_LLM_MAX_TOKENS,
28327
- ...args.url.includes("11434") || args.url.includes("localhost") || args.url.includes("127.0.0.1") ? { options: { num_ctx: 32768 } } : {}
28328
- }),
28329
- locale: args.locale,
28330
- hardTimeoutMs: args.input.hardTimeoutMs ?? 18e4
28331
- }
28332
- );
28333
- if (!res.ok) {
28334
- const errorText = await res.text().catch(() => "");
28335
- if (errorText.includes("image") && (errorText.includes("not support") || errorText.includes("unsupported"))) {
28336
- throw new Error(
28337
- `Vision model "${args.model}" does not support image input. Set a multimodal model: zam settings set llm.vision.model <model>`
28338
- );
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;
28339
28388
  }
28340
- throw new Error(
28341
- `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)
28342
28399
  );
28400
+ logInstructionAction(action, "CLAUDE.md", Boolean(opts.dryRun));
28401
+ return;
28343
28402
  }
28344
- const data = await res.json();
28345
- if (data.error !== void 0) {
28346
- const errorMsg = formatModelError(data.error);
28347
- if (errorMsg.includes("image") && (errorMsg.includes("not support") || errorMsg.includes("unsupported"))) {
28348
- throw new Error(
28349
- `Vision model "${args.model}" does not support image input. Set a multimodal model: zam settings set llm.vision.model <model>`
28350
- );
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;
28351
28438
  }
28352
- 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;
28353
28452
  }
28354
- const content = data.choices?.[0]?.message?.content;
28355
- if (!content || typeof content !== "string") {
28356
- 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`);
28357
28486
  }
28358
- return content.trim();
28359
28487
  }
28360
- function dataUrlToAnthropicImage(dataUrl) {
28361
- const match = dataUrl.match(/^data:(image\/[a-zA-Z]+);base64,(.+)$/);
28362
- if (!match) {
28363
- 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);
28364
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;
28365
28535
  return {
28366
- type: "image",
28367
- 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
+ )
28368
28545
  };
28369
28546
  }
28370
- async function requestAnthropicVisionDraft(args) {
28371
- const language = LANGUAGE_NAMES2[args.locale] ?? "English";
28372
- const base = args.url.replace(/\/+$/, "").replace(/\/v1$/, "");
28373
- const res = await fetchWithInteractiveTimeout(`${base}/v1/messages`, {
28374
- method: "POST",
28375
- headers: {
28376
- "Content-Type": "application/json",
28377
- "x-api-key": args.apiKey,
28378
- "anthropic-version": "2023-06-01"
28379
- },
28380
- body: JSON.stringify({
28381
- model: args.model,
28382
- max_tokens: args.input.maxTokens ?? DEFAULT_LLM_MAX_TOKENS,
28383
- system: VISION_SYSTEM_PROMPT,
28384
- messages: [
28385
- {
28386
- role: "user",
28387
- content: [
28388
- { type: "text", text: visionUserText(args, language) },
28389
- ...args.imageUrls.map(dataUrlToAnthropicImage)
28390
- ]
28391
- }
28392
- ]
28393
- }),
28394
- locale: args.locale,
28395
- hardTimeoutMs: args.input.hardTimeoutMs ?? 18e4
28396
- });
28397
- if (!res.ok) {
28398
- const errorText = await res.text().catch(() => "");
28399
- throw new Error(
28400
- `Vision LLM request failed: ${res.statusText} (${res.status}) - ${errorText}`
28401
- );
28402
- }
28403
- const data = await res.json();
28404
- if (data.error !== void 0) {
28405
- throw new Error(`Vision model failed: ${formatModelError(data.error)}`);
28406
- }
28407
- if (data.stop_reason === "refusal") {
28408
- throw new Error("Vision model refused the request (safety classifier).");
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
+ };
28409
28558
  }
28410
- const text = data.content?.find(
28411
- (b) => b.type === "text" && typeof b.text === "string"
28412
- )?.text;
28413
- if (!text) {
28414
- throw new Error("Empty response from vision model");
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;
28415
28564
  }
28416
- return text.trim();
28417
- }
28418
- function extractDraft(content) {
28419
- const fenced = content.match(/```(?:json)?\s*([\s\S]*?)```/i);
28420
- const candidate = (fenced?.[1] ?? content).trim();
28565
+ let agents;
28421
28566
  try {
28422
- return JSON.parse(candidate);
28423
- } catch {
28424
- const start = candidate.indexOf("{");
28425
- const end = candidate.lastIndexOf("}");
28426
- if (start === -1 || end <= start) throw new Error("no JSON object found");
28427
- return JSON.parse(
28428
- candidate.slice(start, end + 1)
28429
- );
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
+ };
28430
28576
  }
28577
+ (deps.setLastRepaired ?? setLastRepairedVersion)(version);
28578
+ return { version, skipped: false, cli, workspaces, agents };
28431
28579
  }
28432
- function buildReport(input8, draft) {
28433
- return {
28434
- version: UI_OBSERVATION_PROTOCOL_VERSION,
28435
- sessionId: input8.sessionId,
28436
- sequence: input8.sequence,
28437
- observedFrom: input8.observedFrom,
28438
- observedTo: input8.observedTo,
28439
- kind: parseKind(draft.kind),
28440
- application: input8.application,
28441
- summary: parseSummary(draft.summary),
28442
- actions: parseActions(draft.actions),
28443
- evidence: [
28444
- {
28445
- type: "keyframe",
28446
- ref: input8.evidenceRef ?? basename4(input8.imagePath),
28447
- redacted: input8.redacted ?? false
28448
- }
28449
- ],
28450
- candidateTokens: parseCandidateTokens(draft.candidateTokens),
28451
- confidence: parseConfidence(draft.confidence, 0.35)
28452
- };
28453
- }
28454
- function uncertainReport(input8, summary) {
28455
- return {
28456
- version: UI_OBSERVATION_PROTOCOL_VERSION,
28457
- sessionId: input8.sessionId,
28458
- sequence: input8.sequence,
28459
- observedFrom: input8.observedFrom,
28460
- observedTo: input8.observedTo,
28461
- kind: "uncertain",
28462
- application: input8.application,
28463
- summary,
28464
- actions: [],
28465
- evidence: [
28466
- {
28467
- type: "keyframe",
28468
- ref: input8.evidenceRef ?? basename4(input8.imagePath),
28469
- redacted: input8.redacted ?? false
28470
- }
28471
- ],
28472
- candidateTokens: [],
28473
- confidence: 0.1
28474
- };
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));
28475
28613
  }
28476
- function parseKind(value) {
28477
- if (typeof value === "string" && OBSERVATION_KINDS2.has(value)) {
28478
- return value;
28479
- }
28480
- return "uncertain";
28614
+ function catalogHasModel(catalog, model) {
28615
+ const lower = model.toLowerCase();
28616
+ return catalog.some((id) => id.toLowerCase() === lower);
28481
28617
  }
28482
- function parseSummary(value) {
28483
- if (typeof value === "string" && value.trim().length > 0) {
28484
- return value.trim();
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;
28485
28624
  }
28486
- return "Vision model did not provide a factual UI summary.";
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;
28487
28632
  }
28488
- function parseActions(value) {
28489
- if (!Array.isArray(value)) return [];
28490
- return value.flatMap((item) => {
28491
- if (!isRecord2(item) || typeof item.type !== "string") return [];
28492
- if (!ACTION_TYPES2.has(item.type)) return [];
28493
- const action = { type: item.type };
28494
- if (typeof item.target === "string" && item.target.trim()) {
28495
- action.target = item.target.trim();
28496
- }
28497
- if (typeof item.result === "string" && item.result.trim()) {
28498
- action.result = item.result.trim();
28499
- }
28500
- return [action];
28501
- });
28633
+ function resolveApiKey(apiKeyRef) {
28634
+ if (!apiKeyRef) return DEFAULT_LLM_API_KEY;
28635
+ return getProviderApiKey(apiKeyRef) ?? DEFAULT_LLM_API_KEY;
28502
28636
  }
28503
- function parseCandidateTokens(value) {
28504
- if (!Array.isArray(value)) return [];
28505
- return value.flatMap((item) => {
28506
- if (!isRecord2(item)) return [];
28507
- if (typeof item.slug !== "string" || !/^[A-Za-z0-9._-]+$/.test(item.slug)) {
28508
- return [];
28509
- }
28510
- if (typeof item.rationale !== "string" || !item.rationale.trim()) {
28511
- return [];
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;
28512
28664
  }
28513
- return [
28514
- {
28515
- slug: item.slug,
28516
- confidence: parseConfidence(item.confidence, 0.2),
28517
- rationale: item.rationale.trim()
28518
- }
28519
- ];
28520
- });
28521
- }
28522
- function parseConfidence(value, fallback) {
28523
- if (typeof value !== "number" || !Number.isFinite(value)) return fallback;
28524
- return Math.max(0, Math.min(1, value));
28665
+ }
28666
+ return {
28667
+ reachable: true,
28668
+ catalog,
28669
+ detected: classifyCapabilities(
28670
+ entry,
28671
+ catalog,
28672
+ catalogKnown,
28673
+ dimProbeEmbedding
28674
+ )
28675
+ };
28525
28676
  }
28526
- function formatModelError(error) {
28527
- if (typeof error === "string") return error;
28528
- if (isRecord2(error) && typeof error.message === "string") {
28529
- return error.message;
28677
+ function reconcileCapabilities(userSelected, detected) {
28678
+ const result = emptyCapabilityFlags();
28679
+ for (const key of Object.keys(result)) {
28680
+ result[key] = userSelected[key] && detected[key];
28530
28681
  }
28531
- return JSON.stringify(error);
28682
+ return result;
28532
28683
  }
28533
- function isRecord2(value) {
28534
- return typeof value === "object" && value !== null && !Array.isArray(value);
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
+ };
28690
+ }
28691
+ return {
28692
+ ok: true,
28693
+ entry: {
28694
+ ...entry,
28695
+ capabilities: reconcileCapabilities(entry.capabilities, probe.detected),
28696
+ detectedCapabilities: probe.detected,
28697
+ probedAt: now()
28698
+ }
28699
+ };
28535
28700
  }
28536
28701
 
28537
- // src/cli/providers/config.ts
28538
- init_kernel();
28539
-
28540
- // src/cli/commands/shared/db.ts
28702
+ // src/cli/llm/vision.ts
28541
28703
  init_kernel();
28542
- function defaultErrorHandler(message) {
28543
- console.error("Error:", message);
28544
- process.exit(1);
28545
- }
28546
- async function withDb(fn, onError = defaultErrorHandler) {
28547
- let db;
28548
- try {
28549
- db = await openDatabase();
28550
- await fn(db);
28551
- } catch (err) {
28552
- onError(err.message);
28553
- } finally {
28554
- 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
+ );
28555
28739
  }
28556
- }
28557
- function jsonOut(data) {
28558
- console.log(JSON.stringify(data, null, 2));
28559
- }
28560
-
28561
- // src/cli/providers/config.ts
28562
- var VALID_API_FLAVORS = [
28563
- "chat-completions",
28564
- "anthropic-messages"
28565
- ];
28566
- var VALID_ROLES = ["vision", "recall", "text", "embedding"];
28567
- function upsertProviderRecord(providers, name, patch) {
28568
- const merged = { ...providers[name] ?? {} };
28569
- if (patch.url !== void 0) merged.url = patch.url;
28570
- if (patch.model !== void 0) merged.model = patch.model;
28571
- if (patch.apiFlavor !== void 0) merged.apiFlavor = patch.apiFlavor;
28572
- if (patch.apiKeyRef !== void 0) merged.apiKeyRef = patch.apiKeyRef;
28573
- if (patch.label !== void 0) merged.label = patch.label;
28574
- if (patch.local !== void 0) merged.local = patch.local;
28575
- if (patch.runner !== void 0) merged.runner = patch.runner;
28576
- return { ...providers, [name]: merged };
28577
- }
28578
- function removeProviderRecord(providers, name) {
28579
- if (!(name in providers)) return { providers, removed: false };
28580
- const next = { ...providers };
28581
- delete next[name];
28582
- return { providers: next, removed: true };
28583
- }
28584
- function rolesReferencing(roles, name) {
28585
- return VALID_ROLES.filter((role) => {
28586
- const binding = roles[role];
28587
- return binding?.primary === name || binding?.fallback === name;
28588
- });
28589
- }
28590
- function bindRoleProviders(roles, role, primary, fallback) {
28591
- const binding = { primary };
28592
- if (fallback) binding.fallback = fallback;
28593
- return { ...roles, [role]: binding };
28594
- }
28595
- function unbindRole(roles, role) {
28596
- if (!(role in roles)) return roles;
28597
- const next = { ...roles };
28598
- delete next[role];
28599
- return next;
28600
- }
28601
- function maskSecret(key) {
28602
- return key.length <= 4 ? "\u2022\u2022\u2022\u2022" : `\u2026${key.slice(-4)}`;
28603
- }
28604
- function buildProviderListing(providers, hasKey) {
28605
- return Object.entries(providers).map(([name, rec]) => {
28606
- const apiFlavor = rec.apiFlavor ?? (rec.url ? inferApiFlavor(rec.url) : "chat-completions");
28607
- let keyState;
28608
- if (!rec.apiKeyRef) keyState = "none";
28609
- else keyState = hasKey(rec.apiKeyRef) ? "set" : "missing";
28610
- const row = {
28611
- name,
28612
- url: rec.url,
28613
- model: rec.model,
28614
- apiFlavor,
28615
- apiKeyRef: rec.apiKeyRef,
28616
- keyState
28617
- };
28618
- if (rec.label !== void 0) row.label = rec.label;
28619
- if (rec.local !== void 0) row.local = rec.local;
28620
- if (rec.runner !== void 0) row.runner = rec.runner;
28621
- return row;
28622
- });
28623
- }
28624
- function findOrphanKeyRefs(storedRefs, providers) {
28625
- const used = new Set(
28626
- Object.values(providers).map((rec) => rec.apiKeyRef).filter((ref) => !!ref)
28627
- );
28628
- return storedRefs.filter((ref) => !used.has(ref));
28629
- }
28630
- async function readJson(db, key, fallback) {
28631
- const raw = await getSetting(db, key);
28632
- if (!raw) return fallback;
28633
- try {
28634
- return JSON.parse(raw);
28635
- } catch {
28636
- 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;
28637
28840
  }
28638
- }
28639
- var readProviders = (db) => readJson(db, "llm.providers", {});
28640
- var readRoles = (db) => readJson(db, "llm.roles", {});
28641
- async function readScopedProviders(db, machine) {
28642
- if (machine) return getMachineAiConfig().providers ?? {};
28643
- if (!db)
28644
- throw new Error("Database is required for shared provider settings.");
28645
- return readProviders(db);
28646
- }
28647
- async function readScopedRoles(db, machine) {
28648
- if (machine) return getMachineAiConfig().roles ?? {};
28649
- if (!db)
28650
- throw new Error("Database is required for shared provider settings.");
28651
- return readRoles(db);
28652
- }
28653
- async function writeScopedProviders(db, machine, p) {
28654
- if (machine) {
28655
- const config = getMachineAiConfig();
28656
- saveMachineAiConfig({ ...config, providers: p });
28657
- return;
28841
+ if (lastRequestError && !sawUnparseableDraft && !sawInvalidDraft) {
28842
+ throw lastRequestError;
28658
28843
  }
28659
- if (!db)
28660
- throw new Error("Database is required for shared provider settings.");
28661
- await setSetting(db, "llm.providers", JSON.stringify(p));
28662
- }
28663
- async function writeScopedRoles(db, machine, r) {
28664
- if (machine) {
28665
- const config = getMachineAiConfig();
28666
- saveMachineAiConfig({ ...config, roles: r });
28667
- return;
28844
+ if (sawInvalidDraft) {
28845
+ return uncertainReport(
28846
+ input8,
28847
+ "Vision model returned an invalid UI report draft."
28848
+ );
28668
28849
  }
28669
- if (!db)
28670
- throw new Error("Database is required for shared provider settings.");
28671
- 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
+ );
28672
28854
  }
28673
- async function withProviderScope(machine, action) {
28674
- if (machine) {
28675
- await action(void 0);
28676
- return;
28677
- }
28678
- 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
+ }`;
28679
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)"}
28680
28870
 
28681
- // src/cli/provisioning/index.ts
28682
- import {
28683
- existsSync as existsSync18,
28684
- lstatSync as lstatSync2,
28685
- mkdirSync as mkdirSync13,
28686
- readFileSync as readFileSync15,
28687
- realpathSync,
28688
- rmSync as rmSync2,
28689
- symlinkSync,
28690
- writeFileSync as writeFileSync11
28691
- } from "fs";
28692
- import { basename as basename5, dirname as dirname9, join as join20 } from "path";
28693
- import { fileURLToPath as fileURLToPath5 } from "url";
28694
- var packageRoot3 = [
28695
- fileURLToPath5(new URL("../..", import.meta.url)),
28696
- fileURLToPath5(new URL("../../..", import.meta.url))
28697
- ].find((candidate) => existsSync18(join20(candidate, "package.json"))) ?? fileURLToPath5(new URL("../..", import.meta.url));
28698
- var ALL_SETUP_AGENTS = ["claude", "copilot", "codex", "agent"];
28699
- var SKILL_PAIRS = [
28700
- {
28701
- from: join20(packageRoot3, ".claude", "skills", "zam", "SKILL.md"),
28702
- to: join20(".claude", "skills", "zam", "SKILL.md"),
28703
- agents: ["claude", "copilot"]
28704
- },
28705
- {
28706
- from: join20(packageRoot3, ".agent", "skills", "zam", "SKILL.md"),
28707
- to: join20(".agent", "skills", "zam", "SKILL.md"),
28708
- agents: ["agent"]
28709
- },
28710
- {
28711
- from: join20(packageRoot3, ".agents", "skills", "zam", "SKILL.md"),
28712
- to: join20(".agents", "skills", "zam", "SKILL.md"),
28713
- 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);
28714
28877
  }
28715
- ];
28716
- function parseSetupAgents(value) {
28717
- if (!value || value.trim().toLowerCase() === "all") {
28718
- 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
+ );
28719
28923
  }
28720
- const aliases = {
28721
- claude: ["claude"],
28722
- copilot: ["copilot"],
28723
- codex: ["codex"],
28724
- agent: ["agent"],
28725
- opencode: ["agent"]
28726
- };
28727
- const selected = /* @__PURE__ */ new Set();
28728
- for (const raw of value.split(",")) {
28729
- const key = raw.trim().toLowerCase();
28730
- const mapped = aliases[key];
28731
- 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"))) {
28732
28928
  throw new Error(
28733
- `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>`
28734
28930
  );
28735
28931
  }
28736
- for (const item of mapped) selected.add(item);
28932
+ throw new Error(`Vision model failed: ${errorMsg}`);
28737
28933
  }
28738
- 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();
28739
28939
  }
28740
- function pathExists(path) {
28741
- try {
28742
- lstatSync2(path);
28743
- return true;
28744
- } catch {
28745
- 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");
28746
28995
  }
28996
+ return text.trim();
28747
28997
  }
28748
- function isSymbolicLink(path) {
28998
+ function extractDraft(content) {
28999
+ const fenced = content.match(/```(?:json)?\s*([\s\S]*?)```/i);
29000
+ const candidate = (fenced?.[1] ?? content).trim();
28749
29001
  try {
28750
- return lstatSync2(path).isSymbolicLink();
29002
+ return JSON.parse(candidate);
28751
29003
  } catch {
28752
- 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
+ );
28753
29010
  }
28754
29011
  }
28755
- function comparableRealPath(path) {
28756
- const real = realpathSync(path);
28757
- return process.platform === "win32" ? real.toLowerCase() : real;
28758
- }
28759
- function pathsResolveToSameDirectory(sourceDir, destinationDir) {
28760
- try {
28761
- return comparableRealPath(destinationDir) === comparableRealPath(sourceDir);
28762
- } catch {
28763
- return false;
28764
- }
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
+ };
28765
29033
  }
28766
- function linkPointsTo(sourceDir, destinationDir) {
28767
- 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
+ };
28768
29055
  }
28769
- function isZamSkillCopy(destinationDir) {
28770
- try {
28771
- if (!lstatSync2(destinationDir).isDirectory()) return false;
28772
- const skillFile = join20(destinationDir, "SKILL.md");
28773
- if (!existsSync18(skillFile)) return false;
28774
- return /^name:\s*zam\s*$/m.test(readFileSync15(skillFile, "utf8"));
28775
- } catch {
28776
- return false;
29056
+ function parseKind(value) {
29057
+ if (typeof value === "string" && OBSERVATION_KINDS2.has(value)) {
29058
+ return value;
28777
29059
  }
29060
+ return "uncertain";
28778
29061
  }
28779
- function classifySkillDestination(sourceDir, destinationDir) {
28780
- if (!existsSync18(sourceDir)) return "source-missing";
28781
- if (!isSymbolicLink(destinationDir) && pathsResolveToSameDirectory(sourceDir, destinationDir)) {
28782
- return "source-directory";
29062
+ function parseSummary(value) {
29063
+ if (typeof value === "string" && value.trim().length > 0) {
29064
+ return value.trim();
28783
29065
  }
28784
- if (linkPointsTo(sourceDir, destinationDir)) return "linked";
28785
- if (!pathExists(destinationDir)) return "missing";
28786
- if (isSymbolicLink(destinationDir)) return "broken";
28787
- if (isZamSkillCopy(destinationDir)) return "stale-copy";
28788
- return "unmanaged";
29066
+ return "Vision model did not provide a factual UI summary.";
28789
29067
  }
28790
- function wireSkills(cwd = process.cwd(), agents = parseSetupAgents(), opts = {}) {
28791
- const results = [];
28792
- const log = (message) => {
28793
- if (!opts.quiet) console.log(message);
28794
- };
28795
- for (const { from, to, agents: pairAgents } of SKILL_PAIRS) {
28796
- if (!pairAgents.some((agent) => agents.has(agent))) continue;
28797
- const sourceDir = dirname9(from);
28798
- const destinationDir = dirname9(join20(cwd, to));
28799
- const label = dirname9(to);
28800
- const state = classifySkillDestination(sourceDir, destinationDir);
28801
- if (state === "source-missing") {
28802
- if (!opts.quiet) {
28803
- console.warn(` warn source not found, skipping: ${sourceDir}`);
28804
- }
28805
- results.push({
28806
- source: sourceDir,
28807
- destination: destinationDir,
28808
- action: "skipped",
28809
- reason: "source-not-found"
28810
- });
28811
- 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();
28812
29076
  }
28813
- if (state === "source-directory") {
28814
- log(` skip ${label} (package source)`);
28815
- results.push({
28816
- source: sourceDir,
28817
- destination: destinationDir,
28818
- action: "skipped",
28819
- reason: "source-directory"
28820
- });
28821
- continue;
29077
+ if (typeof item.result === "string" && item.result.trim()) {
29078
+ action.result = item.result.trim();
28822
29079
  }
28823
- if (state === "linked") {
28824
- log(` skip ${label} (already linked)`);
28825
- results.push({
28826
- source: sourceDir,
28827
- destination: destinationDir,
28828
- action: "skipped",
28829
- reason: "already-linked"
28830
- });
28831
- 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 [];
28832
29089
  }
28833
- const destinationExists = state !== "missing";
28834
- const replaceExisting = destinationExists && (Boolean(opts.force) || state === "broken" || state === "stale-copy");
28835
- if (destinationExists && !replaceExisting) {
28836
- if (!opts.quiet) {
28837
- console.warn(
28838
- ` warn ${label} already exists and is not managed by ZAM; use --force to replace it`
28839
- );
28840
- }
28841
- results.push({
28842
- source: sourceDir,
28843
- destination: destinationDir,
28844
- action: "skipped",
28845
- reason: "unmanaged-destination"
28846
- });
28847
- continue;
29090
+ if (typeof item.rationale !== "string" || !item.rationale.trim()) {
29091
+ return [];
28848
29092
  }
28849
- const action = destinationExists ? "relinked" : "linked";
28850
- if (opts.dryRun) {
28851
- log(` would ${action === "linked" ? "link" : "relink"} ${label}`);
28852
- } else {
28853
- if (destinationExists) {
28854
- 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()
28855
29098
  }
28856
- mkdirSync13(dirname9(destinationDir), { recursive: true });
28857
- symlinkSync(
28858
- sourceDir,
28859
- destinationDir,
28860
- process.platform === "win32" ? "junction" : "dir"
28861
- );
28862
- log(` ${action === "linked" ? "link" : "relink"} ${label}`);
28863
- }
28864
- 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;
28865
29110
  }
28866
- 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 };
28867
29174
  }
28868
- function inspectSkillLinks(cwd = process.cwd(), agents = parseSetupAgents()) {
28869
- const results = [];
28870
- for (const { from, to, agents: pairAgents } of SKILL_PAIRS) {
28871
- if (!pairAgents.some((agent) => agents.has(agent))) continue;
28872
- const sourceDir = dirname9(from);
28873
- const destinationDir = dirname9(join20(cwd, to));
28874
- results.push({
28875
- agents: pairAgents,
28876
- source: sourceDir,
28877
- destination: destinationDir,
28878
- state: classifySkillDestination(sourceDir, destinationDir)
28879
- });
28880
- }
28881
- 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;
28882
29180
  }
28883
- function summarizeSkillLinkHealth(inspections) {
28884
- if (inspections.some((item) => item.state === "unmanaged")) {
28885
- return "unmanaged";
28886
- }
28887
- const repairable = ["missing", "broken", "stale-copy"];
28888
- if (inspections.some((item) => repairable.includes(item.state))) {
28889
- return "needs-repair";
28890
- }
28891
- return "healthy";
29181
+ function maskSecret(key) {
29182
+ return key.length <= 4 ? "\u2022\u2022\u2022\u2022" : `\u2026${key.slice(-4)}`;
28892
29183
  }
28893
- var ZAM_BLOCK_START = "<!-- ZAM:START -->";
28894
- var ZAM_BLOCK_END = "<!-- ZAM:END -->";
28895
- function upsertMarkedBlock(dest, blockBody, dryRun) {
28896
- const block = `${ZAM_BLOCK_START}
28897
- ${blockBody.trim()}
28898
- ${ZAM_BLOCK_END}`;
28899
- if (!existsSync18(dest)) {
28900
- if (!dryRun) {
28901
- mkdirSync13(dirname9(dest), { recursive: true });
28902
- writeFileSync11(dest, `${block}
28903
- `, "utf8");
28904
- }
28905
- return "write";
28906
- }
28907
- const existing = readFileSync15(dest, "utf8");
28908
- if (existing.includes(block)) return "skip";
28909
- const start = existing.indexOf(ZAM_BLOCK_START);
28910
- const end = existing.indexOf(ZAM_BLOCK_END);
28911
- const next = start >= 0 && end > start ? `${existing.slice(0, start)}${block}${existing.slice(end + ZAM_BLOCK_END.length)}` : `${existing.trimEnd()}
28912
-
28913
- ${block}
28914
- `;
28915
- if (!dryRun) writeFileSync11(dest, next, "utf8");
28916
- 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
+ });
28917
29203
  }
28918
- function logInstructionAction(action, label, dryRun) {
28919
- if (action === "skip") {
28920
- console.log(` skip ${label} (ZAM block already present)`);
28921
- } else {
28922
- 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;
28923
29217
  }
28924
29218
  }
28925
- function writeClaudeMd(skipClaudeMd, cwd = process.cwd(), opts = {}) {
28926
- if (skipClaudeMd) return;
28927
- const dest = join20(cwd, "CLAUDE.md");
28928
- if (existsSync18(dest)) {
28929
- if (!opts.updateExisting) {
28930
- console.log(` skip CLAUDE.md (already present)`);
28931
- return;
28932
- }
28933
- const action = upsertMarkedBlock(
28934
- dest,
28935
- `## ZAM learning sessions
28936
-
28937
- 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.
28938
-
28939
- - Skill files live under \`.claude/skills/zam/\`.
28940
- - Fast-changing review data lives in \`~/.zam/\`, not in this repository.
28941
- - The skill directory is linked to this ZAM installation and updates with it.`,
28942
- Boolean(opts.dryRun)
28943
- );
28944
- 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 });
28945
29237
  return;
28946
29238
  }
28947
- const name = basename5(cwd);
28948
- const content = `# ZAM Personal Kernel \u2014 ${name}
28949
-
28950
- This is a ZAM personal instance. ZAM builds lasting skills through spaced
28951
- repetition during real work \u2014 not separate study sessions.
28952
-
28953
- ## First time here?
28954
- Run \`/setup\` in Claude Code or Gemini CLI to complete first-time setup.
28955
-
28956
- ## Regular use
28957
- Run \`/zam\` to start a learning session on whatever you are working on.
28958
-
28959
- ## What lives here
28960
- - \`beliefs/\` \u2014 your worldview, approved by git commit
28961
- - \`goals/\` \u2014 your objectives, decomposed into tasks and learning tokens
28962
-
28963
- ## Fast-changing data
28964
- Learning tokens, cards, and review history live in local SQLite by default.
28965
- Use \`zam connector setup turso\` to store cloud credentials in
28966
- \`~/.zam/credentials.json\` and use a Turso database across machines.
28967
- `;
28968
- if (opts.dryRun) {
28969
- console.log(` would write CLAUDE.md`);
28970
- } else {
28971
- writeFileSync11(dest, content, "utf8");
28972
- console.log(` write CLAUDE.md`);
28973
- }
29239
+ if (!db)
29240
+ throw new Error("Database is required for shared provider settings.");
29241
+ await setSetting(db, "llm.providers", JSON.stringify(p));
28974
29242
  }
28975
- function writeAgentsMd(skipAgentsMd, cwd = process.cwd(), opts = {}) {
28976
- if (skipAgentsMd) return;
28977
- const dest = join20(cwd, "AGENTS.md");
28978
- if (existsSync18(dest)) {
28979
- if (!opts.updateExisting) {
28980
- console.log(` skip AGENTS.md (already present)`);
28981
- return;
28982
- }
28983
- const action = upsertMarkedBlock(
28984
- dest,
28985
- `## ZAM learning sessions
28986
-
28987
- 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.
28988
-
28989
- - Skill files live under \`.agents/skills/zam/\`.
28990
- - Fast-changing review data lives in \`~/.zam/\`, not in this repository.
28991
- - The skill directory is linked to this ZAM installation and updates with it.`,
28992
- Boolean(opts.dryRun)
28993
- );
28994
- 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 });
28995
29247
  return;
28996
29248
  }
28997
- const name = basename5(cwd);
28998
- const content = `# ZAM Personal Kernel - ${name}
28999
-
29000
- This is a ZAM personal instance. ZAM builds lasting skills through spaced
29001
- repetition during real work, not separate study sessions.
29002
-
29003
- ## First time here?
29004
- Run \`zam setup\` from the shell. When this repository includes
29005
- \`.agents/skills/setup/\`, you can instead select \`setup\` through \`/skills\`
29006
- or invoke \`$setup\`.
29007
-
29008
- ## Regular use
29009
- Select the \`zam\` skill through \`/skills\` or invoke \`$zam\` to start a
29010
- learning session on whatever you are working on.
29011
-
29012
- ## What lives here
29013
- - \`beliefs/\` - your worldview, approved by git commit
29014
- - \`goals/\` - your objectives, decomposed into tasks and learning tokens
29015
-
29016
- ## Fast-changing data
29017
- Learning tokens, cards, and review history live in local SQLite by default.
29018
- Use \`zam connector setup turso\` to store cloud credentials in
29019
- \`~/.zam/credentials.json\` and use a Turso database across machines.
29020
-
29021
- ## Codex skills
29022
- Codex discovers repository skills under \`.agents/skills/\`. Run
29023
- \`zam setup --force\` after upgrading \`zam-core\` to refresh them.
29024
- `;
29025
- if (opts.dryRun) {
29026
- console.log(` would write AGENTS.md`);
29027
- } else {
29028
- writeFileSync11(dest, content, "utf8");
29029
- console.log(` write AGENTS.md`);
29030
- }
29249
+ if (!db)
29250
+ throw new Error("Database is required for shared provider settings.");
29251
+ await setSetting(db, "llm.roles", JSON.stringify(r));
29031
29252
  }
29032
- function writeCopilotInstructions(cwd = process.cwd(), opts = {}) {
29033
- const dest = join20(cwd, ".github", "copilot-instructions.md");
29034
- const action = upsertMarkedBlock(
29035
- dest,
29036
- `## ZAM learning sessions
29037
-
29038
- 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.
29039
-
29040
- - Fast-changing review data lives in \`~/.zam/\`, not in this repository.
29041
- - The skill directory is linked to this ZAM installation and updates with it.`,
29042
- Boolean(opts.dryRun)
29043
- );
29044
- logInstructionAction(
29045
- action,
29046
- ".github/copilot-instructions.md",
29047
- Boolean(opts.dryRun)
29048
- );
29253
+ async function withProviderScope(machine, action) {
29254
+ if (machine) {
29255
+ await action(void 0);
29256
+ return;
29257
+ }
29258
+ await withDb(action);
29049
29259
  }
29050
29260
 
29051
29261
  // src/cli/users/identity.ts
@@ -29071,13 +29281,13 @@ async function resolveUser(opts, db, resolveOpts) {
29071
29281
  }
29072
29282
 
29073
29283
  // src/cli/workspaces/backup.ts
29074
- import { mkdirSync as mkdirSync14 } from "fs";
29075
- import { join as join21 } from "path";
29284
+ import { mkdirSync as mkdirSync15 } from "fs";
29285
+ import { join as join22 } from "path";
29076
29286
  async function backupDatabaseTo(db, targetDir) {
29077
- const backupDir = join21(targetDir, "zam-backups");
29078
- mkdirSync14(backupDir, { recursive: true });
29287
+ const backupDir = join22(targetDir, "zam-backups");
29288
+ mkdirSync15(backupDir, { recursive: true });
29079
29289
  const stamp = (/* @__PURE__ */ new Date()).toISOString().replace(/[:.]/g, "-");
29080
- const dest = join21(backupDir, `zam-${stamp}.db`);
29290
+ const dest = join22(backupDir, `zam-${stamp}.db`);
29081
29291
  await db.exec(`VACUUM INTO '${dest.replace(/'/g, "''")}'`);
29082
29292
  return dest;
29083
29293
  }
@@ -29208,20 +29418,20 @@ bridgeCommand.command("workspace-info").description("Report the workspace dir, i
29208
29418
  activeWorkspace,
29209
29419
  workspaceDir: activeWorkspace.path,
29210
29420
  defaultWorkspaceDir: defaultWorkspaceDir(),
29211
- dataDir: join22(homedir13(), ".zam")
29421
+ dataDir: join23(homedir14(), ".zam")
29212
29422
  });
29213
29423
  });
29214
29424
  });
29215
29425
  function provisionConfiguredWorkspaces() {
29216
29426
  return getConfiguredWorkspaces().flatMap(
29217
- (workspace) => existsSync19(workspace.path) ? wireSkills(workspace.path, parseSetupAgents(), { quiet: true }) : []
29427
+ (workspace) => existsSync21(workspace.path) ? wireSkills(workspace.path, parseSetupAgents(), { quiet: true }) : []
29218
29428
  );
29219
29429
  }
29220
29430
  function buildWorkspaceLinkHealth(workspaces) {
29221
29431
  const agents = parseSetupAgents();
29222
29432
  const map = {};
29223
29433
  for (const workspace of workspaces) {
29224
- if (!existsSync19(workspace.path)) continue;
29434
+ if (!existsSync21(workspace.path)) continue;
29225
29435
  const links = inspectSkillLinks(workspace.path, agents);
29226
29436
  map[workspace.id] = {
29227
29437
  health: summarizeSkillLinkHealth(links),
@@ -29251,7 +29461,7 @@ bridgeCommand.command("workspace-list").description("List configured ZAM workspa
29251
29461
  activeWorkspace,
29252
29462
  workspaceDir: activeWorkspace.path,
29253
29463
  defaultWorkspaceDir: defaultWorkspaceDir(),
29254
- dataDir: join22(homedir13(), ".zam"),
29464
+ dataDir: join23(homedir14(), ".zam"),
29255
29465
  linkHealth: buildWorkspaceLinkHealth(workspaces)
29256
29466
  });
29257
29467
  });
@@ -29266,7 +29476,7 @@ bridgeCommand.command("workspace-repair-links").description(
29266
29476
  if (!id) jsonError("A non-empty --id is required");
29267
29477
  const workspace = getConfiguredWorkspaces().find((item) => item.id === id);
29268
29478
  if (!workspace) jsonError(`Workspace "${id}" is not configured`);
29269
- if (!existsSync19(workspace.path)) {
29479
+ if (!existsSync21(workspace.path)) {
29270
29480
  jsonError(`Workspace path does not exist: ${workspace.path}`);
29271
29481
  }
29272
29482
  const agents = parseSetupAgents(opts.agents);
@@ -29297,8 +29507,8 @@ function parseBridgeWorkspaceKind(value) {
29297
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) => {
29298
29508
  const raw = String(opts.path ?? "").trim();
29299
29509
  if (!raw) jsonError("A non-empty --path is required");
29300
- const path = resolve5(raw);
29301
- 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}`);
29302
29512
  const id = opts.id ? String(opts.id).trim() : void 0;
29303
29513
  if (opts.id && !id) jsonError("A non-empty --id is required");
29304
29514
  const kind = parseBridgeWorkspaceKind(opts.kind);
@@ -29342,8 +29552,8 @@ bridgeCommand.command("workspace-remove").description("Unregister a ZAM workspac
29342
29552
  bridgeCommand.command("set-workspace-dir").description("Set the personal workspace directory (JSON)").requiredOption("--dir <path>", "Path to the workspace directory").action(async (opts) => {
29343
29553
  const raw = String(opts.dir ?? "").trim();
29344
29554
  if (!raw) jsonError("A non-empty --dir is required");
29345
- const dir = resolve5(raw);
29346
- 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}`);
29347
29557
  const skillLinks = wireSkills(dir, parseSetupAgents(), { quiet: true });
29348
29558
  await withDb2(async (db) => {
29349
29559
  const workspace = await activateWorkspacePath(db, dir);
@@ -29406,7 +29616,7 @@ bridgeCommand.command("agent-open").description("Launch an agent harness in the
29406
29616
  jsonError(`Workspace is not configured: ${opts.workspace}`);
29407
29617
  }
29408
29618
  const activeWorkspace = await ensureActiveWorkspace(db);
29409
- 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);
29410
29620
  launchHarness(harness, {
29411
29621
  executable,
29412
29622
  workspace,
@@ -29778,7 +29988,7 @@ bridgeCommand.command("discover-skills").description(
29778
29988
  "20"
29779
29989
  ).action(async (opts) => {
29780
29990
  try {
29781
- const monitorDir = join22(homedir13(), ".zam", "monitor");
29991
+ const monitorDir = join23(homedir14(), ".zam", "monitor");
29782
29992
  let files;
29783
29993
  try {
29784
29994
  files = readdirSync2(monitorDir).filter((f) => f.endsWith(".jsonl"));
@@ -29791,7 +30001,7 @@ bridgeCommand.command("discover-skills").description(
29791
30001
  return;
29792
30002
  }
29793
30003
  const limit = Number(opts.limit);
29794
- 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);
29795
30005
  const sessionCommands = /* @__PURE__ */ new Map();
29796
30006
  for (const file of sorted) {
29797
30007
  const sessionId = file.name.replace(".jsonl", "");
@@ -29903,7 +30113,7 @@ bridgeCommand.command("observe-ui-snapshot").description(
29903
30113
  });
29904
30114
  function resolveWindowsPowerShell() {
29905
30115
  try {
29906
- execFileSync4("where.exe", ["pwsh.exe"], { stdio: "ignore" });
30116
+ execFileSync5("where.exe", ["pwsh.exe"], { stdio: "ignore" });
29907
30117
  return "pwsh";
29908
30118
  } catch {
29909
30119
  return "powershell";
@@ -29918,7 +30128,7 @@ function captureScreenshot(outputPath, hwnd, processName) {
29918
30128
  throw new Error(`Invalid process name format: ${processName}`);
29919
30129
  }
29920
30130
  if (platform === "win32") {
29921
- const stdout = execFileSync4(
30131
+ const stdout = execFileSync5(
29922
30132
  resolveWindowsPowerShell(),
29923
30133
  [
29924
30134
  "-NoProfile",
@@ -30231,13 +30441,13 @@ Write-CaptureResult "fullscreen" $hwndVal $matchedBy
30231
30441
  } else if (platform === "darwin") {
30232
30442
  if (hwnd) {
30233
30443
  const parsedHwnd = hwnd.startsWith("0x") ? parseInt(hwnd, 16) : parseInt(hwnd, 10);
30234
- execFileSync4("screencapture", ["-l", String(parsedHwnd), outputPath], {
30444
+ execFileSync5("screencapture", ["-l", String(parsedHwnd), outputPath], {
30235
30445
  stdio: "pipe"
30236
30446
  });
30237
30447
  return { method: "screencapture-window", target: null };
30238
30448
  } else if (processName) {
30239
30449
  try {
30240
- const windowId = execFileSync4(
30450
+ const windowId = execFileSync5(
30241
30451
  "osascript",
30242
30452
  [
30243
30453
  "-e",
@@ -30246,17 +30456,17 @@ Write-CaptureResult "fullscreen" $hwndVal $matchedBy
30246
30456
  { encoding: "utf8", stdio: ["pipe", "pipe", "ignore"] }
30247
30457
  ).trim();
30248
30458
  if (windowId && /^\d+$/.test(windowId)) {
30249
- execFileSync4("screencapture", ["-l", windowId, outputPath], {
30459
+ execFileSync5("screencapture", ["-l", windowId, outputPath], {
30250
30460
  stdio: "pipe"
30251
30461
  });
30252
30462
  return { method: "screencapture-window", target: null };
30253
30463
  }
30254
30464
  } catch {
30255
30465
  }
30256
- execFileSync4("screencapture", ["-x", outputPath], { stdio: "pipe" });
30466
+ execFileSync5("screencapture", ["-x", outputPath], { stdio: "pipe" });
30257
30467
  return { method: "screencapture-full", target: null };
30258
30468
  } else {
30259
- execFileSync4("screencapture", ["-x", outputPath], { stdio: "pipe" });
30469
+ execFileSync5("screencapture", ["-x", outputPath], { stdio: "pipe" });
30260
30470
  return { method: "screencapture-full", target: null };
30261
30471
  }
30262
30472
  } else {
@@ -30293,7 +30503,7 @@ bridgeCommand.command("capture-ui").description("Capture a screenshot for agent-
30293
30503
  return;
30294
30504
  }
30295
30505
  }
30296
- 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`);
30297
30507
  const captureResult = isProvided ? { method: "provided", target: null } : captureScreenshot(outputPath, opts.hwnd, opts.processName);
30298
30508
  if (!isProvided) {
30299
30509
  const post = decidePostCapture(policy, {
@@ -30321,7 +30531,7 @@ bridgeCommand.command("capture-ui").description("Capture a screenshot for agent-
30321
30531
  return;
30322
30532
  }
30323
30533
  }
30324
- const imageBytes = readFileSync16(outputPath);
30534
+ const imageBytes = readFileSync17(outputPath);
30325
30535
  const base64 = imageBytes.toString("base64");
30326
30536
  jsonOut2({
30327
30537
  sessionId: opts.session ?? null,
@@ -30348,11 +30558,11 @@ bridgeCommand.command("start-recording").description("Start screen recording in
30348
30558
  return;
30349
30559
  }
30350
30560
  const sessionId = opts.session;
30351
- const statePath = join22(tmpdir3(), `zam-recording-${sessionId}.json`);
30561
+ const statePath = join23(tmpdir3(), `zam-recording-${sessionId}.json`);
30352
30562
  const defaultExt = platform === "win32" ? ".mkv" : ".mov";
30353
- const outputPath = opts.output ?? join22(tmpdir3(), `zam-recording-${sessionId}${defaultExt}`);
30354
- const { existsSync: existsSync28, writeFileSync: writeFileSync16, openSync, closeSync } = await import("fs");
30355
- 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)) {
30356
30566
  jsonOut2({
30357
30567
  sessionId,
30358
30568
  started: false,
@@ -30360,7 +30570,7 @@ bridgeCommand.command("start-recording").description("Start screen recording in
30360
30570
  });
30361
30571
  return;
30362
30572
  }
30363
- const logPath = join22(tmpdir3(), `zam-recording-${sessionId}.log`);
30573
+ const logPath = join23(tmpdir3(), `zam-recording-${sessionId}.log`);
30364
30574
  let logFd;
30365
30575
  try {
30366
30576
  logFd = openSync(logPath, "w");
@@ -30406,7 +30616,7 @@ bridgeCommand.command("start-recording").description("Start screen recording in
30406
30616
  }
30407
30617
  child.unref();
30408
30618
  if (child.pid) {
30409
- writeFileSync16(
30619
+ writeFileSync17(
30410
30620
  statePath,
30411
30621
  JSON.stringify({
30412
30622
  pid: child.pid,
@@ -30442,9 +30652,9 @@ bridgeCommand.command("stop-recording").description(
30442
30652
  return;
30443
30653
  }
30444
30654
  const sessionId = opts.session;
30445
- const statePath = join22(tmpdir3(), `zam-recording-${sessionId}.json`);
30446
- const { existsSync: existsSync28, readFileSync: readFileSync21, rmSync: rmSync4 } = await import("fs");
30447
- 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)) {
30448
30658
  jsonOut2({
30449
30659
  sessionId,
30450
30660
  stopped: false,
@@ -30452,7 +30662,7 @@ bridgeCommand.command("stop-recording").description(
30452
30662
  });
30453
30663
  return;
30454
30664
  }
30455
- const state = JSON.parse(readFileSync21(statePath, "utf8"));
30665
+ const state = JSON.parse(readFileSync22(statePath, "utf8"));
30456
30666
  const { pid, outputPath } = state;
30457
30667
  try {
30458
30668
  process.kill(pid, "SIGINT");
@@ -30468,7 +30678,7 @@ bridgeCommand.command("stop-recording").description(
30468
30678
  };
30469
30679
  let attempts = 0;
30470
30680
  while (isProcessRunning(pid) && attempts < 20) {
30471
- await new Promise((resolve11) => setTimeout(resolve11, 250));
30681
+ await new Promise((resolve12) => setTimeout(resolve12, 250));
30472
30682
  attempts++;
30473
30683
  }
30474
30684
  if (isProcessRunning(pid)) {
@@ -30481,7 +30691,7 @@ bridgeCommand.command("stop-recording").description(
30481
30691
  rmSync4(statePath, { force: true });
30482
30692
  } catch {
30483
30693
  }
30484
- if (!existsSync28(outputPath)) {
30694
+ if (!existsSync30(outputPath)) {
30485
30695
  jsonOut2({
30486
30696
  sessionId,
30487
30697
  stopped: false,
@@ -30911,7 +31121,7 @@ bridgeCommand.command("cloud-model-hint").description("Suggest a cloud model for
30911
31121
  });
30912
31122
  bridgeCommand.command("local-llm-hints").description("Detect installed local LLM servers and suggest defaults (JSON)").action(() => {
30913
31123
  const profile = getSystemProfile();
30914
- const flmInstalled = hasCommand("flm") || existsSync19("C:\\Program Files\\flm\\flm.exe");
31124
+ const flmInstalled = hasCommand("flm") || existsSync21("C:\\Program Files\\flm\\flm.exe");
30915
31125
  const ollamaInstalled = isOllamaInstalled();
30916
31126
  const runners = [
30917
31127
  { id: "flm", label: "FastFlowLM", installed: flmInstalled },
@@ -31143,13 +31353,15 @@ bridgeCommand.command("desktop-bootstrap").description("Initialize first-run des
31143
31353
  const userId = await ensureDefaultUser(db, opts.user);
31144
31354
  const { enabled, url, model, locale } = await getLlmConfig(db);
31145
31355
  const { workspaceDir, activeWorkspaceId, skillLinks } = await ensureDesktopWorkspace(db);
31356
+ const cli = installCliShim();
31146
31357
  jsonOut2({
31147
31358
  userId,
31148
31359
  locale,
31149
31360
  llm: { enabled, url, model },
31150
31361
  activeWorkspaceId,
31151
31362
  workspaceDir,
31152
- skillLinks
31363
+ skillLinks,
31364
+ cli
31153
31365
  });
31154
31366
  });
31155
31367
  });
@@ -31166,6 +31378,22 @@ bridgeCommand.command("get-settings").description("Get active ZAM settings (JSON
31166
31378
  });
31167
31379
  });
31168
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
+ });
31169
31397
  bridgeCommand.command("agent-harness-status").description(
31170
31398
  "Detect installed agent harnesses and their ZAM MCP configuration state (JSON)"
31171
31399
  ).action(() => {
@@ -34098,28 +34326,28 @@ var doctorCommand = new Command5("doctor").description(
34098
34326
  // src/cli/commands/git-sync.ts
34099
34327
  init_kernel();
34100
34328
  import { execSync as execSync5 } from "child_process";
34101
- import { chmodSync as chmodSync2, existsSync as existsSync20, writeFileSync as writeFileSync12 } from "fs";
34102
- 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";
34103
34331
  import { Command as Command6 } from "commander";
34104
34332
  function installHook2() {
34105
- const gitDir = join23(process.cwd(), ".git");
34106
- if (!existsSync20(gitDir)) {
34333
+ const gitDir = join24(process.cwd(), ".git");
34334
+ if (!existsSync22(gitDir)) {
34107
34335
  console.error(
34108
34336
  "Error: Current directory is not the root of a Git repository."
34109
34337
  );
34110
34338
  process.exit(1);
34111
34339
  }
34112
- const hooksDir = join23(gitDir, "hooks");
34113
- const hookPath = join23(hooksDir, "post-commit");
34340
+ const hooksDir = join24(gitDir, "hooks");
34341
+ const hookPath = join24(hooksDir, "post-commit");
34114
34342
  const hookContent = `#!/bin/sh
34115
34343
  # ZAM Spaced Repetition Auto-Stale Hook
34116
34344
  # Triggered automatically on git commits to decay modified concept cards.
34117
34345
  zam git-sync --commit HEAD --quiet
34118
34346
  `;
34119
34347
  try {
34120
- writeFileSync12(hookPath, hookContent, { encoding: "utf-8", flag: "w" });
34348
+ writeFileSync13(hookPath, hookContent, { encoding: "utf-8", flag: "w" });
34121
34349
  try {
34122
- chmodSync2(hookPath, "755");
34350
+ chmodSync3(hookPath, "755");
34123
34351
  } catch (_e) {
34124
34352
  }
34125
34353
  console.log(
@@ -34220,8 +34448,8 @@ ZAM Auto-Stale Complete: Scanned ${changedFiles.length} file(s).`
34220
34448
 
34221
34449
  // src/cli/commands/goal.ts
34222
34450
  init_kernel();
34223
- import { existsSync as existsSync21, mkdirSync as mkdirSync15 } from "fs";
34224
- import { resolve as resolve6 } from "path";
34451
+ import { existsSync as existsSync23, mkdirSync as mkdirSync16 } from "fs";
34452
+ import { resolve as resolve7 } from "path";
34225
34453
  import { input as input2 } from "@inquirer/prompts";
34226
34454
  import { Command as Command7 } from "commander";
34227
34455
  async function resolveGoalsDir() {
@@ -34234,7 +34462,7 @@ async function resolveGoalsDir() {
34234
34462
  } finally {
34235
34463
  await db?.close();
34236
34464
  }
34237
- return goalsDir ? resolve6(goalsDir) : resolve6("goals");
34465
+ return goalsDir ? resolve7(goalsDir) : resolve7("goals");
34238
34466
  }
34239
34467
  var goalCommand = new Command7("goal").description(
34240
34468
  "Manage learning goals (markdown files)"
@@ -34244,7 +34472,7 @@ goalCommand.command("list").description("List all goals").option(
34244
34472
  "Filter by status (active, completed, paused, abandoned)"
34245
34473
  ).option("--tree", "Show goals as a tree with parent/child relationships").option("--json", "Output as JSON").action(async (opts) => {
34246
34474
  const goalsDir = await resolveGoalsDir();
34247
- if (!existsSync21(goalsDir)) {
34475
+ if (!existsSync23(goalsDir)) {
34248
34476
  console.error(`Goals directory not found: ${goalsDir}`);
34249
34477
  console.error(
34250
34478
  "Set it with: zam settings set personal.goals_dir /path/to/goals"
@@ -34345,8 +34573,8 @@ ${"\u2500".repeat(50)}`);
34345
34573
  });
34346
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) => {
34347
34575
  const goalsDir = await resolveGoalsDir();
34348
- if (!existsSync21(goalsDir)) {
34349
- mkdirSync15(goalsDir, { recursive: true });
34576
+ if (!existsSync23(goalsDir)) {
34577
+ mkdirSync16(goalsDir, { recursive: true });
34350
34578
  }
34351
34579
  let slug = opts.slug;
34352
34580
  let title = opts.title;
@@ -34406,22 +34634,22 @@ goalCommand.command("status <slug> <status>").description("Update a goal's statu
34406
34634
 
34407
34635
  // src/cli/commands/init.ts
34408
34636
  init_kernel();
34409
- import { existsSync as existsSync22, mkdirSync as mkdirSync16, writeFileSync as writeFileSync13 } from "fs";
34410
- import { homedir as homedir14 } from "os";
34411
- 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";
34412
34640
  import { confirm, input as input3 } from "@inquirer/prompts";
34413
34641
  import { Command as Command8 } from "commander";
34414
- var HOME2 = homedir14();
34642
+ var HOME2 = homedir15();
34415
34643
  function printLine(char = "\u2550", len = 60, color = "\x1B[36m") {
34416
34644
  console.log(`${color}${char.repeat(len)}\x1B[0m`);
34417
34645
  }
34418
34646
  function bootstrapSandboxWorkspace(workspaceDir) {
34419
- mkdirSync16(join24(workspaceDir, "beliefs"), { recursive: true });
34420
- mkdirSync16(join24(workspaceDir, "goals"), { recursive: true });
34421
- mkdirSync16(join24(workspaceDir, "skills"), { recursive: true });
34422
- const worldviewFile = join24(workspaceDir, "beliefs", "worldview.md");
34423
- if (!existsSync22(worldviewFile)) {
34424
- 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(
34425
34653
  worldviewFile,
34426
34654
  `# Personal Worldview
34427
34655
 
@@ -34433,9 +34661,9 @@ Here, I declare the core concepts and principles I want to master.
34433
34661
  "utf8"
34434
34662
  );
34435
34663
  }
34436
- const goalsFile = join24(workspaceDir, "goals", "goals.md");
34437
- if (!existsSync22(goalsFile)) {
34438
- writeFileSync13(
34664
+ const goalsFile = join25(workspaceDir, "goals", "goals.md");
34665
+ if (!existsSync24(goalsFile)) {
34666
+ writeFileSync14(
34439
34667
  goalsFile,
34440
34668
  `# Personal Goals
34441
34669
 
@@ -34457,8 +34685,8 @@ var initCommand = new Command8("init").description("Launch the guided interactiv
34457
34685
  );
34458
34686
  printLine();
34459
34687
  console.log("\n\x1B[1m[1/5] Setting up Local Workspace Sandbox\x1B[0m");
34460
- const defaultWorkspace = join24(HOME2, "Documents", "zam");
34461
- const workspacePath = resolve7(
34688
+ const defaultWorkspace = join25(HOME2, "Documents", "zam");
34689
+ const workspacePath = resolve8(
34462
34690
  await input3({
34463
34691
  message: "Choose your ZAM workspace directory:",
34464
34692
  default: defaultWorkspace
@@ -35659,7 +35887,7 @@ observerCommand.command("revoke <process>").description("Remove a process from o
35659
35887
 
35660
35888
  // src/cli/commands/profile.ts
35661
35889
  init_kernel();
35662
- import { dirname as dirname10, resolve as resolve8 } from "path";
35890
+ import { dirname as dirname10, resolve as resolve9 } from "path";
35663
35891
  import { Command as Command13 } from "commander";
35664
35892
  var C2 = {
35665
35893
  reset: "\x1B[0m",
@@ -35687,7 +35915,7 @@ var profileCommand = new Command13("profile").description("Show or change this m
35687
35915
  if (opts.mode) setInstallMode(opts.mode);
35688
35916
  db = await openDatabaseWithSync({ initialize: true });
35689
35917
  if (opts.dir) {
35690
- await activateWorkspacePath(db, resolve8(opts.dir), {
35918
+ await activateWorkspacePath(db, resolve9(opts.dir), {
35691
35919
  kind: "personal",
35692
35920
  label: "Personal"
35693
35921
  });
@@ -36089,7 +36317,7 @@ ${"\u2550".repeat(50)}`);
36089
36317
 
36090
36318
  // src/cli/commands/session.ts
36091
36319
  init_kernel();
36092
- import { readFileSync as readFileSync17 } from "fs";
36320
+ import { readFileSync as readFileSync18 } from "fs";
36093
36321
  import { input as input6, select as select2 } from "@inquirer/prompts";
36094
36322
  import { Command as Command16 } from "commander";
36095
36323
  var sessionCommand = new Command16("session").description(
@@ -36280,7 +36508,7 @@ function loadPatternFile(path) {
36280
36508
  if (!path) return [];
36281
36509
  let parsed;
36282
36510
  try {
36283
- parsed = JSON.parse(readFileSync17(path, "utf-8"));
36511
+ parsed = JSON.parse(readFileSync18(path, "utf-8"));
36284
36512
  } catch (err) {
36285
36513
  throw new Error(
36286
36514
  `Cannot read synthesis patterns from ${path}: ${err.message}`
@@ -36471,7 +36699,7 @@ sessionCommand.command("end").description("End a session and show summary").requ
36471
36699
 
36472
36700
  // src/cli/commands/settings.ts
36473
36701
  init_kernel();
36474
- import { existsSync as existsSync23 } from "fs";
36702
+ import { existsSync as existsSync25 } from "fs";
36475
36703
  import { Command as Command17 } from "commander";
36476
36704
  var settingsCommand = new Command17("settings").description(
36477
36705
  "Manage user settings"
@@ -36636,7 +36864,7 @@ settingsCommand.command("repos").description("Show or set Personal, Team, and Or
36636
36864
  console.log("\nValidation:");
36637
36865
  for (const [name, path] of Object.entries(paths)) {
36638
36866
  if (path) {
36639
- const exists = existsSync23(path);
36867
+ const exists = existsSync25(path);
36640
36868
  console.log(
36641
36869
  ` ${name.padEnd(9)}: ${exists ? "\x1B[32m\u2713 Valid folder\x1B[0m" : "\x1B[31m\u2717 Directory does not exist\x1B[0m"}`
36642
36870
  );
@@ -36649,7 +36877,7 @@ settingsCommand.command("repos").description("Show or set Personal, Team, and Or
36649
36877
 
36650
36878
  // src/cli/commands/setup.ts
36651
36879
  init_kernel();
36652
- import { resolve as resolve9 } from "path";
36880
+ import { resolve as resolve10 } from "path";
36653
36881
  import { Command as Command18 } from "commander";
36654
36882
  function formatDatabaseInitTarget(target) {
36655
36883
  switch (target.kind) {
@@ -36749,7 +36977,7 @@ var setupCommand = new Command18("setup").description(
36749
36977
  console.error(`Error: ${err.message}`);
36750
36978
  process.exit(1);
36751
36979
  }
36752
- const target = resolve9(opts.target ?? process.cwd());
36980
+ const target = resolve10(opts.target ?? process.cwd());
36753
36981
  const updateExistingInstructions = Boolean(opts.target) || opts.force;
36754
36982
  console.log(
36755
36983
  `Setting up ZAM in ${target}${opts.dryRun ? " (dry run)" : ""}
@@ -36874,8 +37102,8 @@ skillCommand.command("add").description("Register a new agent skill").requiredOp
36874
37102
 
36875
37103
  // src/cli/commands/snapshot.ts
36876
37104
  init_kernel();
36877
- import { existsSync as existsSync24, mkdirSync as mkdirSync17, readFileSync as readFileSync18, writeFileSync as writeFileSync14 } from "fs";
36878
- 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";
36879
37107
  import { Command as Command20 } from "commander";
36880
37108
  function defaultOutName() {
36881
37109
  const stamp = (/* @__PURE__ */ new Date()).toISOString().replace(/\.\d+Z$/, "").replace(/:/g, "-");
@@ -36904,12 +37132,12 @@ var exportCmd = new Command20("export").description("Write a portable SQL-text s
36904
37132
  process.stdout.write(snapshot);
36905
37133
  return;
36906
37134
  }
36907
- const out = opts.out ?? join25(personalDir, "snapshots", defaultOutName());
37135
+ const out = opts.out ?? join26(personalDir, "snapshots", defaultOutName());
36908
37136
  const dir = dirname11(out);
36909
- if (dir && dir !== "." && !existsSync24(dir)) {
36910
- mkdirSync17(dir, { recursive: true });
37137
+ if (dir && dir !== "." && !existsSync26(dir)) {
37138
+ mkdirSync18(dir, { recursive: true });
36911
37139
  }
36912
- writeFileSync14(out, snapshot, "utf-8");
37140
+ writeFileSync15(out, snapshot, "utf-8");
36913
37141
  console.log(`Snapshot written: ${out}`);
36914
37142
  console.log(
36915
37143
  ` ${total} row(s)${nonEmpty.length ? ` \u2014 ${nonEmpty.join(", ")}` : ""}`
@@ -36923,11 +37151,11 @@ var exportCmd = new Command20("export").description("Write a portable SQL-text s
36923
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) => {
36924
37152
  let db;
36925
37153
  try {
36926
- if (!existsSync24(file)) {
37154
+ if (!existsSync26(file)) {
36927
37155
  console.error(`Error: Snapshot file not found: ${file}`);
36928
37156
  process.exit(1);
36929
37157
  }
36930
- const snapshot = readFileSync18(file, "utf-8");
37158
+ const snapshot = readFileSync19(file, "utf-8");
36931
37159
  db = await openDatabaseWithSync({ initialize: true });
36932
37160
  const result = await importSnapshot(db, snapshot, { force: opts.force });
36933
37161
  await db.close();
@@ -36945,11 +37173,11 @@ var importCmd = new Command20("import").description("Restore a snapshot into the
36945
37173
  });
36946
37174
  var verifyCmd = new Command20("verify").description("Check a snapshot's manifest and checksum without importing").argument("<file>", "Snapshot file to verify").action((file) => {
36947
37175
  try {
36948
- if (!existsSync24(file)) {
37176
+ if (!existsSync26(file)) {
36949
37177
  console.error(`Error: Snapshot file not found: ${file}`);
36950
37178
  process.exit(1);
36951
37179
  }
36952
- const manifest = verifySnapshot(readFileSync18(file, "utf-8"));
37180
+ const manifest = verifySnapshot(readFileSync19(file, "utf-8"));
36953
37181
  const { total, nonEmpty } = summarize(manifest.tables);
36954
37182
  console.log(`Valid snapshot (format v${manifest.version})`);
36955
37183
  console.log(` created: ${manifest.createdAt}`);
@@ -37498,9 +37726,9 @@ tokenCommand.command("reembed").description("Backfill or refresh semantic-search
37498
37726
  // src/cli/commands/ui.ts
37499
37727
  init_kernel();
37500
37728
  import { spawn as spawn3, spawnSync } from "child_process";
37501
- import { existsSync as existsSync25 } from "fs";
37502
- import { homedir as homedir15 } from "os";
37503
- 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";
37504
37732
  import { fileURLToPath as fileURLToPath6 } from "url";
37505
37733
  import { Command as Command23 } from "commander";
37506
37734
  var C3 = {
@@ -37516,8 +37744,8 @@ function findDesktopDir() {
37516
37744
  for (const start of starts) {
37517
37745
  let dir = start;
37518
37746
  for (let i = 0; i < 10; i++) {
37519
- if (existsSync25(join26(dir, "desktop", "src-tauri", "tauri.conf.json"))) {
37520
- return join26(dir, "desktop");
37747
+ if (existsSync27(join27(dir, "desktop", "src-tauri", "tauri.conf.json"))) {
37748
+ return join27(dir, "desktop");
37521
37749
  }
37522
37750
  const parent = dirname12(dir);
37523
37751
  if (parent === dir) break;
@@ -37527,30 +37755,30 @@ function findDesktopDir() {
37527
37755
  return null;
37528
37756
  }
37529
37757
  function findBuiltApp(desktopDir) {
37530
- const releaseDir = join26(desktopDir, "src-tauri", "target", "release");
37758
+ const releaseDir = join27(desktopDir, "src-tauri", "target", "release");
37531
37759
  if (process.platform === "win32") {
37532
37760
  for (const name of ["ZAM.exe", "zam.exe", "zam-desktop.exe"]) {
37533
- const p = join26(releaseDir, name);
37534
- if (existsSync25(p)) return p;
37761
+ const p = join27(releaseDir, name);
37762
+ if (existsSync27(p)) return p;
37535
37763
  }
37536
37764
  } else if (process.platform === "darwin") {
37537
- const app = join26(releaseDir, "bundle", "macos", "ZAM.app");
37538
- if (existsSync25(app)) return app;
37765
+ const app = join27(releaseDir, "bundle", "macos", "ZAM.app");
37766
+ if (existsSync27(app)) return app;
37539
37767
  } else {
37540
37768
  for (const name of ["zam", "ZAM", "zam-desktop"]) {
37541
- const p = join26(releaseDir, name);
37542
- if (existsSync25(p)) return p;
37769
+ const p = join27(releaseDir, name);
37770
+ if (existsSync27(p)) return p;
37543
37771
  }
37544
37772
  }
37545
37773
  return null;
37546
37774
  }
37547
37775
  function findInstalledApp() {
37548
37776
  const candidates = process.platform === "win32" ? [
37549
- process.env.LOCALAPPDATA && join26(process.env.LOCALAPPDATA, "Programs", "ZAM", "ZAM.exe"),
37550
- process.env.ProgramFiles && join26(process.env.ProgramFiles, "ZAM", "ZAM.exe"),
37551
- process.env["ProgramFiles(x86)"] && join26(process.env["ProgramFiles(x86)"], "ZAM", "ZAM.exe")
37552
- ] : process.platform === "darwin" ? ["/Applications/ZAM.app", join26(homedir15(), "Applications", "ZAM.app")] : ["/opt/ZAM/zam", "/usr/bin/zam-desktop"];
37553
- 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;
37554
37782
  }
37555
37783
  function runNpm(args, opts) {
37556
37784
  const res = spawnSync("npm", args, {
@@ -37561,7 +37789,7 @@ function runNpm(args, opts) {
37561
37789
  return res.status ?? 1;
37562
37790
  }
37563
37791
  function ensureDesktopDeps(desktopDir) {
37564
- if (existsSync25(join26(desktopDir, "node_modules"))) return true;
37792
+ if (existsSync27(join27(desktopDir, "node_modules"))) return true;
37565
37793
  console.log(
37566
37794
  `${C3.cyan}Installing desktop dependencies (one-time)...${C3.reset}`
37567
37795
  );
@@ -37587,13 +37815,13 @@ function requireRust() {
37587
37815
  function hasMsvcBuildTools() {
37588
37816
  if (process.platform !== "win32") return true;
37589
37817
  const pf86 = process.env["ProgramFiles(x86)"] || "C:\\Program Files (x86)";
37590
- const vswhere = join26(
37818
+ const vswhere = join27(
37591
37819
  pf86,
37592
37820
  "Microsoft Visual Studio",
37593
37821
  "Installer",
37594
37822
  "vswhere.exe"
37595
37823
  );
37596
- if (!existsSync25(vswhere)) return false;
37824
+ if (!existsSync27(vswhere)) return false;
37597
37825
  const res = spawnSync(
37598
37826
  vswhere,
37599
37827
  [
@@ -37625,7 +37853,7 @@ function requireMsvcOnWindows() {
37625
37853
  return false;
37626
37854
  }
37627
37855
  function warnIfCliMissing(repoRoot) {
37628
- if (!existsSync25(join26(repoRoot, "dist", "cli", "index.js"))) {
37856
+ if (!existsSync27(join27(repoRoot, "dist", "cli", "index.js"))) {
37629
37857
  console.warn(
37630
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}`
37631
37859
  );
@@ -37688,7 +37916,7 @@ var uiCommand = new Command23("ui").description("Launch the ZAM Desktop GUI (Act
37688
37916
  ).option("--shortcut", "Create Desktop + Start-menu shortcuts to the GUI").action((opts) => {
37689
37917
  const installedApp = findInstalledApp();
37690
37918
  if (!opts.dev && !opts.build && !opts.shortcut && installedApp) {
37691
- launchApp(installedApp, homedir15());
37919
+ launchApp(installedApp, homedir16());
37692
37920
  return;
37693
37921
  }
37694
37922
  const desktopDir = findDesktopDir();
@@ -37709,7 +37937,7 @@ var uiCommand = new Command23("ui").description("Launch the ZAM Desktop GUI (Act
37709
37937
  );
37710
37938
  const code = runNpm(["run", "tauri", "build"], { cwd: desktopDir });
37711
37939
  if (code === 0) {
37712
- const bundle = join26(
37940
+ const bundle = join27(
37713
37941
  desktopDir,
37714
37942
  "src-tauri",
37715
37943
  "target",
@@ -37787,8 +38015,8 @@ ${C3.dim}After that, 'zam ui' launches it directly.${C3.reset}`
37787
38015
  // src/cli/commands/update.ts
37788
38016
  init_kernel();
37789
38017
  import { spawnSync as spawnSync2 } from "child_process";
37790
- import { existsSync as existsSync26, readFileSync as readFileSync19, realpathSync as realpathSync2 } from "fs";
37791
- 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";
37792
38020
  import { fileURLToPath as fileURLToPath7 } from "url";
37793
38021
  import { confirm as confirm3 } from "@inquirer/prompts";
37794
38022
  import { Command as Command24 } from "commander";
@@ -37810,7 +38038,7 @@ var C4 = {
37810
38038
  function versionAt(dir) {
37811
38039
  try {
37812
38040
  const pkg2 = JSON.parse(
37813
- readFileSync19(join27(dir, "package.json"), "utf-8")
38041
+ readFileSync20(join28(dir, "package.json"), "utf-8")
37814
38042
  );
37815
38043
  return pkg2.version ?? "unknown";
37816
38044
  } catch {
@@ -37871,11 +38099,11 @@ function findSourceRepo() {
37871
38099
  let dir = realpathSync2(dirname13(fileURLToPath7(import.meta.url)));
37872
38100
  let parent = dirname13(dir);
37873
38101
  while (parent !== dir) {
37874
- if (existsSync26(join27(dir, ".git"))) return dir;
38102
+ if (existsSync28(join28(dir, ".git"))) return dir;
37875
38103
  dir = parent;
37876
38104
  parent = dirname13(dir);
37877
38105
  }
37878
- return existsSync26(join27(dir, ".git")) ? dir : null;
38106
+ return existsSync28(join28(dir, ".git")) ? dir : null;
37879
38107
  }
37880
38108
  function runGit(cwd, args, capture) {
37881
38109
  const res = spawnSync2("git", args, {
@@ -37896,7 +38124,7 @@ function runNpm2(args, cwd) {
37896
38124
  function smokeTestBuild(src) {
37897
38125
  const res = spawnSync2(
37898
38126
  process.execPath,
37899
- [join27(src, "dist", "cli", "index.js"), "--version"],
38127
+ [join28(src, "dist", "cli", "index.js"), "--version"],
37900
38128
  {
37901
38129
  cwd: src,
37902
38130
  encoding: "utf8",
@@ -37976,7 +38204,7 @@ Fix it manually in ${src} (try \`npm ci && npm run build\`, and check your Node
37976
38204
  console.log(`${C4.dim}\u2192 zam setup --force${C4.reset}`);
37977
38205
  const setup = spawnSync2(
37978
38206
  process.execPath,
37979
- [join27(src, "dist", "cli", "index.js"), "setup", "--force"],
38207
+ [join28(src, "dist", "cli", "index.js"), "setup", "--force"],
37980
38208
  { cwd: process.cwd(), stdio: "inherit" }
37981
38209
  );
37982
38210
  if (setup.status !== 0) {
@@ -38088,15 +38316,15 @@ var whoamiCommand = new Command25("whoami").description("Show or set the default
38088
38316
 
38089
38317
  // src/cli/commands/workspace.ts
38090
38318
  init_kernel();
38091
- import { execFileSync as execFileSync5 } from "child_process";
38092
- import { existsSync as existsSync27, writeFileSync as writeFileSync15 } from "fs";
38093
- import { homedir as homedir16 } from "os";
38094
- 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";
38095
38323
  import { confirm as confirm4, input as input7 } from "@inquirer/prompts";
38096
38324
  import { Command as Command26 } from "commander";
38097
38325
  function runGit2(cwd, args) {
38098
38326
  try {
38099
- return execFileSync5("git", args, {
38327
+ return execFileSync6("git", args, {
38100
38328
  cwd,
38101
38329
  stdio: "pipe",
38102
38330
  encoding: "utf8"
@@ -38175,7 +38403,7 @@ workspaceCommand.command("list").description("List configured ZAM workspaces").o
38175
38403
  const workspaces = getConfiguredWorkspaces();
38176
38404
  const agents = parseSetupAgents();
38177
38405
  const linkHealth = Object.fromEntries(
38178
- workspaces.filter((workspace) => existsSync27(workspace.path)).map((workspace) => [
38406
+ workspaces.filter((workspace) => existsSync29(workspace.path)).map((workspace) => [
38179
38407
  workspace.id,
38180
38408
  summarizeSkillLinkHealth(inspectSkillLinks(workspace.path, agents))
38181
38409
  ])
@@ -38217,8 +38445,8 @@ workspaceCommand.command("add <id>").description("Register an existing directory
38217
38445
  `Source-control provider (${WORKSPACE_SOURCE_CONTROLS.join(" | ")})`
38218
38446
  ).option("--scopes <list>", "Comma-separated knowledge scopes").option("--default-agent <id>", "Default agent harness for this workspace").action((id, opts) => {
38219
38447
  try {
38220
- const path = resolve10(String(opts.path));
38221
- if (!existsSync27(path)) {
38448
+ const path = resolve11(String(opts.path));
38449
+ if (!existsSync29(path)) {
38222
38450
  console.error(`Workspace path does not exist: ${path}`);
38223
38451
  process.exit(1);
38224
38452
  }
@@ -38312,7 +38540,7 @@ workspaceCommand.command("publish").description("Publish your local workspace sa
38312
38540
  );
38313
38541
  process.exit(1);
38314
38542
  }
38315
- if (!existsSync27(workspaceDir)) {
38543
+ if (!existsSync29(workspaceDir)) {
38316
38544
  console.error(
38317
38545
  `\x1B[31m\u2717 Workspace directory does not exist: ${workspaceDir}\x1B[0m`
38318
38546
  );
@@ -38326,15 +38554,15 @@ Active workspace: \x1B[36m${workspaceDir}\x1B[0m`);
38326
38554
  );
38327
38555
  process.exit(1);
38328
38556
  }
38329
- const gitignorePath = join28(workspaceDir, ".gitignore");
38330
- if (!existsSync27(gitignorePath)) {
38331
- writeFileSync15(
38557
+ const gitignorePath = join29(workspaceDir, ".gitignore");
38558
+ if (!existsSync29(gitignorePath)) {
38559
+ writeFileSync16(
38332
38560
  gitignorePath,
38333
38561
  "node_modules/\n.agent/\n.agents/\n.claude/\n.gemini/\n.goose/\n*.log\n",
38334
38562
  "utf8"
38335
38563
  );
38336
38564
  }
38337
- const hasGitRepo = existsSync27(join28(workspaceDir, ".git"));
38565
+ const hasGitRepo = existsSync29(join29(workspaceDir, ".git"));
38338
38566
  if (!hasGitRepo) {
38339
38567
  console.log("Initializing local Git repository...");
38340
38568
  runGit2(workspaceDir, ["init", "-b", "main"]);
@@ -38366,7 +38594,7 @@ Active workspace: \x1B[36m${workspaceDir}\x1B[0m`);
38366
38594
  if (proceedGh) {
38367
38595
  try {
38368
38596
  console.log(`Creating GitHub repository ${repoName}...`);
38369
- execFileSync5("gh", ghRepoCreateArgs(repoName, repoVisibility), {
38597
+ execFileSync6("gh", ghRepoCreateArgs(repoName, repoVisibility), {
38370
38598
  cwd: workspaceDir,
38371
38599
  stdio: "inherit"
38372
38600
  });
@@ -38421,7 +38649,7 @@ Active workspace: \x1B[36m${workspaceDir}\x1B[0m`);
38421
38649
  }
38422
38650
  });
38423
38651
  workspaceCommand.command("data-dir").description("Print the ZAM data directory (database, credentials, config)").option("--json", "Output as JSON").action((opts) => {
38424
- const dir = join28(homedir16(), ".zam");
38652
+ const dir = join29(homedir17(), ".zam");
38425
38653
  console.log(opts.json ? JSON.stringify({ dataDir: dir }) : dir);
38426
38654
  });
38427
38655
  workspaceCommand.command("backup").description("Back up the local ZAM database into your workspace").option(
@@ -38464,7 +38692,7 @@ workspaceCommand.command("backup").description("Back up the local ZAM database i
38464
38692
  // src/cli/app.ts
38465
38693
  var __dirname = dirname14(fileURLToPath8(import.meta.url));
38466
38694
  var pkg = JSON.parse(
38467
- readFileSync20(join29(__dirname, "..", "..", "package.json"), "utf-8")
38695
+ readFileSync21(join30(__dirname, "..", "..", "package.json"), "utf-8")
38468
38696
  );
38469
38697
  var program = new Command27();
38470
38698
  program.name("zam").description(