@reticlehq/server 2.7.0 → 2.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 (357) hide show
  1. package/README.md +14 -5
  2. package/SKILL.md +130 -778
  3. package/dist/bridge/bridge.d.ts +23 -0
  4. package/dist/bridge/bridge.js +80 -2
  5. package/dist/bridge/bridge.test-harness.d.ts +5 -0
  6. package/dist/bridge/bridge.test-harness.js +6 -4
  7. package/dist/capsule/causal-summary.d.ts +36 -1
  8. package/dist/capsule/causal-summary.js +33 -7
  9. package/dist/capsule/divergence.js +7 -1
  10. package/dist/cli/chromium-hint.d.ts +33 -5
  11. package/dist/cli/chromium-hint.js +82 -12
  12. package/dist/cli/cli-doctor.js +65 -34
  13. package/dist/cli/cli-flow-commands.js +8 -8
  14. package/dist/cli/cli-kill.d.ts +58 -0
  15. package/dist/cli/cli-kill.js +162 -0
  16. package/dist/cli/cli-parse.d.ts +16 -1
  17. package/dist/cli/cli-parse.js +32 -9
  18. package/dist/cli/cli-port.d.ts +32 -0
  19. package/dist/cli/cli-port.js +82 -35
  20. package/dist/cli/cli-verify.d.ts +21 -0
  21. package/dist/cli/cli-verify.js +47 -3
  22. package/dist/cli/config-discovery.d.ts +35 -0
  23. package/dist/cli/config-discovery.js +163 -0
  24. package/dist/cli/doctor-daemon-line.js +4 -1
  25. package/dist/cli/doctor-project-line.d.ts +37 -0
  26. package/dist/cli/doctor-project-line.js +35 -0
  27. package/dist/cli/doctor-rows.d.ts +51 -0
  28. package/dist/cli/doctor-rows.js +54 -0
  29. package/dist/cli/doctor-sessions-line.d.ts +12 -0
  30. package/dist/cli/doctor-sessions-line.js +69 -0
  31. package/dist/cli/drive-attach.d.ts +99 -0
  32. package/dist/cli/drive-attach.js +203 -0
  33. package/dist/cli/drive-command.d.ts +13 -0
  34. package/dist/cli/drive-command.js +103 -0
  35. package/dist/cli/open-note.d.ts +20 -0
  36. package/dist/cli/open-note.js +37 -0
  37. package/dist/cli/port-holder.d.ts +10 -0
  38. package/dist/cli/port-holder.js +26 -2
  39. package/dist/cli/status-next-action.d.ts +37 -0
  40. package/dist/cli/status-next-action.js +61 -0
  41. package/dist/cli.js +119 -39
  42. package/dist/crawl/crawl-tools.js +12 -2
  43. package/dist/crawl/crawl.d.ts +13 -0
  44. package/dist/crawl/crawl.js +50 -2
  45. package/dist/events/contradictions.d.ts +46 -0
  46. package/dist/events/contradictions.js +35 -22
  47. package/dist/events/declared.d.ts +45 -0
  48. package/dist/events/declared.js +73 -0
  49. package/dist/events/echo-mismatch.d.ts +8 -2
  50. package/dist/events/echo-mismatch.js +33 -5
  51. package/dist/events/event-filters.d.ts +1 -1
  52. package/dist/events/event-filters.js +7 -4
  53. package/dist/events/predicate-eval.d.ts +17 -72
  54. package/dist/events/predicate-eval.js +117 -236
  55. package/dist/events/predicate-parse.js +31 -3
  56. package/dist/events/predicate-route.d.ts +32 -0
  57. package/dist/events/predicate-route.js +102 -0
  58. package/dist/events/predicate-schema.d.ts +99 -0
  59. package/dist/events/predicate-schema.js +309 -0
  60. package/dist/events/predicate.d.ts +8 -0
  61. package/dist/events/predicate.js +296 -22
  62. package/dist/flows/annotate-tools.js +3 -0
  63. package/dist/flows/annotate.js +18 -4
  64. package/dist/flows/coverage.d.ts +38 -5
  65. package/dist/flows/coverage.js +30 -4
  66. package/dist/flows/decision.d.ts +15 -0
  67. package/dist/flows/decision.js +1 -1
  68. package/dist/flows/flow-classify.js +1 -1
  69. package/dist/flows/flow-replay-run.js +8 -1
  70. package/dist/flows/flow-tools.js +21 -5
  71. package/dist/flows/heal-run.js +1 -1
  72. package/dist/flows/replay.js +3 -0
  73. package/dist/honesty/accepted-write.d.ts +3 -0
  74. package/dist/honesty/accepted-write.js +8 -3
  75. package/dist/honesty/already-true.js +15 -0
  76. package/dist/honesty/blind-spots.js +10 -2
  77. package/dist/honesty/honesty.d.ts +11 -0
  78. package/dist/honesty/honesty.js +1 -0
  79. package/dist/honesty/uncaptured-bodies.d.ts +4 -2
  80. package/dist/honesty/uncaptured-bodies.js +9 -2
  81. package/dist/honesty/unread-outcome.d.ts +8 -0
  82. package/dist/honesty/unread-outcome.js +36 -9
  83. package/dist/honesty/unsettled.d.ts +49 -0
  84. package/dist/honesty/unsettled.js +82 -0
  85. package/dist/honesty/verified.d.ts +36 -3
  86. package/dist/honesty/verified.js +54 -11
  87. package/dist/http-server.d.ts +19 -0
  88. package/dist/http-server.js +202 -4
  89. package/dist/impact/impact-recorder.d.ts +23 -0
  90. package/dist/impact/impact-recorder.js +76 -0
  91. package/dist/impact/impact-store.d.ts +47 -0
  92. package/dist/impact/impact-store.js +171 -0
  93. package/dist/index.d.ts +7 -0
  94. package/dist/index.js +47 -10
  95. package/dist/init/agent-rules.d.ts +36 -0
  96. package/dist/init/agent-rules.js +132 -22
  97. package/dist/init/astro-patch.d.ts +2 -1
  98. package/dist/init/astro-patch.js +38 -6
  99. package/dist/init/capabilities.d.ts +22 -0
  100. package/dist/init/capabilities.js +76 -0
  101. package/dist/init/confirm.d.ts +114 -0
  102. package/dist/init/confirm.js +165 -0
  103. package/dist/init/connect-steps.d.ts +2 -0
  104. package/dist/init/connect-steps.js +50 -0
  105. package/dist/init/cra.js +3 -2
  106. package/dist/init/csp-check.d.ts +38 -0
  107. package/dist/init/csp-check.js +92 -0
  108. package/dist/init/csp-doctor.d.ts +25 -0
  109. package/dist/init/csp-doctor.js +49 -0
  110. package/dist/init/cursor.js +6 -9
  111. package/dist/init/detect.d.ts +8 -0
  112. package/dist/init/detect.js +33 -3
  113. package/dist/init/mcp-clients.d.ts +1 -0
  114. package/dist/init/mcp-clients.js +51 -7
  115. package/dist/init/mcp.d.ts +15 -0
  116. package/dist/init/mcp.js +19 -0
  117. package/dist/init/non-js-project.js +29 -5
  118. package/dist/init/plan-framework.d.ts +10 -0
  119. package/dist/init/plan-framework.js +78 -16
  120. package/dist/init/plan.d.ts +47 -6
  121. package/dist/init/plan.js +228 -69
  122. package/dist/init/run.d.ts +24 -2
  123. package/dist/init/run.js +157 -45
  124. package/dist/init/slash-command.d.ts +1 -1
  125. package/dist/init/slash-command.js +54 -6
  126. package/dist/init/snippets.d.ts +45 -5
  127. package/dist/init/snippets.js +194 -42
  128. package/dist/init/vite-config.js +53 -5
  129. package/dist/input/network-mock-tools.d.ts +19 -0
  130. package/dist/input/network-mock-tools.js +37 -5
  131. package/dist/input/scroll-find.d.ts +6 -0
  132. package/dist/input/scroll-find.js +17 -2
  133. package/dist/input/scroll-tools.js +4 -0
  134. package/dist/journal/attach-journal.d.ts +9 -1
  135. package/dist/journal/attach-journal.js +13 -2
  136. package/dist/journal/journal-query.d.ts +13 -0
  137. package/dist/journal/journal-query.js +28 -3
  138. package/dist/journal/workspace-gitignore.d.ts +10 -0
  139. package/dist/journal/workspace-gitignore.js +59 -0
  140. package/dist/license/license.js +1 -1
  141. package/dist/mcp/mcp-proxy.d.ts +35 -30
  142. package/dist/mcp/mcp-proxy.js +306 -104
  143. package/dist/mcp/mcp.d.ts +10 -1
  144. package/dist/mcp/mcp.js +70 -34
  145. package/dist/mcp/proxy-handshake.d.ts +82 -1
  146. package/dist/mcp/proxy-handshake.js +109 -2
  147. package/dist/mcp/proxy-lifecycle.d.ts +11 -2
  148. package/dist/mcp/proxy-lifecycle.js +12 -3
  149. package/dist/mcp/proxy-log.d.ts +63 -0
  150. package/dist/mcp/proxy-log.js +132 -0
  151. package/dist/mcp/server-instructions.d.ts +17 -0
  152. package/dist/mcp/server-instructions.js +56 -0
  153. package/dist/mcp/shared-params.d.ts +27 -0
  154. package/dist/mcp/shared-params.js +36 -0
  155. package/dist/pool/browser-pool.d.ts +22 -2
  156. package/dist/pool/browser-pool.js +44 -4
  157. package/dist/project/learned-routes.d.ts +24 -0
  158. package/dist/project/learned-routes.js +63 -0
  159. package/dist/project/project-store.d.ts +6 -0
  160. package/dist/project/project-store.js +25 -1
  161. package/dist/project/reticle-dir.d.ts +2 -0
  162. package/dist/project/reticle-dir.js +1 -0
  163. package/dist/session/connection-memory.d.ts +59 -0
  164. package/dist/session/connection-memory.js +108 -0
  165. package/dist/session/dev-command.d.ts +26 -0
  166. package/dist/session/dev-command.js +89 -0
  167. package/dist/session/dev-server-probe.d.ts +68 -0
  168. package/dist/session/dev-server-probe.js +102 -7
  169. package/dist/session/last-act.d.ts +26 -0
  170. package/dist/session/last-act.js +35 -1
  171. package/dist/session/live-control-tools.js +42 -5
  172. package/dist/session/no-session-diagnosis.d.ts +67 -2
  173. package/dist/session/no-session-diagnosis.js +236 -27
  174. package/dist/session/no-session-next-action.d.ts +46 -0
  175. package/dist/session/no-session-next-action.js +85 -0
  176. package/dist/session/no-session-watch.d.ts +40 -1
  177. package/dist/session/no-session-watch.js +174 -8
  178. package/dist/session/pending-commands.d.ts +12 -0
  179. package/dist/session/pending-commands.js +13 -1
  180. package/dist/session/ref-provenance.d.ts +54 -0
  181. package/dist/session/ref-provenance.js +61 -0
  182. package/dist/session/session-lease.js +1 -1
  183. package/dist/session/session-manager.d.ts +4 -0
  184. package/dist/session/session-manager.js +35 -5
  185. package/dist/session/session.d.ts +10 -1
  186. package/dist/session/session.js +146 -5
  187. package/dist/telemetry/app-instrumented.d.ts +8 -0
  188. package/dist/telemetry/app-instrumented.js +10 -0
  189. package/dist/telemetry/automation-hint.d.ts +17 -0
  190. package/dist/telemetry/automation-hint.js +71 -0
  191. package/dist/telemetry/bug-found.d.ts +15 -17
  192. package/dist/telemetry/bug-found.js +52 -21
  193. package/dist/telemetry/cli-telemetry.js +7 -1
  194. package/dist/telemetry/daemon-telemetry.js +24 -0
  195. package/dist/telemetry/feedback.js +1 -1
  196. package/dist/telemetry/init-telemetry.js +9 -1
  197. package/dist/telemetry/install-source.d.ts +56 -0
  198. package/dist/telemetry/install-source.js +74 -0
  199. package/dist/telemetry/instrumentation-stall.d.ts +50 -0
  200. package/dist/telemetry/instrumentation-stall.js +90 -0
  201. package/dist/telemetry/run-telemetry.js +4 -1
  202. package/dist/telemetry/session-metrics.d.ts +8 -0
  203. package/dist/telemetry/session-metrics.js +15 -0
  204. package/dist/telemetry/telemetry.d.ts +7 -1
  205. package/dist/telemetry/telemetry.js +26 -2
  206. package/dist/telemetry/tool-refused.d.ts +19 -0
  207. package/dist/telemetry/tool-refused.js +55 -0
  208. package/dist/telemetry/verification-of.js +46 -7
  209. package/dist/temp-dir.d.ts +27 -0
  210. package/dist/temp-dir.js +46 -0
  211. package/dist/tools/act-tools.js +209 -28
  212. package/dist/tools/assert-grade.d.ts +1 -1
  213. package/dist/tools/assert-grade.js +27 -5
  214. package/dist/tools/assert-verdict.js +49 -7
  215. package/dist/tools/browser-tools.js +10 -0
  216. package/dist/tools/contract-tools.js +4 -0
  217. package/dist/tools/dynamic-tools.js +16 -7
  218. package/dist/tools/error-recovery.d.ts +10 -0
  219. package/dist/tools/error-recovery.js +73 -9
  220. package/dist/tools/feedback-invite.js +1 -1
  221. package/dist/tools/invoke-tool.js +159 -26
  222. package/dist/tools/lease-hint.d.ts +30 -8
  223. package/dist/tools/lease-hint.js +65 -14
  224. package/dist/tools/lease-tools.d.ts +40 -1
  225. package/dist/tools/lease-tools.js +181 -83
  226. package/dist/tools/navigate-result.js +1 -1
  227. package/dist/tools/observe-tools.js +10 -4
  228. package/dist/tools/observed-nothing.js +1 -1
  229. package/dist/tools/react-grace.d.ts +52 -0
  230. package/dist/tools/react-grace.js +90 -0
  231. package/dist/tools/read-tools.js +67 -5
  232. package/dist/tools/resolve-target.d.ts +33 -0
  233. package/dist/tools/resolve-target.js +53 -0
  234. package/dist/tools/settle-in-flight.d.ts +94 -0
  235. package/dist/tools/settle-in-flight.js +146 -0
  236. package/dist/tools/snapshot-delta.d.ts +10 -0
  237. package/dist/tools/snapshot-delta.js +194 -9
  238. package/dist/tools/tool-kit.d.ts +8 -0
  239. package/dist/tools/tool-surface.d.ts +60 -0
  240. package/dist/tools/tool-surface.js +86 -1
  241. package/dist/tools/tools.js +61 -8
  242. package/dist/tools/unadvertised-help.js +9 -6
  243. package/docs/README.md +55 -11
  244. package/docs/actions.mdx +120 -0
  245. package/docs/agent-cheatsheet.md +95 -29
  246. package/docs/architecture.md +52 -35
  247. package/docs/benchmarks.md +127 -75
  248. package/docs/best-practices.mdx +216 -0
  249. package/docs/capabilities.mdx +534 -0
  250. package/docs/cli/affected.mdx +53 -0
  251. package/docs/cli/capsules.mdx +70 -0
  252. package/docs/cli/cloud.mdx +153 -0
  253. package/docs/cli/doctor.mdx +109 -0
  254. package/docs/cli/drive.mdx +74 -0
  255. package/docs/cli/feedback.mdx +67 -0
  256. package/docs/cli/gate.mdx +64 -0
  257. package/docs/cli/help.mdx +94 -0
  258. package/docs/cli/hunt.mdx +73 -0
  259. package/docs/cli/identify.mdx +76 -0
  260. package/docs/cli/init.mdx +121 -0
  261. package/docs/cli/kill.mdx +91 -0
  262. package/docs/cli/license.mdx +48 -0
  263. package/docs/cli/mcp.mdx +61 -0
  264. package/docs/cli/open.mdx +74 -0
  265. package/docs/cli/restart.mdx +60 -0
  266. package/docs/cli/rollback.mdx +35 -0
  267. package/docs/cli/serve.mdx +69 -0
  268. package/docs/cli/status.mdx +88 -0
  269. package/docs/cli/stop.mdx +64 -0
  270. package/docs/cli/telemetry.mdx +57 -0
  271. package/docs/cli/update.mdx +52 -0
  272. package/docs/cli/verify.mdx +76 -0
  273. package/docs/cli/version.mdx +39 -0
  274. package/docs/cli/watch.mdx +62 -0
  275. package/docs/cli.mdx +241 -0
  276. package/docs/debugging.md +16 -10
  277. package/docs/deploy-checks.md +45 -12
  278. package/docs/desktop.mdx +384 -0
  279. package/docs/docs.json +364 -0
  280. package/docs/enterprise.md +22 -16
  281. package/docs/faq.mdx +102 -0
  282. package/docs/fixtures.md +13 -7
  283. package/docs/flows.md +29 -23
  284. package/docs/for-agents.md +71 -0
  285. package/docs/frameworks.mdx +238 -0
  286. package/docs/gate-plan.md +48 -42
  287. package/docs/gates.md +28 -22
  288. package/docs/getting-started.md +129 -77
  289. package/docs/human-control.md +24 -18
  290. package/docs/index.mdx +143 -0
  291. package/docs/install-agentic.mdx +224 -0
  292. package/docs/install-manual.mdx +196 -0
  293. package/docs/instrumentation.mdx +238 -0
  294. package/docs/integration-patterns.md +41 -35
  295. package/docs/local-registry.md +19 -13
  296. package/docs/multi-agent-testing.md +19 -13
  297. package/docs/packages/babel-plugin.mdx +79 -0
  298. package/docs/packages/browser.mdx +111 -0
  299. package/docs/packages/core.mdx +116 -0
  300. package/docs/packages/electron.mdx +85 -0
  301. package/docs/packages/eslint-plugin.mdx +86 -0
  302. package/docs/packages/next.mdx +89 -0
  303. package/docs/packages/react.mdx +91 -0
  304. package/docs/packages/server.mdx +98 -0
  305. package/docs/packages/tauri.mdx +63 -0
  306. package/docs/packages/test.mdx +119 -0
  307. package/docs/packages/vite-plugin.mdx +94 -0
  308. package/docs/packages.mdx +218 -0
  309. package/docs/platform-integration.md +35 -26
  310. package/docs/predicates.mdx +248 -0
  311. package/docs/quickstart.mdx +257 -0
  312. package/docs/recipes.mdx +282 -0
  313. package/docs/skill-file.mdx +107 -0
  314. package/docs/state-management.mdx +199 -0
  315. package/docs/style.css +14 -0
  316. package/docs/system-map.md +50 -33
  317. package/docs/telemetry-contract.md +106 -52
  318. package/docs/telemetry-events.mdx +475 -0
  319. package/docs/telemetry.md +63 -55
  320. package/docs/testing.md +44 -6
  321. package/docs/token-efficiency.md +37 -27
  322. package/docs/tools-act-and-wait.mdx +173 -0
  323. package/docs/tools-act-sequence.mdx +137 -0
  324. package/docs/tools-act.mdx +95 -0
  325. package/docs/tools-assert.mdx +129 -0
  326. package/docs/tools-console.mdx +63 -0
  327. package/docs/tools-inspect.mdx +88 -0
  328. package/docs/tools-navigate.mdx +99 -0
  329. package/docs/tools-network.mdx +119 -0
  330. package/docs/tools-observe.mdx +145 -0
  331. package/docs/tools-overview.mdx +160 -0
  332. package/docs/tools-query.mdx +126 -0
  333. package/docs/tools-session-and-feedback.mdx +112 -0
  334. package/docs/tools-sessions.mdx +94 -0
  335. package/docs/tools-snapshot.mdx +129 -0
  336. package/docs/tools-state.mdx +104 -0
  337. package/docs/tools-tools-and-run.mdx +156 -0
  338. package/docs/tools-wait-for.mdx +85 -0
  339. package/docs/troubleshooting.mdx +263 -0
  340. package/docs/usage.md +209 -165
  341. package/docs/vs-chrome-devtools-mcp.mdx +123 -0
  342. package/docs/vs-playwright-mcp.mdx +125 -0
  343. package/docs/vs-screenshots.mdx +137 -0
  344. package/docs/what-is-reticle.mdx +63 -0
  345. package/docs/why-reticle.mdx +109 -0
  346. package/package.json +5 -5
  347. package/docs/desktop-apps.md +0 -256
  348. package/docs/matrix/2.5.0/claude-code-machine.json +0 -19
  349. package/docs/matrix/2.5.0/codex-machine.json +0 -20
  350. package/docs/matrix/2.5.0/cursor-machine.json +0 -22
  351. package/docs/matrix/2.5.0/gemini-machine.json +0 -22
  352. package/docs/matrix/2.5.0/opencode-machine.json +0 -22
  353. package/docs/matrix/2.5.0/vscode-machine.json +0 -22
  354. package/docs/matrix/2.5.0/windsurf-machine.json +0 -22
  355. package/docs/matrix/MATRIX.md +0 -26
  356. package/docs/matrix/README.md +0 -59
  357. package/docs/mint.json +0 -61
