zam-core 0.5.0 → 0.5.1

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
@@ -4477,6 +4477,13 @@ function upsertConfiguredWorkspace(workspace, path = defaultConfigPath()) {
4477
4477
  saveConfiguredWorkspaces(next, path);
4478
4478
  return next;
4479
4479
  }
4480
+ function removeConfiguredWorkspace(id, path = defaultConfigPath()) {
4481
+ const next = getConfiguredWorkspaces(path).filter(
4482
+ (workspace) => workspace.id !== id
4483
+ );
4484
+ saveConfiguredWorkspaces(next, path);
4485
+ return next;
4486
+ }
4480
4487
  function detectSyncProvider(dir) {
4481
4488
  const p = dir.toLowerCase();
4482
4489
  if (p.includes("onedrive")) return "OneDrive";
@@ -5352,7 +5359,13 @@ var agentCommand = new Command("agent").description("Provision and inspect the a
5352
5359
  // src/cli/commands/bridge.ts
5353
5360
  import { execFileSync as execFileSync4 } from "child_process";
5354
5361
  import { randomBytes as randomBytes2 } from "crypto";
5355
- import { existsSync as existsSync17, readdirSync as readdirSync2, readFileSync as readFileSync11, rmSync as rmSync2 } from "fs";
5362
+ import {
5363
+ existsSync as existsSync17,
5364
+ mkdirSync as mkdirSync10,
5365
+ readdirSync as readdirSync3,
5366
+ readFileSync as readFileSync11,
5367
+ rmSync as rmSync3
5368
+ } from "fs";
5356
5369
  import { homedir as homedir10, tmpdir as tmpdir3 } from "os";
5357
5370
  import { basename as basename5, join as join17, resolve as resolve5 } from "path";
5358
5371
  import { Command as Command4 } from "commander";
@@ -5974,7 +5987,7 @@ async function startLocalRunner(url, model, locale) {
5974
5987
  let attempts = 0;
5975
5988
  const dotsPerLine = 30;
5976
5989
  while (true) {
5977
- await new Promise((resolve8) => setTimeout(resolve8, 500));
5990
+ await new Promise((resolve9) => setTimeout(resolve9, 500));
5978
5991
  if (await isLlmOnline(url)) {
5979
5992
  if (attempts > 0) process.stdout.write("\n");
5980
5993
  return true;
@@ -6129,8 +6142,8 @@ async function fetchWithInteractiveTimeout(url, options = {}) {
6129
6142
  const dotsPerLine = 30;
6130
6143
  while (true) {
6131
6144
  let timeoutId;
6132
- const timeoutPromise = new Promise((resolve8) => {
6133
- timeoutId = setTimeout(() => resolve8("timeout"), timeoutMs);
6145
+ const timeoutPromise = new Promise((resolve9) => {
6146
+ timeoutId = setTimeout(() => resolve9("timeout"), timeoutMs);
6134
6147
  });
6135
6148
  const dotsInterval = setInterval(() => {
6136
6149
  process.stdout.write(".");
@@ -6240,25 +6253,25 @@ async function observeUiSnapshotViaLLM(db, input8) {
6240
6253
  const imageUrls = [];
6241
6254
  const isVideo = /\.(mp4|mov|m4v|avi|mkv|webm)$/i.test(input8.imagePath);
6242
6255
  if (isVideo) {
6243
- const { mkdirSync: mkdirSync13, readdirSync: readdirSync3, rmSync: rmSync3 } = await import("fs");
6256
+ const { mkdirSync: mkdirSync14, readdirSync: readdirSync4, rmSync: rmSync4 } = await import("fs");
6244
6257
  const { execSync: execSync6 } = await import("child_process");
6245
6258
  const tempDir = join14(
6246
6259
  tmpdir2(),
6247
6260
  `zam-frames-${randomBytes(4).toString("hex")}`
6248
6261
  );
6249
- mkdirSync13(tempDir, { recursive: true });
6262
+ mkdirSync14(tempDir, { recursive: true });
6250
6263
  try {
6251
6264
  execSync6(
6252
6265
  `ffmpeg -i "${input8.imagePath}" -vf "fps=1/3,scale=1280:-1" -vsync vfr "${tempDir}/frame_%03d.png"`,
6253
6266
  { stdio: "ignore" }
6254
6267
  );
6255
- let files = readdirSync3(tempDir).filter((f) => f.endsWith(".png")).sort();
6268
+ let files = readdirSync4(tempDir).filter((f) => f.endsWith(".png")).sort();
6256
6269
  if (files.length === 0) {
6257
6270
  execSync6(
6258
6271
  `ffmpeg -i "${input8.imagePath}" -vframes 1 "${tempDir}/frame_001.png"`,
6259
6272
  { stdio: "ignore" }
6260
6273
  );
6261
- files = readdirSync3(tempDir).filter((f) => f.endsWith(".png")).sort();
6274
+ files = readdirSync4(tempDir).filter((f) => f.endsWith(".png")).sort();
6262
6275
  }
6263
6276
  const maxFrames = cfg.maxFrames ?? 100;
6264
6277
  let sampledFiles = files;
@@ -6280,7 +6293,7 @@ async function observeUiSnapshotViaLLM(db, input8) {
6280
6293
  }
6281
6294
  } finally {
6282
6295
  try {
6283
- rmSync3(tempDir, { recursive: true, force: true });
6296
+ rmSync4(tempDir, { recursive: true, force: true });
6284
6297
  } catch {
6285
6298
  }
6286
6299
  }
@@ -6635,40 +6648,16 @@ async function resolveUser(opts, db, resolveOpts) {
6635
6648
  process.exit(1);
6636
6649
  }
6637
6650
 
6638
- // src/cli/commands/shared/db.ts
6639
- function defaultErrorHandler(message) {
6640
- console.error("Error:", message);
6641
- process.exit(1);
6642
- }
6643
- async function withDb(fn, onError = defaultErrorHandler) {
6644
- let db;
6645
- try {
6646
- db = await openDatabase();
6647
- await fn(db);
6648
- } catch (err) {
6649
- onError(err.message);
6650
- } finally {
6651
- await db?.close();
6652
- }
6653
- }
6654
- function jsonOut(data) {
6655
- console.log(JSON.stringify(data, null, 2));
6656
- }
6657
-
6658
- // src/cli/commands/workspace.ts
6659
- import { execFileSync as execFileSync3 } from "child_process";
6660
- import { existsSync as existsSync16, mkdirSync as mkdirSync9, writeFileSync as writeFileSync8 } from "fs";
6661
- import { homedir as homedir9 } from "os";
6662
- import { join as join16, resolve as resolve4 } from "path";
6663
- import { confirm, input } from "@inquirer/prompts";
6664
- import { Command as Command3 } from "commander";
6665
-
6666
6651
  // src/cli/commands/setup.ts
6667
6652
  import {
6668
- copyFileSync as copyFileSync2,
6669
6653
  existsSync as existsSync15,
6654
+ lstatSync,
6670
6655
  mkdirSync as mkdirSync8,
6656
+ readdirSync as readdirSync2,
6671
6657
  readFileSync as readFileSync10,
6658
+ realpathSync,
6659
+ rmSync as rmSync2,
6660
+ symlinkSync,
6672
6661
  writeFileSync as writeFileSync7
6673
6662
  } from "fs";
6674
6663
  import { basename as basename4, dirname as dirname5, join as join15, resolve as resolve3 } from "path";
@@ -6683,7 +6672,7 @@ var SKILL_PAIRS = [
6683
6672
  {
6684
6673
  from: join15(packageRoot, ".claude", "skills", "zam", "SKILL.md"),
6685
6674
  to: join15(".claude", "skills", "zam", "SKILL.md"),
6686
- agents: ["claude"]
6675
+ agents: ["claude", "copilot"]
6687
6676
  },
6688
6677
  {
6689
6678
  from: join15(packageRoot, ".agent", "skills", "zam", "SKILL.md"),
@@ -6693,7 +6682,7 @@ var SKILL_PAIRS = [
6693
6682
  {
6694
6683
  from: join15(packageRoot, ".agents", "skills", "zam", "SKILL.md"),
6695
6684
  to: join15(".agents", "skills", "zam", "SKILL.md"),
6696
- agents: ["copilot", "codex"]
6685
+ agents: ["codex"]
6697
6686
  }
6698
6687
  ];
6699
6688
  function parseSetupAgents(value) {
@@ -6720,34 +6709,119 @@ function parseSetupAgents(value) {
6720
6709
  }
6721
6710
  return selected;
6722
6711
  }
6723
- function copySkills(force, cwd = process.cwd(), agents = parseSetupAgents(), dryRun = false) {
6724
- let anyAction = false;
6712
+ function pathExists(path) {
6713
+ try {
6714
+ lstatSync(path);
6715
+ return true;
6716
+ } catch {
6717
+ return false;
6718
+ }
6719
+ }
6720
+ function comparableRealPath(path) {
6721
+ const real = realpathSync(path);
6722
+ return process.platform === "win32" ? real.toLowerCase() : real;
6723
+ }
6724
+ function pathsResolveToSameDirectory(sourceDir, destinationDir) {
6725
+ try {
6726
+ return comparableRealPath(destinationDir) === comparableRealPath(sourceDir);
6727
+ } catch {
6728
+ return false;
6729
+ }
6730
+ }
6731
+ function linkPointsTo(sourceDir, destinationDir) {
6732
+ try {
6733
+ return lstatSync(destinationDir).isSymbolicLink() && pathsResolveToSameDirectory(sourceDir, destinationDir);
6734
+ } catch {
6735
+ return false;
6736
+ }
6737
+ }
6738
+ function isReplaceableCopiedSkill(destinationDir) {
6739
+ try {
6740
+ if (!lstatSync(destinationDir).isDirectory()) return false;
6741
+ const entries = readdirSync2(destinationDir);
6742
+ if (entries.length !== 1 || entries[0] !== "SKILL.md") return false;
6743
+ return /^name:\s*zam\s*$/m.test(
6744
+ readFileSync10(join15(destinationDir, "SKILL.md"), "utf8")
6745
+ );
6746
+ } catch {
6747
+ return false;
6748
+ }
6749
+ }
6750
+ function wireSkills(cwd = process.cwd(), agents = parseSetupAgents(), opts = {}) {
6751
+ const results = [];
6752
+ const log = (message) => {
6753
+ if (!opts.quiet) console.log(message);
6754
+ };
6725
6755
  for (const { from, to, agents: pairAgents } of SKILL_PAIRS) {
6726
6756
  if (!pairAgents.some((agent) => agents.has(agent))) continue;
6727
- const dest = join15(cwd, to);
6728
- if (!existsSync15(from)) {
6729
- console.warn(` warn source not found, skipping: ${from}`);
6757
+ const sourceDir = dirname5(from);
6758
+ const destinationDir = dirname5(join15(cwd, to));
6759
+ if (!existsSync15(sourceDir)) {
6760
+ if (!opts.quiet) {
6761
+ console.warn(` warn source not found, skipping: ${sourceDir}`);
6762
+ }
6763
+ results.push({
6764
+ source: sourceDir,
6765
+ destination: destinationDir,
6766
+ action: "skipped",
6767
+ reason: "source-not-found"
6768
+ });
6730
6769
  continue;
6731
6770
  }
6732
- if (existsSync15(dest) && !force) {
6733
- console.log(` skip ${to} (already present \xE2\u20AC\u201D use --force to update)`);
6771
+ if (pathsResolveToSameDirectory(sourceDir, destinationDir) && !lstatSync(destinationDir).isSymbolicLink()) {
6772
+ log(` skip ${dirname5(to)} (package source)`);
6773
+ results.push({
6774
+ source: sourceDir,
6775
+ destination: destinationDir,
6776
+ action: "skipped",
6777
+ reason: "source-directory"
6778
+ });
6734
6779
  continue;
6735
6780
  }
6736
- if (dryRun) {
6737
- console.log(` would copy ${to}`);
6738
- anyAction = true;
6781
+ if (linkPointsTo(sourceDir, destinationDir)) {
6782
+ log(` skip ${dirname5(to)} (already linked)`);
6783
+ results.push({
6784
+ source: sourceDir,
6785
+ destination: destinationDir,
6786
+ action: "skipped",
6787
+ reason: "already-linked"
6788
+ });
6739
6789
  continue;
6740
6790
  }
6741
- mkdirSync8(dirname5(dest), { recursive: true });
6742
- copyFileSync2(from, dest);
6743
- console.log(` copy ${to}`);
6744
- anyAction = true;
6745
- }
6746
- if (!anyAction && !force) {
6747
- console.log(
6748
- "\nSkill files are already up to date. Run with --force to overwrite."
6749
- );
6791
+ const destinationExists = pathExists(destinationDir);
6792
+ const replaceExisting = destinationExists && (Boolean(opts.force) || isReplaceableCopiedSkill(destinationDir));
6793
+ if (destinationExists && !replaceExisting) {
6794
+ if (!opts.quiet) {
6795
+ console.warn(
6796
+ ` warn ${dirname5(to)} already exists and is not managed by ZAM; use --force to replace it`
6797
+ );
6798
+ }
6799
+ results.push({
6800
+ source: sourceDir,
6801
+ destination: destinationDir,
6802
+ action: "skipped",
6803
+ reason: "unmanaged-destination"
6804
+ });
6805
+ continue;
6806
+ }
6807
+ const action = destinationExists ? "relinked" : "linked";
6808
+ if (opts.dryRun) {
6809
+ log(` would ${action === "linked" ? "link" : "relink"} ${dirname5(to)}`);
6810
+ } else {
6811
+ if (destinationExists) {
6812
+ rmSync2(destinationDir, { recursive: true, force: true });
6813
+ }
6814
+ mkdirSync8(dirname5(destinationDir), { recursive: true });
6815
+ symlinkSync(
6816
+ sourceDir,
6817
+ destinationDir,
6818
+ process.platform === "win32" ? "junction" : "dir"
6819
+ );
6820
+ log(` ${action === "linked" ? "link" : "relink"} ${dirname5(to)}`);
6821
+ }
6822
+ results.push({ source: sourceDir, destination: destinationDir, action });
6750
6823
  }
6824
+ return results;
6751
6825
  }
6752
6826
  function formatDatabaseInitTarget(target) {
6753
6827
  switch (target.kind) {
@@ -6825,7 +6899,7 @@ ZAM is available in this repository. Use the \`zam\` skill in Claude Code to tur
6825
6899
 
6826
6900
  - Skill files live under \`.claude/skills/zam/\`.
6827
6901
  - Fast-changing review data lives in \`~/.zam/\`, not in this repository.
6828
- - Run \`zam setup --target . --agents claude --force\` after upgrading ZAM to refresh the skill.`,
6902
+ - The skill directory is linked to this ZAM installation and updates with it.`,
6829
6903
  Boolean(opts.dryRun)
6830
6904
  );
6831
6905
  logInstructionAction(action, "CLAUDE.md", Boolean(opts.dryRun));
@@ -6875,7 +6949,7 @@ ZAM is available in this repository. Select the \`zam\` skill through \`/skills\
6875
6949
 
6876
6950
  - Skill files live under \`.agents/skills/zam/\`.
6877
6951
  - Fast-changing review data lives in \`~/.zam/\`, not in this repository.
6878
- - Run \`zam setup --target . --agents copilot,codex --force\` after upgrading ZAM to refresh the skill.`,
6952
+ - The skill directory is linked to this ZAM installation and updates with it.`,
6879
6953
  Boolean(opts.dryRun)
6880
6954
  );
6881
6955
  logInstructionAction(action, "AGENTS.md", Boolean(opts.dryRun));
@@ -6922,10 +6996,10 @@ function writeCopilotInstructions(cwd = process.cwd(), opts = {}) {
6922
6996
  dest,
6923
6997
  `## ZAM learning sessions
6924
6998
 
6925
- ZAM is available in this repository through \`.agents/skills/zam/\`. Use the \`zam\` skill from Copilot-compatible skill selection surfaces to turn real work into an observed learning session with active recall and FSRS scheduling.
6999
+ 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.
6926
7000
 
6927
7001
  - Fast-changing review data lives in \`~/.zam/\`, not in this repository.
6928
- - Run \`zam setup --target . --agents copilot --force\` after upgrading ZAM to refresh the skill.`,
7002
+ - The skill directory is linked to this ZAM installation and updates with it.`,
6929
7003
  Boolean(opts.dryRun)
6930
7004
  );
6931
7005
  logInstructionAction(
@@ -6935,12 +7009,8 @@ ZAM is available in this repository through \`.agents/skills/zam/\`. Use the \`z
6935
7009
  );
6936
7010
  }
6937
7011
  var setupCommand = new Command2("setup").description(
6938
- "Distribute ZAM skill files into this personal instance and initialize the database"
6939
- ).option(
6940
- "--force",
6941
- "overwrite existing skill files (use after upgrading zam)",
6942
- false
6943
- ).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(
7012
+ "Link ZAM skill directories into this workspace and initialize the database"
7013
+ ).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(
6944
7014
  "--agents <list>",
6945
7015
  "comma-separated agents to wire: all, claude, copilot, codex, agent"
6946
7016
  ).option(
@@ -6962,7 +7032,10 @@ var setupCommand = new Command2("setup").description(
6962
7032
  `Setting up ZAM in ${target}${opts.dryRun ? " (dry run)" : ""}
6963
7033
  `
6964
7034
  );
6965
- copySkills(opts.force, target, agents, opts.dryRun);
7035
+ wireSkills(target, agents, {
7036
+ force: opts.force,
7037
+ dryRun: opts.dryRun
7038
+ });
6966
7039
  await initDatabase(opts.skipInit || opts.dryRun);
6967
7040
  if (agents.has("claude")) {
6968
7041
  writeClaudeMd(opts.skipClaudeMd, target, {
@@ -6970,7 +7043,7 @@ var setupCommand = new Command2("setup").description(
6970
7043
  updateExisting: updateExistingInstructions
6971
7044
  });
6972
7045
  }
6973
- if (agents.has("copilot") || agents.has("codex") || agents.has("agent")) {
7046
+ if (agents.has("codex") || agents.has("agent")) {
6974
7047
  writeAgentsMd(opts.skipAgentsMd, target, {
6975
7048
  dryRun: opts.dryRun,
6976
7049
  updateExisting: updateExistingInstructions
@@ -6983,12 +7056,38 @@ var setupCommand = new Command2("setup").description(
6983
7056
  });
6984
7057
  }
6985
7058
  console.log(
6986
- "\nDone. Run `zam whoami --set <your-id>` to set your identity. Start the `zam` skill with `/zam` in Claude/Gemini-compatible clients or `$zam` (or `/skills`) in Codex."
7059
+ "\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."
6987
7060
  );
6988
7061
  }
6989
7062
  );
6990
7063
 
7064
+ // src/cli/commands/shared/db.ts
7065
+ function defaultErrorHandler(message) {
7066
+ console.error("Error:", message);
7067
+ process.exit(1);
7068
+ }
7069
+ async function withDb(fn, onError = defaultErrorHandler) {
7070
+ let db;
7071
+ try {
7072
+ db = await openDatabase();
7073
+ await fn(db);
7074
+ } catch (err) {
7075
+ onError(err.message);
7076
+ } finally {
7077
+ await db?.close();
7078
+ }
7079
+ }
7080
+ function jsonOut(data) {
7081
+ console.log(JSON.stringify(data, null, 2));
7082
+ }
7083
+
6991
7084
  // src/cli/commands/workspace.ts
7085
+ import { execFileSync as execFileSync3 } from "child_process";
7086
+ import { existsSync as existsSync16, mkdirSync as mkdirSync9, writeFileSync as writeFileSync8 } from "fs";
7087
+ import { homedir as homedir9 } from "os";
7088
+ import { join as join16, resolve as resolve4 } from "path";
7089
+ import { confirm, input } from "@inquirer/prompts";
7090
+ import { Command as Command3 } from "commander";
6992
7091
  function runGit(cwd, args) {
6993
7092
  try {
6994
7093
  return execFileSync3("git", args, {
@@ -7105,13 +7204,25 @@ workspaceCommand.command("add <id>").description("Register an existing directory
7105
7204
  ...parseScopes(opts.scopes) ? { knowledgeScopes: parseScopes(opts.scopes) } : {},
7106
7205
  ...opts.defaultAgent ? { defaultAgent: opts.defaultAgent } : {}
7107
7206
  };
7207
+ wireSkills(path, parseSetupAgents());
7108
7208
  upsertConfiguredWorkspace(workspace);
7109
- console.log(`Registered workspace "${id}" at ${path}.`);
7209
+ console.log(`Registered and linked workspace "${id}" at ${path}.`);
7110
7210
  } catch (err) {
7111
7211
  console.error(`Error: ${err.message}`);
7112
7212
  process.exit(1);
7113
7213
  }
7114
7214
  });
7215
+ workspaceCommand.command("remove <id>").description("Unregister a ZAM workspace without deleting its files").action((id) => {
7216
+ const existing = getConfiguredWorkspaces().find((item) => item.id === id);
7217
+ if (!existing) {
7218
+ console.error(`Workspace "${id}" is not configured.`);
7219
+ process.exit(1);
7220
+ }
7221
+ removeConfiguredWorkspace(id);
7222
+ console.log(
7223
+ `Unregistered workspace "${id}". Files in ${existing.path} were not changed.`
7224
+ );
7225
+ });
7115
7226
  workspaceCommand.command("setup <id>").description("Install ZAM skills into a configured workspace").option(
7116
7227
  "--agents <list>",
7117
7228
  "comma-separated agents to wire: all, claude, copilot, codex, agent"
@@ -7131,19 +7242,17 @@ workspaceCommand.command("setup <id>").description("Install ZAM skills into a co
7131
7242
  `Setting up workspace "${workspace.id}" in ${workspace.path}${opts.dryRun ? " (dry run)" : ""}
7132
7243
  `
7133
7244
  );
7134
- copySkills(
7135
- Boolean(opts.force),
7136
- workspace.path,
7137
- agents,
7138
- Boolean(opts.dryRun)
7139
- );
7245
+ wireSkills(workspace.path, agents, {
7246
+ force: Boolean(opts.force),
7247
+ dryRun: Boolean(opts.dryRun)
7248
+ });
7140
7249
  if (agents.has("claude")) {
7141
7250
  writeClaudeMd(false, workspace.path, {
7142
7251
  dryRun: Boolean(opts.dryRun),
7143
7252
  updateExisting: true
7144
7253
  });
7145
7254
  }
7146
- if (agents.has("copilot") || agents.has("codex") || agents.has("agent")) {
7255
+ if (agents.has("codex") || agents.has("agent")) {
7147
7256
  writeAgentsMd(false, workspace.path, {
7148
7257
  dryRun: Boolean(opts.dryRun),
7149
7258
  updateExisting: true
@@ -7447,6 +7556,37 @@ bridgeCommand.command("workspace-info").description("Report the workspace dir, i
7447
7556
  });
7448
7557
  });
7449
7558
  });
7559
+ function sameWorkspacePath(left, right) {
7560
+ const normalize = (value) => {
7561
+ const path = resolve5(value);
7562
+ return process.platform === "win32" ? path.toLowerCase() : path;
7563
+ };
7564
+ return normalize(left) === normalize(right);
7565
+ }
7566
+ async function ensureDesktopWorkspace(db) {
7567
+ const configured = getConfiguredWorkspaces();
7568
+ const savedWorkspaceDir = await getSetting(db, "personal.workspace_dir");
7569
+ const workspaceDir = savedWorkspaceDir || configured.find((workspace) => workspace.kind === "personal")?.path || join17(homedir10(), "Documents", "zam");
7570
+ mkdirSync10(workspaceDir, { recursive: true });
7571
+ if (!savedWorkspaceDir) {
7572
+ await setSetting(db, "personal.workspace_dir", workspaceDir);
7573
+ }
7574
+ if (!configured.some(
7575
+ (workspace) => sameWorkspacePath(workspace.path, workspaceDir)
7576
+ )) {
7577
+ const id = configured.some((workspace) => workspace.id === "personal") ? workspaceIdFromPath(workspaceDir) : "personal";
7578
+ upsertConfiguredWorkspace({
7579
+ id,
7580
+ label: basename5(workspaceDir) || "ZAM",
7581
+ kind: "personal",
7582
+ path: workspaceDir
7583
+ });
7584
+ }
7585
+ const skillLinks = getConfiguredWorkspaces().flatMap(
7586
+ (workspace) => existsSync17(workspace.path) ? wireSkills(workspace.path, parseSetupAgents(), { quiet: true }) : []
7587
+ );
7588
+ return { workspaceDir, skillLinks };
7589
+ }
7450
7590
  bridgeCommand.command("workspace-list").description("List configured ZAM workspaces (JSON)").action(async () => {
7451
7591
  await withDb2(async (db) => {
7452
7592
  const workspaceDir = await getSetting(db, "personal.workspace_dir") || null;
@@ -7487,6 +7627,7 @@ bridgeCommand.command("workspace-add").description("Register an existing directo
7487
7627
  kind: parseBridgeWorkspaceKind(opts.kind),
7488
7628
  path
7489
7629
  };
7630
+ const skillLinks = wireSkills(path, parseSetupAgents(), { quiet: true });
7490
7631
  await withDb2(async (db) => {
7491
7632
  await setSetting(db, "personal.workspace_dir", path);
7492
7633
  upsertConfiguredWorkspace(workspace);
@@ -7494,7 +7635,39 @@ bridgeCommand.command("workspace-add").description("Register an existing directo
7494
7635
  ok: true,
7495
7636
  workspace,
7496
7637
  workspaces: getConfiguredWorkspaces(),
7497
- workspaceDir: path
7638
+ workspaceDir: path,
7639
+ skillLinks
7640
+ });
7641
+ });
7642
+ });
7643
+ bridgeCommand.command("workspace-remove").description("Unregister a ZAM workspace without deleting its files (JSON)").requiredOption("--id <id>", "Workspace id").action(async (opts) => {
7644
+ const id = String(opts.id ?? "").trim();
7645
+ if (!id) jsonError("A non-empty --id is required");
7646
+ const workspace = getConfiguredWorkspaces().find((item) => item.id === id);
7647
+ if (!workspace) jsonError(`Workspace "${id}" is not configured`);
7648
+ await withDb2(async (db) => {
7649
+ const activeWorkspaceDir = await getSetting(db, "personal.workspace_dir") || null;
7650
+ const remaining = removeConfiguredWorkspace(id);
7651
+ let workspaceDir = activeWorkspaceDir;
7652
+ let skillLinks = [];
7653
+ if (activeWorkspaceDir && sameWorkspacePath(activeWorkspaceDir, workspace.path)) {
7654
+ if (remaining.length > 0) {
7655
+ workspaceDir = remaining[0].path;
7656
+ await setSetting(db, "personal.workspace_dir", workspaceDir);
7657
+ } else {
7658
+ workspaceDir = join17(homedir10(), "Documents", "zam");
7659
+ await setSetting(db, "personal.workspace_dir", workspaceDir);
7660
+ const ensured = await ensureDesktopWorkspace(db);
7661
+ workspaceDir = ensured.workspaceDir;
7662
+ skillLinks = ensured.skillLinks;
7663
+ }
7664
+ }
7665
+ jsonOut2({
7666
+ ok: true,
7667
+ removed: workspace,
7668
+ workspaces: getConfiguredWorkspaces(),
7669
+ workspaceDir,
7670
+ skillLinks
7498
7671
  });
7499
7672
  });
7500
7673
  });
@@ -7502,9 +7675,11 @@ bridgeCommand.command("set-workspace-dir").description("Set the personal workspa
7502
7675
  const raw = String(opts.dir ?? "").trim();
7503
7676
  if (!raw) jsonError("A non-empty --dir is required");
7504
7677
  const dir = resolve5(raw);
7678
+ if (!existsSync17(dir)) jsonError(`Workspace path does not exist: ${dir}`);
7679
+ const skillLinks = wireSkills(dir, parseSetupAgents(), { quiet: true });
7505
7680
  await withDb2(async (db) => {
7506
7681
  await setSetting(db, "personal.workspace_dir", dir);
7507
- jsonOut2({ ok: true, workspaceDir: dir });
7682
+ jsonOut2({ ok: true, workspaceDir: dir, skillLinks });
7508
7683
  });
7509
7684
  });
7510
7685
  bridgeCommand.command("agent-list").description("List agent harnesses with detection state + the default (JSON)").action(async () => {
@@ -7932,7 +8107,7 @@ bridgeCommand.command("discover-skills").description(
7932
8107
  const monitorDir = join17(homedir10(), ".zam", "monitor");
7933
8108
  let files;
7934
8109
  try {
7935
- files = readdirSync2(monitorDir).filter((f) => f.endsWith(".jsonl"));
8110
+ files = readdirSync3(monitorDir).filter((f) => f.endsWith(".jsonl"));
7936
8111
  } catch {
7937
8112
  jsonOut2({ proposals: [], message: "No monitor logs found." });
7938
8113
  return;
@@ -8455,7 +8630,7 @@ bridgeCommand.command("capture-ui").description("Capture a screenshot for agent-
8455
8630
  if (!post.allowed) {
8456
8631
  if (!opts.output) {
8457
8632
  try {
8458
- rmSync2(outputPath, { force: true });
8633
+ rmSync3(outputPath, { force: true });
8459
8634
  } catch {
8460
8635
  }
8461
8636
  }
@@ -8594,7 +8769,7 @@ bridgeCommand.command("stop-recording").description(
8594
8769
  }
8595
8770
  const sessionId = opts.session;
8596
8771
  const statePath = join17(tmpdir3(), `zam-recording-${sessionId}.json`);
8597
- const { existsSync: existsSync25, readFileSync: readFileSync16, rmSync: rmSync3 } = await import("fs");
8772
+ const { existsSync: existsSync25, readFileSync: readFileSync16, rmSync: rmSync4 } = await import("fs");
8598
8773
  if (!existsSync25(statePath)) {
8599
8774
  jsonOut2({
8600
8775
  sessionId,
@@ -8619,7 +8794,7 @@ bridgeCommand.command("stop-recording").description(
8619
8794
  };
8620
8795
  let attempts = 0;
8621
8796
  while (isProcessRunning(pid) && attempts < 20) {
8622
- await new Promise((resolve8) => setTimeout(resolve8, 250));
8797
+ await new Promise((resolve9) => setTimeout(resolve9, 250));
8623
8798
  attempts++;
8624
8799
  }
8625
8800
  if (isProcessRunning(pid)) {
@@ -8629,7 +8804,7 @@ bridgeCommand.command("stop-recording").description(
8629
8804
  }
8630
8805
  }
8631
8806
  try {
8632
- rmSync3(statePath, { force: true });
8807
+ rmSync4(statePath, { force: true });
8633
8808
  } catch {
8634
8809
  }
8635
8810
  if (!existsSync25(outputPath)) {
@@ -8658,7 +8833,7 @@ bridgeCommand.command("stop-recording").description(
8658
8833
  return;
8659
8834
  }
8660
8835
  try {
8661
- rmSync3(outputPath, { force: true });
8836
+ rmSync4(outputPath, { force: true });
8662
8837
  } catch {
8663
8838
  }
8664
8839
  jsonOut2({
@@ -8823,10 +8998,13 @@ bridgeCommand.command("desktop-bootstrap").description("Initialize first-run des
8823
8998
  await withDb2(async (db) => {
8824
8999
  const userId = await ensureDefaultUser(db, opts.user);
8825
9000
  const { enabled, url, model, locale } = await getLlmConfig(db);
9001
+ const { workspaceDir, skillLinks } = await ensureDesktopWorkspace(db);
8826
9002
  jsonOut2({
8827
9003
  userId,
8828
9004
  locale,
8829
- llm: { enabled, url, model }
9005
+ llm: { enabled, url, model },
9006
+ workspaceDir,
9007
+ skillLinks
8830
9008
  });
8831
9009
  });
8832
9010
  });
@@ -9510,7 +9688,7 @@ ZAM Auto-Stale Complete: Scanned ${changedFiles.length} file(s).`
9510
9688
  });
9511
9689
 
9512
9690
  // src/cli/commands/goal.ts
9513
- import { existsSync as existsSync19, mkdirSync as mkdirSync10 } from "fs";
9691
+ import { existsSync as existsSync19, mkdirSync as mkdirSync11 } from "fs";
9514
9692
  import { resolve as resolve6 } from "path";
9515
9693
  import { input as input3 } from "@inquirer/prompts";
9516
9694
  import { Command as Command8 } from "commander";
@@ -9636,7 +9814,7 @@ ${"\u2500".repeat(50)}`);
9636
9814
  goalCommand.command("create").description("Create a new goal").option("--slug <slug>", "Goal slug (used as filename)").option("--title <title>", "Goal title").option("--parent <slug>", "Parent goal slug").option("--description <text>", "Goal description").option("--json", "Output as JSON").action(async (opts) => {
9637
9815
  const goalsDir = await resolveGoalsDir();
9638
9816
  if (!existsSync19(goalsDir)) {
9639
- mkdirSync10(goalsDir, { recursive: true });
9817
+ mkdirSync11(goalsDir, { recursive: true });
9640
9818
  }
9641
9819
  let slug = opts.slug;
9642
9820
  let title = opts.title;
@@ -9695,9 +9873,9 @@ goalCommand.command("status <slug> <status>").description("Update a goal's statu
9695
9873
  });
9696
9874
 
9697
9875
  // src/cli/commands/init.ts
9698
- import { existsSync as existsSync20, mkdirSync as mkdirSync11, writeFileSync as writeFileSync10 } from "fs";
9876
+ import { existsSync as existsSync20, mkdirSync as mkdirSync12, writeFileSync as writeFileSync10 } from "fs";
9699
9877
  import { homedir as homedir11 } from "os";
9700
- import { join as join19 } from "path";
9878
+ import { join as join19, resolve as resolve7 } from "path";
9701
9879
  import { confirm as confirm2, input as input4 } from "@inquirer/prompts";
9702
9880
  import { Command as Command9 } from "commander";
9703
9881
  var HOME2 = homedir11();
@@ -9705,9 +9883,9 @@ function printLine(char = "\u2550", len = 60, color = "\x1B[36m") {
9705
9883
  console.log(`${color}${char.repeat(len)}\x1B[0m`);
9706
9884
  }
9707
9885
  function bootstrapSandboxWorkspace(workspaceDir) {
9708
- mkdirSync11(join19(workspaceDir, "beliefs"), { recursive: true });
9709
- mkdirSync11(join19(workspaceDir, "goals"), { recursive: true });
9710
- mkdirSync11(join19(workspaceDir, "skills"), { recursive: true });
9886
+ mkdirSync12(join19(workspaceDir, "beliefs"), { recursive: true });
9887
+ mkdirSync12(join19(workspaceDir, "goals"), { recursive: true });
9888
+ mkdirSync12(join19(workspaceDir, "skills"), { recursive: true });
9711
9889
  const worldviewFile = join19(workspaceDir, "beliefs", "worldview.md");
9712
9890
  if (!existsSync20(worldviewFile)) {
9713
9891
  writeFileSync10(
@@ -9747,12 +9925,21 @@ var initCommand = new Command9("init").description("Launch the guided interactiv
9747
9925
  printLine();
9748
9926
  console.log("\n\x1B[1m[1/5] Setting up Local Workspace Sandbox\x1B[0m");
9749
9927
  const defaultWorkspace = join19(HOME2, "Documents", "zam");
9750
- const workspacePath = await input4({
9751
- message: "Choose your ZAM workspace directory:",
9752
- default: defaultWorkspace
9753
- });
9928
+ const workspacePath = resolve7(
9929
+ await input4({
9930
+ message: "Choose your ZAM workspace directory:",
9931
+ default: defaultWorkspace
9932
+ })
9933
+ );
9754
9934
  try {
9755
9935
  bootstrapSandboxWorkspace(workspacePath);
9936
+ wireSkills(workspacePath, parseSetupAgents());
9937
+ upsertConfiguredWorkspace({
9938
+ id: "personal",
9939
+ label: "Personal",
9940
+ kind: "personal",
9941
+ path: workspacePath
9942
+ });
9756
9943
  console.log(
9757
9944
  `\x1B[32m\u2713 Local Sandbox created at: ${workspacePath}\x1B[0m`
9758
9945
  );
@@ -10732,7 +10919,7 @@ observerCommand.command("revoke <process>").description("Remove a process from o
10732
10919
 
10733
10920
  // src/cli/commands/profile.ts
10734
10921
  import { homedir as homedir12 } from "os";
10735
- import { dirname as dirname6, join as join20, resolve as resolve7 } from "path";
10922
+ import { dirname as dirname6, join as join20, resolve as resolve8 } from "path";
10736
10923
  import { Command as Command13 } from "commander";
10737
10924
  var C2 = {
10738
10925
  reset: "\x1B[0m",
@@ -10763,7 +10950,7 @@ var profileCommand = new Command13("profile").description("Show or change this m
10763
10950
  if (opts.mode) setInstallMode(opts.mode);
10764
10951
  db = await openDatabaseWithSync({ initialize: true });
10765
10952
  if (opts.dir) {
10766
- await setSetting(db, "personal.workspace_dir", resolve7(opts.dir));
10953
+ await setSetting(db, "personal.workspace_dir", resolve8(opts.dir));
10767
10954
  }
10768
10955
  const personalDir = await getSetting(db, "personal.workspace_dir") || defaultPersonalDir();
10769
10956
  await db.close();
@@ -11897,7 +12084,7 @@ skillCommand.command("add").description("Register a new agent skill").requiredOp
11897
12084
  });
11898
12085
 
11899
12086
  // src/cli/commands/snapshot.ts
11900
- import { existsSync as existsSync22, mkdirSync as mkdirSync12, readFileSync as readFileSync13, writeFileSync as writeFileSync11 } from "fs";
12087
+ import { existsSync as existsSync22, mkdirSync as mkdirSync13, readFileSync as readFileSync13, writeFileSync as writeFileSync11 } from "fs";
11901
12088
  import { homedir as homedir13 } from "os";
11902
12089
  import { dirname as dirname7, join as join21 } from "path";
11903
12090
  import { Command as Command19 } from "commander";
@@ -11931,7 +12118,7 @@ var exportCmd = new Command19("export").description("Write a portable SQL-text s
11931
12118
  const out = opts.out ?? join21(personalDir, "snapshots", defaultOutName());
11932
12119
  const dir = dirname7(out);
11933
12120
  if (dir && dir !== "." && !existsSync22(dir)) {
11934
- mkdirSync12(dir, { recursive: true });
12121
+ mkdirSync13(dir, { recursive: true });
11935
12122
  }
11936
12123
  writeFileSync11(out, snapshot, "utf-8");
11937
12124
  console.log(`Snapshot written: ${out}`);
@@ -12601,7 +12788,7 @@ ${C3.dim}After that, 'zam ui' launches it directly.${C3.reset}`
12601
12788
 
12602
12789
  // src/cli/commands/update.ts
12603
12790
  import { spawnSync as spawnSync2 } from "child_process";
12604
- import { existsSync as existsSync24, readFileSync as readFileSync14, realpathSync } from "fs";
12791
+ import { existsSync as existsSync24, readFileSync as readFileSync14, realpathSync as realpathSync2 } from "fs";
12605
12792
  import { dirname as dirname9, join as join23 } from "path";
12606
12793
  import { fileURLToPath as fileURLToPath4 } from "url";
12607
12794
  import { confirm as confirm4 } from "@inquirer/prompts";
@@ -12715,7 +12902,7 @@ var checkCmd = new Command23("check").description("Check whether a newer ZAM has
12715
12902
  }
12716
12903
  );
12717
12904
  function findSourceRepo() {
12718
- let dir = realpathSync(dirname9(fileURLToPath4(import.meta.url)));
12905
+ let dir = realpathSync2(dirname9(fileURLToPath4(import.meta.url)));
12719
12906
  let parent = dirname9(dir);
12720
12907
  while (parent !== dir) {
12721
12908
  if (existsSync24(join23(dir, ".git"))) return dir;