@reticlehq/server 3.8.2 → 3.9.0

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 (224) hide show
  1. package/LICENSE +201 -104
  2. package/NOTICE +5 -0
  3. package/README.md +1 -1
  4. package/bin/launch.js +13 -0
  5. package/bin/package.json +3 -0
  6. package/bin/reticle.js +66 -8
  7. package/dist/chromium-launch-options.d.ts +25 -0
  8. package/dist/chromium-launch-options.js +21 -1
  9. package/dist/command/cli/adhoc-verdict.js +34 -7
  10. package/dist/command/cli/answers/gate-exit.d.ts +6 -2
  11. package/dist/command/cli/answers/gate-exit.js +12 -2
  12. package/dist/command/cli/ci-hint.d.ts +44 -0
  13. package/dist/command/cli/ci-hint.js +72 -0
  14. package/dist/command/cli/cli-doctor.d.ts +6 -1
  15. package/dist/command/cli/cli-doctor.js +45 -4
  16. package/dist/command/cli/cli-flow-commands.d.ts +7 -1
  17. package/dist/command/cli/cli-flow-commands.js +39 -17
  18. package/dist/command/cli/cli-kill.d.ts +21 -0
  19. package/dist/command/cli/cli-kill.js +46 -3
  20. package/dist/command/cli/cli-parse.d.ts +5 -2
  21. package/dist/command/cli/cli-parse.js +10 -5
  22. package/dist/command/cli/cli-usage.js +3 -2
  23. package/dist/command/cli/cli-verify.d.ts +6 -1
  24. package/dist/command/cli/cli-verify.js +32 -13
  25. package/dist/command/cli/doctor/browser/chromium-hint.d.ts +6 -1
  26. package/dist/command/cli/doctor/browser/chromium-hint.js +11 -5
  27. package/dist/command/cli/doctor/doctor-fix.d.ts +44 -0
  28. package/dist/command/cli/doctor/doctor-fix.js +89 -0
  29. package/dist/command/cli/doctor/doctor-launcher-line.d.ts +23 -0
  30. package/dist/command/cli/doctor/doctor-launcher-line.js +60 -0
  31. package/dist/command/cli/doctor/doctor-rows.d.ts +4 -0
  32. package/dist/command/cli/doctor/doctor-rows.js +4 -0
  33. package/dist/command/cli/doctor-repairs.d.ts +64 -0
  34. package/dist/command/cli/doctor-repairs.js +214 -0
  35. package/dist/command/cli/drive/drive-command.js +3 -7
  36. package/dist/command/cli/first-use-checks.d.ts +41 -0
  37. package/dist/command/cli/first-use-checks.js +127 -0
  38. package/dist/command/cli/first-verdict-node.d.ts +3 -0
  39. package/dist/command/cli/first-verdict-node.js +189 -0
  40. package/dist/command/cli/first-verdict.d.ts +126 -0
  41. package/dist/command/cli/first-verdict.js +267 -0
  42. package/dist/command/cli/launch/sdk-version-match.d.ts +3 -2
  43. package/dist/command/cli/launch/sdk-version-match.js +3 -2
  44. package/dist/command/cli/lifecycle/daemon-lifecycle.js +6 -5
  45. package/dist/command/cli/mcp-command.js +13 -4
  46. package/dist/command/cli/ports/port-holder.d.ts +2 -0
  47. package/dist/command/cli/ports/port-holder.js +11 -3
  48. package/dist/command/cli/setup-mcp-cli.d.ts +27 -1
  49. package/dist/command/cli/setup-mcp-cli.js +98 -23
  50. package/dist/command/cli/status/status-lines.d.ts +1 -0
  51. package/dist/command/cli/status/status-lines.js +50 -17
  52. package/dist/command/cli/status-fields.js +17 -3
  53. package/dist/command/cli/try-command.d.ts +7 -0
  54. package/dist/command/cli/try-command.js +11 -3
  55. package/dist/command/cli.js +70 -33
  56. package/dist/command/daemon/binding/daemon-status-probe.d.ts +3 -1
  57. package/dist/command/daemon/binding/daemon-status-probe.js +4 -2
  58. package/dist/command/daemon/daemon-argv.d.ts +9 -0
  59. package/dist/command/daemon/daemon-argv.js +11 -0
  60. package/dist/command/daemon/daemon.d.ts +10 -3
  61. package/dist/command/daemon/daemon.js +17 -6
  62. package/dist/command/daemon/lifetime/daemon-exit.d.ts +24 -0
  63. package/dist/command/daemon/lifetime/daemon-exit.js +39 -0
  64. package/dist/command/daemon/lifetime/death-cause.d.ts +15 -0
  65. package/dist/command/daemon/lifetime/death-cause.js +130 -0
  66. package/dist/command/daemon/lifetime/heartbeat.d.ts +7 -15
  67. package/dist/command/daemon/lifetime/heartbeat.js +4 -13
  68. package/dist/command/daemon/port-state.d.ts +14 -0
  69. package/dist/command/daemon/port-state.js +31 -0
  70. package/dist/command/setup/agent-configs.d.ts +2 -0
  71. package/dist/command/setup/agent-configs.js +42 -15
  72. package/dist/command/setup/agent-next-steps.d.ts +77 -0
  73. package/dist/command/setup/agent-next-steps.js +284 -0
  74. package/dist/command/setup/agent-writer.js +11 -6
  75. package/dist/command/setup/init/init-host.js +2 -0
  76. package/dist/command/setup/init/init-runtime.d.ts +6 -1
  77. package/dist/command/setup/init/init-runtime.js +57 -21
  78. package/dist/command/setup/mcp-launcher.d.ts +3 -0
  79. package/dist/command/setup/mcp-launcher.js +11 -0
  80. package/dist/command/setup/setup-command.js +30 -12
  81. package/dist/command/setup/setup-mcp.d.ts +8 -0
  82. package/dist/command/setup/setup-mcp.js +47 -20
  83. package/dist/command/setup/start-dev-server.d.ts +8 -0
  84. package/dist/command/setup/start-dev-server.js +43 -0
  85. package/dist/command/version/identity/server-version.d.ts +2 -0
  86. package/dist/command/version/identity/server-version.js +2 -0
  87. package/dist/command/version/version-skew.d.ts +7 -0
  88. package/dist/command/version/version-skew.js +13 -2
  89. package/dist/features/license/license.d.ts +11 -71
  90. package/dist/features/license/license.js +43 -136
  91. package/dist/features/visual/desktop-capture.d.ts +33 -0
  92. package/dist/features/visual/desktop-capture.js +94 -0
  93. package/dist/features/visual/step-shots.js +139 -35
  94. package/dist/features/visual/visual-tools.d.ts +0 -18
  95. package/dist/features/visual/visual-tools.js +2 -81
  96. package/dist/import-graph.js +5 -6
  97. package/dist/index.d.ts +3 -1
  98. package/dist/index.js +14 -5
  99. package/dist/judgement/runs/run-ledger.d.ts +7 -5
  100. package/dist/judgement/runs/run-ledger.js +98 -69
  101. package/dist/judgement/runs/verification-sync.d.ts +6 -3
  102. package/dist/judgement/runs/verification-sync.js +21 -10
  103. package/dist/language/flows/flow-store-for-session.js +7 -5
  104. package/dist/language/flows/recording/tape/recordings.d.ts +2 -13
  105. package/dist/language/flows/recording/tape/recordings.js +41 -44
  106. package/dist/language/flows/replay.d.ts +17 -7
  107. package/dist/language/flows/replay.js +95 -44
  108. package/dist/launch-chromium.d.ts +1 -0
  109. package/dist/launch-chromium.js +49 -12
  110. package/dist/memory/cloud/sync-screenshots.js +6 -1
  111. package/dist/portal/bridge/bridge.d.ts +13 -4
  112. package/dist/portal/bridge/bridge.js +10 -9
  113. package/dist/portal/input/network-mock.js +3 -2
  114. package/dist/portal/input/real-input.d.ts +15 -0
  115. package/dist/portal/input/real-input.js +59 -3
  116. package/dist/portal/input/viewport-tools.js +6 -3
  117. package/dist/portal/pool/browser-pool.d.ts +5 -1
  118. package/dist/portal/pool/browser-pool.js +32 -38
  119. package/dist/portal/pool/context-permissions.d.ts +20 -17
  120. package/dist/portal/pool/context-permissions.js +63 -1
  121. package/dist/portal/pool/csp-widen.d.ts +62 -0
  122. package/dist/portal/pool/csp-widen.js +132 -0
  123. package/dist/portal/pool/lease-page-error.d.ts +29 -0
  124. package/dist/portal/pool/lease-page-error.js +18 -0
  125. package/dist/portal/pool/page-error-journal.d.ts +57 -0
  126. package/dist/portal/pool/page-error-journal.js +181 -0
  127. package/dist/portal/pool/playwright-launcher.js +21 -8
  128. package/dist/portal/pool/pool-contract.d.ts +22 -0
  129. package/dist/portal/pool/pool-lease.d.ts +35 -2
  130. package/dist/portal/pool/pool-lease.js +23 -0
  131. package/dist/portal/pool/zero-install.js +69 -1
  132. package/dist/portal/session/dev-server/dev-command.d.ts +8 -0
  133. package/dist/portal/session/dev-server/dev-command.js +56 -8
  134. package/dist/portal/session/no-session-diagnosis.js +10 -3
  135. package/dist/portal/session/no-session-next-action.d.ts +12 -0
  136. package/dist/portal/session/no-session-next-action.js +44 -4
  137. package/dist/portal/session/no-session-watch.js +3 -1
  138. package/dist/portal/session/page-commands/undeclared-command.d.ts +9 -3
  139. package/dist/portal/session/page-commands/undeclared-command.js +16 -7
  140. package/dist/portal/session/recorded-gaps.d.ts +1 -1
  141. package/dist/portal/session/recorded-gaps.js +5 -2
  142. package/dist/portal/session/session-manager.js +40 -21
  143. package/dist/portal/session/session.js +4 -3
  144. package/dist/portal/session/zero-install-first-call.d.ts +20 -1
  145. package/dist/portal/session/zero-install-first-call.js +34 -2
  146. package/dist/start-wiring.d.ts +29 -2
  147. package/dist/start-wiring.js +73 -9
  148. package/dist/surface/mcp/mcp-proxy.js +10 -2
  149. package/dist/surface/mcp/mcp.js +5 -3
  150. package/dist/surface/mcp/peer/agents-connected.d.ts +6 -0
  151. package/dist/surface/mcp/peer/agents-connected.js +58 -0
  152. package/dist/surface/mcp/proxy/proxy-daemon-probe.js +17 -5
  153. package/dist/surface/tools/act/act-preflight.js +37 -17
  154. package/dist/surface/tools/act/act-target.js +3 -1
  155. package/dist/surface/tools/act/after-match.d.ts +7 -0
  156. package/dist/surface/tools/act/after-match.js +33 -0
  157. package/dist/surface/tools/act/resolve-target.d.ts +2 -0
  158. package/dist/surface/tools/act/resolve-target.js +35 -8
  159. package/dist/surface/tools/act/sequence-grade.d.ts +3 -1
  160. package/dist/surface/tools/act/sequence-grade.js +8 -1
  161. package/dist/surface/tools/act-sequence-tool.js +3 -0
  162. package/dist/surface/tools/act-tools.js +10 -10
  163. package/dist/surface/tools/assert/assert-verdict.js +28 -1
  164. package/dist/surface/tools/browser-tools.js +75 -7
  165. package/dist/surface/tools/dynamic-tools.js +2 -1
  166. package/dist/surface/tools/empty-session-list.d.ts +2 -0
  167. package/dist/surface/tools/empty-session-list.js +4 -2
  168. package/dist/surface/tools/error-recovery.js +43 -22
  169. package/dist/surface/tools/invoke-tool.js +27 -20
  170. package/dist/surface/tools/lease-hint.d.ts +7 -0
  171. package/dist/surface/tools/lease-hint.js +10 -3
  172. package/dist/surface/tools/lease-readiness.d.ts +21 -0
  173. package/dist/surface/tools/lease-readiness.js +47 -0
  174. package/dist/surface/tools/lease-seed.d.ts +2 -0
  175. package/dist/surface/tools/lease-seed.js +2 -1
  176. package/dist/surface/tools/lease-tools.js +43 -15
  177. package/dist/surface/tools/repeat-hint.d.ts +40 -0
  178. package/dist/surface/tools/repeat-hint.js +171 -0
  179. package/dist/surface/tools/run-report.d.ts +2 -2
  180. package/dist/surface/tools/run-report.js +27 -15
  181. package/dist/surface/tools/tool-kit.d.ts +12 -0
  182. package/dist/surface/tools/tool-kit.js +7 -0
  183. package/dist/surface/tools/tools.js +26 -13
  184. package/dist/telemetry/argument-shape.js +16 -7
  185. package/dist/telemetry/cli-telemetry.d.ts +10 -1
  186. package/dist/telemetry/cli-telemetry.js +15 -0
  187. package/dist/telemetry/init-telemetry.d.ts +2 -1
  188. package/dist/telemetry/init-telemetry.js +47 -2
  189. package/dist/telemetry/install-trace.js +23 -4
  190. package/dist/telemetry/onboarding-funnel.d.ts +19 -2
  191. package/dist/telemetry/onboarding-funnel.js +25 -3
  192. package/dist/telemetry/proxy-telemetry.d.ts +2 -1
  193. package/dist/telemetry/proxy-telemetry.js +2 -2
  194. package/dist/telemetry/session-metrics.d.ts +1 -1
  195. package/dist/telemetry/session-metrics.js +67 -27
  196. package/dist/telemetry/telemetry.d.ts +3 -1
  197. package/dist/telemetry/telemetry.js +60 -28
  198. package/dist/telemetry/tool-refused.d.ts +2 -2
  199. package/dist/telemetry/tool-refused.js +9 -2
  200. package/docs/architecture.md +3 -6
  201. package/docs/cli/doctor.mdx +41 -2
  202. package/docs/cli/help.mdx +4 -2
  203. package/docs/cli/license.mdx +4 -5
  204. package/docs/cli/setup.mdx +13 -2
  205. package/docs/cli/status.mdx +4 -0
  206. package/docs/cli.mdx +1 -1
  207. package/docs/deploy-checks.md +25 -0
  208. package/docs/desktop.mdx +1 -1
  209. package/docs/enterprise.md +15 -20
  210. package/docs/environment.mdx +1 -0
  211. package/docs/first-drive.md +1 -1
  212. package/docs/install-manual.mdx +16 -1
  213. package/docs/packages/engine.mdx +1 -1
  214. package/docs/packages/server.mdx +1 -1
  215. package/docs/packages/tauri.mdx +1 -1
  216. package/docs/packages/test.mdx +1 -1
  217. package/docs/packages.mdx +1 -1
  218. package/docs/platform-integration.md +1 -1
  219. package/docs/skill.md +1 -1
  220. package/docs/telemetry.md +4 -2
  221. package/package.json +13 -9
  222. package/LICENSE-ENTERPRISE +0 -37
  223. package/dist/features/ee/audit-log.d.ts +0 -14
  224. package/dist/features/ee/audit-log.js +0 -11
