@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
@@ -2,7 +2,29 @@
2
2
  * Generated file contents and copy-paste snippets for `reticle init`. Kept as named constants/builders
3
3
  * so the runner never inlines free strings.
4
4
  */
5
- import { RETICLE_DEFAULT_PORT, bridgeWsUrl } from '@reticlehq/core';
5
+ import { RETICLE_DEFAULT_PORT, ReticleDir, bridgeWsUrl } from '@reticlehq/core';
6
+ import { UiLibrary } from './detect.js';
7
+ import { SERVER_VERSION } from '../version/server-version.js';
8
+ /**
9
+ * The SDK as one import a plain page can actually resolve.
10
+ *
11
+ * `init` used to tell static-HTML users that a page with no build step could not load the SDK, and
12
+ * to go and stand up a bundler. That is true of a BARE specifier and false of a URL, and the
13
+ * difference was the whole road for every server-rendered app we hear from: FastAPI, Flask, Django,
14
+ * Streamlit, Rails. Proven end to end before this shipped, on a page served by `python3 -m
15
+ * http.server`: a session connected, a snapshot returned, and two act_and_wait calls came back
16
+ * `verified: "yes"`.
17
+ *
18
+ * jsDelivr rather than esm.sh, measured: a third of the requests and a third of the bytes for the
19
+ * same result. `/+esm` is what makes it work, and the bare package URL does NOT: every file in
20
+ * `dist` carries bare specifiers for `@reticlehq/core` and `@testing-library/dom`, so an unbundled
21
+ * entry point dies on the first import. That is also why adding `unpkg`/`jsdelivr` fields to
22
+ * package.json would not help.
23
+ *
24
+ * PINNED to this server's version on purpose. A floating import upgrades the page SDK underneath a
25
+ * daemon that did not move, which is `version_skew` arriving by a route nothing checks.
26
+ */
27
+ const CDN_SDK_URL = `https://cdn.jsdelivr.net/npm/@reticlehq/browser@${SERVER_VERSION}/+esm`;
6
28
  /**
7
29
  * The connect argument literal: a non-default port adds a `url`, and a projectId is always passed
8
30
  * (so the app is identifiable across port changes). Empty string only when neither applies.
@@ -16,18 +38,76 @@ function connectArg(port, projectId) {
16
38
  parts.push(`projectId: '${projectId}'`);
17
39
  return parts.length > 0 ? `{ ${parts.join(', ')} }` : '';
18
40
  }
41
+ /**
42
+ * The same literal, with the pairing token folded in.
43
+ *
44
+ * The token belongs INSIDE the call the user pastes. Every other stack has a build step to inline
45
+ * it (the Vite plugin's `define`, Next's NEXT_PUBLIC_*, Astro's config, CRA's .env); the hand-wired
46
+ * paths have none, so `init` inlines the literal it already read. Without it the bridge closes the
47
+ * socket with AUTH_FAILED and no session ever appears: see Bridge's hello handler.
48
+ *
49
+ * An empty token is omitted rather than emitted as `token: ''`. A daemon that could not write to
50
+ * $HOME runs without auth and trusts loopback, and an empty string would fail the comparison against
51
+ * one that does hold a token.
52
+ */
53
+ export function connectArgWithToken(port, projectId, pairingToken) {
54
+ const base = connectArg(port, projectId);
55
+ if (pairingToken === undefined || 0 === pairingToken.length)
56
+ return base;
57
+ const inner = base.length > 0 ? base.slice(1, -1).trim() : '';
58
+ return `{ ${[inner, `token: '${pairingToken}'`].filter((p) => p.length > 0).join(', ')} }`;
59
+ }
60
+ /**
61
+ * Which SDK package the GENERATED code should import, and whether `install()` applies.
62
+ *
63
+ * This has to agree with `frameworkPackages`, and it did not. That function was changed so a Vue or
64
+ * Svelte app installs `@reticlehq/browser` instead of the React adapter — correctly — while every
65
+ * generated connect snippet still said `import('@reticlehq/react')`. A SvelteKit app would have
66
+ * installed the sensor and then run a hook importing a package that is not there.
67
+ *
68
+ * `install()` is the React adapter's, not the sensor's: `@reticlehq/browser` exports `reticle` and no
69
+ * `install`, so swapping the specifier alone would trade a missing module for a missing export.
70
+ */
71
+ export function sdkImport(uiLibrary) {
72
+ const react = uiLibrary !== UiLibrary.VUE && uiLibrary !== UiLibrary.SVELTE;
73
+ return react
74
+ ? { specifier: '@reticlehq/react', usesInstall: true }
75
+ : { specifier: '@reticlehq/browser', usesInstall: false };
76
+ }
77
+ /**
78
+ * The framework plugin to show ALONGSIDE reticle() in the example, so the ordering is clear.
79
+ *
80
+ * It used to be `react()` unconditionally, which is what a Vue app was shown — a plugin it does not
81
+ * have, four lines after `init` had correctly detected Vue and said so. The example exists to show
82
+ * that `reticle()` goes last, not to tell anyone which UI framework they are using.
83
+ */
84
+ function frameworkPluginExample(uiLibrary) {
85
+ switch (uiLibrary) {
86
+ case UiLibrary.VUE:
87
+ return 'vue()';
88
+ case UiLibrary.SVELTE:
89
+ return 'svelte()';
90
+ case UiLibrary.REACT:
91
+ case UiLibrary.PREACT:
92
+ return 'react()';
93
+ default:
94
+ // Nothing detected: name no plugin rather than invent one. The reader keeps whatever they have.
95
+ return '/* your existing plugins */';
96
+ }
97
+ }
19
98
  /** The Vite-config snippet printed when we can't safely auto-patch the config. */
