@dev.fast/whiteboard 0.2.5-preview.20261013.113 → 0.2.5-preview.20261015.116

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 (127) hide show
  1. package/THIRD_PARTY_NOTICES.md +29 -0
  2. package/dist/{account-alias-D0hF0sKk.js → account-alias-ChZR3RIU.js} +94 -8
  3. package/dist/{agent-cli-04xbDj23.js → agent-cli-CR2PnA9G.js} +9 -12
  4. package/dist/agent-cli-CRPPmS3l.js +2 -0
  5. package/dist/{agent-client-P3J4y8Zn.js → agent-client-DVd-prfp.js} +31 -14
  6. package/dist/{authoring-tools-fdA0RPIT.js → authoring-tools-BXKEwhfi.js} +5 -5
  7. package/dist/background-server-Cw7CcfaZ.js +156 -0
  8. package/dist/background-server-D2Rrr_e2.js +2 -0
  9. package/dist/build-info.json +1 -1
  10. package/dist/{cli-C0ziP6hg.js → cli-CSrz_wGk.js} +1 -1
  11. package/dist/{cli-runner-C3men22A.js → cli-runner-PlGqBeMw.js} +368 -252
  12. package/dist/cli.js +3 -2
  13. package/dist/{client-DzcSt6pV.js → client-DhZzPePs.js} +5 -3
  14. package/dist/{process-error-telemetry-BCBYp2t4.js → headless-host-CVXTSIlx.js} +3285 -2585
  15. package/dist/headless-host-Cp37C-v0.js +2 -0
  16. package/dist/input-error-OLgB_h31.js +11 -0
  17. package/dist/{local-data-Dy1pNtLF.js → local-data-yfLp5PiJ.js} +113 -62
  18. package/dist/{mcp-B8kuRSXM.js → mcp-DBbDwG4y.js} +4 -4
  19. package/dist/{package-paths-B9PDoAl6.js → package-paths-BsuI4WUf.js} +1 -1
  20. package/dist/profile-BSOkA6A-.js +2 -0
  21. package/dist/{profile-tbGWuWHu.js → profile-pAzg7fxC.js} +3 -3
  22. package/dist/remote-attach-BAt2eZx3.js +137 -0
  23. package/dist/remote-extensions-2Cxf3hMo.js +2 -0
  24. package/dist/remote-extensions-BiNx6NQK.js +323 -0
  25. package/dist/remote-language-server-DQgWUNeV.js +252 -0
  26. package/dist/remote-uninstall-DLtDG0o7.js +153 -0
  27. package/dist/{request-origin-DA4K1qWY.js → request-origin-DaRIV3cB.js} +5 -2
  28. package/dist/review-app-launcher-D7I4DGoH.js +222 -0
  29. package/dist/{review-home-paths-Y9mYWkCI.js → review-home-paths-zwevIq4I.js} +7 -7
  30. package/dist/{review-telemetry-CmM6tCbj.js → review-telemetry-BqB4CiTD.js} +5 -5
  31. package/dist/{s3-SzLpeCG6.js → s3-77-qkEhs.js} +1 -1
  32. package/dist/{s3-config-BgxkSoOy.js → s3-config-0xuCh-F_.js} +1 -1
  33. package/dist/{s3-config-CAxhO9u_.js → s3-config-DDWWMh5x.js} +5 -1
  34. package/dist/server/desktop-host.js +1663 -266
  35. package/dist/{desktop-discovery-Ba3dD5Cn.js → server-discovery-CifQ3yQS.js} +88 -3
  36. package/dist/sharing/index.d.ts +88 -32
  37. package/dist/sharing/index.js +1 -1
  38. package/dist/{src-DiHP11l3.js → src-BMPuxWv4.js} +6 -11
  39. package/dist/{src-WCbabvdS.js → src-Bgvf1sEN.js} +128 -8
  40. package/dist/{stored-document-migration-u4L_1VZa.js → stored-document-migration-CFOvjRwW.js} +5 -18
  41. package/dist/{tool-failure-CJHBCHMh.js → tool-failure-BXWzQG8T.js} +2 -2
  42. package/instructions/authoring.md +16 -12
  43. package/instructions/file-lenses.md +4 -2
  44. package/package.json +8 -13
  45. package/src/agent-connect.ts +507 -0
  46. package/src/agent-selection.ts +4 -0
  47. package/src/ask/agents.ts +59 -13
  48. package/src/ask/pi-mcp.ts +7 -3
  49. package/src/ask/thread-state.ts +9 -4
  50. package/src/ask/thread.ts +88 -50
  51. package/src/ask/threads.ts +53 -7
  52. package/src/cli-install.ts +3 -1
  53. package/src/cli-runner.ts +472 -81
  54. package/src/cli.ts +1 -1
  55. package/src/connect-agents.ts +285 -0
  56. package/src/connect-prompts.ts +60 -6
  57. package/src/desktop-discovery.ts +24 -0
  58. package/src/pinned-download.ts +118 -0
  59. package/src/remote-attach.ts +130 -0
  60. package/src/remote-extensions.ts +359 -0
  61. package/src/remote-language-server.ts +428 -0
  62. package/src/remote-toolchains.ts +107 -0
  63. package/src/remote-uninstall.ts +280 -0
  64. package/src/review-api/README.md +1 -3
  65. package/src/review-api/agent-cli.ts +4 -4
  66. package/src/review-api/agent-client.ts +23 -0
  67. package/src/review-api/ask-history.ts +31 -16
  68. package/src/review-api/authoring-tools.ts +1 -1
  69. package/src/review-api/blocks/index.ts +2 -4
  70. package/src/review-api/document.ts +1 -4
  71. package/src/review-api/http.ts +265 -237
  72. package/src/review-api/local-data.ts +18 -3
  73. package/src/review-api/onboarding.ts +478 -0
  74. package/src/review-api/origin.ts +7 -0
  75. package/src/review-api/recent-folders.ts +173 -0
  76. package/src/review-api/store.ts +56 -10
  77. package/src/review-api/workspaces.ts +56 -11
  78. package/src/review-app-launcher.ts +61 -3
  79. package/src/review-stack.ts +162 -36
  80. package/src/server/background-server.ts +250 -0
  81. package/src/server/desktop-host-shutdown.ts +24 -0
  82. package/src/server/desktop-host.ts +39 -24
  83. package/src/server/desktop-server.ts +68 -118
  84. package/src/server/global-verb-relay.ts +16 -0
  85. package/src/server/headless-host.ts +128 -3
  86. package/src/server/hono-http.ts +6 -1
  87. package/src/server/legacy-tutorial-cleanup.ts +38 -0
  88. package/src/server/review-gateway-hosts.ts +379 -0
  89. package/src/server/review-gateway-list.ts +109 -0
  90. package/src/server/review-gateway-memory.ts +130 -0
  91. package/src/server/review-gateway-pushes.ts +142 -0
  92. package/src/server/review-gateway-streams.ts +415 -0
  93. package/src/server/review-gateway-transport.ts +204 -0
  94. package/src/server/review-gateway.ts +433 -0
  95. package/src/server/review-server-core.ts +36 -6
  96. package/src/server-discovery.ts +11 -0
  97. package/src/sharing/import.ts +1 -0
  98. package/src/ui-telemetry-events.ts +5 -1
  99. package/src/vsix.ts +122 -0
  100. package/dist/agent-cli-BR_Jw5BL.js +0 -2
  101. package/dist/headless-host-BLLJ7Dyu.js +0 -95
  102. package/dist/profile-aMSw1oCM.js +0 -2
  103. package/dist/server-discovery-ZVi8Ay8J.js +0 -66
  104. package/src/fixtures/blocks/tutorial.json +0 -14
  105. package/src/review-api/blocks/tutorial.ts +0 -23
  106. package/src/server/tutorial-service.ts +0 -217
  107. package/tutorial/document.json +0 -216
  108. package/tutorial/lenses.json +0 -10
  109. package/tutorial/runtime-manifest.json +0 -11
  110. package/tutorial/sample-service/package.json +0 -9
  111. package/tutorial/sample-service/src/api/checkout-api.ts +0 -17
  112. package/tutorial/sample-service/src/app.ts +0 -26
  113. package/tutorial/sample-service/src/database/drizzle-orm.d.ts +0 -16
  114. package/tutorial/sample-service/src/database/schema.ts +0 -9
  115. package/tutorial/sample-service/src/fulfillment/fulfillment-queue.ts +0 -15
  116. package/tutorial/sample-service/src/fulfillment/fulfillment-worker.ts +0 -24
  117. package/tutorial/sample-service/src/inventory/forklift-mood.ts +0 -4
  118. package/tutorial/sample-service/src/inventory/inventory-service.ts +0 -28
  119. package/tutorial/sample-service/src/inventory/warehouse-lore.ts +0 -11
  120. package/tutorial/sample-service/src/orders/order-service.ts +0 -35
  121. package/tutorial/sample-service/src/orders/order.ts +0 -43
  122. package/tutorial/sample-service/src/orders/orders-repository.ts +0 -24
  123. package/tutorial/sample-service/src/payments/fraud-vibes.ts +0 -4
  124. package/tutorial/sample-service/src/payments/payment-gateway.ts +0 -29
  125. package/tutorial/sample-service/src/payments/raccoon-ledger.ts +0 -9
  126. package/tutorial/sample-service/src/shipping/shipping-gateway.ts +0 -15
  127. package/tutorial/sample-service/tsconfig.json +0 -12
