zam-core 0.10.2 → 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 dirname7, join as join20 } 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(".");
@@ -9438,15 +9511,76 @@ async function updateCheck(params) {
9438
9511
  });
9439
9512
  }
9440
9513
 
9514
+ // src/cli/ui-intent.ts
9515
+ import { mkdir, readFile, rename, writeFile } from "fs/promises";
9516
+ import { homedir as homedir9 } from "os";
9517
+ import { dirname as dirname6, join as join14 } from "path";
9518
+ import { ulid as ulid9 } from "ulid";
9519
+ function getUiIntentPath(home = homedir9()) {
9520
+ return join14(home, ".zam", "ui-intent.json");
9521
+ }
9522
+ function getUiHostRegistrationPath(home = homedir9()) {
9523
+ return join14(home, ".zam", "vscode-host.json");
9524
+ }
9525
+ function compactStringInput(input) {
9526
+ return Object.fromEntries(
9527
+ Object.entries(input).filter(
9528
+ (entry) => typeof entry[1] === "string"
9529
+ )
9530
+ );
9531
+ }
9532
+ async function writeUiIntent(app, input = {}, opts = {}) {
9533
+ const path = opts.path ?? process.env.ZAM_UI_INTENT_PATH ?? getUiIntentPath();
9534
+ const id = opts.id ?? ulid9();
9535
+ const intent = {
9536
+ version: 1,
9537
+ id,
9538
+ app,
9539
+ input: compactStringInput(input),
9540
+ createdAt: (opts.now ?? (() => /* @__PURE__ */ new Date()))().toISOString()
9541
+ };
9542
+ const tempPath = join14(dirname6(path), `.ui-intent-${process.pid}-${id}.tmp`);
9543
+ await mkdir(dirname6(path), { recursive: true });
9544
+ await writeFile(tempPath, `${JSON.stringify(intent, null, 2)}
9545
+ `, "utf8");
9546
+ await rename(tempPath, path);
9547
+ return intent;
9548
+ }
9549
+ async function publishUiIntent(app, input = {}, opts = {}) {
9550
+ try {
9551
+ if (process.env.ZAM_DISABLE_UI_INTENT === "1") return void 0;
9552
+ const explicitPath = opts.path ?? process.env.ZAM_UI_INTENT_PATH;
9553
+ if (explicitPath) {
9554
+ return await writeUiIntent(app, input, { ...opts, path: explicitPath });
9555
+ }
9556
+ const now = (opts.now ?? (() => /* @__PURE__ */ new Date()))();
9557
+ const registrationPath = opts.hostRegistrationPath ?? getUiHostRegistrationPath();
9558
+ const registration = JSON.parse(
9559
+ await readFile(registrationPath, "utf8")
9560
+ );
9561
+ const updatedAt = Date.parse(registration.updatedAt ?? "");
9562
+ if (registration.version !== 1 || typeof registration.intentPath !== "string" || !Number.isFinite(updatedAt) || now.getTime() - updatedAt > 15e3) {
9563
+ return void 0;
9564
+ }
9565
+ return await writeUiIntent(app, input, {
9566
+ ...opts,
9567
+ path: registration.intentPath,
9568
+ now: () => now
9569
+ });
9570
+ } catch {
9571
+ return void 0;
9572
+ }
9573
+ }
9574
+
9441
9575
  // src/cli/commands/bridge.ts
9442
9576
  init_kernel();
9443
- import { execFileSync as execFileSync3 } from "child_process";
9577
+ import { execFileSync as execFileSync4 } from "child_process";
9444
9578
  import { randomBytes as randomBytes2 } from "crypto";
9445
- import { existsSync as existsSync16, readdirSync as readdirSync2, readFileSync as readFileSync14, rmSync as rmSync3 } from "fs";
9446
- import { homedir as homedir10, tmpdir as tmpdir3 } from "os";
9447
- import { join as join19, 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";
9448
9582
  import { Command } from "commander";
9449
- import { ulid as ulid9 } from "ulid";
9583
+ import { ulid as ulid10 } from "ulid";
9450
9584
 
9451
9585
  // src/cli/adapters/source-reader.ts
9452
9586
  import dns from "dns";
@@ -9523,7 +9657,7 @@ async function readWebLink(url) {
9523
9657
  redirect: "manual",
9524
9658
  signal: controller.signal,
9525
9659
  headers: {
9526
- "User-Agent": "ZAM-Content-Studio/0.10.0"
9660
+ "User-Agent": "ZAM-Content-Studio/0.10.4"
9527
9661
  }
9528
9662
  });
