zam-core 0.10.3 → 0.10.4

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -5481,20 +5481,20 @@ import { homedir as homedir5 } from "os";
5481
5481
  import { join as join8 } from "path";
5482
5482
  import { fileURLToPath } from "url";
5483
5483
  function getPackageSkillPath(agent = "default") {
5484
- const packageRoot2 = [
5484
+ const packageRoot4 = [
5485
5485
  fileURLToPath(new URL("../../..", import.meta.url)),
5486
5486
  fileURLToPath(new URL("../..", import.meta.url)),
5487
5487
  fileURLToPath(new URL("..", import.meta.url))
5488
5488
  ].find((candidate) => existsSync7(join8(candidate, "package.json"))) ?? "";
5489
- if (!packageRoot2) return "";
5489
+ if (!packageRoot4) return "";
5490
5490
  if (agent === "codex") {
5491
- const codexPath = join8(packageRoot2, ".agents", "skills", "zam", "SKILL.md");
5491
+ const codexPath = join8(packageRoot4, ".agents", "skills", "zam", "SKILL.md");
5492
5492
  if (existsSync7(codexPath)) return codexPath;
5493
5493
  return "";
5494
5494
  }
5495
5495
  if (agent === "claude") {
5496
5496
  const claudePath = join8(
5497
- packageRoot2,
5497
+ packageRoot4,
5498
5498
  ".claude",
5499
5499
  "skills",
5500
5500
  "zam",
@@ -5503,9 +5503,9 @@ function getPackageSkillPath(agent = "default") {
5503
5503
  if (existsSync7(claudePath)) return claudePath;
5504
5504
  return "";
5505
5505
  }
5506
- let path = join8(packageRoot2, ".agent", "skills", "zam", "SKILL.md");
5506
+ let path = join8(packageRoot4, ".agent", "skills", "zam", "SKILL.md");
5507
5507
  if (existsSync7(path)) return path;
5508
- path = join8(packageRoot2, ".claude", "skills", "zam", "SKILL.md");
5508
+ path = join8(packageRoot4, ".claude", "skills", "zam", "SKILL.md");
5509
5509
  if (existsSync7(path)) return path;
5510
5510
  return "";
5511
5511
  }
@@ -5946,6 +5946,18 @@ function saveMachineAiConfig(ai, path = defaultConfigPath()) {
5946
5946
  config.ai = ai;
5947
5947
  saveInstallConfig(config, path);
5948
5948
  }
5949
+ function getAgentConnectAutoDone(path = defaultConfigPath()) {
5950
+ return loadInstallConfig(path).agent?.connectAutoDone === true;
5951
+ }
5952
+ function setAgentConnectAutoDone(done, path = defaultConfigPath()) {
5953
+ const config = loadInstallConfig(path);
5954
+ if (done) {
5955
+ config.agent = { ...config.agent ?? {}, connectAutoDone: true };
5956
+ } else if (config.agent) {
5957
+ delete config.agent.connectAutoDone;
5958
+ }
5959
+ saveInstallConfig(config, path);
5960
+ }
5949
5961
  function getConfiguredWorkspaces(path = defaultConfigPath()) {
5950
5962
  return loadInstallConfig(path).workspaces ?? [];
5951
5963
  }
@@ -6622,6 +6634,7 @@ __export(kernel_exports, {
6622
6634
  getActiveWorkspace: () => getActiveWorkspace,
6623
6635
  getActiveWorkspaceContext: () => getActiveWorkspaceContext,
6624
6636
  getActiveWorkspaceId: () => getActiveWorkspaceId,
6637
+ getAgentConnectAutoDone: () => getAgentConnectAutoDone,
6625
6638
  getAgentSkill: () => getAgentSkill,
6626
6639
  getAllSettings: () => getAllSettings,
6627
6640
  getAllSettingsDetailed: () => getAllSettingsDetailed,
@@ -6731,6 +6744,7 @@ __export(kernel_exports, {
6731
6744
  setADOCredentials: () => setADOCredentials,
6732
6745
  setActiveWorkspaceContext: () => setActiveWorkspaceContext,
6733
6746
  setActiveWorkspaceId: () => setActiveWorkspaceId,
6747
+ setAgentConnectAutoDone: () => setAgentConnectAutoDone,
6734
6748
  setInstallChannel: () => setInstallChannel,
6735
6749
  setInstallMode: () => setInstallMode,
6736
6750
  setProviderApiKey: () => setProviderApiKey,
@@ -6809,9 +6823,9 @@ var init_kernel = __esm({
6809
6823
 
6810
6824
  // src/cli/commands/mcp.ts
6811
6825
  init_kernel();
6812
- import { existsSync as existsSync17, readFileSync as readFileSync15 } from "fs";
6813
- import { dirname as dirname8, join as join21 } from "path";
6814
- import { fileURLToPath as fileURLToPath4 } from "url";
6826
+ import { existsSync as existsSync19, readFileSync as readFileSync17 } from "fs";
6827
+ import { dirname as dirname11, join as join23 } from "path";
6828
+ import { fileURLToPath as fileURLToPath6 } from "url";
6815
6829
  import {
6816
6830
  RESOURCE_MIME_TYPE,
6817
6831
  registerAppResource,
@@ -6854,6 +6868,7 @@ var DEFAULT_LLM_URL = "http://localhost:8000/v1";
6854
6868
  var DEFAULT_LLM_MAX_TOKENS = 1e4;
6855
6869
  var RECALL_QUESTION_MAX_OUTPUT_TOKENS = 400;
6856
6870
  var RECALL_EVALUATION_MAX_OUTPUT_TOKENS = 1200;
6871
+ var RECALL_DISCUSSION_MAX_OUTPUT_TOKENS = 1200;
6857
6872
  var RECALL_ENDPOINT_CACHE_MS = 6e4;
6858
6873
  var cachedRecallEndpoint = null;
6859
6874
  function recallEndpointSignature(cfg) {
@@ -7216,6 +7231,64 @@ Evaluation:`;
7216
7231
  providerName: endpoint.providerName
7217
7232
  };
7218
7233
  }
7234
+ async function discussReviewViaLLM(db, input) {
7235
+ const cfg = await getProviderForRole(db, "recall");
7236
+ const endpoint = await resolveUsableRecallEndpoint(db);
7237
+ const langName = LANGUAGE_NAMES[cfg.locale] || "English";
7238
+ const systemPrompt = `You are ZAM, a warm, precise, and encouraging skills trainer in a follow-up discussion about one flashcard.
7239
+ The learner has already answered, the reference answer is revealed, and your evaluation feedback was shown \u2014 nothing about this card is a spoiler anymore.
7240
+
7241
+ Guidelines:
7242
+ 1. Answer the learner's follow-up directly and concretely in ${langName}, grounded in the card's target concept, context, and source reference.
7243
+ 2. Stay scoped to this card and its concept. If the learner drifts to unrelated territory, answer briefly and steer back to the concept.
7244
+ 3. Keep replies conversational and short (a few sentences) unless the learner explicitly asks for depth. Plain text only \u2014 no markdown wrapper, headers, or bullet lists.
7245
+ 4. The self-rating is the learner's own choice. If asked, explain the FSRS scale (1 forgot, 2 hard, 3 good, 4 easy) but never pressure them toward a specific rating.`;
7246
+ const cardFrame = `The card under discussion:
7247
+ Domain: ${input.domain}
7248
+ Slug: ${input.slug}
7249
+ Recall Question: ${input.question}
7250
+ Learner's Answer: ${input.userAnswer}
7251
+
7252
+ Target Concept (Correct Answer): ${input.concept}
7253
+ Target Context: ${input.context || "(none)"}
7254
+ ${input.sourceLinkContent ? `Source Code Reference:
7255
+ ${input.sourceLinkContent}` : ""}`;
7256
+ const messages = [
7257
+ { role: "system", content: systemPrompt },
7258
+ { role: "user", content: cardFrame }
7259
+ ];
7260
+ const feedback = input.feedback?.trim();
7261
+ if (feedback) {
7262
+ messages.push({ role: "assistant", content: feedback });
7263
+ }
7264
+ for (const turn of input.thread) {
7265
+ messages.push({ role: turn.role, content: turn.content });
7266
+ }
7267
+ messages.push({ role: "user", content: input.message });
7268
+ const res = await fetchWithInteractiveTimeout(
7269
+ `${endpoint.url}/chat/completions`,
7270
+ {
7271
+ method: "POST",
7272
+ headers: {
7273
+ "Content-Type": "application/json",
7274
+ Authorization: `Bearer ${endpoint.apiKey}`
7275
+ },
7276
+ body: JSON.stringify({
7277
+ model: endpoint.model,
7278
+ messages,
7279
+ temperature: 0.3,
7280
+ max_tokens: RECALL_DISCUSSION_MAX_OUTPUT_TOKENS
7281
+ }),
7282
+ locale: cfg.locale
7283
+ }
7284
+ );
7285
+ const text = await readChatContent(res, "LLM discussion");
7286
+ return {
7287
+ text,
7288
+ model: endpoint.model,
7289
+ providerName: endpoint.providerName
7290
+ };
7291
+ }
7219
7292
  var MAX_IMPORT_TEXT_CHARS = 2e5;
7220
7293
  var VALID_GENERATED_MODES = /* @__PURE__ */ new Set(["shadowing", "copilot", "autonomy"]);
7221
7294
  function parseGeneratedCardArray(responseText, label, limits) {
@@ -7803,7 +7876,7 @@ async function prepareRecallChain(db, opts) {
7803
7876
  } else {
7804
7877
  spawnLocalRunner(endpoint.url, endpoint.model, endpoint.runner);
7805
7878
  while (Date.now() < deadline) {
7806
- await new Promise((resolve5) => setTimeout(resolve5, 1e3));
7879
+ await new Promise((resolve6) => setTimeout(resolve6, 1e3));
7807
7880
  if (await isLlmOnline(endpoint.url)) {
7808
7881
  online = true;
7809
7882
  break;
@@ -8096,7 +8169,7 @@ async function startLocalRunner(url, model, locale, hint) {
8096
8169
  let attempts = 0;
8097
8170
  const dotsPerLine = 30;
8098
8171
  while (true) {
8099
- await new Promise((resolve5) => setTimeout(resolve5, 500));
8172
+ await new Promise((resolve6) => setTimeout(resolve6, 500));
8100
8173
  if (await isLlmOnline(url)) {
8101
8174
  if (attempts > 0) process.stdout.write("\n");
8102
8175
  return true;
@@ -8156,8 +8229,8 @@ async function fetchWithInteractiveTimeout(url, options = {}) {
8156
8229
  const dotsPerLine = 30;
8157
8230
  while (true) {
8158
8231
  let timeoutId;
8159
- const timeoutPromise = new Promise((resolve5) => {
8160
- timeoutId = setTimeout(() => resolve5("timeout"), timeoutMs);
8232
+ const timeoutPromise = new Promise((resolve6) => {
8233
+ timeoutId = setTimeout(() => resolve6("timeout"), timeoutMs);
8161
8234
  });
8162
8235
  const dotsInterval = setInterval(() => {
8163
8236
  process.stdout.write(".");
@@ -9501,11 +9574,11 @@ async function publishUiIntent(app, input = {}, opts = {}) {
9501
9574
 
9502
9575
  // src/cli/commands/bridge.ts
9503
9576
  init_kernel();
9504
- import { execFileSync as execFileSync3 } from "child_process";
9577
+ import { execFileSync as execFileSync4 } from "child_process";
9505
9578
  import { randomBytes as randomBytes2 } from "crypto";
9506
- import { existsSync as existsSync16, readdirSync as readdirSync2, readFileSync as readFileSync14, rmSync as rmSync3 } from "fs";
9507
- import { homedir as homedir11, tmpdir as tmpdir3 } from "os";
9508
- import { join as join20, resolve as resolve4 } from "path";
9579
+ import { existsSync as existsSync18, readdirSync as readdirSync2, readFileSync as readFileSync16, rmSync as rmSync3 } from "fs";
9580
+ import { homedir as homedir14, tmpdir as tmpdir3 } from "os";
9581
+ import { join as join22, resolve as resolve5 } from "path";
9509
9582
  import { Command } from "commander";
9510
9583
  import { ulid as ulid10 } from "ulid";
9511
9584
 
@@ -9584,7 +9657,7 @@ async function readWebLink(url) {
9584
9657
  redirect: "manual",
9585
9658
  signal: controller.signal,
9586
9659
  headers: {
9587
- "User-Agent": "ZAM-Content-Studio/0.10.0"
9660
+ "User-Agent": "ZAM-Content-Studio/0.10.4"
9588
9661
  }
9589
9662
  });
9590
9663
  if (res.status >= 300 && res.status < 400) {
@@ -9647,6 +9720,12 @@ async function readImageOCR(db, imagePath) {
9647
9720
  return extractTextFromScanViaLLM(db, imagePath);
9648
9721
  }
9649
9722
 
9723
+ // src/cli/agent-connect.ts
9724
+ init_kernel();
9725
+ import { mkdirSync as mkdirSync12, writeFileSync as writeFileSync10 } from "fs";
9726
+ import { homedir as homedir13 } from "os";
9727
+ import { dirname as dirname9 } from "path";
9728
+
9650
9729
  // src/cli/agent-harness.ts
9651
9730
  import { spawn as spawn2 } from "child_process";
9652
9731
  import { existsSync as existsSync14, readFileSync as readFileSync11 } from "fs";
@@ -9967,6 +10046,710 @@ function launchHarness(harness, opts) {
9967
10046
  console.log(`Launched ${harness.label} in ${opts.workspace}`);
9968
10047
  }
9969
10048
  }
10049
+ function detectInstalledConnectHarnesses(options = {}) {
10050
+ const home = options.home ?? homedir10();
10051
+ const platform = options.platform ?? process.platform;
10052
+ const find = options.find ?? findExecutable;
10053
+ const exists = options.exists ?? existsSync14;
10054
+ const detected = [];
10055
+ const hasCommandOrPath = (command, paths = []) => Boolean(find(command)) || paths.some((path) => exists(path));
10056
+ if (hasCommandOrPath("codex", [
10057
+ join16(home, ".codex"),
10058
+ ...platform === "darwin" ? ["/Applications/Codex.app"] : platform === "win32" ? [join16(home, "AppData", "Local", "Programs", "Codex")] : []
10059
+ ])) {
10060
+ detected.push("codex");
10061
+ }
10062
+ const vscodePaths = platform === "darwin" ? [
10063
+ "/Applications/Visual Studio Code.app/Contents/Resources/app/bin/code",
10064
+ join16(
10065
+ home,
10066
+ "Applications",
10067
+ "Visual Studio Code.app",
10068
+ "Contents",
10069
+ "Resources",
10070
+ "app",
10071
+ "bin",
10072
+ "code"
10073
+ )
10074
+ ] : platform === "win32" ? [
10075
+ join16(
10076
+ home,
10077
+ "AppData",
10078
+ "Local",
10079
+ "Programs",
10080
+ "Microsoft VS Code",
10081
+ "bin",
10082
+ "code.cmd"
10083
+ )
10084
+ ] : ["/usr/bin/code", "/usr/local/bin/code", "/snap/bin/code"];
10085
+ if (hasCommandOrPath("code", vscodePaths)) detected.push("vscode");
10086
+ const copilotHome = options.copilotHome ?? join16(home, ".copilot");
10087
+ if (hasCommandOrPath("copilot", [
10088
+ copilotHome,
10089
+ ...platform === "darwin" ? ["/Applications/GitHub Copilot.app"] : []
10090
+ ])) {
10091
+ detected.push("copilot");
10092
+ }
10093
+ if (hasCommandOrPath("opencode", [join16(home, ".config", "opencode")])) {
10094
+ detected.push("opencode");
10095
+ }
10096
+ if (hasCommandOrPath("goose", [join16(home, ".config", "goose")])) {
10097
+ detected.push("goose");
10098
+ }
10099
+ if (hasCommandOrPath("antigravity", [
10100
+ join16(home, ".gemini", "config"),
10101
+ ...platform === "darwin" ? [
10102
+ join16(
10103
+ home,
10104
+ ".antigravity-ide",
10105
+ "antigravity-ide",
10106
+ "bin",
10107
+ "antigravity-ide"
10108
+ ),
10109
+ "/Applications/Antigravity.app",
10110
+ "/Applications/Antigravity IDE.app"
10111
+ ] : []
10112
+ ]) || find("antigravity-ide")) {
10113
+ detected.push("antigravity");
10114
+ }
10115
+ const claudeDesktopPath = platform === "darwin" ? "/Applications/Claude.app" : platform === "win32" ? join16(home, "AppData", "Local", "AnthropicClaude") : join16(home, ".config", "Claude");
10116
+ if (exists(claudeDesktopPath)) detected.push("claude-desktop");
10117
+ return detected;
10118
+ }
10119
+ function parseMcpJsonConfig(path, content) {
10120
+ let parsed;
10121
+ try {
10122
+ parsed = JSON.parse(content);
10123
+ } catch (error) {
10124
+ throw new Error(
10125
+ `Cannot update ${path}: existing file is not valid JSON (${error instanceof Error ? error.message : String(error)})`
10126
+ );
10127
+ }
10128
+ if (typeof parsed !== "object" || parsed === null || Array.isArray(parsed)) {
10129
+ throw new Error(`Cannot update ${path}: expected a JSON object`);
10130
+ }
10131
+ const config = parsed;
10132
+ if (config.mcpServers !== void 0 && (typeof config.mcpServers !== "object" || config.mcpServers === null || Array.isArray(config.mcpServers))) {
10133
+ throw new Error(`Cannot update ${path}: mcpServers must be a JSON object`);
10134
+ }
10135
+ return config;
10136
+ }
10137
+ function connectHarnessMcp(harnessId, opts) {
10138
+ const exists = (p) => {
10139
+ if (opts.readFile) {
10140
+ try {
10141
+ opts.readFile(p);
10142
+ return true;
10143
+ } catch {
10144
+ return false;
10145
+ }
10146
+ }
10147
+ return existsSync14(p);
10148
+ };
10149
+ const read = (p) => {
10150
+ if (opts.readFile) return opts.readFile(p);
10151
+ return readFileSync11(p, "utf-8");
10152
+ };
10153
+ let targetPath = "";
10154
+ let content = "";
10155
+ let alreadyConfigured = false;
10156
+ let hint = "";
10157
+ const mergeMcpServersJson = (path) => {
10158
+ let existing = {};
10159
+ if (exists(path)) {
10160
+ existing = parseMcpJsonConfig(path, read(path));
10161
+ }
10162
+ if (!existing.mcpServers) {
10163
+ existing.mcpServers = {};
10164
+ }
10165
+ existing.mcpServers.zam = {
10166
+ command: opts.zamPath,
10167
+ args: ["mcp"]
10168
+ };
10169
+ return JSON.stringify(existing, null, 2);
10170
+ };
10171
+ if (harnessId === "claude-code") {
10172
+ targetPath = join16(opts.cwd, ".mcp.json");
10173
+ hint = "Claude Code will prompt you to approve the 'zam' MCP server on next launch.";
10174
+ content = mergeMcpServersJson(targetPath);
10175
+ } else if (harnessId === "claude-desktop") {
10176
+ const platform = opts.platform ?? process.platform;
10177
+ targetPath = platform === "win32" ? join16(
10178
+ opts.home,
10179
+ "AppData",
10180
+ "Roaming",
10181
+ "Claude",
10182
+ "claude_desktop_config.json"
10183
+ ) : platform === "darwin" ? join16(
10184
+ opts.home,
10185
+ "Library",
10186
+ "Application Support",
10187
+ "Claude",
10188
+ "claude_desktop_config.json"
10189
+ ) : join16(opts.home, ".config", "Claude", "claude_desktop_config.json");
10190
+ hint = "Restart Claude Desktop to load the 'zam' MCP server; MCP Apps panels render inline in the chat.";
10191
+ content = mergeMcpServersJson(targetPath);
10192
+ } else if (harnessId === "antigravity") {
10193
+ targetPath = join16(opts.home, ".gemini", "config", "mcp_config.json");
10194
+ hint = "Shared config read by Antigravity CLI and IDE (2.0+); older IDE builds read ~/.gemini/antigravity/mcp_config.json instead. Refresh Installed MCP Servers; the first tool call may still require approval.";
10195
+ let existing = {};
10196
+ if (exists(targetPath)) {
10197
+ existing = parseMcpJsonConfig(targetPath, read(targetPath));
10198
+ }
10199
+ if (!existing.mcpServers) {
10200
+ existing.mcpServers = {};
10201
+ }
10202
+ existing.mcpServers.zam = {
10203
+ command: opts.zamPath,
10204
+ args: ["mcp"]
10205
+ };
10206
+ content = JSON.stringify(existing, null, 2);
10207
+ } else if (harnessId === "opencode") {
10208
+ targetPath = join16(opts.home, ".config", "opencode", "opencode.json");
10209
+ hint = "OpenCode will load the enabled 'zam' MCP server on next launch.";
10210
+ let existing = {};
10211
+ if (exists(targetPath)) {
10212
+ existing = parseMcpJsonConfig(targetPath, read(targetPath));
10213
+ }
10214
+ const mcp = existing.mcp;
10215
+ if (mcp !== void 0 && (typeof mcp !== "object" || mcp === null || Array.isArray(mcp))) {
10216
+ throw new Error(`Cannot update ${targetPath}: mcp must be a JSON object`);
10217
+ }
10218
+ const servers = mcp ?? {};
10219
+ servers.zam = {
10220
+ type: "local",
10221
+ command: [opts.zamPath, "mcp"],
10222
+ enabled: true
10223
+ };
10224
+ existing.mcp = servers;
10225
+ content = JSON.stringify(existing, null, 2);
10226
+ } else if (harnessId === "codex") {
10227
+ targetPath = join16(opts.home, ".codex", "config.toml");
10228
+ hint = "Codex will prompt for tool execution approvals or respect the TOML approval modes.";
10229
+ let existingStr = "";
10230
+ if (exists(targetPath)) {
10231
+ existingStr = read(targetPath);
10232
+ }
10233
+ if (existingStr.includes("[mcp_servers.zam]")) {
10234
+ alreadyConfigured = true;
10235
+ content = existingStr;
10236
+ } else {
10237
+ const block = `
10238
+ [mcp_servers.zam]
10239
+ command = ${JSON.stringify(opts.zamPath)}
10240
+ args = ["mcp"]
10241
+ default_tools_approval_mode = "approve"
10242
+
10243
+ [mcp_servers.zam.tools.zam_review_action]
10244
+ approval_mode = "prompt"
10245
+ `;
10246
+ content = existingStr ? `${existingStr.trimEnd()}
10247
+ ${block}` : block;
10248
+ }
10249
+ } else if (harnessId === "vscode") {
10250
+ const platform = opts.platform ?? process.platform;
10251
+ targetPath = platform === "win32" ? join16(opts.home, "AppData", "Roaming", "Code", "User", "mcp.json") : platform === "darwin" ? join16(
10252
+ opts.home,
10253
+ "Library",
10254
+ "Application Support",
10255
+ "Code",
10256
+ "User",
10257
+ "mcp.json"
10258
+ ) : join16(opts.home, ".config", "Code", "User", "mcp.json");
10259
+ hint = "Reload VS Code after setup. ZAM Companion stays separate from the Codex chat and can be moved to any panel or sidebar.";
10260
+ let existing = {};
10261
+ if (exists(targetPath)) {
10262
+ existing = parseMcpJsonConfig(targetPath, read(targetPath));
10263
+ }
10264
+ if (existing.servers !== void 0 && (typeof existing.servers !== "object" || existing.servers === null || Array.isArray(existing.servers))) {
10265
+ throw new Error(
10266
+ `Cannot update ${targetPath}: servers must be a JSON object`
10267
+ );
10268
+ }
10269
+ if (existing.inputs !== void 0 && !Array.isArray(existing.inputs)) {
10270
+ throw new Error(`Cannot update ${targetPath}: inputs must be an array`);
10271
+ }
10272
+ if (!existing.servers) existing.servers = {};
10273
+ const expectedServer = { command: opts.zamPath, args: ["mcp"] };
10274
+ const currentServer = existing.servers.zam;
10275
+ alreadyConfigured = Array.isArray(existing.inputs) && typeof currentServer === "object" && currentServer !== null && !Array.isArray(currentServer) && JSON.stringify(currentServer) === JSON.stringify(expectedServer);
10276
+ existing.servers.zam = expectedServer;
10277
+ if (!existing.inputs) existing.inputs = [];
10278
+ content = JSON.stringify(existing, null, 2);
10279
+ } else if (harnessId === "goose") {
10280
+ targetPath = join16(opts.home, ".config", "goose", "config.yaml");
10281
+ hint = "goose will load the 'zam' extension on next session start. Run 'goose configure' to manage extensions.";
10282
+ const zamExtension = [
10283
+ " zam:",
10284
+ " name: ZAM",
10285
+ ` cmd: ${opts.zamPath}`,
10286
+ " args:",
10287
+ " - mcp",
10288
+ " enabled: true",
10289
+ " type: stdio",
10290
+ " timeout: 300",
10291
+ " description: Symbiotic learning agent with spaced repetition"
10292
+ ].join("\n");
10293
+ let existingStr = "";
10294
+ if (exists(targetPath)) {
10295
+ existingStr = read(targetPath);
10296
+ }
10297
+ if (/^\s+zam:\s*$/m.test(existingStr) && existingStr.includes("- mcp")) {
10298
+ alreadyConfigured = true;
10299
+ content = existingStr;
10300
+ } else if (/^extensions:[ \t]*$/m.test(existingStr)) {
10301
+ content = existingStr.replace(
10302
+ /^extensions:[ \t]*$/m,
10303
+ (line) => `${line}
10304
+ ${zamExtension}`
10305
+ );
10306
+ } else if (existingStr.trim()) {
10307
+ content = `${existingStr.trimEnd()}
10308
+ extensions:
10309
+ ${zamExtension}
10310
+ `;
10311
+ } else {
10312
+ content = `extensions:
10313
+ ${zamExtension}
10314
+ `;
10315
+ }
10316
+ } else if (harnessId === "copilot") {
10317
+ targetPath = join16(
10318
+ opts.copilotHome ?? join16(opts.home, ".copilot"),
10319
+ "mcp-config.json"
10320
+ );
10321
+ hint = "Restart GitHub Copilot or start a new session to load the 'zam' MCP server and its focused Recall, Graph, and Settings canvases.";
10322
+ let existing = {};
10323
+ if (exists(targetPath)) {
10324
+ existing = parseMcpJsonConfig(targetPath, read(targetPath));
10325
+ }
10326
+ if (!existing.mcpServers) {
10327
+ existing.mcpServers = {};
10328
+ }
10329
+ existing.mcpServers.zam = {
10330
+ type: "local",
10331
+ command: opts.zamPath,
10332
+ args: ["mcp"],
10333
+ tools: ["*"]
10334
+ };
10335
+ content = JSON.stringify(existing, null, 2);
10336
+ }
10337
+ return {
10338
+ path: targetPath,
10339
+ content,
10340
+ alreadyConfigured,
10341
+ hint
10342
+ };
10343
+ }
10344
+
10345
+ // src/cli/copilot-extension.ts
10346
+ import {
10347
+ existsSync as existsSync15,
10348
+ lstatSync,
10349
+ mkdirSync as mkdirSync10,
10350
+ readFileSync as readFileSync12,
10351
+ writeFileSync as writeFileSync8
10352
+ } from "fs";
10353
+ import { homedir as homedir11 } from "os";
10354
+ import { dirname as dirname7, join as join17, resolve as resolve4 } from "path";
10355
+ import { fileURLToPath as fileURLToPath3 } from "url";
10356
+ var EXTENSION_NAME = "zam-mcp-apps";
10357
+ var EXTENSION_FILES = [
10358
+ "extension.mjs",
10359
+ "host.bundle.js",
10360
+ "mcp-client.bundle.mjs",
10361
+ "manifest.json"
10362
+ ];
10363
+ var packageRoot = [
10364
+ fileURLToPath3(new URL("../..", import.meta.url)),
10365
+ fileURLToPath3(new URL("../../..", import.meta.url))
10366
+ ].find((candidate) => existsSync15(join17(candidate, "package.json"))) ?? fileURLToPath3(new URL("../..", import.meta.url));
10367
+ function defaultCliEntry() {
10368
+ return join17(dirname7(fileURLToPath3(import.meta.url)), "index.js");
10369
+ }
10370
+ function resolveCopilotHome(home = homedir11(), configuredHome = process.env.COPILOT_HOME) {
10371
+ return configuredHome?.trim() ? resolve4(configuredHome) : join17(home, ".copilot");
10372
+ }
10373
+ function resolveCopilotZamLaunch(zamPath, options = {}) {
10374
+ const cliEntry = options.cliEntry ?? defaultCliEntry();
10375
+ if (existsSync15(cliEntry)) {
10376
+ return {
10377
+ command: options.nodePath ?? process.execPath,
10378
+ args: [cliEntry, "mcp"]
10379
+ };
10380
+ }
10381
+ return {
10382
+ command: zamPath,
10383
+ args: ["mcp"]
10384
+ };
10385
+ }
10386
+ function planCopilotExtensionInstall(options) {
10387
+ const sourceDir = options.assetsDir ?? join17(packageRoot, "dist", "copilot-extension");
10388
+ for (const file of EXTENSION_FILES) {
10389
+ if (!existsSync15(join17(sourceDir, file))) {
10390
+ throw new Error(
10391
+ `Copilot MCP Apps asset is missing: ${join17(sourceDir, file)}. Run \`npm run build\` and retry.`
10392
+ );
10393
+ }
10394
+ }
10395
+ const manifest = JSON.parse(
10396
+ readFileSync12(join17(sourceDir, "manifest.json"), "utf8")
10397
+ );
10398
+ if (manifest.name !== EXTENSION_NAME || typeof manifest.version !== "string" || !manifest.version) {
10399
+ throw new Error(
10400
+ `Invalid Copilot MCP Apps manifest: ${join17(sourceDir, "manifest.json")}`
10401
+ );
10402
+ }
10403
+ const copilotHome = resolveCopilotHome(
10404
+ options.home,
10405
+ options.copilotHome ?? process.env.COPILOT_HOME
10406
+ );
10407
+ return {
10408
+ sourceDir,
10409
+ destinationDir: join17(copilotHome, "extensions", EXTENSION_NAME),
10410
+ launch: resolveCopilotZamLaunch(options.zamPath, options),
10411
+ files: EXTENSION_FILES
10412
+ };
10413
+ }
10414
+ function writeIfChanged(path, content) {
10415
+ const next = Buffer.isBuffer(content) ? content : Buffer.from(content);
10416
+ if (existsSync15(path) && readFileSync12(path).equals(next)) return false;
10417
+ writeFileSync8(path, next);
10418
+ return true;
10419
+ }
10420
+ function installCopilotExtension(options) {
10421
+ const plan = planCopilotExtensionInstall(options);
10422
+ if (options.dryRun) {
10423
+ return { ...plan, action: "planned", changedFiles: [] };
10424
+ }
10425
+ const destinationExisted = existsSync15(plan.destinationDir);
10426
+ if (destinationExisted && lstatSync(plan.destinationDir).isSymbolicLink()) {
10427
+ throw new Error(
10428
+ `Refusing to replace symlinked Copilot extension directory: ${plan.destinationDir}`
10429
+ );
10430
+ }
10431
+ mkdirSync10(plan.destinationDir, { recursive: true });
10432
+ const changedFiles = [];
10433
+ for (const file of plan.files) {
10434
+ const source = join17(plan.sourceDir, file);
10435
+ const destination = join17(plan.destinationDir, file);
10436
+ if (writeIfChanged(destination, readFileSync12(source))) {
10437
+ changedFiles.push(file);
10438
+ }
10439
+ }
10440
+ const launchContent = `${JSON.stringify(
10441
+ {
10442
+ schemaVersion: 1,
10443
+ command: plan.launch.command,
10444
+ args: plan.launch.args
10445
+ },
10446
+ null,
10447
+ 2
10448
+ )}
10449
+ `;
10450
+ if (writeIfChanged(join17(plan.destinationDir, "launch.json"), launchContent)) {
10451
+ changedFiles.push("launch.json");
10452
+ }
10453
+ return {
10454
+ ...plan,
10455
+ action: !destinationExisted ? "installed" : changedFiles.length > 0 ? "updated" : "unchanged",
10456
+ changedFiles
10457
+ };
10458
+ }
10459
+
10460
+ // src/cli/vscode-extension.ts
10461
+ import { execFileSync as execFileSync3 } from "child_process";
10462
+ import { existsSync as existsSync16, mkdirSync as mkdirSync11, readFileSync as readFileSync13, writeFileSync as writeFileSync9 } from "fs";
10463
+ import { homedir as homedir12 } from "os";
10464
+ import { dirname as dirname8, join as join18 } from "path";
10465
+ import { fileURLToPath as fileURLToPath4 } from "url";
10466
+ var packageRoot2 = [
10467
+ fileURLToPath4(new URL("../..", import.meta.url)),
10468
+ fileURLToPath4(new URL("../../..", import.meta.url))
10469
+ ].find((candidate) => existsSync16(join18(candidate, "package.json"))) ?? fileURLToPath4(new URL("../..", import.meta.url));
10470
+ function resolveVscodeExecutable(options = {}) {
10471
+ const home = options.home ?? homedir12();
10472
+ const platform = options.platform ?? process.platform;
10473
+ const find = options.find ?? findExecutable;
10474
+ const exists = options.exists ?? existsSync16;
10475
+ const found = find("code");
10476
+ if (found) return found;
10477
+ const candidates = platform === "darwin" ? [
10478
+ "/Applications/Visual Studio Code.app/Contents/Resources/app/bin/code",
10479
+ join18(
10480
+ home,
10481
+ "Applications",
10482
+ "Visual Studio Code.app",
10483
+ "Contents",
10484
+ "Resources",
10485
+ "app",
10486
+ "bin",
10487
+ "code"
10488
+ )
10489
+ ] : platform === "win32" ? [
10490
+ join18(
10491
+ home,
10492
+ "AppData",
10493
+ "Local",
10494
+ "Programs",
10495
+ "Microsoft VS Code",
10496
+ "bin",
10497
+ "code.cmd"
10498
+ )
10499
+ ] : ["/usr/bin/code", "/usr/local/bin/code", "/snap/bin/code"];
10500
+ return candidates.find((candidate) => exists(candidate)) ?? null;
10501
+ }
10502
+ function packageVersion() {
10503
+ const parsed = JSON.parse(
10504
+ readFileSync13(join18(packageRoot2, "package.json"), "utf8")
10505
+ );
10506
+ if (typeof parsed.version !== "string" || !parsed.version) {
10507
+ throw new Error("Cannot determine the ZAM package version");
10508
+ }
10509
+ return parsed.version;
10510
+ }
10511
+ function planVscodeExtensionInstall(options) {
10512
+ const home = options.home ?? homedir12();
10513
+ const version = options.version ?? packageVersion();
10514
+ const assetsDir = options.assetsDir ?? join18(packageRoot2, "dist", "vscode-extension");
10515
+ const vsixPath = join18(assetsDir, `ZAM_Companion_${version}.vsix`);
10516
+ if (!existsSync16(vsixPath)) {
10517
+ throw new Error(
10518
+ `ZAM Companion VSIX asset is missing: ${vsixPath}. Run \`npm run build\` and retry.`
10519
+ );
10520
+ }
10521
+ const codePath = options.codePath ?? resolveVscodeExecutable({
10522
+ home,
10523
+ platform: options.platform,
10524
+ find: options.find,
10525
+ exists: options.exists
10526
+ });
10527
+ if (!codePath) {
10528
+ throw new Error(
10529
+ "Visual Studio Code was not detected. Install VS Code or add its 'code' command to PATH."
10530
+ );
10531
+ }
10532
+ return {
10533
+ version,
10534
+ vsixPath,
10535
+ codePath,
10536
+ launchConfigPath: join18(home, ".zam", "vscode-launch.json"),
10537
+ launch: { command: options.zamPath, args: ["mcp"] }
10538
+ };
10539
+ }
10540
+ function installVscodeExtension(options) {
10541
+ const plan = planVscodeExtensionInstall(options);
10542
+ if (options.dryRun) return { ...plan, action: "planned" };
10543
+ const launchContent = `${JSON.stringify(
10544
+ {
10545
+ schemaVersion: 1,
10546
+ version: plan.version,
10547
+ command: plan.launch.command,
10548
+ args: plan.launch.args
10549
+ },
10550
+ null,
10551
+ 2
10552
+ )}
10553
+ `;
10554
+ const existed = existsSync16(plan.launchConfigPath);
10555
+ const changed = !existed || readFileSync13(plan.launchConfigPath, "utf8") !== launchContent;
10556
+ const run = options.run ?? ((command, args) => {
10557
+ execFileSync3(command, args, { stdio: "pipe" });
10558
+ });
10559
+ run(plan.codePath, ["--install-extension", plan.vsixPath, "--force"]);
10560
+ if (changed) {
10561
+ mkdirSync11(dirname8(plan.launchConfigPath), { recursive: true });
10562
+ writeFileSync9(plan.launchConfigPath, launchContent, "utf8");
10563
+ }
10564
+ return {
10565
+ ...plan,
10566
+ action: !existed ? "installed" : changed ? "updated" : "unchanged"
10567
+ };
10568
+ }
10569
+
10570
+ // src/cli/agent-connect.ts
10571
+ var CONNECT_HARNESSES = [
10572
+ "claude-code",
10573
+ "claude-desktop",
10574
+ "antigravity",
10575
+ "codex",
10576
+ "vscode",
10577
+ "opencode",
10578
+ "goose",
10579
+ "copilot"
10580
+ ];
10581
+ var USER_SCOPED_CONNECT_HARNESSES = [
10582
+ "claude-desktop",
10583
+ "antigravity",
10584
+ "codex",
10585
+ "vscode",
10586
+ "opencode",
10587
+ "goose",
10588
+ "copilot"
10589
+ ];
10590
+ var CONNECT_HARNESS_LABELS = {
10591
+ "claude-code": "Claude Code",
10592
+ "claude-desktop": "Claude Desktop",
10593
+ antigravity: "Antigravity",
10594
+ codex: "Codex",
10595
+ vscode: "VS Code",
10596
+ opencode: "OpenCode",
10597
+ goose: "Goose",
10598
+ copilot: "GitHub Copilot"
10599
+ };
10600
+ function isConnectHarnessId(value) {
10601
+ return CONNECT_HARNESSES.includes(value);
10602
+ }
10603
+ function resolveDeps(deps) {
10604
+ const home = deps.home ?? homedir13();
10605
+ const cwd = deps.cwd ?? process.cwd();
10606
+ const copilotHome = deps.copilotHome ?? process.env.COPILOT_HOME;
10607
+ return {
10608
+ home,
10609
+ cwd,
10610
+ copilotHome,
10611
+ findZam: deps.findZam ?? (() => findExecutable("zam")),
10612
+ detect: deps.detect ?? (() => detectInstalledConnectHarnesses({ home, copilotHome })),
10613
+ connectMcp: deps.connectMcp ?? connectHarnessMcp,
10614
+ writeConfig: deps.writeConfig ?? ((path, content) => {
10615
+ mkdirSync12(dirname9(path), { recursive: true });
10616
+ writeFileSync10(path, content, "utf-8");
10617
+ }),
10618
+ installCopilot: deps.installCopilot ?? installCopilotExtension,
10619
+ installVscode: deps.installVscode ?? installVscodeExtension,
10620
+ resolveAntigravity: deps.resolveAntigravity ?? resolveAntigravityIdeExecutable,
10621
+ refreshSkills: deps.refreshSkills ?? distributeGlobalSkills
10622
+ };
10623
+ }
10624
+ function performAgentConnect(opts = {}, deps = {}) {
10625
+ const d = resolveDeps(deps);
10626
+ const dryRun = Boolean(opts.dryRun);
10627
+ const detected = opts.harness ? [opts.harness] : d.detect();
10628
+ const foundZam = d.findZam();
10629
+ const zamPath = foundZam ?? "zam";
10630
+ const results = [];
10631
+ for (const harness of detected) {
10632
+ const label = CONNECT_HARNESS_LABELS[harness];
10633
+ try {
10634
+ const prepared = d.connectMcp(harness, {
10635
+ zamPath,
10636
+ cwd: d.cwd,
10637
+ home: d.home,
10638
+ copilotHome: d.copilotHome
10639
+ });
10640
+ let extension = null;
10641
+ if (harness === "copilot") {
10642
+ const installed = d.installCopilot({
10643
+ home: d.home,
10644
+ zamPath,
10645
+ dryRun
10646
+ });
10647
+ extension = {
10648
+ kind: "copilot",
10649
+ action: installed.action,
10650
+ location: installed.destinationDir,
10651
+ detail: `${installed.launch.command} ${installed.launch.args.join(" ")}`
10652
+ };
10653
+ } else if (harness === "vscode") {
10654
+ const installed = d.installVscode({ home: d.home, zamPath, dryRun });
10655
+ extension = {
10656
+ kind: "vscode",
10657
+ action: installed.action,
10658
+ location: installed.vsixPath,
10659
+ detail: installed.launchConfigPath
10660
+ };
10661
+ } else if (harness === "antigravity") {
10662
+ const antigravityPath = d.resolveAntigravity();
10663
+ if (antigravityPath) {
10664
+ const installed = d.installVscode({
10665
+ home: d.home,
10666
+ zamPath,
10667
+ codePath: antigravityPath,
10668
+ dryRun
10669
+ });
10670
+ extension = {
10671
+ kind: "vscode",
10672
+ action: installed.action,
10673
+ location: installed.vsixPath,
10674
+ detail: installed.launchConfigPath
10675
+ };
10676
+ }
10677
+ }
10678
+ let wrote = false;
10679
+ if (!prepared.alreadyConfigured && !dryRun) {
10680
+ d.writeConfig(prepared.path, prepared.content);
10681
+ wrote = true;
10682
+ }
10683
+ results.push({
10684
+ harness,
10685
+ label,
10686
+ path: prepared.path,
10687
+ content: prepared.content,
10688
+ alreadyConfigured: prepared.alreadyConfigured,
10689
+ wrote,
10690
+ hint: prepared.hint,
10691
+ extension
10692
+ });
10693
+ } catch (error) {
10694
+ results.push({
10695
+ harness,
10696
+ label,
10697
+ path: "",
10698
+ content: "",
10699
+ alreadyConfigured: false,
10700
+ wrote: false,
10701
+ hint: "",
10702
+ extension: null,
10703
+ error: error instanceof Error ? error.message : String(error)
10704
+ });
10705
+ }
10706
+ }
10707
+ let skills = null;
10708
+ if (!dryRun && detected.length > 0) {
10709
+ const skillResults = d.refreshSkills(d.home);
10710
+ skills = {
10711
+ refreshed: skillResults.filter((result) => result.success).length,
10712
+ total: skillResults.length
10713
+ };
10714
+ }
10715
+ return {
10716
+ success: results.every((result) => !result.error),
10717
+ detected,
10718
+ zamPath,
10719
+ zamOnPath: Boolean(foundZam),
10720
+ results,
10721
+ skills
10722
+ };
10723
+ }
10724
+ function inspectConnectHarnesses(deps = {}) {
10725
+ const d = resolveDeps(deps);
10726
+ const foundZam = d.findZam();
10727
+ const zamPath = foundZam ?? "zam";
10728
+ const installed = new Set(d.detect());
10729
+ const harnesses = USER_SCOPED_CONNECT_HARNESSES.map((harness) => {
10730
+ const status = {
10731
+ harness,
10732
+ label: CONNECT_HARNESS_LABELS[harness],
10733
+ installed: installed.has(harness),
10734
+ configured: false,
10735
+ configPath: ""
10736
+ };
10737
+ try {
10738
+ const probe = d.connectMcp(harness, {
10739
+ zamPath,
10740
+ cwd: d.cwd,
10741
+ home: d.home,
10742
+ copilotHome: d.copilotHome
10743
+ });
10744
+ status.configured = probe.alreadyConfigured;
10745
+ status.configPath = probe.path;
10746
+ } catch (error) {
10747
+ status.note = error instanceof Error ? error.message : String(error);
10748
+ }
10749
+ return status;
10750
+ });
10751
+ return { zamOnPath: Boolean(foundZam), harnesses };
10752
+ }
9970
10753
 
9971
10754
  // src/cli/curriculum/breadcrumb.ts
9972
10755
  init_kernel();
@@ -12633,9 +13416,9 @@ function getCurriculumProvider(id) {
12633
13416
  // src/cli/llm/vision.ts
12634
13417
  init_kernel();
12635
13418
  import { randomBytes } from "crypto";
12636
- import { readFileSync as readFileSync12 } from "fs";
13419
+ import { readFileSync as readFileSync14 } from "fs";
12637
13420
  import { tmpdir as tmpdir2 } from "os";
12638
- import { basename as basename4, join as join17 } from "path";
13421
+ import { basename as basename4, join as join19 } from "path";
12639
13422
  var LANGUAGE_NAMES2 = {
12640
13423
  en: "English",
12641
13424
  de: "German",
@@ -12671,13 +13454,13 @@ async function observeUiSnapshotViaLLM(db, input) {
12671
13454
  const imageUrls = [];
12672
13455
  const isVideo = /\.(mp4|mov|m4v|avi|mkv|webm)$/i.test(input.imagePath);
12673
13456
  if (isVideo) {
12674
- const { mkdirSync: mkdirSync12, readdirSync: readdirSync3, rmSync: rmSync4 } = await import("fs");
13457
+ const { mkdirSync: mkdirSync15, readdirSync: readdirSync3, rmSync: rmSync4 } = await import("fs");
12675
13458
  const { execSync: execSync5 } = await import("child_process");
12676
- const tempDir = join17(
13459
+ const tempDir = join19(
12677
13460
  tmpdir2(),
12678
13461
  `zam-frames-${randomBytes(4).toString("hex")}`
12679
13462
  );
12680
- mkdirSync12(tempDir, { recursive: true });
13463
+ mkdirSync15(tempDir, { recursive: true });
12681
13464
  try {
12682
13465
  execSync5(
12683
13466
  `ffmpeg -i "${input.imagePath}" -vf "fps=1/3,scale=1280:-1" -vsync vfr "${tempDir}/frame_%03d.png"`,
@@ -12706,7 +13489,7 @@ async function observeUiSnapshotViaLLM(db, input) {
12706
13489
  }
12707
13490
  }
12708
13491
  for (const file of sampledFiles) {
12709
- const bytes = readFileSync12(join17(tempDir, file));
13492
+ const bytes = readFileSync14(join19(tempDir, file));
12710
13493
  imageUrls.push(`data:image/png;base64,${bytes.toString("base64")}`);
12711
13494
  }
12712
13495
  } finally {
@@ -12716,7 +13499,7 @@ async function observeUiSnapshotViaLLM(db, input) {
12716
13499
  }
12717
13500
  }
12718
13501
  } else {
12719
- const imageBytes = readFileSync12(input.imagePath);
13502
+ const imageBytes = readFileSync14(input.imagePath);
12720
13503
  const ext = input.imagePath.split(".").pop()?.toLowerCase();
12721
13504
  const mime = ext === "jpg" || ext === "jpeg" ? "image/jpeg" : "image/png";
12722
13505
  imageUrls.push(`data:${mime};base64,${imageBytes.toString("base64")}`);
@@ -13182,36 +13965,36 @@ async function withProviderScope(machine, action) {
13182
13965
 
13183
13966
  // src/cli/provisioning/index.ts
13184
13967
  import {
13185
- existsSync as existsSync15,
13186
- lstatSync,
13187
- mkdirSync as mkdirSync10,
13188
- readFileSync as readFileSync13,
13968
+ existsSync as existsSync17,
13969
+ lstatSync as lstatSync2,
13970
+ mkdirSync as mkdirSync13,
13971
+ readFileSync as readFileSync15,
13189
13972
  realpathSync,
13190
13973
  rmSync as rmSync2,
13191
13974
  symlinkSync,
13192
- writeFileSync as writeFileSync8
13975
+ writeFileSync as writeFileSync11
13193
13976
  } from "fs";
13194
- import { basename as basename5, dirname as dirname7, join as join18 } from "path";
13195
- import { fileURLToPath as fileURLToPath3 } from "url";
13196
- var packageRoot = [
13197
- fileURLToPath3(new URL("../..", import.meta.url)),
13198
- fileURLToPath3(new URL("../../..", import.meta.url))
13199
- ].find((candidate) => existsSync15(join18(candidate, "package.json"))) ?? fileURLToPath3(new URL("../..", import.meta.url));
13977
+ import { basename as basename5, dirname as dirname10, join as join20 } from "path";
13978
+ import { fileURLToPath as fileURLToPath5 } from "url";
13979
+ var packageRoot3 = [
13980
+ fileURLToPath5(new URL("../..", import.meta.url)),
13981
+ fileURLToPath5(new URL("../../..", import.meta.url))
13982
+ ].find((candidate) => existsSync17(join20(candidate, "package.json"))) ?? fileURLToPath5(new URL("../..", import.meta.url));
13200
13983
  var ALL_SETUP_AGENTS = ["claude", "copilot", "codex", "agent"];
13201
13984
  var SKILL_PAIRS = [
13202
13985
  {
13203
- from: join18(packageRoot, ".claude", "skills", "zam", "SKILL.md"),
13204
- to: join18(".claude", "skills", "zam", "SKILL.md"),
13986
+ from: join20(packageRoot3, ".claude", "skills", "zam", "SKILL.md"),
13987
+ to: join20(".claude", "skills", "zam", "SKILL.md"),
13205
13988
  agents: ["claude", "copilot"]
13206
13989
  },
13207
13990
  {
13208
- from: join18(packageRoot, ".agent", "skills", "zam", "SKILL.md"),
13209
- to: join18(".agent", "skills", "zam", "SKILL.md"),
13991
+ from: join20(packageRoot3, ".agent", "skills", "zam", "SKILL.md"),
13992
+ to: join20(".agent", "skills", "zam", "SKILL.md"),
13210
13993
  agents: ["agent"]
13211
13994
  },
13212
13995
  {
13213
- from: join18(packageRoot, ".agents", "skills", "zam", "SKILL.md"),
13214
- to: join18(".agents", "skills", "zam", "SKILL.md"),
13996
+ from: join20(packageRoot3, ".agents", "skills", "zam", "SKILL.md"),
13997
+ to: join20(".agents", "skills", "zam", "SKILL.md"),
13215
13998
  agents: ["codex"]
13216
13999
  }
13217
14000
  ];
@@ -13241,7 +14024,7 @@ function parseSetupAgents(value) {
13241
14024
  }
13242
14025
  function pathExists(path) {
13243
14026
  try {
13244
- lstatSync(path);
14027
+ lstatSync2(path);
13245
14028
  return true;
13246
14029
  } catch {
13247
14030
  return false;
@@ -13249,7 +14032,7 @@ function pathExists(path) {
13249
14032
  }
13250
14033
  function isSymbolicLink(path) {
13251
14034
  try {
13252
- return lstatSync(path).isSymbolicLink();
14035
+ return lstatSync2(path).isSymbolicLink();
13253
14036
  } catch {
13254
14037
  return false;
13255
14038
  }
@@ -13270,16 +14053,16 @@ function linkPointsTo(sourceDir, destinationDir) {
13270
14053
  }
13271
14054
  function isZamSkillCopy(destinationDir) {
13272
14055
  try {
13273
- if (!lstatSync(destinationDir).isDirectory()) return false;
13274
- const skillFile = join18(destinationDir, "SKILL.md");
13275
- if (!existsSync15(skillFile)) return false;
13276
- return /^name:\s*zam\s*$/m.test(readFileSync13(skillFile, "utf8"));
14056
+ if (!lstatSync2(destinationDir).isDirectory()) return false;
14057
+ const skillFile = join20(destinationDir, "SKILL.md");
14058
+ if (!existsSync17(skillFile)) return false;
14059
+ return /^name:\s*zam\s*$/m.test(readFileSync15(skillFile, "utf8"));
13277
14060
  } catch {
13278
14061
  return false;
13279
14062
  }
13280
14063
  }
13281
14064
  function classifySkillDestination(sourceDir, destinationDir) {
13282
- if (!existsSync15(sourceDir)) return "source-missing";
14065
+ if (!existsSync17(sourceDir)) return "source-missing";
13283
14066
  if (!isSymbolicLink(destinationDir) && pathsResolveToSameDirectory(sourceDir, destinationDir)) {
13284
14067
  return "source-directory";
13285
14068
  }
@@ -13296,9 +14079,9 @@ function wireSkills(cwd = process.cwd(), agents = parseSetupAgents(), opts = {})
13296
14079
  };
13297
14080
  for (const { from, to, agents: pairAgents } of SKILL_PAIRS) {
13298
14081
  if (!pairAgents.some((agent) => agents.has(agent))) continue;
13299
- const sourceDir = dirname7(from);
13300
- const destinationDir = dirname7(join18(cwd, to));
13301
- const label = dirname7(to);
14082
+ const sourceDir = dirname10(from);
14083
+ const destinationDir = dirname10(join20(cwd, to));
14084
+ const label = dirname10(to);
13302
14085
  const state = classifySkillDestination(sourceDir, destinationDir);
13303
14086
  if (state === "source-missing") {
13304
14087
  if (!opts.quiet) {
@@ -13355,7 +14138,7 @@ function wireSkills(cwd = process.cwd(), agents = parseSetupAgents(), opts = {})
13355
14138
  if (destinationExists) {
13356
14139
  rmSync2(destinationDir, { recursive: true, force: true });
13357
14140
  }
13358
- mkdirSync10(dirname7(destinationDir), { recursive: true });
14141
+ mkdirSync13(dirname10(destinationDir), { recursive: true });
13359
14142
  symlinkSync(
13360
14143
  sourceDir,
13361
14144
  destinationDir,
@@ -13371,8 +14154,8 @@ function inspectSkillLinks(cwd = process.cwd(), agents = parseSetupAgents()) {
13371
14154
  const results = [];
13372
14155
  for (const { from, to, agents: pairAgents } of SKILL_PAIRS) {
13373
14156
  if (!pairAgents.some((agent) => agents.has(agent))) continue;
13374
- const sourceDir = dirname7(from);
13375
- const destinationDir = dirname7(join18(cwd, to));
14157
+ const sourceDir = dirname10(from);
14158
+ const destinationDir = dirname10(join20(cwd, to));
13376
14159
  results.push({
13377
14160
  agents: pairAgents,
13378
14161
  source: sourceDir,
@@ -13416,13 +14199,13 @@ async function resolveUser(opts, db, resolveOpts) {
13416
14199
  }
13417
14200
 
13418
14201
  // src/cli/workspaces/backup.ts
13419
- import { mkdirSync as mkdirSync11 } from "fs";
13420
- import { join as join19 } from "path";
14202
+ import { mkdirSync as mkdirSync14 } from "fs";
14203
+ import { join as join21 } from "path";
13421
14204
  async function backupDatabaseTo(db, targetDir) {
13422
- const backupDir = join19(targetDir, "zam-backups");
13423
- mkdirSync11(backupDir, { recursive: true });
14205
+ const backupDir = join21(targetDir, "zam-backups");
14206
+ mkdirSync14(backupDir, { recursive: true });
13424
14207
  const stamp = (/* @__PURE__ */ new Date()).toISOString().replace(/[:.]/g, "-");
13425
- const dest = join19(backupDir, `zam-${stamp}.db`);
14208
+ const dest = join21(backupDir, `zam-${stamp}.db`);
13426
14209
  await db.exec(`VACUUM INTO '${dest.replace(/'/g, "''")}'`);
13427
14210
  return dest;
13428
14211
  }
@@ -13553,20 +14336,20 @@ bridgeCommand.command("workspace-info").description("Report the workspace dir, i
13553
14336
  activeWorkspace,
13554
14337
  workspaceDir: activeWorkspace.path,
13555
14338
  defaultWorkspaceDir: defaultWorkspaceDir(),
13556
- dataDir: join20(homedir11(), ".zam")
14339
+ dataDir: join22(homedir14(), ".zam")
13557
14340
  });
13558
14341
  });
13559
14342
  });
13560
14343
  function provisionConfiguredWorkspaces() {
13561
14344
  return getConfiguredWorkspaces().flatMap(
13562
- (workspace) => existsSync16(workspace.path) ? wireSkills(workspace.path, parseSetupAgents(), { quiet: true }) : []
14345
+ (workspace) => existsSync18(workspace.path) ? wireSkills(workspace.path, parseSetupAgents(), { quiet: true }) : []
13563
14346
  );
13564
14347
  }
13565
14348
  function buildWorkspaceLinkHealth(workspaces) {
13566
14349
  const agents = parseSetupAgents();
13567
14350
  const map = {};
13568
14351
  for (const workspace of workspaces) {
13569
- if (!existsSync16(workspace.path)) continue;
14352
+ if (!existsSync18(workspace.path)) continue;
13570
14353
  const links = inspectSkillLinks(workspace.path, agents);
13571
14354
  map[workspace.id] = {
13572
14355
  health: summarizeSkillLinkHealth(links),
@@ -13596,7 +14379,7 @@ bridgeCommand.command("workspace-list").description("List configured ZAM workspa
13596
14379
  activeWorkspace,
13597
14380
  workspaceDir: activeWorkspace.path,
13598
14381
  defaultWorkspaceDir: defaultWorkspaceDir(),
13599
- dataDir: join20(homedir11(), ".zam"),
14382
+ dataDir: join22(homedir14(), ".zam"),
13600
14383
  linkHealth: buildWorkspaceLinkHealth(workspaces)
13601
14384
  });
13602
14385
  });
@@ -13611,7 +14394,7 @@ bridgeCommand.command("workspace-repair-links").description(
13611
14394
  if (!id) jsonError("A non-empty --id is required");
13612
14395
  const workspace = getConfiguredWorkspaces().find((item) => item.id === id);
13613
14396
  if (!workspace) jsonError(`Workspace "${id}" is not configured`);
13614
- if (!existsSync16(workspace.path)) {
14397
+ if (!existsSync18(workspace.path)) {
13615
14398
  jsonError(`Workspace path does not exist: ${workspace.path}`);
13616
14399
  }
13617
14400
  const agents = parseSetupAgents(opts.agents);
@@ -13642,8 +14425,8 @@ function parseBridgeWorkspaceKind(value) {
13642
14425
  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) => {
13643
14426
  const raw = String(opts.path ?? "").trim();
13644
14427
  if (!raw) jsonError("A non-empty --path is required");
13645
- const path = resolve4(raw);
13646
- if (!existsSync16(path)) jsonError(`Workspace path does not exist: ${path}`);
14428
+ const path = resolve5(raw);
14429
+ if (!existsSync18(path)) jsonError(`Workspace path does not exist: ${path}`);
13647
14430
  const id = opts.id ? String(opts.id).trim() : void 0;
13648
14431
  if (opts.id && !id) jsonError("A non-empty --id is required");
13649
14432
  const kind = parseBridgeWorkspaceKind(opts.kind);
@@ -13687,8 +14470,8 @@ bridgeCommand.command("workspace-remove").description("Unregister a ZAM workspac
13687
14470
  bridgeCommand.command("set-workspace-dir").description("Set the personal workspace directory (JSON)").requiredOption("--dir <path>", "Path to the workspace directory").action(async (opts) => {
13688
14471
  const raw = String(opts.dir ?? "").trim();
13689
14472
  if (!raw) jsonError("A non-empty --dir is required");
13690
- const dir = resolve4(raw);
13691
- if (!existsSync16(dir)) jsonError(`Workspace path does not exist: ${dir}`);
14473
+ const dir = resolve5(raw);
14474
+ if (!existsSync18(dir)) jsonError(`Workspace path does not exist: ${dir}`);
13692
14475
  const skillLinks = wireSkills(dir, parseSetupAgents(), { quiet: true });
13693
14476
  await withDb2(async (db) => {
13694
14477
  const workspace = await activateWorkspacePath(db, dir);
@@ -13751,7 +14534,7 @@ bridgeCommand.command("agent-open").description("Launch an agent harness in the
13751
14534
  jsonError(`Workspace is not configured: ${opts.workspace}`);
13752
14535
  }
13753
14536
  const activeWorkspace = await ensureActiveWorkspace(db);
13754
- const workspace = opts.dir ? existsSync16(opts.dir) ? opts.dir : homedir11() : existingWorkspaceDirOrHome(configuredWorkspace ?? activeWorkspace);
14537
+ const workspace = opts.dir ? existsSync18(opts.dir) ? opts.dir : homedir14() : existingWorkspaceDirOrHome(configuredWorkspace ?? activeWorkspace);
13755
14538
  launchHarness(harness, {
13756
14539
  executable,
13757
14540
  workspace,
@@ -14123,7 +14906,7 @@ bridgeCommand.command("discover-skills").description(
14123
14906
  "20"
14124
14907
  ).action(async (opts) => {
14125
14908
  try {
14126
- const monitorDir = join20(homedir11(), ".zam", "monitor");
14909
+ const monitorDir = join22(homedir14(), ".zam", "monitor");
14127
14910
  let files;
14128
14911
  try {
14129
14912
  files = readdirSync2(monitorDir).filter((f) => f.endsWith(".jsonl"));
@@ -14136,7 +14919,7 @@ bridgeCommand.command("discover-skills").description(
14136
14919
  return;
14137
14920
  }
14138
14921
  const limit = Number(opts.limit);
14139
- const sorted = files.map((f) => ({ name: f, path: join20(monitorDir, f) })).sort((a, b) => b.name.localeCompare(a.name)).slice(0, limit);
14922
+ const sorted = files.map((f) => ({ name: f, path: join22(monitorDir, f) })).sort((a, b) => b.name.localeCompare(a.name)).slice(0, limit);
14140
14923
  const sessionCommands = /* @__PURE__ */ new Map();
14141
14924
  for (const file of sorted) {
14142
14925
  const sessionId = file.name.replace(".jsonl", "");
@@ -14248,7 +15031,7 @@ bridgeCommand.command("observe-ui-snapshot").description(
14248
15031
  });
14249
15032
  function resolveWindowsPowerShell() {
14250
15033
  try {
14251
- execFileSync3("where.exe", ["pwsh.exe"], { stdio: "ignore" });
15034
+ execFileSync4("where.exe", ["pwsh.exe"], { stdio: "ignore" });
14252
15035
  return "pwsh";
14253
15036
  } catch {
14254
15037
  return "powershell";
@@ -14263,7 +15046,7 @@ function captureScreenshot(outputPath, hwnd, processName) {
14263
15046
  throw new Error(`Invalid process name format: ${processName}`);
14264
15047
  }
14265
15048
  if (platform === "win32") {
14266
- const stdout = execFileSync3(
15049
+ const stdout = execFileSync4(
14267
15050
  resolveWindowsPowerShell(),
14268
15051
  [
14269
15052
  "-NoProfile",
@@ -14576,13 +15359,13 @@ Write-CaptureResult "fullscreen" $hwndVal $matchedBy
14576
15359
  } else if (platform === "darwin") {
14577
15360
  if (hwnd) {
14578
15361
  const parsedHwnd = hwnd.startsWith("0x") ? parseInt(hwnd, 16) : parseInt(hwnd, 10);
14579
- execFileSync3("screencapture", ["-l", String(parsedHwnd), outputPath], {
15362
+ execFileSync4("screencapture", ["-l", String(parsedHwnd), outputPath], {
14580
15363
  stdio: "pipe"
14581
15364
  });
14582
15365
  return { method: "screencapture-window", target: null };
14583
15366
  } else if (processName) {
14584
15367
  try {
14585
- const windowId = execFileSync3(
15368
+ const windowId = execFileSync4(
14586
15369
  "osascript",
14587
15370
  [
14588
15371
  "-e",
@@ -14591,17 +15374,17 @@ Write-CaptureResult "fullscreen" $hwndVal $matchedBy
14591
15374
  { encoding: "utf8", stdio: ["pipe", "pipe", "ignore"] }
14592
15375
  ).trim();
14593
15376
  if (windowId && /^\d+$/.test(windowId)) {
14594
- execFileSync3("screencapture", ["-l", windowId, outputPath], {
15377
+ execFileSync4("screencapture", ["-l", windowId, outputPath], {
14595
15378
  stdio: "pipe"
14596
15379
  });
14597
15380
  return { method: "screencapture-window", target: null };
14598
15381
  }
14599
15382
  } catch {
14600
15383
  }
14601
- execFileSync3("screencapture", ["-x", outputPath], { stdio: "pipe" });
15384
+ execFileSync4("screencapture", ["-x", outputPath], { stdio: "pipe" });
14602
15385
  return { method: "screencapture-full", target: null };
14603
15386
  } else {
14604
- execFileSync3("screencapture", ["-x", outputPath], { stdio: "pipe" });
15387
+ execFileSync4("screencapture", ["-x", outputPath], { stdio: "pipe" });
14605
15388
  return { method: "screencapture-full", target: null };
14606
15389
  }
14607
15390
  } else {
@@ -14638,7 +15421,7 @@ bridgeCommand.command("capture-ui").description("Capture a screenshot for agent-
14638
15421
  return;
14639
15422
  }
14640
15423
  }
14641
- const outputPath = opts.image ?? opts.output ?? join20(tmpdir3(), `zam-capture-${randomBytes2(4).toString("hex")}.png`);
15424
+ const outputPath = opts.image ?? opts.output ?? join22(tmpdir3(), `zam-capture-${randomBytes2(4).toString("hex")}.png`);
14642
15425
  const captureResult = isProvided ? { method: "provided", target: null } : captureScreenshot(outputPath, opts.hwnd, opts.processName);
14643
15426
  if (!isProvided) {
14644
15427
  const post = decidePostCapture(policy, {
@@ -14666,7 +15449,7 @@ bridgeCommand.command("capture-ui").description("Capture a screenshot for agent-
14666
15449
  return;
14667
15450
  }
14668
15451
  }
14669
- const imageBytes = readFileSync14(outputPath);
15452
+ const imageBytes = readFileSync16(outputPath);
14670
15453
  const base64 = imageBytes.toString("base64");
14671
15454
  jsonOut({
14672
15455
  sessionId: opts.session ?? null,
@@ -14693,11 +15476,11 @@ bridgeCommand.command("start-recording").description("Start screen recording in
14693
15476
  return;
14694
15477
  }
14695
15478
  const sessionId = opts.session;
14696
- const statePath = join20(tmpdir3(), `zam-recording-${sessionId}.json`);
15479
+ const statePath = join22(tmpdir3(), `zam-recording-${sessionId}.json`);
14697
15480
  const defaultExt = platform === "win32" ? ".mkv" : ".mov";
14698
- const outputPath = opts.output ?? join20(tmpdir3(), `zam-recording-${sessionId}${defaultExt}`);
14699
- const { existsSync: existsSync18, writeFileSync: writeFileSync9, openSync, closeSync } = await import("fs");
14700
- if (existsSync18(statePath)) {
15481
+ const outputPath = opts.output ?? join22(tmpdir3(), `zam-recording-${sessionId}${defaultExt}`);
15482
+ const { existsSync: existsSync20, writeFileSync: writeFileSync12, openSync, closeSync } = await import("fs");
15483
+ if (existsSync20(statePath)) {
14701
15484
  jsonOut({
14702
15485
  sessionId,
14703
15486
  started: false,
@@ -14705,7 +15488,7 @@ bridgeCommand.command("start-recording").description("Start screen recording in
14705
15488
  });
14706
15489
  return;
14707
15490
  }
14708
- const logPath = join20(tmpdir3(), `zam-recording-${sessionId}.log`);
15491
+ const logPath = join22(tmpdir3(), `zam-recording-${sessionId}.log`);
14709
15492
  let logFd;
14710
15493
  try {
14711
15494
  logFd = openSync(logPath, "w");
@@ -14751,7 +15534,7 @@ bridgeCommand.command("start-recording").description("Start screen recording in
14751
15534
  }
14752
15535
  child.unref();
14753
15536
  if (child.pid) {
14754
- writeFileSync9(
15537
+ writeFileSync12(
14755
15538
  statePath,
14756
15539
  JSON.stringify({
14757
15540
  pid: child.pid,
@@ -14787,9 +15570,9 @@ bridgeCommand.command("stop-recording").description(
14787
15570
  return;
14788
15571
  }
14789
15572
  const sessionId = opts.session;
14790
- const statePath = join20(tmpdir3(), `zam-recording-${sessionId}.json`);
14791
- const { existsSync: existsSync18, readFileSync: readFileSync16, rmSync: rmSync4 } = await import("fs");
14792
- if (!existsSync18(statePath)) {
15573
+ const statePath = join22(tmpdir3(), `zam-recording-${sessionId}.json`);
15574
+ const { existsSync: existsSync20, readFileSync: readFileSync18, rmSync: rmSync4 } = await import("fs");
15575
+ if (!existsSync20(statePath)) {
14793
15576
  jsonOut({
14794
15577
  sessionId,
14795
15578
  stopped: false,
@@ -14797,7 +15580,7 @@ bridgeCommand.command("stop-recording").description(
14797
15580
  });
14798
15581
  return;
14799
15582
  }
14800
- const state = JSON.parse(readFileSync16(statePath, "utf8"));
15583
+ const state = JSON.parse(readFileSync18(statePath, "utf8"));
14801
15584
  const { pid, outputPath } = state;
14802
15585
  try {
14803
15586
  process.kill(pid, "SIGINT");
@@ -14813,7 +15596,7 @@ bridgeCommand.command("stop-recording").description(
14813
15596
  };
14814
15597
  let attempts = 0;
14815
15598
  while (isProcessRunning(pid) && attempts < 20) {
14816
- await new Promise((resolve5) => setTimeout(resolve5, 250));
15599
+ await new Promise((resolve6) => setTimeout(resolve6, 250));
14817
15600
  attempts++;
14818
15601
  }
14819
15602
  if (isProcessRunning(pid)) {
@@ -14826,7 +15609,7 @@ bridgeCommand.command("stop-recording").description(
14826
15609
  rmSync4(statePath, { force: true });
14827
15610
  } catch {
14828
15611
  }
14829
- if (!existsSync18(outputPath)) {
15612
+ if (!existsSync20(outputPath)) {
14830
15613
  jsonOut({
14831
15614
  sessionId,
14832
15615
  stopped: false,
@@ -15063,7 +15846,7 @@ bridgeCommand.command("cloud-model-hint").description("Suggest a cloud model for
15063
15846
  });
15064
15847
  bridgeCommand.command("local-llm-hints").description("Detect installed local LLM servers and suggest defaults (JSON)").action(() => {
15065
15848
  const profile = getSystemProfile();
15066
- const flmInstalled = hasCommand("flm") || existsSync16("C:\\Program Files\\flm\\flm.exe");
15849
+ const flmInstalled = hasCommand("flm") || existsSync18("C:\\Program Files\\flm\\flm.exe");
15067
15850
  const ollamaInstalled = isOllamaInstalled();
15068
15851
  const runners = [
15069
15852
  { id: "flm", label: "FastFlowLM", installed: flmInstalled },
@@ -15203,6 +15986,93 @@ bridgeCommand.command("evaluate-answer").description(
15203
15986
  }
15204
15987
  });
15205
15988
  });
15989
+ function parseDiscussionThread(raw) {
15990
+ if (!raw) return [];
15991
+ let parsed;
15992
+ try {
15993
+ parsed = JSON.parse(raw);
15994
+ } catch {
15995
+ throw new Error("Invalid --thread: not valid JSON");
15996
+ }
15997
+ if (!Array.isArray(parsed)) {
15998
+ throw new Error("Invalid --thread: expected a JSON array of turns");
15999
+ }
16000
+ return parsed.map((turn, index) => {
16001
+ const candidate = turn;
16002
+ if (candidate?.role !== "user" && candidate?.role !== "assistant" || typeof candidate?.content !== "string") {
16003
+ throw new Error(
16004
+ `Invalid --thread: turn ${index} must be {"role": "user"|"assistant", "content": string}`
16005
+ );
16006
+ }
16007
+ return { role: candidate.role, content: candidate.content };
16008
+ });
16009
+ }
16010
+ bridgeCommand.command("discuss-review").description(
16011
+ "Answer one turn of the post-reveal follow-up discussion about a card (JSON)"
16012
+ ).requiredOption("--slug <slug>", "Token slug").requiredOption("--concept <concept>", "Target concept text").requiredOption("--domain <domain>", "Token domain").requiredOption("--bloom-level <level>", "Bloom taxonomy level").requiredOption("--question <question>", "Question prompt presented").requiredOption("--user-answer <answer>", "User's typed answer").requiredOption("--message <text>", "The learner's newest discussion turn").option("--feedback <text>", "AI feedback already shown for this answer").option(
16013
+ "--thread <json>",
16014
+ 'Prior turns, oldest first, as JSON: [{"role":"user"|"assistant","content":"\u2026"},\u2026]'
16015
+ ).option("--context <context>", "Optional token context details").option("--source-link <link>", "Optional source link").option(
16016
+ "--source-content <content>",
16017
+ "Pre-resolved source reference content (skips re-fetch when set)"
16018
+ ).action(async (opts) => {
16019
+ await withDb2(async (db) => {
16020
+ const isEnabled = await getSetting(db, "llm.enabled") === "true";
16021
+ if (!isEnabled) {
16022
+ jsonOut({
16023
+ success: false,
16024
+ error: "LLM integration is disabled",
16025
+ reply: ""
16026
+ });
16027
+ return;
16028
+ }
16029
+ let thread;
16030
+ try {
16031
+ thread = parseDiscussionThread(opts.thread);
16032
+ } catch (err) {
16033
+ jsonOut({
16034
+ success: false,
16035
+ error: err.message,
16036
+ reply: ""
16037
+ });
16038
+ return;
16039
+ }
16040
+ let resolvedContextContent = opts.sourceContent ?? null;
16041
+ if (resolvedContextContent == null && opts.sourceLink) {
16042
+ try {
16043
+ const resolved = await resolveReviewContext(opts.sourceLink);
16044
+ resolvedContextContent = resolved?.content ?? null;
16045
+ } catch {
16046
+ }
16047
+ }
16048
+ try {
16049
+ const result = await discussReviewViaLLM(db, {
16050
+ slug: opts.slug,
16051
+ concept: opts.concept,
16052
+ domain: opts.domain,
16053
+ bloomLevel: Number(opts.bloomLevel),
16054
+ context: opts.context,
16055
+ question: opts.question,
16056
+ userAnswer: opts.userAnswer,
16057
+ sourceLinkContent: resolvedContextContent,
16058
+ feedback: opts.feedback ?? null,
16059
+ thread,
16060
+ message: opts.message
16061
+ });
16062
+ jsonOut({
16063
+ success: true,
16064
+ reply: result.text,
16065
+ replyModel: result.model
16066
+ });
16067
+ } catch (err) {
16068
+ jsonOut({
16069
+ success: false,
16070
+ error: err.message,
16071
+ reply: ""
16072
+ });
16073
+ }
16074
+ });
16075
+ });
15206
16076
  bridgeCommand.command("desktop-bootstrap").description("Initialize first-run desktop state (JSON)").option("--user <id>", "Preferred user ID when none is configured").action(async (opts) => {
15207
16077
  await withDb2(async (db) => {
15208
16078
  const userId = await ensureDefaultUser(db, opts.user);
@@ -15231,6 +16101,54 @@ bridgeCommand.command("get-settings").description("Get active ZAM settings (JSON
15231
16101
  });
15232
16102
  });
15233
16103
  });
16104
+ bridgeCommand.command("agent-harness-status").description(
16105
+ "Detect installed agent harnesses and their ZAM MCP configuration state (JSON)"
16106
+ ).action(() => {
16107
+ const report = inspectConnectHarnesses();
16108
+ jsonOut({
16109
+ success: true,
16110
+ zamOnPath: report.zamOnPath,
16111
+ harnesses: report.harnesses
16112
+ });
16113
+ });
16114
+ bridgeCommand.command("agent-connect").description(
16115
+ "Run the idempotent agent-connect flow for one or all detected harnesses (JSON)"
16116
+ ).option("--harness <id>", "Explicit harness id (default: all detected)").option(
16117
+ "--auto-once",
16118
+ "First-run mode: skip when the auto-connect marker is already set; set the marker after a run that detected at least one harness"
16119
+ ).action(async (opts) => {
16120
+ if (opts.harness && !isConnectHarnessId(opts.harness)) {
16121
+ jsonOut({
16122
+ success: false,
16123
+ error: `Unsupported harness: ${opts.harness}`
16124
+ });
16125
+ return;
16126
+ }
16127
+ const harness = opts.harness;
16128
+ const run = () => {
16129
+ const report = performAgentConnect({ harness });
16130
+ return {
16131
+ success: report.success,
16132
+ detected: report.detected,
16133
+ zamOnPath: report.zamOnPath,
16134
+ results: report.results.map(({ content: _content, ...rest }) => rest),
16135
+ skills: report.skills
16136
+ };
16137
+ };
16138
+ if (opts.autoOnce) {
16139
+ if (getAgentConnectAutoDone()) {
16140
+ jsonOut({ success: true, skipped: true });
16141
+ return;
16142
+ }
16143
+ const payload = run();
16144
+ if (payload.detected.length > 0) {
16145
+ setAgentConnectAutoDone(true);
16146
+ }
16147
+ jsonOut(payload);
16148
+ return;
16149
+ }
16150
+ jsonOut(run());
16151
+ });
15234
16152
  async function readDatabaseUserSummaries(db) {
15235
16153
  return await db.prepare(
15236
16154
  `SELECT user_id AS id, COUNT(*) AS cardCount
@@ -16336,12 +17254,12 @@ bridgeCommand.command("serve").description("Start the persistent JSON-RPC stdin/
16336
17254
  });
16337
17255
 
16338
17256
  // src/cli/commands/mcp.ts
16339
- var __dirname = dirname8(fileURLToPath4(import.meta.url));
16340
- var pkgPath = join21(__dirname, "..", "..", "package.json");
16341
- if (!existsSync17(pkgPath)) {
16342
- pkgPath = join21(__dirname, "..", "..", "..", "package.json");
17257
+ var __dirname = dirname11(fileURLToPath6(import.meta.url));
17258
+ var pkgPath = join23(__dirname, "..", "..", "package.json");
17259
+ if (!existsSync19(pkgPath)) {
17260
+ pkgPath = join23(__dirname, "..", "..", "..", "package.json");
16343
17261
  }
16344
- var pkg = JSON.parse(readFileSync15(pkgPath, "utf-8"));
17262
+ var pkg = JSON.parse(readFileSync17(pkgPath, "utf-8"));
16345
17263
  var STUDIO_RESOURCE_URI = "ui://zam/studio";
16346
17264
  var RECALL_RESOURCE_URI = "ui://zam/recall";
16347
17265
  var GRAPH_RESOURCE_URI = "ui://zam/graph";
@@ -16366,13 +17284,13 @@ var STUDIO_BRIDGE_ALLOWED_COMMANDS = /* @__PURE__ */ new Set([
16366
17284
  function loadPanelHtml(fileName, placeholderTitle) {
16367
17285
  const candidates = [
16368
17286
  // dist/cli/commands/mcp.js → dist/ui/
16369
- join21(__dirname, "..", "..", "ui", fileName),
17287
+ join23(__dirname, "..", "..", "ui", fileName),
16370
17288
  // src/cli/commands/mcp.ts via tsx → <repo>/dist/ui/
16371
- join21(__dirname, "..", "..", "..", "dist", "ui", fileName)
17289
+ join23(__dirname, "..", "..", "..", "dist", "ui", fileName)
16372
17290
  ];
16373
17291
  for (const candidate of candidates) {
16374
- if (existsSync17(candidate)) {
16375
- return readFileSync15(candidate, "utf-8");
17292
+ if (existsSync19(candidate)) {
17293
+ return readFileSync17(candidate, "utf-8");
16376
17294
  }
16377
17295
  }
16378
17296
  return `<!doctype html>