@@ -1,27 +1,31 @@
1
- import { E as __toESM, S as setLocalVcsCommandObserver, T as __require, w as __commonJSMin } from "./src-DiHP11l3.js";
1
+ import { C as setLocalVcsCommandObserver, E as __toESM, T as __require, w as __commonJSMin } from "./src-BMPuxWv4.js";
2
2
  import { c as jsonString, n as isJsonObject, u as parseJsonText } from "./src-X9phtB2j.js";
3
- import { a as parseReviewVerbResponse } from "./src-WCbabvdS.js";
4
- import { $ as resolveTraceCommand, A as runTraceDeny, E as traceMachineStatus, F as describeSelection, H as resolveTraceRepositoryTarget, J as HOSTED_CAPTURE_SCOPE_DESCRIPTION, K as traceRepositoryStatus, L as loadS3TraceStorage, M as runTraceSessions, N as runTraceStoreDelete, P as runTraceStoreInfo, T as traceMachineEnabled, V as requireTraceConsent, _ as runTraceRepair, b as runTraceGitHook, c as registerTraceCommands, ct as runStoreLogout, d as runTraceList$1, dt as StoreClient, f as runTracePull$1, ft as withStoreAuthorization, g as runTraceInstallMachine, gt as jsonRequestedInArgv, h as runTraceEnable, ht as humanStream, j as runTraceOnboard, k as runTraceAllow, l as resolveTraceReadStorage, lt as runStoreWhoami, m as runTraceDisable, mt as failWithJsonError, nt as traceScope, ot as readStoreAuth, p as runTraceShow, pt as emitJsonEvent, q as readTraceUserConfig, rt as DEFAULT_STORE_ORIGIN, st as runStoreLogin, tt as traceHomeDir, u as runTraceBlame, ut as StoreApiError, v as runTraceStatus, w as readLegacyCaptureSettings, x as runTraceHook, y as runTraceSync, z as selectTraceStorage } from "./review-home-paths-Y9mYWkCI.js";
5
- import { o as ReviewApiClient, s as ReviewApiError } from "./agent-client-P3J4y8Zn.js";
3
+ import { _ as REVIEW_REMOTE_AGENT_IDS, p as REVIEW_CLI_INSTALL_TARGET_LABELS, v as REVIEW_REMOTE_ATTACH_BEGIN, y as REVIEW_REMOTE_ATTACH_END } from "./src-Bgvf1sEN.js";
4
+ import { A as runTraceDeny, E as traceMachineStatus, F as describeSelection, H as resolveTraceRepositoryTarget, J as HOSTED_CAPTURE_SCOPE_DESCRIPTION, K as traceRepositoryStatus, L as loadS3TraceStorage, M as runTraceSessions, N as runTraceStoreDelete, P as runTraceStoreInfo, T as traceMachineEnabled, V as requireTraceConsent, X as agentTraceHomeDirectory, _ as runTraceRepair, _t as jsonRequestedInArgv, b as runTraceGitHook, c as registerTraceCommands, ct as runStoreLogin, d as runTraceList$1, dt as StoreApiError, et as resolveTraceCommand, f as runTracePull$1, ft as StoreClient, g as runTraceInstallMachine, gt as humanStream, h as runTraceEnable, ht as failWithJsonError, it as DEFAULT_STORE_ORIGIN, j as runTraceOnboard, k as runTraceAllow, l as resolveTraceReadStorage, lt as runStoreLogout, m as runTraceDisable, mt as emitJsonEvent, nt as traceHomeDir, p as runTraceShow, pt as withStoreAuthorization, q as readTraceUserConfig, rt as traceScope, st as readStoreAuth, u as runTraceBlame, ut as runStoreWhoami, v as runTraceStatus, w as readLegacyCaptureSettings, x as runTraceHook, y as runTraceSync, z as selectTraceStorage } from "./review-home-paths-zwevIq4I.js";
5
+ import { o as ReviewApiClient, s as ReviewApiError } from "./agent-client-DVd-prfp.js";
6
6
  import { t as resolveReviewRoot } from "./runtime-CKiWhGrf.js";
7
- import { A as devReviewHome, C as s3ProfileSchema, D as processIsAlive, E as writeTraceConfigFile, S as readTraceConfigFile, T as sameS3Profile, _ as emptyTraceConfig, c as resolveS3Setup, g as currentStore, h as TraceConfigurationError, i as isS3MockMode, j as normalizeStoreOrigin, k as DEV_REVIEW_HOME_ENV, n as clearTraceEnvCache, o as readTraceEnvFile, p as traceSettingsPath, w as s3Store } from "./s3-config-CAxhO9u_.js";
7
+ import { C as s3ProfileSchema, E as writeTraceConfigFile, M as devReviewHome, N as normalizeStoreOrigin, S as readTraceConfigFile, T as sameS3Profile, _ as emptyTraceConfig, c as resolveS3Setup, g as currentStore, h as TraceConfigurationError, i as isS3MockMode, n as clearTraceEnvCache, o as readTraceEnvFile, p as traceSettingsPath, w as s3Store } from "./s3-config-DDWWMh5x.js";
8
8
  import { t as errorMessage } from "./error-message-OtiDonty.js";