9529
9663
  if (res.status >= 300 && res.status < 400) {
@@ -9586,11 +9720,17 @@ async function readImageOCR(db, imagePath) {
9586
9720
  return extractTextFromScanViaLLM(db, imagePath);
9587
9721
  }
9588
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
+
9589
9729
  // src/cli/agent-harness.ts
9590
9730
  import { spawn as spawn2 } from "child_process";
9591
9731
  import { existsSync as existsSync14, readFileSync as readFileSync11 } from "fs";
9592
- import { homedir as homedir9 } from "os";
9593
- import { join as join15 } from "path";
9732
+ import { homedir as homedir10 } from "os";
9733
+ import { join as join16 } from "path";
9594
9734
 
9595
9735
  // src/cli/terminal-open.ts
9596
9736
  import { execFileSync as execFileSync2, execSync as execSync4 } from "child_process";
@@ -9602,7 +9742,7 @@ import {
9602
9742
  writeFileSync as writeFileSync7
9603
9743
  } from "fs";
9604
9744
  import { tmpdir } from "os";
9605
- import { basename as basename3, delimiter, extname, isAbsolute, join as join14 } from "path";
9745
+ import { basename as basename3, delimiter, extname, isAbsolute, join as join15 } from "path";
9606
9746
  function isPowerShellShell(shell) {
9607
9747
  return shell === "pwsh" || shell === "powershell";
9608
9748
  }
@@ -9675,7 +9815,7 @@ function findExecutable(command) {
9675
9815
  const pathEntries = (process.env.PATH ?? "").split(delimiter).map((entry) => entry.trim()).filter(Boolean);
9676
9816
  for (const entry of pathEntries) {
9677
9817
  for (const name of windowsExecutableNames(normalized)) {
9678
- const candidate = join14(entry, name);
9818
+ const candidate = join15(entry, name);
9679
9819
  if (executableExists(candidate)) matches.push(candidate);
9680
9820
  }
9681
9821
  }
@@ -9713,7 +9853,7 @@ end tell` : `tell application "Terminal"
9713
9853
  activate
9714
9854
  do script "${escaped}"
9715
9855
  end tell`;
9716
- const tmpFile = join14(tmpdir(), `zam-terminal-${label}.scpt`);
9856
+ const tmpFile = join15(tmpdir(), `zam-terminal-${label}.scpt`);
9717
9857
  try {
9718
9858
  writeFileSync7(tmpFile, appleScript);
9719
9859
  execSync4(`osascript ${JSON.stringify(tmpFile)}`, { stdio: "ignore" });
@@ -9794,6 +9934,18 @@ function openTerminalWindow(opts) {
9794
9934
  }
9795
9935
 
9796
9936
  // src/cli/agent-harness.ts
9937
+ var ANTIGRAVITY_IDE_CANDIDATE_PATHS = {
9938
+ darwin: [
9939
+ join16(
9940
+ homedir10(),
9941
+ ".antigravity-ide",
9942
+ "antigravity-ide",
9943
+ "bin",
9944
+ "antigravity-ide"
9945
+ ),
9946
+ "/Applications/Antigravity IDE.app/Contents/Resources/app/bin/antigravity-ide"
9947
+ ]
9948
+ };
9797
9949
  var AGENT_HARNESSES = [
9798
9950
  { id: "claude-code", label: "Claude Code", kind: "cli", command: "claude" },
9799
9951
  { id: "codex", label: "Codex", kind: "cli", command: "codex" },
@@ -9805,7 +9957,7 @@ var AGENT_HARNESSES = [
9805
9957
  command: "cursor",
9806
9958
  candidatePaths: {
9807
9959
  win32: [
9808
- join15(homedir9(), "AppData", "Local", "Programs", "cursor", "Cursor.exe")
9960
+ join16(homedir10(), "AppData", "Local", "Programs", "cursor", "Cursor.exe")
9809
9961
  ],
9810
9962
  darwin: ["/Applications/Cursor.app/Contents/MacOS/Cursor"]
9811
9963
  }
@@ -9815,19 +9967,41 @@ var AGENT_HARNESSES = [
9815
9967
  id: "antigravity",
9816
9968
  label: "Antigravity",
9817
9969
  kind: "app",
9818
- command: "antigravity"
9970
+ command: "antigravity",
9971
+ candidatePaths: {
9972
+ darwin: [
9973
+ ...ANTIGRAVITY_IDE_CANDIDATE_PATHS.darwin ?? [],
9974
+ "/Applications/Antigravity.app/Contents/MacOS/Antigravity"
9975
+ ]
9976
+ }
9819
9977
  },
9820
9978
  { id: "goose", label: "goose", kind: "cli", command: "goose" }
9821
9979
  ];
9822
9980
  function getHarness(id) {
9823
9981
  return AGENT_HARNESSES.find((h) => h.id === id);
9824
9982
  }
9983
+ function resolveAntigravityIdeExecutable(deps = {}) {
9984
+ const find = deps.find ?? findExecutable;
9985
+ const exists = deps.exists ?? existsSync14;
9986
+ const platform = deps.platform ?? process.platform;
9987
+ const found = find("antigravity-ide");
9988
+ if (found) return found;
9989
+ return ANTIGRAVITY_IDE_CANDIDATE_PATHS[platform]?.find((path) => exists(path)) ?? null;
9990
+ }
9825
9991
  function resolveHarnessExecutable(harness, overrideCommand, deps = {}) {
9826
9992
  const find = deps.find ?? findExecutable;
9827
9993
  const exists = deps.exists ?? existsSync14;
9828
9994
  const platform = deps.platform ?? process.platform;
9829
9995
  const found = find(overrideCommand || harness.command);
9830
9996
  if (found) return found;
9997
+ if (!overrideCommand && harness.id === "antigravity") {
9998
+ const foundIde = resolveAntigravityIdeExecutable({
9999
+ find,
10000
+ exists,
10001
+ platform
10002
+ });
10003
+ if (foundIde) return foundIde;
10004
+ }
9831
10005
  if (harness.kind === "app") {
9832
10006
  for (const candidate of harness.candidatePaths?.[platform] ?? []) {
9833
10007
  if (exists(candidate)) return candidate;
@@ -9872,6 +10046,710 @@ function launchHarness(harness, opts) {
9872
10046
  console.log(`Launched ${harness.label} in ${opts.workspace}`);
9873
10047
  }
9874
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
+ }
9875
10753
 
9876
10754
  // src/cli/curriculum/breadcrumb.ts
9877
10755
  init_kernel();
@@ -12538,9 +13416,9 @@ function getCurriculumProvider(id) {
12538
13416
  // src/cli/llm/vision.ts
12539
13417
  init_kernel();
12540
13418
  import { randomBytes } from "crypto";
12541
- import { readFileSync as readFileSync12 } from "fs";
13419
+ import { readFileSync as readFileSync14 } from "fs";
12542
13420
  import { tmpdir as tmpdir2 } from "os";
12543
- import { basename as basename4, join as join16 } from "path";
13421
+ import { basename as basename4, join as join19 } from "path";
12544
13422
  var LANGUAGE_NAMES2 = {
12545
13423
  en: "English",
12546
13424
  de: "German",
@@ -12576,13 +13454,13 @@ async function observeUiSnapshotViaLLM(db, input) {
12576
13454
  const imageUrls = [];
12577
13455
  const isVideo = /\.(mp4|mov|m4v|avi|mkv|webm)$/i.test(input.imagePath);
12578
13456
  if (isVideo) {
12579
- const { mkdirSync: mkdirSync12, readdirSync: readdirSync3, rmSync: rmSync4 } = await import("fs");
13457
+ const { mkdirSync: mkdirSync15, readdirSync: readdirSync3, rmSync: rmSync4 } = await import("fs");
12580
13458
  const { execSync: execSync5 } = await import("child_process");
12581
- const tempDir = join16(
13459
+ const tempDir = join19(
12582
13460
  tmpdir2(),
12583
13461
  `zam-frames-${randomBytes(4).toString("hex")}`
12584
13462
  );
12585
- mkdirSync12(tempDir, { recursive: true });
13463
+ mkdirSync15(tempDir, { recursive: true });
12586
13464
  try {
12587
13465
  execSync5(
12588
13466
  `ffmpeg -i "${input.imagePath}" -vf "fps=1/3,scale=1280:-1" -vsync vfr "${tempDir}/frame_%03d.png"`,
@@ -12611,7 +13489,7 @@ async function observeUiSnapshotViaLLM(db, input) {
12611
13489
  }
12612
13490
  }
12613
13491
  for (const file of sampledFiles) {
12614
- const bytes = readFileSync12(join16(tempDir, file));
13492
+ const bytes = readFileSync14(join19(tempDir, file));
12615
13493
  imageUrls.push(`data:image/png;base64,${bytes.toString("base64")}`);
12616
13494
  }
12617
13495
  } finally {
@@ -12621,7 +13499,7 @@ async function observeUiSnapshotViaLLM(db, input) {
12621
13499
  }
12622
13500
  }
12623
13501
  } else {
12624
- const imageBytes = readFileSync12(input.imagePath);
13502
+ const imageBytes = readFileSync14(input.imagePath);
12625
13503
  const ext = input.imagePath.split(".").pop()?.toLowerCase();
12626
13504
  const mime = ext === "jpg" || ext === "jpeg" ? "image/jpeg" : "image/png";
12627
13505
  imageUrls.push(`data:${mime};base64,${imageBytes.toString("base64")}`);
@@ -13087,36 +13965,36 @@ async function withProviderScope(machine, action) {
13087
13965
 
13088
13966
  // src/cli/provisioning/index.ts
13089
13967
  import {
13090
- existsSync as existsSync15,
13091
- lstatSync,
13092
- mkdirSync as mkdirSync10,
13093
- readFileSync as readFileSync13,
13968
+ existsSync as existsSync17,
13969
+ lstatSync as lstatSync2,
13970
+ mkdirSync as mkdirSync13,
13971
+ readFileSync as readFileSync15,
13094
13972
  realpathSync,
13095
13973
  rmSync as rmSync2,
13096
13974
  symlinkSync,
13097
- writeFileSync as writeFileSync8
13975
+ writeFileSync as writeFileSync11
13098
13976
  } from "fs";
13099
- import { basename as basename5, dirname as dirname6, join as join17 } from "path";
13100
- import { fileURLToPath as fileURLToPath3 } from "url";
13101
- var packageRoot = [
13102
- fileURLToPath3(new URL("../..", import.meta.url)),
13103
- fileURLToPath3(new URL("../../..", import.meta.url))
13104
- ].find((candidate) => existsSync15(join17(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));
13105
13983
  var ALL_SETUP_AGENTS = ["claude", "copilot", "codex", "agent"];
13106
13984
  var SKILL_PAIRS = [
13107
13985
  {
13108
- from: join17(packageRoot, ".claude", "skills", "zam", "SKILL.md"),
13109
- to: join17(".claude", "skills", "zam", "SKILL.md"),
13986
+ from: join20(packageRoot3, ".claude", "skills", "zam", "SKILL.md"),
13987
+ to: join20(".claude", "skills", "zam", "SKILL.md"),
13110
13988
  agents: ["claude", "copilot"]
13111
13989
  },
13112
13990
  {
13113
- from: join17(packageRoot, ".agent", "skills", "zam", "SKILL.md"),
13114
- to: join17(".agent", "skills", "zam", "SKILL.md"),
13991
+ from: join20(packageRoot3, ".agent", "skills", "zam", "SKILL.md"),
13992
+ to: join20(".agent", "skills", "zam", "SKILL.md"),
13115
13993
  agents: ["agent"]
13116
13994
  },
13117
13995
  {
13118
- from: join17(packageRoot, ".agents", "skills", "zam", "SKILL.md"),
13119
- to: join17(".agents", "skills", "zam", "SKILL.md"),
13996
+ from: join20(packageRoot3, ".agents", "skills", "zam", "SKILL.md"),
13997
+ to: join20(".agents", "skills", "zam", "SKILL.md"),
13120
13998
  agents: ["codex"]
13121
13999
  }
13122
14000
  ];
@@ -13146,7 +14024,7 @@ function parseSetupAgents(value) {
13146
14024
  }
13147
14025
  function pathExists(path) {
13148
14026
  try {
13149
- lstatSync(path);
14027
+ lstatSync2(path);
13150
14028
  return true;
13151
14029
  } catch {
13152
14030
  return false;
@@ -13154,7 +14032,7 @@ function pathExists(path) {
13154
14032
  }
13155
14033
  function isSymbolicLink(path) {
13156
14034
  try {
13157
- return lstatSync(path).isSymbolicLink();
14035
+ return lstatSync2(path).isSymbolicLink();
13158
14036
  } catch {
13159
14037
  return false;
13160
14038
  }
@@ -13175,16 +14053,16 @@ function linkPointsTo(sourceDir, destinationDir) {
13175
14053
  }
13176
14054
  function isZamSkillCopy(destinationDir) {
13177
14055
  try {
13178
- if (!lstatSync(destinationDir).isDirectory()) return false;
13179
- const skillFile = join17(destinationDir, "SKILL.md");
13180
- if (!existsSync15(skillFile)) return false;
13181
- 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"));
13182
14060
  } catch {
13183
14061
  return false;
13184
14062
  }
13185
14063
  }
13186
14064
  function classifySkillDestination(sourceDir, destinationDir) {
13187
- if (!existsSync15(sourceDir)) return "source-missing";
14065
+ if (!existsSync17(sourceDir)) return "source-missing";
13188
14066
  if (!isSymbolicLink(destinationDir) && pathsResolveToSameDirectory(sourceDir, destinationDir)) {
13189
14067
  return "source-directory";
13190
14068
  }
@@ -13201,9 +14079,9 @@ function wireSkills(cwd = process.cwd(), agents = parseSetupAgents(), opts = {})
13201
14079
  };
13202
14080
  for (const { from, to, agents: pairAgents } of SKILL_PAIRS) {
13203
14081
  if (!pairAgents.some((agent) => agents.has(agent))) continue;
13204
- const sourceDir = dirname6(from);
13205
- const destinationDir = dirname6(join17(cwd, to));
13206
- const label = dirname6(to);
14082
+ const sourceDir = dirname10(from);
14083
+ const destinationDir = dirname10(join20(cwd, to));
14084
+ const label = dirname10(to);
13207
14085
  const state = classifySkillDestination(sourceDir, destinationDir);
13208
14086
  if (state === "source-missing") {
13209
14087
  if (!opts.quiet) {
@@ -13260,7 +14138,7 @@ function wireSkills(cwd = process.cwd(), agents = parseSetupAgents(), opts = {})
13260
14138
  if (destinationExists) {
13261
14139
  rmSync2(destinationDir, { recursive: true, force: true });
13262
14140
  }
13263
- mkdirSync10(dirname6(destinationDir), { recursive: true });
14141
+ mkdirSync13(dirname10(destinationDir), { recursive: true });
13264
14142
  symlinkSync(
13265
14143
  sourceDir,
13266
14144
  destinationDir,
@@ -13276,8 +14154,8 @@ function inspectSkillLinks(cwd = process.cwd(), agents = parseSetupAgents()) {
13276
14154
  const results = [];
13277
14155
  for (const { from, to, agents: pairAgents } of SKILL_PAIRS) {
13278
14156
  if (!pairAgents.some((agent) => agents.has(agent))) continue;
13279
- const sourceDir = dirname6(from);
13280
- const destinationDir = dirname6(join17(cwd, to));
14157
+ const sourceDir = dirname10(from);
14158
+ const destinationDir = dirname10(join20(cwd, to));
13281
14159
  results.push({
13282
14160
  agents: pairAgents,
13283
14161
  source: sourceDir,
@@ -13321,13 +14199,13 @@ async function resolveUser(opts, db, resolveOpts) {
13321
14199
  }
13322
14200
 
13323
14201
  // src/cli/workspaces/backup.ts
13324
- import { mkdirSync as mkdirSync11 } from "fs";
13325
- import { join as join18 } from "path";
14202
+ import { mkdirSync as mkdirSync14 } from "fs";
14203
+ import { join as join21 } from "path";
13326
14204
  async function backupDatabaseTo(db, targetDir) {
13327
- const backupDir = join18(targetDir, "zam-backups");
13328
- mkdirSync11(backupDir, { recursive: true });
14205
+ const backupDir = join21(targetDir, "zam-backups");
14206
+ mkdirSync14(backupDir, { recursive: true });
13329
14207
  const stamp = (/* @__PURE__ */ new Date()).toISOString().replace(/[:.]/g, "-");
13330
- const dest = join18(backupDir, `zam-${stamp}.db`);
14208
+ const dest = join21(backupDir, `zam-${stamp}.db`);
13331
14209
  await db.exec(`VACUUM INTO '${dest.replace(/'/g, "''")}'`);
