@dev.fast/whiteboard 0.2.5-preview.20261015.119 → 0.2.5-preview.20261016.121

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.
Files changed (69) hide show
  1. package/dist/{account-alias-C1ixP6bt.js → account-alias-CwNaMOwU.js} +372 -85
  2. package/dist/agent-cli-NWQPsL0W.js +2 -0
  3. package/dist/{agent-cli-CPSnWc9V.js → agent-cli-S0bt9I7V.js} +7 -7
  4. package/dist/{agent-client-Dogh8I83.js → agent-client-DjXQy6CL.js} +5 -6
  5. package/dist/{authoring-tools-CtsslOEf.js → authoring-tools-CjFAGHBa.js} +5 -4
  6. package/dist/{background-server-BKR_lpYb.js → background-server-CVL7QpsL.js} +1 -1
  7. package/dist/{background-server-DJT0CnYH.js → background-server-ONy-xKJ1.js} +4 -4
  8. package/dist/build-info.json +1 -1
  9. package/dist/{cli-Df6p-C19.js → cli-Cko4zfif.js} +1 -1
  10. package/dist/{cli-runner-ZktCwlLk.js → cli-runner-BhBnAC_R.js} +51 -187
  11. package/dist/cli.js +2 -2
  12. package/dist/{client-_ltX1mah.js → client-CLqKkzOc.js} +4 -4
  13. package/dist/fuzzy-match-DFPKdSLR.js +335 -0
  14. package/dist/{headless-host-Pj2xfGAo.js → headless-host-BNF-TR3x.js} +1 -1
  15. package/dist/{headless-host-ZNZ_6ZRo.js → headless-host-mg9sQG6H.js} +53 -345
  16. package/dist/{local-data-D_9S--yI.js → local-data-DN-A5p0K.js} +57 -415
  17. package/dist/{mcp-Blb7rzGw.js → mcp-BY4ExeMQ.js} +3 -3
  18. package/dist/{package-paths-BsuI4WUf.js → package-paths-jMg6JxDP.js} +1 -1
  19. package/dist/profile-BBw-jMW3.js +2 -0
  20. package/dist/{profile-CKSB6UDi.js → profile-Dm6irjbg.js} +2 -2
  21. package/dist/{remote-attach-CiOwonJ3.js → remote-attach-DJxW9402.js} +4 -4
  22. package/dist/{remote-extensions-D_4DSOhO.js → remote-extensions-BCAbQZTv.js} +2 -2
  23. package/dist/{remote-extensions-ByiuSl1D.js → remote-extensions-Cf3L-ZGG.js} +1 -1
  24. package/dist/{remote-language-server-BQjBk81F.js → remote-language-server-CGsCaqro.js} +4 -4
  25. package/dist/{remote-uninstall-pzHb-Hfd.js → remote-uninstall-DYmNOetF.js} +5 -5
  26. package/dist/{request-origin-C-V9f3Fr.js → request-origin-D1RNVSeK.js} +2 -7
  27. package/dist/{review-app-launcher-S2FZBY4W.js → review-app-launcher-i1hmxOZA.js} +2 -2
  28. package/dist/{review-home-paths-zwevIq4I.js → review-home-paths-Lc02WFuX.js} +1 -1
  29. package/dist/{review-telemetry-9ceqh4wz.js → review-telemetry-DnQNwgR1.js} +4 -4
  30. package/dist/server/desktop-host.js +99 -445
  31. package/dist/{server-discovery-CqRxbsER.js → server-discovery-DVqrcIDT.js} +2 -2
  32. package/dist/sharing/index.d.ts +17 -13
  33. package/dist/sharing/index.js +1 -1
  34. package/dist/{src-HnIPjtjx.js → src-DZG1xAqW.js} +4 -1
  35. package/dist/{stored-document-migration-DenDEYdI.js → stored-document-migration-DLBhkSfp.js} +46 -6
  36. package/dist/thread-state-CaQ2lGdj.js +400 -0
  37. package/dist/{tool-failure-aSYcHDxP.js → tool-failure-CVJrfi5s.js} +1 -1
  38. package/package.json +3 -3
  39. package/src/agent-connect.ts +29 -106
  40. package/src/cli-install.ts +13 -11
  41. package/src/cli-runner.ts +51 -24
  42. package/src/connect-prompts.ts +135 -71
  43. package/src/review-api/README.md +1 -1
  44. package/src/review-api/azure-devops-provider.ts +4 -0
  45. package/src/review-api/blocks/definition.ts +3 -1
  46. package/src/review-api/blocks/index.ts +19 -0
  47. package/src/review-api/blocks/sequence.ts +80 -2
  48. package/src/review-api/comparison-coverage.ts +0 -13
  49. package/src/review-api/component-reference.ts +2 -1
  50. package/src/review-api/document-text.ts +7 -2
  51. package/src/review-api/document.ts +13 -9
  52. package/src/review-api/github-provider.ts +21 -3
  53. package/src/review-api/http.ts +22 -41
  54. package/src/review-api/local-data.ts +22 -27
  55. package/src/review-api/pull-request.ts +2 -0
  56. package/src/review-api/review-progress.ts +6 -8
  57. package/src/review-api/store.ts +36 -25
  58. package/src/review-stack.ts +3 -2
  59. package/src/server/review-gateway-hosts.ts +8 -1
  60. package/src/server/review-gateway-list.ts +4 -10
  61. package/src/server/review-gateway-memory.ts +6 -19
  62. package/src/server/review-gateway-streams.ts +32 -46
  63. package/src/sharing/import.ts +2 -2
  64. package/src/ui-telemetry-events.ts +1 -2
  65. package/dist/agent-cli-CGapivA1.js +0 -2
  66. package/dist/fs-utils-BMPLt0cr.js +0 -22
  67. package/dist/fuzzy-match-BoAmcyak.js +0 -30
  68. package/dist/profile-BUoTujrK.js +0 -2
  69. package/src/connect-agents.ts +0 -285