@@ -0,0 +1,114 @@
1
+ /**
2
+ * The step `init` never took: stay and watch for an app to connect.
3
+ *
4
+ * The install has two halves. Registering the MCP server gives the agent the tools; getting the SDK
5
+ * into a running page gives it something to look at. Almost everyone finishes the first, and nothing
6
+ * joined "init finished" to "an app connected" — so `init` exited 0 having written files, and the
7
+ * person left with a config file and no instrumented page was never told (#269).
8
+ *
9
+ * The capability existed in three places and was used together by none of them: the poll behind
10
+ * `reticle open`, the install gate (which calls this exact check "the only step that proves the
11
+ * install works"), and the event that measures whether it ever happened.
12
+ *
13
+ * WHY NO FLAG, and no dev-server management:
14
+ *
15
+ * - No flag, because the people this is for are the ones who do not know to ask. `--verify` would
16
+ * be typed by the users who already run `status` afterwards, which is exactly the cohort that is
17
+ * not being lost. The gate instead is whether a HUMAN IS AT THE TERMINAL: `init` waits when
18
+ * stdout is a TTY and skips the wait entirely otherwise, so a scripted or agent-driven `init`
19
+ * keeps today's cost and today's exit behaviour, and blocking never happens where blocking is
20
+ * wrong. The signal is the standard "am I being piped" one and needs no argument parsing, which
21
+ * also keeps `init`'s documented flag surface where it is.
22
+ * - It never starts anything. A dev server started by a long-lived daemon is invisible to the
23
+ * person whose machine it runs on and orphans when the daemon exits, so the daemon stays out of
24
+ * it — the AGENT starts one, under the guards in agent-rules.ts, because it is attributable and
25
+ * stoppable there. So the honest shape here is: confirm the app if it connects, otherwise name
26
+ * the ONE command that is outstanding and the one that proves it worked.
27
+ */
28
+ import { InitConfirmation } from '@reticlehq/core';
29
+ import type { InitOutcome } from '@reticlehq/core';
30
+ /**
31
+ * How long a human is asked to wait.
32
+ *
33
+ * Long enough for a page that is already loading to finish dialling, short enough that somebody who
34
+ * has not restarted their dev server yet is not held hostage by a wait that cannot succeed — they
35
+ * have not typed the command that would make it succeed, and the message tells them which one.
36
+ */
37
+ export declare const CONFIRM_WINDOW_MS = 12000;
38
+ export declare const CONFIRM_POLL_MS = 500;
39
+ export interface ConfirmDeps {
40
+ /** Session ids connected right now, or null when nothing is listening on the bridge port. */
41
+ listSessionIds: () => Promise<readonly string[] | null>;
42
+ now: () => number;
43
+ sleep: (ms: number) => Promise<void>;
44
+ windowMs?: number;
45
+ pollMs?: number;
46
+ }
47
+ /** The narrow slice of the init IO this needs — it prints, and nothing else. */
48
+ interface PrintIo {
49
+ print: (line: string) => void;
50
+ }
51
+ /** What `runInit` hands back; `outcome` is present only on a real (non-dry) run. */
52
+ interface ConfirmableResult {
53
+ ok: boolean;
54
+ applied: number;
55
+ manual: number;
56
+ outcome?: InitOutcome;
57
+ }
58
+ /**
59
+ * Poll until an app connects or the window runs out.
60
+ *
61
+ * A session that was ALREADY there when the wait began does not count, and this is the whole reason
62
+ * the check reads ids rather than a count. A daemon is shared across every project on the machine,
63
+ * so "a session exists" is satisfied by another app, by a stray tab, by yesterday's work — and
64
+ * confirming this install on somebody else's session is a false green in the one message a user
65
+ * reads to decide whether they are done. `reticle open` refuses the same way, for the same reason.
66
+ *
67
+ * The conservative direction is deliberate: an app that reconnects under an id it already held is
68
+ * reported as not-yet-connected, and the message it gets sends them to `status`, which shows it. A
69
+ * missed confirmation costs one command; a false one costs the whole point of confirming.
70
+ */
71
+ export declare function confirmAppConnected(deps: ConfirmDeps): Promise<InitConfirmation>;
72
+ /**
73
+ * What the user reads, in the marks the rest of the report already uses: `⚠` means work left to do
74
+ * and nothing else, `ℹ` is worth reading but is not work, `✓` happened.
75
+ *
76
+ * Only NO_SESSION earns a `⚠`, and it earns it honestly: the app is wired and has not connected, so
77
+ * there is a command outstanding. A missing daemon is not a defect in the install — the daemon comes
78
+ * up when the agent loads the tools — so it is a notice.
79
+ */
80
+ export declare function confirmationMessage(confirmation: InitConfirmation, port: number): string;
81
+ /**
82
+ * What a non-TTY run says instead of waiting.
83
+ *
84
+ * `init` deliberately does not block when nobody is at the terminal — a 12-second wait in a script
85
+ * is wrong, and an agent driving `init` through a shell is never a TTY. But silence was the wrong
86
+ * other half of that decision: the agent path IS the prescribed path (every skill and every README
87
+ * block tells an agent to run exactly this command), so the one message joining "init finished" to
88
+ * "an app connected" was withheld from most of the people who run it. They were left with files on
89
+ * disk, an exit code of 0, and nothing saying the page half had not happened.
90
+ *
91
+ * It claims nothing it did not check. It did not look for a session, so it does not say whether one
92
+ * exists — a one-shot look would be worse than none, because a daemon is shared across every project
93
+ * on the machine and "a session exists" is satisfied by somebody else's app, which is the false
94
+ * green `confirmAppConnected` refuses by design.
95
+ */
96
+ export declare function unwatchedMessage(port: number): string;
97
+ /**
98
+ * Finish `init` on evidence rather than on a write.
99
+ *
100
+ * The telemetry report happens HERE rather than inside `runInit`, so the one `init_completed` event
101
+ * carries what was seen. Emitting inside `runInit` and again after the wait would double-count the
102
+ * funnel it exists to measure, and a second event kind for the same command would need joining back
103
+ * to the first before anybody could read either.
104
+ */
105
+ export declare function confirmInstall(result: ConfirmableResult, io: PrintIo, deps: ConfirmDeps & {
106
+ interactive: boolean;
107
+ port: number;
108
+ }, report?: (outcome: InitOutcome) => void): Promise<void>;
109
+ /** The real deps: the daemon's own status endpoint, and whether a human is watching. */
110
+ export declare function nodeConfirmDeps(port?: number): ConfirmDeps & {
111
+ interactive: boolean;
112
+ port: number;
113
+ };
114
+ export {};
@@ -0,0 +1,165 @@
1
+ /**
2
+ * The step `init` never took: stay and watch for an app to connect.
3
+ *
4
+ * The install has two halves. Registering the MCP server gives the agent the tools; getting the SDK
5
+ * into a running page gives it something to look at. Almost everyone finishes the first, and nothing
6
+ * joined "init finished" to "an app connected" — so `init` exited 0 having written files, and the
7
+ * person left with a config file and no instrumented page was never told (#269).
8
+ *
9
+ * The capability existed in three places and was used together by none of them: the poll behind
10
+ * `reticle open`, the install gate (which calls this exact check "the only step that proves the
11
+ * install works"), and the event that measures whether it ever happened.
12
+ *
13
+ * WHY NO FLAG, and no dev-server management:
14
+ *
15
+ * - No flag, because the people this is for are the ones who do not know to ask. `--verify` would
16
+ * be typed by the users who already run `status` afterwards, which is exactly the cohort that is
17
+ * not being lost. The gate instead is whether a HUMAN IS AT THE TERMINAL: `init` waits when
18
+ * stdout is a TTY and skips the wait entirely otherwise, so a scripted or agent-driven `init`
19
+ * keeps today's cost and today's exit behaviour, and blocking never happens where blocking is
20
+ * wrong. The signal is the standard "am I being piped" one and needs no argument parsing, which
21
+ * also keeps `init`'s documented flag surface where it is.
22
+ * - It never starts anything. A dev server started by a long-lived daemon is invisible to the
23
+ * person whose machine it runs on and orphans when the daemon exits, so the daemon stays out of
24
+ * it — the AGENT starts one, under the guards in agent-rules.ts, because it is attributable and
25
+ * stoppable there. So the honest shape here is: confirm the app if it connects, otherwise name
26
+ * the ONE command that is outstanding and the one that proves it worked.
27
+ */
28
+ import { InitConfirmation, RETICLE_DEFAULT_PORT } from '@reticlehq/core';
29
+ import { fetchStatus, summarizeStatus } from '../cli/cli-launch.js';
30
+ import { reportInitOutcome } from '../telemetry/init-telemetry.js';
31
+ /**
32
+ * How long a human is asked to wait.
33
+ *
34
+ * Long enough for a page that is already loading to finish dialling, short enough that somebody who
35
+ * has not restarted their dev server yet is not held hostage by a wait that cannot succeed — they
36
+ * have not typed the command that would make it succeed, and the message tells them which one.
37
+ */
38
+ export const CONFIRM_WINDOW_MS = 12_000;
39
+ export const CONFIRM_POLL_MS = 500;
40
+ /**
41
+ * Poll until an app connects or the window runs out.
42
+ *
43
+ * A session that was ALREADY there when the wait began does not count, and this is the whole reason
44
+ * the check reads ids rather than a count. A daemon is shared across every project on the machine,
45
+ * so "a session exists" is satisfied by another app, by a stray tab, by yesterday's work — and
46
+ * confirming this install on somebody else's session is a false green in the one message a user
47
+ * reads to decide whether they are done. `reticle open` refuses the same way, for the same reason.
48
+ *
49
+ * The conservative direction is deliberate: an app that reconnects under an id it already held is
50
+ * reported as not-yet-connected, and the message it gets sends them to `status`, which shows it. A
51
+ * missed confirmation costs one command; a false one costs the whole point of confirming.
52
+ */
53
+ export async function confirmAppConnected(deps) {
54
+ const deadline = deps.now() + (deps.windowMs ?? CONFIRM_WINDOW_MS);
55
+ let before;
56
+ for (;;) {
57
+ const ids = await deps.listSessionIds();
58
+ // A port nobody is listening on cannot receive a session, and no amount of waiting changes that.
59
+ // One look settles it; polling on would spend the whole window to re-learn the first answer.
60
+ if (null === ids)
61
+ return InitConfirmation.NO_DAEMON;
62
+ const baseline = before;
63
+ if (baseline === undefined)
64
+ before = new Set(ids);
65
+ else if (ids.some((id) => !baseline.has(id)))
66
+ return InitConfirmation.CONNECTED;
67
+ if (deadline <= deps.now())
68
+ return InitConfirmation.NO_SESSION;
69
+ await deps.sleep(deps.pollMs ?? CONFIRM_POLL_MS);
70
+ }
71
+ }
72
+ const PROVE_COMMAND = '`npx @reticlehq/server status`';
73
+ /**
74
+ * What the user reads, in the marks the rest of the report already uses: `⚠` means work left to do
75
+ * and nothing else, `ℹ` is worth reading but is not work, `✓` happened.
76
+ *
77
+ * Only NO_SESSION earns a `⚠`, and it earns it honestly: the app is wired and has not connected, so
78
+ * there is a command outstanding. A missing daemon is not a defect in the install — the daemon comes
79
+ * up when the agent loads the tools — so it is a notice.
80
+ */
81
+ export function confirmationMessage(confirmation, port) {
82
+ if (confirmation === InitConfirmation.CONNECTED) {
83
+ return (' [✓] an app is connected — that is the install finished, not just written.\n' +
84
+ ' Ask your agent to drive a flow.');
85
+ }
86
+ if (confirmation === InitConfirmation.NO_DAEMON) {
87
+ return (` [ℹ] nothing is listening on port ${String(port)} yet, so no app could have connected and ` +
88
+ 'this run did not wait for one.\n' +
89
+ ' The daemon starts on its own when your agent loads the Reticle tools. After that, ' +
90
+ `${PROVE_COMMAND} confirms the app.`);
91
+ }
92
+ return (' [⚠] no app has connected yet — the files are written, the page half of the install is not ' +
93
+ 'done.\n' +
94
+ ' Restart your dev server and load the app in a browser — or ask your agent to, which it is ' +
95
+ 'now told to do for you, in the background and with the command from your own scripts.\n' +
96
+ ` ${PROVE_COMMAND} then confirms it, or says exactly why it has not connected.`);
97
+ }
98
+ /**
99
+ * What a non-TTY run says instead of waiting.
100
+ *
101
+ * `init` deliberately does not block when nobody is at the terminal — a 12-second wait in a script
102
+ * is wrong, and an agent driving `init` through a shell is never a TTY. But silence was the wrong
103
+ * other half of that decision: the agent path IS the prescribed path (every skill and every README
104
+ * block tells an agent to run exactly this command), so the one message joining "init finished" to
105
+ * "an app connected" was withheld from most of the people who run it. They were left with files on
106
+ * disk, an exit code of 0, and nothing saying the page half had not happened.
107
+ *
108
+ * It claims nothing it did not check. It did not look for a session, so it does not say whether one
109
+ * exists — a one-shot look would be worse than none, because a daemon is shared across every project
110
+ * on the machine and "a session exists" is satisfied by somebody else's app, which is the false
111
+ * green `confirmAppConnected` refuses by design.
112
+ */
113
+ export function unwatchedMessage(port) {
114
+ return (' [ℹ] the files are written. An app CONNECTING is what finishes the install, and this run did ' +
115
+ 'not wait to see one.\n' +
116
+ ' Start the dev server, load the app in a browser, then ' +
117
+ `${PROVE_COMMAND} — it confirms the app, or says exactly why it has not connected.\n` +
118
+ ` Bridge port ${String(port)}.`);
119
+ }
120
+ const WATCHING = (windowMs) => ` watching for an app to connect (up to ${String(Math.round(windowMs / 1000))}s) — Ctrl-C is safe, ` +
121
+ 'everything above is already written.';
122
+ /**
123
+ * Finish `init` on evidence rather than on a write.
124
+ *
125
+ * The telemetry report happens HERE rather than inside `runInit`, so the one `init_completed` event
126
+ * carries what was seen. Emitting inside `runInit` and again after the wait would double-count the
127
+ * funnel it exists to measure, and a second event kind for the same command would need joining back
128
+ * to the first before anybody could read either.
129
+ */
130
+ export async function confirmInstall(result, io, deps, report = reportInitOutcome) {
131
+ const outcome = result.outcome;
132
+ // No outcome means a dry run or an exit that already reported for itself (no package.json). A
133
+ // preview is not an install and must not land in the funnel as either a success or a failure.
134
+ if (outcome === undefined)
135
+ return;
136
+ if (!deps.interactive) {
137
+ // Still no waiting — see unwatchedMessage for why it speaks anyway.
138
+ io.print('');
139
+ io.print(unwatchedMessage(deps.port));
140
+ report(outcome);
141
+ return;
142
+ }
143
+ io.print('');
144
+ io.print(WATCHING(deps.windowMs ?? CONFIRM_WINDOW_MS));
145
+ const confirmation = await confirmAppConnected(deps);
146
+ io.print(confirmationMessage(confirmation, deps.port));
147
+ report({ ...outcome, confirmation });
148
+ }
149
+ /** The real deps: the daemon's own status endpoint, and whether a human is watching. */
150
+ export function nodeConfirmDeps(port = RETICLE_DEFAULT_PORT) {
151
+ return {
152
+ listSessionIds: async () => {
153
+ const payload = await fetchStatus(port);
154
+ // `fetchStatus` resolves undefined when nothing answered, which is precisely the "no daemon"
155
+ // case — a zero-session daemon and an absent one need opposite messages.
156
+ if (payload === undefined)
157
+ return null;
158
+ return summarizeStatus(payload).sessions.map((s) => s.sessionId);
159
+ },
160
+ now: () => Date.now(),
161
+ sleep: (ms) => new Promise((resolve) => setTimeout(resolve, ms)),
162
+ interactive: true === process.stdout.isTTY,
163
+ port,
164
+ };
165
+ }
@@ -0,0 +1,2 @@
1
+ /** True when this step is what makes the app dial the daemon. */
2
+ export declare function isConnectStep(title: string): boolean;
@@ -0,0 +1,50 @@
1
+ /**
2
+ * Which `init` steps decide whether the app can dial the daemon.
3
+ *
4
+ * Lifted out of `plan.ts` when that file reached the size backstop. A cohesive unit on its own
5
+ * terms: one question, asked of a step title, with one consequence — a manual step in this set makes
6
+ * `init` exit non-zero instead of reporting a success over an app that can never connect.
7
+ *
8
+ * Matched on TITLES, which is the fragile part and worth stating: renaming a step in
9
+ * `plan-framework.ts` silently drops it out of this set and nothing goes red. `connect-steps.test.ts`
10
+ * pins the membership for exactly that reason.
11
+ */
12
+ /**
13
+ * Titles of the steps WITHOUT which no session ever appears.
14
+ *
15
+ * A ⚠ on one of these is not a warning, it is a guaranteed failure: nothing performs the manual step,
16
+ * so the app will not connect and every Reticle tool will answer "no browser session connected".
17
+ * Reported from a field sweep, where the ⚠ count and "did it connect" were treated as independent
18
+ * signals and are not.
19
+ */
20
+ const CONNECT_STEP_TITLES = new Set([
21
+ 'Connect snippet',
22
+ 'Connect snippet (CRA)',
23
+ 'Connect snippet (Astro)',
24
+ 'Connect snippet (Nuxt)',
25
+ 'Reticle client hook',
26
+ 'Reticle connect module',
27
+ 'ReticleDev component',
28
+ // Writing the component and MOUNTING it are two steps, and only the write was here. A root layout
29
+ // whose shape `init` does not recognise leaves the component on disk and never rendered: the SDK
30
+ // is in the project, nothing imports it, and `init` exited 0 over an app that could not connect.
31
+ 'Mount ReticleDev',
32
+ // NOT here, and the reason is worth keeping: 'Pairing token'.
33
+ //
34
+ // It is a genuine connect step — CRA inlines only REACT_APP_*, so without the token in the env
35
+ // file the bridge refuses every connection and the app boots, looks correct, and never pairs. But
36
+ // it goes MANUAL in exactly one situation: no daemon has ever run on this machine, so there is no
37
+ // token to inline. That is the FIRST CRA install on a fresh machine, i.e. the first-time user —
38
+ // and making that exit non-zero reports a broken install to the one person least able to tell
39
+ // that it is not.
40
+ //
41
+ // The real fix is for `init` to mint the token rather than only read it, which is what both the
42
+ // daemon and the Vite plugin already do. `runInit` is synchronous and the existing helpers are
43
+ // not, and `gate:install` scaffolds no CRA app, so that change would ship with no coverage of the
44
+ // path it changes. It is worth doing, and worth doing with a scaffold behind it.
45
+ 'Vite plugin',
46
+ ]);
47
+ /** True when this step is what makes the app dial the daemon. */
48
+ export function isConnectStep(title) {
49
+ return CONNECT_STEP_TITLES.has(title);
50
+ }
package/dist/init/cra.js CHANGED
@@ -12,6 +12,7 @@
12
12
  * `REACT_APP_`. So it travels through `.env.development.local`, which is CRA's own documented