13332
14210
  return dest;
13333
14211
  }
@@ -13458,20 +14336,20 @@ bridgeCommand.command("workspace-info").description("Report the workspace dir, i
13458
14336
  activeWorkspace,
13459
14337
  workspaceDir: activeWorkspace.path,
13460
14338
  defaultWorkspaceDir: defaultWorkspaceDir(),
13461
- dataDir: join19(homedir10(), ".zam")
14339
+ dataDir: join22(homedir14(), ".zam")
13462
14340
  });
13463
14341
  });
13464
14342
  });
13465
14343
  function provisionConfiguredWorkspaces() {
13466
14344
  return getConfiguredWorkspaces().flatMap(
13467
- (workspace) => existsSync16(workspace.path) ? wireSkills(workspace.path, parseSetupAgents(), { quiet: true }) : []
14345
+ (workspace) => existsSync18(workspace.path) ? wireSkills(workspace.path, parseSetupAgents(), { quiet: true }) : []
13468
14346
  );
13469
14347
  }
13470
14348
  function buildWorkspaceLinkHealth(workspaces) {
13471
14349
  const agents = parseSetupAgents();
13472
14350
  const map = {};
13473
14351
  for (const workspace of workspaces) {
13474
- if (!existsSync16(workspace.path)) continue;
14352
+ if (!existsSync18(workspace.path)) continue;
13475
14353
  const links = inspectSkillLinks(workspace.path, agents);
13476
14354
  map[workspace.id] = {
13477
14355
  health: summarizeSkillLinkHealth(links),
@@ -13501,7 +14379,7 @@ bridgeCommand.command("workspace-list").description("List configured ZAM workspa
13501
14379
  activeWorkspace,
13502
14380
  workspaceDir: activeWorkspace.path,
13503
14381
  defaultWorkspaceDir: defaultWorkspaceDir(),
13504
- dataDir: join19(homedir10(), ".zam"),
14382
+ dataDir: join22(homedir14(), ".zam"),
13505
14383
  linkHealth: buildWorkspaceLinkHealth(workspaces)
13506
14384
  });
13507
14385
  });