20
- export function viteManual(port) {
99
+ export function viteManual(port, uiLibrary = UiLibrary.UNKNOWN) {
21
100
  const call = port === undefined ? 'reticle()' : `reticle({ port: ${String(port)} })`;
22
101
  return `Add the Reticle plugin to your Vite config:
23
102
 
24
103
  import { reticle } from '@reticlehq/vite-plugin';
25
104
 
26
105
  export default defineConfig({
27
- plugins: [react(), ${call}],
106
+ plugins: [${frameworkPluginExample(uiLibrary)}, ${call}],
28
107
  });
29
108
 
30
- The plugin only applies during \`vite\` (dev) — it is dropped from \`vite build\`.`;
109
+ Keep \`reticle()\` LAST so it sees the output of your other plugins. It only applies during \`vite\`
110
+ (dev) — it is dropped from \`vite build\`.`;
31
111
  }
32
112
  /** Next.js config wrap — always printed (we never auto-rewrite next.config). */
33
113
  export function nextConfigManual(configFile) {
@@ -45,21 +125,27 @@ export function nextConfigManual(configFile) {
45
125
  * as `NEXT_PUBLIC_RETICLE_TOKEN`. This file used to connect with only a projectId, so every Next
46
126
  * setup ended at `bridge refused the connection: authentication failed` and no session ever appeared.
47
127
  */
48
- export function nextReticleDevFile(port, projectId, testids = [], stores = []) {
128
+ export function nextReticleDevFile(port, projectId, testids = [], stores = [], found = []) {
49
129
  const base = connectArg(port, projectId);
50
130
  const fields = '' === base ? '' : `${base.slice(1, -1).trim()}, `;
51
131
  const ids = testids.map((t) => `'${t}'`).join(', ');
52
- const storeBlock = 0 === stores.length
53
- ? ' // No state library detected. If you add one, register it here — see node_modules/@reticlehq/server/docs/usage.md.'
54
- : stores.map((h) => ` // import your store, then: ${h}`).join('\n');
132
+ // Same rule as the Vite module: a store we found is registered outright, and the commented hint
133
+ // survives only for the libraries we can name but not wire.
134
+ const storeImports = found.map((s) => `import { ${s.ident} } from '${s.importPath}';`).join('\n');
135
+ const storeBlock = found.length > 0
136
+ ? found.map((s) => ` registerStore('${s.key}', ${s.ident});`).join('\n')
137
+ : 0 === stores.length
138
+ ? ' // No state library detected. If you add one, register it here — see node_modules/@reticlehq/server/docs/usage.md.'
139
+ : stores.map((h) => ` // import your store, then: ${h}`).join('\n');
55
140
  return `'use client';
56
141
  import { useEffect } from 'react';
142
+ ${storeImports.length > 0 ? storeImports : ''}
57
143
 
58
144
  /** Dev-only: connect Reticle + install the React adapter, after hydration. */
59
145
  export function ReticleDev() {
60
146
  useEffect(() => {
61
147
  if (process.env.NODE_ENV !== 'development') return;
62
- void import('@reticlehq/react').then(({ reticle, install, registerCapabilities }) => {
148
+ void import('@reticlehq/react').then(({ reticle, install, registerCapabilities${found.length > 0 ? ', registerStore' : ''} }) => {
63
149
  install();
64
150
  // Both provided by withReticle() in next.config. The bridge rejects a connect with no token;
65
151
  // the root makes source paths repo-relative instead of absolute.
@@ -73,9 +159,9 @@ export function ReticleDev() {
73
159
  // form wires \`subscribe\` too, so every mutation emits a diff; the getter form is read-only.
74
160
  ${storeBlock}
75
161
  registerCapabilities({
76
- testids: [${ids}],${0 === testids.length ? ' // none found — add data-testid to your key elements' : ''}
162
+ testids: [${ids}],${0 === testids.length ? ' // none found; add data-testid to your key elements' : ''}
77
163
  signals: [], // names you pass to reticle.signal()
78
- stores: [], // the keys you registered above
164
+ stores: [${found.map((s) => `'${s.key}'`).join(', ')}], // the keys you registered above
79
165
  });
80
166
  });
81
167
  }, []);
@@ -171,15 +257,29 @@ Start the daemon BEFORE \`astro dev\`, so the token file exists when the config
171
257
  * breaks the module everything else hangs off. So the store lines are generated COMMENTED, naming
172
258
  * the libraries actually found in package.json, with the exact call to uncomment.
173
259
  */
174
- export function viteDevModuleFile(testids, stores) {
260
+ export function viteDevModuleFile(testids, stores, found = [], uiLibrary = UiLibrary.REACT) {
261
+ // The specifier has to be the package `frameworkPackages` installed. This file is the Vite path —
262
+ // the commonest install there is — and it was hardcoded to `@reticlehq/react` while a Vue or
263
+ // Svelte app was being given `@reticlehq/browser`, so the generated file imported something that
264
+ // was not there. Caught by running `init` against a pristine Vue app, not by any gate.
265
+ //
266
+ // The sensor exports `registerCapabilities` and `registerStore` (and the store adapters) just as
267
+ // the React kit does; the only thing it lacks is `install()`, which this file never called.
268
+ const sdk = sdkImport(uiLibrary);
175
269
  const ids = testids.map((t) => `'${t}'`).join(', ');
176
- const storeBlock = 0 === stores.length
177
- ? ' // No state library detected. If you add one, register it here — see node_modules/@reticlehq/server/docs/usage.md.'
178
- : stores.map((h) => ` // import your store, then: ${h}`).join('\n');
179
- return `// Dev-only. Imported automatically by @reticlehq/vite-plugin — you do not need to import it.
270
+ // A store we FOUND is imported and registered outright — the whole point of the file. The hints
271
+ // stay only for the libraries we can name but not wire (they need an argument we cannot infer).
272
+ const storeImports = found.map((s) => `import { ${s.ident} } from '${s.importPath}';`).join('\n');
273
+ const storeBlock = found.length > 0
274
+ ? found.map((s) => ` registerStore('${s.key}', ${s.ident});`).join('\n')
275
+ : 0 === stores.length
276
+ ? ' // No state library detected. If you add one, register it here — see node_modules/@reticlehq/server/docs/usage.md.'
277
+ : stores.map((h) => ` // import your store, then: ${h}`).join('\n');
278
+ const registerImport = found.length > 0 ? 'registerCapabilities, registerStore' : 'registerCapabilities';
279
+ return `// Dev-only. Imported automatically by @reticlehq/vite-plugin, so you do not need to import it.
180
280
  // Self-guards on import.meta.env.DEV, so it is a no-op in a production build.
181
- import { registerCapabilities } from '@reticlehq/react';
182
-
281
+ import { ${registerImport} } from '${sdk.specifier}';
282
+ ${storeImports.length > 0 ? `${storeImports}\n` : ''}
183
283
  if (import.meta.env.DEV) {
184
284
  // ── Start with ONE flow. ─────────────────────────────────────────────────────────────────────
185
285
  // You do not need to describe the whole app to get value, and trying to is the slow path. Register
@@ -193,9 +293,9 @@ if (import.meta.env.DEV) {
193
293
  ${storeBlock}
194
294
 
195
295
  registerCapabilities({
196
- testids: [${ids}],${0 === testids.length ? ' // none found — add data-testid to your key elements' : ''}
296
+ testids: [${ids}],${0 === testids.length ? ' // none found; add data-testid to your key elements' : ''}
197
297
  signals: [], // names you pass to reticle.signal()
198
- stores: [], // the keys you registered above
298
+ stores: [${found.map((s) => `'${s.key}'`).join(', ')}], // the keys you registered above
199
299
  });
200
300
  }
201
301
  `;
@@ -216,17 +316,23 @@ export const NEXT_LAYOUT_MANUAL = `Mount <ReticleDev /> in your root layout (app
216
316
  * where a bare `@reticlehq/react` import resolves. A bare import in a plain index.html does NOT resolve in
217
317
  * the browser, so we never tell a bundled app to do that (the old advice silently failed for CRA).
218
318
  */
319
+ /**
320
+ * The whole install, for a page with no build step, as one block a person can paste.
321
+ *
322
+ * Extracted so the `no package.json` exit can print the SAME snippet `htmlManual` offers. That exit
323
+ * is the one every server-rendered app reaches (FastAPI, Flask, Django, Rails, Streamlit), it is
324
+ * where `init` stops, and until now it stopped with an explanation instead of an answer. A message
325
+ * that says "add the snippet below" and then prints no snippet is the same defect wearing the
326
+ * opposite sign, so the two share a builder rather than a copy.
327
+ */
328
+ export function staticPageSnippet(connectArgLiteral) {
329
+ return ` <script type="module">
330
+ import { reticle } from '${CDN_SDK_URL}';
331
+ reticle.connect(${connectArgLiteral});
332
+ </script>`;
333
+ }
219
334
  export function htmlManual(port, projectId, pairingToken) {
220
- const base = connectArg(port, projectId);
221
- // The token belongs INSIDE the call the user pastes. Every other stack has a build step to inline
222
- // it (the Vite plugin's `define`, Next's NEXT_PUBLIC_*, Astro's config, CRA's .env); this path has
223
- // none, so `init` inlines the literal it already read. Without it the bridge closes the socket
224
- // with AUTH_FAILED and no session ever appears — see Bridge's hello handler.
225
- const withToken = pairingToken === undefined || 0 === pairingToken.length
226
- ? base
227
- : `{ ${[base.length > 0 ? base.slice(1, -1).trim() : '', `token: '${pairingToken}'`]
228
- .filter((p) => p.length > 0)
229
- .join(', ')} }`;
335
+ const withToken = connectArgWithToken(port, projectId, pairingToken);
230
336
  const tokenNote = pairingToken === undefined || 0 === pairingToken.length
231
337
  ? ''
232
338
  : `\n\n The \`token\` is this machine's pairing token, read from ~/.reticle/pairing-token. Keep it: the
@@ -237,16 +343,26 @@ export function htmlManual(port, projectId, pairingToken) {
237
343
  • Bundled app (Create React App, webpack, Parcel, Vue/Svelte CLI, etc.) — add to your ENTRY module
238
344
  (e.g. src/index.js or src/main.js), where '@reticlehq/react' resolves through your bundler:
239
345
 
240
- if (window.location.hostname === 'localhost') {
346
+ if (process.env.NODE_ENV !== 'production') {
241
347
  void import('@reticlehq/react').then(({ reticle, install }) => {
242
348
  install();
243
349
  reticle.connect(${withToken});
244
350
  });
245
351
  }${tokenNote}
246
352
 
247
- • Plain static HTML with no build step — the browser can't resolve the bare '@reticlehq/react' import, so
248
- bundle the SDK once (e.g. \`npx esbuild\`) and point a dev-only <script type="module"> at the output,
249
- or serve the page through a dev server (Vite) that resolves bare imports.`;
353
+ • Plain HTML with NO build step (FastAPI, Flask, Django, Rails, Streamlit, a hand-written page) —
354
+ paste this into the page, in a template you only serve in development. There is nothing to
355
+ install: no npm, no bundler, no package.json.
356
+
357
+ ${staticPageSnippet(withToken)}
358
+
359
+ Serving the app on something other than localhost (a hosts-file alias, a LAN IP, a container, a
360
+ tunnel)? You need TWO things, not one: \`allowNonLocalhost: true\` AND a pairing token, passed as
361
+ \`token\` on the same connect. The flag alone is NOT sufficient — off localhost the SDK refuses
362
+ without a token as well, and that refusal is page-side, so the daemon sees only silence and every
363
+ \`reticle doctor\` check still passes. The token is the one in \`~/.reticle/pairing-token\` (the
364
+ build plugins read the same file). Without both, the SDK says so in the browser console only, so
365
+ from here it looks exactly like nothing happened.`;
250
366
  }
251
367
  export const NEXT_RETICLE_DEV_PATH = 'app/reticle-dev.tsx';
252
368
  export const SVELTEKIT_HOOKS_PATH = 'src/hooks.client.ts';
@@ -261,7 +377,15 @@ export const SVELTEKIT_HOOKS_PATH = 'src/hooks.client.ts';
261
377
  * here is the one thing this project exists not to do.
262
378
  */
263
379
  export function unverifiedUiLibraryNote(library) {
264
- return `Detected a ${library} app. Reticle's DOM, network, console and state tools work here, and source file:line does too — the build plugin stamps data-reticle-source regardless of UI library (measured on preact and svelte). What @reticlehq/react adds and you will NOT get is React component identity: component names and component stacks. No CI gate covers ${library}. Gated today: Vite + React, Next.js, Remix, Astro. If something doesn't work, please open an issue.`;
380
+ // Preact is not in the same position as Vue or Svelte and must not be told it is. The React
381
+ // adapter reaches Preact through `preact/compat`, which is what `docs/frameworks.mdx` has always
382
+ // said, so telling a Preact reader they get no component identity contradicts our own docs and
383
+ // talks them out of a package that is the right one for them. It is still ungated, which is the
384
+ // honest caveat, and #129 is the issue for closing that.
385
+ const identity = 'preact' === library
386
+ ? 'React component identity — component names and stacks — comes from `@reticlehq/react`, which reaches Preact through `preact/compat`. That path is not covered by a CI gate here, so treat it as expected-to-work rather than proven.'
387
+ : 'What `@reticlehq/react` adds and you will NOT get is React component identity: component names and component stacks.';
388
+ return `Detected a ${library} app. Reticle's DOM, network, console and state tools work here. ${'vue' === library ? 'Source `file:line` does NOT come through: the build plugin stamps JSX and, separately, Svelte components, and a Vue single-file component is neither — measured, a Svelte counter reports `src/lib/Counter.svelte:5` and the same drive on Vue reports no source at all.' : 'Source `file:line` does too — the build plugin stamps it for this library (measured on preact and svelte).'} ${identity} ${'vue' === library ? 'The install gate scaffolds a Vue app from scratch on every change, so this SETUP is proven; no gate drives a Vue app to a verdict, so the drive is not.' : `No CI gate covers ${library}.`} Driven on every change: Vite + React, Next.js, Remix, Astro. If something doesn't work, please open an issue.`;
265
389
  }
266
390
  export const UNVERIFIED_FRAMEWORK_NOTE = 'Reticle has no SvelteKit app and no CI gate for one, so this wiring is untested — it may work, but nothing proves it and nothing will tell us if it breaks. Supported and gated today: Vite + React, Next.js, Remix and Astro. If the hook does not register a session, please open an issue.';
267
391
  /**
@@ -269,14 +393,18 @@ export const UNVERIFIED_FRAMEWORK_NOTE = 'Reticle has no SvelteKit app and no CI
269
393
  * never triggers Vite's index.html injection (verified), so the standard plugin can't auto-connect —
270
394
  * a client hook is the reliable path. SvelteKit runs src/hooks.client.ts on the client at startup.
271
395
  */
272
- export function svelteKitHooksFile(port, projectId) {
396
+ export function svelteKitHooksFile(port, projectId, uiLibrary = UiLibrary.SVELTE) {
397
+ // SvelteKit is Svelte, so this defaults to the sensor rather than the React adapter — and the
398
+ // import here MUST match what `frameworkPackages` installed, or the hook loads a package that is
399
+ // not in node_modules. See sdkImport.
400
+ const sdk = sdkImport(uiLibrary);
273
401
  const base = connectArg(port, projectId);
274
402
  const fields = '' === base ? '' : `${base.slice(1, -1).trim()}, `;
275
403
  return `// Dev-only: connect Reticle on the client. SvelteKit renders via app.html, so the Vite-plugin
276
404
  // index.html injection doesn't fire — connect from this client hook instead.
277
405
  if (import.meta.env.DEV) {
278
- void import('@reticlehq/react').then(({ reticle, install }) => {
279
- install();
406
+ void import('${sdk.specifier}').then(({ reticle${sdk.usesInstall ? ', install' : ''} }) => {
407
+ ${sdk.usesInstall ? 'install();' : '// No React adapter here: the sensor has no install() to call.'}
280
408
  // The bridge requires the pairing token even on localhost. Nothing in a browser can read the
281
409
  // file it lives in, so @reticlehq/vite-plugin inlines it here at build time. Without it the
282
410
  // console reads "bridge refused the connection: authentication failed" and no session appears.
@@ -338,9 +466,23 @@ and no index.html to inject into. Wire it with a dev-only CLIENT plugin, which i
338
466
  2. Restart the dev server. A dev server that is already running does not pick up a new plugin —
339
467
  it will not appear in .nuxt/plugins/client.mjs, and the app will come up with no SDK at all.
340
468
 
341
- 3. If your dev host is anything other than localhost (a hosts-file alias, a LAN IP, a tunnel), add
342
- allowNonLocalhost: true to that connect call. Without it the SDK loads and then refuses, and the
343
- only sign is one line in the browser console.
469
+ 3. If your dev host is anything other than localhost (a hosts-file alias, a LAN IP, a tunnel), that
470
+ connect call needs TWO additions, not one: allowNonLocalhost: true AND a pairing token passed as
471
+ token. The flag alone is NOT sufficient off localhost. The token is the one in
472
+ ~/.reticle/pairing-token. Without both, the SDK loads and then refuses, and the only sign is one
473
+ line in the browser console.
474
+
475
+ 4. Add this to nuxt.config, so the dev server does not watch Reticle's own journal:
476
+
477
+ vite: { server: { watch: { ignored: [/(^|[\\\\/])\\.${ReticleDir.ROOT.slice(1)}([\\\\/]|$)/] } } }
478
+
479
+ Reticle journals every session into ${ReticleDir.ROOT}/ in your project root, rewriting one file
480
+ in it continuously while a session is live. Nuxt's dev server watches that root, so without this
481
+ it sees each write as a project file changing and full-reloads the page — which reconnects the
482
+ SDK, which produces the next write. The loop runs several times a second and looks like anything
483
+ except what it is: refs go stale, actions die mid-flight, and the session appears to flap.
484
+ It is a RegExp rather than a glob on purpose — chokidar dropped glob support in v4, so a
485
+ double-star pattern here is accepted and matches nothing.
344
486
 
345
487
  The package is @reticlehq/browser — the framework-neutral sensor. DOM, network, console, routing and
346
488
  source file:line all work in Vue. What you do not get is React component identity, which is the only
@@ -351,11 +493,21 @@ if something does not work, please open an issue.`;
351
493
  * Root-level project config for Reticle. Written by `reticle init`; read by `reticle mcp` for the port
352
494
  * and by tooling for the stable projectId (the app's identity across port changes).
353
495
  */
354
- export function reticleConfigContent(framework, port, projectId) {
496
+ export function reticleConfigContent(framework, port, projectId, installSource) {
355
497
  const fields = { framework };
356
498
  if (projectId !== undefined && projectId.length > 0)
357
499
  fields['projectId'] = projectId;
358
500
  if (port !== undefined && port !== RETICLE_DEFAULT_PORT)
359
501
  fields['port'] = port;
502
+ // How this install arrived, recorded HERE because it is a property of the install and the only
503
+ // moment anything knows it is the moment it happens. It reaches us as an environment variable set
504
+ // by whichever channel ran the install, and an environment variable is gone by the next command,
505
+ // so every event after this one reported `unknown` and the question "which channel actually
506
+ // converts" could not be asked at all. Written once, read for the life of the project.
507
+ //
508
+ // A closed vocabulary, narrowed before it gets here, so this can never carry a path or a URL.
509
+ if (installSource !== undefined && installSource.length > 0) {
510
+ fields['installSource'] = installSource;
511
+ }
360
512
  return `${JSON.stringify(fields, null, 2)}\n`;
361
513
  }
@@ -7,14 +7,46 @@
7
7
  import { PatchKind } from './patch-kind.js';
8
8
  export const VITE_IMPORT = "import { reticle } from '@reticlehq/vite-plugin';";
9
9
  const RETICLE_MARKER = '@reticlehq/vite-plugin';
10
- /** The `reticle...)` call — carries the bridge port so the injected connect targets it. */
10
+ /**
11
+ * The `reticle(...)` call — the bridge port so the injected connect targets it, and body capture.
12
+ *
13
+ * `captureNetworkBodies` is here rather than in the SDK's defaults, deliberately. Without it a write
14
+ * that answers 2xx with a body nobody recorded grades `unknown / outcome_unread`, because a 200
15
+ * describes the transport and not the result — so the single bug class this product exists to catch is
16
+ * unreachable on a default install. Measured on a real payments UI: a refund posted rupees into a
17
+ * paise field, the server answered 200 having refunded a hundredth of it, the page rendered the amount
18
+ * the user had typed rather than the amount that came back, and every DOM-level check passed. An agent
19
+ * asked to verify that flow had to edit the app's own vite config mid-task to see the payload, and
20
+ * then tell its human to undo the edit.
21
+ *
22
+ * Written into the USER'S config, not switched on inside the SDK, and the difference is the point. A
23
+ * body is the one part of a request that routinely carries personal data: the credential classes are
24
+ * redacted before anything is journalled — tokens, cookies, card numbers, cvv, ssn — but an address or
25
+ * an email is not, and nothing here should decide that for someone silently. In the config it is one
26
+ * visible line they can read, keep, or delete, and an SDK that updates underneath them never starts
27
+ * recording more than it did yesterday.
28
+ */
11
29
  function reticlePluginCall(port) {
12
- return port === undefined ? 'reticle()' : `reticle({ port: ${String(port)} })`;
30
+ const options = [
31
+ ...(port === undefined ? [] : [`port: ${String(port)}`]),
32
+ 'captureNetworkBodies: true',
33
+ ];
34
+ return `reticle({ ${options.join(', ')} })`;
13
35
  }
14
36
  /** Matches the start of a `plugins: [` array literal. */
15
37
  const PLUGINS_ARRAY = /plugins\s*:\s*\[/;
16
38
  /** Matches an ES import statement (used to place our import after the last one). */
17
39
  const IMPORT_LINE = /^import\s.+from\s+['"][^'"]+['"];?\s*$/gm;
40
+ /**
41
+ * The opening `{` of the exported config object — `defineConfig({`, or a bare `export default {`.
42
+ * Used only when there is no `plugins` array to extend: the object is right there, so adding the
43
+ * key is the same edit as extending the array, and bailing sent a user whose config merely set
44
+ * `server.port` to a manual paste for a change we can make correctly.
45
+ *
46
+ * A config built by a call (`defineConfig(buildOptions())`) has no literal to extend and still
47
+ * bails — the rule stays "only edit a shape we can see whole".
48
+ */
49
+ const CONFIG_OBJECT = /(export\s+default\s+(?:defineConfig\s*\(\s*)?)\{/;
18
50
  /** Alias kept so existing call sites read in Vite terms; the vocabulary is shared (see patch-kind). */
19
51
  export const VitePatchKind = PatchKind;
20
52
  const NO_PLUGINS_REASON = "couldn't find a `plugins: [...]` array to extend";
@@ -41,12 +73,28 @@ function insertPlugin(source, port) {
41
73
  return `${match}${reticlePluginCall(port)},${separator}`;
42
74
  });
43
75
  }
76
+ /**
77
+ * Add a whole `plugins: [reticle()]` key to a config object that has none, matching the layout of
78
+ * the object it lands in: a multi-line object gets its own indented line, a one-liner stays inline.
79
+ */
80
+ function insertPluginsKey(source, port) {
81
+ return source.replace(CONFIG_OBJECT, (_match, prefix, offset) => {
82
+ const rest = source.slice(offset + _match.length);
83
+ const multiline = /^\s*\n/.test(rest);
84
+ const indent = /^\s*\n(\s*)\S/.exec(rest)?.[1] ?? ' ';
85
+ const key = `plugins: [${reticlePluginCall(port)}],`;
86
+ return multiline ? `${prefix}{\n${indent}${key}` : `${prefix}{ ${key}`;
87
+ });
88
+ }
44
89
  export function patchViteConfig(source, port) {
45
90
  if (source.includes(RETICLE_MARKER)) {
46
91
  return { kind: VitePatchKind.ALREADY };
47
92
  }
48
- if (!PLUGINS_ARRAY.test(source)) {
49
- return { kind: VitePatchKind.MANUAL, reason: NO_PLUGINS_REASON };
93
+ if (PLUGINS_ARRAY.test(source)) {
94
+ return { kind: VitePatchKind.APPLY, code: insertImport(insertPlugin(source, port)) };
95
+ }
96
+ if (CONFIG_OBJECT.test(source)) {
97
+ return { kind: VitePatchKind.APPLY, code: insertImport(insertPluginsKey(source, port)) };
50
98
  }
51
- return { kind: VitePatchKind.APPLY, code: insertImport(insertPlugin(source, port)) };
99
+ return { kind: VitePatchKind.MANUAL, reason: NO_PLUGINS_REASON };
52
100
  }
@@ -1,2 +1,21 @@
1
+ import type { MockRule } from './network-mock.js';
1
2
  import type { ToolDef } from '../tools/tools.js';
3
+ /**
4
+ * Narrow the validated tool args into MockRule[], omitting undefined keys (exactOptionalPropertyTypes).
5
+ *
6
+ * THROWS on a malformed rule rather than falling back to `[]`, because `[]` is not a neutral value
7
+ * here — it is the documented way to clear every active mock. A rule with a wrong field name used to
8
+ * produce exactly the call that turns mocking OFF, and the handler then reported `applied: true`
9
+ * with `count: 0`. For a tool whose entire job is forcing an error state, that means the agent
10
+ * checks the app's failure handling against the real backend, sees the happy path, and reports that
11
+ * the error state works.
12
+ *
13
+ * Same rule as the action arguments: an argument that could not be understood is refused by name,
14
+ * never reinterpreted into a destructive default. A valueless `fill` used to wipe the field and
15
+ * report ok; it throws now, for this reason.
16
+ *
17
+ * `undefined` and `[]` still mean "clear" — those are deliberate, documented requests, not failures
18
+ * to parse.
19
+ */
20
+ export declare function toRules(value: unknown): MockRule[];
2
21
  export declare const NETWORK_MOCK_TOOLS: ToolDef[];
@@ -26,11 +26,36 @@ const ruleShape = z.object({
26
26
  .optional()
27
27
  .describe('Simulate a network failure (offline) instead of a response.'),
28
28
  });
29
- /** Narrow the validated tool args into MockRule[], omitting undefined keys (exactOptionalPropertyTypes). */
30
- function toRules(value) {
31
- const parsed = z.array(ruleShape).safeParse(value);
32
- if (!parsed.success)
29
+ /**
30
+ * Narrow the validated tool args into MockRule[], omitting undefined keys (exactOptionalPropertyTypes).
31
+ *
32
+ * THROWS on a malformed rule rather than falling back to `[]`, because `[]` is not a neutral value
33
+ * here — it is the documented way to clear every active mock. A rule with a wrong field name used to
34
+ * produce exactly the call that turns mocking OFF, and the handler then reported `applied: true`
35
+ * with `count: 0`. For a tool whose entire job is forcing an error state, that means the agent
36
+ * checks the app's failure handling against the real backend, sees the happy path, and reports that
37
+ * the error state works.
38
+ *
39
+ * Same rule as the action arguments: an argument that could not be understood is refused by name,
40
+ * never reinterpreted into a destructive default. A valueless `fill` used to wipe the field and
41
+ * report ok; it throws now, for this reason.
42
+ *
43
+ * `undefined` and `[]` still mean "clear" — those are deliberate, documented requests, not failures
44
+ * to parse.
45
+ */
46
+ export function toRules(value) {
47
+ if (value === undefined)
33
48
  return [];
49
+ const parsed = z.array(ruleShape).safeParse(value);
50
+ if (!parsed.success) {
51
+ const problem = parsed.error.issues
52
+ .map((i) => `${0 === i.path.length ? 'mocks' : `mocks.${i.path.join('.')}`}: ${i.message}`)
53
+ .join('; ');
54
+ throw new Error(`reticle_network_mock could not read \`mocks\` (${problem}). Nothing was applied and mocking ` +
55
+ 'was left unchanged — an unreadable rule is NOT treated as a request to clear. A rule is ' +
56
+ '{ urlContains, method?, status?, body?, contentType?, delayMs?, abort? }; pass `clear: true` ' +
57
+ 'or an empty array if you meant to turn mocking off.');
58
+ }
34
59
  return parsed.data.map((r) => {
35
60
  const rule = { urlContains: r.urlContains };
36
61
  if (r.method !== undefined)
@@ -56,7 +81,14 @@ export const NETWORK_MOCK_TOOLS = [
56
81
  'states without touching the backend ("verify the app handles a failed payment"). Pass `mocks` ' +
57
82
  '(first matching rule wins); pass an empty array or `clear: true` to turn mocking off.',
58
83
  inputSchema: {
59
- mocks: z.array(ruleShape).optional().describe('Interception rules; omit/empty to clear.'),
84
+ // The rule fields each carry their own `.describe()` on `ruleShape`, but the params
85
+ // view reads only the top-level description of each entry, so those are flattened
86
+ // away before an agent sees them. Naming the shape here is what keeps the tool
87
+ // callable from its own description instead of by guessing and failing first.
88
+ mocks: z
89
+ .array(ruleShape)
90
+ .optional()
91
+ .describe('Interception rules, first match wins: { urlContains, method?, status?, body?, contentType?, delayMs?, abort? }. Omit or pass [] to clear.'),
60
92
  clear: z.boolean().optional().describe('Clear all active mocks (same as mocks: []).'),
61
93
  ...sessionIdShape,
62
94
  },
@@ -28,6 +28,12 @@ export interface ScrollFindResult {
28
28
  * help). false ⇒ stopped at the maxScrolls budget (more rows may exist further down).
29
29
  */
30
30
  exhausted: boolean;
31
+ /**
32
+ * Present only when informative: the document itself did not scroll (nothing scrollable), so the
33
+ * target is likely inside a list with its own scroll container. A genuine exhaustion — reaching
34
+ * the real end of a scrolled container — stays quiet.
35
+ */
36
+ note?: string;
31
37
  }
32
38
  /**
33
39
  * Reveal an element that a windowed/virtualized list has not mounted yet. Queries
@@ -12,6 +12,21 @@ async function queryFirst(session, q) {
12
12
  return asRecord(elements[0]);
13
13
  return undefined;
14
14
  }
15
+ /**
16
+ * A note attached to `{ found: false, exhausted: true }` only when the failure is
17
+ * actually informative: the document itself had nothing to scroll (scrolled:false with
18
+ * no explicit container), so the target is likely inside a list with its own scroll
19
+ * container. Reaching the genuine end of a scrolled container stays quiet.
20
+ */
21
+ function exhaustNote(q, data) {
22
+ if (q.container !== undefined)
23
+ return {};
24
+ if (false !== data['scrolled'])
25
+ return {};
26
+ return {
27
+ note: "The document did not scroll — the target may be inside a list with its own scroll container. Pass that container's ref as `container`.",
28
+ };
29
+ }
15
30
  /**
16
31
  * Reveal an element that a windowed/virtualized list has not mounted yet. Queries
17
32
  * once (it may already be visible), then scrolls the container ~one viewport at a time, re-querying
@@ -39,7 +54,7 @@ export async function scrollToFind(session, q, opts = {}) {
39
54
  // Fall through to linear refinement from current position (already near the target).
40
55
  const data = asRecord(sr.result);
41
56
  if (true === data['atEnd'] || false === data['scrolled']) {
42
- return { found: false, scrolls, exhausted: true };
57
+ return { found: false, scrolls, exhausted: true, ...exhaustNote(q, data) };
43
58
  }
44
59
  }
45
60
  for (let i = 0; i < max; i += 1) {
@@ -51,7 +66,7 @@ export async function scrollToFind(session, q, opts = {}) {
51
66
  return { found: true, element: hit, scrolls, exhausted: false };
52
67
  // Reached the bottom or the container would not move — no more rows to reveal.
53
68
  if (true === data['atEnd'] || false === data['scrolled']) {
54
- return { found: false, scrolls, exhausted: true };
69
+ return { found: false, scrolls, exhausted: true, ...exhaustNote(q, data) };
55
70
  }
56
71
  }
57
72
  return { found: false, scrolls, exhausted: false }; // spent the budget; more may lie further down
@@ -52,6 +52,10 @@ export const SCROLL_TOOLS = [
52
52
  element: z.object({ ref: z.string(), role: z.string(), name: z.string() }).optional(),
53
53
  scrolls: z.number(),
54
54
  exhausted: z.boolean(),
55
+ note: z
56
+ .string()
57
+ .optional()
58
+ .describe('Present only when informative: the document itself did not scroll, so the target may be inside a list with its own scroll container — pass its ref as `container`.'),
55
59
  },
56
60
  handler: (deps, args) => {
57
61
  const session = deps.sessions.resolve(asString(args['sessionId']));
@@ -15,8 +15,16 @@ interface JournalAttachDeps {
15
15
  }
16
16
  /**
17
17
  * Build the per-session journal attachment the bridge fires on session creation. Off when disabled;
18
- * silently skips a session whose id is not a safe path segment (never crashes the live session over a
18
+ * skips a session whose id is not a safe path segment (never crashes the live session over a
19
19
  * journaling concern). The recorder's clock is the session's own elapsed time.
20
+ *
21
+ * That skip used to be SILENT, which is the part that mattered. The id becomes a directory name, so
22
+ * one that is not a safe path segment cannot be journalled — but the session itself connects and
23
+ * drives perfectly well, so the only observable difference is that the durable causal record for it
24
+ * does not exist. Every query that reads back through the journal then returns nothing, and nothing
25
+ * anywhere says why. An app self-assigning an unusual id gets a quietly degraded session; a hostile
26
+ * one gets the same, which is why this is worth a line either way. Nothing escapes the directory —
27
+ * the guard holds, and that is exactly why the failure is invisible rather than loud.
20
28
  */
21
29
  export declare function makeJournalAttach(deps: JournalAttachDeps): (session: JournalTarget) => void;
22
30
  export {};