13
13
  * mechanism and is gitignored by CRA's own template.
14
14
  */
15
+ import { CLI } from './agent-rules.js';
15
16
  /** The line added to `src/index.tsx`. Side-effect import: the module guards itself on NODE_ENV. */
16
17
  export const CRA_DEV_MODULE_IMPORT = "import './reticle-dev';";
17
18
  export const CRA_DEV_MODULE_PATH = 'src/reticle-dev.ts';
@@ -30,7 +31,7 @@ export const TOKEN_VAR = 'REACT_APP_RETICLE_TOKEN';
30
31
  */
31
32
  export const CRA_TOKEN_MISSING_NOTE = `${TOKEN_VAR} is not set, so Reticle cannot pair with the daemon. ` +
32
33
  `The pairing token is per-machine and ${CRA_ENV_PATH} is gitignored by CRA's template, ` +
33
- 'so it does not survive a clone. Run `npx reticle init` in this project to write it for this machine.';
34
+ `so it does not survive a clone. Run \`${CLI} init\` in this project to write it for this machine.`;
34
35
  /**
35
36
  * Said as its own NOTICE line, beside the write — not inside it.
36
37
  *
@@ -46,7 +47,7 @@ export const CRA_TOKEN_MISSING_NOTE = `${TOKEN_VAR} is not set, so Reticle canno
46
47
  export const CRA_TOKEN_PER_MACHINE_NOTICE = `${CRA_ENV_PATH} is in CRA's own .gitignore, so the token just written does not travel. ` +
47
48
  'It works on THIS machine and nowhere else: a teammate cloning the repo, CI, or a container ' +
48
49
  'gets an app that boots, never pairs, and reports it only in the browser console. ' +
49
- 'Each developer runs `npx reticle init` once locally.';
50
+ `Each developer runs \`${CLI} init\` once locally.`;
50
51
  /** Add the import after the last existing one, or null when it is already present. */