@@ -13516,7 +14394,7 @@ bridgeCommand.command("workspace-repair-links").description(
13516
14394
  if (!id) jsonError("A non-empty --id is required");
13517
14395
  const workspace = getConfiguredWorkspaces().find((item) => item.id === id);
13518
14396
  if (!workspace) jsonError(`Workspace "${id}" is not configured`);
13519
- if (!existsSync16(workspace.path)) {
14397
+ if (!existsSync18(workspace.path)) {
13520
14398
  jsonError(`Workspace path does not exist: ${workspace.path}`);
13521
14399
  }
13522
14400
  const agents = parseSetupAgents(opts.agents);
@@ -13547,8 +14425,8 @@ function parseBridgeWorkspaceKind(value) {
13547
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) => {
13548
14426
  const raw = String(opts.path ?? "").trim();
13549
14427
  if (!raw) jsonError("A non-empty --path is required");
13550
- const path = resolve4(raw);
13551
- 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}`);
13552
14430
  const id = opts.id ? String(opts.id).trim() : void 0;
13553
14431
  if (opts.id && !id) jsonError("A non-empty --id is required");
13554
14432
  const kind = parseBridgeWorkspaceKind(opts.kind);
@@ -13592,8 +14470,8 @@ bridgeCommand.command("workspace-remove").description("Unregister a ZAM workspac
13592
14470
  bridgeCommand.command("set-workspace-dir").description("Set the personal workspace directory (JSON)").requiredOption("--dir <path>", "Path to the workspace directory").action(async (opts) => {
13593
14471
  const raw = String(opts.dir ?? "").trim();
13594
14472
  if (!raw) jsonError("A non-empty --dir is required");
13595
- const dir = resolve4(raw);
13596
- 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}`);
13597
14475
  const skillLinks = wireSkills(dir, parseSetupAgents(), { quiet: true });
13598
14476
  await withDb2(async (db) => {
13599
14477
  const workspace = await activateWorkspacePath(db, dir);
@@ -13656,7 +14534,7 @@ bridgeCommand.command("agent-open").description("Launch an agent harness in the
13656
14534
  jsonError(`Workspace is not configured: ${opts.workspace}`);
13657
14535
  }
13658
14536
  const activeWorkspace = await ensureActiveWorkspace(db);
13659
- const workspace = opts.dir ? existsSync16(opts.dir) ? opts.dir : homedir10() : existingWorkspaceDirOrHome(configuredWorkspace ?? activeWorkspace);
14537
+ const workspace = opts.dir ? existsSync18(opts.dir) ? opts.dir : homedir14() : existingWorkspaceDirOrHome(configuredWorkspace ?? activeWorkspace);
13660
14538
  launchHarness(harness, {
13661
14539
  executable,
13662
14540
  workspace,
@@ -14028,7 +14906,7 @@ bridgeCommand.command("discover-skills").description(
14028
14906
  "20"
14029
14907
  ).action(async (opts) => {
14030
14908
  try {
14031
- const monitorDir = join19(homedir10(), ".zam", "monitor");
14909
+ const monitorDir = join22(homedir14(), ".zam", "monitor");
14032
14910
  let files;
14033
14911
  try {
14034
14912
  files = readdirSync2(monitorDir).filter((f) => f.endsWith(".jsonl"));
@@ -14041,7 +14919,7 @@ bridgeCommand.command("discover-skills").description(
14041
14919
  return;
14042
14920
  }
14043
14921
  const limit = Number(opts.limit);
14044
- const sorted = files.map((f) => ({ name: f, path: join19(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);
14045
14923
  const sessionCommands = /* @__PURE__ */ new Map();
14046
14924
  for (const file of sorted) {
14047
14925
  const sessionId = file.name.replace(".jsonl", "");
@@ -14153,7 +15031,7 @@ bridgeCommand.command("observe-ui-snapshot").description(
14153
15031
  });
14154
15032
  function resolveWindowsPowerShell() {
14155
15033
  try {
14156
- execFileSync3("where.exe", ["pwsh.exe"], { stdio: "ignore" });
15034
+ execFileSync4("where.exe", ["pwsh.exe"], { stdio: "ignore" });
14157
15035
  return "pwsh";
14158
15036
  } catch {
14159
15037
  return "powershell";
@@ -14168,7 +15046,7 @@ function captureScreenshot(outputPath, hwnd, processName) {
14168
15046
  throw new Error(`Invalid process name format: ${processName}`);
14169
15047
  }
14170
15048
  if (platform === "win32") {
14171
- const stdout = execFileSync3(
15049
+ const stdout = execFileSync4(
14172
15050
  resolveWindowsPowerShell(),
14173
15051
  [
14174
15052
  "-NoProfile",
@@ -14481,13 +15359,13 @@ Write-CaptureResult "fullscreen" $hwndVal $matchedBy
14481
15359
  } else if (platform === "darwin") {
14482
15360
  if (hwnd) {
14483
15361
  const parsedHwnd = hwnd.startsWith("0x") ? parseInt(hwnd, 16) : parseInt(hwnd, 10);
14484
- execFileSync3("screencapture", ["-l", String(parsedHwnd), outputPath], {
15362
+ execFileSync4("screencapture", ["-l", String(parsedHwnd), outputPath], {
14485
15363
  stdio: "pipe"
14486
15364
  });
14487
15365
  return { method: "screencapture-window", target: null };
14488
15366
  } else if (processName) {
14489
15367
  try {
14490
- const windowId = execFileSync3(
15368
+ const windowId = execFileSync4(
14491
15369
  "osascript",
14492
15370
  [
14493
15371
  "-e",
@@ -14496,17 +15374,17 @@ Write-CaptureResult "fullscreen" $hwndVal $matchedBy
14496
15374
  { encoding: "utf8", stdio: ["pipe", "pipe", "ignore"] }
14497
15375
  ).trim();
14498
15376
  if (windowId && /^\d+$/.test(windowId)) {
14499
- execFileSync3("screencapture", ["-l", windowId, outputPath], {
15377
+ execFileSync4("screencapture", ["-l", windowId, outputPath], {
14500
15378
  stdio: "pipe"
14501
15379
  });
14502
15380
  return { method: "screencapture-window", target: null };
14503
15381
  }
14504
15382
  } catch {
14505
15383
  }
14506
- execFileSync3("screencapture", ["-x", outputPath], { stdio: "pipe" });
15384
+ execFileSync4("screencapture", ["-x", outputPath], { stdio: "pipe" });
14507
15385
  return { method: "screencapture-full", target: null };
14508
15386
  } else {
14509
- execFileSync3("screencapture", ["-x", outputPath], { stdio: "pipe" });
15387
+ execFileSync4("screencapture", ["-x", outputPath], { stdio: "pipe" });
14510
15388
  return { method: "screencapture-full", target: null };
14511
15389
  }
14512
15390
  } else {
@@ -14543,7 +15421,7 @@ bridgeCommand.command("capture-ui").description("Capture a screenshot for agent-
14543
15421
  return;
14544
15422
  }
14545
15423
  }
14546
- const outputPath = opts.image ?? opts.output ?? join19(tmpdir3(), `zam-capture-${randomBytes2(4).toString("hex")}.png`);
15424
+ const outputPath = opts.image ?? opts.output ?? join22(tmpdir3(), `zam-capture-${randomBytes2(4).toString("hex")}.png`);
14547
15425
  const captureResult = isProvided ? { method: "provided", target: null } : captureScreenshot(outputPath, opts.hwnd, opts.processName);
14548
15426
  if (!isProvided) {
14549
15427
  const post = decidePostCapture(policy, {
@@ -14571,7 +15449,7 @@ bridgeCommand.command("capture-ui").description("Capture a screenshot for agent-
14571
15449
  return;
14572
15450
  }
14573
15451
  }
14574
- const imageBytes = readFileSync14(outputPath);
15452
+ const imageBytes = readFileSync16(outputPath);
14575
15453
  const base64 = imageBytes.toString("base64");
14576
15454
  jsonOut({
14577
15455
  sessionId: opts.session ?? null,
@@ -14598,11 +15476,11 @@ bridgeCommand.command("start-recording").description("Start screen recording in
14598
15476
  return;
14599
15477
  }
14600
15478
  const sessionId = opts.session;
14601
- const statePath = join19(tmpdir3(), `zam-recording-${sessionId}.json`);
15479
+ const statePath = join22(tmpdir3(), `zam-recording-${sessionId}.json`);
14602
15480
  const defaultExt = platform === "win32" ? ".mkv" : ".mov";
14603
- const outputPath = opts.output ?? join19(tmpdir3(), `zam-recording-${sessionId}${defaultExt}`);
14604
- const { existsSync: existsSync18, writeFileSync: writeFileSync9, openSync, closeSync } = await import("fs");
14605
- 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)) {
14606
15484
  jsonOut({
14607
15485
  sessionId,
14608
15486
  started: false,
@@ -14610,7 +15488,7 @@ bridgeCommand.command("start-recording").description("Start screen recording in
14610
15488
  });
14611
15489
  return;
14612
15490
  }
14613
- const logPath = join19(tmpdir3(), `zam-recording-${sessionId}.log`);
15491
+ const logPath = join22(tmpdir3(), `zam-recording-${sessionId}.log`);
14614
15492
  let logFd;
14615
15493
  try {
14616
15494
  logFd = openSync(logPath, "w");
@@ -14656,7 +15534,7 @@ bridgeCommand.command("start-recording").description("Start screen recording in
14656
15534
  }
14657
15535
  child.unref();
14658
15536
  if (child.pid) {
14659
- writeFileSync9(
15537
+ writeFileSync12(
14660
15538
  statePath,
14661
15539
  JSON.stringify({
14662
15540
  pid: child.pid,
@@ -14692,9 +15570,9 @@ bridgeCommand.command("stop-recording").description(
14692
15570
  return;
14693
15571
  }
14694
15572
  const sessionId = opts.session;
14695
- const statePath = join19(tmpdir3(), `zam-recording-${sessionId}.json`);
14696
- const { existsSync: existsSync18, readFileSync: readFileSync16, rmSync: rmSync4 } = await import("fs");
14697
- 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)) {
14698
15576
  jsonOut({
14699
15577
  sessionId,
14700
15578
  stopped: false,
@@ -14702,7 +15580,7 @@ bridgeCommand.command("stop-recording").description(
14702
15580
  });
14703
15581
  return;
14704
15582
  }
14705
- const state = JSON.parse(readFileSync16(statePath, "utf8"));
15583
+ const state = JSON.parse(readFileSync18(statePath, "utf8"));
14706
15584
  const { pid, outputPath } = state;
14707
15585
  try {
14708
15586
  process.kill(pid, "SIGINT");
@@ -14718,7 +15596,7 @@ bridgeCommand.command("stop-recording").description(
14718
15596
  };
14719
15597
  let attempts = 0;
14720
15598
  while (isProcessRunning(pid) && attempts < 20) {
14721
- await new Promise((resolve5) => setTimeout(resolve5, 250));
15599
+ await new Promise((resolve6) => setTimeout(resolve6, 250));
14722
15600
  attempts++;
14723
15601
  }
14724
15602
  if (isProcessRunning(pid)) {
@@ -14731,7 +15609,7 @@ bridgeCommand.command("stop-recording").description(
14731
15609
  rmSync4(statePath, { force: true });
14732
15610
  } catch {
14733
15611
  }
14734
- if (!existsSync18(outputPath)) {
15612
+ if (!existsSync20(outputPath)) {
14735
15613
  jsonOut({
14736
15614
  sessionId,
14737
15615
  stopped: false,
@@ -14968,7 +15846,7 @@ bridgeCommand.command("cloud-model-hint").description("Suggest a cloud model for
14968
15846
  });
14969
15847
  bridgeCommand.command("local-llm-hints").description("Detect installed local LLM servers and suggest defaults (JSON)").action(() => {
14970
15848
  const profile = getSystemProfile();
14971
- const flmInstalled = hasCommand("flm") || existsSync16("C:\\Program Files\\flm\\flm.exe");
15849
+ const flmInstalled = hasCommand("flm") || existsSync18("C:\\Program Files\\flm\\flm.exe");
14972
15850
  const ollamaInstalled = isOllamaInstalled();
14973
15851
  const runners = [
14974
15852
  { id: "flm", label: "FastFlowLM", installed: flmInstalled },
@@ -15108,6 +15986,93 @@ bridgeCommand.command("evaluate-answer").description(
15108
15986
  }
15109
15987
  });
15110
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
+ });
15111
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) => {
15112
16077
  await withDb2(async (db) => {
15113
16078
  const userId = await ensureDefaultUser(db, opts.user);
@@ -15136,6 +16101,54 @@ bridgeCommand.command("get-settings").description("Get active ZAM settings (JSON
15136
16101
  });
15137
16102
  });
15138
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
+ });
15139
16152
  async function readDatabaseUserSummaries(db) {
15140
16153
  return await db.prepare(
15141
16154
  `SELECT user_id AS id, COUNT(*) AS cardCount
@@ -15757,7 +16770,7 @@ bridgeCommand.command("personal-source-import").description(
15757
16770
  } else {
15758
16771
  throw new Error(`Invalid source type: ${opts.type}`);
15759
16772
  }
15760
- const sourceId = ulid9();
16773
+ const sourceId = ulid10();
15761
16774
  await db.prepare(
15762
16775
  `INSERT INTO sources (id, type, uri, content)
15763
16776
  VALUES (?, ?, ?, ?)
@@ -15976,7 +16989,7 @@ bridgeCommand.command("curriculum-extract-topics").description(
15976
16989
  }
15977
16990
  }
15978
16991
  const pageCleanedText = cleanHtml(rawHtml);
15979
- const sourceId = ulid9();
16992
+ const sourceId = ulid10();
15980
16993
  await db.prepare(
15981
16994
  `INSERT INTO sources (id, type, uri, content)
15982
16995
  VALUES (?, 'web', ?, ?)
@@ -16241,12 +17254,12 @@ bridgeCommand.command("serve").description("Start the persistent JSON-RPC stdin/
16241
17254
  });
16242
17255
 
16243
17256
  // src/cli/commands/mcp.ts
16244
- var __dirname = dirname7(fileURLToPath4(import.meta.url));
16245
- var pkgPath = join20(__dirname, "..", "..", "package.json");
16246
- if (!existsSync17(pkgPath)) {
16247
- pkgPath = join20(__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");
16248
17261
  }
16249
- var pkg = JSON.parse(readFileSync15(pkgPath, "utf-8"));
17262
+ var pkg = JSON.parse(readFileSync17(pkgPath, "utf-8"));
16250
17263
  var STUDIO_RESOURCE_URI = "ui://zam/studio";
16251
17264
  var RECALL_RESOURCE_URI = "ui://zam/recall";
16252
17265
  var GRAPH_RESOURCE_URI = "ui://zam/graph";
@@ -16271,13 +17284,13 @@ var STUDIO_BRIDGE_ALLOWED_COMMANDS = /* @__PURE__ */ new Set([
16271
17284
  function loadPanelHtml(fileName, placeholderTitle) {
16272
17285
  const candidates = [
16273
17286
  // dist/cli/commands/mcp.js → dist/ui/
16274
- join20(__dirname, "..", "..", "ui", fileName),
17287
+ join23(__dirname, "..", "..", "ui", fileName),
16275
17288
  // src/cli/commands/mcp.ts via tsx → <repo>/dist/ui/
16276
- join20(__dirname, "..", "..", "..", "dist", "ui", fileName)
17289
+ join23(__dirname, "..", "..", "..", "dist", "ui", fileName)
16277
17290
  ];
16278
17291
  for (const candidate of candidates) {
16279
- if (existsSync17(candidate)) {
16280
- return readFileSync15(candidate, "utf-8");
17292
+ if (existsSync19(candidate)) {
17293
+ return readFileSync17(candidate, "utf-8");
16281
17294
  }
16282
17295
  }
16283
17296
  return `<!doctype html>
@@ -16672,7 +17685,7 @@ function createMcpServer(db) {
16672
17685
  "zam_open_studio",
16673
17686
  {
16674
17687
  title: "ZAM Studio",
16675
- description: "Open the ZAM Studio panel (content editor, knowledge graph, settings) inline",
17688
+ description: "Open the legacy all-in-one ZAM Studio panel for standalone onboarding and content curation. Agent harness workflows should use the focused Recall, Graph, and Settings apps instead.",
16676
17689
  inputSchema: {},
16677
17690
  annotations: {
16678
17691
  ...commonAnnotations,
@@ -16727,6 +17740,7 @@ function createMcpServer(db) {
16727
17740
  wrapHandler(
16728
17741
  async ({ user, domain }) => {
16729
17742
  const userId = await getUserId(user).catch(() => null);
17743
+ await publishUiIntent("recall", { user, domain });
16730
17744
  return {
16731
17745
  recall: "zam",
16732
17746
  version: pkg.version,
@@ -16771,6 +17785,7 @@ function createMcpServer(db) {
16771
17785
  },
16772
17786
  wrapHandler(async ({ focus, user }) => {
16773
17787
  const userId = await getUserId(user).catch(() => null);
17788
+ await publishUiIntent("graph", { user, focus });
16774
17789
  return {
16775
17790
  graph: "zam",
16776
17791
  focus: focus ?? null,
@@ -16812,6 +17827,7 @@ function createMcpServer(db) {
16812
17827
  },
16813
17828
  wrapHandler(async ({ user }) => {
16814
17829
  const userId = await getUserId(user).catch(() => null);
17830
+ await publishUiIntent("settings", { user });
16815
17831
  return {
16816
17832
  settings: "zam",
16817
17833
  version: pkg.version,