@@ -86,8 +86,9 @@ async function openConnection(ports) {
86
86
  /** Orchestration: boot → wait for a session → replay flows → verdict → exit code. Browser-free. */
87
87
  export async function runVerify(args, outer) {
88
88
  const conn = await openConnection(outer);
89
- if (conn === undefined)
89
+ if (conn === undefined) {
90
90
  return;
91
+ }
91
92
  /*
92
93
  * The exit is held until the connection has closed. The live exit is process.exit, and close()
93
94
  * runs the last cloud sync cycle that sends this run's flows and screenshots: exiting first skipped
@@ -132,8 +133,9 @@ export async function runVerify(args, outer) {
132
133
  */
133
134
  if (0 === names.length && (true === args.explore || args.persona !== undefined)) {
134
135
  const explored = await explore(conn, args, ports);
135
- if (!explored)
136
+ if (!explored) {
136
137
  return;
138
+ }
137
139
  names = await conn.listFlows();
138
140
  }
139
141
  if (0 === names.length) {
@@ -192,8 +194,9 @@ export async function runVerify(args, outer) {
192
194
  finally {
193
195
  await conn.close().catch(() => undefined);
194
196
  // In the finally, so the early refusals that `return` from the try exit too.
195
- if (exitCode !== undefined)
197
+ if (exitCode !== undefined) {
196
198
  outer.exit(exitCode);
199
+ }
197
200
  }
198
201
  }
199
202
  /**
@@ -269,8 +272,9 @@ function delay(ms) {
269
272
  async function waitForSession(sessions, timeoutMs, now) {
270
273
  const deadline = now() + timeoutMs;
271
274
  while (now() < deadline) {
272
- if (sessions.count() > 0)
275
+ if (sessions.count() > 0) {
273
276
  return true;
277
+ }
274
278
  await delay(SESSION_POLL_MS);
275
279
  }
276
280
  return sessions.count() > 0;
@@ -298,8 +302,9 @@ export function buildVerifyDeps(running, reticleRoot, now) {
298
302
  // RETICLE_API_KEY was told to run `reticle connect` when asking the Harness to drive.
299
303
  linkedCloud: linkedCloudPort(fs, reticleRoot, homedir(), process.env),
300
304
  };
301
- if (running.realInput !== undefined)
305
+ if (running.realInput !== undefined) {
302
306
  deps.realInput = running.realInput;
307
+ }
303
308
  return deps;
304
309
  }
305
310
  /** Split a drive URL into its origin + whether it's loopback — decides token/injection pairing. */
@@ -501,8 +506,9 @@ export function routeVerify(args) {
501
506
  ? { route: VerifyRoute.ADHOC }
502
507
  : { route: VerifyRoute.REFUSE, message: expectNeedsDaemonMessage(args.port, args.presence) };
503
508
  }
504
- if (args.presence !== PortPresence.DAEMON)
509
+ if (args.presence !== PortPresence.DAEMON) {
505
510
  return { route: VerifyRoute.FLOWS };
511
+ }
506
512
  if (true === args.explore && true !== args.wantsOwnBrowser && true !== args.hasSavedFlows) {
507
513
  return { route: VerifyRoute.ADHOC_EXPLORE };
508
514
  }
@@ -512,7 +518,12 @@ export function routeVerify(args) {
512
518
  ? { route: VerifyRoute.REFUSE, message: portBusyMessage(args.port) }
513
519
  : { route: VerifyRoute.ADHOC_SUITE };
514
520
  }
515
- export function handleVerify(parsed) {
521
+ export function handleVerify(parsed,
522
+ /**
523
+ * Called once, after the last verdict line and before the exit. `cli.ts` passes the CI line:
524
+ * `telemetry` already reaches this directory, so this directory does not reach back.
525
+ */
526
+ beforeExit = () => undefined) {
516
527
  const now = () => Date.now();
517
528
  /*
518
529
  * `--expect-file` is `--expect` with the shell taken out of the loop.
@@ -559,7 +570,10 @@ export function handleVerify(parsed) {
559
570
  }),
560
571
  out: (line) => process.stdout.write(`${line}\n`),
561
572
  fail: (line) => process.stderr.write(`${line}\n`),
562
- exit: (code) => process.exit(code),
573
+ exit: (code) => {
574
+ beforeExit();
575
+ process.exit(code);
576
+ },
563
577
  cloud: linkedRunsCloudPort(createNodeFileSystem(), reticleRoot, homedir(), process.env),
564
578
  dashboardUrl: () => readDashboardUrl(reticleRoot),
565
579
  writeResults: (path, text) => writeFile(path, text, 'utf8'),
@@ -569,8 +583,9 @@ export function handleVerify(parsed) {
569
583
  // this used to reach the user as a raw node stack rather than as an answer.
570
584
  void (async () => {
571
585
  const keyNote = (await resolveProjectCloud(createNodeFileSystem(), reticleRoot, homedir(), process.env)).note;
572
- if (keyNote !== undefined)
586
+ if (keyNote !== undefined) {
573
587
  process.stderr.write(`${keyNote}\n`);
588
+ }
574
589
  const port = parsed.port ?? RETICLE_DEFAULT_PORT;
575
590
  // A lease opened for this verdict writes its runs into this folder's project, not `unmatched/`.
576
591
  const root = callerArtifactRoot(process.cwd());
@@ -613,8 +628,9 @@ export function handleVerify(parsed) {
613
628
  ...(parsed.sessionId === undefined ? {} : { sessionId: parsed.sessionId }),
614
629
  ...((t) => (t === undefined || 0 === t.length ? {} : { token: t }))(readOrCreatePairingTokenSync(defaultPairingTokenDir())),
615
630
  });
616
- for (const line of verdict.lines)
631
+ for (const line of verdict.lines) {
617
632
  ports.out(line);
633
+ }
618
634
  ports.exit(verdict.code);
619
635
  return;
620
636
  }
@@ -631,8 +647,9 @@ export function handleVerify(parsed) {
631
647
  ...(parsed.sessionId === undefined ? {} : { sessionId: parsed.sessionId }),
632
648
  ...((t) => (t === undefined || 0 === t.length ? {} : { token: t }))(readOrCreatePairingTokenSync(defaultPairingTokenDir())),
633
649
  });
634
- for (const line of suite.lines)
650
+ for (const line of suite.lines) {
635
651
  ports.out(line);
652
+ }
636
653
  ports.exit(suite.code);
637
654
  return;
638
655
  }
@@ -647,15 +664,17 @@ export function handleVerify(parsed) {
647
664
  ...callerRoot,
648
665
  ...((t) => (t === undefined || 0 === t.length ? {} : { token: t }))(readOrCreatePairingTokenSync(defaultPairingTokenDir())),
649
666
  });
650
- for (const line of explored.lines)
667
+ for (const line of explored.lines) {
651
668
  ports.out(line);
669
+ }
652
670
  // The drive runs INSIDE the daemon, so the key has to be in ITS environment. A reader who
653
671
  // exported it in this shell and was told to link has been told to do the thing they just
654
672
  // did; say where the key is actually missing.
655
673
  if (0 !== explored.code && harnessAvailable(process.env)) {
656
674
  const refusedForKey = explored.lines.some((line) => line.includes(MSG_NO_HARNESS_KEY));
657
- if (refusedForKey)
675
+ if (refusedForKey) {
658
676
  ports.fail(daemonLacksKeyMessage(port));
677
+ }
659
678
  }
660
679
  ports.exit(explored.code);
661
680
  return;
@@ -48,6 +48,11 @@ export interface ChromiumProbe {
48
48
  * `Microsoft Edge`) — see launch-chromium. Present only when `exists` is false and one was found.
49
49
  */
50
50
  fallback?: string | undefined;
51
+ /**
52
+ * Where Reticle looked for an installed Chrome or Edge when neither Playwright's build nor a
53
+ * fallback browser was found.
54
+ */
55
+ fallbackPaths?: readonly string[] | undefined;
51
56
  /** `executablePath` came from RETICLE_CHROMIUM_PATH, so the bundled build is not in play at all. */
52
57
  configured?: boolean | undefined;
53
58
  /** Why that configured executable cannot be launched. Present only when `exists` is false. */
@@ -118,7 +123,7 @@ export declare function parseChromiumRevision(executablePath: string): {
118
123
  * `executablePath()` resolves the revision the daemon will actually launch AND the root it will look
119
124
  * in, which is why the probe never reads `PLAYWRIGHT_BROWSERS_PATH` itself.
120
125
  */
121
- export declare function probeChromium(): Promise<ChromiumProbe>;
126
+ export declare function probeChromium(exists?: (path: string) => boolean): Promise<ChromiumProbe>;
122
127
  /** The bundled playwright's version, or undefined if it cannot be read (the hint then goes unpinned). */
123
128
  export declare function bundledPlaywrightVersion(): string | undefined;
124
129
  /**
@@ -173,7 +173,7 @@ function installedRevisions(root) {
173
173
  * `executablePath()` resolves the revision the daemon will actually launch AND the root it will look
174
174
  * in, which is why the probe never reads `PLAYWRIGHT_BROWSERS_PATH` itself.
175
175
  */
176
- export async function probeChromium() {
176
+ export async function probeChromium(exists = existsSync) {
177
177
  const searched = playwrightResolutionRoots();
178
178
  try {
179
179
  const { chromium } = await import('playwright');
@@ -181,14 +181,14 @@ export async function probeChromium() {
181
181
  const parsed = parseChromiumRevision(executablePath);
182
182
  return {
183
183
  executablePath,
184
- exists: existsSync(executablePath),
184
+ exists: exists(executablePath),
185
185
  playwrightVersion: bundledPlaywrightVersion(),
186
186
  ...(null === parsed
187
187
  ? {}
188
188
  : {
189
189
  wantedRevision: parsed.revision,
190
190
  browsersRoot: parsed.root,
191
- installedRevisions: installedRevisions(parsed.root),
191
+ installedRevisions: exists === existsSync ? installedRevisions(parsed.root) : [],
192
192
  }),
193
193
  };
194
194
  }
@@ -248,6 +248,9 @@ export function bundledPlaywrightVersion() {
248
248
  export function chromiumPreflightRefusal(probe) {
249
249
  if (probe.exists)
250
250
  return undefined;
251
+ if (undefined !== probe.fallback) {
252
+ return undefined;
253
+ }
251
254
  if (true === probe.configured && probe.executablePath !== undefined) {
252
255
  return configuredChromiumRefusal(probe.executablePath, probe.problem ?? ChromiumPathProblem.MISSING);
253
256
  }
@@ -281,16 +284,19 @@ export function chromiumHint(probe) {
281
284
  const where = 0 === searched.length ? '' : ` — looked in ${searched.join(', ')}`;
282
285
  return `✗ the playwright package is not installed${where}; run: ${command}`;
283
286
  }
287
+ const fallbackPaths = probe.fallbackPaths ?? [];
284
288
  const present = probe.installedRevisions ?? [];
285
289
  if (present.length > 0 && probe.wantedRevision !== undefined) {
286
290
  // The reported machine. Every fact here is one the reader could not otherwise get: what is
287
291
  // wanted, what is there, and where "there" is. `npx playwright install chromium` unpinned adds
288
292
  // a build to this list and changes nothing, which is what happened.
293
+ const fallbackWhere = 0 === fallbackPaths.length ? '' : `; also looked for ${fallbackPaths.join(', ')}`;
289
294
  return (`✗ wrong revision — the bundled playwright wants ${probe.wantedRevision}; ` +
290
- `${probe.browsersRoot ?? ''} holds ${present.join(', ')}. run: ${command}`);
295
+ `${probe.browsersRoot ?? ''} holds ${present.join(', ')}${fallbackWhere}. run: ${command}`);
291
296
  }
292
297
  // Naming the path is what turns "missing" from a verdict into evidence: a reader with browsers on
293
298
  // disk can see immediately whether this is a missing install or a lookup pointed somewhere else
294
299
  // (PLAYWRIGHT_BROWSERS_PATH, a different browsers root, a revision bump).
295
- return `✗ missing — looked for ${probe.executablePath}; run: ${command}`;
300
+ const lookedFor = [probe.executablePath, ...fallbackPaths].join(', ');
301
+ return `✗ missing — looked for ${lookedFor}; run: ${command}`;
296
302
  }
@@ -0,0 +1,44 @@
1
+ /**
2
+ * `reticle doctor --fix`: repair a half-done install, then say what still needs a person.
3
+ *
4
+ * A repair changes only Reticle's own state: its pid file, its own stuck daemon, its own Chromium,
5
+ * and its entry in an agent's MCP config. A row that needs a change to the user's app, a process
6
+ * that Reticle cannot prove is its daemon, or an action only the user can do, is never changed
7
+ * here. Its row already names the step, and this prints that step.
8
+ *
9
+ * Each repair checks again before it acts, so a second run finds nothing to do and changes nothing.
10
+ */
11
+ import { DoctorRow } from './doctor-rows.js';
12
+ type DoctorRowLabel = (typeof DoctorRow)[keyof typeof DoctorRow];
13
+ /** One safe repair. `needed` reads the machine; `apply` changes only Reticle's own state. */
14
+ export interface DoctorRepair {
15
+ /** The doctor row this repair makes green. */
16
+ readonly row: DoctorRowLabel;
17
+ /** What the repair is about, in a few words, so two repairs on one row read apart. */
18
+ readonly what: string;
19
+ /** The manual step, printed when the repair is not possible or does not take. */
20
+ readonly step: string;
21
+ needed(): Promise<boolean>;
22
+ apply(): Promise<void>;
23
+ }
24
+ /** What `--fix` says about each repair. */
25
+ export declare const FixOutcome: {
26
+ readonly FIXED: "fixed";
27
+ readonly NOTHING: "nothing to do";
28
+ readonly STILL_NEEDS: "still needs";
29
+ };
30
+ /** The last line of a plain `doctor` run, when `--fix` can repair `count` rows. */
31
+ export declare function fixFooter(count: number): string | undefined;
32
+ /** How many repairs have work to do now. */
33
+ export declare function countNeeded(repairs: readonly DoctorRepair[]): Promise<number>;
34
+ /**
35
+ * Run doctor, apply the needed repairs in the given order, run doctor again, and print one line for
36
+ * each repair and one for each row that still fails. True when no row fails after the repairs.
37
+ */
38
+ export declare function runDoctorFix(deps: {
39
+ /** Runs doctor and returns its rows. Prints them only when `show` is true. */
40
+ doctor: (show: boolean) => Promise<string[]>;
41
+ repairs: readonly DoctorRepair[];
42
+ print: (line: string) => void;
43
+ }): Promise<boolean>;
44
+ export {};
@@ -0,0 +1,89 @@
1
+ /**
2
+ * `reticle doctor --fix`: repair a half-done install, then say what still needs a person.
3
+ *
4
+ * A repair changes only Reticle's own state: its pid file, its own stuck daemon, its own Chromium,
5
+ * and its entry in an agent's MCP config. A row that needs a change to the user's app, a process
6
+ * that Reticle cannot prove is its daemon, or an action only the user can do, is never changed
7
+ * here. Its row already names the step, and this prints that step.
8
+ *
9
+ * Each repair checks again before it acts, so a second run finds nothing to do and changes nothing.
10
+ */
11
+ import { DoctorRow, doctorRow, LABEL_COLUMN } from './doctor-rows.js';
12
+ /** What `--fix` says about each repair. */
13
+ export const FixOutcome = {
14
+ FIXED: 'fixed',
15
+ NOTHING: 'nothing to do',
16
+ STILL_NEEDS: 'still needs',
17
+ };
18
+ /** The mark doctor puts on a row that fails. */
19
+ const FAIL_MARK = '✗';
20
+ const FIX_HEADING = 'reticle doctor --fix';
21
+ /** The last line of a plain `doctor` run, when `--fix` can repair `count` rows. */
22
+ export function fixFooter(count) {
23
+ if (0 === count) {
24
+ return undefined;
25
+ }
26
+ return `Run \`reticle doctor --fix\` to repair ${String(count)} of these.`;
27
+ }
28
+ /** How many repairs have work to do now. */
29
+ export async function countNeeded(repairs) {
30
+ let count = 0;
31
+ for (const repair of repairs) {
32
+ if (await repair.needed()) {
33
+ count++;
34
+ }
35
+ }
36
+ return count;
37
+ }
38
+ /** The row label at the head of a doctor line, or undefined for a line that is not a row. */
39
+ function rowLabelOf(line) {
40
+ const label = line.slice(0, LABEL_COLUMN + 2).trim();
41
+ return 0 === label.length ? undefined : label;
42
+ }
43
+ async function applyOne(repair) {
44
+ if (!(await repair.needed())) {
45
+ return FixOutcome.NOTHING;
46
+ }
47
+ try {
48
+ await repair.apply();
49
+ }
50
+ catch {
51
+ // The check below decides. A repair that threw and a repair that did nothing say the same thing.
52
+ }
53
+ if (await repair.needed()) {
54
+ return `${FixOutcome.STILL_NEEDS}: ${repair.step}`;
55
+ }
56
+ return FixOutcome.FIXED;
57
+ }
58
+ /**
59
+ * Run doctor, apply the needed repairs in the given order, run doctor again, and print one line for
60
+ * each repair and one for each row that still fails. True when no row fails after the repairs.
61
+ */
62
+ export async function runDoctorFix(deps) {
63
+ await deps.doctor(true);
64
+ const results = [];
65
+ const stillFailing = new Set();
66
+ for (const repair of deps.repairs) {
67
+ const outcome = await applyOne(repair);
68
+ if (outcome.startsWith(FixOutcome.STILL_NEEDS)) {
69
+ stillFailing.add(repair.row);
70
+ }
71
+ results.push(doctorRow(repair.row, `${repair.what}: ${outcome}`));
72
+ }
73
+ const after = await deps.doctor(false);
74
+ const failures = after.filter((line) => line.includes(FAIL_MARK));
75
+ deps.print('');
76
+ deps.print(FIX_HEADING);
77
+ for (const line of results) {
78
+ deps.print(line);
79
+ }
80
+ for (const line of failures) {
81
+ // A repair that did not take already printed its step for this row.
82
+ const label = rowLabelOf(line);
83
+ if (label !== undefined && stillFailing.has(label)) {
84
+ continue;
85
+ }
86
+ deps.print(` ${FixOutcome.STILL_NEEDS}: ${line.trim()}`);
87
+ }
88
+ return 0 === failures.length;
89
+ }
@@ -0,0 +1,23 @@
1
+ /** One agent's `reticle` entry, as its config holds it. */
2
+ export interface RegisteredEntry {
3
+ readonly client: string;
4
+ readonly entry: unknown;
5
+ }
6
+ interface ConfigIo {
7
+ readFile(path: string): string | null;
8
+ exists(path: string): boolean;
9
+ homeDir(): string;
10
+ }
11
+ /**
12
+ * The `reticle` entry of each agent config here: Claude Code's user, local and project scopes, and
13
+ * every file-backed client that `init` registers with.
14
+ */
15
+ export declare function registeredEntries(io: ConfigIo, cwd: string): RegisteredEntry[];
16
+ /**
17
+ * The row text, or undefined when every registered launcher exists and no entry is stale.
18
+ *
19
+ * @param floatingNote `claudeStaleEntryNote`, when Claude Code's user-scope entry is the floating
20
+ * npx form. `setup mcp` keeps that entry, so the row names the remove-then-add step.
21
+ */
22
+ export declare function launcherLine(entries: readonly RegisteredEntry[], exists: (path: string) => boolean, floatingNote?: string): string | undefined;
23
+ export {};
@@ -0,0 +1,60 @@
1
+ /**
2
+ * The `launcher` row of `reticle doctor`: an agent config starts Reticle from an installed copy
3
+ * (`node <path> mcp`), and that file is gone.
4
+ *
5
+ * `init` registers the installed bin when there is one, so a sandbox boot does not download the
6
+ * package again. When the package is removed later, the agent shows the server as failed and says
7
+ * no more. This row names the path and the command that writes a working entry. It prints only
8
+ * when a launcher is missing, like `version`.
9
+ */
10
+ import { McpClient, RETICLE_NPM_PACKAGE, claudeReticleEntries, clientSpec, detectMcpClients, isReticleLauncher, } from '@reticlehq/init';
11
+ import { MCP_SERVER_NAME } from '@reticlehq/core';
12
+ const SETUP_MCP = `npx ${RETICLE_NPM_PACKAGE} setup mcp`;
13
+ /** `setup mcp` keeps an existing Claude Code entry, so that entry goes first. */
14
+ const CLAUDE_REMOVE = `claude mcp remove ${MCP_SERVER_NAME} -s user`;
15
+ const record = (value) => 'object' === typeof value && null !== value ? value : {};
16
+ /** Every string in `command` and `args`, in both the `{ command, args }` and the array shape. */
17
+ function strings(entry) {
18
+ const { command, args } = record(entry);
19
+ return [command, args].flat().filter((v) => 'string' === typeof v);
20
+ }
21
+ function parsed(text) {
22
+ try {
23
+ return JSON.parse(text ?? '');
24
+ }
25
+ catch {
26
+ return undefined;
27
+ }
28
+ }
29
+ /**
30
+ * The `reticle` entry of each agent config here: Claude Code's user, local and project scopes, and
31
+ * every file-backed client that `init` registers with.
32
+ */
33
+ export function registeredEntries(io, cwd) {
34
+ const claude = claudeReticleEntries(io, cwd).map((entry) => ({
35
+ client: McpClient.CLAUDE_CODE,
36
+ entry,
37
+ }));
38
+ const others = detectMcpClients(io).map((c) => ({
39
+ client: c.id,
40
+ entry: record(record(parsed(c.existing))[clientSpec(c.id).serversKey])[MCP_SERVER_NAME],
41
+ }));
42
+ return [...claude, ...others].filter((e) => e.entry !== undefined);
43
+ }
44
+ /**
45
+ * The row text, or undefined when every registered launcher exists and no entry is stale.
46
+ *
47
+ * @param floatingNote `claudeStaleEntryNote`, when Claude Code's user-scope entry is the floating
48
+ * npx form. `setup mcp` keeps that entry, so the row names the remove-then-add step.
49
+ */
50
+ export function launcherLine(entries, exists, floatingNote) {
51
+ for (const { client, entry } of entries) {
52
+ const missing = strings(entry).find((s) => isReticleLauncher(s) && !exists(s));
53
+ if (missing === undefined) {
54
+ continue;
55
+ }
56
+ const fix = McpClient.CLAUDE_CODE === client ? `${CLAUDE_REMOVE}, then ${SETUP_MCP}` : SETUP_MCP;
57
+ return `✗ ${client} starts Reticle from ${missing}, and that file does not exist. Run: ${fix}`;
58
+ }
59
+ return floatingNote === undefined ? undefined : `✗ ${McpClient.CLAUDE_CODE}: ${floatingNote}`;
60
+ }
@@ -40,6 +40,8 @@ export declare const DoctorRow: {
40
40
  * verification." Every row in this list checks a COMPONENT; none checked a LINK.
41
41
  */
42
42
  readonly AGENT_LINK: "agent link";
43
+ /** An agent config starts Reticle from an installed copy that is gone. Only then. */
44
+ readonly LAUNCHER: "launcher";
43
45
  /** Is the APP in this folder wired, not just the tools. Printed by `doctor-project-line.ts`. */
44
46
  readonly PROJECT: "project";
45
47
  /**
@@ -68,6 +70,8 @@ export declare const DoctorRow: {
68
70
  readonly PROMPTS: "prompts";
69
71
  /** The daemon's background sync for this project: on or off, why, and the last push. */
70
72
  readonly SYNC: "sync";
73
+ /** Which coding agents found on this machine have Reticle's MCP server registered. */
74
+ readonly MCP: "mcp";
71
75
  };
72
76
  type DoctorRowLabel = (typeof DoctorRow)[keyof typeof DoctorRow];
73
77
  export declare const DOCTOR_ROW_LABELS: readonly DoctorRowLabel[];
@@ -40,6 +40,8 @@ export const DoctorRow = {
40
40
  * verification." Every row in this list checks a COMPONENT; none checked a LINK.
41
41
  */
42
42
  AGENT_LINK: 'agent link',
43
+ /** An agent config starts Reticle from an installed copy that is gone. Only then. */
44
+ LAUNCHER: 'launcher',
43
45
  /** Is the APP in this folder wired, not just the tools. Printed by `doctor-project-line.ts`. */
44
46
  PROJECT: 'project',
45
47
  /**
@@ -68,6 +70,8 @@ export const DoctorRow = {
68
70
  PROMPTS: 'prompts',
69
71
  /** The daemon's background sync for this project: on or off, why, and the last push. */
70
72
  SYNC: 'sync',
73
+ /** Which coding agents found on this machine have Reticle's MCP server registered. */
74
+ MCP: 'mcp',
71
75
  };
72
76
  export const DOCTOR_ROW_LABELS = Object.values(DoctorRow);
73
77
  /** Indent every row carries, before the label. */
@@ -0,0 +1,64 @@
1
+ import { PortPresence } from '../daemon/binding/port-presence.js';
2
+ import { type StepReporter } from './setup-mcp-cli.js';
3
+ import { type PortHolder } from './ports/port-holder.js';
4
+ import { type ChromiumProbe } from './doctor/browser/chromium-hint.js';
5
+ import type { DoctorRepair } from './doctor/doctor-fix.js';
6
+ /** A pid file whose process is gone. `removePid` itself never removes the file of a live process. */
7
+ export declare function stalePidRepair(deps: {
8
+ readPid: () => number | null;
9
+ isAlive: (pid: number) => boolean;
10
+ removePid: () => void;
11
+ }): DoctorRepair;
12
+ /**
13
+ * A Reticle daemon that holds the port and does not answer. Only a holder whose command line is a
14
+ * Reticle daemon's qualifies; `stopWedgedDaemon` checks that again, after a grace period, before it
15
+ * sends a signal. A stranger and the agent's own `reticle mcp` proxy are never stopped.
16
+ */
17
+ export declare function wedgedDaemonRepair(port: number, deps: {
18
+ presence: () => Promise<PortPresence>;
19
+ holder: () => PortHolder | null;
20
+ stop: () => Promise<boolean>;
21
+ }): DoctorRepair;
22
+ /**
23
+ * No browser that a launch can use. Only when Playwright's own build is missing or at the wrong
24
+ * revision and no installed Chrome or Edge stands in. A RETICLE_CHROMIUM_PATH the user set is never
25
+ * replaced, and an absent playwright package is not a download this can fix.
26
+ */
27
+ export declare function chromiumRepair(deps: {
28
+ probe: () => Promise<ChromiumProbe>;
29
+ install: () => Promise<unknown>;
30
+ /** The bundled playwright's version, which pins the manual install command. */
31
+ playwrightVersion: string | undefined;
32
+ }): DoctorRepair;
33
+ /** Agents found on this machine that do not have Reticle's MCP server registered. */
34
+ export declare function mcpRepair(deps: {
35
+ gaps: () => readonly string[];
36
+ register: () => void;
37
+ }): DoctorRepair;
38
+ /**
39
+ * An agent config starts Reticle from an installed copy that no longer exists. `setup mcp`
40
+ * replaces Reticle's own entry in the JSON clients. It keeps an existing Claude Code entry, so for
41
+ * Claude Code the row still fails after the repair, and the step is the row's remove-then-add.
42
+ */
43
+ export declare function launcherRepair(deps: {
44
+ /** The launcher row, or undefined when every registered launcher exists. */
45
+ line: () => string | undefined;
46
+ register: () => void;
47
+ }): DoctorRepair;
48
+ /** The launcher row for the agent configs of this machine and folder. */
49
+ export declare function readLauncherLine(cwd: string): string | undefined;
50
+ /** Which agents found here have Reticle, and which do not. */
51
+ export interface McpRegistration {
52
+ readonly missing: readonly string[];
53
+ readonly present: readonly string[];
54
+ }
55
+ /** The `mcp` doctor row. */
56
+ export declare function mcpRegistrationRow(registration: McpRegistration): string;
57
+ /**
58
+ * Ask `setupMcp` what it would do, and do none of it. Config writes and registration commands are
59
+ * recorded, not run; only the two `--version` probes run, because they say whether an agent is here.
60
+ * No telemetry: this is a check, not a registration.
61
+ */
62
+ export declare function readMcpRegistration(): McpRegistration;
63
+ /** The repairs for this machine and port, in the order `--fix` applies them. */
64
+ export declare function machineRepairs(port: number, reportStep: StepReporter): DoctorRepair[];