51
52
  export function craImportPatch(source) {
52
53
  if (source.includes(CRA_DEV_MODULE_IMPORT))
@@ -0,0 +1,38 @@
1
+ /**
2
+ * Does this app's Content-Security-Policy let the browser reach the Reticle bridge?
3
+ *
4
+ * Two independent field reports, both Next: `init` reported every step successful, the SDK mounted,
5
+ * the dial URL was correct, and the app never connected because a strict `connect-src` excluded
6
+ * `ws://localhost:<port>`. The browser blocks the WebSocket and says so in its own console, which
7
+ * nothing on the Reticle side reads — so `status` and `doctor` reported perfect health at an app
8
+ * that could not connect at all, forever. A setup step that exits 0 while leaving the user broken is
9
+ * the worst failure mode this repo has.
10
+ *
11
+ * A TEXT SCAN, deliberately. `headers()` is a function; `init` cannot execute a user's Next config
12
+ * without importing their whole app, and the thing worth finding is the policy string a developer
13
+ * typed — in `next.config.*`, in middleware, or in a `<meta http-equiv>` tag. The scan finds the
14
+ * string wherever it is written.
15
+ *
16
+ * NARROW on purpose: it fires only when a `connect-src` exists AND does not admit the bridge. No
17
+ * CSP, or one that already covers us, produces nothing. A warning that fires on a working setup
18
+ * costs precisely what a green check on a broken one costs, and this file exists because of the
19
+ * second kind.
20
+ */
21
+ /**
22
+ * The problem with this app's `connect-src`, or undefined if there is not one.
23
+ *
24
+ * `https:` counts as a wildcard here only in the sense that a policy written that loosely will not
25
+ * be the thing blocking a dev WebSocket; the check stays quiet rather than arguing about it.
26
+ */
27
+ export declare function cspConnectSrcProblem(text: string, port: number): string | undefined;
28
+ /** The exact addition to paste. Text to copy, not advice to interpret. */
29
+ export declare function devCspAddition(port: number): string;
30
+ /** The step title, named here so `doctor` and the plan cannot drift apart on what this check is called. */
31
+ export declare const CSP_STEP_TITLE = "Content-Security-Policy";
32
+ /**
33
+ * The first blocking policy among the sources `init` already has in hand, or undefined.
34
+ *
35
+ * At most one finding: a second copy of the same policy in a second file is the same problem, and
36
+ * two identical notices read as two problems.
37
+ */
38
+ export declare function cspPlanProblem(sources: readonly (string | null | undefined)[], port: number): string | undefined;
@@ -0,0 +1,92 @@
1
+ /**
2
+ * Does this app's Content-Security-Policy let the browser reach the Reticle bridge?
3
+ *
4
+ * Two independent field reports, both Next: `init` reported every step successful, the SDK mounted,
5
+ * the dial URL was correct, and the app never connected because a strict `connect-src` excluded
6
+ * `ws://localhost:<port>`. The browser blocks the WebSocket and says so in its own console, which
7
+ * nothing on the Reticle side reads — so `status` and `doctor` reported perfect health at an app
8
+ * that could not connect at all, forever. A setup step that exits 0 while leaving the user broken is
9
+ * the worst failure mode this repo has.
10
+ *
11
+ * A TEXT SCAN, deliberately. `headers()` is a function; `init` cannot execute a user's Next config
12
+ * without importing their whole app, and the thing worth finding is the policy string a developer
13
+ * typed — in `next.config.*`, in middleware, or in a `<meta http-equiv>` tag. The scan finds the
14
+ * string wherever it is written.
15
+ *
16
+ * NARROW on purpose: it fires only when a `connect-src` exists AND does not admit the bridge. No
17
+ * CSP, or one that already covers us, produces nothing. A warning that fires on a working setup
18
+ * costs precisely what a green check on a broken one costs, and this file exists because of the
19
+ * second kind.
20
+ */
21
+ /** Sources that admit any ws origin, so the bridge is already reachable. */
22
+ const WILDCARDS = ['*', 'ws:', 'wss:', 'ws://*', 'https:'];
23
+ /** The two hosts the SDK may dial. `localhost` and the IPv4 loopback are different CSP sources. */
24
+ function bridgeOrigins(port) {
25
+ return [`ws://localhost:${String(port)}`, `ws://127.0.0.1:${String(port)}`];
26
+ }
27
+ /**
28
+ * The `connect-src` directive's source list, or undefined when the text declares none.
29
+ *
30
+ * Matches to the end of the directive (`;`) or the end of the enclosing quoted string, which is how
31
+ * these are written in both a Next `headers()` value and a `<meta content="...">`.
32
+ */
33
+ function connectSrcSources(text) {
34
+ // Stops at the directive separator or the closing double quote of the enclosing string. Single
35
+ // quotes are NOT terminators: `'self'` is a source, not the end of the list.
36
+ const match = /connect-src([^;"]*)/i.exec(text);
37
+ const list = match?.[1];
38
+ if (list === undefined)
39
+ return undefined;
40
+ return list
41
+ .split(/\s+/)
42
+ .map((source) => source.trim())
43
+ .filter((source) => source.length > 0);
44
+ }
45
+ /**
46
+ * The problem with this app's `connect-src`, or undefined if there is not one.
47
+ *
48
+ * `https:` counts as a wildcard here only in the sense that a policy written that loosely will not
49
+ * be the thing blocking a dev WebSocket; the check stays quiet rather than arguing about it.
50
+ */
51
+ export function cspConnectSrcProblem(text, port) {
52
+ const sources = connectSrcSources(text);
53
+ if (sources === undefined || 0 === sources.length)
54
+ return undefined;
55
+ if (sources.some((source) => WILDCARDS.includes(source)))
56
+ return undefined;
57
+ const wanted = bridgeOrigins(port);
58
+ // BOTH, not either: the SDK picks its host from how the page was served, and a policy that admits
59
+ // one is a coin flip. A coin flip that fails is indistinguishable from every other silent
60
+ // non-connect, which is the whole cost being avoided here.
61
+ if (wanted.every((origin) => sources.includes(origin)))
62
+ return undefined;
63
+ const missing = wanted.filter((origin) => !sources.includes(origin));
64
+ return (`this app declares a Content-Security-Policy whose \`connect-src\` does not admit the Reticle ` +
65
+ `bridge: ${missing.join(' and ')} ${1 === missing.length ? 'is' : 'are'} missing. The browser ` +
66
+ `will block the WebSocket and report it in ITS console only — every check on the Reticle side ` +
67
+ `will pass while the app never connects. ${devCspAddition(port)}`);
68
+ }
69
+ /** The exact addition to paste. Text to copy, not advice to interpret. */
70
+ export function devCspAddition(port) {
71
+ return (`Add to \`connect-src\` in development only (e.g. behind ` +
72
+ `\`process.env.NODE_ENV === 'development'\`): ` +
73
+ `${bridgeOrigins(port).join(' ')}`);
74
+ }
75
+ /** The step title, named here so `doctor` and the plan cannot drift apart on what this check is called. */
76
+ export const CSP_STEP_TITLE = 'Content-Security-Policy';
77
+ /**
78
+ * The first blocking policy among the sources `init` already has in hand, or undefined.
79
+ *
80
+ * At most one finding: a second copy of the same policy in a second file is the same problem, and
81
+ * two identical notices read as two problems.
82
+ */
83
+ export function cspPlanProblem(sources, port) {
84
+ for (const source of sources) {
85
+ if ('string' !== typeof source)
86
+ continue;
87
+ const problem = cspConnectSrcProblem(source, port);
88
+ if (problem !== undefined)
89
+ return problem;
90
+ }
91
+ return undefined;
92
+ }
@@ -0,0 +1,25 @@
1
+ /**
2
+ * `doctor`'s web CSP check — the sibling of the desktop one next door.
3
+ *
4
+ * The desktop findings exist because a Tauri app with the default CSP runs perfectly and never
5
+ * connects. The WEB version of that failure had no check at all, and it arrived twice in one batch
6
+ * of field reports: two Next apps where `init` printed success for every step, the SDK mounted, the
7
+ * dial URL was right, and the browser silently refused the WebSocket because `connect-src` excluded
8
+ * the bridge. The violation is reported in the browser's console and nowhere else, so every check on
9
+ * this side stayed green while the app was permanently unreachable.
10
+ *
11
+ * Reads the files a CSP is actually written in, by name — a full source scan would be slower and
12
+ * would find policy strings in test fixtures and documentation.
13
+ */
14
+ /** Read a project-relative file, or undefined when it is absent/unreadable. */
15
+ type ReadFile = (relative: string) => string | undefined;
16
+ export interface CspDiagnosis {
17
+ /** Which file to look at. */
18
+ file: string;
19
+ /** What is wrong, in one line. */
20
+ problem: string;
21
+ /** Exactly what to add — copy-pasteable, with the port actually in use. */
22
+ fix: string;
23
+ }
24
+ export declare function diagnoseWebCsp(read: ReadFile, port: number): CspDiagnosis[];
25
+ export {};
@@ -0,0 +1,49 @@
1
+ /**
2
+ * `doctor`'s web CSP check — the sibling of the desktop one next door.
3
+ *
4
+ * The desktop findings exist because a Tauri app with the default CSP runs perfectly and never
5
+ * connects. The WEB version of that failure had no check at all, and it arrived twice in one batch
6
+ * of field reports: two Next apps where `init` printed success for every step, the SDK mounted, the
7
+ * dial URL was right, and the browser silently refused the WebSocket because `connect-src` excluded
8
+ * the bridge. The violation is reported in the browser's console and nowhere else, so every check on
9
+ * this side stayed green while the app was permanently unreachable.
10
+ *
11
+ * Reads the files a CSP is actually written in, by name — a full source scan would be slower and
12
+ * would find policy strings in test fixtures and documentation.
13
+ */
14
+ import { cspConnectSrcProblem, devCspAddition } from './csp-check.js';
15
+ /**
16
+ * The places a Content-Security-Policy gets written in a JS app.
17
+ *
18
+ * Next config and middleware cover `headers()` and the edge-middleware style; the layouts and the
19
+ * plain `index.html` cover the `<meta http-equiv>` style. Both reported cases were in this list.
20
+ */
21
+ const CSP_FILES = [
22
+ 'next.config.js',
23
+ 'next.config.mjs',
24
+ 'next.config.ts',
25
+ 'middleware.ts',
26
+ 'middleware.js',
27
+ 'src/middleware.ts',
28
+ 'app/layout.tsx',
29
+ 'app/layout.js',
30
+ 'pages/_document.tsx',
31
+ 'pages/_document.js',
32
+ 'index.html',
33
+ 'public/index.html',
34
+ 'vercel.json',
35
+ 'netlify.toml',
36
+ ];
37
+ export function diagnoseWebCsp(read, port) {
38
+ const findings = [];
39
+ for (const file of CSP_FILES) {
40
+ const source = read(file);
41
+ if (source === undefined)
42
+ continue;
43
+ const problem = cspConnectSrcProblem(source, port);
44
+ if (problem === undefined)
45
+ continue;
46
+ findings.push({ file, problem, fix: devCspAddition(port) });
47
+ }
48
+ return findings;
49
+ }
@@ -4,8 +4,7 @@
4
4
  * We add the `reticle` server once at this global path so every project picks it up. Unparseable
5
5
  * (jsonc/comment) files bail to manual rather than being rewritten.
6
6
  */
7
- import { NPX, MCP_SERVER_NAME, npxServerArgs } from './mcp.js';
8
- import { RETICLE_NPM_PACKAGE } from '../version/server-version.js';
7
+ import { NPX, MCP_SERVER_NAME, npxServerArgs, isReticleRegistration } from './mcp.js';
9
8
  /** Path of Cursor's global MCP config, relative to the user's home directory. */
10
9
  export const CURSOR_MCP_RELPATH = '.cursor/mcp.json';
11
10
  /** The directory whose presence signals Cursor is installed for this user. */
@@ -46,19 +45,17 @@ function parseConfig(existing) {
46
45
  *
47
46
  * What IS repaired is a stale entry of our own shape — an old pin, a command that has moved — which
48
47
  * presence-only idempotency reported as "already registered" and never fixed, so an upgrade could
49
- * not repair the thing an upgrade exists to repair.
48
+ * not repair the thing an upgrade exists to repair. A WRONG entry counts as ours to repair too:
49
+ * `["@reticlehq/core","mcp"]` was reported from the field, and `core` has no MCP server in it.
50
50
  */
51
51
  function leaveEntryAlone(existing) {
52
52
  if (JSON.stringify(existing) === JSON.stringify(cursorServerEntry()))
53
53
  return true;
54
54
  if (typeof existing !== 'object' || null === existing)
55
55
  return true;
56
- const command = existing.command;
57
- const args = existing.args;
58
- const ours = command === NPX &&
59
- Array.isArray(args) &&
60
- args.some((arg) => 'string' === typeof arg && arg.includes(RETICLE_NPM_PACKAGE));
61
- return !ours;
56
+ const record = existing;
57
+ const tokens = [record.command, record.args].flat().filter((v) => 'string' === typeof v);
58
+ return !isReticleRegistration(tokens);
62
59
  }
63
60
  export function mergeCursorConfig(existing) {
64
61
  const parsed = parseConfig(existing);
@@ -84,5 +84,13 @@ interface InstallCommand {
84
84
  }
85
85
  /** Build a dev-dependency install command for one or more packages (e.g. the kit + its build plugin). */
86
86
  export declare function installCommandParts(pm: PackageManager, pkgs: string | readonly string[]): InstallCommand;
87
+ /**
88
+ * The install command as a human reads it — and deliberately NOT `installCommandParts` joined.
89
+ *
90
+ * This string is what the plan prints and what a user retypes when running the step by hand, so it
91
+ * must not carry the flags we add only to quieten our own child process. Built from `INSTALL_ARGS`
92
+ * directly for that reason; routing it through the parts would put `--no-audit --no-fund` in front of
93
+ * every reader and teach them our noise-suppression as if it were part of installing Reticle.
94
+ */
87
95
  export declare function installCommand(pm: PackageManager, pkgs: string | readonly string[]): string;
88
96
  export {};