@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
@@ -8,6 +8,7 @@
8
8
  import { chmodSync, existsSync, readFileSync, writeFileSync } from 'node:fs';
9
9
  import { homedir } from 'node:os';
10
10
  import { join } from 'node:path';
11
+ import { mcpLauncher } from './mcp-launcher.js';
11
12
  import { planAgentConfigs, platformKey } from './agent-configs.js';
12
13
  import { AppShape, describeShape, readShape } from './desktop-shape.js';
13
14
  import { devConfigText, expectedDevPort, loneLoopbackOnDevPort, pinnedDevPort, } from './probe/dev-port.js';
@@ -73,8 +74,9 @@ export function installCrashGuard() {
73
74
  function hasElectronDependency(dir) {
74
75
  try {
75
76
  const pkg = JSON.parse(readFileSync(join(dir, 'package.json'), 'utf8'));
76
- if ('object' !== typeof pkg || null === pkg)
77
+ if ('object' !== typeof pkg || null === pkg) {
77
78
  return false;
79
+ }
78
80
  const p = pkg;
79
81
  return undefined !== { ...p.dependencies, ...p.devDependencies }['electron'];
80
82
  }
@@ -95,7 +97,13 @@ const agentWord = (n) => (1 === n ? 'agent' : 'agents');
95
97
  export function registerOtherAgents(print) {
96
98
  const platform = platformKey(process.platform);
97
99
  const home = homedir();
98
- const results = applyAgentPlan(planAgentConfigs({ home, platform, exists: agentIo.exists, readFile: agentIo.readFile }), agentIo);
100
+ const results = applyAgentPlan(planAgentConfigs({
101
+ home,
102
+ platform,
103
+ exists: agentIo.exists,
104
+ readFile: agentIo.readFile,
105
+ launcher: mcpLauncher(),
106
+ }), agentIo);
99
107
  const wrote = results.filter((r) => 'created' === r.action || 'merged' === r.action);
100
108
  if (0 < wrote.length) {
101
109
  print(` ✓ ${String(wrote.length)} more ${agentWord(wrote.length)}: ${wrote.map((r) => r.name).join(', ')}`);
@@ -115,8 +123,9 @@ export function registerOtherAgents(print) {
115
123
  }
116
124
  rememberSaid(stateHome, fresh.map((n) => noticeKey(n)));
117
125
  const skills = applyAgentSkills(agentIo, { home, platform });
118
- if (0 < skills.length)
126
+ if (0 < skills.length) {
119
127
  print(` ✓ the /reticle skill, for ${String(skills.length)} ${agentWord(skills.length)}`);
128
+ }
120
129
  // Registration alone still leaves an Accept dialog in front of every call, and a verification run
121
130
  // makes dozens of them: the loop is only autonomous once the tools are pre-approved.
122
131
  const approvals = grantAutoApproval(agentIo, { home, platform });
@@ -173,8 +182,9 @@ async function restartHandedOverServer(appDir) {
173
182
  */
174
183
  async function afterConnect(input, url, sessionId, outcome, print) {
175
184
  const notes = [...outcome.notes];
176
- if (input.firstFlow === undefined)
185
+ if (input.firstFlow === undefined) {
177
186
  return { flowSaved: false, notes };
187
+ }
178
188
  const { flowSaved } = await input.firstFlow({
179
189
  appDir: input.appDir,
180
190
  bridgePort: input.bridgePort,
@@ -192,8 +202,9 @@ async function afterConnect(input, url, sessionId, outcome, print) {
192
202
  return { flowSaved, notes };
193
203
  }
194
204
  export async function runSetupCommand(input, print) {
195
- if (input.registerAgents)
205
+ if (input.registerAgents) {
196
206
  registerOtherAgents(print);
207
+ }
197
208
  // A moved port first: this project's daemon on the OLD port would otherwise be the one the page
198
209
  // finds and dials, while everything below waits on the new one.
199
210
  const retired = await retireMovedDaemons(readProjectId(input.appDir), input.bridgePort, nodeRetireDeps());
@@ -209,8 +220,9 @@ export async function runSetupCommand(input, print) {
209
220
  // Before the app is booted, because the app's whole job from here is to dial this port. Without
210
221
  // it the phases wait out their budget and then report the SDK as the thing that failed.
211
222
  const daemon = await ensureDaemon(input.bridgePort, nodeEnsureDaemonDeps((status) => undefined !== daemonSkew(status)));
212
- if (undefined !== daemon.message)
223
+ if (undefined !== daemon.message) {
213
224
  print(daemon.message);
225
+ }
214
226
  if (EnsureDaemon.SKEWED_IN_USE === daemon.state) {
215
227
  // Stop here: every hello from the page would be refused, and waiting out the connect budget
216
228
  // only to blame the page is the run this exists to prevent.
@@ -235,8 +247,9 @@ export async function runSetupCommand(input, print) {
235
247
  hasTauriConf: existsSync(join(input.appDir, 'src-tauri', 'tauri.conf.json')),
236
248
  hasElectronDep: hasElectronDependency(input.appDir),
237
249
  });
238
- if (AppShape.WEB !== shape)
250
+ if (AppShape.WEB !== shape) {
239
251
  print(`detected ${describeShape(shape)}`);
252
+ }
240
253
  const server = new OwnedDevServer();
241
254
  const effects = {
242
255
  startDevServer: (command, cwd) => {
@@ -261,8 +274,9 @@ export async function runSetupCommand(input, print) {
261
274
  },
262
275
  })
263
276
  : {};
264
- if (SigningEnv.IDENTITY in signing)
277
+ if (SigningEnv.IDENTITY in signing) {
265
278
  print(`signing the dev build as ${String(signing[SigningEnv.IDENTITY])}, so macOS does not ask for the keychain password`);
279
+ }
266
280
  server.start(command, cwd, { ...input.env, ...signing });
267
281
  return Promise.resolve();
268
282
  },
@@ -288,8 +302,9 @@ export async function runSetupCommand(input, print) {
288
302
  probePage,
289
303
  openBrowser: async (url) => {
290
304
  const failure = await openInBrowser(url);
291
- if (null === failure)
305
+ if (null === failure) {
292
306
  return undefined;
307
+ }
293
308
  // It used to end "nothing else here will open one", which stopped being true once a
294
309
  // Reticle-owned browser could always launch: run-setup opens a lease on this failure, so the
295
310
  // reader is told what happens next rather than sent to find a browser by hand.
@@ -310,8 +325,9 @@ export async function runSetupCommand(input, print) {
310
325
  // A daemon too old to report the short one falls back to the long one, because saying the
311
326
  // right thing at length beats saying nothing.
312
327
  const lead = status.whyLead ?? full;
313
- if (undefined === lead)
328
+ if (undefined === lead) {
314
329
  return undefined;
330
+ }
315
331
  return undefined === full ? { lead } : { lead, full };
316
332
  },
317
333
  now: () => Date.now(),
@@ -352,11 +368,13 @@ export async function runSetupCommand(input, print) {
352
368
  // Its output is on a file now, not this terminal, so say where: a compile error that used to
353
369
  // scroll past here is the first thing somebody will look for.
354
370
  const log = server.logPath();
355
- if (undefined !== log)
371
+ if (undefined !== log) {
356
372
  print(`The dev server keeps running; its output goes to ${log}`);
373
+ }
357
374
  }
358
- if (!outcome.ok || undefined === outcome.url || undefined === outcome.sessionId)
375
+ if (!outcome.ok || undefined === outcome.url || undefined === outcome.sessionId) {
359
376
  return outcome;
377
+ }
360
378
  return {
361
379
  ...outcome,
362
380
  ...(await afterConnect(input, outcome.url, outcome.sessionId, outcome, print)),
@@ -17,6 +17,8 @@ export interface SetupMcpIo {
17
17
  writeFile(path: string, contents: string): void;
18
18
  homeDir(): string;
19
19
  print(line: string): void;
20
+ /** The installed bin to register (`InitHost.mcpLauncher`). Absent: the pinned npx command. */
21
+ readonly launcher?: string | undefined;
20
22
  /** Register with a client that owns its own registration (Claude Code). Returns whether it took. */
21
23
  runCli(command: string, args: readonly string[]): boolean;
22
24
  /**
@@ -46,6 +48,12 @@ export interface SetupMcpResult {
46
48
  * and a pre-seeded `~/.codex/config.toml`; the file came back byte-identical both times.
47
49
  */
48
50
  readonly manual: readonly ManualClient[];
51
+ /** Clients whose Reticle plugin brings the server, so nothing was registered for them. */
52
+ readonly pluginActive: readonly string[];
53
+ /** Clients with the plugin AND a user-scope `reticle` entry: two servers. Named, never removed. */
54
+ readonly duplicates: readonly string[];
55
+ /** Clients whose `reticle` entry is the floating npx form. Named with the fix, never replaced. */
56
+ readonly stale: readonly string[];
49
57
  }
50
58
  /** A client the caller has to tell somebody about, with the two facts they need to act. */
51
59
  export interface ManualClient {
@@ -1,5 +1,5 @@
1
- import { McpClient, MCP_CLIENTS, claudeAddCommand, claudeAvailableProbe, claudeHasReticle, codexAddCommand, codexAvailableProbe, codexNamesOurServer, detectMcpClients, mergeClientConfig, clientSpec, ClientMergeStatus, } from '@reticlehq/init';
2
- import { OnboardingPhase, OnboardingStepStatus, } from '@reticlehq/core/telemetry';
1
+ import { McpClient, MCP_CLIENTS, claudeAddCommand, claudeAvailableProbe, claudeHasReticle, claudePluginInstalled, claudeUserEntryIsFloating, codexAddCommand, codexAvailableProbe, codexNamesOurServer, detectMcpClients, mergeClientConfig, clientSpec, ClientMergeStatus, } from '@reticlehq/init';
2
+ import { OnboardingPhase, OnboardingStepStatus, OnboardingStop, } from '@reticlehq/core/telemetry';
3
3
  /**
4
4
  * Codex registers through `codex mcp add`, which does the TOML editing we refuse to do (#1238).
5
5
  * `undefined` when that does not apply, and the caller reports a hand edit exactly as before: not
@@ -12,9 +12,10 @@ function registerWithCodexCli(io, client, serversKey) {
12
12
  return undefined;
13
13
  }
14
14
  const probe = codexAvailableProbe();
15
- if (!io.runCli(probe.command, probe.args))
15
+ if (!io.runCli(probe.command, probe.args)) {
16
16
  return undefined;
17
- const cmd = codexAddCommand();
17
+ }
18
+ const cmd = codexAddCommand(io.launcher);
18
19
  return io.runCli(cmd.command, cmd.args);
19
20
  }
20
21
  export function setupMcp(io) {
@@ -23,6 +24,9 @@ export function setupMcp(io) {
23
24
  const alreadyThere = [];
24
25
  const failed = [];
25
26
  const manual = [];
27
+ const pluginActive = [];
28
+ const duplicates = [];
29
+ const stale = [];
26
30
  /*
27
31
  * Claude Code first, because nothing else can find it.
28
32
  *
@@ -41,21 +45,34 @@ export function setupMcp(io) {
41
45
  if (io.runCli(available.command, available.args)) {
42
46
  detected.push(McpClient.CLAUDE_CODE);
43
47
  // Read from Claude's config: `claude mcp get` health-checks by launching the server.
44
- if (claudeHasReticle(io, undefined)) {
48
+ const hasEntry = claudeHasReticle(io, undefined);
49
+ if (claudePluginInstalled(io)) {
50
+ // The plugin brings its own server. A user-scope entry next to it is a second one.
51
+ pluginActive.push(McpClient.CLAUDE_CODE);
52
+ if (hasEntry) {
53
+ duplicates.push(McpClient.CLAUDE_CODE);
54
+ }
55
+ }
56
+ else if (hasEntry) {
45
57
  alreadyThere.push(McpClient.CLAUDE_CODE);
58
+ if (claudeUserEntryIsFloating(io)) {
59
+ stale.push(McpClient.CLAUDE_CODE);
60
+ }
46
61
  }
47
62
  else {
48
- const cmd = claudeAddCommand();
49
- if (io.runCli(cmd.command, cmd.args))
63
+ const cmd = claudeAddCommand(io.launcher);
64
+ if (io.runCli(cmd.command, cmd.args)) {
50
65
  registered.push(McpClient.CLAUDE_CODE);
51
- else
66
+ }
67
+ else {
52
68
  failed.push(McpClient.CLAUDE_CODE);
69
+ }
53
70
  }
54
71
  }
55
72
  for (const client of detectMcpClients(io)) {
56
73
  detected.push(client.id);
57
74
  const spec = clientSpec(client.id);
58
- const merged = mergeClientConfig(spec, client.existing);
75
+ const merged = mergeClientConfig(spec, client.existing, io.launcher);
59
76
  if (ClientMergeStatus.ALREADY === merged.status) {
60
77
  alreadyThere.push(client.id);
61
78
  continue;
@@ -65,12 +82,15 @@ export function setupMcp(io) {
65
82
  // sends somebody to an agent that has no Reticle tools in it.
66
83
  if (ClientMergeStatus.MANUAL === merged.status) {
67
84
  const viaCli = registerWithCodexCli(io, client, spec.serversKey);
68
- if (true === viaCli)
85
+ if (true === viaCli) {
69
86
  registered.push(client.id);
70
- else if (false === viaCli)
87
+ }
88
+ else if (false === viaCli) {
71
89
  failed.push(client.id);
72
- else
90
+ }
91
+ else {
73
92
  manual.push({ id: client.id, configPath: client.configPath, docs: spec.docs });
93
+ }
74
94
  continue;
75
95
  }
76
96
  try {
@@ -93,6 +113,7 @@ export function setupMcp(io) {
93
113
  step: 'agents_detected',
94
114
  status: OnboardingStepStatus.COMPLETED,
95
115
  });
116
+ const mcpStatus = mcpRegisteredStatus(detected.length, registered.length, alreadyThere.length + pluginActive.length);
96
117
  io.reportStep({
97
118
  phase: OnboardingPhase.INSTALL,
98
119
  step: 'mcp_registered',
@@ -107,15 +128,21 @@ export function setupMcp(io) {
107
128
  * for. Registering some and not others is still COMPLETED: the agent they are using may well be
108
129
  * one that worked, and a partial win is not a failed step.
109
130
  */
110
- status: 0 === detected.length
111
- ? OnboardingStepStatus.SKIPPED
112
- : 0 === registered.length && alreadyThere.length > 0
113
- ? OnboardingStepStatus.SKIPPED
114
- : 0 === registered.length && 0 === alreadyThere.length
115
- ? OnboardingStepStatus.FAILED
116
- : OnboardingStepStatus.COMPLETED,
131
+ status: mcpStatus,
132
+ ...(OnboardingStepStatus.FAILED === mcpStatus
133
+ ? { stop: OnboardingStop.MCP_NOT_REGISTERED }
134
+ : {}),
117
135
  });
118
- return { detected, registered, alreadyThere, failed, manual };
136
+ return { detected, registered, alreadyThere, failed, manual, pluginActive, duplicates, stale };
137
+ }
138
+ function mcpRegisteredStatus(detected, registered, alreadyThere) {
139
+ if (0 === detected || (0 === registered && 0 < alreadyThere)) {
140
+ return OnboardingStepStatus.SKIPPED;
141
+ }
142
+ if (0 === registered) {
143
+ return OnboardingStepStatus.FAILED;
144
+ }
145
+ return OnboardingStepStatus.COMPLETED;
119
146
  }
120
147
  /** Everything this machine could be asked about, for the report when nothing was found. */
121
148
  export function knownClientLabels() {
@@ -0,0 +1,8 @@
1
+ /** The url it serves and how to stop it, or why it did not serve. */
2
+ export type StartedDevServer = {
3
+ url: string;
4
+ stop: () => void;
5
+ } | {
6
+ failed: string;
7
+ };
8
+ export declare function startDevServerAndWait(command: string, dir: string, budgetMs: number): Promise<StartedDevServer>;
@@ -0,0 +1,43 @@
1
+ /**
2
+ * Start a project's dev script and wait until it serves a page, for a command that is not `init`.
3
+ *
4
+ * The same pieces `init` uses: the supervised `OwnedDevServer`, the URL it announces or the port its
5
+ * process tree binds, and `judgeWait` for ready, dead or hung. `init` keeps its own loop because it
6
+ * also reports progress and hands the server over; this one stops the server when the caller is done.
7
+ */
8
+ import { OwnedDevServer, probePage } from './node-effects.js';
9
+ import { WaitVerdict, devServerDiedMessage, judgeWait, portBusyMessage, urlToWatch, } from './bringup/dev-server-wait.js';
10
+ const POLL_MS = 500;
11
+ const MS_PER_S = 1000;
12
+ export async function startDevServerAndWait(command, dir, budgetMs) {
13
+ const server = new OwnedDevServer();
14
+ server.start(command, dir, {});
15
+ const startedAt = Date.now();
16
+ for (;;) {
17
+ const watching = urlToWatch(server.output(), server.listeningPorts());
18
+ const probe = watching === undefined ? undefined : await probePage(watching);
19
+ const verdict = judgeWait({
20
+ output: server.output(),
21
+ launcherExited: server.exited(),
22
+ serving: true === probe?.served,
23
+ quietForMs: server.quietForMs(),
24
+ elapsedMs: Date.now() - startedAt,
25
+ budgetMs,
26
+ });
27
+ if (WaitVerdict.READY === verdict && watching !== undefined) {
28
+ return { url: probe?.reachedUrl ?? watching, stop: () => server.stop() };
29
+ }
30
+ if (WaitVerdict.DEAD === verdict) {
31
+ const failed = portBusyMessage(server.output()) ?? devServerDiedMessage(server.output());
32
+ server.stop();
33
+ return { failed };
34
+ }
35
+ if (WaitVerdict.HUNG === verdict) {
36
+ server.stop();
37
+ return {
38
+ failed: `The dev server did not serve a page in ${String(budgetMs / MS_PER_S)}s.`,
39
+ };
40
+ }
41
+ await new Promise((resolve) => setTimeout(resolve, POLL_MS));
42
+ }
43
+ }
@@ -5,3 +5,5 @@ export declare const SERVER_VERSION: string;
5
5
  * drift. Self-update installs THIS (never `@reticlehq/core`, which is schema-only and has no bin).
6
6
  */
7
7
  export declare const RETICLE_NPM_PACKAGE: string;
8
+ /** The Node.js range this package declares in `engines`, such as `>=20.11`. */
9
+ export declare const SERVER_NODE_RANGE: string | undefined;
@@ -48,3 +48,5 @@ export const SERVER_VERSION = _pkg.version;
48
48
  * drift. Self-update installs THIS (never `@reticlehq/core`, which is schema-only and has no bin).
49
49
  */
50
50
  export const RETICLE_NPM_PACKAGE = _pkg.name;
51
+ /** The Node.js range this package declares in `engines`, such as `>=20.11`. */
52
+ export const SERVER_NODE_RANGE = _pkg.engines?.node;
@@ -106,6 +106,13 @@ export declare function compareVersions(a: string | undefined, b: string): numbe
106
106
  export declare function sdkIsBehind(sdk: string | undefined, daemon: string): boolean;
107
107
  /** The same fact for the person looking at the page, on its overlay. */
108
108
  export declare const SDK_BEHIND_NARRATION: (sdk: string, daemon: string) => string;
109
+ /**
110
+ * The sentence for a screenshot the page cannot draw because its SDK is behind this daemon.
111
+ *
112
+ * `render` is one of the things a behind page does not have. The refusal from the page names the
113
+ * missing command and nothing else; this is the same update the overlay already shows.
114
+ */
115
+ export declare function sdkBehindNote(sdk: string | undefined): string | undefined;
109
116
  /**
110
117
  * A page whose SDK is a minor release or more behind this daemon.
111
118
  *
@@ -23,6 +23,7 @@
23
23
  * a human acts on.
24
24
  */
25
25
  import { RETIRED_WIRE_NAMES } from '@reticlehq/core';
26
+ import { SERVER_VERSION } from './identity/server-version.js';
26
27
  import { resolveSdkFix } from './sdk-fix.js';
27
28
  /** Which pair disagreed. Named so the nudge can report each independently. */
28
29
  export const SkewPair = {
@@ -177,9 +178,19 @@ export function sdkIsBehind(sdk, daemon) {
177
178
  const order = compareVersions(minorOf(sdk), minorOf(daemon));
178
179
  return order !== undefined && order < 0;
179
180
  }
181
+ /** What to run so the page picks up the daemon's SDK. */
182
+ const UPDATE_THE_SDK = "Run `reticle update` in the app's folder, then restart its dev server.";
180
183
  /** The same fact for the person looking at the page, on its overlay. */
181
- export const SDK_BEHIND_NARRATION = (sdk, daemon) => `This app runs Reticle SDK ${sdk}, older than Reticle ${daemon}. Run \`reticle update\` in the ` +
182
- "app's folder, then restart its dev server.";
184
+ export const SDK_BEHIND_NARRATION = (sdk, daemon) => `This app runs Reticle SDK ${sdk}, older than Reticle ${daemon}. ${UPDATE_THE_SDK}`;
185
+ /**
186
+ * The sentence for a screenshot the page cannot draw because its SDK is behind this daemon.
187
+ *
188
+ * `render` is one of the things a behind page does not have. The refusal from the page names the
189
+ * missing command and nothing else; this is the same update the overlay already shows.
190
+ */
191
+ export function sdkBehindNote(sdk) {
192
+ return describeSdkBehind(sdk, SERVER_VERSION, () => UPDATE_THE_SDK);
193
+ }
183
194
  /**
184
195
  * A page whose SDK is a minor release or more behind this daemon.
185
196
  *
@@ -1,14 +1,9 @@
1
1
  /**
2
- * Enterprise license-key verification + the assertEnterprise gate. ENTERPRISE CODE (Reticle Enterprise
3
- * License — see./LICENSE), not the FSL that covers the rest of the server.
2
+ * License-key verification for `reticle license` and the telemetry `licenseStatus` field.
4
3
  *
5
- * Offline + no phone-home (preserves the no-telemetry brand): a key is a signed payload Reticle issues
6
- * with its private key; this verifies it against the issuer's PUBLIC key with Ed25519. Dev/eval is a
7
- * no-op (requireLicense:false) so the gate never gets in a contributor's way; only a production-mode
8
- * caller with requireLicense:true must present a valid, unexpired key. The clock is injected (rule 7).
9
- *
10
- * Rules for this directory: the free server must never import from ee/; ee/ depends only on protocol +
11
- * node + zod. This file imports neither server core nor any sibling outside ee/.
4
+ * Offline, with no phone-home: a key is a signed payload that Reticle issues with its private key.
5
+ * This file verifies it against the issuer's PUBLIC key with Ed25519. Nothing in this package is
6
+ * gated on a key. The clock is injected (rule 7).
12
7
  */
13
8
  import { type KeyObject } from 'node:crypto';
14
9
  import { z } from 'zod';
@@ -23,8 +18,8 @@ export declare const LicenseStatus: {
23
18
  };
24
19
  export type LicenseStatus = (typeof LicenseStatus)[keyof typeof LicenseStatus];
25
20
  /**
26
- * The signed claims inside a key. `exp` is epoch ms; `features` (optional) scopes which ee features
27
- * unlock.
21
+ * The signed claims inside a key. `exp` is epoch ms; `features` (optional) scopes the features that
22
+ * the key covers.
28
23
  *
29
24
  * `lid` is the stable licence id and the only safe identity: `org` is a display name a human typed at
30
25
  * signing time, so two organisations with the same name collapse into one and a rename splits one into
@@ -61,48 +56,14 @@ export declare function signLicenseKey(payload: LicensePayload, privateKey: KeyO
61
56
  /** Verify a key against the issuer public key. Never throws — returns a structured status. */
62
57
  export declare function verifyLicenseKey(key: string | undefined, publicKey: KeyObject, now: number): LicenseCheck;
63
58
  /**
64
- * Where to get a key. A named constant because it appears in the gate's error AND in what
65
- * `reticle license` prints, and a contact address that drifts between the two is a support request
66
- * that reaches nobody.
59
+ * Where to get a key. A named constant because `reticle license` prints it on every branch, and a
60
+ * contact address that drifts is a support request that reaches nobody.
67
61
  */
68
62
  export declare const LICENSE_CONTACT = "hey@reticle.sh";
69
- /**
70
- * The features a licence gates in THIS build. The single source: `assertEnterprise` callers name a
71
- * member rather than a string literal, and `reticle license` reports the list, so what the CLI claims
72
- * is gated is derived from what is actually gated rather than a second list somebody maintains.
73
- *
74
- * It is deliberately short. Reticle's roadmap for enterprise is longer (SSO, SCIM, RBAC, multi-org,
75
- * policy gates), and none of it is here yet — printing a roadmap as though it were shipped is how a
76
- * buyer discovers on day two that the thing they paid for does not exist. What is listed is what the
77
- * running build will actually refuse without a key.
78
- */
79
- export declare const EnterpriseFeature: {
80
- /** Recording an audit event. */
81
- readonly AUDIT_LOG: "audit-log";
82
- };
83
- export type EnterpriseFeature = (typeof EnterpriseFeature)[keyof typeof EnterpriseFeature];
84
- /** Thrown when a production-mode enterprise feature is used without a valid license. */
85
- export declare class EnterpriseLicenseError extends Error {
86
- readonly feature: string;
87
- readonly reason: string;
88
- constructor(feature: string, reason: string);
89
- }
90
- /** Context for the gate. requireLicense:false ⇒ dev/eval no-op. publicKey injectable for tests. */
91
- export interface GateContext {
92
- requireLicense: boolean;
93
- now: () => number;
94
- key?: string;
95
- publicKey?: KeyObject;
96
- }
97
- /**
98
- * Gate an enterprise feature. No-op in dev/eval (requireLicense:false). In production it throws
99
- * EnterpriseLicenseError unless a valid, unexpired key that covers `feature` is present.
100
- */
101
- export declare function assertEnterprise(feature: string, ctx: GateContext, env?: NodeJS.ProcessEnv, baked?: string): void;
102
63
  /** Env names that carry the activation: the operator's key, and the issuer public key baked at release. */
103
64
  export declare const LICENSE_KEY_ENV = "RETICLE_LICENSE_KEY";
104
65
  export declare const LICENSE_PUBLIC_KEY_ENV = "RETICLE_LICENSE_PUBLIC_KEY";
105
- /** The human-facing state of enterprise activation on this machine (what `reticle license status` shows). */
66
+ /** The human-facing state of license activation on this machine (what `reticle license` shows). */
106
67
  interface LicenseReport {
107
68
  /**
108
69
  * Typed against core's closed list rather than re-listing the strings here: this status is reported
@@ -112,18 +73,7 @@ interface LicenseReport {
112
73
  status: LicenseActivation;
113
74
  /** The stable license id — what usage is attributed to. Present only when `status` is `active`. */
114
75
  licenseId?: string;
115
- /**
116
- * What a licence unlocks in this build, reported alongside where to get one — being told to set a
117
- * variable with no idea what it unlocks is a dead end at the moment somebody is asking. Omitted
118
- * once a licence is `active`, where the reader already has it.
119
- */
120
- gated?: readonly string[];
121
76
  contact: string;
122
- /**
123
- * The key is valid, and scoped to features this build does not gate, so it unlocks nothing here.
124
- * Present only when true: a `false` on every healthy licence is noise on the common case.
125
- */
126
- coversNothingHere?: boolean;
127
77
  org?: string;
128
78
  plan?: string;
129
79
  expiresAt?: number;
@@ -132,18 +82,8 @@ interface LicenseReport {
132
82
  }
133
83
  /**
134
84
  * Resolve activation entirely from the environment — the install mechanism: the release bakes the
135
- * issuer public key, the operator sets RETICLE_LICENSE_KEY. No public key configured ⇒ evaluation mode
136
- * (enterprise features run free, dev/test only). Offline, no phone-home.
85
+ * issuer public key, the operator sets RETICLE_LICENSE_KEY. No public key configured ⇒ evaluation mode.
86
+ * Offline, no phone-home.
137
87
  */
138
88
  export declare function describeLicense(now: number, env?: NodeJS.ProcessEnv, baked?: string): LicenseReport;
139
- /**
140
- * Gate an enterprise feature using env-resolved activation. Enforcement is ON when the issuer public key
141
- * is configured (a real release). Without it, behaviour splits on runtime:
142
- * - dev/eval (NODE_ENV ≠ production): features run FREE — never blocks a contributor or CI.
143
- * - production (NODE_ENV = production): FAIL CLOSED — a release that reaches production with no
144
- * resolvable issuer key was mis-built (the baked key wasn't stamped in, BAKED_ISSUER_PUBLIC_KEY_PEM
145
- * is still ''), and silently unlocking every ee feature with no key and no warning is the worst
146
- * outcome. Deny instead, with a distinct reason so the misconfiguration is obvious.
147
- */
148
- export declare function assertEnterpriseFromEnv(feature: string, now: number, env?: NodeJS.ProcessEnv, baked?: string): void;
149
89
  export {};