@@ -1,11 +1,13 @@
1
- import { w as ReviewCliInstallStampSchema } from "./src-HnIPjtjx.js";
2
- import { $ as removeAgentTraceHook, C as disableTraceMachine, E as traceMachineStatus, G as listTraceRepositoryRoots, K as traceRepositoryStatus, Q as installHarnessHooks, S as configureTraceMachine, T as traceMachineEnabled, U as disableAllTraceRepositories, W as enableTraceRepository, Y as AGENT_TRACE_HOOK_AGENTS, Z as describeTraceHookOwners, ft as StoreClient, n as reviewDesktopStateDir, rt as traceScope, st as readStoreAuth } from "./review-home-paths-zwevIq4I.js";
1
+ import { n as isJsonObject } from "./src-X9phtB2j.js";
2
+ import { w as ReviewCliInstallStampSchema } from "./src-DZG1xAqW.js";
3
+ import { C as disableTraceMachine, E as traceMachineStatus, G as listTraceRepositoryRoots, K as traceRepositoryStatus, Q as removeAgentTraceHook, S as configureTraceMachine, T as traceMachineEnabled, U as disableAllTraceRepositories, W as enableTraceRepository, X as describeTraceHookOwners, Y as AGENT_TRACE_HOOK_AGENTS, Z as installHarnessHooks, dt as StoreClient, n as reviewDesktopStateDir, nt as traceScope, ot as readStoreAuth } from "./review-home-paths-Lc02WFuX.js";
3
4
  import { A as withFileLock, F as writeFileAtomicAsync, I as writePrivateJsonAtomic, M as devReviewHome } from "./s3-config-DDWWMh5x.js";
4
- import { i as readReviewPackageVersion } from "./package-paths-BsuI4WUf.js";
5
- import { n as isFile, t as isDirectory } from "./fs-utils-BMPLt0cr.js";
5
+ import { i as readReviewPackageVersion } from "./package-paths-jMg6JxDP.js";
6
+ import { f as isDirectory, p as isFile } from "./thread-state-CaQ2lGdj.js";
7
+ import { s as findExecutable } from "./fuzzy-match-DFPKdSLR.js";
6
8
  import { createHash } from "node:crypto";
7
9
  import { constants } from "node:fs";
8
- import { access, chmod, lstat, readFile, rm, stat } from "node:fs/promises";
10
+ import { access, chmod, lstat, mkdir, readFile, rm, stat, writeFile } from "node:fs/promises";
9
11
  import path from "node:path";
10
12
  import os from "node:os";
11
13
  import { execFile } from "node:child_process";
@@ -71,105 +73,131 @@ function fffSteps(input, target) {
71
73
  const root = shellQuote(input.fffCorpusRoot);
72
74
  return [`Unless \`${target} mcp get fff\` finds an existing server, run:\n\n\`\`\`sh\n[ -x ${binary} ] || curl -fsSL ${FFF_INSTALL_URL} | bash\n${target === "claude" ? "claude mcp add -s user" : "codex mcp add"} fff -- ${binary} ${root}\n\`\`\``];
73
75
  }
