zam-core 0.5.2 → 0.5.3

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/dist/cli/index.js CHANGED
@@ -2,7 +2,7 @@
2
2
 
3
3
  // src/cli/index.ts
4
4
  import { readFileSync as readFileSync15 } from "fs";
5
- import { dirname as dirname10, join as join24 } from "path";
5
+ import { dirname as dirname10, join as join25 } from "path";
6
6
  import { fileURLToPath as fileURLToPath5 } from "url";
7
7
  import { Command as Command25 } from "commander";
8
8
 
@@ -4478,25 +4478,41 @@ function saveMachineAiConfig(ai, path = defaultConfigPath()) {
4478
4478
  function getConfiguredWorkspaces(path = defaultConfigPath()) {
4479
4479
  return loadInstallConfig(path).workspaces ?? [];
4480
4480
  }
4481
- function saveConfiguredWorkspaces(workspaces, path = defaultConfigPath()) {
4481
+ function setActiveWorkspaceId(id, path = defaultConfigPath()) {
4482
4482
  const config = loadInstallConfig(path);
4483
- config.workspaces = workspaces;
4483
+ if (id) {
4484
+ config.activeWorkspaceId = id;
4485
+ } else {
4486
+ delete config.activeWorkspaceId;
4487
+ }
4484
4488
  saveInstallConfig(config, path);
4485
4489
  }
4490
+ function getActiveWorkspace(path = defaultConfigPath()) {
4491
+ const config = loadInstallConfig(path);
4492
+ const id = config.activeWorkspaceId;
4493
+ return id ? config.workspaces?.find((workspace) => workspace.id === id) : void 0;
4494
+ }
4486
4495
  function upsertConfiguredWorkspace(workspace, path = defaultConfigPath()) {
4487
- const current = getConfiguredWorkspaces(path);
4496
+ const config = loadInstallConfig(path);
4497
+ const current = config.workspaces ?? [];
4488
4498
  const next = [
4489
4499
  ...current.filter((candidate) => candidate.id !== workspace.id),
4490
4500
  workspace
4491
4501
  ];
4492
- saveConfiguredWorkspaces(next, path);
4502
+ config.workspaces = next;
4503
+ saveInstallConfig(config, path);
4493
4504
  return next;
4494
4505
  }
4495
4506
  function removeConfiguredWorkspace(id, path = defaultConfigPath()) {
4496
- const next = getConfiguredWorkspaces(path).filter(
4507
+ const config = loadInstallConfig(path);
4508
+ const next = (config.workspaces ?? []).filter(
4497
4509
  (workspace) => workspace.id !== id
4498
4510
  );
4499
- saveConfiguredWorkspaces(next, path);
4511
+ config.workspaces = next;
4512
+ if (config.activeWorkspaceId === id) {
4513
+ config.activeWorkspaceId = next[0]?.id;
4514
+ }
4515
+ saveInstallConfig(config, path);
4500
4516
  return next;
4501
4517
  }
4502
4518
  function detectSyncProvider(dir) {
@@ -4828,7 +4844,7 @@ function getSystemProfile() {
4828
4844
  import { existsSync as existsSync10 } from "fs";
4829
4845
  import { resolve as resolve2 } from "path";
4830
4846
  async function getRepoPaths(db) {
4831
- const personalSetting = await getSetting(db, "repo.personal") || await getSetting(db, "personal.workspace_dir");
4847
+ const personalSetting = await getSetting(db, "repo.personal") || getActiveWorkspace()?.path;
4832
4848
  const teamSetting = await getSetting(db, "repo.team");
4833
4849
  const orgSetting = await getSetting(db, "repo.org");
4834
4850
  return {
@@ -5372,18 +5388,12 @@ var openCmd = new Command("open").description("Open an agent harness in the work
5372
5388
  var agentCommand = new Command("agent").description("Provision and inspect the agent that drives ZAM sessions").addCommand(installCmd).addCommand(statusCmd).addCommand(openCmd).addCommand(listCmd).action(printStatus);
5373
5389
 
5374
5390
  // src/cli/commands/bridge.ts
5375
- import { execFileSync as execFileSync4 } from "child_process";
5391
+ import { execFileSync as execFileSync3 } from "child_process";
5376
5392
  import { randomBytes as randomBytes2 } from "crypto";
5377
- import {
5378
- existsSync as existsSync17,
5379
- mkdirSync as mkdirSync10,
5380
- readdirSync as readdirSync3,
5381
- readFileSync as readFileSync11,
5382
- rmSync as rmSync3
5383
- } from "fs";
5393
+ import { existsSync as existsSync17, readdirSync as readdirSync2, readFileSync as readFileSync11, rmSync as rmSync3 } from "fs";
5384
5394
  import { homedir as homedir10, tmpdir as tmpdir3 } from "os";
5385
- import { basename as basename5, join as join17, resolve as resolve5 } from "path";
5386
- import { Command as Command5 } from "commander";
5395
+ import { join as join18, resolve as resolve4 } from "path";
5396
+ import { Command as Command2 } from "commander";
5387
5397
 
5388
5398
  // src/cli/llm/client.ts
5389
5399
  import { spawn as spawn2 } from "child_process";
@@ -5419,19 +5429,25 @@ async function getLlmConfig(db) {
5419
5429
  function getCloudModelRecommendation(url) {
5420
5430
  const lowercase = url.toLowerCase();
5421
5431
  if (lowercase.includes("openrouter.ai")) {
5422
- return "openrouter/free";
5432
+ return { model: "openrouter/free", flavor: "chat-completions" };
5423
5433
  }
5424
5434
  if (lowercase.includes("openai.com") || lowercase.includes("api.openai")) {
5425
- return "gpt-5-mini";
5435
+ return { model: "gpt-5-mini", flavor: "chat-completions" };
5426
5436
  }
5427
5437
  if (lowercase.includes("googleapis.com") || lowercase.includes("google")) {
5428
- return "gemini-3.5-flash";
5438
+ return { model: "gemini-3.5-flash", flavor: "chat-completions" };
5429
5439
  }
5430
5440
  if (lowercase.includes("deepseek.com")) {
5431
- return "deepseek-v4-flash";
5441
+ return { model: "deepseek-v4-flash", flavor: "chat-completions" };
5432
5442
  }
5433
5443
  if (lowercase.includes("mimo")) {
5434
- return "mimo-v2.5";
5444
+ return { model: "mimo-v2.5", flavor: "chat-completions" };
5445
+ }
5446
+ if (lowercase.includes("anthropic.com")) {
5447
+ return {
5448
+ model: "claude-haiku-4-5-20251001",
5449
+ flavor: "anthropic-messages"
5450
+ };
5435
5451
  }
5436
5452
  return null;
5437
5453
  }
@@ -5525,14 +5541,15 @@ async function getLegacyRoleConfig(db, role, enabled) {
5525
5541
  const maxFramesStr = await getSetting(db, "llm.vision.max_frames");
5526
5542
  const parsed = maxFramesStr ? parseInt(maxFramesStr, 10) : 100;
5527
5543
  const url = await getSetting(db, "llm.vision.url") || base.url;
5544
+ const recommendation = getCloudModelRecommendation(url);
5528
5545
  let model = await getSetting(db, "llm.vision.model");
5529
- if (!model) model = getCloudModelRecommendation(url) || base.model;
5546
+ if (!model) model = recommendation?.model || base.model;
5530
5547
  return {
5531
5548
  enabled,
5532
5549
  url,
5533
5550
  model,
5534
5551
  apiKey: await getSetting(db, "llm.vision.api_key") || base.apiKey,
5535
- apiFlavor: inferApiFlavor(url),
5552
+ apiFlavor: recommendation?.flavor || inferApiFlavor(url),
5536
5553
  locale: base.locale,
5537
5554
  source: "legacy",
5538
5555
  local: isLocalEndpoint(url),
@@ -5860,7 +5877,7 @@ async function prepareRecallChain(db, opts) {
5860
5877
  } else {
5861
5878
  spawnLocalRunner(endpoint.url, endpoint.model, endpoint.runner);
5862
5879
  while (Date.now() < deadline) {
5863
- await new Promise((resolve9) => setTimeout(resolve9, 1e3));
5880
+ await new Promise((resolve10) => setTimeout(resolve10, 1e3));
5864
5881
  if (await isLlmOnline(endpoint.url)) {
5865
5882
  online = true;
5866
5883
  break;
@@ -5934,7 +5951,7 @@ async function checkVisionReadiness(db) {
5934
5951
  let warning;
5935
5952
  if (cfg.enabled && online && modelAvailable && !visionModelExplicit) {
5936
5953
  const cloudRec = getCloudModelRecommendation(active.url);
5937
- if (cloudRec && active.model === cloudRec) {
5954
+ if (cloudRec && active.model === cloudRec.model) {
5938
5955
  } else {
5939
5956
  warning = `No explicit vision model configured (llm.vision.model). Falling back to base model "${active.model}", which may not support image input. Set a multimodal model: zam settings set llm.vision.model <model>`;
5940
5957
  }
@@ -6153,7 +6170,7 @@ async function startLocalRunner(url, model, locale, hint) {
6153
6170
  let attempts = 0;
6154
6171
  const dotsPerLine = 30;
6155
6172
  while (true) {
6156
- await new Promise((resolve9) => setTimeout(resolve9, 500));
6173
+ await new Promise((resolve10) => setTimeout(resolve10, 500));
6157
6174
  if (await isLlmOnline(url)) {
6158
6175
  if (attempts > 0) process.stdout.write("\n");
6159
6176
  return true;
@@ -6242,8 +6259,8 @@ async function fetchWithInteractiveTimeout(url, options = {}) {
6242
6259
  const dotsPerLine = 30;
6243
6260
  while (true) {
6244
6261
  let timeoutId;
6245
- const timeoutPromise = new Promise((resolve9) => {
6246
- timeoutId = setTimeout(() => resolve9("timeout"), timeoutMs);
6262
+ const timeoutPromise = new Promise((resolve10) => {
6263
+ timeoutId = setTimeout(() => resolve10("timeout"), timeoutMs);
6247
6264
  });
6248
6265
  const dotsInterval = setInterval(() => {
6249
6266
  process.stdout.write(".");
@@ -6360,7 +6377,7 @@ async function observeUiSnapshotViaLLM(db, input8) {
6360
6377
  const imageUrls = [];
6361
6378
  const isVideo = /\.(mp4|mov|m4v|avi|mkv|webm)$/i.test(input8.imagePath);
6362
6379
  if (isVideo) {
6363
- const { mkdirSync: mkdirSync14, readdirSync: readdirSync4, rmSync: rmSync4 } = await import("fs");
6380
+ const { mkdirSync: mkdirSync14, readdirSync: readdirSync3, rmSync: rmSync4 } = await import("fs");
6364
6381
  const { execSync: execSync6 } = await import("child_process");
6365
6382
  const tempDir = join14(
6366
6383
  tmpdir2(),
@@ -6372,13 +6389,13 @@ async function observeUiSnapshotViaLLM(db, input8) {
6372
6389
  `ffmpeg -i "${input8.imagePath}" -vf "fps=1/3,scale=1280:-1" -vsync vfr "${tempDir}/frame_%03d.png"`,
6373
6390
  { stdio: "ignore" }
6374
6391
  );
6375
- let files = readdirSync4(tempDir).filter((f) => f.endsWith(".png")).sort();
6392
+ let files = readdirSync3(tempDir).filter((f) => f.endsWith(".png")).sort();
6376
6393
  if (files.length === 0) {
6377
6394
  execSync6(
6378
6395
  `ffmpeg -i "${input8.imagePath}" -vframes 1 "${tempDir}/frame_001.png"`,
6379
6396
  { stdio: "ignore" }
6380
6397
  );
6381
- files = readdirSync4(tempDir).filter((f) => f.endsWith(".png")).sort();
6398
+ files = readdirSync3(tempDir).filter((f) => f.endsWith(".png")).sort();
6382
6399
  }
6383
6400
  const maxFrames = cfg.maxFrames ?? 100;
6384
6401
  let sampledFiles = files;
@@ -6734,10 +6751,6 @@ function isRecord2(value) {
6734
6751
  return typeof value === "object" && value !== null && !Array.isArray(value);
6735
6752
  }
6736
6753
 
6737
- // src/cli/commands/provider.ts
6738
- import { password } from "@inquirer/prompts";
6739
- import { Command as Command2 } from "commander";
6740
-
6741
6754
  // src/cli/commands/shared/db.ts
6742
6755
  function defaultErrorHandler(message) {
6743
6756
  console.error("Error:", message);
@@ -6758,7 +6771,7 @@ function jsonOut(data) {
6758
6771
  console.log(JSON.stringify(data, null, 2));
6759
6772
  }
6760
6773
 
6761
- // src/cli/commands/provider.ts
6774
+ // src/cli/providers/config.ts
6762
6775
  var VALID_API_FLAVORS = [
6763
6776
  "chat-completions",
6764
6777
  "anthropic-messages"
@@ -6871,1034 +6884,528 @@ async function withProviderScope(machine, action) {
6871
6884
  }
6872
6885
  await withDb(action);
6873
6886
  }
6874
- var providerCommand = new Command2("provider").description(
6875
- "Configure role-based AI providers (url/model/flavor/key, per role)"
6876
- );
6877
- providerCommand.command("list").description("Show configured providers, role bindings, and key status").option("--json", "Output as JSON").option("--machine", "Read machine-local providers from ~/.zam/config.json").action(async (opts) => {
6878
- const machine = Boolean(opts.machine);
6879
- await withProviderScope(machine, async (db) => {
6880
- const providers = await readScopedProviders(db, machine);
6881
- const roles = await readScopedRoles(db, machine);
6882
- const rows = buildProviderListing(
6883
- providers,
6884
- (ref) => getProviderApiKey(ref) !== null
6885
- );
6886
- const orphans = findOrphanKeyRefs(listProviderApiKeyRefs(), providers);
6887
- if (opts.json) {
6888
- console.log(
6889
- JSON.stringify(
6890
- {
6891
- scope: machine ? "machine" : "shared",
6892
- providers: rows,
6893
- roles,
6894
- orphans
6895
- },
6896
- null,
6897
- 2
6898
- )
6887
+
6888
+ // src/cli/provisioning/index.ts
6889
+ import {
6890
+ existsSync as existsSync15,
6891
+ lstatSync,
6892
+ mkdirSync as mkdirSync8,
6893
+ readFileSync as readFileSync10,
6894
+ realpathSync,
6895
+ rmSync as rmSync2,
6896
+ symlinkSync,
6897
+ writeFileSync as writeFileSync7
6898
+ } from "fs";
6899
+ import { basename as basename4, dirname as dirname5, join as join15 } from "path";
6900
+ import { fileURLToPath as fileURLToPath2 } from "url";
6901
+ var packageRoot = [
6902
+ fileURLToPath2(new URL("../..", import.meta.url)),
6903
+ fileURLToPath2(new URL("../../..", import.meta.url))
6904
+ ].find((candidate) => existsSync15(join15(candidate, "package.json"))) ?? fileURLToPath2(new URL("../..", import.meta.url));
6905
+ var ALL_SETUP_AGENTS = ["claude", "copilot", "codex", "agent"];
6906
+ var SKILL_PAIRS = [
6907
+ {
6908
+ from: join15(packageRoot, ".claude", "skills", "zam", "SKILL.md"),
6909
+ to: join15(".claude", "skills", "zam", "SKILL.md"),
6910
+ agents: ["claude", "copilot"]
6911
+ },
6912
+ {
6913
+ from: join15(packageRoot, ".agent", "skills", "zam", "SKILL.md"),
6914
+ to: join15(".agent", "skills", "zam", "SKILL.md"),
6915
+ agents: ["agent"]
6916
+ },
6917
+ {
6918
+ from: join15(packageRoot, ".agents", "skills", "zam", "SKILL.md"),
6919
+ to: join15(".agents", "skills", "zam", "SKILL.md"),
6920
+ agents: ["codex"]
6921
+ }
6922
+ ];
6923
+ function parseSetupAgents(value) {
6924
+ if (!value || value.trim().toLowerCase() === "all") {
6925
+ return new Set(ALL_SETUP_AGENTS);
6926
+ }
6927
+ const aliases = {
6928
+ claude: ["claude"],
6929
+ copilot: ["copilot"],
6930
+ codex: ["codex"],
6931
+ agent: ["agent"],
6932
+ opencode: ["agent"]
6933
+ };
6934
+ const selected = /* @__PURE__ */ new Set();
6935
+ for (const raw of value.split(",")) {
6936
+ const key = raw.trim().toLowerCase();
6937
+ const mapped = aliases[key];
6938
+ if (!mapped) {
6939
+ throw new Error(
6940
+ `Unknown agent "${raw}". Use: all, claude, copilot, codex, agent.`
6899
6941
  );
6900
- return;
6901
6942
  }
6902
- console.log(
6903
- `Providers (${opts.machine ? "~/.zam/config.json ai.providers" : "llm.providers"}):
6904
- `
6905
- );
6906
- if (rows.length === 0) {
6907
- console.log(
6908
- " (none) \u2014 add one: zam provider add <name> --url <url> --model <model>"
6909
- );
6910
- } else {
6911
- for (const row of rows) {
6912
- const key = row.keyState === "set" ? "\x1B[32mkey \u2713\x1B[0m" : row.keyState === "missing" ? `\x1B[31mkey \u2717 (set: zam provider set-key ${row.apiKeyRef})\x1B[0m` : "\x1B[90mno key\x1B[0m";
6913
- console.log(` \x1B[36m${row.name.padEnd(12)}\x1B[0m ${key}`);
6914
- console.log(` url: ${row.url ?? "(inherits llm.url)"}`);
6915
- console.log(` model: ${row.model ?? "(inherits llm.model)"}`);
6916
- console.log(` flavor: ${row.apiFlavor}`);
6917
- if (row.label) console.log(` label: ${row.label}`);
6918
- if (row.local !== void 0) {
6919
- console.log(` local: ${row.local ? "yes" : "no"}`);
6920
- }
6921
- if (row.runner) console.log(` runner: ${row.runner}`);
6922
- if (row.apiKeyRef) console.log(` key-ref: ${row.apiKeyRef}`);
6943
+ for (const item of mapped) selected.add(item);
6944
+ }
6945
+ return selected;
6946
+ }
6947
+ function pathExists(path) {
6948
+ try {
6949
+ lstatSync(path);
6950
+ return true;
6951
+ } catch {
6952
+ return false;
6953
+ }
6954
+ }
6955
+ function isSymbolicLink(path) {
6956
+ try {
6957
+ return lstatSync(path).isSymbolicLink();
6958
+ } catch {
6959
+ return false;
6960
+ }
6961
+ }
6962
+ function comparableRealPath(path) {
6963
+ const real = realpathSync(path);
6964
+ return process.platform === "win32" ? real.toLowerCase() : real;
6965
+ }
6966
+ function pathsResolveToSameDirectory(sourceDir, destinationDir) {
6967
+ try {
6968
+ return comparableRealPath(destinationDir) === comparableRealPath(sourceDir);
6969
+ } catch {
6970
+ return false;
6971
+ }
6972
+ }
6973
+ function linkPointsTo(sourceDir, destinationDir) {
6974
+ return isSymbolicLink(destinationDir) && pathsResolveToSameDirectory(sourceDir, destinationDir);
6975
+ }
6976
+ function isZamSkillCopy(destinationDir) {
6977
+ try {
6978
+ if (!lstatSync(destinationDir).isDirectory()) return false;
6979
+ const skillFile = join15(destinationDir, "SKILL.md");
6980
+ if (!existsSync15(skillFile)) return false;
6981
+ return /^name:\s*zam\s*$/m.test(readFileSync10(skillFile, "utf8"));
6982
+ } catch {
6983
+ return false;
6984
+ }
6985
+ }
6986
+ function classifySkillDestination(sourceDir, destinationDir) {
6987
+ if (!existsSync15(sourceDir)) return "source-missing";
6988
+ if (!isSymbolicLink(destinationDir) && pathsResolveToSameDirectory(sourceDir, destinationDir)) {
6989
+ return "source-directory";
6990
+ }
6991
+ if (linkPointsTo(sourceDir, destinationDir)) return "linked";
6992
+ if (!pathExists(destinationDir)) return "missing";
6993
+ if (isSymbolicLink(destinationDir)) return "broken";
6994
+ if (isZamSkillCopy(destinationDir)) return "stale-copy";
6995
+ return "unmanaged";
6996
+ }
6997
+ function wireSkills(cwd = process.cwd(), agents = parseSetupAgents(), opts = {}) {
6998
+ const results = [];
6999
+ const log = (message) => {
7000
+ if (!opts.quiet) console.log(message);
7001
+ };
7002
+ for (const { from, to, agents: pairAgents } of SKILL_PAIRS) {
7003
+ if (!pairAgents.some((agent) => agents.has(agent))) continue;
7004
+ const sourceDir = dirname5(from);
7005
+ const destinationDir = dirname5(join15(cwd, to));
7006
+ const label = dirname5(to);
7007
+ const state = classifySkillDestination(sourceDir, destinationDir);
7008
+ if (state === "source-missing") {
7009
+ if (!opts.quiet) {
7010
+ console.warn(` warn source not found, skipping: ${sourceDir}`);
6923
7011
  }
7012
+ results.push({
7013
+ source: sourceDir,
7014
+ destination: destinationDir,
7015
+ action: "skipped",
7016
+ reason: "source-not-found"
7017
+ });
7018
+ continue;
6924
7019
  }
6925
- console.log("\nRoles (llm.roles):\n");
6926
- for (const role of VALID_ROLES) {
6927
- const binding = roles[role];
6928
- if (!binding?.primary) {
6929
- console.log(` ${role.padEnd(7)} \x1B[90m(unset)\x1B[0m`);
6930
- } else {
6931
- const fb = binding.fallback ? ` \u2192 fallback: ${binding.fallback}` : "";
6932
- console.log(` ${role.padEnd(7)} primary: ${binding.primary}${fb}`);
6933
- }
7020
+ if (state === "source-directory") {
7021
+ log(` skip ${label} (package source)`);
7022
+ results.push({
7023
+ source: sourceDir,
7024
+ destination: destinationDir,
7025
+ action: "skipped",
7026
+ reason: "source-directory"
7027
+ });
7028
+ continue;
6934
7029
  }
6935
- if (orphans.length > 0) {
6936
- console.log(
6937
- `
6938
- \x1B[33mOrphan keys (stored but unreferenced):\x1B[0m ${orphans.join(", ")}`
6939
- );
7030
+ if (state === "linked") {
7031
+ log(` skip ${label} (already linked)`);
7032
+ results.push({
7033
+ source: sourceDir,
7034
+ destination: destinationDir,
7035
+ action: "skipped",
7036
+ reason: "already-linked"
7037
+ });
7038
+ continue;
6940
7039
  }
6941
- });
6942
- });
6943
- providerCommand.command("add <name>").description("Add or update a provider record in llm.providers").option("--label <label>", "Human-readable provider label").option("--url <url>", "Endpoint base URL (e.g. https://api.deepseek.com/v1)").option("--model <model>", "Model id (e.g. deepseek-v4-flash)").option("--local", "Mark this provider as a local/on-device endpoint").option(
6944
- "--runner <runner>",
6945
- "Local runner hint (flm, foundry-local, ollama, ...)"
6946
- ).option(
6947
- "--flavor <flavor>",
6948
- `Wire protocol: ${VALID_API_FLAVORS.join(" | ")} (default: inferred from URL)`
6949
- ).option(
6950
- "--key-ref <ref>",
6951
- "Credential reference for the API key (default: <name> when --key is given)"
6952
- ).option(
6953
- "--key <value>",
6954
- "Store this API key now (prefer `set-key` to keep it out of shell history)"
6955
- ).option("--machine", "Store this provider in ~/.zam/config.json").action(async (name, opts) => {
6956
- let apiFlavor;
6957
- if (opts.flavor) {
6958
- if (!VALID_API_FLAVORS.includes(opts.flavor)) {
6959
- console.error(
6960
- `Invalid --flavor: ${opts.flavor}. Use ${VALID_API_FLAVORS.join(" or ")}.`
7040
+ const destinationExists = state !== "missing";
7041
+ const replaceExisting = destinationExists && (Boolean(opts.force) || state === "broken" || state === "stale-copy");
7042
+ if (destinationExists && !replaceExisting) {
7043
+ if (!opts.quiet) {
7044
+ console.warn(
7045
+ ` warn ${label} already exists and is not managed by ZAM; use --force to replace it`
7046
+ );
7047
+ }
7048
+ results.push({
7049
+ source: sourceDir,
7050
+ destination: destinationDir,
7051
+ action: "skipped",
7052
+ reason: "unmanaged-destination"
7053
+ });
7054
+ continue;
7055
+ }
7056
+ const action = destinationExists ? "relinked" : "linked";
7057
+ if (opts.dryRun) {
7058
+ log(` would ${action === "linked" ? "link" : "relink"} ${label}`);
7059
+ } else {
7060
+ if (destinationExists) {
7061
+ rmSync2(destinationDir, { recursive: true, force: true });
7062
+ }
7063
+ mkdirSync8(dirname5(destinationDir), { recursive: true });
7064
+ symlinkSync(
7065
+ sourceDir,
7066
+ destinationDir,
7067
+ process.platform === "win32" ? "junction" : "dir"
6961
7068
  );
6962
- process.exit(1);
7069
+ log(` ${action === "linked" ? "link" : "relink"} ${label}`);
6963
7070
  }
6964
- apiFlavor = opts.flavor;
7071
+ results.push({ source: sourceDir, destination: destinationDir, action });
6965
7072
  }
6966
- const apiKeyRef = opts.keyRef ?? (opts.key ? name : void 0);
6967
- const machine = Boolean(opts.machine);
6968
- await withProviderScope(machine, async (db) => {
6969
- const providers = await readScopedProviders(db, machine);
6970
- const next = upsertProviderRecord(providers, name, {
6971
- label: opts.label,
6972
- url: opts.url,
6973
- model: opts.model,
6974
- apiFlavor,
6975
- apiKeyRef,
6976
- local: opts.local ? true : void 0,
6977
- runner: opts.runner
6978
- });
6979
- await writeScopedProviders(db, machine, next);
6980
- if (opts.key && apiKeyRef) setProviderApiKey(apiKeyRef, opts.key);
6981
- const rec = next[name];
6982
- console.log(
6983
- `Provider "${name}" saved (${machine ? "machine-local" : "shared"}):`
6984
- );
6985
- console.log(` url: ${rec.url ?? "(inherits llm.url)"}`);
6986
- console.log(` model: ${rec.model ?? "(inherits llm.model)"}`);
6987
- if (rec.label) console.log(` label: ${rec.label}`);
6988
- if (rec.local !== void 0) {
6989
- console.log(` local: ${rec.local ? "yes" : "no"}`);
6990
- }
6991
- if (rec.runner) console.log(` runner: ${rec.runner}`);
6992
- console.log(
6993
- ` flavor: ${rec.apiFlavor ?? `${rec.url ? inferApiFlavor(rec.url) : "chat-completions"} (inferred)`}`
6994
- );
6995
- console.log(` key-ref: ${rec.apiKeyRef ?? "(none \u2014 uses default key)"}`);
6996
- if (opts.key) {
6997
- console.log(` key: stored (${maskSecret(opts.key)})`);
6998
- } else if (rec.apiKeyRef && getProviderApiKey(rec.apiKeyRef) === null) {
6999
- console.log(
7000
- `
7001
- \u26A0 No key stored for "${rec.apiKeyRef}". Run: zam provider set-key ${rec.apiKeyRef}`
7002
- );
7003
- }
7004
- if (!rec.url) {
7005
- console.log(
7006
- "\n \u26A0 No --url set; this provider inherits the base llm.url."
7007
- );
7008
- }
7009
- console.log(
7010
- `
7011
- Bind it to a role: zam provider use recall --primary ${name}`
7012
- );
7013
- });
7014
- });
7015
- providerCommand.command("remove <name>").description("Remove a provider record from llm.providers").option("--machine", "Remove from ~/.zam/config.json instead of the DB").action(async (name, opts) => {
7016
- const machine = Boolean(opts.machine);
7017
- await withProviderScope(machine, async (db) => {
7018
- const providers = await readScopedProviders(db, machine);
7019
- const { providers: next, removed } = removeProviderRecord(
7020
- providers,
7021
- name
7022
- );
7023
- if (!removed) {
7024
- console.log(`No such provider: ${name}`);
7025
- return;
7026
- }
7027
- await writeScopedProviders(db, machine, next);
7028
- console.log(
7029
- `Removed provider "${name}" (${machine ? "machine-local" : "shared"}).`
7030
- );
7031
- const referencing = rolesReferencing(
7032
- await readScopedRoles(db, machine),
7033
- name
7034
- );
7035
- if (referencing.length > 0) {
7036
- console.log(
7037
- ` \u26A0 Still referenced by role(s): ${referencing.join(", ")} \u2014 rebind with: zam provider use <role> --primary <name>`
7038
- );
7039
- }
7040
- });
7041
- });
7042
- providerCommand.command("use <role>").description(`Bind providers to a role (${VALID_ROLES.join(" | ")})`).option("--primary <name>", "Primary provider").option("--fallback <name>", "Fallback provider (optional)").option("--machine", "Bind the role in ~/.zam/config.json").action(async (role, opts) => {
7043
- if (!VALID_ROLES.includes(role)) {
7044
- console.error(`Invalid role: ${role}. Use ${VALID_ROLES.join(", ")}.`);
7045
- process.exit(1);
7046
- }
7047
- if (!opts.primary) {
7048
- console.error("--primary is required.");
7049
- process.exit(1);
7050
- }
7051
- const machine = Boolean(opts.machine);
7052
- await withProviderScope(machine, async (db) => {
7053
- const providers = await readScopedProviders(db, machine);
7054
- await writeScopedRoles(
7055
- db,
7056
- machine,
7057
- bindRoleProviders(
7058
- await readScopedRoles(db, machine),
7059
- role,
7060
- opts.primary,
7061
- opts.fallback
7062
- )
7063
- );
7064
- const fb = opts.fallback ? `, fallback: ${opts.fallback}` : "";
7065
- console.log(
7066
- `Role "${role}" \u2192 primary: ${opts.primary}${fb} (${machine ? "machine-local" : "shared"})`
7067
- );
7068
- for (const [label, ref] of [
7069
- ["primary", opts.primary],
7070
- ["fallback", opts.fallback]
7071
- ]) {
7072
- if (ref && !(ref in providers)) {
7073
- console.log(
7074
- ` \u26A0 ${label} "${ref}" is not a defined provider yet. Add it: zam provider add ${ref} --url <url> --model <model>`
7075
- );
7076
- }
7077
- }
7078
- });
7079
- });
7080
- providerCommand.command("set-key <ref>").description(
7081
- "Store an API key for a provider reference (in credentials.json)"
7082
- ).option(
7083
- "--key <value>",
7084
- "The API key (omit to enter it interactively, hidden)"
7085
- ).action(async (ref, opts) => {
7086
- try {
7087
- const key = opts.key ?? await password({ message: `API key for "${ref}":` });
7088
- if (!key || key.trim().length === 0) {
7089
- console.error("No key provided.");
7090
- process.exit(1);
7091
- }
7092
- setProviderApiKey(ref, key.trim());
7093
- console.log(`Stored API key for "${ref}" (${maskSecret(key.trim())}).`);
7094
- } catch (err) {
7095
- if (err.name === "ExitPromptError") {
7096
- console.log("\nCancelled.");
7097
- process.exit(0);
7098
- }
7099
- console.error("Error:", err.message);
7100
- process.exit(1);
7101
- }
7102
- });
7103
- providerCommand.command("clear-key <ref>").description("Remove a stored API key").action((ref) => {
7104
- clearProviderApiKey(ref);
7105
- console.log(`Cleared API key for "${ref}".`);
7106
- });
7107
-
7108
- // src/cli/commands/resolve-user.ts
7109
- async function ensureDefaultUser(db, preferredUserId) {
7110
- const stored = await getSetting(db, "user.id");
7111
- if (stored) return stored;
7112
- const userId = preferredUserId?.trim() || process.env.ZAM_USER?.trim() || process.env.USERNAME?.trim() || process.env.USER?.trim() || "default";
7113
- await setSetting(db, "user.id", userId);
7114
- return userId;
7115
- }
7116
- async function resolveUser(opts, db, resolveOpts) {
7117
- if (opts.user) return opts.user;
7118
- const stored = await getSetting(db, "user.id");
7119
- if (stored) return stored;
7120
- const message = "No user specified. Set a default with: zam whoami --set <id>";
7121
- if (resolveOpts?.json) {
7122
- console.log(JSON.stringify({ error: message }, null, 2));
7123
- } else {
7124
- console.error(message);
7125
- }
7126
- process.exit(1);
7127
- }
7128
-
7129
- // src/cli/commands/setup.ts
7130
- import {
7131
- existsSync as existsSync15,
7132
- lstatSync,
7133
- mkdirSync as mkdirSync8,
7134
- readdirSync as readdirSync2,
7135
- readFileSync as readFileSync10,
7136
- realpathSync,
7137
- rmSync as rmSync2,
7138
- symlinkSync,
7139
- writeFileSync as writeFileSync7
7140
- } from "fs";
7141
- import { basename as basename4, dirname as dirname5, join as join15, resolve as resolve3 } from "path";
7142
- import { fileURLToPath as fileURLToPath2 } from "url";
7143
- import { Command as Command3 } from "commander";
7144
- var packageRoot = [
7145
- fileURLToPath2(new URL("../..", import.meta.url)),
7146
- fileURLToPath2(new URL("../../..", import.meta.url))
7147
- ].find((candidate) => existsSync15(join15(candidate, "package.json"))) ?? fileURLToPath2(new URL("../..", import.meta.url));
7148
- var ALL_SETUP_AGENTS = ["claude", "copilot", "codex", "agent"];
7149
- var SKILL_PAIRS = [
7150
- {
7151
- from: join15(packageRoot, ".claude", "skills", "zam", "SKILL.md"),
7152
- to: join15(".claude", "skills", "zam", "SKILL.md"),
7153
- agents: ["claude", "copilot"]
7154
- },
7155
- {
7156
- from: join15(packageRoot, ".agent", "skills", "zam", "SKILL.md"),
7157
- to: join15(".agent", "skills", "zam", "SKILL.md"),
7158
- agents: ["agent"]
7159
- },
7160
- {
7161
- from: join15(packageRoot, ".agents", "skills", "zam", "SKILL.md"),
7162
- to: join15(".agents", "skills", "zam", "SKILL.md"),
7163
- agents: ["codex"]
7164
- }
7165
- ];
7166
- function parseSetupAgents(value) {
7167
- if (!value || value.trim().toLowerCase() === "all") {
7168
- return new Set(ALL_SETUP_AGENTS);
7169
- }
7170
- const aliases = {
7171
- claude: ["claude"],
7172
- copilot: ["copilot"],
7173
- codex: ["codex"],
7174
- agent: ["agent"],
7175
- opencode: ["agent"]
7176
- };
7177
- const selected = /* @__PURE__ */ new Set();
7178
- for (const raw of value.split(",")) {
7179
- const key = raw.trim().toLowerCase();
7180
- const mapped = aliases[key];
7181
- if (!mapped) {
7182
- throw new Error(
7183
- `Unknown agent "${raw}". Use: all, claude, copilot, codex, agent.`
7184
- );
7185
- }
7186
- for (const item of mapped) selected.add(item);
7187
- }
7188
- return selected;
7189
- }
7190
- function pathExists(path) {
7191
- try {
7192
- lstatSync(path);
7193
- return true;
7194
- } catch {
7195
- return false;
7196
- }
7197
- }
7198
- function comparableRealPath(path) {
7199
- const real = realpathSync(path);
7200
- return process.platform === "win32" ? real.toLowerCase() : real;
7201
- }
7202
- function pathsResolveToSameDirectory(sourceDir, destinationDir) {
7203
- try {
7204
- return comparableRealPath(destinationDir) === comparableRealPath(sourceDir);
7205
- } catch {
7206
- return false;
7207
- }
7208
- }
7209
- function linkPointsTo(sourceDir, destinationDir) {
7210
- try {
7211
- return lstatSync(destinationDir).isSymbolicLink() && pathsResolveToSameDirectory(sourceDir, destinationDir);
7212
- } catch {
7213
- return false;
7214
- }
7215
- }
7216
- function isReplaceableCopiedSkill(destinationDir) {
7217
- try {
7218
- if (!lstatSync(destinationDir).isDirectory()) return false;
7219
- const entries = readdirSync2(destinationDir);
7220
- if (entries.length !== 1 || entries[0] !== "SKILL.md") return false;
7221
- return /^name:\s*zam\s*$/m.test(
7222
- readFileSync10(join15(destinationDir, "SKILL.md"), "utf8")
7223
- );
7224
- } catch {
7225
- return false;
7226
- }
7227
- }
7228
- function wireSkills(cwd = process.cwd(), agents = parseSetupAgents(), opts = {}) {
7229
- const results = [];
7230
- const log = (message) => {
7231
- if (!opts.quiet) console.log(message);
7232
- };
7233
- for (const { from, to, agents: pairAgents } of SKILL_PAIRS) {
7234
- if (!pairAgents.some((agent) => agents.has(agent))) continue;
7235
- const sourceDir = dirname5(from);
7236
- const destinationDir = dirname5(join15(cwd, to));
7237
- if (!existsSync15(sourceDir)) {
7238
- if (!opts.quiet) {
7239
- console.warn(` warn source not found, skipping: ${sourceDir}`);
7240
- }
7241
- results.push({
7242
- source: sourceDir,
7243
- destination: destinationDir,
7244
- action: "skipped",
7245
- reason: "source-not-found"
7246
- });
7247
- continue;
7248
- }
7249
- if (pathsResolveToSameDirectory(sourceDir, destinationDir) && !lstatSync(destinationDir).isSymbolicLink()) {
7250
- log(` skip ${dirname5(to)} (package source)`);
7251
- results.push({
7252
- source: sourceDir,
7253
- destination: destinationDir,
7254
- action: "skipped",
7255
- reason: "source-directory"
7256
- });
7257
- continue;
7258
- }
7259
- if (linkPointsTo(sourceDir, destinationDir)) {
7260
- log(` skip ${dirname5(to)} (already linked)`);
7261
- results.push({
7262
- source: sourceDir,
7263
- destination: destinationDir,
7264
- action: "skipped",
7265
- reason: "already-linked"
7266
- });
7267
- continue;
7268
- }
7269
- const destinationExists = pathExists(destinationDir);
7270
- const replaceExisting = destinationExists && (Boolean(opts.force) || isReplaceableCopiedSkill(destinationDir));
7271
- if (destinationExists && !replaceExisting) {
7272
- if (!opts.quiet) {
7273
- console.warn(
7274
- ` warn ${dirname5(to)} already exists and is not managed by ZAM; use --force to replace it`
7275
- );
7276
- }
7277
- results.push({
7278
- source: sourceDir,
7279
- destination: destinationDir,
7280
- action: "skipped",
7281
- reason: "unmanaged-destination"
7282
- });
7283
- continue;
7284
- }
7285
- const action = destinationExists ? "relinked" : "linked";
7286
- if (opts.dryRun) {
7287
- log(` would ${action === "linked" ? "link" : "relink"} ${dirname5(to)}`);
7288
- } else {
7289
- if (destinationExists) {
7290
- rmSync2(destinationDir, { recursive: true, force: true });
7291
- }
7292
- mkdirSync8(dirname5(destinationDir), { recursive: true });
7293
- symlinkSync(
7294
- sourceDir,
7295
- destinationDir,
7296
- process.platform === "win32" ? "junction" : "dir"
7297
- );
7298
- log(` ${action === "linked" ? "link" : "relink"} ${dirname5(to)}`);
7299
- }
7300
- results.push({ source: sourceDir, destination: destinationDir, action });
7301
- }
7302
- return results;
7303
- }
7304
- function formatDatabaseInitTarget(target) {
7305
- switch (target.kind) {
7306
- case "local":
7307
- return `ZAM database at ${target.location} (local SQLite)`;
7308
- case "turso-remote":
7309
- return `ZAM database via Turso remote at ${target.location}`;
7310
- case "turso-native":
7311
- return `ZAM database via Turso native driver at ${target.location}`;
7312
- case "turso-replica":
7313
- return `ZAM database replica at ${target.location} syncing from ${target.syncUrl}`;
7314
- }
7315
- }
7316
- async function initDatabase(skipInit) {
7317
- if (skipInit) return;
7318
- try {
7319
- const target = getDatabaseTargetInfo();
7320
- const db = await openDatabaseWithSync({ initialize: true });
7321
- await db.close();
7322
- console.log(` init ${formatDatabaseInitTarget(target)}`);
7323
- } catch (err) {
7324
- const msg = err.message;
7325
- if (!msg.includes("already")) {
7326
- console.warn(` warn database init: ${msg}`);
7327
- } else {
7328
- console.log(` skip database already initialized`);
7329
- }
7330
- }
7331
- }
7332
- var ZAM_BLOCK_START = "<!-- ZAM:START -->";
7333
- var ZAM_BLOCK_END = "<!-- ZAM:END -->";
7334
- function upsertMarkedBlock(dest, blockBody, dryRun) {
7335
- const block = `${ZAM_BLOCK_START}
7336
- ${blockBody.trim()}
7337
- ${ZAM_BLOCK_END}`;
7338
- if (!existsSync15(dest)) {
7339
- if (!dryRun) {
7340
- mkdirSync8(dirname5(dest), { recursive: true });
7341
- writeFileSync7(dest, `${block}
7342
- `, "utf8");
7343
- }
7344
- return "write";
7345
- }
7346
- const existing = readFileSync10(dest, "utf8");
7347
- if (existing.includes(block)) return "skip";
7348
- const start = existing.indexOf(ZAM_BLOCK_START);
7349
- const end = existing.indexOf(ZAM_BLOCK_END);
7350
- const next = start >= 0 && end > start ? `${existing.slice(0, start)}${block}${existing.slice(end + ZAM_BLOCK_END.length)}` : `${existing.trimEnd()}
7351
-
7352
- ${block}
7353
- `;
7354
- if (!dryRun) writeFileSync7(dest, next, "utf8");
7355
- return start >= 0 && end > start ? "update" : "write";
7356
- }
7357
- function logInstructionAction(action, label, dryRun) {
7358
- if (action === "skip") {
7359
- console.log(` skip ${label} (ZAM block already present)`);
7360
- } else {
7361
- console.log(` ${dryRun ? "would " : ""}${action} ${label}`);
7362
- }
7363
- }
7364
- function writeClaudeMd(skipClaudeMd, cwd = process.cwd(), opts = {}) {
7365
- if (skipClaudeMd) return;
7366
- const dest = join15(cwd, "CLAUDE.md");
7367
- if (existsSync15(dest)) {
7368
- if (!opts.updateExisting) {
7369
- console.log(` skip CLAUDE.md (already present)`);
7370
- return;
7371
- }
7372
- const action = upsertMarkedBlock(
7373
- dest,
7374
- `## ZAM learning sessions
7375
-
7376
- 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.
7377
-
7378
- - Skill files live under \`.claude/skills/zam/\`.
7379
- - Fast-changing review data lives in \`~/.zam/\`, not in this repository.
7380
- - The skill directory is linked to this ZAM installation and updates with it.`,
7381
- Boolean(opts.dryRun)
7382
- );
7383
- logInstructionAction(action, "CLAUDE.md", Boolean(opts.dryRun));
7384
- return;
7385
- }
7386
- const name = basename4(cwd);
7387
- const content = `# ZAM Personal Kernel \xE2\u20AC\u201D ${name}
7388
-
7389
- This is a ZAM personal instance. ZAM builds lasting skills through spaced
7390
- repetition during real work \xE2\u20AC\u201D not separate study sessions.
7391
-
7392
- ## First time here?
7393
- Run \`/setup\` in Claude Code or Gemini CLI to complete first-time setup.
7394
-
7395
- ## Regular use
7396
- Run \`/zam\` to start a learning session on whatever you are working on.
7397
-
7398
- ## What lives here
7399
- - \`beliefs/\` \xE2\u20AC\u201D your worldview, approved by git commit
7400
- - \`goals/\` \xE2\u20AC\u201D your objectives, decomposed into tasks and learning tokens
7401
-
7402
- ## Fast-changing data
7403
- Learning tokens, cards, and review history live in local SQLite by default.
7404
- Use \`zam connector setup turso\` to store cloud credentials in
7405
- \`~/.zam/credentials.json\` and use a Turso database across machines.
7406
- `;
7407
- if (opts.dryRun) {
7408
- console.log(` would write CLAUDE.md`);
7409
- } else {
7410
- writeFileSync7(dest, content, "utf8");
7411
- console.log(` write CLAUDE.md`);
7412
- }
7413
- }
7414
- function writeAgentsMd(skipAgentsMd, cwd = process.cwd(), opts = {}) {
7415
- if (skipAgentsMd) return;
7416
- const dest = join15(cwd, "AGENTS.md");
7417
- if (existsSync15(dest)) {
7418
- if (!opts.updateExisting) {
7419
- console.log(` skip AGENTS.md (already present)`);
7420
- return;
7421
- }
7422
- const action = upsertMarkedBlock(
7423
- dest,
7424
- `## ZAM learning sessions
7425
-
7426
- 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.
7427
-
7428
- - Skill files live under \`.agents/skills/zam/\`.
7429
- - Fast-changing review data lives in \`~/.zam/\`, not in this repository.
7430
- - The skill directory is linked to this ZAM installation and updates with it.`,
7431
- Boolean(opts.dryRun)
7432
- );
7433
- logInstructionAction(action, "AGENTS.md", Boolean(opts.dryRun));
7434
- return;
7435
- }
7436
- const name = basename4(cwd);
7437
- const content = `# ZAM Personal Kernel - ${name}
7438
-
7439
- This is a ZAM personal instance. ZAM builds lasting skills through spaced
7440
- repetition during real work, not separate study sessions.
7441
-
7442
- ## First time here?
7443
- Run \`zam setup\` from the shell. When this repository includes
7444
- \`.agents/skills/setup/\`, you can instead select \`setup\` through \`/skills\`
7445
- or invoke \`$setup\`.
7446
-
7447
- ## Regular use
7448
- Select the \`zam\` skill through \`/skills\` or invoke \`$zam\` to start a
7449
- learning session on whatever you are working on.
7450
-
7451
- ## What lives here
7452
- - \`beliefs/\` - your worldview, approved by git commit
7453
- - \`goals/\` - your objectives, decomposed into tasks and learning tokens
7454
-
7455
- ## Fast-changing data
7456
- Learning tokens, cards, and review history live in local SQLite by default.
7457
- Use \`zam connector setup turso\` to store cloud credentials in
7458
- \`~/.zam/credentials.json\` and use a Turso database across machines.
7459
-
7460
- ## Codex skills
7461
- Codex discovers repository skills under \`.agents/skills/\`. Run
7462
- \`zam setup --force\` after upgrading \`zam-core\` to refresh them.
7463
- `;
7464
- if (opts.dryRun) {
7465
- console.log(` would write AGENTS.md`);
7466
- } else {
7467
- writeFileSync7(dest, content, "utf8");
7468
- console.log(` write AGENTS.md`);
7469
- }
7470
- }
7471
- function writeCopilotInstructions(cwd = process.cwd(), opts = {}) {
7472
- const dest = join15(cwd, ".github", "copilot-instructions.md");
7473
- const action = upsertMarkedBlock(
7474
- dest,
7475
- `## ZAM learning sessions
7476
-
7477
- 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.
7478
-
7479
- - Fast-changing review data lives in \`~/.zam/\`, not in this repository.
7480
- - The skill directory is linked to this ZAM installation and updates with it.`,
7481
- Boolean(opts.dryRun)
7482
- );
7483
- logInstructionAction(
7484
- action,
7485
- ".github/copilot-instructions.md",
7486
- Boolean(opts.dryRun)
7487
- );
7488
- }
7489
- var setupCommand = new Command3("setup").description(
7490
- "Link ZAM skill directories into this workspace and initialize the database"
7491
- ).option("--force", "replace an unmanaged existing ZAM skill directory", false).option("--skip-init", "skip database initialization", false).option("--skip-claude-md", "skip CLAUDE.md generation", false).option("--skip-agents-md", "skip AGENTS.md generation", false).option("--target <path>", "repository/workspace directory to set up").option(
7492
- "--agents <list>",
7493
- "comma-separated agents to wire: all, claude, copilot, codex, agent"
7494
- ).option(
7495
- "--dry-run",
7496
- "show what would be written without changing files",
7497
- false
7498
- ).action(
7499
- async (opts) => {
7500
- let agents;
7501
- try {
7502
- agents = parseSetupAgents(opts.agents);
7503
- } catch (err) {
7504
- console.error(`Error: ${err.message}`);
7505
- process.exit(1);
7506
- }
7507
- const target = resolve3(opts.target ?? process.cwd());
7508
- const updateExistingInstructions = Boolean(opts.target) || opts.force;
7509
- console.log(
7510
- `Setting up ZAM in ${target}${opts.dryRun ? " (dry run)" : ""}
7511
- `
7512
- );
7513
- wireSkills(target, agents, {
7514
- force: opts.force,
7515
- dryRun: opts.dryRun
7516
- });
7517
- await initDatabase(opts.skipInit || opts.dryRun);
7518
- if (agents.has("claude")) {
7519
- writeClaudeMd(opts.skipClaudeMd, target, {
7520
- dryRun: opts.dryRun,
7521
- updateExisting: updateExistingInstructions
7522
- });
7523
- }
7524
- if (agents.has("codex") || agents.has("agent")) {
7525
- writeAgentsMd(opts.skipAgentsMd, target, {
7526
- dryRun: opts.dryRun,
7527
- updateExisting: updateExistingInstructions
7528
- });
7529
- }
7530
- if (agents.has("copilot") && (opts.target || opts.agents)) {
7531
- writeCopilotInstructions(target, {
7532
- dryRun: opts.dryRun,
7533
- updateExisting: updateExistingInstructions
7534
- });
7535
- }
7536
- console.log(
7537
- "\nDone. Run `zam whoami --set <your-id>` to set your identity. Start the `zam` skill with `/zam` in Claude/Copilot/Gemini-compatible clients or `$zam` (or `/skills`) in Codex."
7538
- );
7539
- }
7540
- );
7541
-
7542
- // src/cli/commands/workspace.ts
7543
- import { execFileSync as execFileSync3 } from "child_process";
7544
- import { existsSync as existsSync16, mkdirSync as mkdirSync9, writeFileSync as writeFileSync8 } from "fs";
7545
- import { homedir as homedir9 } from "os";
7546
- import { join as join16, resolve as resolve4 } from "path";
7547
- import { confirm, input } from "@inquirer/prompts";
7548
- import { Command as Command4 } from "commander";
7549
- function runGit(cwd, args) {
7550
- try {
7551
- return execFileSync3("git", args, {
7552
- cwd,
7553
- stdio: "pipe",
7554
- encoding: "utf8"
7555
- }).trim();
7556
- } catch (err) {
7557
- throw new Error(`Git command failed: ${err.message}`);
7558
- }
7559
- }
7560
- function ghRepoCreateArgs(repoName, repoVisibility) {
7561
- return ["repo", "create", repoName, repoVisibility, "--source=.", "--push"];
7562
- }
7563
- function gitRemoteArgs(githubUrl, hasOrigin) {
7564
- return hasOrigin ? ["remote", "set-url", "origin", githubUrl] : ["remote", "add", "origin", githubUrl];
7565
- }
7566
- var workspaceCommand = new Command4("workspace").description(
7567
- "Manage your ZAM learning workspace"
7568
- );
7569
- var WORKSPACE_KINDS = [
7570
- "personal",
7571
- "team",
7572
- "family",
7573
- "community",
7574
- "organization",
7575
- "custom"
7576
- ];
7577
- var WORKSPACE_SOURCE_CONTROLS = [
7578
- "github",
7579
- "azure-devops",
7580
- "git",
7581
- "none"
7582
- ];
7583
- function parseWorkspaceKind(value) {
7584
- const kind = (value ?? "custom").toLowerCase();
7585
- if (WORKSPACE_KINDS.includes(kind)) {
7586
- return kind;
7587
- }
7588
- throw new Error(
7589
- `Invalid workspace kind: ${value}. Use ${WORKSPACE_KINDS.join(", ")}.`
7590
- );
7591
- }
7592
- function parseWorkspaceSourceControl(value) {
7593
- if (!value) return void 0;
7594
- const source = value.toLowerCase();
7595
- if (WORKSPACE_SOURCE_CONTROLS.includes(source)) {
7596
- return source;
7597
- }
7598
- throw new Error(
7599
- `Invalid source control: ${value}. Use ${WORKSPACE_SOURCE_CONTROLS.join(", ")}.`
7600
- );
7601
- }
7602
- function parseScopes(value) {
7603
- if (!value) return void 0;
7604
- const scopes = value.split(",").map((item) => item.trim()).filter(Boolean);
7605
- return scopes.length > 0 ? scopes : void 0;
7073
+ return results;
7606
7074
  }
7607
- function requireWorkspace(id) {
7608
- const workspace = getConfiguredWorkspaces().find((item) => item.id === id);
7609
- if (!workspace) {
7610
- throw new Error(
7611
- `Workspace "${id}" is not configured. Add it with: zam workspace add ${id} --path <dir>`
7612
- );
7075
+ function inspectSkillLinks(cwd = process.cwd(), agents = parseSetupAgents()) {
7076
+ const results = [];
7077
+ for (const { from, to, agents: pairAgents } of SKILL_PAIRS) {
7078
+ if (!pairAgents.some((agent) => agents.has(agent))) continue;
7079
+ const sourceDir = dirname5(from);
7080
+ const destinationDir = dirname5(join15(cwd, to));
7081
+ results.push({
7082
+ agents: pairAgents,
7083
+ source: sourceDir,
7084
+ destination: destinationDir,
7085
+ state: classifySkillDestination(sourceDir, destinationDir)
7086
+ });
7613
7087
  }
7614
- return workspace;
7088
+ return results;
7615
7089
  }
7616
- workspaceCommand.command("list").description("List configured ZAM workspaces").option("--json", "Output as JSON").action((opts) => {
7617
- const workspaces = getConfiguredWorkspaces();
7618
- if (opts.json) {
7619
- console.log(JSON.stringify({ workspaces }, null, 2));
7620
- return;
7090
+ function summarizeSkillLinkHealth(inspections) {
7091
+ if (inspections.some((item) => item.state === "unmanaged")) {
7092
+ return "unmanaged";
7621
7093
  }
7622
- console.log("Configured ZAM workspaces:\n");
7623
- if (workspaces.length === 0) {
7624
- console.log(
7625
- " (none) \u2014 add one: zam workspace add personal --path <dir>"
7626
- );
7627
- return;
7628
- }
7629
- for (const workspace of workspaces) {
7630
- const label = workspace.label ? ` (${workspace.label})` : "";
7631
- console.log(` ${workspace.id}${label}`);
7632
- console.log(` kind: ${workspace.kind}`);
7633
- console.log(` path: ${workspace.path}`);
7634
- if (workspace.sourceControl) {
7635
- console.log(` source: ${workspace.sourceControl}`);
7636
- }
7637
- if (workspace.knowledgeScopes?.length) {
7638
- console.log(` scopes: ${workspace.knowledgeScopes.join(", ")}`);
7639
- }
7094
+ const repairable = ["missing", "broken", "stale-copy"];
7095
+ if (inspections.some((item) => repairable.includes(item.state))) {
7096
+ return "needs-repair";
7640
7097
  }
7641
- });
7642
- workspaceCommand.command("add <id>").description("Register an existing directory as a ZAM workspace").requiredOption("--path <dir>", "Existing workspace/repository directory").option(
7643
- "--kind <kind>",
7644
- `Workspace kind (${WORKSPACE_KINDS.join(" | ")})`,
7645
- "custom"
7646
- ).option("--label <label>", "Human-readable label").option(
7647
- "--source-control <provider>",
7648
- `Source-control provider (${WORKSPACE_SOURCE_CONTROLS.join(" | ")})`
7649
- ).option("--scopes <list>", "Comma-separated knowledge scopes").option("--default-agent <id>", "Default agent harness for this workspace").action((id, opts) => {
7650
- try {
7651
- const path = resolve4(String(opts.path));
7652
- if (!existsSync16(path)) {
7653
- console.error(`Workspace path does not exist: ${path}`);
7654
- process.exit(1);
7098
+ return "healthy";
7099
+ }
7100
+ var ZAM_BLOCK_START = "<!-- ZAM:START -->";
7101
+ var ZAM_BLOCK_END = "<!-- ZAM:END -->";
7102
+ function upsertMarkedBlock(dest, blockBody, dryRun) {
7103
+ const block = `${ZAM_BLOCK_START}
7104
+ ${blockBody.trim()}
7105
+ ${ZAM_BLOCK_END}`;
7106
+ if (!existsSync15(dest)) {
7107
+ if (!dryRun) {
7108
+ mkdirSync8(dirname5(dest), { recursive: true });
7109
+ writeFileSync7(dest, `${block}
7110
+ `, "utf8");
7655
7111
  }
7656
- const workspace = {
7657
- id,
7658
- kind: parseWorkspaceKind(opts.kind),
7659
- path,
7660
- ...opts.label ? { label: opts.label } : {},
7661
- ...opts.sourceControl ? { sourceControl: parseWorkspaceSourceControl(opts.sourceControl) } : {},
7662
- ...parseScopes(opts.scopes) ? { knowledgeScopes: parseScopes(opts.scopes) } : {},
7663
- ...opts.defaultAgent ? { defaultAgent: opts.defaultAgent } : {}
7664
- };
7665
- wireSkills(path, parseSetupAgents());
7666
- upsertConfiguredWorkspace(workspace);
7667
- console.log(`Registered and linked workspace "${id}" at ${path}.`);
7668
- } catch (err) {
7669
- console.error(`Error: ${err.message}`);
7670
- process.exit(1);
7112
+ return "write";
7671
7113
  }
7672
- });
7673
- workspaceCommand.command("remove <id>").description("Unregister a ZAM workspace without deleting its files").action((id) => {
7674
- const existing = getConfiguredWorkspaces().find((item) => item.id === id);
7675
- if (!existing) {
7676
- console.error(`Workspace "${id}" is not configured.`);
7677
- process.exit(1);
7114
+ const existing = readFileSync10(dest, "utf8");
7115
+ if (existing.includes(block)) return "skip";
7116
+ const start = existing.indexOf(ZAM_BLOCK_START);
7117
+ const end = existing.indexOf(ZAM_BLOCK_END);
7118
+ const next = start >= 0 && end > start ? `${existing.slice(0, start)}${block}${existing.slice(end + ZAM_BLOCK_END.length)}` : `${existing.trimEnd()}
7119
+
7120
+ ${block}
7121
+ `;
7122
+ if (!dryRun) writeFileSync7(dest, next, "utf8");
7123
+ return start >= 0 && end > start ? "update" : "write";
7124
+ }
7125
+ function logInstructionAction(action, label, dryRun) {
7126
+ if (action === "skip") {
7127
+ console.log(` skip ${label} (ZAM block already present)`);
7128
+ } else {
7129
+ console.log(` ${dryRun ? "would " : ""}${action} ${label}`);
7678
7130
  }
7679
- removeConfiguredWorkspace(id);
7680
- console.log(
7681
- `Unregistered workspace "${id}". Files in ${existing.path} were not changed.`
7682
- );
7683
- });
7684
- workspaceCommand.command("setup <id>").description("Install ZAM skills into a configured workspace").option(
7685
- "--agents <list>",
7686
- "comma-separated agents to wire: all, claude, copilot, codex, agent"
7687
- ).option(
7688
- "--force",
7689
- "overwrite existing skill files and refresh ZAM blocks",
7690
- false
7691
- ).option(
7692
- "--dry-run",
7693
- "show what would be written without changing files",
7694
- false
7695
- ).action((id, opts) => {
7696
- try {
7697
- const workspace = requireWorkspace(id);
7698
- const agents = parseSetupAgents(opts.agents);
7699
- console.log(
7700
- `Setting up workspace "${workspace.id}" in ${workspace.path}${opts.dryRun ? " (dry run)" : ""}
7701
- `
7702
- );
7703
- wireSkills(workspace.path, agents, {
7704
- force: Boolean(opts.force),
7705
- dryRun: Boolean(opts.dryRun)
7706
- });
7707
- if (agents.has("claude")) {
7708
- writeClaudeMd(false, workspace.path, {
7709
- dryRun: Boolean(opts.dryRun),
7710
- updateExisting: true
7711
- });
7712
- }
7713
- if (agents.has("codex") || agents.has("agent")) {
7714
- writeAgentsMd(false, workspace.path, {
7715
- dryRun: Boolean(opts.dryRun),
7716
- updateExisting: true
7717
- });
7718
- }
7719
- if (agents.has("copilot")) {
7720
- writeCopilotInstructions(workspace.path, {
7721
- dryRun: Boolean(opts.dryRun),
7722
- updateExisting: true
7723
- });
7131
+ }
7132
+ function writeClaudeMd(skipClaudeMd, cwd = process.cwd(), opts = {}) {
7133
+ if (skipClaudeMd) return;
7134
+ const dest = join15(cwd, "CLAUDE.md");
7135
+ if (existsSync15(dest)) {
7136
+ if (!opts.updateExisting) {
7137
+ console.log(` skip CLAUDE.md (already present)`);
7138
+ return;
7724
7139
  }
7725
- } catch (err) {
7726
- console.error(`Error: ${err.message}`);
7727
- process.exit(1);
7728
- }
7729
- });
7730
- workspaceCommand.command("publish").description("Publish your local workspace sandbox to GitHub").action(async () => {
7731
- let db;
7732
- let workspaceDir = "";
7733
- try {
7734
- db = await openDatabase();
7735
- workspaceDir = await getSetting(db, "personal.workspace_dir") || "";
7736
- await db.close();
7737
- } catch {
7738
- await db?.close();
7739
- }
7740
- if (!workspaceDir) {
7741
- console.error(
7742
- "\x1B[31m\u2717 No active workspace configured. Please run `zam init` first.\x1B[0m"
7743
- );
7744
- process.exit(1);
7745
- }
7746
- if (!existsSync16(workspaceDir)) {
7747
- console.error(
7748
- `\x1B[31m\u2717 Workspace directory does not exist: ${workspaceDir}\x1B[0m`
7140
+ const action = upsertMarkedBlock(
7141
+ dest,
7142
+ `## ZAM learning sessions
7143
+
7144
+ 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.
7145
+
7146
+ - Skill files live under \`.claude/skills/zam/\`.
7147
+ - Fast-changing review data lives in \`~/.zam/\`, not in this repository.
7148
+ - The skill directory is linked to this ZAM installation and updates with it.`,
7149
+ Boolean(opts.dryRun)
7749
7150
  );
7750
- process.exit(1);
7151
+ logInstructionAction(action, "CLAUDE.md", Boolean(opts.dryRun));
7152
+ return;
7751
7153
  }
7752
- console.log(`
7753
- Active workspace: \x1B[36m${workspaceDir}\x1B[0m`);
7754
- if (!hasCommand("git")) {
7755
- console.error(
7756
- "\x1B[31m\u2717 Git command was not found on this system. Please install Git first.\x1B[0m"
7757
- );
7758
- process.exit(1);
7154
+ const name = basename4(cwd);
7155
+ const content = `# ZAM Personal Kernel \u2014 ${name}
7156
+
7157
+ This is a ZAM personal instance. ZAM builds lasting skills through spaced
7158
+ repetition during real work \u2014 not separate study sessions.
7159
+
7160
+ ## First time here?
7161
+ Run \`/setup\` in Claude Code or Gemini CLI to complete first-time setup.
7162
+
7163
+ ## Regular use
7164
+ Run \`/zam\` to start a learning session on whatever you are working on.
7165
+
7166
+ ## What lives here
7167
+ - \`beliefs/\` \u2014 your worldview, approved by git commit
7168
+ - \`goals/\` \u2014 your objectives, decomposed into tasks and learning tokens
7169
+
7170
+ ## Fast-changing data
7171
+ Learning tokens, cards, and review history live in local SQLite by default.
7172
+ Use \`zam connector setup turso\` to store cloud credentials in
7173
+ \`~/.zam/credentials.json\` and use a Turso database across machines.
7174
+ `;
7175
+ if (opts.dryRun) {
7176
+ console.log(` would write CLAUDE.md`);
7177
+ } else {
7178
+ writeFileSync7(dest, content, "utf8");
7179
+ console.log(` write CLAUDE.md`);
7759
7180
  }
7760
- const gitignorePath = join16(workspaceDir, ".gitignore");
7761
- if (!existsSync16(gitignorePath)) {
7762
- writeFileSync8(
7763
- gitignorePath,
7764
- "node_modules/\n.agent/\n.agents/\n.claude/\n.gemini/\n.goose/\n*.log\n",
7765
- "utf8"
7181
+ }
7182
+ function writeAgentsMd(skipAgentsMd, cwd = process.cwd(), opts = {}) {
7183
+ if (skipAgentsMd) return;
7184
+ const dest = join15(cwd, "AGENTS.md");
7185
+ if (existsSync15(dest)) {
7186
+ if (!opts.updateExisting) {
7187
+ console.log(` skip AGENTS.md (already present)`);
7188
+ return;
7189
+ }
7190
+ const action = upsertMarkedBlock(
7191
+ dest,
7192
+ `## ZAM learning sessions
7193
+
7194
+ 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.
7195
+
7196
+ - Skill files live under \`.agents/skills/zam/\`.
7197
+ - Fast-changing review data lives in \`~/.zam/\`, not in this repository.
7198
+ - The skill directory is linked to this ZAM installation and updates with it.`,
7199
+ Boolean(opts.dryRun)
7766
7200
  );
7201
+ logInstructionAction(action, "AGENTS.md", Boolean(opts.dryRun));
7202
+ return;
7767
7203
  }
7768
- const hasGitRepo = existsSync16(join16(workspaceDir, ".git"));
7769
- if (!hasGitRepo) {
7770
- console.log("Initializing local Git repository...");
7771
- runGit(workspaceDir, ["init", "-b", "main"]);
7772
- runGit(workspaceDir, ["add", "."]);
7773
- runGit(workspaceDir, [
7774
- "commit",
7775
- "-m",
7776
- "chore: initial workspace sandbox bootstrap"
7777
- ]);
7778
- console.log("\x1B[32m\u2713 Local Git repository initialized.\x1B[0m");
7204
+ const name = basename4(cwd);
7205
+ const content = `# ZAM Personal Kernel - ${name}
7206
+
7207
+ This is a ZAM personal instance. ZAM builds lasting skills through spaced
7208
+ repetition during real work, not separate study sessions.
7209
+
7210
+ ## First time here?
7211
+ Run \`zam setup\` from the shell. When this repository includes
7212
+ \`.agents/skills/setup/\`, you can instead select \`setup\` through \`/skills\`
7213
+ or invoke \`$setup\`.
7214
+
7215
+ ## Regular use
7216
+ Select the \`zam\` skill through \`/skills\` or invoke \`$zam\` to start a
7217
+ learning session on whatever you are working on.
7218
+
7219
+ ## What lives here
7220
+ - \`beliefs/\` - your worldview, approved by git commit
7221
+ - \`goals/\` - your objectives, decomposed into tasks and learning tokens
7222
+
7223
+ ## Fast-changing data
7224
+ Learning tokens, cards, and review history live in local SQLite by default.
7225
+ Use \`zam connector setup turso\` to store cloud credentials in
7226
+ \`~/.zam/credentials.json\` and use a Turso database across machines.
7227
+
7228
+ ## Codex skills
7229
+ Codex discovers repository skills under \`.agents/skills/\`. Run
7230
+ \`zam setup --force\` after upgrading \`zam-core\` to refresh them.
7231
+ `;
7232
+ if (opts.dryRun) {
7233
+ console.log(` would write AGENTS.md`);
7779
7234
  } else {
7780
- console.log("Git repository is already initialized.");
7781
- }
7782
- const repoName = await input({
7783
- message: "Choose a name for your GitHub repository:",
7784
- default: "zam-personal"
7785
- });
7786
- const isPrivate = await confirm({
7787
- message: "Should the repository be private?",
7788
- default: true
7789
- });
7790
- const repoVisibility = isPrivate ? "--private" : "--public";
7791
- if (hasCommand("gh")) {
7792
- console.log("GitHub CLI detected! Automating repository creation...");
7793
- const proceedGh = await confirm({
7794
- message: "Would you like ZAM to create the repository using the GitHub CLI?",
7795
- default: true
7796
- });
7797
- if (proceedGh) {
7798
- try {
7799
- console.log(`Creating GitHub repository ${repoName}...`);
7800
- execFileSync3("gh", ghRepoCreateArgs(repoName, repoVisibility), {
7801
- cwd: workspaceDir,
7802
- stdio: "inherit"
7803
- });
7804
- console.log(
7805
- "\n\x1B[32m\u2713 Successfully published workspace to GitHub!\x1B[0m"
7806
- );
7807
- process.exit(0);
7808
- } catch (err) {
7809
- console.warn(
7810
- `\x1B[33m\u26A0 GitHub CLI creation failed: ${err.message}\x1B[0m`
7811
- );
7812
- }
7813
- }
7235
+ writeFileSync7(dest, content, "utf8");
7236
+ console.log(` write AGENTS.md`);
7814
7237
  }
7815
- console.log(
7816
- "\n\x1B[1mPlease create the repository manually on GitHub:\x1B[0m"
7238
+ }
7239
+ function writeCopilotInstructions(cwd = process.cwd(), opts = {}) {
7240
+ const dest = join15(cwd, ".github", "copilot-instructions.md");
7241
+ const action = upsertMarkedBlock(
7242
+ dest,
7243
+ `## ZAM learning sessions
7244
+
7245
+ 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.
7246
+
7247
+ - Fast-changing review data lives in \`~/.zam/\`, not in this repository.
7248
+ - The skill directory is linked to this ZAM installation and updates with it.`,
7249
+ Boolean(opts.dryRun)
7817
7250
  );
7818
- console.log(" 1. Go to https://github.com/new");
7819
- console.log(` 2. Name it exactly: \x1B[36m${repoName}\x1B[0m`);
7820
- console.log(
7821
- ` 3. Choose \x1B[36m${isPrivate ? "Private" : "Public"}\x1B[0m`
7251
+ logInstructionAction(
7252
+ action,
7253
+ ".github/copilot-instructions.md",
7254
+ Boolean(opts.dryRun)
7822
7255
  );
7823
- console.log(
7824
- " 4. Do NOT initialize it with README, .gitignore, or license"
7256
+ }
7257
+
7258
+ // src/cli/users/identity.ts
7259
+ async function ensureDefaultUser(db, preferredUserId) {
7260
+ const stored = await getSetting(db, "user.id");
7261
+ if (stored) return stored;
7262
+ const userId = preferredUserId?.trim() || process.env.ZAM_USER?.trim() || process.env.USERNAME?.trim() || process.env.USER?.trim() || "default";
7263
+ await setSetting(db, "user.id", userId);
7264
+ return userId;
7265
+ }
7266
+ async function resolveUser(opts, db, resolveOpts) {
7267
+ if (opts.user) return opts.user;
7268
+ const stored = await getSetting(db, "user.id");
7269
+ if (stored) return stored;
7270
+ const message = "No user specified. Set a default with: zam whoami --set <id>";
7271
+ if (resolveOpts?.json) {
7272
+ console.log(JSON.stringify({ error: message }, null, 2));
7273
+ } else {
7274
+ console.error(message);
7275
+ }
7276
+ process.exit(1);
7277
+ }
7278
+
7279
+ // src/cli/workspaces/active.ts
7280
+ import { existsSync as existsSync16, mkdirSync as mkdirSync9 } from "fs";
7281
+ import { homedir as homedir9 } from "os";
7282
+ import { basename as basename5, join as join16, resolve as resolve3 } from "path";
7283
+ var LEGACY_WORKSPACE_DIR_KEY = "personal.workspace_dir";
7284
+ var DEFAULT_WORKSPACE_ID = "personal";
7285
+ function defaultWorkspaceDir() {
7286
+ return join16(homedir9(), "Documents", "zam");
7287
+ }
7288
+ function normalizeWorkspacePath(path) {
7289
+ const resolved = resolve3(path);
7290
+ return process.platform === "win32" ? resolved.toLowerCase() : resolved;
7291
+ }
7292
+ function sameWorkspacePath(left, right) {
7293
+ return normalizeWorkspacePath(left) === normalizeWorkspacePath(right);
7294
+ }
7295
+ function labelFromPath(path) {
7296
+ return basename5(path) || "ZAM";
7297
+ }
7298
+ function workspaceIdFromPath(dir) {
7299
+ const base = basename5(dir).toLowerCase().replace(/[^a-z0-9]+/g, "-").replace(/^-+|-+$/g, "");
7300
+ const prefix = base || "workspace";
7301
+ const existing = new Set(getConfiguredWorkspaces().map((item) => item.id));
7302
+ if (!existing.has(prefix)) return prefix;
7303
+ let index = 2;
7304
+ while (existing.has(`${prefix}-${index}`)) index++;
7305
+ return `${prefix}-${index}`;
7306
+ }
7307
+ function findWorkspaceByPath(dir) {
7308
+ return getConfiguredWorkspaces().find(
7309
+ (workspace) => sameWorkspacePath(workspace.path, dir)
7825
7310
  );
7826
- console.log(" 5. Click 'Create repository'\n");
7827
- const githubUrl = await input({
7828
- message: "Paste the repository Git URL (e.g. git@github.com:user/repo.git):"
7829
- });
7830
- if (githubUrl) {
7831
- try {
7832
- console.log("Linking remote repository and pushing...");
7833
- let hasOrigin = false;
7834
- try {
7835
- runGit(workspaceDir, ["remote", "get-url", "origin"]);
7836
- hasOrigin = true;
7837
- } catch {
7838
- }
7839
- runGit(workspaceDir, gitRemoteArgs(githubUrl, hasOrigin));
7840
- runGit(workspaceDir, ["push", "-u", "origin", "main"]);
7841
- console.log(
7842
- "\x1B[32m\u2713 Successfully linked and pushed to GitHub!\x1B[0m"
7843
- );
7844
- } catch (err) {
7845
- console.error(
7846
- `\x1B[31m\u2717 Push failed: ${err.message}\x1B[0m`
7847
- );
7848
- console.log(
7849
- "You can push manually later using: git push -u origin main"
7850
- );
7851
- }
7311
+ }
7312
+ async function clearLegacyWorkspaceDir(db) {
7313
+ await deleteSetting(db, LEGACY_WORKSPACE_DIR_KEY);
7314
+ }
7315
+ async function migrateLegacyWorkspaceDir(db) {
7316
+ const legacyDir = await getSetting(db, LEGACY_WORKSPACE_DIR_KEY);
7317
+ if (!legacyDir) return void 0;
7318
+ const path = resolve3(legacyDir);
7319
+ const existing = findWorkspaceByPath(path);
7320
+ const migrated = existing ?? {
7321
+ id: getConfiguredWorkspaces().some(
7322
+ (workspace) => workspace.id === DEFAULT_WORKSPACE_ID
7323
+ ) ? workspaceIdFromPath(path) : DEFAULT_WORKSPACE_ID,
7324
+ label: labelFromPath(path),
7325
+ kind: "personal",
7326
+ path
7327
+ };
7328
+ if (!existing) {
7329
+ upsertConfiguredWorkspace(migrated);
7852
7330
  }
7853
- });
7331
+ if (!getActiveWorkspace()) {
7332
+ setActiveWorkspaceId(migrated.id);
7333
+ }
7334
+ await clearLegacyWorkspaceDir(db);
7335
+ return migrated;
7336
+ }
7337
+ async function ensureActiveWorkspace(db) {
7338
+ await migrateLegacyWorkspaceDir(db);
7339
+ const active = getActiveWorkspace();
7340
+ if (active) {
7341
+ mkdirSync9(active.path, { recursive: true });
7342
+ return active;
7343
+ }
7344
+ const configured = getConfiguredWorkspaces()[0];
7345
+ if (configured) {
7346
+ setActiveWorkspaceId(configured.id);
7347
+ mkdirSync9(configured.path, { recursive: true });
7348
+ return configured;
7349
+ }
7350
+ const workspace = {
7351
+ id: DEFAULT_WORKSPACE_ID,
7352
+ label: "Personal",
7353
+ kind: "personal",
7354
+ path: defaultWorkspaceDir()
7355
+ };
7356
+ mkdirSync9(workspace.path, { recursive: true });
7357
+ upsertConfiguredWorkspace(workspace);
7358
+ setActiveWorkspaceId(workspace.id);
7359
+ await clearLegacyWorkspaceDir(db);
7360
+ return workspace;
7361
+ }
7362
+ async function activateWorkspace(db, workspace) {
7363
+ upsertConfiguredWorkspace(workspace);
7364
+ setActiveWorkspaceId(workspace.id);
7365
+ await clearLegacyWorkspaceDir(db);
7366
+ return workspace;
7367
+ }
7368
+ async function activateWorkspacePath(db, dir, opts = {}) {
7369
+ await migrateLegacyWorkspaceDir(db);
7370
+ const path = resolve3(dir);
7371
+ const existing = opts.id ? void 0 : findWorkspaceByPath(path);
7372
+ if (existing) {
7373
+ setActiveWorkspaceId(existing.id);
7374
+ await clearLegacyWorkspaceDir(db);
7375
+ return existing;
7376
+ }
7377
+ const workspace = {
7378
+ id: opts.id || workspaceIdFromPath(path),
7379
+ label: opts.label || labelFromPath(path),
7380
+ kind: opts.kind || "personal",
7381
+ path
7382
+ };
7383
+ return activateWorkspace(db, workspace);
7384
+ }
7385
+ async function removeWorkspaceAndResolveActive(db, id) {
7386
+ removeConfiguredWorkspace(id);
7387
+ await clearLegacyWorkspaceDir(db);
7388
+ const activeWorkspace = await ensureActiveWorkspace(db);
7389
+ return {
7390
+ activeWorkspace,
7391
+ workspaces: getConfiguredWorkspaces()
7392
+ };
7393
+ }
7394
+ function existingWorkspaceDirOrHome(workspace) {
7395
+ return existsSync16(workspace.path) ? workspace.path : homedir9();
7396
+ }
7397
+
7398
+ // src/cli/workspaces/backup.ts
7399
+ import { mkdirSync as mkdirSync10 } from "fs";
7400
+ import { join as join17 } from "path";
7854
7401
  async function backupDatabaseTo(db, targetDir) {
7855
- const backupDir = join16(targetDir, "zam-backups");
7856
- mkdirSync9(backupDir, { recursive: true });
7402
+ const backupDir = join17(targetDir, "zam-backups");
7403
+ mkdirSync10(backupDir, { recursive: true });
7857
7404
  const stamp = (/* @__PURE__ */ new Date()).toISOString().replace(/[:.]/g, "-");
7858
- const dest = join16(backupDir, `zam-${stamp}.db`);
7405
+ const dest = join17(backupDir, `zam-${stamp}.db`);
7859
7406
  await db.exec(`VACUUM INTO '${dest.replace(/'/g, "''")}'`);
7860
7407
  return dest;
7861
7408
  }
7862
- workspaceCommand.command("data-dir").description("Print the ZAM data directory (database, credentials, config)").option("--json", "Output as JSON").action((opts) => {
7863
- const dir = join16(homedir9(), ".zam");
7864
- console.log(opts.json ? JSON.stringify({ dataDir: dir }) : dir);
7865
- });
7866
- workspaceCommand.command("backup").description("Back up the local ZAM database into your workspace").option(
7867
- "--dir <path>",
7868
- "Target directory (default: workspace dir, else ~/Documents/zam)"
7869
- ).option("--json", "Output as JSON").action(async (opts) => {
7870
- const target = getDatabaseTargetInfo();
7871
- if (target.kind !== "local") {
7872
- const reason = `The database is ${target.kind} (${target.location}); file backup applies only to a local database \u2014 your Turso remote is already the cloud backup.`;
7873
- if (opts.json) {
7874
- console.log(JSON.stringify({ ok: false, reason }));
7875
- return;
7876
- }
7877
- console.error(`\x1B[33m\u26A0 ${reason}\x1B[0m`);
7878
- process.exit(1);
7879
- }
7880
- let db;
7881
- try {
7882
- db = await openDatabase();
7883
- const workspaceDir = opts.dir || await getSetting(db, "personal.workspace_dir") || join16(homedir9(), "Documents", "zam");
7884
- const dest = await backupDatabaseTo(db, workspaceDir);
7885
- if (opts.json) {
7886
- console.log(JSON.stringify({ ok: true, path: dest }));
7887
- } else {
7888
- console.log(`\x1B[32m\u2713 Database backed up to ${dest}\x1B[0m`);
7889
- }
7890
- } catch (err) {
7891
- const reason = err.message;
7892
- if (opts.json) {
7893
- console.log(JSON.stringify({ ok: false, reason }));
7894
- } else {
7895
- console.error(`\x1B[31m\u2717 Backup failed: ${reason}\x1B[0m`);
7896
- }
7897
- process.exit(1);
7898
- } finally {
7899
- await db?.close();
7900
- }
7901
- });
7902
7409
 
7903
7410
  // src/cli/commands/bridge.ts
7904
7411
  var isServeMode = false;
@@ -7962,7 +7469,7 @@ function parseTokenUpdates(opts) {
7962
7469
  }
7963
7470
  return updates;
7964
7471
  }
7965
- var bridgeCommand = new Command5("bridge").description(
7472
+ var bridgeCommand = new Command2("bridge").description(
7966
7473
  "Machine-readable JSON protocol for AI integration"
7967
7474
  );
7968
7475
  bridgeCommand.command("check-due").description("Check due cards for a user (JSON)").option("--user <id>", "User ID (default: whoami)").option("--domain <domain>", "Filter by knowledge domain").action(async (opts) => {
@@ -8005,72 +7512,98 @@ bridgeCommand.command("backup-db").description("Back up the local database into
8005
7512
  return;
8006
7513
  }
8007
7514
  await withDb2(async (db) => {
8008
- const workspaceDir = opts.dir || await getSetting(db, "personal.workspace_dir") || join17(homedir10(), "Documents", "zam");
7515
+ const workspaceDir = opts.dir || (await ensureActiveWorkspace(db)).path;
8009
7516
  const path = await backupDatabaseTo(db, workspaceDir);
8010
7517
  jsonOut2({ ok: true, path });
8011
7518
  });
8012
7519
  });
8013
7520
  bridgeCommand.command("workspace-info").description("Report the workspace dir, its default, and the data dir (JSON)").action(async () => {
8014
7521
  await withDb2(async (db) => {
8015
- const workspaceDir = await getSetting(db, "personal.workspace_dir") || null;
7522
+ const activeWorkspace = await ensureActiveWorkspace(db);
8016
7523
  jsonOut2({
8017
- workspaceDir,
8018
- defaultWorkspaceDir: join17(homedir10(), "Documents", "zam"),
8019
- dataDir: join17(homedir10(), ".zam")
7524
+ activeWorkspaceId: activeWorkspace.id,
7525
+ activeWorkspace,
7526
+ workspaceDir: activeWorkspace.path,
7527
+ defaultWorkspaceDir: defaultWorkspaceDir(),
7528
+ dataDir: join18(homedir10(), ".zam")
8020
7529
  });
8021
7530
  });
8022
7531
  });
8023
- function sameWorkspacePath(left, right) {
8024
- const normalize = (value) => {
8025
- const path = resolve5(value);
8026
- return process.platform === "win32" ? path.toLowerCase() : path;
8027
- };
8028
- return normalize(left) === normalize(right);
8029
- }
8030
- async function ensureDesktopWorkspace(db) {
8031
- const configured = getConfiguredWorkspaces();
8032
- const savedWorkspaceDir = await getSetting(db, "personal.workspace_dir");
8033
- const workspaceDir = savedWorkspaceDir || configured.find((workspace) => workspace.kind === "personal")?.path || join17(homedir10(), "Documents", "zam");
8034
- mkdirSync10(workspaceDir, { recursive: true });
8035
- if (!savedWorkspaceDir) {
8036
- await setSetting(db, "personal.workspace_dir", workspaceDir);
8037
- }
8038
- if (!configured.some(
8039
- (workspace) => sameWorkspacePath(workspace.path, workspaceDir)
8040
- )) {
8041
- const id = configured.some((workspace) => workspace.id === "personal") ? workspaceIdFromPath(workspaceDir) : "personal";
8042
- upsertConfiguredWorkspace({
8043
- id,
8044
- label: basename5(workspaceDir) || "ZAM",
8045
- kind: "personal",
8046
- path: workspaceDir
8047
- });
8048
- }
8049
- const skillLinks = getConfiguredWorkspaces().flatMap(
7532
+ function provisionConfiguredWorkspaces() {
7533
+ return getConfiguredWorkspaces().flatMap(
8050
7534
  (workspace) => existsSync17(workspace.path) ? wireSkills(workspace.path, parseSetupAgents(), { quiet: true }) : []
8051
7535
  );
8052
- return { workspaceDir, skillLinks };
7536
+ }
7537
+ function buildWorkspaceLinkHealth(workspaces) {
7538
+ const agents = parseSetupAgents();
7539
+ const map = {};
7540
+ for (const workspace of workspaces) {
7541
+ if (!existsSync17(workspace.path)) continue;
7542
+ const links = inspectSkillLinks(workspace.path, agents);
7543
+ map[workspace.id] = {
7544
+ health: summarizeSkillLinkHealth(links),
7545
+ states: Object.fromEntries(
7546
+ links.map((link) => [link.agents.join("+"), link.state])
7547
+ )
7548
+ };
7549
+ }
7550
+ return map;
7551
+ }
7552
+ async function ensureDesktopWorkspace(db) {
7553
+ const activeWorkspace = await ensureActiveWorkspace(db);
7554
+ const skillLinks = provisionConfiguredWorkspaces();
7555
+ return {
7556
+ workspaceDir: activeWorkspace.path,
7557
+ activeWorkspaceId: activeWorkspace.id,
7558
+ skillLinks
7559
+ };
8053
7560
  }
8054
7561
  bridgeCommand.command("workspace-list").description("List configured ZAM workspaces (JSON)").action(async () => {
8055
7562
  await withDb2(async (db) => {
8056
- const workspaceDir = await getSetting(db, "personal.workspace_dir") || null;
7563
+ const activeWorkspace = await ensureActiveWorkspace(db);
7564
+ const workspaces = getConfiguredWorkspaces();
8057
7565
  jsonOut2({
8058
- workspaces: getConfiguredWorkspaces(),
8059
- workspaceDir,
8060
- defaultWorkspaceDir: join17(homedir10(), "Documents", "zam"),
8061
- dataDir: join17(homedir10(), ".zam")
7566
+ workspaces,
7567
+ activeWorkspaceId: activeWorkspace.id,
7568
+ activeWorkspace,
7569
+ workspaceDir: activeWorkspace.path,
7570
+ defaultWorkspaceDir: defaultWorkspaceDir(),
7571
+ dataDir: join18(homedir10(), ".zam"),
7572
+ linkHealth: buildWorkspaceLinkHealth(workspaces)
8062
7573
  });
8063
7574
  });
8064
7575
  });
8065
- function workspaceIdFromPath(dir) {
8066
- const base = basename5(dir).toLowerCase().replace(/[^a-z0-9]+/g, "-").replace(/^-+|-+$/g, "");
8067
- const prefix = base || "workspace";
8068
- const existing = new Set(getConfiguredWorkspaces().map((item) => item.id));
8069
- if (!existing.has(prefix)) return prefix;
8070
- let index = 2;
8071
- while (existing.has(`${prefix}-${index}`)) index++;
8072
- return `${prefix}-${index}`;
8073
- }
7576
+ bridgeCommand.command("workspace-repair-links").description(
7577
+ "Relink ZAM skill junctions for a configured workspace, replacing broken links and outdated copies (JSON)"
7578
+ ).requiredOption("--id <id>", "Workspace id").option(
7579
+ "--agents <list>",
7580
+ "comma-separated agents to wire: all, claude, copilot, codex, agent"
7581
+ ).action(async (opts) => {
7582
+ const id = String(opts.id ?? "").trim();
7583
+ if (!id) jsonError("A non-empty --id is required");
7584
+ const workspace = getConfiguredWorkspaces().find((item) => item.id === id);
7585
+ if (!workspace) jsonError(`Workspace "${id}" is not configured`);
7586
+ if (!existsSync17(workspace.path)) {
7587
+ jsonError(`Workspace path does not exist: ${workspace.path}`);
7588
+ }
7589
+ const agents = parseSetupAgents(opts.agents);
7590
+ const skillLinks = wireSkills(workspace.path, agents, {
7591
+ force: true,
7592
+ quiet: true
7593
+ });
7594
+ const links = inspectSkillLinks(workspace.path, agents);
7595
+ jsonOut2({
7596
+ ok: true,
7597
+ workspace,
7598
+ skillLinks,
7599
+ linkHealth: {
7600
+ health: summarizeSkillLinkHealth(links),
7601
+ states: Object.fromEntries(
7602
+ links.map((link) => [link.agents.join("+"), link.state])
7603
+ )
7604
+ }
7605
+ });
7606
+ });
8074
7607
  function parseBridgeWorkspaceKind(value) {
8075
7608
  const kind = (value || "custom").toLowerCase();
8076
7609
  if (kind === "personal" || kind === "team" || kind === "family" || kind === "community" || kind === "organization" || kind === "custom") {
@@ -8081,25 +7614,25 @@ function parseBridgeWorkspaceKind(value) {
8081
7614
  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) => {
8082
7615
  const raw = String(opts.path ?? "").trim();
8083
7616
  if (!raw) jsonError("A non-empty --path is required");
8084
- const path = resolve5(raw);
7617
+ const path = resolve4(raw);
8085
7618
  if (!existsSync17(path)) jsonError(`Workspace path does not exist: ${path}`);
8086
- const id = String(opts.id || workspaceIdFromPath(path)).trim();
8087
- if (!id) jsonError("A non-empty --id is required");
8088
- const workspace = {
8089
- id,
8090
- label: opts.label || basename5(path) || id,
8091
- kind: parseBridgeWorkspaceKind(opts.kind),
8092
- path
8093
- };
7619
+ const id = opts.id ? String(opts.id).trim() : void 0;
7620
+ if (opts.id && !id) jsonError("A non-empty --id is required");
7621
+ const kind = parseBridgeWorkspaceKind(opts.kind);
8094
7622
  const skillLinks = wireSkills(path, parseSetupAgents(), { quiet: true });
8095
7623
  await withDb2(async (db) => {
8096
- await setSetting(db, "personal.workspace_dir", path);
8097
- upsertConfiguredWorkspace(workspace);
7624
+ const workspace = await activateWorkspacePath(db, path, {
7625
+ ...id ? { id } : {},
7626
+ ...opts.label ? { label: opts.label } : {},
7627
+ kind
7628
+ });
8098
7629
  jsonOut2({
8099
7630
  ok: true,
8100
7631
  workspace,
8101
7632
  workspaces: getConfiguredWorkspaces(),
8102
- workspaceDir: path,
7633
+ activeWorkspaceId: workspace.id,
7634
+ activeWorkspace: workspace,
7635
+ workspaceDir: workspace.path,
8103
7636
  skillLinks
8104
7637
  });
8105
7638
  });
@@ -8110,27 +7643,15 @@ bridgeCommand.command("workspace-remove").description("Unregister a ZAM workspac
8110
7643
  const workspace = getConfiguredWorkspaces().find((item) => item.id === id);
8111
7644
  if (!workspace) jsonError(`Workspace "${id}" is not configured`);
8112
7645
  await withDb2(async (db) => {
8113
- const activeWorkspaceDir = await getSetting(db, "personal.workspace_dir") || null;
8114
- const remaining = removeConfiguredWorkspace(id);
8115
- let workspaceDir = activeWorkspaceDir;
8116
- let skillLinks = [];
8117
- if (activeWorkspaceDir && sameWorkspacePath(activeWorkspaceDir, workspace.path)) {
8118
- if (remaining.length > 0) {
8119
- workspaceDir = remaining[0].path;
8120
- await setSetting(db, "personal.workspace_dir", workspaceDir);
8121
- } else {
8122
- workspaceDir = join17(homedir10(), "Documents", "zam");
8123
- await setSetting(db, "personal.workspace_dir", workspaceDir);
8124
- const ensured = await ensureDesktopWorkspace(db);
8125
- workspaceDir = ensured.workspaceDir;
8126
- skillLinks = ensured.skillLinks;
8127
- }
8128
- }
7646
+ const { activeWorkspace, workspaces } = await removeWorkspaceAndResolveActive(db, id);
7647
+ const skillLinks = provisionConfiguredWorkspaces();
8129
7648
  jsonOut2({
8130
7649
  ok: true,
8131
7650
  removed: workspace,
8132
- workspaces: getConfiguredWorkspaces(),
8133
- workspaceDir,
7651
+ workspaces,
7652
+ activeWorkspaceId: activeWorkspace.id,
7653
+ activeWorkspace,
7654
+ workspaceDir: activeWorkspace.path,
8134
7655
  skillLinks
8135
7656
  });
8136
7657
  });
@@ -8138,12 +7659,20 @@ bridgeCommand.command("workspace-remove").description("Unregister a ZAM workspac
8138
7659
  bridgeCommand.command("set-workspace-dir").description("Set the personal workspace directory (JSON)").requiredOption("--dir <path>", "Path to the workspace directory").action(async (opts) => {
8139
7660
  const raw = String(opts.dir ?? "").trim();
8140
7661
  if (!raw) jsonError("A non-empty --dir is required");
8141
- const dir = resolve5(raw);
7662
+ const dir = resolve4(raw);
8142
7663
  if (!existsSync17(dir)) jsonError(`Workspace path does not exist: ${dir}`);
8143
7664
  const skillLinks = wireSkills(dir, parseSetupAgents(), { quiet: true });
8144
7665
  await withDb2(async (db) => {
8145
- await setSetting(db, "personal.workspace_dir", dir);
8146
- jsonOut2({ ok: true, workspaceDir: dir, skillLinks });
7666
+ const workspace = await activateWorkspacePath(db, dir);
7667
+ jsonOut2({
7668
+ ok: true,
7669
+ workspace,
7670
+ workspaces: getConfiguredWorkspaces(),
7671
+ activeWorkspaceId: workspace.id,
7672
+ activeWorkspace: workspace,
7673
+ workspaceDir: workspace.path,
7674
+ skillLinks
7675
+ });
8147
7676
  });
8148
7677
  });
8149
7678
  bridgeCommand.command("agent-list").description("List agent harnesses with detection state + the default (JSON)").action(async () => {
@@ -8193,8 +7722,8 @@ bridgeCommand.command("agent-open").description("Launch an agent harness in the
8193
7722
  if (opts.workspace && !configuredWorkspace) {
8194
7723
  jsonError(`Workspace is not configured: ${opts.workspace}`);
8195
7724
  }
8196
- let workspace = opts.dir || configuredWorkspace?.path || await getSetting(db, "personal.workspace_dir") || join17(homedir10(), "Documents", "zam");
8197
- if (!existsSync17(workspace)) workspace = homedir10();
7725
+ const activeWorkspace = await ensureActiveWorkspace(db);
7726
+ const workspace = opts.dir ? existsSync17(opts.dir) ? opts.dir : homedir10() : existingWorkspaceDirOrHome(configuredWorkspace ?? activeWorkspace);
8198
7727
  launchHarness(harness, {
8199
7728
  executable,
8200
7729
  workspace,
@@ -8574,10 +8103,10 @@ bridgeCommand.command("discover-skills").description(
8574
8103
  "20"
8575
8104
  ).action(async (opts) => {
8576
8105
  try {
8577
- const monitorDir = join17(homedir10(), ".zam", "monitor");
8106
+ const monitorDir = join18(homedir10(), ".zam", "monitor");
8578
8107
  let files;
8579
8108
  try {
8580
- files = readdirSync3(monitorDir).filter((f) => f.endsWith(".jsonl"));
8109
+ files = readdirSync2(monitorDir).filter((f) => f.endsWith(".jsonl"));
8581
8110
  } catch {
8582
8111
  jsonOut2({ proposals: [], message: "No monitor logs found." });
8583
8112
  return;
@@ -8587,7 +8116,7 @@ bridgeCommand.command("discover-skills").description(
8587
8116
  return;
8588
8117
  }
8589
8118
  const limit = Number(opts.limit);
8590
- const sorted = files.map((f) => ({ name: f, path: join17(monitorDir, f) })).sort((a, b) => b.name.localeCompare(a.name)).slice(0, limit);
8119
+ const sorted = files.map((f) => ({ name: f, path: join18(monitorDir, f) })).sort((a, b) => b.name.localeCompare(a.name)).slice(0, limit);
8591
8120
  const sessionCommands = /* @__PURE__ */ new Map();
8592
8121
  for (const file of sorted) {
8593
8122
  const sessionId = file.name.replace(".jsonl", "");
@@ -8699,7 +8228,7 @@ bridgeCommand.command("observe-ui-snapshot").description(
8699
8228
  });
8700
8229
  function resolveWindowsPowerShell() {
8701
8230
  try {
8702
- execFileSync4("where.exe", ["pwsh.exe"], { stdio: "ignore" });
8231
+ execFileSync3("where.exe", ["pwsh.exe"], { stdio: "ignore" });
8703
8232
  return "pwsh";
8704
8233
  } catch {
8705
8234
  return "powershell";
@@ -8714,7 +8243,7 @@ function captureScreenshot(outputPath, hwnd, processName) {
8714
8243
  throw new Error(`Invalid process name format: ${processName}`);
8715
8244
  }
8716
8245
  if (platform === "win32") {
8717
- const stdout = execFileSync4(
8246
+ const stdout = execFileSync3(
8718
8247
  resolveWindowsPowerShell(),
8719
8248
  [
8720
8249
  "-NoProfile",
@@ -9027,13 +8556,13 @@ Write-CaptureResult "fullscreen" $hwndVal $matchedBy
9027
8556
  } else if (platform === "darwin") {
9028
8557
  if (hwnd) {
9029
8558
  const parsedHwnd = hwnd.startsWith("0x") ? parseInt(hwnd, 16) : parseInt(hwnd, 10);
9030
- execFileSync4("screencapture", ["-l", String(parsedHwnd), outputPath], {
8559
+ execFileSync3("screencapture", ["-l", String(parsedHwnd), outputPath], {
9031
8560
  stdio: "pipe"
9032
8561
  });
9033
8562
  return { method: "screencapture-window", target: null };
9034
8563
  } else if (processName) {
9035
8564
  try {
9036
- const windowId = execFileSync4(
8565
+ const windowId = execFileSync3(
9037
8566
  "osascript",
9038
8567
  [
9039
8568
  "-e",
@@ -9042,17 +8571,17 @@ Write-CaptureResult "fullscreen" $hwndVal $matchedBy
9042
8571
  { encoding: "utf8", stdio: ["pipe", "pipe", "ignore"] }
9043
8572
  ).trim();
9044
8573
  if (windowId && /^\d+$/.test(windowId)) {
9045
- execFileSync4("screencapture", ["-l", windowId, outputPath], {
8574
+ execFileSync3("screencapture", ["-l", windowId, outputPath], {
9046
8575
  stdio: "pipe"
9047
8576
  });
9048
8577
  return { method: "screencapture-window", target: null };
9049
8578
  }
9050
8579
  } catch {
9051
8580
  }
9052
- execFileSync4("screencapture", ["-x", outputPath], { stdio: "pipe" });
8581
+ execFileSync3("screencapture", ["-x", outputPath], { stdio: "pipe" });
9053
8582
  return { method: "screencapture-full", target: null };
9054
8583
  } else {
9055
- execFileSync4("screencapture", ["-x", outputPath], { stdio: "pipe" });
8584
+ execFileSync3("screencapture", ["-x", outputPath], { stdio: "pipe" });
9056
8585
  return { method: "screencapture-full", target: null };
9057
8586
  }
9058
8587
  } else {
@@ -9089,7 +8618,7 @@ bridgeCommand.command("capture-ui").description("Capture a screenshot for agent-
9089
8618
  return;
9090
8619
  }
9091
8620
  }
9092
- const outputPath = opts.image ?? opts.output ?? join17(tmpdir3(), `zam-capture-${randomBytes2(4).toString("hex")}.png`);
8621
+ const outputPath = opts.image ?? opts.output ?? join18(tmpdir3(), `zam-capture-${randomBytes2(4).toString("hex")}.png`);
9093
8622
  const captureResult = isProvided ? { method: "provided", target: null } : captureScreenshot(outputPath, opts.hwnd, opts.processName);
9094
8623
  if (!isProvided) {
9095
8624
  const post = decidePostCapture(policy, {
@@ -9144,11 +8673,11 @@ bridgeCommand.command("start-recording").description("Start screen recording in
9144
8673
  return;
9145
8674
  }
9146
8675
  const sessionId = opts.session;
9147
- const statePath = join17(tmpdir3(), `zam-recording-${sessionId}.json`);
8676
+ const statePath = join18(tmpdir3(), `zam-recording-${sessionId}.json`);
9148
8677
  const defaultExt = platform === "win32" ? ".mkv" : ".mov";
9149
- const outputPath = opts.output ?? join17(tmpdir3(), `zam-recording-${sessionId}${defaultExt}`);
9150
- const { existsSync: existsSync25, writeFileSync: writeFileSync12, openSync, closeSync } = await import("fs");
9151
- if (existsSync25(statePath)) {
8678
+ const outputPath = opts.output ?? join18(tmpdir3(), `zam-recording-${sessionId}${defaultExt}`);
8679
+ const { existsSync: existsSync26, writeFileSync: writeFileSync12, openSync, closeSync } = await import("fs");
8680
+ if (existsSync26(statePath)) {
9152
8681
  jsonOut2({
9153
8682
  sessionId,
9154
8683
  started: false,
@@ -9156,7 +8685,7 @@ bridgeCommand.command("start-recording").description("Start screen recording in
9156
8685
  });
9157
8686
  return;
9158
8687
  }
9159
- const logPath = join17(tmpdir3(), `zam-recording-${sessionId}.log`);
8688
+ const logPath = join18(tmpdir3(), `zam-recording-${sessionId}.log`);
9160
8689
  let logFd;
9161
8690
  try {
9162
8691
  logFd = openSync(logPath, "w");
@@ -9238,9 +8767,9 @@ bridgeCommand.command("stop-recording").description(
9238
8767
  return;
9239
8768
  }
9240
8769
  const sessionId = opts.session;
9241
- const statePath = join17(tmpdir3(), `zam-recording-${sessionId}.json`);
9242
- const { existsSync: existsSync25, readFileSync: readFileSync16, rmSync: rmSync4 } = await import("fs");
9243
- if (!existsSync25(statePath)) {
8770
+ const statePath = join18(tmpdir3(), `zam-recording-${sessionId}.json`);
8771
+ const { existsSync: existsSync26, readFileSync: readFileSync16, rmSync: rmSync4 } = await import("fs");
8772
+ if (!existsSync26(statePath)) {
9244
8773
  jsonOut2({
9245
8774
  sessionId,
9246
8775
  stopped: false,
@@ -9264,7 +8793,7 @@ bridgeCommand.command("stop-recording").description(
9264
8793
  };
9265
8794
  let attempts = 0;
9266
8795
  while (isProcessRunning(pid) && attempts < 20) {
9267
- await new Promise((resolve9) => setTimeout(resolve9, 250));
8796
+ await new Promise((resolve10) => setTimeout(resolve10, 250));
9268
8797
  attempts++;
9269
8798
  }
9270
8799
  if (isProcessRunning(pid)) {
@@ -9277,7 +8806,7 @@ bridgeCommand.command("stop-recording").description(
9277
8806
  rmSync4(statePath, { force: true });
9278
8807
  } catch {
9279
8808
  }
9280
- if (!existsSync25(outputPath)) {
8809
+ if (!existsSync26(outputPath)) {
9281
8810
  jsonOut2({
9282
8811
  sessionId,
9283
8812
  stopped: false,
@@ -9657,11 +9186,12 @@ bridgeCommand.command("desktop-bootstrap").description("Initialize first-run des
9657
9186
  await withDb2(async (db) => {
9658
9187
  const userId = await ensureDefaultUser(db, opts.user);
9659
9188
  const { enabled, url, model, locale } = await getLlmConfig(db);
9660
- const { workspaceDir, skillLinks } = await ensureDesktopWorkspace(db);
9189
+ const { workspaceDir, activeWorkspaceId, skillLinks } = await ensureDesktopWorkspace(db);
9661
9190
  jsonOut2({
9662
9191
  userId,
9663
9192
  locale,
9664
9193
  llm: { enabled, url, model },
9194
+ activeWorkspaceId,
9665
9195
  workspaceDir,
9666
9196
  skillLinks
9667
9197
  });
@@ -9882,8 +9412,8 @@ bridgeCommand.command("serve").description("Start the persistent JSON-RPC stdin/
9882
9412
  });
9883
9413
 
9884
9414
  // src/cli/commands/card.ts
9885
- import { Command as Command6 } from "commander";
9886
- var cardCommand = new Command6("card").description(
9415
+ import { Command as Command3 } from "commander";
9416
+ var cardCommand = new Command3("card").description(
9887
9417
  "Manage spaced-repetition cards"
9888
9418
  );
9889
9419
  cardCommand.command("due").description("Show due tokens for a user").option("--user <id>", "User ID (default: whoami)").option("--domain <domain>", "Filter by knowledge domain").option("--json", "Output as JSON").option("--summary", "Show only counts per domain (no slugs or concepts)").action(async (opts) => {
@@ -10081,9 +9611,9 @@ cardCommand.command("delete").description("Delete one user's card for a token").
10081
9611
  });
10082
9612
 
10083
9613
  // src/cli/commands/connector.ts
10084
- import { input as input2, password as password2 } from "@inquirer/prompts";
10085
- import { Command as Command7 } from "commander";
10086
- var connectorCommand = new Command7("connector").description(
9614
+ import { input, password } from "@inquirer/prompts";
9615
+ import { Command as Command4 } from "commander";
9616
+ var connectorCommand = new Command4("connector").description(
10087
9617
  "Manage external service connectors"
10088
9618
  );
10089
9619
  connectorCommand.command("setup").description("Configure a connector").argument("<type>", "Connector type (ado, turso)").option("--url <url>", "Turso database URL (non-interactive)").option("--token <token>", "Turso auth token (non-interactive)").option(
@@ -10102,13 +9632,13 @@ connectorCommand.command("setup").description("Configure a connector").argument(
10102
9632
  process.exit(1);
10103
9633
  }
10104
9634
  try {
10105
- const orgUrl = await input2({
9635
+ const orgUrl = await input({
10106
9636
  message: "Organization URL (e.g. https://dev.azure.com/myorg):"
10107
9637
  });
10108
- const project = await input2({
9638
+ const project = await input({
10109
9639
  message: "Project name:"
10110
9640
  });
10111
- const pat = await password2({
9641
+ const pat = await password({
10112
9642
  message: "Personal Access Token:"
10113
9643
  });
10114
9644
  if (!orgUrl || !project || !pat) {
@@ -10195,23 +9725,31 @@ connectorCommand.command("sync").description("Verify the Turso cloud database co
10195
9725
  });
10196
9726
  async function setupTurso(urlArg, tokenArg, mode) {
10197
9727
  let db;
9728
+ const arch = getSystemProfile().arch;
9729
+ const isWindowsArm64 = process.platform === "win32" && arch === "arm64";
9730
+ const effectiveMode = mode ?? (isWindowsArm64 ? "remote" : void 0);
9731
+ if (!mode && isWindowsArm64) {
9732
+ console.log(
9733
+ "Detected Windows ARM64 \u2014 defaulting to remote (HTTP) mode.\n The native libsql driver is not available on this architecture.\n Remote mode uses the Turso HTTP API and works everywhere.\n"
9734
+ );
9735
+ }
10198
9736
  try {
10199
- const url = urlArg ?? await input2({
9737
+ const url = urlArg ?? await input({
10200
9738
  message: "Turso database URL (e.g. libsql://my-db-user.turso.io):"
10201
9739
  });
10202
- const token = tokenArg ?? await password2({
9740
+ const token = tokenArg ?? await password({
10203
9741
  message: "Auth token:"
10204
9742
  });
10205
9743
  if (!url || !token) {
10206
9744
  console.error("Both URL and token are required.");
10207
9745
  process.exit(1);
10208
9746
  }
10209
- setTursoCredentials(url, token, void 0, mode);
9747
+ setTursoCredentials(url, token, void 0, effectiveMode);
10210
9748
  db = await openDatabaseWithSync({ initialize: true });
10211
9749
  await db.prepare("SELECT 1").get();
10212
9750
  await db.close();
10213
9751
  console.log(
10214
- `Turso cloud database configured and verified: ${url}` + (mode ? ` (mode: ${mode})` : "")
9752
+ `Turso cloud database configured and verified: ${url}` + (effectiveMode ? ` (mode: ${effectiveMode})` : "")
10215
9753
  );
10216
9754
  } catch (err) {
10217
9755
  await db?.close();
@@ -10226,26 +9764,26 @@ async function setupTurso(urlArg, tokenArg, mode) {
10226
9764
 
10227
9765
  // src/cli/commands/git-sync.ts
10228
9766
  import { execSync as execSync5 } from "child_process";
10229
- import { chmodSync as chmodSync2, existsSync as existsSync18, writeFileSync as writeFileSync9 } from "fs";
10230
- import { join as join18 } from "path";
10231
- import { Command as Command8 } from "commander";
9767
+ import { chmodSync as chmodSync2, existsSync as existsSync18, writeFileSync as writeFileSync8 } from "fs";
9768
+ import { join as join19 } from "path";
9769
+ import { Command as Command5 } from "commander";
10232
9770
  function installHook2() {
10233
- const gitDir = join18(process.cwd(), ".git");
9771
+ const gitDir = join19(process.cwd(), ".git");
10234
9772
  if (!existsSync18(gitDir)) {
10235
9773
  console.error(
10236
9774
  "Error: Current directory is not the root of a Git repository."
10237
9775
  );
10238
9776
  process.exit(1);
10239
9777
  }
10240
- const hooksDir = join18(gitDir, "hooks");
10241
- const hookPath = join18(hooksDir, "post-commit");
9778
+ const hooksDir = join19(gitDir, "hooks");
9779
+ const hookPath = join19(hooksDir, "post-commit");
10242
9780
  const hookContent = `#!/bin/sh
10243
9781
  # ZAM Spaced Repetition Auto-Stale Hook
10244
9782
  # Triggered automatically on git commits to decay modified concept cards.
10245
9783
  zam git-sync --commit HEAD --quiet
10246
9784
  `;
10247
9785
  try {
10248
- writeFileSync9(hookPath, hookContent, { encoding: "utf-8", flag: "w" });
9786
+ writeFileSync8(hookPath, hookContent, { encoding: "utf-8", flag: "w" });
10249
9787
  try {
10250
9788
  chmodSync2(hookPath, "755");
10251
9789
  } catch (_e) {
@@ -10258,7 +9796,7 @@ zam git-sync --commit HEAD --quiet
10258
9796
  process.exit(1);
10259
9797
  }
10260
9798
  }
10261
- var gitSyncCommand = new Command8("git-sync").description("Sync learning cards with recent Git file modifications").option("--commit <hash>", "Git commit hash to check", "HEAD").option("--user <id>", "User ID (default: whoami)").option("--install", "Install git post-commit hook in current repo").option("--quiet", "Suppress verbose output").action(async (opts) => {
9799
+ var gitSyncCommand = new Command5("git-sync").description("Sync learning cards with recent Git file modifications").option("--commit <hash>", "Git commit hash to check", "HEAD").option("--user <id>", "User ID (default: whoami)").option("--install", "Install git post-commit hook in current repo").option("--quiet", "Suppress verbose output").action(async (opts) => {
10262
9800
  if (opts.install) {
10263
9801
  installHook2();
10264
9802
  return;
@@ -10348,9 +9886,9 @@ ZAM Auto-Stale Complete: Scanned ${changedFiles.length} file(s).`
10348
9886
 
10349
9887
  // src/cli/commands/goal.ts
10350
9888
  import { existsSync as existsSync19, mkdirSync as mkdirSync11 } from "fs";
10351
- import { resolve as resolve6 } from "path";
10352
- import { input as input3 } from "@inquirer/prompts";
10353
- import { Command as Command9 } from "commander";
9889
+ import { resolve as resolve5 } from "path";
9890
+ import { input as input2 } from "@inquirer/prompts";
9891
+ import { Command as Command6 } from "commander";
10354
9892
  async function resolveGoalsDir() {
10355
9893
  let goalsDir;
10356
9894
  let db;
@@ -10361,9 +9899,9 @@ async function resolveGoalsDir() {
10361
9899
  } finally {
10362
9900
  await db?.close();
10363
9901
  }
10364
- return goalsDir ? resolve6(goalsDir) : resolve6("goals");
9902
+ return goalsDir ? resolve5(goalsDir) : resolve5("goals");
10365
9903
  }
10366
- var goalCommand = new Command9("goal").description(
9904
+ var goalCommand = new Command6("goal").description(
10367
9905
  "Manage learning goals (markdown files)"
10368
9906
  );
10369
9907
  goalCommand.command("list").description("List all goals").option(
@@ -10482,11 +10020,11 @@ goalCommand.command("create").description("Create a new goal").option("--slug <s
10482
10020
  if (!slug || !title) {
10483
10021
  try {
10484
10022
  if (!title) {
10485
- title = await input3({ message: "Goal title:" });
10023
+ title = await input2({ message: "Goal title:" });
10486
10024
  }
10487
10025
  if (!slug) {
10488
10026
  const suggested = title.toLowerCase().replace(/[^a-z0-9]+/g, "-").replace(/^-|-$/g, "");
10489
- slug = await input3({
10027
+ slug = await input2({
10490
10028
  message: "Goal slug (filename):",
10491
10029
  default: suggested
10492
10030
  });
@@ -10532,22 +10070,22 @@ goalCommand.command("status <slug> <status>").description("Update a goal's statu
10532
10070
  });
10533
10071
 
10534
10072
  // src/cli/commands/init.ts
10535
- import { existsSync as existsSync20, mkdirSync as mkdirSync12, writeFileSync as writeFileSync10 } from "fs";
10073
+ import { existsSync as existsSync20, mkdirSync as mkdirSync12, writeFileSync as writeFileSync9 } from "fs";
10536
10074
  import { homedir as homedir11 } from "os";
10537
- import { join as join19, resolve as resolve7 } from "path";
10538
- import { confirm as confirm2, input as input4 } from "@inquirer/prompts";
10539
- import { Command as Command10 } from "commander";
10075
+ import { join as join20, resolve as resolve6 } from "path";
10076
+ import { confirm, input as input3 } from "@inquirer/prompts";
10077
+ import { Command as Command7 } from "commander";
10540
10078
  var HOME2 = homedir11();
10541
10079
  function printLine(char = "\u2550", len = 60, color = "\x1B[36m") {
10542
10080
  console.log(`${color}${char.repeat(len)}\x1B[0m`);
10543
10081
  }
10544
10082
  function bootstrapSandboxWorkspace(workspaceDir) {
10545
- mkdirSync12(join19(workspaceDir, "beliefs"), { recursive: true });
10546
- mkdirSync12(join19(workspaceDir, "goals"), { recursive: true });
10547
- mkdirSync12(join19(workspaceDir, "skills"), { recursive: true });
10548
- const worldviewFile = join19(workspaceDir, "beliefs", "worldview.md");
10083
+ mkdirSync12(join20(workspaceDir, "beliefs"), { recursive: true });
10084
+ mkdirSync12(join20(workspaceDir, "goals"), { recursive: true });
10085
+ mkdirSync12(join20(workspaceDir, "skills"), { recursive: true });
10086
+ const worldviewFile = join20(workspaceDir, "beliefs", "worldview.md");
10549
10087
  if (!existsSync20(worldviewFile)) {
10550
- writeFileSync10(
10088
+ writeFileSync9(
10551
10089
  worldviewFile,
10552
10090
  `# Personal Worldview
10553
10091
 
@@ -10559,9 +10097,9 @@ Here, I declare the core concepts and principles I want to master.
10559
10097
  "utf8"
10560
10098
  );
10561
10099
  }
10562
- const goalsFile = join19(workspaceDir, "goals", "goals.md");
10100
+ const goalsFile = join20(workspaceDir, "goals", "goals.md");
10563
10101
  if (!existsSync20(goalsFile)) {
10564
- writeFileSync10(
10102
+ writeFileSync9(
10565
10103
  goalsFile,
10566
10104
  `# Personal Goals
10567
10105
 
@@ -10573,7 +10111,7 @@ Here, I declare the core concepts and principles I want to master.
10573
10111
  );
10574
10112
  }
10575
10113
  }
10576
- var initCommand = new Command10("init").description("Launch the guided interactive onboarding wizard").action(async () => {
10114
+ var initCommand = new Command7("init").description("Launch the guided interactive onboarding wizard").action(async () => {
10577
10115
  printLine();
10578
10116
  console.log(
10579
10117
  "\x1B[1m\x1B[32m ZAM \u2014 The Symbiotic Learning Agent Onboarding\x1B[0m"
@@ -10583,9 +10121,9 @@ var initCommand = new Command10("init").description("Launch the guided interacti
10583
10121
  );
10584
10122
  printLine();
10585
10123
  console.log("\n\x1B[1m[1/5] Setting up Local Workspace Sandbox\x1B[0m");
10586
- const defaultWorkspace = join19(HOME2, "Documents", "zam");
10587
- const workspacePath = resolve7(
10588
- await input4({
10124
+ const defaultWorkspace = join20(HOME2, "Documents", "zam");
10125
+ const workspacePath = resolve6(
10126
+ await input3({
10589
10127
  message: "Choose your ZAM workspace directory:",
10590
10128
  default: defaultWorkspace
10591
10129
  })
@@ -10599,6 +10137,7 @@ var initCommand = new Command10("init").description("Launch the guided interacti
10599
10137
  kind: "personal",
10600
10138
  path: workspacePath
10601
10139
  });
10140
+ setActiveWorkspaceId("personal");
10602
10141
  console.log(
10603
10142
  `\x1B[32m\u2713 Local Sandbox created at: ${workspacePath}\x1B[0m`
10604
10143
  );
@@ -10629,7 +10168,7 @@ var initCommand = new Command10("init").description("Launch the guided interacti
10629
10168
  \x1B[1mRecommendation:\x1B[0m ZAM suggests installing \x1B[32m${runnerLabel}\x1B[0m with \x1B[36m${profile.recommendedModel}\x1B[0m.`
10630
10169
  );
10631
10170
  console.log("\n\x1B[1m[3/5] Setting up Local LLM Runner\x1B[0m");
10632
- const proceedInstall = await confirm2({
10171
+ const proceedInstall = await confirm({
10633
10172
  message: `Would you like ZAM to install and configure ${runnerLabel} automatically?`,
10634
10173
  default: true
10635
10174
  });
@@ -10666,7 +10205,7 @@ var initCommand = new Command10("init").description("Launch the guided interacti
10666
10205
  let db;
10667
10206
  try {
10668
10207
  db = await openDatabaseWithSync({ initialize: true });
10669
- await setSetting(db, "personal.workspace_dir", workspacePath);
10208
+ await deleteSetting(db, "personal.workspace_dir");
10670
10209
  const detectedLocale = detectSystemLocale();
10671
10210
  await setSetting(db, "system.locale", detectedLocale);
10672
10211
  console.log(
@@ -10697,7 +10236,7 @@ var initCommand = new Command10("init").description("Launch the guided interacti
10697
10236
  console.log(
10698
10237
  "\n\x1B[1m[5/5] Wiring Developer Agents & Terminal Helpers\x1B[0m"
10699
10238
  );
10700
- const proceedHooks = await confirm2({
10239
+ const proceedHooks = await confirm({
10701
10240
  message: "Distribute ZAM skills and install optional monitored-session shell helpers?",
10702
10241
  default: true
10703
10242
  });
@@ -10748,8 +10287,8 @@ var initCommand = new Command10("init").description("Launch the guided interacti
10748
10287
  });
10749
10288
 
10750
10289
  // src/cli/commands/learn.ts
10751
- import { input as input6 } from "@inquirer/prompts";
10752
- import { Command as Command11 } from "commander";
10290
+ import { input as input5 } from "@inquirer/prompts";
10291
+ import { Command as Command8 } from "commander";
10753
10292
 
10754
10293
  // src/cli/learn-format.ts
10755
10294
  var BLOOM_VERBS3 = {
@@ -10798,7 +10337,7 @@ function formatReveal(input8) {
10798
10337
  }
10799
10338
 
10800
10339
  // src/cli/review-actions.ts
10801
- import { confirm as confirm3, input as input5, select } from "@inquirer/prompts";
10340
+ import { confirm as confirm2, input as input4, select } from "@inquirer/prompts";
10802
10341
  async function runInteractiveReviewAction(inputData) {
10803
10342
  let currentItem = { ...inputData.item };
10804
10343
  while (true) {
@@ -10913,7 +10452,7 @@ async function runInteractiveReviewAction(inputData) {
10913
10452
  continue;
10914
10453
  }
10915
10454
  if (choice === "deprecate-token") {
10916
- const approved = await confirm3({
10455
+ const approved = await confirm2({
10917
10456
  message: `Deprecate ${currentItem.slug} so it stops appearing in review queues?`,
10918
10457
  default: false
10919
10458
  });
@@ -10944,7 +10483,7 @@ async function runInteractiveReviewAction(inputData) {
10944
10483
  console.log(` Session steps: ${impact.session_steps}`);
10945
10484
  console.log(` Sessions touched: ${impact.sessions_touched}`);
10946
10485
  console.log(` Agent skills updated: ${impact.agent_skills}`);
10947
- const approved = await confirm3({
10486
+ const approved = await confirm2({
10948
10487
  message: "Permanently delete this token and its dependent learning data?",
10949
10488
  default: false
10950
10489
  });
@@ -10969,7 +10508,7 @@ async function runInteractiveReviewAction(inputData) {
10969
10508
  );
10970
10509
  console.log(`Delete your card for ${currentItem.slug}?`);
10971
10510
  console.log(` Review logs removed: ${impact.review_logs}`);
10972
- const approved = await confirm3({
10511
+ const approved = await confirm2({
10973
10512
  message: "Delete only your card for this token?",
10974
10513
  default: false
10975
10514
  });
@@ -11007,21 +10546,21 @@ async function promptTokenEdit(token) {
11007
10546
  }
11008
10547
  switch (field) {
11009
10548
  case "concept": {
11010
- const concept = await input5({
10549
+ const concept = await input4({
11011
10550
  message: "Concept:",
11012
10551
  default: token.concept
11013
10552
  });
11014
10553
  return concept === token.concept ? null : { concept };
11015
10554
  }
11016
10555
  case "domain": {
11017
- const domain = await input5({
10556
+ const domain = await input4({
11018
10557
  message: "Domain (blank allowed):",
11019
10558
  default: token.domain
11020
10559
  });
11021
10560
  return domain === token.domain ? null : { domain };
11022
10561
  }
11023
10562
  case "context": {
11024
- const context = await input5({
10563
+ const context = await input4({
11025
10564
  message: "Context (blank allowed):",
11026
10565
  default: token.context
11027
10566
  });
@@ -11062,14 +10601,14 @@ async function promptTokenEdit(token) {
11062
10601
  return mode === token.symbiosis_mode ? null : { symbiosis_mode: mode };
11063
10602
  }
11064
10603
  case "source_link": {
11065
- const link = await input5({
10604
+ const link = await input4({
11066
10605
  message: "Source link (blank allowed):",
11067
10606
  default: token.source_link ?? ""
11068
10607
  });
11069
10608
  return link === token.source_link ? null : { source_link: link || null };
11070
10609
  }
11071
10610
  case "question": {
11072
- const question = await input5({
10611
+ const question = await input4({
11073
10612
  message: "Recall question (blank allowed):",
11074
10613
  default: token.question ?? ""
11075
10614
  });
@@ -11087,7 +10626,7 @@ var STOP_WORDS = /* @__PURE__ */ new Set(["q", ":q", "quit", "stop"]);
11087
10626
  function isExitPrompt(err) {
11088
10627
  return err instanceof Error && err.name === "ExitPromptError";
11089
10628
  }
11090
- var learnCommand = new Command11("learn").description(
10629
+ var learnCommand = new Command8("learn").description(
11091
10630
  "Run a spoiler-free, in-process learning session (recall \u2192 reveal \u2192 self-rate)"
11092
10631
  ).option("--user <id>", "User ID (default: whoami)").option("--max-new <n>", "Maximum new cards", "10").option("--max-reviews <n>", "Maximum review cards", "50").option("--no-resolve", "Skip resolving source_link into the revealed answer").action(async (opts) => {
11093
10632
  let db;
@@ -11167,7 +10706,7 @@ ${"\u2500".repeat(50)}`);
11167
10706
  ${prompt.question}`);
11168
10707
  let answer;
11169
10708
  try {
11170
- answer = await input6({
10709
+ answer = await input5({
11171
10710
  message: t(locale, "prompt_answer")
11172
10711
  });
11173
10712
  } catch (err) {
@@ -11332,8 +10871,8 @@ learnCommand.command("open").description("Open a new terminal window running zam
11332
10871
  });
11333
10872
 
11334
10873
  // src/cli/commands/monitor.ts
11335
- import { Command as Command12 } from "commander";
11336
- var monitorCommand = new Command12("monitor").description(
10874
+ import { Command as Command9 } from "commander";
10875
+ var monitorCommand = new Command9("monitor").description(
11337
10876
  "Shell observation for real-time task monitoring"
11338
10877
  );
11339
10878
  monitorCommand.command("start").description("Output shell hook code to install monitoring").requiredOption("--session <id>", "Session ID to monitor").option(
@@ -11515,8 +11054,8 @@ monitorCommand.command("open").description("Open a new monitored terminal window
11515
11054
  });
11516
11055
 
11517
11056
  // src/cli/commands/observer.ts
11518
- import { Command as Command13 } from "commander";
11519
- var observerCommand = new Command13("observer").description(
11057
+ import { Command as Command10 } from "commander";
11058
+ var observerCommand = new Command10("observer").description(
11520
11059
  "Configure what the UI observer may capture (Layer 2 policy)"
11521
11060
  );
11522
11061
  function applyObserverListChange(current, entry, op) {
@@ -11537,107 +11076,343 @@ observerCommand.command("status").description("Show the active observer policy")
11537
11076
  console.log(` Consent: ${policy.consent}`);
11538
11077
  console.log(` Retention: ${policy.retention}`);
11539
11078
  console.log(
11540
- ` Allowlist: ${policy.allowlist.length ? policy.allowlist.join(", ") : "(any targeted window)"}`
11079
+ ` Allowlist: ${policy.allowlist.length ? policy.allowlist.join(", ") : "(any targeted window)"}`
11080
+ );
11081
+ console.log(
11082
+ ` Denylist: ${policy.denylist.length ? policy.denylist.join(", ") : "(none)"}`
11083
+ );
11084
+ console.log(` Redact titles: ${policy.redactWindowTitles}`);
11085
+ console.log(
11086
+ "\nBuilt-in sensitive surfaces (password managers, auth/UAC dialogs, banking) are always refused and cannot be allowlisted."
11087
+ );
11088
+ });
11089
+ });
11090
+ observerCommand.command("grant <process>").description(
11091
+ "Allow the observer to capture a process (adds it to observer.allowlist)"
11092
+ ).action(async (processName) => {
11093
+ await withDb(async (db) => {
11094
+ const next = applyObserverListChange(
11095
+ await getSetting(db, "observer.allowlist"),
11096
+ processName,
11097
+ "add"
11098
+ );
11099
+ await setSetting(db, "observer.allowlist", next);
11100
+ await syncObserverSidecarPolicy(db);
11101
+ console.log(`Granted: ${processName.trim().toLowerCase()}`);
11102
+ console.log(`observer.allowlist = ${next || "(empty)"}`);
11103
+ });
11104
+ });
11105
+ observerCommand.command("revoke <process>").description("Remove a process from observer.allowlist").action(async (processName) => {
11106
+ await withDb(async (db) => {
11107
+ const next = applyObserverListChange(
11108
+ await getSetting(db, "observer.allowlist"),
11109
+ processName,
11110
+ "remove"
11111
+ );
11112
+ await setSetting(db, "observer.allowlist", next);
11113
+ await syncObserverSidecarPolicy(db);
11114
+ console.log(`Revoked: ${processName.trim().toLowerCase()}`);
11115
+ console.log(`observer.allowlist = ${next || "(empty)"}`);
11116
+ });
11117
+ });
11118
+
11119
+ // src/cli/commands/profile.ts
11120
+ import { dirname as dirname6, resolve as resolve7 } from "path";
11121
+ import { Command as Command11 } from "commander";
11122
+ var C2 = {
11123
+ reset: "\x1B[0m",
11124
+ bold: "\x1B[1m",
11125
+ cyan: "\x1B[36m",
11126
+ dim: "\x1B[2m",
11127
+ green: "\x1B[32m"
11128
+ };
11129
+ function render(profile) {
11130
+ const sync = profile.syncProvider ? `${C2.green}${profile.syncProvider}${C2.reset} ${C2.dim}(good for cross-device snapshots)${C2.reset}` : `${C2.dim}local folder (use a synced folder or snapshots to move between machines)${C2.reset}`;
11131
+ console.log(`${C2.bold}ZAM install profile${C2.reset}`);
11132
+ console.log(` mode: ${C2.cyan}${profile.mode}${C2.reset}`);
11133
+ console.log(` personal dir: ${C2.cyan}${profile.personalDir}${C2.reset}`);
11134
+ console.log(` sync: ${sync}`);
11135
+ console.log(` data dir: ${C2.cyan}${profile.dataDir}${C2.reset}`);
11136
+ console.log(` database: ${C2.cyan}${profile.dbPath}${C2.reset}`);
11137
+ }
11138
+ var profileCommand = new Command11("profile").description("Show or change this machine's ZAM install profile").option("--mode <mode>", "Set install mode: developer | default").option("--dir <path>", "Set the personal-content folder").option("--json", "Output as JSON").action(async (opts) => {
11139
+ if (opts.mode && opts.mode !== "developer" && opts.mode !== "default") {
11140
+ console.error(`Invalid --mode: ${opts.mode}. Use developer or default.`);
11141
+ process.exit(1);
11142
+ }
11143
+ let db;
11144
+ try {
11145
+ if (opts.mode) setInstallMode(opts.mode);
11146
+ db = await openDatabaseWithSync({ initialize: true });
11147
+ if (opts.dir) {
11148
+ await activateWorkspacePath(db, resolve7(opts.dir), {
11149
+ kind: "personal",
11150
+ label: "Personal"
11151
+ });
11152
+ }
11153
+ const personalDir = (await ensureActiveWorkspace(db)).path;
11154
+ await db.close();
11155
+ db = void 0;
11156
+ const dbPath = getDefaultDbPath();
11157
+ const profile = {
11158
+ mode: getInstallMode(),
11159
+ personalDir,
11160
+ syncProvider: detectSyncProvider(personalDir),
11161
+ dataDir: dirname6(dbPath),
11162
+ dbPath
11163
+ };
11164
+ if (opts.json) {
11165
+ console.log(JSON.stringify(profile, null, 2));
11166
+ return;
11167
+ }
11168
+ render(profile);
11169
+ } catch (err) {
11170
+ await db?.close();
11171
+ console.error("Error:", err.message);
11172
+ process.exit(1);
11173
+ }
11174
+ });
11175
+
11176
+ // src/cli/commands/provider.ts
11177
+ import { password as password2 } from "@inquirer/prompts";
11178
+ import { Command as Command12 } from "commander";
11179
+ var providerCommand = new Command12("provider").description(
11180
+ "Configure role-based AI providers (url/model/flavor/key, per role)"
11181
+ );
11182
+ providerCommand.command("list").description("Show configured providers, role bindings, and key status").option("--json", "Output as JSON").option("--machine", "Read machine-local providers from ~/.zam/config.json").action(async (opts) => {
11183
+ const machine = Boolean(opts.machine);
11184
+ await withProviderScope(machine, async (db) => {
11185
+ const providers = await readScopedProviders(db, machine);
11186
+ const roles = await readScopedRoles(db, machine);
11187
+ const rows = buildProviderListing(
11188
+ providers,
11189
+ (ref) => getProviderApiKey(ref) !== null
11190
+ );
11191
+ const orphans = findOrphanKeyRefs(listProviderApiKeyRefs(), providers);
11192
+ if (opts.json) {
11193
+ console.log(
11194
+ JSON.stringify(
11195
+ {
11196
+ scope: machine ? "machine" : "shared",
11197
+ providers: rows,
11198
+ roles,
11199
+ orphans
11200
+ },
11201
+ null,
11202
+ 2
11203
+ )
11204
+ );
11205
+ return;
11206
+ }
11207
+ console.log(
11208
+ `Providers (${opts.machine ? "~/.zam/config.json ai.providers" : "llm.providers"}):
11209
+ `
11210
+ );
11211
+ if (rows.length === 0) {
11212
+ console.log(
11213
+ " (none) \u2014 add one: zam provider add <name> --url <url> --model <model>"
11214
+ );
11215
+ } else {
11216
+ for (const row of rows) {
11217
+ const key = row.keyState === "set" ? "\x1B[32mkey \u2713\x1B[0m" : row.keyState === "missing" ? `\x1B[31mkey \u2717 (set: zam provider set-key ${row.apiKeyRef})\x1B[0m` : "\x1B[90mno key\x1B[0m";
11218
+ console.log(` \x1B[36m${row.name.padEnd(12)}\x1B[0m ${key}`);
11219
+ console.log(` url: ${row.url ?? "(inherits llm.url)"}`);
11220
+ console.log(` model: ${row.model ?? "(inherits llm.model)"}`);
11221
+ console.log(` flavor: ${row.apiFlavor}`);
11222
+ if (row.label) console.log(` label: ${row.label}`);
11223
+ if (row.local !== void 0) {
11224
+ console.log(` local: ${row.local ? "yes" : "no"}`);
11225
+ }
11226
+ if (row.runner) console.log(` runner: ${row.runner}`);
11227
+ if (row.apiKeyRef) console.log(` key-ref: ${row.apiKeyRef}`);
11228
+ }
11229
+ }
11230
+ console.log("\nRoles (llm.roles):\n");
11231
+ for (const role of VALID_ROLES) {
11232
+ const binding = roles[role];
11233
+ if (!binding?.primary) {
11234
+ console.log(` ${role.padEnd(7)} \x1B[90m(unset)\x1B[0m`);
11235
+ } else {
11236
+ const fb = binding.fallback ? ` \u2192 fallback: ${binding.fallback}` : "";
11237
+ console.log(` ${role.padEnd(7)} primary: ${binding.primary}${fb}`);
11238
+ }
11239
+ }
11240
+ if (orphans.length > 0) {
11241
+ console.log(
11242
+ `
11243
+ \x1B[33mOrphan keys (stored but unreferenced):\x1B[0m ${orphans.join(", ")}`
11244
+ );
11245
+ }
11246
+ });
11247
+ });
11248
+ providerCommand.command("add <name>").description("Add or update a provider record in llm.providers").option("--label <label>", "Human-readable provider label").option("--url <url>", "Endpoint base URL (e.g. https://api.deepseek.com/v1)").option("--model <model>", "Model id (e.g. deepseek-v4-flash)").option("--local", "Mark this provider as a local/on-device endpoint").option(
11249
+ "--runner <runner>",
11250
+ "Local runner hint (flm, foundry-local, ollama, ...)"
11251
+ ).option(
11252
+ "--flavor <flavor>",
11253
+ `Wire protocol: ${VALID_API_FLAVORS.join(" | ")} (default: inferred from URL)`
11254
+ ).option(
11255
+ "--key-ref <ref>",
11256
+ "Credential reference for the API key (default: <name> when --key is given)"
11257
+ ).option(
11258
+ "--key <value>",
11259
+ "Store this API key now (prefer `set-key` to keep it out of shell history)"
11260
+ ).option("--machine", "Store this provider in ~/.zam/config.json").action(async (name, opts) => {
11261
+ let apiFlavor;
11262
+ if (opts.flavor) {
11263
+ if (!VALID_API_FLAVORS.includes(opts.flavor)) {
11264
+ console.error(
11265
+ `Invalid --flavor: ${opts.flavor}. Use ${VALID_API_FLAVORS.join(" or ")}.`
11266
+ );
11267
+ process.exit(1);
11268
+ }
11269
+ apiFlavor = opts.flavor;
11270
+ }
11271
+ const apiKeyRef = opts.keyRef ?? (opts.key ? name : void 0);
11272
+ const machine = Boolean(opts.machine);
11273
+ await withProviderScope(machine, async (db) => {
11274
+ const providers = await readScopedProviders(db, machine);
11275
+ const next = upsertProviderRecord(providers, name, {
11276
+ label: opts.label,
11277
+ url: opts.url,
11278
+ model: opts.model,
11279
+ apiFlavor,
11280
+ apiKeyRef,
11281
+ local: opts.local ? true : void 0,
11282
+ runner: opts.runner
11283
+ });
11284
+ await writeScopedProviders(db, machine, next);
11285
+ if (opts.key && apiKeyRef) setProviderApiKey(apiKeyRef, opts.key);
11286
+ const rec = next[name];
11287
+ console.log(
11288
+ `Provider "${name}" saved (${machine ? "machine-local" : "shared"}):`
11541
11289
  );
11290
+ console.log(` url: ${rec.url ?? "(inherits llm.url)"}`);
11291
+ console.log(` model: ${rec.model ?? "(inherits llm.model)"}`);
11292
+ if (rec.label) console.log(` label: ${rec.label}`);
11293
+ if (rec.local !== void 0) {
11294
+ console.log(` local: ${rec.local ? "yes" : "no"}`);
11295
+ }
11296
+ if (rec.runner) console.log(` runner: ${rec.runner}`);
11542
11297
  console.log(
11543
- ` Denylist: ${policy.denylist.length ? policy.denylist.join(", ") : "(none)"}`
11298
+ ` flavor: ${rec.apiFlavor ?? `${rec.url ? inferApiFlavor(rec.url) : "chat-completions"} (inferred)`}`
11544
11299
  );
11545
- console.log(` Redact titles: ${policy.redactWindowTitles}`);
11300
+ console.log(` key-ref: ${rec.apiKeyRef ?? "(none \u2014 uses default key)"}`);
11301
+ if (opts.key) {
11302
+ console.log(` key: stored (${maskSecret(opts.key)})`);
11303
+ } else if (rec.apiKeyRef && getProviderApiKey(rec.apiKeyRef) === null) {
11304
+ console.log(
11305
+ `
11306
+ \u26A0 No key stored for "${rec.apiKeyRef}". Run: zam provider set-key ${rec.apiKeyRef}`
11307
+ );
11308
+ }
11309
+ if (!rec.url) {
11310
+ console.log(
11311
+ "\n \u26A0 No --url set; this provider inherits the base llm.url."
11312
+ );
11313
+ }
11546
11314
  console.log(
11547
- "\nBuilt-in sensitive surfaces (password managers, auth/UAC dialogs, banking) are always refused and cannot be allowlisted."
11315
+ `
11316
+ Bind it to a role: zam provider use recall --primary ${name}`
11548
11317
  );
11549
11318
  });
11550
11319
  });
11551
- observerCommand.command("grant <process>").description(
11552
- "Allow the observer to capture a process (adds it to observer.allowlist)"
11553
- ).action(async (processName) => {
11554
- await withDb(async (db) => {
11555
- const next = applyObserverListChange(
11556
- await getSetting(db, "observer.allowlist"),
11557
- processName,
11558
- "add"
11320
+ providerCommand.command("remove <name>").description("Remove a provider record from llm.providers").option("--machine", "Remove from ~/.zam/config.json instead of the DB").action(async (name, opts) => {
11321
+ const machine = Boolean(opts.machine);
11322
+ await withProviderScope(machine, async (db) => {
11323
+ const providers = await readScopedProviders(db, machine);
11324
+ const { providers: next, removed } = removeProviderRecord(
11325
+ providers,
11326
+ name
11559
11327
  );
11560
- await setSetting(db, "observer.allowlist", next);
11561
- await syncObserverSidecarPolicy(db);
11562
- console.log(`Granted: ${processName.trim().toLowerCase()}`);
11563
- console.log(`observer.allowlist = ${next || "(empty)"}`);
11564
- });
11565
- });
11566
- observerCommand.command("revoke <process>").description("Remove a process from observer.allowlist").action(async (processName) => {
11567
- await withDb(async (db) => {
11568
- const next = applyObserverListChange(
11569
- await getSetting(db, "observer.allowlist"),
11570
- processName,
11571
- "remove"
11328
+ if (!removed) {
11329
+ console.log(`No such provider: ${name}`);
11330
+ return;
11331
+ }
11332
+ await writeScopedProviders(db, machine, next);
11333
+ console.log(
11334
+ `Removed provider "${name}" (${machine ? "machine-local" : "shared"}).`
11572
11335
  );
11573
- await setSetting(db, "observer.allowlist", next);
11574
- await syncObserverSidecarPolicy(db);
11575
- console.log(`Revoked: ${processName.trim().toLowerCase()}`);
11576
- console.log(`observer.allowlist = ${next || "(empty)"}`);
11336
+ const referencing = rolesReferencing(
11337
+ await readScopedRoles(db, machine),
11338
+ name
11339
+ );
11340
+ if (referencing.length > 0) {
11341
+ console.log(
11342
+ ` \u26A0 Still referenced by role(s): ${referencing.join(", ")} \u2014 rebind with: zam provider use <role> --primary <name>`
11343
+ );
11344
+ }
11577
11345
  });
11578
11346
  });
11579
-
11580
- // src/cli/commands/profile.ts
11581
- import { homedir as homedir12 } from "os";
11582
- import { dirname as dirname6, join as join20, resolve as resolve8 } from "path";
11583
- import { Command as Command14 } from "commander";
11584
- var C2 = {
11585
- reset: "\x1B[0m",
11586
- bold: "\x1B[1m",
11587
- cyan: "\x1B[36m",
11588
- dim: "\x1B[2m",
11589
- green: "\x1B[32m"
11590
- };
11591
- function defaultPersonalDir() {
11592
- return join20(homedir12(), "Documents", "zam");
11593
- }
11594
- function render(profile) {
11595
- const sync = profile.syncProvider ? `${C2.green}${profile.syncProvider}${C2.reset} ${C2.dim}(good for cross-device snapshots)${C2.reset}` : `${C2.dim}local folder (use a synced folder or snapshots to move between machines)${C2.reset}`;
11596
- console.log(`${C2.bold}ZAM install profile${C2.reset}`);
11597
- console.log(` mode: ${C2.cyan}${profile.mode}${C2.reset}`);
11598
- console.log(` personal dir: ${C2.cyan}${profile.personalDir}${C2.reset}`);
11599
- console.log(` sync: ${sync}`);
11600
- console.log(` data dir: ${C2.cyan}${profile.dataDir}${C2.reset}`);
11601
- console.log(` database: ${C2.cyan}${profile.dbPath}${C2.reset}`);
11602
- }
11603
- var profileCommand = new Command14("profile").description("Show or change this machine's ZAM install profile").option("--mode <mode>", "Set install mode: developer | default").option("--dir <path>", "Set the personal-content folder").option("--json", "Output as JSON").action(async (opts) => {
11604
- if (opts.mode && opts.mode !== "developer" && opts.mode !== "default") {
11605
- console.error(`Invalid --mode: ${opts.mode}. Use developer or default.`);
11347
+ providerCommand.command("use <role>").description(`Bind providers to a role (${VALID_ROLES.join(" | ")})`).option("--primary <name>", "Primary provider").option("--fallback <name>", "Fallback provider (optional)").option("--machine", "Bind the role in ~/.zam/config.json").action(async (role, opts) => {
11348
+ if (!VALID_ROLES.includes(role)) {
11349
+ console.error(`Invalid role: ${role}. Use ${VALID_ROLES.join(", ")}.`);
11606
11350
  process.exit(1);
11607
11351
  }
11608
- let db;
11609
- try {
11610
- if (opts.mode) setInstallMode(opts.mode);
11611
- db = await openDatabaseWithSync({ initialize: true });
11612
- if (opts.dir) {
11613
- await setSetting(db, "personal.workspace_dir", resolve8(opts.dir));
11352
+ if (!opts.primary) {
11353
+ console.error("--primary is required.");
11354
+ process.exit(1);
11355
+ }
11356
+ const machine = Boolean(opts.machine);
11357
+ await withProviderScope(machine, async (db) => {
11358
+ const providers = await readScopedProviders(db, machine);
11359
+ await writeScopedRoles(
11360
+ db,
11361
+ machine,
11362
+ bindRoleProviders(
11363
+ await readScopedRoles(db, machine),
11364
+ role,
11365
+ opts.primary,
11366
+ opts.fallback
11367
+ )
11368
+ );
11369
+ const fb = opts.fallback ? `, fallback: ${opts.fallback}` : "";
11370
+ console.log(
11371
+ `Role "${role}" \u2192 primary: ${opts.primary}${fb} (${machine ? "machine-local" : "shared"})`
11372
+ );
11373
+ for (const [label, ref] of [
11374
+ ["primary", opts.primary],
11375
+ ["fallback", opts.fallback]
11376
+ ]) {
11377
+ if (ref && !(ref in providers)) {
11378
+ console.log(
11379
+ ` \u26A0 ${label} "${ref}" is not a defined provider yet. Add it: zam provider add ${ref} --url <url> --model <model>`
11380
+ );
11381
+ }
11614
11382
  }
11615
- const personalDir = await getSetting(db, "personal.workspace_dir") || defaultPersonalDir();
11616
- await db.close();
11617
- db = void 0;
11618
- const dbPath = getDefaultDbPath();
11619
- const profile = {
11620
- mode: getInstallMode(),
11621
- personalDir,
11622
- syncProvider: detectSyncProvider(personalDir),
11623
- dataDir: dirname6(dbPath),
11624
- dbPath
11625
- };
11626
- if (opts.json) {
11627
- console.log(JSON.stringify(profile, null, 2));
11628
- return;
11383
+ });
11384
+ });
11385
+ providerCommand.command("set-key <ref>").description(
11386
+ "Store an API key for a provider reference (in credentials.json)"
11387
+ ).option(
11388
+ "--key <value>",
11389
+ "The API key (omit to enter it interactively, hidden)"
11390
+ ).action(async (ref, opts) => {
11391
+ try {
11392
+ const key = opts.key ?? await password2({ message: `API key for "${ref}":` });
11393
+ if (!key || key.trim().length === 0) {
11394
+ console.error("No key provided.");
11395
+ process.exit(1);
11629
11396
  }
11630
- render(profile);
11397
+ setProviderApiKey(ref, key.trim());
11398
+ console.log(`Stored API key for "${ref}" (${maskSecret(key.trim())}).`);
11631
11399
  } catch (err) {
11632
- await db?.close();
11400
+ if (err.name === "ExitPromptError") {
11401
+ console.log("\nCancelled.");
11402
+ process.exit(0);
11403
+ }
11633
11404
  console.error("Error:", err.message);
11634
11405
  process.exit(1);
11635
11406
  }
11636
11407
  });
11408
+ providerCommand.command("clear-key <ref>").description("Remove a stored API key").action((ref) => {
11409
+ clearProviderApiKey(ref);
11410
+ console.log(`Cleared API key for "${ref}".`);
11411
+ });
11637
11412
 
11638
11413
  // src/cli/commands/review.ts
11639
- import { Command as Command15 } from "commander";
11640
- var reviewCommand = new Command15("review").description("Start an interactive review session").option("--user <id>", "User ID (default: whoami)").option("--max-new <n>", "Maximum new cards", "10").option("--max-reviews <n>", "Maximum review cards", "50").option("--no-resolve", "Skip resolving source_link into inline context").action(async (opts) => {
11414
+ import { Command as Command13 } from "commander";
11415
+ var reviewCommand = new Command13("review").description("Start an interactive review session").option("--user <id>", "User ID (default: whoami)").option("--max-new <n>", "Maximum new cards", "10").option("--max-reviews <n>", "Maximum review cards", "50").option("--no-resolve", "Skip resolving source_link into inline context").action(async (opts) => {
11641
11416
  let db;
11642
11417
  try {
11643
11418
  db = await openDatabase();
@@ -11751,9 +11526,9 @@ ${"\u2550".repeat(50)}`);
11751
11526
 
11752
11527
  // src/cli/commands/session.ts
11753
11528
  import { readFileSync as readFileSync12 } from "fs";
11754
- import { input as input7, select as select2 } from "@inquirer/prompts";
11755
- import { Command as Command16 } from "commander";
11756
- var sessionCommand = new Command16("session").description(
11529
+ import { input as input6, select as select2 } from "@inquirer/prompts";
11530
+ import { Command as Command14 } from "commander";
11531
+ var sessionCommand = new Command14("session").description(
11757
11532
  "Manage learning sessions"
11758
11533
  );
11759
11534
  sessionCommand.command("start").description("Start a new learning session (review \u2192 task)").option("--user <id>", "User ID (default: whoami)").option("--task <description>", "Task description (interactive if omitted)").option(
@@ -11930,7 +11705,7 @@ async function selectTask() {
11930
11705
  console.log("No active work items found in Azure DevOps.");
11931
11706
  }
11932
11707
  }
11933
- return input7({ message: "Task description:" });
11708
+ return input6({ message: "Task description:" });
11934
11709
  }
11935
11710
  var RATING_LABELS = {
11936
11711
  1: "Again",
@@ -12133,8 +11908,8 @@ sessionCommand.command("end").description("End a session and show summary").requ
12133
11908
 
12134
11909
  // src/cli/commands/settings.ts
12135
11910
  import { existsSync as existsSync21 } from "fs";
12136
- import { Command as Command17 } from "commander";
12137
- var settingsCommand = new Command17("settings").description(
11911
+ import { Command as Command15 } from "commander";
11912
+ var settingsCommand = new Command15("settings").description(
12138
11913
  "Manage user settings"
12139
11914
  );
12140
11915
  var BOOLEAN_SETTING_KEYS = /* @__PURE__ */ new Set(["llm.enabled", "llm.vision.enabled"]);
@@ -12305,12 +12080,108 @@ settingsCommand.command("repos").description("Show or set Personal, Team, and Or
12305
12080
  console.log(` ${name.padEnd(9)}: \x1B[33m- Not Set\x1B[0m`);
12306
12081
  }
12307
12082
  }
12308
- });
12309
- });
12083
+ });
12084
+ });
12085
+
12086
+ // src/cli/commands/setup.ts
12087
+ import { resolve as resolve8 } from "path";
12088
+ import { Command as Command16 } from "commander";
12089
+ function formatDatabaseInitTarget(target) {
12090
+ switch (target.kind) {
12091
+ case "local":
12092
+ return `ZAM database at ${target.location} (local SQLite)`;
12093
+ case "turso-remote":
12094
+ return `ZAM database via Turso remote at ${target.location}`;
12095
+ case "turso-native":
12096
+ return `ZAM database via Turso native driver at ${target.location}`;
12097
+ case "turso-replica":
12098
+ return `ZAM database replica at ${target.location} syncing from ${target.syncUrl}`;
12099
+ }
12100
+ }
12101
+ async function initDatabase(skipInit) {
12102
+ if (skipInit) return;
12103
+ try {
12104
+ const target = getDatabaseTargetInfo();
12105
+ const db = await openDatabaseWithSync({ initialize: true });
12106
+ await activateMachineProviderConfig(db);
12107
+ await db.close();
12108
+ console.log(` init ${formatDatabaseInitTarget(target)}`);
12109
+ } catch (err) {
12110
+ const msg = err.message;
12111
+ if (!msg.includes("already")) {
12112
+ console.warn(` warn database init: ${msg}`);
12113
+ } else {
12114
+ console.log(` skip database already initialized`);
12115
+ }
12116
+ }
12117
+ }
12118
+ async function activateMachineProviderConfig(db) {
12119
+ const machineAi = getMachineAiConfig();
12120
+ const providerCount = Object.keys(machineAi.providers ?? {}).length;
12121
+ const roleCount = Object.keys(machineAi.roles ?? {}).length;
12122
+ if (providerCount === 0 && roleCount === 0) return;
12123
+ if (await getSetting(db, "llm.enabled") !== void 0) return;
12124
+ await setSetting(db, "llm.enabled", "true");
12125
+ console.log(
12126
+ ` activate ${providerCount} machine-local provider(s) from ~/.zam/config.json`
12127
+ );
12128
+ }
12129
+ var setupCommand = new Command16("setup").description(
12130
+ "Link ZAM skill directories into this workspace and initialize the database"
12131
+ ).option("--force", "replace an unmanaged existing ZAM skill directory", false).option("--skip-init", "skip database initialization", false).option("--skip-claude-md", "skip CLAUDE.md generation", false).option("--skip-agents-md", "skip AGENTS.md generation", false).option("--target <path>", "repository/workspace directory to set up").option(
12132
+ "--agents <list>",
12133
+ "comma-separated agents to wire: all, claude, copilot, codex, agent"
12134
+ ).option(
12135
+ "--dry-run",
12136
+ "show what would be written without changing files",
12137
+ false
12138
+ ).action(
12139
+ async (opts) => {
12140
+ let agents;
12141
+ try {
12142
+ agents = parseSetupAgents(opts.agents);
12143
+ } catch (err) {
12144
+ console.error(`Error: ${err.message}`);
12145
+ process.exit(1);
12146
+ }
12147
+ const target = resolve8(opts.target ?? process.cwd());
12148
+ const updateExistingInstructions = Boolean(opts.target) || opts.force;
12149
+ console.log(
12150
+ `Setting up ZAM in ${target}${opts.dryRun ? " (dry run)" : ""}
12151
+ `
12152
+ );
12153
+ wireSkills(target, agents, {
12154
+ force: opts.force,
12155
+ dryRun: opts.dryRun
12156
+ });
12157
+ await initDatabase(opts.skipInit || opts.dryRun);
12158
+ if (agents.has("claude")) {
12159
+ writeClaudeMd(opts.skipClaudeMd, target, {
12160
+ dryRun: opts.dryRun,
12161
+ updateExisting: updateExistingInstructions
12162
+ });
12163
+ }
12164
+ if (agents.has("codex") || agents.has("agent")) {
12165
+ writeAgentsMd(opts.skipAgentsMd, target, {
12166
+ dryRun: opts.dryRun,
12167
+ updateExisting: updateExistingInstructions
12168
+ });
12169
+ }
12170
+ if (agents.has("copilot") && (opts.target || opts.agents)) {
12171
+ writeCopilotInstructions(target, {
12172
+ dryRun: opts.dryRun,
12173
+ updateExisting: updateExistingInstructions
12174
+ });
12175
+ }
12176
+ console.log(
12177
+ "\nDone. Run `zam whoami --set <your-id>` to set your identity. Start the `zam` skill with `/zam` in Claude/Copilot/Gemini-compatible clients or `$zam` (or `/skills`) in Codex."
12178
+ );
12179
+ }
12180
+ );
12310
12181
 
12311
12182
  // src/cli/commands/skill.ts
12312
- import { Command as Command18 } from "commander";
12313
- var skillCommand = new Command18("skill").description(
12183
+ import { Command as Command17 } from "commander";
12184
+ var skillCommand = new Command17("skill").description(
12314
12185
  "Manage agent skill entries (task recipes)"
12315
12186
  );
12316
12187
  skillCommand.command("list").description("List all agent skills").option("--json", "Output as JSON").action(async (opts) => {
@@ -12395,10 +12266,9 @@ skillCommand.command("add").description("Register a new agent skill").requiredOp
12395
12266
  });
12396
12267
 
12397
12268
  // src/cli/commands/snapshot.ts
12398
- import { existsSync as existsSync22, mkdirSync as mkdirSync13, readFileSync as readFileSync13, writeFileSync as writeFileSync11 } from "fs";
12399
- import { homedir as homedir13 } from "os";
12269
+ import { existsSync as existsSync22, mkdirSync as mkdirSync13, readFileSync as readFileSync13, writeFileSync as writeFileSync10 } from "fs";
12400
12270
  import { dirname as dirname7, join as join21 } from "path";
12401
- import { Command as Command19 } from "commander";
12271
+ import { Command as Command18 } from "commander";
12402
12272
  function defaultOutName() {
12403
12273
  const stamp = (/* @__PURE__ */ new Date()).toISOString().replace(/\.\d+Z$/, "").replace(/:/g, "-");
12404
12274
  return `zam-snapshot-${stamp}.sql`;
@@ -12412,12 +12282,12 @@ function summarize(tables) {
12412
12282
  }
12413
12283
  return { total, nonEmpty };
12414
12284
  }
12415
- var exportCmd = new Command19("export").description("Write a portable SQL-text snapshot of the database").option("--out <file>", "Output file (use - for stdout)").action(async (opts) => {
12285
+ var exportCmd = new Command18("export").description("Write a portable SQL-text snapshot of the database").option("--out <file>", "Output file (use - for stdout)").action(async (opts) => {
12416
12286
  let db;
12417
12287
  try {
12418
12288
  db = await openDatabaseWithSync({ initialize: true });
12419
12289
  const snapshot = await exportSnapshot(db);
12420
- const personalDir = await getSetting(db, "personal.workspace_dir") || join21(homedir13(), "Documents", "zam");
12290
+ const personalDir = (await ensureActiveWorkspace(db)).path;
12421
12291
  await db.close();
12422
12292
  db = void 0;
12423
12293
  const manifest = verifySnapshot(snapshot);
@@ -12431,7 +12301,7 @@ var exportCmd = new Command19("export").description("Write a portable SQL-text s
12431
12301
  if (dir && dir !== "." && !existsSync22(dir)) {
12432
12302
  mkdirSync13(dir, { recursive: true });
12433
12303
  }
12434
- writeFileSync11(out, snapshot, "utf-8");
12304
+ writeFileSync10(out, snapshot, "utf-8");
12435
12305
  console.log(`Snapshot written: ${out}`);
12436
12306
  console.log(
12437
12307
  ` ${total} row(s)${nonEmpty.length ? ` \u2014 ${nonEmpty.join(", ")}` : ""}`
@@ -12442,7 +12312,7 @@ var exportCmd = new Command19("export").description("Write a portable SQL-text s
12442
12312
  process.exit(1);
12443
12313
  }
12444
12314
  });
12445
- var importCmd = new Command19("import").description("Restore a snapshot into the database").argument("<file>", "Snapshot file to restore").option("--force", "Overwrite a non-empty database", false).action(async (file, opts) => {
12315
+ var importCmd = new Command18("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) => {
12446
12316
  let db;
12447
12317
  try {
12448
12318
  if (!existsSync22(file)) {
@@ -12465,7 +12335,7 @@ var importCmd = new Command19("import").description("Restore a snapshot into the
12465
12335
  process.exit(1);
12466
12336
  }
12467
12337
  });
12468
- var verifyCmd = new Command19("verify").description("Check a snapshot's manifest and checksum without importing").argument("<file>", "Snapshot file to verify").action((file) => {
12338
+ var verifyCmd = new Command18("verify").description("Check a snapshot's manifest and checksum without importing").argument("<file>", "Snapshot file to verify").action((file) => {
12469
12339
  try {
12470
12340
  if (!existsSync22(file)) {
12471
12341
  console.error(`Error: Snapshot file not found: ${file}`);
@@ -12483,11 +12353,11 @@ var verifyCmd = new Command19("verify").description("Check a snapshot's manifest
12483
12353
  process.exit(1);
12484
12354
  }
12485
12355
  });
12486
- var snapshotCommand = new Command19("snapshot").description("Export, import, or verify a portable database snapshot").addCommand(exportCmd).addCommand(importCmd).addCommand(verifyCmd);
12356
+ var snapshotCommand = new Command18("snapshot").description("Export, import, or verify a portable database snapshot").addCommand(exportCmd).addCommand(importCmd).addCommand(verifyCmd);
12487
12357
 
12488
12358
  // src/cli/commands/stats.ts
12489
- import { Command as Command20 } from "commander";
12490
- var statsCommand = new Command20("stats").description("Show learning dashboard for a user").option("--user <id>", "User ID (default: whoami)").option("--json", "Output as JSON").action(async (opts) => {
12359
+ import { Command as Command19 } from "commander";
12360
+ var statsCommand = new Command19("stats").description("Show learning dashboard for a user").option("--user <id>", "User ID (default: whoami)").option("--json", "Output as JSON").action(async (opts) => {
12491
12361
  await withDb(async (db) => {
12492
12362
  const userId = await resolveUser(opts, db);
12493
12363
  const stats = await getUserStats(db, userId);
@@ -12523,8 +12393,8 @@ var statsCommand = new Command20("stats").description("Show learning dashboard f
12523
12393
  });
12524
12394
 
12525
12395
  // src/cli/commands/token.ts
12526
- import { Command as Command21 } from "commander";
12527
- var tokenCommand = new Command21("token").description(
12396
+ import { Command as Command20 } from "commander";
12397
+ var tokenCommand = new Command20("token").description(
12528
12398
  "Manage knowledge tokens"
12529
12399
  );
12530
12400
  tokenCommand.command("register").description("Register a new knowledge token").requiredOption("--slug <slug>", "Unique token slug").requiredOption("--concept <concept>", "Concept description").option("--domain <domain>", "Knowledge domain", "").option("--bloom <level>", "Bloom taxonomy level (1-5)", "1").option("--source-link <link>", "Source file path or reference URL", "").option("--question <question>", "Specific question prompt for recall", "").option("--json", "Output as JSON").option("--quiet", "Suppress output (exit code only)").action(async (opts) => {
@@ -12812,10 +12682,10 @@ tokenCommand.command("status").description("Show full status of a token for a us
12812
12682
  // src/cli/commands/ui.ts
12813
12683
  import { spawn as spawn3, spawnSync } from "child_process";
12814
12684
  import { existsSync as existsSync23 } from "fs";
12815
- import { homedir as homedir14 } from "os";
12685
+ import { homedir as homedir12 } from "os";
12816
12686
  import { dirname as dirname8, join as join22 } from "path";
12817
12687
  import { fileURLToPath as fileURLToPath3 } from "url";
12818
- import { Command as Command22 } from "commander";
12688
+ import { Command as Command21 } from "commander";
12819
12689
  var C3 = {
12820
12690
  reset: "\x1B[0m",
12821
12691
  red: "\x1B[31m",
@@ -12862,7 +12732,7 @@ function findInstalledApp() {
12862
12732
  process.env.LOCALAPPDATA && join22(process.env.LOCALAPPDATA, "Programs", "ZAM", "ZAM.exe"),
12863
12733
  process.env.ProgramFiles && join22(process.env.ProgramFiles, "ZAM", "ZAM.exe"),
12864
12734
  process.env["ProgramFiles(x86)"] && join22(process.env["ProgramFiles(x86)"], "ZAM", "ZAM.exe")
12865
- ] : process.platform === "darwin" ? ["/Applications/ZAM.app", join22(homedir14(), "Applications", "ZAM.app")] : ["/opt/ZAM/zam", "/usr/bin/zam-desktop"];
12735
+ ] : process.platform === "darwin" ? ["/Applications/ZAM.app", join22(homedir12(), "Applications", "ZAM.app")] : ["/opt/ZAM/zam", "/usr/bin/zam-desktop"];
12866
12736
  return candidates.find((candidate) => candidate && existsSync23(candidate)) || null;
12867
12737
  }
12868
12738
  function runNpm(args, opts) {
@@ -12995,13 +12865,13 @@ function createShortcuts(appPath, repoRoot) {
12995
12865
  console.error(`${C3.red}\u2717 Could not create shortcuts.${C3.reset}`);
12996
12866
  }
12997
12867
  }
12998
- var uiCommand = new Command22("ui").description("Launch the ZAM Desktop GUI (Active-Recall Studio)").option("--dev", "Run in hot-reload development mode (needs Rust)").option(
12868
+ var uiCommand = new Command21("ui").description("Launch the ZAM Desktop GUI (Active-Recall Studio)").option("--dev", "Run in hot-reload development mode (needs Rust)").option(
12999
12869
  "--build",
13000
12870
  "Build the native installer for your OS (needs Rust, one-time)"
13001
12871
  ).option("--shortcut", "Create Desktop + Start-menu shortcuts to the GUI").action((opts) => {
13002
12872
  const installedApp = findInstalledApp();
13003
12873
  if (!opts.dev && !opts.build && !opts.shortcut && installedApp) {
13004
- launchApp(installedApp, homedir14());
12874
+ launchApp(installedApp, homedir12());
13005
12875
  return;
13006
12876
  }
13007
12877
  const desktopDir = findDesktopDir();
@@ -13102,8 +12972,8 @@ import { spawnSync as spawnSync2 } from "child_process";
13102
12972
  import { existsSync as existsSync24, readFileSync as readFileSync14, realpathSync as realpathSync2 } from "fs";
13103
12973
  import { dirname as dirname9, join as join23 } from "path";
13104
12974
  import { fileURLToPath as fileURLToPath4 } from "url";
13105
- import { confirm as confirm4 } from "@inquirer/prompts";
13106
- import { Command as Command23 } from "commander";
12975
+ import { confirm as confirm3 } from "@inquirer/prompts";
12976
+ import { Command as Command22 } from "commander";
13107
12977
  var GITHUB_REPO = "zam-os/zam";
13108
12978
  var CHANNELS = [
13109
12979
  "developer",
@@ -13131,273 +13001,647 @@ function currentVersion() {
13131
13001
  } catch {
13132
13002
  }
13133
13003
  }
13134
- return "0.0.0";
13004
+ return "0.0.0";
13005
+ }
13006
+ function versionAt(dir) {
13007
+ try {
13008
+ const pkg2 = JSON.parse(
13009
+ readFileSync14(join23(dir, "package.json"), "utf-8")
13010
+ );
13011
+ return pkg2.version ?? "unknown";
13012
+ } catch {
13013
+ return "unknown";
13014
+ }
13015
+ }
13016
+ async function fetchLatestVersion(repo) {
13017
+ const res = await fetch(
13018
+ `https://api.github.com/repos/${repo}/releases/latest`,
13019
+ {
13020
+ headers: {
13021
+ Accept: "application/vnd.github+json",
13022
+ "User-Agent": "zam-cli"
13023
+ }
13024
+ }
13025
+ );
13026
+ if (!res.ok) {
13027
+ throw new Error(
13028
+ `Could not reach the release server (HTTP ${res.status}). Pass --latest <version> to check offline.`
13029
+ );
13030
+ }
13031
+ const data = await res.json();
13032
+ if (!data.tag_name) throw new Error("No published release found yet.");
13033
+ return data.tag_name;
13034
+ }
13035
+ function render2(decision) {
13036
+ if (!decision.updateAvailable) {
13037
+ console.log(
13038
+ `${C4.green}\u2713${C4.reset} ZAM is up to date (${C4.cyan}${decision.currentVersion}${C4.reset}).`
13039
+ );
13040
+ return;
13041
+ }
13042
+ console.log(
13043
+ `${C4.yellow}\u2191${C4.reset} ${C4.bold}Update available${C4.reset}: ${C4.dim}${decision.currentVersion}${C4.reset} \u2192 ${C4.cyan}${decision.latestVersion}${C4.reset}`
13044
+ );
13045
+ console.log(` ${decision.reason}`);
13046
+ if (decision.action === "run-command" || decision.action === "inform") {
13047
+ console.log(` Run: ${C4.cyan}${decision.command}${C4.reset}`);
13048
+ } else if (decision.action === "self-update") {
13049
+ console.log(
13050
+ ` ${C4.dim}The desktop app can apply this update for you.${C4.reset}`
13051
+ );
13052
+ }
13053
+ }
13054
+ var checkCmd = new Command22("check").description("Check whether a newer ZAM has been released").option(
13055
+ "--latest <version>",
13056
+ "Compare against this version instead of fetching"
13057
+ ).option("--channel <channel>", "Override the detected install channel").option("--json", "Output as JSON").action(
13058
+ async (opts) => {
13059
+ try {
13060
+ if (opts.channel && !CHANNELS.includes(opts.channel)) {
13061
+ console.error(
13062
+ `Invalid --channel: ${opts.channel}. Use ${CHANNELS.join(", ")}.`
13063
+ );
13064
+ process.exit(1);
13065
+ }
13066
+ const current = currentVersion();
13067
+ const latest = opts.latest ?? await fetchLatestVersion(GITHUB_REPO);
13068
+ const channel = opts.channel ?? getInstallChannel();
13069
+ const decision = decideUpdate({
13070
+ currentVersion: current,
13071
+ latestVersion: latest,
13072
+ channel
13073
+ });
13074
+ if (opts.json) {
13075
+ console.log(JSON.stringify(decision, null, 2));
13076
+ return;
13077
+ }
13078
+ render2(decision);
13079
+ } catch (err) {
13080
+ console.error("Error:", err.message);
13081
+ process.exit(1);
13082
+ }
13083
+ }
13084
+ );
13085
+ function findSourceRepo() {
13086
+ let dir = realpathSync2(dirname9(fileURLToPath4(import.meta.url)));
13087
+ let parent = dirname9(dir);
13088
+ while (parent !== dir) {
13089
+ if (existsSync24(join23(dir, ".git"))) return dir;
13090
+ dir = parent;
13091
+ parent = dirname9(dir);
13092
+ }
13093
+ return existsSync24(join23(dir, ".git")) ? dir : null;
13094
+ }
13095
+ function runGit(cwd, args, capture) {
13096
+ const res = spawnSync2("git", args, {
13097
+ cwd,
13098
+ stdio: capture ? ["ignore", "pipe", "pipe"] : "inherit",
13099
+ encoding: "utf8"
13100
+ });
13101
+ return { ok: res.status === 0, out: (res.stdout ?? "").trim() };
13102
+ }
13103
+ function runNpm2(args, cwd) {
13104
+ const res = spawnSync2("npm", args, {
13105
+ cwd,
13106
+ stdio: "inherit",
13107
+ shell: process.platform === "win32"
13108
+ });
13109
+ return res.status ?? 1;
13110
+ }
13111
+ function runShell(command) {
13112
+ const res = spawnSync2(command, { stdio: "inherit", shell: true });
13113
+ return res.status === 0;
13114
+ }
13115
+ function applyDeveloperUpdate(force) {
13116
+ if (!hasCommand("git")) {
13117
+ console.error(`${C4.red}\u2717${C4.reset} git was not found on PATH.`);
13118
+ process.exit(1);
13119
+ }
13120
+ const src = findSourceRepo();
13121
+ if (!src) {
13122
+ console.error(
13123
+ `${C4.red}\u2717${C4.reset} Could not locate the ZAM source checkout to update.`
13124
+ );
13125
+ process.exit(1);
13126
+ }
13127
+ const status = runGit(src, ["status", "--porcelain"], true);
13128
+ if (status.ok && status.out && !force) {
13129
+ console.error(
13130
+ `${C4.red}\u2717${C4.reset} The source checkout has uncommitted changes:
13131
+ ${status.out}
13132
+ Commit or stash them, or re-run with ${C4.cyan}--force${C4.reset}.`
13133
+ );
13134
+ process.exit(1);
13135
+ }
13136
+ console.log(
13137
+ `${C4.dim}\u2192 git pull --ff-only${C4.reset} ${C4.dim}(${src})${C4.reset}`
13138
+ );
13139
+ if (!runGit(src, ["pull", "--ff-only"], false).ok) {
13140
+ console.error(
13141
+ `${C4.red}\u2717${C4.reset} git pull failed \u2014 the branch may have diverged or you are offline. Resolve it manually, then retry.`
13142
+ );
13143
+ process.exit(1);
13144
+ }
13145
+ console.log(`${C4.dim}\u2192 npm install${C4.reset}`);
13146
+ if (runNpm2(["install"], src) !== 0) {
13147
+ console.error(`${C4.red}\u2717${C4.reset} npm install failed.`);
13148
+ process.exit(1);
13149
+ }
13150
+ console.log(`${C4.dim}\u2192 npm run build${C4.reset}`);
13151
+ if (runNpm2(["run", "build"], src) !== 0) {
13152
+ console.error(`${C4.red}\u2717${C4.reset} Build failed.`);
13153
+ process.exit(1);
13154
+ }
13155
+ console.log(`${C4.dim}\u2192 zam setup --force${C4.reset}`);
13156
+ const setup = spawnSync2(
13157
+ process.execPath,
13158
+ [join23(src, "dist", "cli", "index.js"), "setup", "--force"],
13159
+ { cwd: process.cwd(), stdio: "inherit" }
13160
+ );
13161
+ if (setup.status !== 0) {
13162
+ console.warn(
13163
+ `${C4.yellow}\u26A0${C4.reset} Skill refresh reported a problem \u2014 run '${C4.cyan}zam setup --force${C4.reset}' here manually.`
13164
+ );
13165
+ }
13166
+ console.log(
13167
+ `
13168
+ ${C4.green}\u2713${C4.reset} Updated to ${C4.cyan}${versionAt(src)}${C4.reset}. Restart your agent client (e.g. Claude Code) to load the refreshed ${C4.cyan}/zam${C4.reset} skill.`
13169
+ );
13170
+ }
13171
+ async function applyUpdate(opts) {
13172
+ try {
13173
+ const current = currentVersion();
13174
+ const latest = await fetchLatestVersion(GITHUB_REPO);
13175
+ const channel = getInstallChannel();
13176
+ const decision = decideUpdate({
13177
+ currentVersion: current,
13178
+ latestVersion: latest,
13179
+ channel
13180
+ });
13181
+ if (!decision.updateAvailable) {
13182
+ console.log(
13183
+ `${C4.green}\u2713${C4.reset} ZAM is up to date (${C4.cyan}${current}${C4.reset}).`
13184
+ );
13185
+ return;
13186
+ }
13187
+ console.log(
13188
+ `${C4.yellow}\u2191${C4.reset} ${C4.bold}Update available${C4.reset}: ${C4.dim}${current}${C4.reset} \u2192 ${C4.cyan}${latest}${C4.reset} ${C4.dim}(${channel} install)${C4.reset}`
13189
+ );
13190
+ for (const step of planUpdate(decision)) {
13191
+ console.log(` ${C4.dim}\u2022${C4.reset} ${step.label}`);
13192
+ }
13193
+ if (channel === "direct") {
13194
+ console.log(
13195
+ `
13196
+ ${C4.dim}This install updates through the desktop app's signed updater \u2014 open ZAM Desktop to install ${latest}.${C4.reset}`
13197
+ );
13198
+ return;
13199
+ }
13200
+ if (!opts.yes) {
13201
+ const ok = await confirm3({
13202
+ message: "Apply this update?",
13203
+ default: true
13204
+ });
13205
+ if (!ok) {
13206
+ console.log("Aborted.");
13207
+ return;
13208
+ }
13209
+ }
13210
+ console.log();
13211
+ if (channel === "winget" || channel === "homebrew") {
13212
+ if (!decision.command || !runShell(decision.command)) process.exit(1);
13213
+ return;
13214
+ }
13215
+ applyDeveloperUpdate(opts.force ?? false);
13216
+ } catch (err) {
13217
+ console.error("Error:", err.message);
13218
+ process.exit(1);
13219
+ }
13220
+ }
13221
+ var updateCommand = new Command22("update").description(
13222
+ "Update ZAM to the latest release (use `update check` to only check)"
13223
+ ).option("-y, --yes", "Apply without confirmation").option(
13224
+ "--force",
13225
+ "Update even if the source checkout has uncommitted changes"
13226
+ ).action(async (opts) => {
13227
+ await applyUpdate(opts);
13228
+ }).addCommand(checkCmd);
13229
+
13230
+ // src/cli/commands/whoami.ts
13231
+ import { Command as Command23 } from "commander";
13232
+ var whoamiCommand = new Command23("whoami").description("Show or set the default user identity").option("--set <id>", "Set the default user ID").option("--clear", "Remove the default user ID").option("--json", "Output as JSON").action(async (opts) => {
13233
+ await withDb(async (db) => {
13234
+ if (opts.set) {
13235
+ await setSetting(db, "user.id", opts.set);
13236
+ if (opts.json) {
13237
+ console.log(JSON.stringify({ userId: opts.set }));
13238
+ } else {
13239
+ console.log(`Default user set to: ${opts.set}`);
13240
+ }
13241
+ return;
13242
+ }
13243
+ if (opts.clear) {
13244
+ const deleted = await deleteSetting(db, "user.id");
13245
+ if (opts.json) {
13246
+ console.log(JSON.stringify({ userId: null, cleared: deleted }));
13247
+ } else if (deleted) {
13248
+ console.log("Default user cleared.");
13249
+ } else {
13250
+ console.log("No default user was set.");
13251
+ }
13252
+ return;
13253
+ }
13254
+ const userId = await getSetting(db, "user.id");
13255
+ if (opts.json) {
13256
+ console.log(JSON.stringify({ userId: userId ?? null }));
13257
+ return;
13258
+ }
13259
+ if (userId) {
13260
+ console.log(userId);
13261
+ } else {
13262
+ console.log("No default user set. Use: zam whoami --set <id>");
13263
+ }
13264
+ });
13265
+ });
13266
+
13267
+ // src/cli/commands/workspace.ts
13268
+ import { execFileSync as execFileSync4 } from "child_process";
13269
+ import { existsSync as existsSync25, writeFileSync as writeFileSync11 } from "fs";
13270
+ import { homedir as homedir13 } from "os";
13271
+ import { join as join24, resolve as resolve9 } from "path";
13272
+ import { confirm as confirm4, input as input7 } from "@inquirer/prompts";
13273
+ import { Command as Command24 } from "commander";
13274
+ function runGit2(cwd, args) {
13275
+ try {
13276
+ return execFileSync4("git", args, {
13277
+ cwd,
13278
+ stdio: "pipe",
13279
+ encoding: "utf8"
13280
+ }).trim();
13281
+ } catch (err) {
13282
+ throw new Error(`Git command failed: ${err.message}`);
13283
+ }
13135
13284
  }
13136
- function versionAt(dir) {
13137
- try {
13138
- const pkg2 = JSON.parse(
13139
- readFileSync14(join23(dir, "package.json"), "utf-8")
13140
- );
13141
- return pkg2.version ?? "unknown";
13142
- } catch {
13143
- return "unknown";
13285
+ function ghRepoCreateArgs(repoName, repoVisibility) {
13286
+ return ["repo", "create", repoName, repoVisibility, "--source=.", "--push"];
13287
+ }
13288
+ function gitRemoteArgs(githubUrl, hasOrigin) {
13289
+ return hasOrigin ? ["remote", "set-url", "origin", githubUrl] : ["remote", "add", "origin", githubUrl];
13290
+ }
13291
+ var workspaceCommand = new Command24("workspace").description(
13292
+ "Manage your ZAM learning workspace"
13293
+ );
13294
+ var WORKSPACE_KINDS = [
13295
+ "personal",
13296
+ "team",
13297
+ "family",
13298
+ "community",
13299
+ "organization",
13300
+ "custom"
13301
+ ];
13302
+ var WORKSPACE_SOURCE_CONTROLS = [
13303
+ "github",
13304
+ "azure-devops",
13305
+ "git",
13306
+ "none"
13307
+ ];
13308
+ function parseWorkspaceKind(value) {
13309
+ const kind = (value ?? "custom").toLowerCase();
13310
+ if (WORKSPACE_KINDS.includes(kind)) {
13311
+ return kind;
13144
13312
  }
13313
+ throw new Error(
13314
+ `Invalid workspace kind: ${value}. Use ${WORKSPACE_KINDS.join(", ")}.`
13315
+ );
13145
13316
  }
13146
- async function fetchLatestVersion(repo) {
13147
- const res = await fetch(
13148
- `https://api.github.com/repos/${repo}/releases/latest`,
13149
- {
13150
- headers: {
13151
- Accept: "application/vnd.github+json",
13152
- "User-Agent": "zam-cli"
13153
- }
13154
- }
13317
+ function parseWorkspaceSourceControl(value) {
13318
+ if (!value) return void 0;
13319
+ const source = value.toLowerCase();
13320
+ if (WORKSPACE_SOURCE_CONTROLS.includes(source)) {
13321
+ return source;
13322
+ }
13323
+ throw new Error(
13324
+ `Invalid source control: ${value}. Use ${WORKSPACE_SOURCE_CONTROLS.join(", ")}.`
13155
13325
  );
13156
- if (!res.ok) {
13326
+ }
13327
+ function parseScopes(value) {
13328
+ if (!value) return void 0;
13329
+ const scopes = value.split(",").map((item) => item.trim()).filter(Boolean);
13330
+ return scopes.length > 0 ? scopes : void 0;
13331
+ }
13332
+ function requireWorkspace(id) {
13333
+ const workspace = getConfiguredWorkspaces().find((item) => item.id === id);
13334
+ if (!workspace) {
13157
13335
  throw new Error(
13158
- `Could not reach the release server (HTTP ${res.status}). Pass --latest <version> to check offline.`
13336
+ `Workspace "${id}" is not configured. Add it with: zam workspace add ${id} --path <dir>`
13159
13337
  );
13160
13338
  }
13161
- const data = await res.json();
13162
- if (!data.tag_name) throw new Error("No published release found yet.");
13163
- return data.tag_name;
13339
+ return workspace;
13164
13340
  }
13165
- function render2(decision) {
13166
- if (!decision.updateAvailable) {
13167
- console.log(
13168
- `${C4.green}\u2713${C4.reset} ZAM is up to date (${C4.cyan}${decision.currentVersion}${C4.reset}).`
13169
- );
13170
- return;
13341
+ function formatLinkHealth(id, health) {
13342
+ switch (health) {
13343
+ case "healthy":
13344
+ return "ok";
13345
+ case "needs-repair":
13346
+ return `needs repair \u2014 run: zam workspace setup ${id}`;
13347
+ case "unmanaged":
13348
+ return `unmanaged skill dir \u2014 run: zam workspace setup ${id} --force`;
13171
13349
  }
13172
- console.log(
13173
- `${C4.yellow}\u2191${C4.reset} ${C4.bold}Update available${C4.reset}: ${C4.dim}${decision.currentVersion}${C4.reset} \u2192 ${C4.cyan}${decision.latestVersion}${C4.reset}`
13350
+ }
13351
+ workspaceCommand.command("list").description("List configured ZAM workspaces").option("--json", "Output as JSON").action((opts) => {
13352
+ const workspaces = getConfiguredWorkspaces();
13353
+ const agents = parseSetupAgents();
13354
+ const linkHealth = Object.fromEntries(
13355
+ workspaces.filter((workspace) => existsSync25(workspace.path)).map((workspace) => [
13356
+ workspace.id,
13357
+ summarizeSkillLinkHealth(inspectSkillLinks(workspace.path, agents))
13358
+ ])
13174
13359
  );
13175
- console.log(` ${decision.reason}`);
13176
- if (decision.action === "run-command" || decision.action === "inform") {
13177
- console.log(` Run: ${C4.cyan}${decision.command}${C4.reset}`);
13178
- } else if (decision.action === "self-update") {
13360
+ if (opts.json) {
13361
+ console.log(JSON.stringify({ workspaces, linkHealth }, null, 2));
13362
+ return;
13363
+ }
13364
+ console.log("Configured ZAM workspaces:\n");
13365
+ if (workspaces.length === 0) {
13179
13366
  console.log(
13180
- ` ${C4.dim}The desktop app can apply this update for you.${C4.reset}`
13367
+ " (none) \u2014 add one: zam workspace add personal --path <dir>"
13181
13368
  );
13369
+ return;
13182
13370
  }
13183
- }
13184
- var checkCmd = new Command23("check").description("Check whether a newer ZAM has been released").option(
13185
- "--latest <version>",
13186
- "Compare against this version instead of fetching"
13187
- ).option("--channel <channel>", "Override the detected install channel").option("--json", "Output as JSON").action(
13188
- async (opts) => {
13189
- try {
13190
- if (opts.channel && !CHANNELS.includes(opts.channel)) {
13191
- console.error(
13192
- `Invalid --channel: ${opts.channel}. Use ${CHANNELS.join(", ")}.`
13193
- );
13194
- process.exit(1);
13195
- }
13196
- const current = currentVersion();
13197
- const latest = opts.latest ?? await fetchLatestVersion(GITHUB_REPO);
13198
- const channel = opts.channel ?? getInstallChannel();
13199
- const decision = decideUpdate({
13200
- currentVersion: current,
13201
- latestVersion: latest,
13202
- channel
13203
- });
13204
- if (opts.json) {
13205
- console.log(JSON.stringify(decision, null, 2));
13206
- return;
13207
- }
13208
- render2(decision);
13209
- } catch (err) {
13210
- console.error("Error:", err.message);
13371
+ for (const workspace of workspaces) {
13372
+ const label = workspace.label ? ` (${workspace.label})` : "";
13373
+ console.log(` ${workspace.id}${label}`);
13374
+ console.log(` kind: ${workspace.kind}`);
13375
+ console.log(` path: ${workspace.path}`);
13376
+ if (workspace.sourceControl) {
13377
+ console.log(` source: ${workspace.sourceControl}`);
13378
+ }
13379
+ if (workspace.knowledgeScopes?.length) {
13380
+ console.log(` scopes: ${workspace.knowledgeScopes.join(", ")}`);
13381
+ }
13382
+ const health = linkHealth[workspace.id];
13383
+ if (health) {
13384
+ console.log(` links: ${formatLinkHealth(workspace.id, health)}`);
13385
+ }
13386
+ }
13387
+ });
13388
+ workspaceCommand.command("add <id>").description("Register an existing directory as a ZAM workspace").requiredOption("--path <dir>", "Existing workspace/repository directory").option(
13389
+ "--kind <kind>",
13390
+ `Workspace kind (${WORKSPACE_KINDS.join(" | ")})`,
13391
+ "custom"
13392
+ ).option("--label <label>", "Human-readable label").option(
13393
+ "--source-control <provider>",
13394
+ `Source-control provider (${WORKSPACE_SOURCE_CONTROLS.join(" | ")})`
13395
+ ).option("--scopes <list>", "Comma-separated knowledge scopes").option("--default-agent <id>", "Default agent harness for this workspace").action((id, opts) => {
13396
+ try {
13397
+ const path = resolve9(String(opts.path));
13398
+ if (!existsSync25(path)) {
13399
+ console.error(`Workspace path does not exist: ${path}`);
13211
13400
  process.exit(1);
13212
13401
  }
13402
+ const workspace = {
13403
+ id,
13404
+ kind: parseWorkspaceKind(opts.kind),
13405
+ path,
13406
+ ...opts.label ? { label: opts.label } : {},
13407
+ ...opts.sourceControl ? { sourceControl: parseWorkspaceSourceControl(opts.sourceControl) } : {},
13408
+ ...parseScopes(opts.scopes) ? { knowledgeScopes: parseScopes(opts.scopes) } : {},
13409
+ ...opts.defaultAgent ? { defaultAgent: opts.defaultAgent } : {}
13410
+ };
13411
+ wireSkills(path, parseSetupAgents());
13412
+ upsertConfiguredWorkspace(workspace);
13413
+ console.log(`Registered and linked workspace "${id}" at ${path}.`);
13414
+ } catch (err) {
13415
+ console.error(`Error: ${err.message}`);
13416
+ process.exit(1);
13213
13417
  }
13214
- );
13215
- function findSourceRepo() {
13216
- let dir = realpathSync2(dirname9(fileURLToPath4(import.meta.url)));
13217
- let parent = dirname9(dir);
13218
- while (parent !== dir) {
13219
- if (existsSync24(join23(dir, ".git"))) return dir;
13220
- dir = parent;
13221
- parent = dirname9(dir);
13418
+ });
13419
+ workspaceCommand.command("remove <id>").description("Unregister a ZAM workspace without deleting its files").action((id) => {
13420
+ const existing = getConfiguredWorkspaces().find((item) => item.id === id);
13421
+ if (!existing) {
13422
+ console.error(`Workspace "${id}" is not configured.`);
13423
+ process.exit(1);
13222
13424
  }
13223
- return existsSync24(join23(dir, ".git")) ? dir : null;
13224
- }
13225
- function runGit2(cwd, args, capture) {
13226
- const res = spawnSync2("git", args, {
13227
- cwd,
13228
- stdio: capture ? ["ignore", "pipe", "pipe"] : "inherit",
13229
- encoding: "utf8"
13230
- });
13231
- return { ok: res.status === 0, out: (res.stdout ?? "").trim() };
13232
- }
13233
- function runNpm2(args, cwd) {
13234
- const res = spawnSync2("npm", args, {
13235
- cwd,
13236
- stdio: "inherit",
13237
- shell: process.platform === "win32"
13238
- });
13239
- return res.status ?? 1;
13240
- }
13241
- function runShell(command) {
13242
- const res = spawnSync2(command, { stdio: "inherit", shell: true });
13243
- return res.status === 0;
13244
- }
13245
- function applyDeveloperUpdate(force) {
13246
- if (!hasCommand("git")) {
13247
- console.error(`${C4.red}\u2717${C4.reset} git was not found on PATH.`);
13425
+ removeConfiguredWorkspace(id);
13426
+ console.log(
13427
+ `Unregistered workspace "${id}". Files in ${existing.path} were not changed.`
13428
+ );
13429
+ });
13430
+ workspaceCommand.command("setup <id>").description("Install ZAM skills into a configured workspace").option(
13431
+ "--agents <list>",
13432
+ "comma-separated agents to wire: all, claude, copilot, codex, agent"
13433
+ ).option(
13434
+ "--force",
13435
+ "overwrite existing skill files and refresh ZAM blocks",
13436
+ false
13437
+ ).option(
13438
+ "--dry-run",
13439
+ "show what would be written without changing files",
13440
+ false
13441
+ ).action((id, opts) => {
13442
+ try {
13443
+ const workspace = requireWorkspace(id);
13444
+ const agents = parseSetupAgents(opts.agents);
13445
+ console.log(
13446
+ `Setting up workspace "${workspace.id}" in ${workspace.path}${opts.dryRun ? " (dry run)" : ""}
13447
+ `
13448
+ );
13449
+ wireSkills(workspace.path, agents, {
13450
+ force: Boolean(opts.force),
13451
+ dryRun: Boolean(opts.dryRun)
13452
+ });
13453
+ if (agents.has("claude")) {
13454
+ writeClaudeMd(false, workspace.path, {
13455
+ dryRun: Boolean(opts.dryRun),
13456
+ updateExisting: true
13457
+ });
13458
+ }
13459
+ if (agents.has("codex") || agents.has("agent")) {
13460
+ writeAgentsMd(false, workspace.path, {
13461
+ dryRun: Boolean(opts.dryRun),
13462
+ updateExisting: true
13463
+ });
13464
+ }
13465
+ if (agents.has("copilot")) {
13466
+ writeCopilotInstructions(workspace.path, {
13467
+ dryRun: Boolean(opts.dryRun),
13468
+ updateExisting: true
13469
+ });
13470
+ }
13471
+ } catch (err) {
13472
+ console.error(`Error: ${err.message}`);
13248
13473
  process.exit(1);
13249
13474
  }
13250
- const src = findSourceRepo();
13251
- if (!src) {
13475
+ });
13476
+ workspaceCommand.command("publish").description("Publish your local workspace sandbox to GitHub").action(async () => {
13477
+ let db;
13478
+ let workspaceDir = "";
13479
+ try {
13480
+ db = await openDatabase();
13481
+ workspaceDir = (await ensureActiveWorkspace(db)).path;
13482
+ await db.close();
13483
+ } catch {
13484
+ await db?.close();
13485
+ }
13486
+ if (!workspaceDir) {
13252
13487
  console.error(
13253
- `${C4.red}\u2717${C4.reset} Could not locate the ZAM source checkout to update.`
13488
+ "\x1B[31m\u2717 No active workspace configured. Please run `zam init` first.\x1B[0m"
13254
13489
  );
13255
13490
  process.exit(1);
13256
13491
  }
13257
- const status = runGit2(src, ["status", "--porcelain"], true);
13258
- if (status.ok && status.out && !force) {
13492
+ if (!existsSync25(workspaceDir)) {
13259
13493
  console.error(
13260
- `${C4.red}\u2717${C4.reset} The source checkout has uncommitted changes:
13261
- ${status.out}
13262
- Commit or stash them, or re-run with ${C4.cyan}--force${C4.reset}.`
13494
+ `\x1B[31m\u2717 Workspace directory does not exist: ${workspaceDir}\x1B[0m`
13263
13495
  );
13264
13496
  process.exit(1);
13265
13497
  }
13266
- console.log(
13267
- `${C4.dim}\u2192 git pull --ff-only${C4.reset} ${C4.dim}(${src})${C4.reset}`
13268
- );
13269
- if (!runGit2(src, ["pull", "--ff-only"], false).ok) {
13498
+ console.log(`
13499
+ Active workspace: \x1B[36m${workspaceDir}\x1B[0m`);
13500
+ if (!hasCommand("git")) {
13270
13501
  console.error(
13271
- `${C4.red}\u2717${C4.reset} git pull failed \u2014 the branch may have diverged or you are offline. Resolve it manually, then retry.`
13502
+ "\x1B[31m\u2717 Git command was not found on this system. Please install Git first.\x1B[0m"
13272
13503
  );
13273
13504
  process.exit(1);
13274
13505
  }
13275
- console.log(`${C4.dim}\u2192 npm install${C4.reset}`);
13276
- if (runNpm2(["install"], src) !== 0) {
13277
- console.error(`${C4.red}\u2717${C4.reset} npm install failed.`);
13278
- process.exit(1);
13506
+ const gitignorePath = join24(workspaceDir, ".gitignore");
13507
+ if (!existsSync25(gitignorePath)) {
13508
+ writeFileSync11(
13509
+ gitignorePath,
13510
+ "node_modules/\n.agent/\n.agents/\n.claude/\n.gemini/\n.goose/\n*.log\n",
13511
+ "utf8"
13512
+ );
13279
13513
  }
13280
- console.log(`${C4.dim}\u2192 npm run build${C4.reset}`);
13281
- if (runNpm2(["run", "build"], src) !== 0) {
13282
- console.error(`${C4.red}\u2717${C4.reset} Build failed.`);
13283
- process.exit(1);
13514
+ const hasGitRepo = existsSync25(join24(workspaceDir, ".git"));
13515
+ if (!hasGitRepo) {
13516
+ console.log("Initializing local Git repository...");
13517
+ runGit2(workspaceDir, ["init", "-b", "main"]);
13518
+ runGit2(workspaceDir, ["add", "."]);
13519
+ runGit2(workspaceDir, [
13520
+ "commit",
13521
+ "-m",
13522
+ "chore: initial workspace sandbox bootstrap"
13523
+ ]);
13524
+ console.log("\x1B[32m\u2713 Local Git repository initialized.\x1B[0m");
13525
+ } else {
13526
+ console.log("Git repository is already initialized.");
13284
13527
  }
13285
- console.log(`${C4.dim}\u2192 zam setup --force${C4.reset}`);
13286
- const setup = spawnSync2(
13287
- process.execPath,
13288
- [join23(src, "dist", "cli", "index.js"), "setup", "--force"],
13289
- { cwd: process.cwd(), stdio: "inherit" }
13290
- );
13291
- if (setup.status !== 0) {
13292
- console.warn(
13293
- `${C4.yellow}\u26A0${C4.reset} Skill refresh reported a problem \u2014 run '${C4.cyan}zam setup --force${C4.reset}' here manually.`
13294
- );
13528
+ const repoName = await input7({
13529
+ message: "Choose a name for your GitHub repository:",
13530
+ default: "zam-personal"
13531
+ });
13532
+ const isPrivate = await confirm4({
13533
+ message: "Should the repository be private?",
13534
+ default: true
13535
+ });
13536
+ const repoVisibility = isPrivate ? "--private" : "--public";
13537
+ if (hasCommand("gh")) {
13538
+ console.log("GitHub CLI detected! Automating repository creation...");
13539
+ const proceedGh = await confirm4({
13540
+ message: "Would you like ZAM to create the repository using the GitHub CLI?",
13541
+ default: true
13542
+ });
13543
+ if (proceedGh) {
13544
+ try {
13545
+ console.log(`Creating GitHub repository ${repoName}...`);
13546
+ execFileSync4("gh", ghRepoCreateArgs(repoName, repoVisibility), {
13547
+ cwd: workspaceDir,
13548
+ stdio: "inherit"
13549
+ });
13550
+ console.log(
13551
+ "\n\x1B[32m\u2713 Successfully published workspace to GitHub!\x1B[0m"
13552
+ );
13553
+ process.exit(0);
13554
+ } catch (err) {
13555
+ console.warn(
13556
+ `\x1B[33m\u26A0 GitHub CLI creation failed: ${err.message}\x1B[0m`
13557
+ );
13558
+ }
13559
+ }
13295
13560
  }
13296
13561
  console.log(
13297
- `
13298
- ${C4.green}\u2713${C4.reset} Updated to ${C4.cyan}${versionAt(src)}${C4.reset}. Restart your agent client (e.g. Claude Code) to load the refreshed ${C4.cyan}/zam${C4.reset} skill.`
13562
+ "\n\x1B[1mPlease create the repository manually on GitHub:\x1B[0m"
13299
13563
  );
13300
- }
13301
- async function applyUpdate(opts) {
13302
- try {
13303
- const current = currentVersion();
13304
- const latest = await fetchLatestVersion(GITHUB_REPO);
13305
- const channel = getInstallChannel();
13306
- const decision = decideUpdate({
13307
- currentVersion: current,
13308
- latestVersion: latest,
13309
- channel
13310
- });
13311
- if (!decision.updateAvailable) {
13564
+ console.log(" 1. Go to https://github.com/new");
13565
+ console.log(` 2. Name it exactly: \x1B[36m${repoName}\x1B[0m`);
13566
+ console.log(
13567
+ ` 3. Choose \x1B[36m${isPrivate ? "Private" : "Public"}\x1B[0m`
13568
+ );
13569
+ console.log(
13570
+ " 4. Do NOT initialize it with README, .gitignore, or license"
13571
+ );
13572
+ console.log(" 5. Click 'Create repository'\n");
13573
+ const githubUrl = await input7({
13574
+ message: "Paste the repository Git URL (e.g. git@github.com:user/repo.git):"
13575
+ });
13576
+ if (githubUrl) {
13577
+ try {
13578
+ console.log("Linking remote repository and pushing...");
13579
+ let hasOrigin = false;
13580
+ try {
13581
+ runGit2(workspaceDir, ["remote", "get-url", "origin"]);
13582
+ hasOrigin = true;
13583
+ } catch {
13584
+ }
13585
+ runGit2(workspaceDir, gitRemoteArgs(githubUrl, hasOrigin));
13586
+ runGit2(workspaceDir, ["push", "-u", "origin", "main"]);
13312
13587
  console.log(
13313
- `${C4.green}\u2713${C4.reset} ZAM is up to date (${C4.cyan}${current}${C4.reset}).`
13588
+ "\x1B[32m\u2713 Successfully linked and pushed to GitHub!\x1B[0m"
13589
+ );
13590
+ } catch (err) {
13591
+ console.error(
13592
+ `\x1B[31m\u2717 Push failed: ${err.message}\x1B[0m`
13314
13593
  );
13315
- return;
13316
- }
13317
- console.log(
13318
- `${C4.yellow}\u2191${C4.reset} ${C4.bold}Update available${C4.reset}: ${C4.dim}${current}${C4.reset} \u2192 ${C4.cyan}${latest}${C4.reset} ${C4.dim}(${channel} install)${C4.reset}`
13319
- );
13320
- for (const step of planUpdate(decision)) {
13321
- console.log(` ${C4.dim}\u2022${C4.reset} ${step.label}`);
13322
- }
13323
- if (channel === "direct") {
13324
13594
  console.log(
13325
- `
13326
- ${C4.dim}This install updates through the desktop app's signed updater \u2014 open ZAM Desktop to install ${latest}.${C4.reset}`
13595
+ "You can push manually later using: git push -u origin main"
13327
13596
  );
13328
- return;
13329
- }
13330
- if (!opts.yes) {
13331
- const ok = await confirm4({
13332
- message: "Apply this update?",
13333
- default: true
13334
- });
13335
- if (!ok) {
13336
- console.log("Aborted.");
13337
- return;
13338
- }
13339
13597
  }
13340
- console.log();
13341
- if (channel === "winget" || channel === "homebrew") {
13342
- if (!decision.command || !runShell(decision.command)) process.exit(1);
13598
+ }
13599
+ });
13600
+ workspaceCommand.command("data-dir").description("Print the ZAM data directory (database, credentials, config)").option("--json", "Output as JSON").action((opts) => {
13601
+ const dir = join24(homedir13(), ".zam");
13602
+ console.log(opts.json ? JSON.stringify({ dataDir: dir }) : dir);
13603
+ });
13604
+ workspaceCommand.command("backup").description("Back up the local ZAM database into your workspace").option(
13605
+ "--dir <path>",
13606
+ "Target directory (default: workspace dir, else ~/Documents/zam)"
13607
+ ).option("--json", "Output as JSON").action(async (opts) => {
13608
+ const target = getDatabaseTargetInfo();
13609
+ if (target.kind !== "local") {
13610
+ const reason = `The database is ${target.kind} (${target.location}); file backup applies only to a local database \u2014 your Turso remote is already the cloud backup.`;
13611
+ if (opts.json) {
13612
+ console.log(JSON.stringify({ ok: false, reason }));
13343
13613
  return;
13344
13614
  }
13345
- applyDeveloperUpdate(opts.force ?? false);
13346
- } catch (err) {
13347
- console.error("Error:", err.message);
13615
+ console.error(`\x1B[33m\u26A0 ${reason}\x1B[0m`);
13348
13616
  process.exit(1);
13349
13617
  }
13350
- }
13351
- var updateCommand = new Command23("update").description(
13352
- "Update ZAM to the latest release (use `update check` to only check)"
13353
- ).option("-y, --yes", "Apply without confirmation").option(
13354
- "--force",
13355
- "Update even if the source checkout has uncommitted changes"
13356
- ).action(async (opts) => {
13357
- await applyUpdate(opts);
13358
- }).addCommand(checkCmd);
13359
-
13360
- // src/cli/commands/whoami.ts
13361
- import { Command as Command24 } from "commander";
13362
- var whoamiCommand = new Command24("whoami").description("Show or set the default user identity").option("--set <id>", "Set the default user ID").option("--clear", "Remove the default user ID").option("--json", "Output as JSON").action(async (opts) => {
13363
- await withDb(async (db) => {
13364
- if (opts.set) {
13365
- await setSetting(db, "user.id", opts.set);
13366
- if (opts.json) {
13367
- console.log(JSON.stringify({ userId: opts.set }));
13368
- } else {
13369
- console.log(`Default user set to: ${opts.set}`);
13370
- }
13371
- return;
13372
- }
13373
- if (opts.clear) {
13374
- const deleted = await deleteSetting(db, "user.id");
13375
- if (opts.json) {
13376
- console.log(JSON.stringify({ userId: null, cleared: deleted }));
13377
- } else if (deleted) {
13378
- console.log("Default user cleared.");
13379
- } else {
13380
- console.log("No default user was set.");
13381
- }
13382
- return;
13383
- }
13384
- const userId = await getSetting(db, "user.id");
13618
+ let db;
13619
+ try {
13620
+ db = await openDatabase();
13621
+ const workspaceDir = opts.dir || (await ensureActiveWorkspace(db)).path;
13622
+ const dest = await backupDatabaseTo(db, workspaceDir);
13385
13623
  if (opts.json) {
13386
- console.log(JSON.stringify({ userId: userId ?? null }));
13387
- return;
13624
+ console.log(JSON.stringify({ ok: true, path: dest }));
13625
+ } else {
13626
+ console.log(`\x1B[32m\u2713 Database backed up to ${dest}\x1B[0m`);
13388
13627
  }
13389
- if (userId) {
13390
- console.log(userId);
13628
+ } catch (err) {
13629
+ const reason = err.message;
13630
+ if (opts.json) {
13631
+ console.log(JSON.stringify({ ok: false, reason }));
13391
13632
  } else {
13392
- console.log("No default user set. Use: zam whoami --set <id>");
13633
+ console.error(`\x1B[31m\u2717 Backup failed: ${reason}\x1B[0m`);
13393
13634
  }
13394
- });
13635
+ process.exit(1);
13636
+ } finally {
13637
+ await db?.close();
13638
+ }
13395
13639
  });
13396
13640
 
13397
13641
  // src/cli/index.ts
13398
13642
  var __dirname = dirname10(fileURLToPath5(import.meta.url));
13399
13643
  var pkg = JSON.parse(
13400
- readFileSync15(join24(__dirname, "..", "..", "package.json"), "utf-8")
13644
+ readFileSync15(join25(__dirname, "..", "..", "package.json"), "utf-8")
13401
13645
  );
13402
13646
  var program = new Command25();
13403
13647
  program.name("zam").description(