9
- import { i as readReviewPackageVersion, r as readBuildInfo, t as findReviewPackageRoot } from "./package-paths-B9PDoAl6.js";
10
- import { a as readDefaultReviewInstance, d as selectReviewInstance, f as writeDefaultReviewInstance, i as healthyReviewInstance, l as reviewInstanceStartHint, n as ReviewInstanceUnavailableError, o as readReviewInstances, r as clearDefaultReviewInstance, s as requireHealthyReviewDesktop, t as REVIEW_INSTANCE_ENV, u as reviewInstanceUnavailable } from "./desktop-discovery-Ba3dD5Cn.js";
11
- import { t as ReviewTelemetry } from "./review-telemetry-CmM6tCbj.js";
12
- import { a as isOwnedShim, f as windowsInstallerCommand, m as connectPrompts, o as pathShimPath, p as scanLegacySkills, t as aliasInstallationToAccount } from "./account-alias-D0hF0sKk.js";
9
+ import { i as readReviewPackageVersion, r as readBuildInfo, t as findReviewPackageRoot } from "./package-paths-BsuI4WUf.js";
10
+ import { a as readReviewServerHealth, b as selectReviewInstance, c as reviewServerStateDir, d as ReviewInstanceUnavailableError, f as clearDefaultReviewInstance, g as requireHealthyReviewDesktop, h as readDefaultReviewInstance, i as readReviewServerDiscovery, l as serverNotReady, m as healthyReviewInstance, p as desktopUsesHome, t as DESKTOP_TAKEOVER_SIGNAL, u as REVIEW_INSTANCE_ENV, x as writeDefaultReviewInstance, y as reviewInstanceUnavailable } from "./server-discovery-CifQ3yQS.js";
11
+ import { t as isDirectory } from "./fs-utils-BMPLt0cr.js";
12
+ import { t as ReviewTelemetry } from "./review-telemetry-BqB4CiTD.js";
13
+ import { _ as connectPrompts, a as isOwnedShim, f as windowsInstallerCommand, g as CONNECT_COMMANDS, h as isInstallTarget, m as ALL_INSTALL_TARGETS, o as pathShimPath, p as scanLegacySkills, t as aliasInstallationToAccount } from "./account-alias-ChZR3RIU.js";
13
14
  import { t as fuzzyRank } from "./fuzzy-match-BoAmcyak.js";
14
- import { n as readReviewServerDiscovery, o as reviewServerStateDir, r as readReviewServerHealth, s as serverNotReady } from "./server-discovery-ZVi8Ay8J.js";
15
- import { t as reviewAgentCliHelp } from "./agent-cli-04xbDj23.js";
16
- import { closeSync, existsSync, fstatSync, mkdirSync, mkdtempSync, openSync, readSync, renameSync, writeFileSync } from "node:fs";
17
- import { realpath } from "node:fs/promises";
15
+ import { n as focusReviewDesktop, r as runReviewAppLaunch } from "./review-app-launcher-D7I4DGoH.js";
16
+ import { t as reviewAgentCliHelp } from "./agent-cli-CR2PnA9G.js";
17
+ import { z } from "zod";
18
+ import { constants, existsSync, mkdirSync, renameSync, writeFileSync } from "node:fs";
19
+ import { access, readFile, realpath } from "node:fs/promises";
18
20
  import path from "node:path";
19
- import os, { tmpdir } from "node:os";
21
+ import os from "node:os";
20
22
  import { spawn } from "node:child_process";
21
23
  import { fileURLToPath } from "node:url";
24
+ import { Writable } from "node:stream";
22
25
  import { AsyncLocalStorage } from "node:async_hooks";
23
26
  import { Argument, Command, CommanderError, Option } from "commander";
24
27
  import { emitKeypressEvents } from "node:readline";
28
+ import { parse } from "smol-toml";
25
29
  import { createInterface as createInterface$1 } from "node:readline/promises";
26
30
  import pino from "pino";
27
31
  import { performance } from "node:perf_hooks";
@@ -44,213 +48,155 @@ function describeCliRuntime(info) {
44
48
  return `CLI: ${info.effectivePath}${info.delegated ? ` (delegated from ${info.requestedPath})` : " (direct)"}\nBuild: ${info.version ?? "unknown"}, commit ${info.commit ?? "unknown"}, dirty ${info.dirty ?? "unknown"}, built ${info.builtAt ?? "unknown"}\n`;
45
49
  }
46
50
  //#endregion