74
- /** The Claude plugin's sh launch cannot start on Windows, so register the server directly. */
75
- const CLAUDE_WINDOWS_MCP_ADD = `claude mcp add -s user whiteboard -- ${WINDOWS_MCP_LAUNCH.command} ${WINDOWS_MCP_LAUNCH.args.join(" ")}`;
76
- /** Copilot CLI loads the Claude plugin, so on Windows it too registers the server directly. */
77
- const COPILOT_WINDOWS_MCP_ADD = `copilot mcp add whiteboard -- ${WINDOWS_MCP_LAUNCH.command} ${WINDOWS_MCP_LAUNCH.args.join(" ")}`;
78
- const CONNECT_COMMANDS = {
79
- claude: [
80
- { argv: [
81
- "claude",
76
+ const WHITEBOARD_MARKETPLACE = "devdotfast/whiteboard";
77
+ const WHITEBOARD_PLUGIN = "whiteboard@devfast";
78
+ const ALREADY = /already/i;
79
+ /** Each CLI agent's connect commands: Connect runs them, prompts print them. */
80
+ function connectSteps(target, launch, platform) {
81
+ const marketplace = {
82
+ args: [
82
83
  "plugin",
83
84
  "marketplace",
84
85
  "add",
85
- "devdotfast/whiteboard"
86
- ] },
87
- { argv: [
88
- "claude",
86
+ WHITEBOARD_MARKETPLACE
87
+ ],
88
+ optional: true
89
+ };
90
+ const cleanup = (args, note) => ({
91
+ args,
92
+ optional: true,
93
+ note
94
+ });
95
+ const windows = (scope) => [
96
+ cleanup([
89
97
  "plugin",
90
- "install",
91
- "whiteboard@devfast",
92
- "--scope",
93
- "user"
94
- ] },
98
+ "uninstall",
99
+ WHITEBOARD_PLUGIN
100
+ ], "its launch cannot start on Windows, if installed"),
101
+ cleanup([
102
+ "mcp",
103
+ "remove",
104
+ ...scope,
105
+ "whiteboard"
106
+ ], "old registration, if any"),
95
107
  {
96
- argv: [
97
- "claude",
108
+ args: [
98
109
  "mcp",
99
- "remove",
100
- "-s",
101
- "user",
102
- "whiteboard"
110
+ "add",
111
+ ...scope,
112
+ "whiteboard",
113
+ "--",
114
+ launch.command,
115
+ ...launch.args
103
116
  ],
104
- ifAny: true
117
+ done: ALREADY
105
118
  }
106
- ],
107
- codex: [
108
- { argv: [
109
- "codex",
119
+ ];
120
+ const plugin = (install, scope) => [
121
+ marketplace,
122
+ {
123
+ args: install,
124
+ done: ALREADY
125
+ },
126
+ cleanup([
127
+ "mcp",
128
+ "remove",
129
+ ...scope,
130
+ "whiteboard"
131
+ ], "old manual registration, if any")
132
+ ];
133
+ switch (target) {
134
+ case "claude": return platform === "win32" ? windows(["-s", "user"]) : plugin([
110
135
  "plugin",
111
- "marketplace",
112
- "add",
113
- "devdotfast/whiteboard"
114
- ] },
115
- { argv: [
116
- "codex",
136
+ "install",
137
+ WHITEBOARD_PLUGIN,
138
+ "--scope",
139
+ "user"
140
+ ], ["-s", "user"]);
141
+ case "codex": return plugin([
117
142
  "plugin",
118
143
  "add",
119
- "whiteboard@devfast"
120
- ] },
121
- {
122
- argv: [
123
- "codex",
144
+ WHITEBOARD_PLUGIN
145
+ ], []);
146
+ case "copilot": return platform === "win32" ? windows([]) : plugin([
147
+ "plugin",
148
+ "install",
149
+ WHITEBOARD_PLUGIN
150
+ ], []);
151
+ case "opencode": return [{
152
+ args: [
124
153
  "mcp",
125
- "remove",
126
- "whiteboard"
154
+ "add",
155
+ "--global",
156
+ "whiteboard",
157
+ "--",
158
+ launch.command,
159
+ ...launch.args
127
160
  ],
128
- ifAny: true
129
- }
130
- ],
131
- opencode: [{ argv: [
132
- "opencode",
133
- "mcp",
134
- "add",
135
- "--global",
136
- "whiteboard",
137
- "--",
138
- REVIEW_MCP_LAUNCH.command,
139
- ...REVIEW_MCP_LAUNCH.args
140
- ] }],
141
- pi: [{ argv: [
142
- "pi",
143
- "install",
144
- PI_WHITEBOARD_PACKAGE
145
- ] }]
146
- };
147
- function runBlock(commands) {
148
- return `Run:\n\n\`\`\`sh\n${commands.map(({ argv, ifAny }) => `${argv.join(" ")}${ifAny ? " # old manual registration, if any" : ""}`).join("\n")}\n\`\`\``;
161
+ done: ALREADY
162
+ }];
163
+ case "pi": return [{
164
+ args: [
165
+ "mcp",
166
+ "add",
167
+ "whiteboard",
168
+ "--",
169
+ launch.command,
170
+ ...launch.args
171
+ ],
172
+ done: ALREADY
173
+ }, cleanup(["remove", PI_WHITEBOARD_PACKAGE], "if installed")];
174
+ }
175
+ }
176
+ function connectLines(target, launch, platform, cleanups = true) {
177
+ return connectSteps(target, launch, platform).filter((step) => cleanups || !step.note).map(({ args, note }) => `${shellWords([target, ...args])}${note ? ` # ${note}` : ""}`).join("\n");
178
+ }
179
+ function runBlock(lines) {
180
+ return `\`\`\`sh\n${lines}\n\`\`\``;
149
181
  }
150
182
  function pluginSteps(target, platform) {
183
+ const launch = reviewMcpLaunch(true, platform);
151
184
  switch (target) {
152
185
  case "claude":
153
- if (platform === "win32") return [`Run:\n\n\`\`\`sh\nclaude plugin uninstall whiteboard@devfast # its launch cannot start on Windows, if installed\nclaude mcp remove -s user whiteboard # old registration, if any\n${CLAUDE_WINDOWS_MCP_ADD}\n\`\`\``];
154
- return [runBlock(CONNECT_COMMANDS.claude)];
155
- case "codex": return [runBlock(CONNECT_COMMANDS.codex)];
186
+ case "codex":
187
+ case "copilot": return [`Run:\n\n${runBlock(connectLines(target, launch, platform))}`];
156
188
  case "opencode": {
157
- const launch = reviewMcpLaunch(true, platform);
158
189
  const entry = JSON.stringify({ whiteboard: {
159
190
  type: "local",
160
191
  command: [launch.command, ...launch.args]
161
192
  } }, null, 2);
162
- return [`On OpenCode 2.0 or later, run:\n\n\`\`\`sh\nopencode mcp add --global whiteboard -- ${launchCommand(launch)}\n\`\`\`\n\nOn older OpenCode, add this entry to "mcp" in ~/.config/opencode/opencode.json, keeping the rest of the file:\n\n\`\`\`json\n${entry}\n\`\`\``, `Remove "@dev.fast/opencode-whiteboard" from "plugin" in ~/.config/opencode/opencode.json if it is there.`];
193
+ return [`On OpenCode 2.0 or later, run:\n\n${runBlock(connectLines(target, launch, platform))}\n\nOn older OpenCode, add this entry to "mcp" in ~/.config/opencode/opencode.json, keeping the rest of the file:\n\n\`\`\`json\n${entry}\n\`\`\``, `Remove "@dev.fast/opencode-whiteboard" from "plugin" in ~/.config/opencode/opencode.json if it is there.`];
163
194
  }
164
- case "copilot":
165
- if (platform === "win32") return [`Run:\n\n\`\`\`sh\ncopilot plugin uninstall whiteboard@devfast # its launch cannot start on Windows, if installed\ncopilot mcp remove whiteboard # old registration, if any\n${COPILOT_WINDOWS_MCP_ADD}\n\`\`\``];
166
- return ["Run:\n\n```sh\ncopilot plugin marketplace add devdotfast/whiteboard\ncopilot plugin install whiteboard@devfast\ncopilot mcp remove whiteboard # old manual registration, if any\n```"];
167
- case "pi": return [`On Pi 0.99.0 or later, run:\n\n\`\`\`sh\npi mcp add whiteboard -- ${launchCommand(reviewMcpLaunch(true, platform))}\npi remove ${PI_WHITEBOARD_PACKAGE} # if installed\n\`\`\`\n\nOn older Pi, run \`pi install ${PI_WHITEBOARD_PACKAGE}\`.`];
195
+ case "pi": return [`On Pi 0.99.0 or later, run:\n\n${runBlock(connectLines(target, launch, platform))}\n\nOn older Pi, run \`pi install ${PI_WHITEBOARD_PACKAGE}\`.`];
168
196
  case "omp": return [`Add this entry to mcpServers in ~/.omp/agent/mcp.json, keeping the rest of the file:\n\n\`\`\`json\n${JSON.stringify({ whiteboard: reviewMcpLaunch(true, platform) }, null, 2)}\n\`\`\``, `Run \`omp plugin uninstall @dev.fast/pi-whiteboard\` if installed.`];
169
197
  }
170
198
  }
171
- function launchCommand(launch) {
172
- return [launch.command, ...launch.args].map((arg) => /^[\w./-]+$/.test(arg) ? arg : shellQuote(arg)).join(" ");
199
+ function shellWords(words) {
200
+ return words.map((word) => /^[\w./@:-]+$/.test(word) ? word : shellQuote(word)).join(" ");
173
201
  }
174
202
  function cursorSteps() {
175
203
  const launch = reviewMcpLaunch(true);
@@ -231,6 +259,263 @@ function isInstallTarget(value) {
231
259
  return ALL_INSTALL_TARGETS.some((target) => target === value);
232
260
  }
233
261
  //#endregion
262
+ //#region src/agent-connect.ts
263
+ const STEP_TIMEOUT_MS = 12e4;
264
+ const HARNESSES = {
265
+ claude: {
266
+ name: "Claude Code",
267
+ commands: ["claude"],
268
+ installDirs: [".claude/local", ".local/bin"]
269
+ },
270
+ codex: {
271
+ name: "Codex",
272
+ commands: ["codex"]
273
+ },
274
+ cursor: {
275
+ name: "Cursor",
276
+ commands: ["cursor"],
277
+ homeDir: ".cursor",
278
+ apps: ["/Applications/Cursor.app"]
279
+ },
280
+ opencode: {
281
+ name: "OpenCode",
282
+ commands: ["opencode"],
283
+ installDirs: [".opencode/bin"]
284
+ },
285
+ pi: {
286
+ name: "Pi",
287
+ commands: ["pi"]
288
+ },
289
+ omp: {
290
+ name: "oh-my-pi",
291
+ commands: ["omp"],
292
+ homeDir: ".omp"
293
+ },
294
+ copilot: {
295
+ name: "Copilot CLI",
296
+ commands: ["copilot"]
297
+ }
298
+ };
299
+ const runProcess = (file, args) => new Promise((resolve) => {
300
+ execFile(file, args, {
301
+ timeout: STEP_TIMEOUT_MS,
302
+ maxBuffer: 4194304,
303
+ env: {
304
+ ...process.env,
305
+ CI: "1",
306
+ GIT_TERMINAL_PROMPT: "0"
307
+ },
308
+ windowsHide: true
309
+ }, (error, stdout, stderr) => {
310
+ const output = `${stdout}${stderr}`.trim();
311
+ resolve(error ? {
312
+ code: 1,
313
+ output: output || error.message
314
+ } : {
315
+ code: 0,
316
+ output
317
+ });
318
+ });
319
+ });
320
+ async function exists(target) {
321
+ try {
322
+ await stat(target);
323
+ return true;
324
+ } catch {
325
+ return false;
326
+ }
327
+ }
328
+ async function findHarness(harness, homeDir, env) {
329
+ const installed = (harness.installDirs ?? []).map((directory) => path.join(homeDir, directory));
330
+ for (const command of harness.commands) {
331
+ const found = await findExecutable(command, env) ?? await findExecutable(command, { PATH: installed.join(path.delimiter) });
332
+ if (found) return found;
333
+ }
334
+ }
335
+ async function harnessFound(harness, homeDir, env, platform) {
336
+ if (await findHarness(harness, homeDir, env)) return true;
337
+ if (harness.homeDir && await exists(path.join(homeDir, harness.homeDir))) return true;
338
+ if (platform !== "darwin") return false;
339
+ for (const app of harness.apps ?? []) if (await exists(app)) return true;
340
+ return false;
341
+ }
342
+ /** OpenCode's config is JSONC: drop comments and trailing commas, keep strings. */
343
+ const JSONC_EXTRAS = /("(?:\\.|[^"\\])*")|\/\/[^\n]*|\/\*[\s\S]*?\*\/|,(?=\s*[}\]])/g;
344
+ async function readJson(file) {
345
+ try {
346
+ return JSON.parse((await readFile(file, "utf8")).replace(JSONC_EXTRAS, "$1"));
347
+ } catch {
348
+ return;
349
+ }
350
+ }
351
+ /** Follows `keys` down nested JSON objects. */
352
+ function lookup(value, ...keys) {
353
+ let current = value;
354
+ for (const key of keys) {
355
+ if (!isJsonObject(current)) return void 0;
356
+ current = current[key];
357
+ }
358
+ return current;
359
+ }
360
+ /** Whether a `[plugins."…"]` table names the plugin, and does not turn it
361
+ * off. */
362
+ function codexHasPlugin(toml) {
363
+ const lines = toml.split("\n");
364
+ const start = lines.findIndex((line) => line.trim() === `[plugins."${WHITEBOARD_PLUGIN}"]`);
365
+ if (start === -1) return false;
366
+ for (const line of lines.slice(start + 1)) {
367
+ const trimmed = line.trim();
368
+ if (trimmed.startsWith("[")) break;
369
+ if (/^enabled\s*=\s*false\b/.test(trimmed)) return false;
370
+ }
371
+ return true;
372
+ }
373
+ /** Whether a harness already has Whiteboard, as its own config records it:
374
+ * the plugin it installed, or the MCP server added to it. Copilot keeps no
375
+ * record Whiteboard reads, so it always installs again, which is harmless. */
376
+ async function harnessConnected(target, homeDir, env, platform) {
377
+ const claudeDir = env.CLAUDE_CONFIG_DIR ?? path.join(homeDir, ".claude");
378
+ const hasServer = async (file, ...keys) => lookup(await readJson(file), ...keys, "whiteboard") !== void 0;
379
+ switch (target) {
380
+ case "claude": {
381
+ if (platform === "win32") return hasServer(path.join(homeDir, ".claude.json"), "mcpServers");
382
+ const [installed, settings] = await Promise.all([readJson(path.join(claudeDir, "plugins", "installed_plugins.json")), readJson(path.join(claudeDir, "settings.json"))]);
383
+ const installs = lookup(installed, "plugins", WHITEBOARD_PLUGIN);
384
+ return Array.isArray(installs) && installs.length > 0 && lookup(settings, "enabledPlugins", "whiteboard@devfast") !== false;
385
+ }
386
+ case "codex": try {
387
+ return codexHasPlugin(await readFile(path.join(env.CODEX_HOME ?? path.join(homeDir, ".codex"), "config.toml"), "utf8"));
388
+ } catch {
389
+ return false;
390
+ }
391
+ case "opencode": {
392
+ const configDir = path.join(env.XDG_CONFIG_HOME || path.join(homeDir, ".config"), "opencode");
393
+ for (const name of [
394
+ "opencode.json",
395
+ "opencode.jsonc",
396
+ "config.json"
397
+ ]) {
398
+ const config = await readJson(path.join(configDir, name));
399
+ if (lookup(config, "mcp", "whiteboard") !== void 0 || lookup(config, "mcp", "servers", "whiteboard") !== void 0) return true;
400
+ }
401
+ return false;
402
+ }
403
+ case "pi": return hasServer(path.join(homeDir, ".pi", "agent", "mcp.json"), "mcpServers");
404
+ case "omp": return hasServer(path.join(homeDir, ".omp", "agent", "mcp.json"), "mcpServers");
405
+ case "cursor": return cursorHasServer(homeDir);
406
+ case "copilot": return false;
407
+ }
408
+ }
409
+ /** Every harness Whiteboard connects to, whether this machine has it, and
410
+ * whether it already has Whiteboard. */
411
+ async function detectConnectAgents(input = {}) {
412
+ const homeDir = input.homeDir ?? os.homedir();
413
+ const env = input.env ?? process.env;
414
+ const platform = input.platform ?? process.platform;
415
+ return Promise.all(ALL_INSTALL_TARGETS.map(async (target) => {
416
+ const found = await harnessFound(HARNESSES[target], homeDir, env, platform);
417
+ return {
418
+ target,
419
+ name: HARNESSES[target].name,
420
+ found,
421
+ connected: found && await harnessConnected(target, homeDir, env, platform)
422
+ };
423
+ }));
424
+ }
425
+ /** oh-my-pi reads its MCP servers from one JSON file; merge ours in. */
426
+ async function connectOmp(homeDir, launch) {
427
+ const file = path.join(homeDir, ".omp", "agent", "mcp.json");
428
+ try {
429
+ const config = await exists(file) ? JSON.parse(await readFile(file, "utf8")) : {};
430
+ if (!isJsonObject(config)) throw new Error(`${file} is not a JSON object.`);
431
+ const servers = isJsonObject(config.mcpServers) ? { ...config.mcpServers } : {};
432
+ servers.whiteboard = {
433
+ command: launch.command,
434
+ args: [...launch.args]
435
+ };
436
+ const next = {
437
+ ...config,
438
+ mcpServers: servers
439
+ };
440
+ await mkdir(path.dirname(file), { recursive: true });
441
+ await writeFile(file, `${JSON.stringify(next, null, 2)}\n`);
442
+ return {
443
+ target: "omp",
444
+ state: "connected",
445
+ output: `Updated ${file}`
446
+ };
447
+ } catch (error) {
448
+ return {
449
+ target: "omp",
450
+ state: "failed",
451
+ output: error instanceof Error ? error.message : String(error)
452
+ };
453
+ }
454
+ }
455
+ /** Whether Cursor's global MCP config already names Whiteboard's server. */
456
+ async function cursorHasServer(homeDir) {
457
+ try {
458
+ const config = JSON.parse(await readFile(path.join(homeDir, ".cursor", "mcp.json"), "utf8"));
459
+ return isJsonObject(config) && isJsonObject(config.mcpServers) && config.mcpServers.whiteboard !== void 0;
460
+ } catch {
461
+ return false;
462
+ }
463
+ }
464
+ /** Cursor only installs an MCP server the user confirms, so open its link,
465
+ * unless it already has one: confirming again would ask for nothing new. */
466
+ async function connectCursor(homeDir, launch, platform, run) {
467
+ if (await cursorHasServer(homeDir)) return {
468
+ target: "cursor",
469
+ state: "connected",
470
+ output: "Cursor already has the whiteboard MCP server."
471
+ };
472
+ const url = cursorInstallDeeplink(launch);
473
+ const [file, args] = platform === "darwin" ? ["open", ["-g", url]] : platform === "win32" ? ["cmd", [
474
+ "/d",
475
+ "/c",
476
+ "start",
477
+ "\"\"",
478
+ url
479
+ ]] : ["xdg-open", [url]];
480
+ const result = await run(file, args);
481
+ return {
482
+ target: "cursor",
483
+ state: result.code === 0 ? "approve" : "failed",
484
+ output: result.output
485
+ };
486
+ }
487
+ /** Installs Whiteboard's plugin, or its MCP server, into one harness. */
488
+ async function connectAgent(target, input) {
489
+ const homeDir = input.homeDir ?? os.homedir();
490
+ const env = input.env ?? process.env;
491
+ const platform = input.platform ?? process.platform;
492
+ const run = input.run ?? runProcess;
493
+ const launch = reviewMcpLaunch(input.hasShim, platform);
494
+ if (target === "omp") return connectOmp(homeDir, launch);
495
+ if (target === "cursor") return connectCursor(homeDir, launch, platform, run);
496
+ const executable = await findHarness(HARNESSES[target], homeDir, env);
497
+ if (!executable) return {
498
+ target,
499
+ state: "failed",
500
+ output: `${HARNESSES[target].name} is not installed.`
501
+ };
502
+ const outputs = [];
503
+ for (const step of connectSteps(target, launch, platform)) {
504
+ const result = await run(executable, step.args);
505
+ outputs.push(result.output);
506
+ if (result.code !== 0 && !step.optional && !step.done?.test(result.output)) return {
507
+ target,
508
+ state: "failed",
509
+ output: outputs.join("\n").trim()
510
+ };
511
+ }
512
+ return {
513
+ target,
514
+ state: "connected",
515
+ output: outputs.join("\n").trim()
516
+ };
517
+ }
518
+ //#endregion
234
519
  //#region src/legacy-skills.ts
235
520
  const LEGACY_SKILL_NAMES = [
236
521
  "whiteboard",
@@ -638,17 +923,19 @@ function withShimPath(result, shimPath) {
638
923
  }
639
924
  /** The published plugin per harness; Cursor's link needs the shim it launches. */
640
925
  function connectPlugins(hasShim) {
926
+ const launch = reviewMcpLaunch(hasShim);
927
+ const command = (target) => connectLines(target, launch, process.platform, false);
641
928
  return {
642
929
  claude: process.platform === "win32" ? {
643
930
  label: "Add the Claude Code MCP server",
644
- command: CLAUDE_WINDOWS_MCP_ADD
931
+ command: command("claude")
645
932
  } : {
646
933
  label: "Install the Claude Code plugin",
647
934
  command: "/plugin marketplace add devdotfast/whiteboard\n/plugin install whiteboard@devfast"
648
935
  },
649
936
  codex: {
650
937
  label: "Install the Codex plugin",
651
- command: "codex plugin marketplace add devdotfast/whiteboard\ncodex plugin add whiteboard@devfast"
938
+ command: command("codex")
652
939
  },
653
940
  cursor: hasShim ? {
654
941
  label: "Install in Cursor",
@@ -656,11 +943,11 @@ function connectPlugins(hasShim) {
656
943
  } : { label: "Install in Cursor" },
657
944
  opencode: {
658
945
  label: "Add the OpenCode MCP server (OpenCode 2+)",
659
- command: `opencode mcp add --global whiteboard -- ${launchCommand(reviewMcpLaunch(hasShim))}`
946
+ command: command("opencode")
660
947
  },
661
948
  pi: {
662
949
  label: "Add the Pi MCP server (Pi 0.99+)",
663
- command: `pi mcp add whiteboard -- ${launchCommand(reviewMcpLaunch(hasShim))}`
950
+ command: command("pi")
664
951
  },
665
952
  omp: {
666
953
  label: "Add the oh-my-pi MCP server",
@@ -668,10 +955,10 @@ function connectPlugins(hasShim) {
668
955
  },
669
956
  copilot: process.platform === "win32" ? {
670
957
  label: "Add the Copilot CLI MCP server",
671
- command: COPILOT_WINDOWS_MCP_ADD
958
+ command: command("copilot")
672
959
  } : {
673
960
  label: "Install the Copilot CLI plugin",
674
- command: "copilot plugin marketplace add devdotfast/whiteboard\ncopilot plugin install whiteboard@devfast"
961
+ command: command("copilot")
675
962
  }
676
963
  };
677
964
  }
@@ -1055,4 +1342,4 @@ async function aliasInstallationToAccount(telemetry, env = process.env, fetchImp
1055
1342
  } catch {}
1056
1343
  }
1057
1344
  //#endregion
1058
- export { connectPrompts as _, isOwnedShim as a, removeLegacyReviewSkills as c, skipCliInstall as d, windowsInstallerCommand as f, CONNECT_COMMANDS as g, isInstallTarget as h, finishCliInstallUpdate as i, resetCliInstall as l, ALL_INSTALL_TARGETS as m, applyCliInstall as n, pathShimPath as o, scanLegacySkills as p, declineCliInstall as r, removeCliInstall as s, aliasInstallationToAccount as t, resolveCliInstallStatus as u, reviewMcpLaunch as v, cursorInstallDeeplink as y };
1345
+ export { isInstallTarget as _, isOwnedShim as a, removeLegacyReviewSkills as c, skipCliInstall as d, windowsInstallerCommand as f, ALL_INSTALL_TARGETS as g, detectConnectAgents as h, finishCliInstallUpdate as i, resetCliInstall as l, connectAgent as m, applyCliInstall as n, pathShimPath as o, scanLegacySkills as p, declineCliInstall as r, removeCliInstall as s, aliasInstallationToAccount as t, resolveCliInstallStatus as u, connectPrompts as v };
@@ -0,0 +1,2 @@
1
+ import { n as runReviewAgentCli } from "./agent-cli-S0bt9I7V.js";
2
+ export { runReviewAgentCli };
@@ -1,11 +1,11 @@
1
1
  import { n as isJsonObject, u as parseJsonText } from "./src-X9phtB2j.js";
2
- import { T as traceMachineEnabled } from "./review-home-paths-zwevIq4I.js";
3
- import { a as toolResultText, i as connectReviewInstance, s as ReviewApiError } from "./agent-client-Dogh8I83.js";
2
+ import { T as traceMachineEnabled } from "./review-home-paths-Lc02WFuX.js";
3
+ import { a as toolResultText, i as connectReviewInstance, s as ReviewApiError } from "./agent-client-DjXQy6CL.js";
4
4
  import { M as devReviewHome } from "./s3-config-DDWWMh5x.js";
5
- import { b as selectReviewInstance, u as REVIEW_INSTANCE_ENV } from "./server-discovery-CqRxbsER.js";
6
- import { n as reviewSessionAgent } from "./review-telemetry-9ceqh4wz.js";
7
- import { n as REVIEW_VIA_HEADER, t as REVIEW_AGENT_HEADER } from "./request-origin-C-V9f3Fr.js";
8
- import { i as publicTool, n as RECOVERY, r as callPublicTool, t as toolFailure } from "./tool-failure-aSYcHDxP.js";
5
+ import { b as selectReviewInstance, u as REVIEW_INSTANCE_ENV } from "./server-discovery-DVqrcIDT.js";
6
+ import { n as reviewSessionAgent } from "./review-telemetry-DnQNwgR1.js";
7
+ import { n as REVIEW_VIA_HEADER, t as REVIEW_AGENT_HEADER } from "./request-origin-D1RNVSeK.js";
8
+ import { i as publicTool, n as RECOVERY, r as callPublicTool, t as toolFailure } from "./tool-failure-CVJrfi5s.js";
9
9
  //#region src/review-api/agent-cli.ts
10
10
  const reviewAgentCliHelp = "whiteboard api tools (one line per tool)\nwhiteboard api tools <tool-name> (its description and input schema)\nwhiteboard api <tool-name> '<json>'\nwhiteboard api <tool-name> - (read JSON from stdin)\nwhiteboard mcp (stdio MCP adapter; Whiteboard Desktop or whiteboard server start must be running)\nSelect headless state with DEV_REVIEW_SERVER_DIR or whiteboard --state-dir <path> api/mcp.\n";
11
11
  async function runReviewAgentCli(input) {
@@ -20,7 +20,7 @@ async function runReviewAgentCli(input) {
20
20
  if (extra.length || mode === "mcp" && name) throw new Error("Unexpected arguments. Use whiteboard api --help.");
21
21
  const headers = Object.fromEntries([[REVIEW_VIA_HEADER, mode === "mcp" ? "mcp" : "api"], [REVIEW_AGENT_HEADER, reviewSessionAgent(env)]]);
22
22
  if (mode === "mcp") {
23
- const { serveReviewMcp } = await import("./mcp-Blb7rzGw.js");
23
+ const { serveReviewMcp } = await import("./mcp-BY4ExeMQ.js");
24
24
  await serveReviewMcp(async (key, agentKind) => {
25
25
  const connected = await connectReviewInstance(key ? {
26
26
  ...env,
@@ -1,7 +1,7 @@
1
1
  import { d as isBooleanValue, f as isNumberValue, m as isStringValue, u as parseJsonText } from "./src-X9phtB2j.js";
2
- import "./src-HnIPjtjx.js";
2
+ import "./src-DZG1xAqW.js";
3
3
  import { t as ReviewInputError } from "./input-error-OLgB_h31.js";
4
- import { b as selectReviewInstance, c as reviewServerStateDir, d as ReviewInstanceUnavailableError, i as readReviewServerDiscovery, l as serverNotReady, m as healthyReviewInstance, s as reviewServerIsHealthy, y as reviewInstanceUnavailable } from "./server-discovery-CqRxbsER.js";
4
+ import { b as selectReviewInstance, c as reviewServerStateDir, d as ReviewInstanceUnavailableError, i as readReviewServerDiscovery, l as serverNotReady, m as healthyReviewInstance, s as reviewServerIsHealthy, y as reviewInstanceUnavailable } from "./server-discovery-DVqrcIDT.js";
5
5
  //#region ../review-protocol/src/review-api-client.ts
6
6
  const defaultRequest = (url, init) => fetch(url, init);
7
7
  /** A non-2xx reply; the status tells a caller whether retrying can help. */
@@ -68,7 +68,7 @@ var ReviewApiClient = class {
68
68
  reader.releaseLock();
69
69
  }
70
70
  }
71
- async follow(reviewId, signal, accept, disconnected, mode) {
71
+ async follow(reviewId, signal, accept, disconnected) {
72
72
  if (signal.aborted) return;
73
73
  let connections = liveConnections.get(this.request);
74
74
  if (!connections) liveConnections.set(this.request, connections = /* @__PURE__ */ new Map());
@@ -79,7 +79,6 @@ var ReviewApiClient = class {
79
79
  connections.set(key, live);
80
80
  }
81
81
  const subscription = { reviewId };
82
- if (mode) subscription.mode = mode;
83
82
  return live.add(subscription, signal, (value) => accept(value), disconnected);
84
83
  }
85
84
  /** Reopen the shared stream now rather than after its backoff. */
@@ -104,7 +103,7 @@ function sleep(ms, signal) {
104
103
  });
105
104
  }
106
105
  function concerns(line, subscription) {
107
- return line.kind === "list" ? subscription.reviewId === null && (subscription.mode ?? "structural") === line.mode : subscription.reviewId === line.reviewId;
106
+ return line.kind === "list" ? subscription.reviewId === null : subscription.reviewId === line.reviewId;
108
107
  }
109
108
  function report(listener, cause) {
110
109
  if (listener.signal.aborted) return;
@@ -250,7 +249,7 @@ async function connectReviewInstance(env = process.env, headers = {}, options =
250
249
  const selection = await selectReviewInstance({ env });
251
250
  const discovery = healthyReviewInstance(selection);
252
251
  if (!discovery) {
253
- const { ensureServerWithoutDesktop } = await import("./background-server-BKR_lpYb.js");
252
+ const { ensureServerWithoutDesktop } = await import("./background-server-CVL7QpsL.js");
254
253
  const server = await ensureServerWithoutDesktop({
255
254
  selection,
256
255
  env,
@@ -1,7 +1,7 @@
1
- import { _ as activityUpdateSchema, g as activityEndSchema, h as activityBeginSchema, i as commandSchema, n as uploadSchema } from "./local-data-D_9S--yI.js";
2
- import { S as publishedEditSchema, y as pathTargetSchema } from "./stored-document-migration-DenDEYdI.js";
1
+ import { c as activityEndSchema, i as commandSchema, l as activityUpdateSchema, n as uploadSchema, s as activityBeginSchema } from "./local-data-DN-A5p0K.js";
2
+ import { S as publishedEditSchema, y as pathTargetSchema } from "./stored-document-migration-DLBhkSfp.js";
3
3
  import { t as ReviewInputError } from "./input-error-OLgB_h31.js";
4
- import { t as findReviewPackageRoot } from "./package-paths-BsuI4WUf.js";
4
+ import { t as findReviewPackageRoot } from "./package-paths-jMg6JxDP.js";
5
5
  import { z } from "zod";
6
6
  import { readFile } from "node:fs/promises";
7
7
  import path from "node:path";
@@ -85,7 +85,8 @@ const COMPONENT_REFERENCE = [
85
85
  "- trace_quote {traceId, eventId, text}",
86
86
  "- software_map {mapVersionId, focusElementId?}",
87
87
  "- sequence {title, actors: {key: label}, steps: step[], pins?}",
88
- "- step {from, to (actor keys), label, style?: call|return|async, and exactly one of source: anchor, explanation, or code: {text, language?}; pins?}",
88
+ "- step {from, to (actor keys), label, style?: call|return|async, and exactly one of source: anchor or anchor[] (at most 4), explanation, or code: {text, language?}; pins?}",
89
+ "A step's source is the line where its arrow leaves `from` (the call, return or emit). Add an additional range only when that line can't be read without it: the type or schema of what it passes, or the code that builds its argument. Never the callee's body; that is the next step.",
89
90
  "- flow_diagram {title, nodes: flow_node[] (1 to 100), edges: flow_edge[], description?, direction?: right|down, pins?}",
90
91
  "- flow_node {key, label, description?, kind?: process|decision|terminal, attachments?: {label, sources: anchor[], pins?}[], link?: {from or to, label?, style?}}",
91
92
  "- flow_edge {from, to (node keys), label?, style?: solid|dashed}",
@@ -1,2 +1,2 @@
1
- import { a as headlessServerOwner, c as stopBackgroundServer, i as ensureServerWithoutDesktop, r as ensureBackgroundServer } from "./background-server-DJT0CnYH.js";
1
+ import { a as headlessServerOwner, c as stopBackgroundServer, i as ensureServerWithoutDesktop, r as ensureBackgroundServer } from "./background-server-ONy-xKJ1.js";
2
2
  export { ensureBackgroundServer, ensureServerWithoutDesktop, headlessServerOwner, stopBackgroundServer };
@@ -1,8 +1,8 @@
1
- import "./review-home-paths-zwevIq4I.js";
1
+ import "./review-home-paths-Lc02WFuX.js";
2
2
  import { D as liveLockOwner, O as processIsAlive } from "./s3-config-DDWWMh5x.js";
3
- import { t as findReviewPackageRoot } from "./package-paths-BsuI4WUf.js";
4
- import { a as readReviewServerHealth, c as reviewServerStateDir, i as readReviewServerDiscovery, n as desktopOwnsHome, p as desktopUsesHome, r as headlessServerLockPath } from "./server-discovery-CqRxbsER.js";
5
- import { t as desktopApplicationInstalled } from "./review-app-launcher-S2FZBY4W.js";
3
+ import { t as findReviewPackageRoot } from "./package-paths-jMg6JxDP.js";
4
+ import { a as readReviewServerHealth, c as reviewServerStateDir, i as readReviewServerDiscovery, n as desktopOwnsHome, p as desktopUsesHome, r as headlessServerLockPath } from "./server-discovery-DVqrcIDT.js";
5
+ import { t as desktopApplicationInstalled } from "./review-app-launcher-i1hmxOZA.js";
6
6
  import { mkdir, open, readFile, realpath, stat } from "node:fs/promises";
7
7
  import path from "node:path";
8
8
  import { spawn } from "node:child_process";