47
- //#region src/install.ts
48
- const ALL_INSTALL_TARGETS = [
49
- "claude",
50
- "codex",
51
- "cursor",
52
- "opencode",
53
- "pi",
54
- "omp",
55
- "copilot"
56
- ];
57
- function isInstallTarget(value) {
58
- return ALL_INSTALL_TARGETS.some((target) => target === value);
59
- }
60
- //#endregion
61
- //#region src/review-app-launcher.ts
62
- const RELEASE_APPS = {
63
- stable: {
64
- bundleId: "dev.fast.review",
65
- linuxLauncher: "/usr/bin/review-desktop"
66
- },
67
- preview: {
68
- bundleId: "dev.fast.review.preview",
69
- linuxLauncher: "/usr/bin/review-preview-desktop"
51
+ //#region src/connect-agents.ts
52
+ const AGENT_CONNECT_TARGETS = REVIEW_REMOTE_AGENT_IDS;
53
+ async function detectAgents(scope) {
54
+ const found = [];
55
+ for (const id of AGENT_CONNECT_TARGETS) {
56
+ if (!await isDirectory(agentTraceHomeDirectory(id, scope.homeDir, scope.env))) continue;
57
+ const manual = !await onPath(CONNECT_COMMANDS[id][0].argv[0], scope.env);
58
+ found.push({
59
+ id,
60
+ name: REVIEW_CLI_INSTALL_TARGET_LABELS[id],
61
+ connected: await connected(id, scope),
62
+ ...manual && { manual: true }
63
+ });
70
64
  }
71
- };
72
- /** "1" on launches without --focus; Desktop opens inactive. */
73
- const REVIEW_DESKTOP_BACKGROUND_ENV = "DEV_FAST_REVIEW_DESKTOP_BACKGROUND";
74
- const DEFAULT_LAUNCH_TIMEOUT_MS = 9e4;
75
- const POLL_INTERVAL_MS = 250;
76
- const EARLY_EXIT_GRACE_MS = 5e3;
77
- async function runReviewAppLaunch(input = {}, overrides = {}) {
78
- const fetch = overrides.fetch ?? globalThis.fetch;
79
- const runtime = {
80
- selectInstance: () => selectReviewInstance({ fetch }),
81
- fetch,
82
- focusDesktop: (discovery) => focusReviewDesktop(discovery, fetch),
83
- launchDesktop: launchDesktopApplication,
84
- now: Date.now,
85
- wait: (milliseconds) => new Promise((resolve) => setTimeout(resolve, milliseconds)),
86
- ...overrides
87
- };
88
- const selection = await runtime.selectInstance();
89
- const current = healthyReviewInstance(selection);
90
- if (current) {
91
- if (input.focus) await runtime.focusDesktop(current);
92
- return launchEvent("running", current.instanceId);
93
- }
94
- const running = selection.instances.filter((instance) => instance.healthy);
95
- if (selection.key !== "stable" && selection.key !== "preview") throw new Error(`${reviewInstanceStartHint(selection)}; \`whiteboard app launch\` starts only installed apps.`);
96
- if (selection.source === "fallback" && running.length > 1) throw reviewInstanceUnavailable(selection);
97
- const launch = {
98
- focus: input.focus,
99
- noSandbox: input.noSandbox
100
- };
101
- if (selection.source !== "fallback") launch.instance = {
102
- key: selection.key,
103
- appPath: selection.instance?.discovery.appPath
104
- };
105
- const attempt = runtime.launchDesktop(launch);
106
- let completion = observedCompletion(attempt);
107
- completion.catch(() => void 0);
108
- const deadline = runtime.now() + (input.timeoutMs ?? DEFAULT_LAUNCH_TIMEOUT_MS);
109
- let unexpectedSuccessfulExitAt;
110
- while (runtime.now() < deadline) {
111
- const ready = healthyReviewInstance(await runtime.selectInstance());
112
- if (ready) return launchEvent("launched", ready.instanceId);
113
- if (unexpectedSuccessfulExitAt !== void 0 && runtime.now() - unexpectedSuccessfulExitAt >= EARLY_EXIT_GRACE_MS) throw launchFailure(attempt, /* @__PURE__ */ new Error("the launch process exited before Desktop became ready"));
114
- const remaining = Math.max(0, deadline - runtime.now());
115
- const outcome = completion ? await Promise.race([completion.then((result) => ({ completion: result })), runtime.wait(Math.min(POLL_INTERVAL_MS, remaining)).then(() => ({ completion: null }))]) : await runtime.wait(Math.min(POLL_INTERVAL_MS, remaining)).then(() => ({ completion: null }));
116
- if (outcome.completion) {
117
- assertSuccessfulLaunchCompletion(attempt, outcome.completion);
118
- unexpectedSuccessfulExitAt = runtime.now();
119
- completion = void 0;
65
+ return found;
66
+ }
67
+ async function connectAgents(input) {
68
+ const results = [];
69
+ for (const id of input.agents) {
70
+ const deadline = Date.now() + (input.timeoutMs ?? 12e4);
71
+ let output = "";
72
+ const commands = CONNECT_COMMANDS[id];
73
+ for (const { argv, ifAny } of commands) {
74
+ const left = deadline - Date.now();
75
+ if (left <= 0) break;
76
+ const step = await run(argv, input.env, left);
77
+ output += `$ ${argv.join(" ")}\n${step.output}`;
78
+ if (!step.ok && !ifAny) {
79
+ output += `Stopped: ${argv.join(" ")} failed.\n`;
80
+ break;
81
+ }
120
82
  }
83
+ results.push({
84
+ id,
85
+ name: REVIEW_CLI_INSTALL_TARGET_LABELS[id],
86
+ connected: await connected(id, input),
87
+ output: output.slice(-4e3)
88
+ });
121
89
  }
122
- if (unexpectedSuccessfulExitAt !== void 0) throw launchFailure(attempt, /* @__PURE__ */ new Error("the launch process exited before Desktop became ready"));
123
- throw new Error(`Whiteboard Desktop did not become ready within ${Math.ceil((input.timeoutMs ?? DEFAULT_LAUNCH_TIMEOUT_MS) / 1e3)} seconds after ${attempt.method}. Open Whiteboard Desktop once, then run \`whiteboard app launch\` again.${launchDiagnostics(attempt.logPath)}`);
124
- }
125
- async function focusReviewDesktop(discovery, fetch) {
126
- const response = await fetch(`${discovery.url}/app/focus`, {
127
- method: "POST",
128
- headers: { "x-review-token": discovery.token },
129
- signal: AbortSignal.timeout(5e3)
130
- });
131
- const result = parseReviewVerbResponse(await response.json());
132
- if (!response.ok || !result.ok) throw new Error(result.ok ? `Whiteboard Desktop focus returned ${response.status}.` : result.error);
90
+ return results;
133
91
  }
134
- function launchDesktopApplication(input = {}) {
135
- const platform = input.platform ?? process.platform;
136
- if (platform !== "darwin" && platform !== "linux") return {
137
- method: `the ${platform} application launcher`,
138
- completion: Promise.reject(/* @__PURE__ */ new Error("automatic launch is available only on macOS and Linux"))
139
- };
140
- if (input.noSandbox && platform !== "linux") return {
141
- method: `the ${platform} application launcher`,
142
- completion: Promise.reject(/* @__PURE__ */ new Error("--no-sandbox is only supported for Linux Desktop launches."))
143
- };
144
- const electron = input.electron ?? Boolean(process.versions.electron);
145
- const env = { ...input.env ?? process.env };
146
- const execPath = input.execPath ?? process.execPath;
147
- const focus = input.focus === true;
148
- if (focus) delete env[REVIEW_DESKTOP_BACKGROUND_ENV];
149
- else env[REVIEW_DESKTOP_BACKGROUND_ENV] = "1";
150
- delete env.ELECTRON_RUN_AS_NODE;
151
- delete env.DEV_FAST_REVIEW_CHECKOUT;
152
- const release = RELEASE_APPS[input.instance?.key ?? "stable"];
153
- const stateRoot = env.DEV_FAST_REVIEW_DESKTOP_STATE_ROOT?.trim();
154
- const profileArgs = stateRoot ? [`--user-data-dir=${path.resolve(stateRoot, "user-data")}`, `--extensions-dir=${path.resolve(stateRoot, "extensions")}`] : [];
155
- let command = "/usr/bin/open";
156
- let method;
157
- let args;
158
- if (platform === "linux") {
159
- delete env.VSCODE_DEV;
160
- delete env.VSCODE_CLI;
161
- command = (input.instance ? "" : env.DEV_FAST_REVIEW_DESKTOP_COMMAND?.trim()) || release.linuxLauncher;
162
- method = `the installed Linux launcher at "${command}"`;
163
- if (electron && !env.DEV_FAST_REVIEW_DESKTOP_COMMAND?.trim()) {
164
- command = execPath;
165
- method = `the Desktop-managed bundle at "${command}"`;
166
- }
167
- args = profileArgs;
168
- if (input.noSandbox) args.push("--no-sandbox");
169
- } else {
170
- const appPath = input.instance ? input.instance.appPath : electron ? execPath.match(/^(.*?\.app)\/Contents\/MacOS\//)?.[1] : void 0;
171
- const target = appPath?.endsWith(".app") ? ["-a", appPath] : ["-b", release.bundleId];
172
- method = appPath?.endsWith(".app") ? `the macOS application at "${appPath}"` : `the macOS bundle identifier "${release.bundleId}"`;
173
- args = [
174
- "-n",
175
- "-W",
176
- ...focus ? [] : ["-g"],
177
- ...target
178
- ];
179
- for (const [key, value] of Object.entries(env)) if (value !== void 0 && /^DEV_(REVIEW|FAST)_/.test(key)) args.push("--env", `${key}=${value}`);
180
- if (profileArgs.length > 0) args.push("--args", ...profileArgs);
92
+ const WHITEBOARD_PLUGIN = "whiteboard@devfast";
93
+ const ClaudePluginsSchema = z.object({ plugins: z.record(z.string(), z.unknown()) });
94
+ const CodexConfigSchema = z.object({ plugins: z.record(z.string(), z.unknown()).optional() });
95
+ const CodexPluginSchema = z.object({ enabled: z.boolean().optional() });
96
+ const PiSettingsSchema = z.object({ packages: z.array(z.union([z.string(), z.object({ source: z.string() }).transform((entry) => entry.source)])) });
97
+ const PI_PACKAGE = /^npm:@dev\.fast\/pi-whiteboard(@.*)?$/;
98
+ const OPENCODE_PLUGIN = /"plugin"\s*:\s*\[[^\]]*"@dev\.fast\/opencode-whiteboard(@[^"]*)?"/;
99
+ const OpenCodeConfigSchema = z.object({ mcp: z.object({
100
+ whiteboard: z.unknown().optional(),
101
+ servers: z.object({ whiteboard: z.unknown().optional() }).optional()
102
+ }) });
103
+ /** OpenCode's config is JSONC: drop comments and trailing commas, keep strings. */
104
+ const JSONC_EXTRAS = /("(?:\\.|[^"\\])*")|\/\/[^\n]*|\/\*[\s\S]*?\*\/|,(?=\s*[}\]])/g;
105
+ function openCodeConnected(text) {
106
+ if (OPENCODE_PLUGIN.test(text)) return true;
107
+ try {
108
+ const { mcp } = OpenCodeConfigSchema.parse(JSON.parse(text.replace(JSONC_EXTRAS, "$1")));
109
+ return mcp.whiteboard !== void 0 || mcp.servers?.whiteboard !== void 0;
110
+ } catch {
111
+ return false;
181
112
  }
182
- const { promise: completion, resolve, reject } = Promise.withResolvers();
183
- let logPath;
184
- let logFd;
113
+ }
114
+ async function connected(id, scope) {
115
+ const home = agentTraceHomeDirectory(id, scope.homeDir, scope.env);
116
+ const read = (...parts) => readFile(path.join(home, ...parts), "utf8");
185
117
  try {
186
- if (platform === "linux") {
187
- logPath = path.join(mkdtempSync(path.join(tmpdir(), "whiteboard-launch-")), "stderr.log");
188
- logFd = openSync(logPath, "wx", 384);
118
+ switch (id) {
119
+ case "claude": {
120
+ const installs = ClaudePluginsSchema.parse(JSON.parse(await read("plugins", "installed_plugins.json"))).plugins[WHITEBOARD_PLUGIN];
121
+ return Array.isArray(installs) ? installs.length > 0 : Boolean(installs);
122
+ }
123
+ case "codex": {
124
+ const entry = CodexConfigSchema.parse(parse(await read("config.toml"))).plugins?.[WHITEBOARD_PLUGIN];
125
+ return entry !== void 0 && CodexPluginSchema.parse(entry).enabled !== false;
126
+ }
127
+ case "opencode":
128
+ for (const name of [
129
+ "opencode.json",
130
+ "opencode.jsonc",
131
+ "config.json"
132
+ ]) if (openCodeConnected(await read(name).catch(() => ""))) return true;
133
+ return false;
134
+ case "pi": return PiSettingsSchema.parse(JSON.parse(await read("agent", "settings.json"))).packages.some((entry) => PI_PACKAGE.test(entry));
189
135
  }
190
- const child = (input.spawn ?? spawn)(command, args, {
191
- detached: true,
136
+ } catch {
137
+ return false;
138
+ }
139
+ }
140
+ async function onPath(command, env) {
141
+ for (const directory of (env.PATH ?? "").split(path.delimiter)) {
142
+ if (!path.isAbsolute(directory)) continue;
143
+ const candidate = path.join(directory, command);
144
+ try {
145
+ await access(candidate, constants.X_OK);
146
+ return candidate;
147
+ } catch {}
148
+ }
149
+ }
150
+ async function run(argv, env, timeoutMs) {
151
+ const [command = "", ...args] = argv;
152
+ const file = await onPath(command, env);
153
+ if (!file) return {
154
+ output: `${command} was not found on PATH.\n`,
155
+ ok: false
156
+ };
157
+ return new Promise((resolve) => {
158
+ let output = "";
159
+ const keep = (chunk) => {
160
+ output = (output + chunk.toString("utf8")).slice(-4e3);
161
+ };
162
+ const child = spawn(file, args, {
192
163
  env,
193
- stdio: logFd === void 0 ? "ignore" : [
164
+ stdio: [
194
165
  "ignore",
195
- "ignore",
196
- logFd
197
- ]
166
+ "pipe",
167
+ "pipe"
168
+ ],
169
+ detached: true
198
170
  });
199
- child.once("error", (error) => reject(error));
200
- child.once("exit", (code, signal) => {
171
+ const timer = setTimeout(() => {
172
+ output += `\n${command} did not finish within ${Math.round(timeoutMs / 1e3)} seconds.\n`;
173
+ try {
174
+ process.kill(-child.pid, "SIGKILL");
175
+ } catch {
176
+ child.kill("SIGKILL");
177
+ }
178
+ }, timeoutMs);
179
+ child.stdout.on("data", keep);
180
+ child.stderr.on("data", keep);
181
+ child.once("error", (error) => {
182
+ clearTimeout(timer);
201
183
  resolve({
202
- code,
203
- signal
184
+ output: `${output}${error.message}\n`,
185
+ ok: false
186
+ });
187
+ });
188
+ child.once("close", (code) => {
189
+ clearTimeout(timer);
190
+ resolve(code === 0 ? {
191
+ output,
192
+ ok: true
193
+ } : {
194
+ output: `${output}${output.endsWith("\n") || !output ? "" : "\n"}(exit ${code ?? "signal"})\n`,
195
+ ok: false
204
196
  });
205
197
  });
206
- child.unref();
207
- } catch (error) {
208
- reject(error instanceof Error ? error : new Error(String(error)));
209
- } finally {
210
- if (logFd !== void 0) closeSync(logFd);
211
- }
212
- return {
213
- method,
214
- logPath,
215
- completion
216
- };
217
- }
218
- function launchEvent(state, instanceId) {
219
- return {
220
- event: "app",
221
- action: "launch",
222
- state,
223
- instanceId
224
- };
225
- }
226
- function launchFailure(attempt, error) {
227
- return /* @__PURE__ */ new Error(`Could not launch Whiteboard with ${attempt.method}: ${error.message}. Open Whiteboard once, then run \`whiteboard app launch\` again. A sandboxed agent must run it outside the sandbox.${launchDiagnostics(attempt.logPath)}`);
228
- }
229
- function assertSuccessfulLaunchCompletion(attempt, completion) {
230
- if (completion.code === 0 && !completion.signal) return;
231
- throw launchFailure(attempt, /* @__PURE__ */ new Error(completion.signal ? `the launch process exited on ${completion.signal}` : `the launch process exited with code ${completion.code ?? "unknown"}`));
232
- }
233
- function observedCompletion(attempt) {
234
- return attempt.completion.catch((error) => {
235
- throw launchFailure(attempt, error instanceof Error ? error : new Error(String(error)));
236
198
  });
237
199
  }
238
- function launchDiagnostics(logPath) {
239
- if (!logPath) return "";
240
- let tail = "";
241
- try {
242
- const fd = openSync(logPath, "r");
243
- try {
244
- const size = fstatSync(fd).size;
245
- const buffer = Buffer.alloc(Math.min(size, 16384));
246
- const read = readSync(fd, buffer, 0, buffer.length, size - buffer.length);
247
- tail = buffer.subarray(0, read).toString("utf8").trim().split("\n").slice(-20).join("\n");
248
- } finally {
249
- closeSync(fd);
250
- }
251
- } catch {}
252
- return `\nDesktop log: ${logPath}${tail ? `\n${tail}` : ""}`;
253
- }
254
200
  //#endregion
255
201
  //#region src/review-app-picker.ts
256
202
  const MAX_VISIBLE_ROWS = 10;
@@ -858,7 +804,7 @@ var require_pump = /* @__PURE__ */ __commonJSMin(((exports, module) => {
858
804
  //#endregion
859
805
  //#region ../../node_modules/split2/index.js
860
806
  var require_split2 = /* @__PURE__ */ __commonJSMin(((exports, module) => {
861
- const { Transform: Transform$1 } = __require("stream");
807
+ const { Transform: Transform$2 } = __require("stream");
862
808
  const { StringDecoder } = __require("string_decoder");
863
809
  const kLast = Symbol("last");
864
810
  const kDecoder = Symbol("decoder");
@@ -929,7 +875,7 @@ var require_split2 = /* @__PURE__ */ __commonJSMin(((exports, module) => {
929
875
  options.transform = transform;
930
876
  options.flush = flush;
931
877
  options.readableObjectMode = true;
932
- const stream = new Transform$1(options);
878
+ const stream = new Transform$2(options);
933
879
  stream[kLast] = "";
934
880
  stream[kDecoder] = new StringDecoder("utf8");
935
881
  stream.matcher = matcher;
@@ -3781,7 +3727,7 @@ var require_pretty = /* @__PURE__ */ __commonJSMin(((exports, module) => {
3781
3727
  var import_pino_pretty = /* @__PURE__ */ __toESM((/* @__PURE__ */ __commonJSMin(((exports, module) => {
3782
3728
  const { isColorSupported } = require_colorette();
3783
3729
  const pump = require_pump();
3784
- const { Transform } = __require("node:stream");
3730
+ const { Transform: Transform$1 } = __require("node:stream");
3785
3731
  const abstractTransport = require_pino_abstract_transport();
3786
3732
  const colors = require_colors();
3787
3733
  const { ERROR_LIKE_KEYS, LEVEL_KEY, LEVEL_LABEL, MESSAGE_KEY, TIMESTAMP_KEY } = require_constants();
@@ -3912,7 +3858,7 @@ var import_pino_pretty = /* @__PURE__ */ __toESM((/* @__PURE__ */ __commonJSMin(
3912
3858
  pretty = prettyFactory(opts);
3913
3859
  source.off("message", pinoConfigListener);
3914
3860
  });
3915
- const stream = new Transform({
3861
+ const stream = new Transform$1({
3916
3862
  objectMode: true,
3917
3863
  autoDestroy: true,
3918
3864
  transform(chunk, enc, cb) {
@@ -4482,23 +4428,58 @@ async function runReviewCli(input) {
4482
4428
  ...env,
4483
4429
  DEV_REVIEW_SERVER_DIR: path.resolve(cwd, stateDir)
4484
4430
  } : env;
4431
+ const refuse = (message, json) => {
4432
+ if (json) emitReviewEvent(input.stdout, {
4433
+ event: "error",
4434
+ error: {
4435
+ name: "Error",
4436
+ message
4437
+ }
4438
+ });
4439
+ else input.stderr.write(`${message}\n`);
4440
+ state.exitCode = 1;
4441
+ };
4485
4442
  const serverCommand = configureOutput(program.command("server").description("Run review authoring without Desktop"), "plain");
4486
- configureJsonOutput(serverCommand.command("start").description("Start the foreground authoring server; stop with Ctrl-C or SIGTERM").option("--state-dir <path>", "directory for saved reviews and server discovery").option("--port <port>", "loopback port (0 chooses an available port)", "0").option("--software-maps", "allow the authoring skill to generate optional software maps").addOption(new Option("--authoring-mode <mode>").hideHelp()), "plain").action(async (_options, command) => {
4443
+ configureJsonOutput(serverCommand.command("start").description("Start the foreground authoring server; stop with Ctrl-C or SIGTERM").option("--state-dir <path>", "directory for saved reviews and server discovery").option("--port <port>", "loopback port (0 chooses an available port)", "0").option("--software-maps", "allow the authoring skill to generate optional software maps").option("--detach", "start the server in the background, logging to review-server/server.log").addOption(new Option("--started-by <who>").choices([
4444
+ "user",
4445
+ "cli",
4446
+ "desktop"
4447
+ ]).default("user").hideHelp()).addOption(new Option("--authoring-mode <mode>").hideHelp()), "plain").action(async (_options, command) => {
4487
4448
  const options = command.optsWithGlobals();
4488
4449
  if (options.authoringMode !== void 0) throw new ReviewCliUsageError("--authoring-mode was removed with batch authoring; the server always authors interactively. Drop the option.");
4489
4450
  const port = Number(options.port);
4490
4451
  if (!Number.isInteger(port) || port < 0 || port > 65535) throw new ReviewCliUsageError("--port must be an integer between 0 and 65535.");
4491
4452
  const stateDir = reviewServerStateDir(authoringEnv(options.stateDir));
4453
+ if (options.detach) {
4454
+ const { ensureBackgroundServer } = await import("./background-server-D2Rrr_e2.js");
4455
+ const { discovery, started } = await ensureBackgroundServer({
4456
+ stateDir,
4457
+ env,
4458
+ args: [
4459
+ "--port",
4460
+ `${port}`,
4461
+ ...options.softwareMaps ? ["--software-maps"] : []
4462
+ ]
4463
+ });
4464
+ await writeServerStatus(discovery, stateDir, options.json, { started });
4465
+ return;
4466
+ }
4492
4467
  const controller = new AbortController();
4493
4468
  const stop = () => controller.abort();
4469
+ const takeover = () => {
4470
+ input.stderr.write(`Whiteboard Desktop took over ${stateDir}; stopping this server.\n`);
4471
+ stop();
4472
+ };
4494
4473
  process.once("SIGINT", stop);
4495
4474
  process.once("SIGTERM", stop);
4475
+ process.once(DESKTOP_TAKEOVER_SIGNAL, takeover);
4476
+ const { HeadlessServerBusyError, runHeadlessServer } = await import("./headless-host-Cp37C-v0.js");
4496
4477
  try {
4497
- const { runHeadlessServer } = await import("./headless-host-BLLJ7Dyu.js");
4498
4478
  await runHeadlessServer({
4499
4479
  stateDir,
4500
4480
  port,
4501
4481
  softwareMapEnabled: options.softwareMaps,
4482
+ startedBy: options.startedBy,
4502
4483
  signal: controller.signal,
4503
4484
  telemetry,
4504
4485
  onReady: ({ url, serverPid }) => {
@@ -4510,42 +4491,54 @@ async function runReviewCli(input) {
4510
4491
  })}\n` : `Whiteboard server ready at ${url}\nSaved reviews: ${stateDir}\n`);
4511
4492
  }
4512
4493
  });
4494
+ } catch (error) {
4495
+ if (!(error instanceof HeadlessServerBusyError)) throw error;
4496
+ refuse(error.message, options.json);
4513
4497
  } finally {
4514
4498
  process.off("SIGINT", stop);
4515
4499
  process.off("SIGTERM", stop);
4500
+ process.off(DESKTOP_TAKEOVER_SIGNAL, takeover);
4516
4501
  }
4517
4502
  });
4518
4503
  configureJsonOutput(serverCommand.command("status").description("Check whether the headless server is ready").option("--state-dir <path>", "directory selected when starting the server"), "plain").action(async (_options, command) => {
4519
4504
  const options = command.optsWithGlobals();
4520
4505
  const stateDir = reviewServerStateDir(authoringEnv(options.stateDir));
4521
4506
  const discovery = await readReviewServerDiscovery(stateDir);
4522
- const health = discovery && await readReviewServerHealth(discovery);
4523
- if (!discovery || !health) throw serverNotReady(stateDir);
4524
- const { url, serverPid } = discovery;
4525
- const { version, serverId } = health;
4507
+ if (!discovery) throw serverNotReady(stateDir);
4508
+ await writeServerStatus(discovery, stateDir, options.json);
4509
+ });
4510
+ configureJsonOutput(serverCommand.command("stop").description("Stop a background server started by --detach, the CLI, or Desktop").option("--state-dir <path>", "directory selected when starting the server"), "plain").action(async (_options, command) => {
4511
+ const options = command.optsWithGlobals();
4512
+ const stateDir = reviewServerStateDir(authoringEnv(options.stateDir));
4513
+ const { headlessServerOwner, stopBackgroundServer } = await import("./background-server-D2Rrr_e2.js");
4514
+ const owner = await headlessServerOwner(stateDir);
4515
+ const discovery = await readReviewServerDiscovery(stateDir);
4516
+ if (owner === void 0) {
4517
+ input.stdout.write(options.json ? `${JSON.stringify({
4518
+ event: "server.stop",
4519
+ stopped: false,
4520
+ stateDir
4521
+ })}\n` : `No Whiteboard server is running in ${stateDir}.\n`);
4522
+ return;
4523
+ }
4524
+ const serverPid = owner;
4525
+ if (discovery?.serverPid !== owner) throw new Error(`Process ${owner} holds the Whiteboard server's lock in ${stateDir} but has published no server; it may be starting. Try again, or end that process.`);
4526
+ if (discovery.startedBy === "user") throw new Error(`The Whiteboard server in ${stateDir} (process ${serverPid}) runs in the foreground of \`whiteboard server start\`. Stop it there with Ctrl-C.`);
4527
+ await stopBackgroundServer(discovery);
4526
4528
  input.stdout.write(options.json ? `${JSON.stringify({
4527
- event: "server.status",
4528
- ready: true,
4529
- url,
4529
+ event: "server.stop",
4530
+ stopped: true,
4530
4531
  serverPid,
4531
- stateDir,
4532
- version,
4533
- serverId
4534
- })}\n` : `Whiteboard server ready at ${url}\nSaved reviews: ${stateDir}\n`);
4532
+ stateDir
4533
+ })}\n` : `Stopped the Whiteboard server (process ${serverPid}).\n`);
4535
4534
  });
4536
4535
  configureJsonOutput(serverCommand.command("reset-id").description("Give this machine's saved reviews a new server id; stop the server first").option("--state-dir <path>", "directory selected when starting the server"), "plain").action(async (_options, command) => {
4537
4536
  const options = command.optsWithGlobals();
4538
4537
  const stateDir = reviewServerStateDir(authoringEnv(options.stateDir));
4539
4538
  if (!existsSync(path.join(stateDir, "review-api.db"))) throw new Error(`No saved reviews in ${stateDir}, so there is no server id to reset. Check --state-dir.`);
4540
- const desktops = await readReviewInstances({ env: {
4541
- ...env,
4542
- [DEV_REVIEW_HOME_ENV]: stateDir
4543
- } });
4544
- const [problem] = desktops.broken.values();
4545
- if (problem) throw new Error(`Cannot tell whether a Whiteboard Desktop is using ${stateDir}: ${problem.message}`);
4546
- const inUse = () => /* @__PURE__ */ new Error(`A Whiteboard server is using ${stateDir}. Stop it first, then run whiteboard server reset-id again.`);
4547
- if (desktops.instances.some(({ discovery }) => [discovery.appPid, discovery.serverPid].some((pid) => processIsAlive(pid)))) throw inUse();
4548
- const [{ openReviewProfile }, { withHeadlessServerLock }] = await Promise.all([import("./profile-aMSw1oCM.js"), import("./headless-host-BLLJ7Dyu.js")]);
4539
+ const inUse = `A Whiteboard server is using ${stateDir}. Stop it first, then run whiteboard server reset-id again.`;
4540
+ if (await desktopUsesHome(stateDir, env)) return refuse(inUse, options.json);
4541
+ const [{ openReviewProfile }, { withHeadlessServerLock }] = await Promise.all([import("./profile-BSOkA6A-.js"), import("./headless-host-Cp37C-v0.js")]);
4549
4542
  const reset = await withHeadlessServerLock(await realpath(stateDir), async () => {
4550
4543
  const local = await openReviewProfile(stateDir, { manageWorkspaces: false });
4551
4544
  try {
@@ -4555,7 +4548,7 @@ async function runReviewCli(input) {
4555
4548
  await local.store.close();
4556
4549
  }
4557
4550
  });
4558
- if (!reset.acquired) throw inUse();
4551
+ if (!reset.acquired) return refuse(inUse, options.json);
4559
4552
  const serverId = reset.result;
4560
4553
  input.stdout.write(options.json ? `${JSON.stringify({
4561
4554
  event: "server.reset-id",
@@ -4563,6 +4556,104 @@ async function runReviewCli(input) {
4563
4556
  stateDir
4564
4557
  })}\n` : `New server id ${serverId}\nSaved reviews: ${stateDir}\n`);
4565
4558
  });
4559
+ const remote = configureOutput(program.command("remote").description("Serve reviews to Whiteboard Desktop over SSH"), "plain");
4560
+ configureJsonOutput(remote.command("attach").description("Start a background server if none is healthy and report how to reach it").option("--state-dir <path>", "directory for saved reviews and server discovery").option("--groups <groups>", "comma-separated optional extension groups the Desktop has enabled, such as go").option("--replace", "stop a running server of another version that the CLI or Desktop started, and start this one"), "plain").action(async (_options, command) => {
4561
+ const options = command.optsWithGlobals();
4562
+ const stateDir = reviewServerStateDir(authoringEnv(options.stateDir));
4563
+ const { remoteAttach } = await import("./remote-attach-BAt2eZx3.js");
4564
+ let attach;
4565
+ try {
4566
+ attach = await remoteAttach({
4567
+ stateDir,
4568
+ env,
4569
+ groups: options.groups?.split(",").map((group) => group.trim()),
4570
+ replace: options.replace
4571
+ });
4572
+ } catch (error) {
4573
+ if (!options.json) throw error;
4574
+ let line = "";
4575
+ emitReviewEvent(new Writable({ write(chunk, _encoding, done) {
4576
+ line += chunk;
4577
+ done();
4578
+ } }), {
4579
+ event: "error",
4580
+ error: serializeReviewError(error)
4581
+ });
4582
+ input.stdout.write(`${REVIEW_REMOTE_ATTACH_BEGIN}\n${ensureTrailingNewline(line)}${REVIEW_REMOTE_ATTACH_END}\n`);
4583
+ state.exitCode = 1;
4584
+ return;
4585
+ }
4586
+ input.stdout.write(options.json ? `${REVIEW_REMOTE_ATTACH_BEGIN}\n${JSON.stringify(attach)}\n${REVIEW_REMOTE_ATTACH_END}\n` : `Whiteboard server ${attach.startedServer ? "started" : "already running"} at ${attach.url}\nLanguage features: ${attach.languageServer ? `VS Code server on port ${attach.languageServer.port}` : `unavailable (${attach.languageServerDetail})`}\n`);
4587
+ });
4588
+ configureJsonOutput(remote.command("extensions").description("Language extensions for the VS Code server on a remote").command("ensure").description("Download the curated extensions for this machine and list them for the VS Code server").option("--groups <groups>", "comma-separated optional extension groups the Desktop has enabled, such as go"), "plain").action(async (_options, command) => {
4589
+ const { json, groups } = command.optsWithGlobals();
4590
+ const { ensureRemoteExtensions } = await import("./remote-extensions-2Cxf3hMo.js");
4591
+ const result = await ensureRemoteExtensions({
4592
+ env,
4593
+ groups: groups?.split(",").map((group) => group.trim())
4594
+ });
4595
+ input.stdout.write(json ? `${JSON.stringify(result)}\n` : [
4596
+ ...result.installed.map((id) => `Installed ${id}`),
4597
+ ...result.skipped.map(({ id, reason }) => `Skipped ${id}: ${reason}`),
4598
+ ...result.failed.map(({ id, error }) => `Failed ${id}: ${error}`)
4599
+ ].map((line) => `${line}\n`).join(""));
4600
+ state.exitCode = result.failed.length > 0 ? 1 : 0;
4601
+ });
4602
+ configureJsonOutput(remote.command("uninstall").description("Remove what Whiteboard Desktop installed on this host, stopping a server it started").addOption(new Option("--keep-reviews", "leave the saved reviews in place").conflicts("deleteReviews")).addOption(new Option("--delete-reviews", "also delete the saved reviews")).option("--state-dir <path>", "directory for saved reviews and server discovery"), "plain").action(async (_options, command) => {
4603
+ const options = command.optsWithGlobals();
4604
+ const stateDir = reviewServerStateDir(authoringEnv(options.stateDir));
4605
+ const { remoteUninstall } = await import("./remote-uninstall-DLtDG0o7.js");
4606
+ let deleteReviews = options.deleteReviews ? true : options.keepReviews ? false : void 0;
4607
+ if (deleteReviews === void 0 && !options.json && input.stdin?.isTTY) {
4608
+ const prompt = createInterface$1({
4609
+ input: input.stdin,
4610
+ output: input.stderr
4611
+ });
4612
+ try {
4613
+ deleteReviews = /^y(es)?$/i.test((await prompt.question(`Also delete the saved reviews in ${stateDir}? [y/N] `)).trim());
4614
+ } finally {
4615
+ prompt.close();
4616
+ }
4617
+ }
4618
+ const result = deleteReviews === void 0 ? {
4619
+ event: "remote.uninstall",
4620
+ ok: false,
4621
+ reason: "Say what happens to the saved reviews: pass --keep-reviews or --delete-reviews."
4622
+ } : await remoteUninstall({
4623
+ home: env.HOME ?? "",
4624
+ env,
4625
+ stateDir,
4626
+ deleteReviews
4627
+ });
4628
+ if (!result.ok) {
4629
+ if (!options.json) throw new Error(result.reason);
4630
+ input.stdout.write(`${JSON.stringify(result)}\n`);
4631
+ state.exitCode = 1;
4632
+ return;
4633
+ }
4634
+ input.stdout.write(options.json ? `${JSON.stringify(result)}\n` : [
4635
+ ...result.stoppedServer ? [`Stopped the Whiteboard server (process ${result.stoppedServer.pid}).`] : [],
4636
+ ...result.removed.map((removed) => `Removed ${removed}`),
4637
+ result.keptReviews ? `Saved reviews kept in ${stateDir}` : "Saved reviews deleted.",
4638
+ ""
4639
+ ].join("\n"));
4640
+ });
4641
+ async function writeServerStatus(discovery, stateDir, json, extra = {}) {
4642
+ const health = await readReviewServerHealth(discovery);
4643
+ if (!health) throw serverNotReady(stateDir);
4644
+ const { url, serverPid } = discovery;
4645
+ const { version, serverId } = health;
4646
+ input.stdout.write(json ? `${JSON.stringify({
4647
+ event: "server.status",
4648
+ ready: true,
4649
+ url,
4650
+ serverPid,
4651
+ stateDir,
4652
+ version,
4653
+ serverId,
4654
+ ...extra
4655
+ })}\n` : `Whiteboard server ${extra.started === false ? "already running" : "ready"} at ${url}\nSaved reviews: ${stateDir}\n`);
4656
+ }
4566
4657
  configureJsonOutput(program.command("version").description("Print Whiteboard package version").option("--verbose", "Show executing CLI paths and build identity"), "plain").action(async (options, command) => {
4567
4658
  const { json } = command.optsWithGlobals();
4568
4659
  if (options.verbose) {
@@ -4653,7 +4744,46 @@ async function runReviewCli(input) {
4653
4744
  "omp",
4654
4745
  "copilot",
4655
4746
  "all"
4656
- ])), "plain").action(async (targets, options) => {
4747
+ ])).addOption(new Option("--detect", "list the agents on this machine and whether each is connected; changes nothing").conflicts("yes")).option("--yes", "run the commands the prompt gives, without an agent: claude, codex, opencode or pi"), "plain").action(async (targets, options) => {
4748
+ const output = {
4749
+ json: options.json,
4750
+ stdout: input.stdout,
4751
+ stderr: input.stderr
4752
+ };
4753
+ if (options.detect) {
4754
+ const agents = await detectAgents({
4755
+ homeDir: scope.homeDir,
4756
+ env
4757
+ });
4758
+ if (options.json) {
4759
+ emitJsonEvent(output, {
4760
+ event: "connect.detect",
4761
+ agents
4762
+ });
4763
+ return;
4764
+ }
4765
+ humanStream(output).write(agents.length ? `${agents.map((agent) => `${agent.name}: ${agent.connected ? "connected" : "not connected"}${agent.manual ? " (its command is not on PATH)" : ""}`).join("\n")}\n` : "No agents found.\n");
4766
+ return;
4767
+ }
4768
+ if (options.yes) {
4769
+ const agents = parseTargets(targets);
4770
+ if (targets.length === 0 || !agents.every((target) => AGENT_CONNECT_TARGETS.some((known) => known === target))) throw new Error("connect --yes takes one or more of claude, codex, opencode or pi.");
4771
+ const results = await connectAgents({
4772
+ agents,
4773
+ homeDir: scope.homeDir,
4774
+ env
4775
+ });
4776
+ state.exitCode = results.every((result) => result.connected) ? 0 : 1;
4777
+ if (options.json) {
4778
+ emitJsonEvent(output, {
4779
+ event: "connect.run",
4780
+ agents: results
4781
+ });
4782
+ return;
4783
+ }
4784
+ humanStream(output).write(results.map((result) => `${result.output}${result.name}: ${result.connected ? "connected" : "not connected"}\n`).join(""));
4785
+ return;
4786
+ }
4657
4787
  const selected = parseTargets(targets);
4658
4788
  const { homeDir, devHome } = scope;
4659
4789
  const prompts = connectPrompts({
@@ -4666,11 +4796,6 @@ async function runReviewCli(input) {
4666
4796
  fffBinaryPath: path.join(homeDir, ".local", "bin", "fff-mcp"),
4667
4797
  fffCorpusRoot: path.join(devHome, "trace-search")
4668
4798
  });
4669
- const output = {
4670
- json: options.json,
4671
- stdout: input.stdout,
4672
- stderr: input.stderr
4673
- };
4674
4799
  if (options.json) {
4675
4800
  emitJsonEvent(output, {
4676
4801
  event: "connect",
@@ -4678,21 +4803,21 @@ async function runReviewCli(input) {
4678
4803
  });
4679
4804
  return;
4680
4805
  }
4681
- const sections = selected.map((target) => selected.length > 1 ? `## ${TARGET_LABELS[target]}\n\n${prompts[target]}` : prompts[target]);
4806
+ const sections = selected.map((target) => selected.length > 1 ? `## ${REVIEW_CLI_INSTALL_TARGET_LABELS[target]}\n\n${prompts[target]}` : prompts[target]);
4682
4807
  humanStream(output).write(`${sections.join("\n\n")}\n`);
4683
4808
  });
4684
4809
  configureJsonOutput(configureOutput(program.command("migrate", { hidden: true }), "plain").command("apply").option("--force"), "plain").action((_options, command) => {
4685
4810
  command.showHelpAfterError(false).error("Markdown review migration is no longer supported. Ask your agent to migrate your old Whiteboard reviews.");
4686
4811
  });
4687
4812
  configureJsonOutput(configureJsonOutput(program.command("share").description("Upload an immutable review snapshot and return its share link"), "plain").option("--review <id>", "review ID").option("--version <number>", "Saved version to share").option("--preview", "Open the share link in Whiteboard Preview by default").option("--request-id <uuid>", "Reuse this ID when retrying the same immutable upload").action(async (options) => {
4688
- const { runShareCli } = await import("./cli-C0ziP6hg.js");
4813
+ const { runShareCli } = await import("./cli-CSrz_wGk.js");
4689
4814
  state.exitCode = await runShareCli({
4690
4815
  ...input,
4691
4816
  ...options,
4692
4817
  env: authoringEnv()
4693
4818
  });
4694
4819
  }).command("revoke <share-id>").description("Revoke future downloads of a share"), "plain").action(async (shareId, options) => {
4695
- const { runShareCli } = await import("./cli-C0ziP6hg.js");
4820
+ const { runShareCli } = await import("./cli-CSrz_wGk.js");
4696
4821
  state.exitCode = await runShareCli({
4697
4822
  ...input,
4698
4823
  ...options,
@@ -4773,7 +4898,7 @@ async function runReviewCli(input) {
4773
4898
  return commands;
4774
4899
  } });
4775
4900
  for (const [name, description] of [["api", "Call a JSON review authoring tool on the running server"], ["mcp", "Serve the JSON review authoring tools over stdio MCP"]]) configureOutput(program.command(name).description(description).argument("[args...]", "tool name and JSON input").allowUnknownOption().allowExcessArguments().helpOption(false).passThroughOptions().addHelpText("after", `\n${reviewAgentCliHelp}`), "plain").action(async (args) => {
4776
- const { runReviewAgentCli } = await import("./agent-cli-BR_Jw5BL.js");
4901
+ const { runReviewAgentCli } = await import("./agent-cli-CRPPmS3l.js");
4777
4902
  state.exitCode = await runReviewAgentCli({
4778
4903
  ...input,
4779
4904
  env: authoringEnv(),
@@ -4867,15 +4992,6 @@ async function runReviewCli(input) {
4867
4992
  await attemptTelemetry(() => telemetry.shutdown(1e3));
4868
4993
  }
4869
4994
  }
4870
- const TARGET_LABELS = {
4871
- claude: "Claude Code",
4872
- codex: "Codex",
4873
- cursor: "Cursor",
4874
- opencode: "OpenCode",
4875
- pi: "Pi",
4876
- omp: "oh-my-pi",
4877
- copilot: "Copilot CLI"
4878
- };
4879
4995
  function parseTargets(targets) {
4880
4996
  if (targets.length === 0 || targets.includes("all")) return [...ALL_INSTALL_TARGETS];
4881
4997
  return [...new Set(targets.values().map((target) => target === "claude-code" ? "claude" : target).filter(isInstallTarget))];