@reticlehq/server 2.6.0 → 2.8.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 (276) hide show
  1. package/README.md +14 -5
  2. package/SKILL.md +251 -33
  3. package/dist/bridge/bridge.d.ts +6 -1
  4. package/dist/bridge/bridge.js +27 -4
  5. package/dist/capsule/causal-summary.d.ts +20 -0
  6. package/dist/capsule/causal-summary.js +31 -6
  7. package/dist/capsule/divergence.js +7 -1
  8. package/dist/cli/chromium-hint.d.ts +68 -0
  9. package/dist/cli/chromium-hint.js +145 -0
  10. package/dist/cli/cli-doctor.js +31 -36
  11. package/dist/cli/cli-kill.d.ts +58 -0
  12. package/dist/cli/cli-kill.js +162 -0
  13. package/dist/cli/cli-launch.d.ts +18 -0
  14. package/dist/cli/cli-launch.js +32 -1
  15. package/dist/cli/cli-parse.d.ts +16 -1
  16. package/dist/cli/cli-parse.js +32 -9
  17. package/dist/cli/cli-port.d.ts +8 -0
  18. package/dist/cli/cli-port.js +36 -1
  19. package/dist/cli/cli-verify.js +2 -2
  20. package/dist/cli/doctor-daemon-line.js +10 -1
  21. package/dist/cli/doctor-sessions-line.d.ts +12 -0
  22. package/dist/cli/doctor-sessions-line.js +66 -0
  23. package/dist/cli/port-holder.d.ts +10 -0
  24. package/dist/cli/port-holder.js +26 -2
  25. package/dist/cli.js +139 -23
  26. package/dist/crawl/crawl-tools.js +12 -2
  27. package/dist/crawl/crawl.d.ts +13 -0
  28. package/dist/crawl/crawl.js +50 -2
  29. package/dist/daemon/daemon-resilience.js +2 -2
  30. package/dist/daemon/loopback-alias.d.ts +32 -0
  31. package/dist/daemon/loopback-alias.js +80 -0
  32. package/dist/events/contradictions.d.ts +14 -0
  33. package/dist/events/contradictions.js +26 -17
  34. package/dist/events/predicate-eval.d.ts +26 -3
  35. package/dist/events/predicate-eval.js +284 -95
  36. package/dist/events/predicate-parse.d.ts +1 -1
  37. package/dist/events/predicate-parse.js +18 -4
  38. package/dist/events/predicate.js +170 -18
  39. package/dist/events/ring-buffer.d.ts +8 -0
  40. package/dist/events/ring-buffer.js +34 -1
  41. package/dist/flows/flow-classify.js +1 -1
  42. package/dist/flows/flow-tools.js +12 -4
  43. package/dist/flows/heal-run.js +1 -1
  44. package/dist/honesty/blind-spots.js +6 -2
  45. package/dist/honesty/honesty.d.ts +11 -0
  46. package/dist/honesty/honesty.js +9 -1
  47. package/dist/honesty/unsettled.d.ts +40 -0
  48. package/dist/honesty/unsettled.js +78 -0
  49. package/dist/honesty/verified.d.ts +7 -0
  50. package/dist/honesty/verified.js +27 -4
  51. package/dist/http-server.js +11 -1
  52. package/dist/index.js +29 -2
  53. package/dist/init/agent-rules.d.ts +16 -0
  54. package/dist/init/agent-rules.js +96 -17
  55. package/dist/init/astro-patch.d.ts +2 -1
  56. package/dist/init/astro-patch.js +11 -5
  57. package/dist/init/capabilities.d.ts +22 -0
  58. package/dist/init/capabilities.js +76 -0
  59. package/dist/init/confirm.d.ts +96 -0
  60. package/dist/init/confirm.js +138 -0
  61. package/dist/init/cra.js +3 -2
  62. package/dist/init/detect.d.ts +8 -0
  63. package/dist/init/detect.js +15 -0
  64. package/dist/init/non-js-project.d.ts +27 -0
  65. package/dist/init/non-js-project.js +84 -0
  66. package/dist/init/plan-framework.d.ts +9 -0
  67. package/dist/init/plan-framework.js +72 -16
  68. package/dist/init/plan.d.ts +40 -4
  69. package/dist/init/plan.js +183 -43
  70. package/dist/init/run.d.ts +24 -2
  71. package/dist/init/run.js +181 -46
  72. package/dist/init/slash-command.d.ts +1 -1
  73. package/dist/init/slash-command.js +46 -6
  74. package/dist/init/snippets.d.ts +64 -5
  75. package/dist/init/snippets.js +220 -31
  76. package/dist/init/vite-config.js +29 -3
  77. package/dist/input/network-mock-tools.d.ts +19 -0
  78. package/dist/input/network-mock-tools.js +29 -4
  79. package/dist/license/license.js +1 -1
  80. package/dist/mcp/mcp-proxy.d.ts +69 -3
  81. package/dist/mcp/mcp-proxy.js +215 -23
  82. package/dist/mcp/mcp.js +58 -7
  83. package/dist/mcp/proxy-lifecycle.d.ts +20 -1
  84. package/dist/mcp/proxy-lifecycle.js +22 -1
  85. package/dist/pool/browser-pool.d.ts +22 -2
  86. package/dist/pool/browser-pool.js +44 -4
  87. package/dist/pool/playwright-launcher.js +10 -2
  88. package/dist/project/learned-routes.d.ts +24 -0
  89. package/dist/project/learned-routes.js +63 -0
  90. package/dist/project/project-store.d.ts +6 -0
  91. package/dist/project/project-store.js +25 -1
  92. package/dist/session/live-control-tools.js +39 -2
  93. package/dist/session/live-control.d.ts +7 -9
  94. package/dist/session/live-control.js +34 -1
  95. package/dist/session/no-session-diagnosis.d.ts +23 -4
  96. package/dist/session/no-session-diagnosis.js +128 -44
  97. package/dist/session/no-session-watch.js +12 -0
  98. package/dist/session/ref-provenance.d.ts +54 -0
  99. package/dist/session/ref-provenance.js +61 -0
  100. package/dist/session/review-store.d.ts +19 -0
  101. package/dist/session/review-store.js +34 -5
  102. package/dist/session/session-lease.js +1 -1
  103. package/dist/session/session-manager.d.ts +9 -0
  104. package/dist/session/session-manager.js +11 -0
  105. package/dist/session/session.d.ts +8 -0
  106. package/dist/session/session.js +12 -0
  107. package/dist/status-payload.d.ts +13 -1
  108. package/dist/status-payload.js +6 -1
  109. package/dist/telemetry/app-instrumented.d.ts +37 -0
  110. package/dist/telemetry/app-instrumented.js +54 -0
  111. package/dist/telemetry/automation-hint.d.ts +17 -0
  112. package/dist/telemetry/automation-hint.js +71 -0
  113. package/dist/telemetry/bug-found.d.ts +0 -20
  114. package/dist/telemetry/bug-found.js +38 -21
  115. package/dist/telemetry/cli-telemetry.js +7 -1
  116. package/dist/telemetry/daemon-telemetry.d.ts +4 -4
  117. package/dist/telemetry/daemon-telemetry.js +8 -4
  118. package/dist/telemetry/feedback.js +1 -1
  119. package/dist/telemetry/init-telemetry.js +9 -1
  120. package/dist/telemetry/install-source.d.ts +36 -0
  121. package/dist/telemetry/install-source.js +40 -0
  122. package/dist/telemetry/run-telemetry.js +4 -1
  123. package/dist/telemetry/session-metrics.d.ts +3 -3
  124. package/dist/telemetry/session-metrics.js +9 -4
  125. package/dist/telemetry/telemetry.d.ts +7 -1
  126. package/dist/telemetry/telemetry.js +17 -2
  127. package/dist/telemetry/tool-refused.d.ts +19 -0
  128. package/dist/telemetry/tool-refused.js +55 -0
  129. package/dist/telemetry/verification-of.js +72 -8
  130. package/dist/temp-dir.d.ts +27 -0
  131. package/dist/temp-dir.js +46 -0
  132. package/dist/tools/act-tools.js +77 -11
  133. package/dist/tools/assert-grade.d.ts +1 -1
  134. package/dist/tools/assert-grade.js +27 -5
  135. package/dist/tools/assert-verdict.js +25 -1
  136. package/dist/tools/contract-tools.js +4 -0
  137. package/dist/tools/dynamic-tools.js +1 -1
  138. package/dist/tools/error-recovery.d.ts +10 -0
  139. package/dist/tools/error-recovery.js +70 -5
  140. package/dist/tools/feedback-invite.js +1 -1
  141. package/dist/tools/invoke-tool.js +149 -32
  142. package/dist/tools/lease-tools.d.ts +29 -1
  143. package/dist/tools/lease-tools.js +58 -5
  144. package/dist/tools/navigate-result.js +1 -1
  145. package/dist/tools/observe-tools.js +1 -1
  146. package/dist/tools/observed-nothing.js +1 -1
  147. package/dist/tools/react-grace.d.ts +48 -0
  148. package/dist/tools/react-grace.js +87 -0
  149. package/dist/tools/read-tools.js +20 -4
  150. package/dist/tools/settle-in-flight.d.ts +79 -0
  151. package/dist/tools/settle-in-flight.js +106 -0
  152. package/dist/tools/snapshot-delta.d.ts +7 -0
  153. package/dist/tools/snapshot-delta.js +130 -6
  154. package/dist/tools/tool-kit.d.ts +52 -5
  155. package/dist/tools/tool-kit.js +55 -24
  156. package/dist/tools/tool-surface.js +28 -9
  157. package/dist/tools/tools.js +38 -4
  158. package/dist/visual/capture-cleanup.d.ts +16 -0
  159. package/dist/visual/capture-cleanup.js +39 -0
  160. package/dist/visual/visual-tools.d.ts +18 -0
  161. package/dist/visual/visual-tools.js +24 -5
  162. package/docs/README.md +56 -11
  163. package/docs/actions.mdx +87 -0
  164. package/docs/agent-cheatsheet.md +102 -26
  165. package/docs/architecture.md +46 -35
  166. package/docs/benchmarks.md +76 -72
  167. package/docs/best-practices.mdx +211 -0
  168. package/docs/capabilities.mdx +387 -0
  169. package/docs/cli/affected.mdx +51 -0
  170. package/docs/cli/capsules.mdx +69 -0
  171. package/docs/cli/cloud.mdx +127 -0
  172. package/docs/cli/doctor.mdx +76 -0
  173. package/docs/cli/drive.mdx +55 -0
  174. package/docs/cli/feedback.mdx +67 -0
  175. package/docs/cli/gate.mdx +64 -0
  176. package/docs/cli/help.mdx +92 -0
  177. package/docs/cli/hunt.mdx +57 -0
  178. package/docs/cli/identify.mdx +62 -0
  179. package/docs/cli/init.mdx +94 -0
  180. package/docs/cli/kill.mdx +91 -0
  181. package/docs/cli/license.mdx +48 -0
  182. package/docs/cli/mcp.mdx +61 -0
  183. package/docs/cli/open.mdx +68 -0
  184. package/docs/cli/restart.mdx +60 -0
  185. package/docs/cli/rollback.mdx +35 -0
  186. package/docs/cli/serve.mdx +62 -0
  187. package/docs/cli/status.mdx +64 -0
  188. package/docs/cli/stop.mdx +56 -0
  189. package/docs/cli/telemetry.mdx +57 -0
  190. package/docs/cli/update.mdx +52 -0
  191. package/docs/cli/verify.mdx +57 -0
  192. package/docs/cli/version.mdx +39 -0
  193. package/docs/cli/watch.mdx +55 -0
  194. package/docs/cli.mdx +219 -0
  195. package/docs/debugging.md +13 -9
  196. package/docs/deploy-checks.md +15 -11
  197. package/docs/desktop-apps.md +49 -40
  198. package/docs/desktop.mdx +188 -0
  199. package/docs/docs.json +363 -0
  200. package/docs/enterprise.md +20 -16
  201. package/docs/faq.mdx +88 -0
  202. package/docs/fixtures.md +11 -7
  203. package/docs/flows.md +26 -22
  204. package/docs/for-agents.md +71 -0
  205. package/docs/frameworks.mdx +199 -0
  206. package/docs/gate-plan.md +45 -41
  207. package/docs/gates.md +23 -19
  208. package/docs/getting-started.md +76 -65
  209. package/docs/human-control.md +22 -18
  210. package/docs/index.mdx +101 -0
  211. package/docs/install-agentic.mdx +215 -0
  212. package/docs/install-manual.mdx +194 -0
  213. package/docs/instrumentation.mdx +232 -0
  214. package/docs/integration-patterns.md +38 -34
  215. package/docs/local-registry.md +16 -12
  216. package/docs/multi-agent-testing.md +16 -12
  217. package/docs/packages/babel-plugin.mdx +70 -0
  218. package/docs/packages/browser.mdx +111 -0
  219. package/docs/packages/core.mdx +106 -0
  220. package/docs/packages/electron.mdx +87 -0
  221. package/docs/packages/eslint-plugin.mdx +84 -0
  222. package/docs/packages/next.mdx +91 -0
  223. package/docs/packages/react.mdx +91 -0
  224. package/docs/packages/server.mdx +98 -0
  225. package/docs/packages/tauri.mdx +63 -0
  226. package/docs/packages/test.mdx +108 -0
  227. package/docs/packages/vite-plugin.mdx +96 -0
  228. package/docs/packages.mdx +217 -0
  229. package/docs/platform-integration.md +33 -26
  230. package/docs/predicates.mdx +179 -0
  231. package/docs/quickstart.mdx +211 -0
  232. package/docs/recipes.mdx +242 -0
  233. package/docs/skill-file.mdx +105 -0
  234. package/docs/state-management.mdx +197 -0
  235. package/docs/style.css +14 -0
  236. package/docs/system-map.md +47 -32
  237. package/docs/telemetry-contract.md +101 -46
  238. package/docs/telemetry-events.mdx +430 -0
  239. package/docs/telemetry.md +63 -50
  240. package/docs/testing.md +10 -6
  241. package/docs/token-efficiency.md +30 -26
  242. package/docs/tools-act-and-wait.mdx +123 -0
  243. package/docs/tools-act-sequence.mdx +120 -0
  244. package/docs/tools-act.mdx +82 -0
  245. package/docs/tools-assert.mdx +70 -0
  246. package/docs/tools-console.mdx +59 -0
  247. package/docs/tools-inspect.mdx +85 -0
  248. package/docs/tools-navigate.mdx +63 -0
  249. package/docs/tools-network.mdx +86 -0
  250. package/docs/tools-observe.mdx +89 -0
  251. package/docs/tools-overview.mdx +157 -0
  252. package/docs/tools-query.mdx +82 -0
  253. package/docs/tools-session-and-feedback.mdx +91 -0
  254. package/docs/tools-sessions.mdx +65 -0
  255. package/docs/tools-snapshot.mdx +75 -0
  256. package/docs/tools-state.mdx +81 -0
  257. package/docs/tools-tools-and-run.mdx +125 -0
  258. package/docs/tools-wait-for.mdx +66 -0
  259. package/docs/troubleshooting.mdx +193 -0
  260. package/docs/usage.md +175 -153
  261. package/docs/vs-chrome-devtools-mcp.mdx +82 -0
  262. package/docs/vs-playwright-mcp.mdx +85 -0
  263. package/docs/vs-screenshots.mdx +96 -0
  264. package/docs/what-is-reticle.mdx +63 -0
  265. package/docs/why-reticle.mdx +107 -0
  266. package/package.json +2 -2
  267. package/docs/matrix/2.5.0/claude-code-machine.json +0 -19
  268. package/docs/matrix/2.5.0/codex-machine.json +0 -20
  269. package/docs/matrix/2.5.0/cursor-machine.json +0 -22
  270. package/docs/matrix/2.5.0/gemini-machine.json +0 -22
  271. package/docs/matrix/2.5.0/opencode-machine.json +0 -22
  272. package/docs/matrix/2.5.0/vscode-machine.json +0 -22
  273. package/docs/matrix/2.5.0/windsurf-machine.json +0 -22
  274. package/docs/matrix/MATRIX.md +0 -26
  275. package/docs/matrix/README.md +0 -59
  276. package/docs/mint.json +0 -61
@@ -3,6 +3,28 @@
3
3
  * so the runner never inlines free strings.
4
4
  */
5
5
  import { RETICLE_DEFAULT_PORT, 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 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 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,23 +316,49 @@ 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
  */
219
- export function htmlManual(port, projectId) {
220
- const arg = connectArg(port, projectId);
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
+ }
334
+ export function htmlManual(port, projectId, pairingToken) {
335
+ const withToken = connectArgWithToken(port, projectId, pairingToken);
336
+ const tokenNote = pairingToken === undefined || 0 === pairingToken.length
337
+ ? ''
338
+ : `\n\n The \`token\` is this machine's pairing token, read from ~/.reticle/pairing-token. Keep it: the
339
+ bridge REJECTS a connect without it ("authentication failed") and no session appears. It is
340
+ per-machine and local-only, so do not commit it — a teammate's daemon mints their own.`;
221
341
  return `No Vite/Next plugin detected — wire the dev-only connect by hand. Pick the form for your setup:
222
342
 
223
343
  • Bundled app (Create React App, webpack, Parcel, Vue/Svelte CLI, etc.) — add to your ENTRY module
224
344
  (e.g. src/index.js or src/main.js), where '@reticlehq/react' resolves through your bundler:
225
345
 
226
- if (window.location.hostname === 'localhost') {
346
+ if (process.env.NODE_ENV !== 'production') {
227
347
  void import('@reticlehq/react').then(({ reticle, install }) => {
228
348
  install();
229
- reticle.connect(${arg});
349
+ reticle.connect(${withToken});
230
350
  });
231
- }
351
+ }${tokenNote}
352
+
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)}
232
358
 
233
- • Plain static HTML with no build step — the browser can't resolve the bare '@reticlehq/react' import, so
234
- bundle the SDK once (e.g. \`npx esbuild\`) and point a dev-only <script type="module"> at the output,
235
- or serve the page through a dev server (Vite) that resolves bare imports.`;
359
+ Serving the app on something other than localhost (a hosts-file alias, a LAN IP, a container, a
360
+ tunnel)? Add \`allowNonLocalhost: true\` to the connect. Without it the SDK refuses to dial and says
361
+ so in the browser console only, so from here it looks exactly like nothing happened.`;
236
362
  }
237
363
  export const NEXT_RETICLE_DEV_PATH = 'app/reticle-dev.tsx';
238
364
  export const SVELTEKIT_HOOKS_PATH = 'src/hooks.client.ts';
@@ -247,7 +373,15 @@ export const SVELTEKIT_HOOKS_PATH = 'src/hooks.client.ts';
247
373
  * here is the one thing this project exists not to do.
248
374
  */
249
375
  export function unverifiedUiLibraryNote(library) {
250
- 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.`;
376
+ // Preact is not in the same position as Vue or Svelte and must not be told it is. The React
377
+ // adapter reaches Preact through `preact/compat`, which is what `docs/frameworks.mdx` has always
378
+ // said, so telling a Preact reader they get no component identity contradicts our own docs and
379
+ // talks them out of a package that is the right one for them. It is still ungated, which is the
380
+ // honest caveat, and #129 is the issue for closing that.
381
+ const identity = 'preact' === library
382
+ ? '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.'
383
+ : 'What `@reticlehq/react` adds and you will NOT get is React component identity: component names and component stacks.';
384
+ 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.`;
251
385
  }
252
386
  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.';
253
387
  /**
@@ -255,14 +389,18 @@ export const UNVERIFIED_FRAMEWORK_NOTE = 'Reticle has no SvelteKit app and no CI
255
389
  * never triggers Vite's index.html injection (verified), so the standard plugin can't auto-connect —
256
390
  * a client hook is the reliable path. SvelteKit runs src/hooks.client.ts on the client at startup.
257
391
  */
258
- export function svelteKitHooksFile(port, projectId) {
392
+ export function svelteKitHooksFile(port, projectId, uiLibrary = UiLibrary.SVELTE) {
393
+ // SvelteKit is Svelte, so this defaults to the sensor rather than the React adapter — and the
394
+ // import here MUST match what `frameworkPackages` installed, or the hook loads a package that is
395
+ // not in node_modules. See sdkImport.
396
+ const sdk = sdkImport(uiLibrary);
259
397
  const base = connectArg(port, projectId);
260
398
  const fields = '' === base ? '' : `${base.slice(1, -1).trim()}, `;
261
399
  return `// Dev-only: connect Reticle on the client. SvelteKit renders via app.html, so the Vite-plugin
262
400
  // index.html injection doesn't fire — connect from this client hook instead.
263
401
  if (import.meta.env.DEV) {
264
- void import('@reticlehq/react').then(({ reticle, install }) => {
265
- install();
402
+ void import('${sdk.specifier}').then(({ reticle${sdk.usesInstall ? ', install' : ''} }) => {
403
+ ${sdk.usesInstall ? 'install();' : '// No React adapter here: the sensor has no install() to call.'}
266
404
  // The bridge requires the pairing token even on localhost. Nothing in a browser can read the
267
405
  // file it lives in, so @reticlehq/vite-plugin inlines it here at build time. Without it the
268
406
  // console reads "bridge refused the connection: authentication failed" and no session appears.
@@ -282,6 +420,57 @@ declare const __RETICLE_ROOT__: string | undefined;
282
420
  declare const __RETICLE_SDK_VERSION__: string | undefined;
283
421
  `;
284
422
  }
423
+ /** Where a Nuxt dev-only client plugin belongs. `.client` keeps it out of SSR; Nuxt auto-registers it. */
424
+ export const NUXT_PLUGIN_PATH = 'app/plugins/reticle.client.ts';
425
+ /**
426
+ * The Nuxt recipe, written out in full because every trap in it is one somebody actually hit.
427
+ *
428
+ * Reported from the field, in the order they were hit: `init` classified a Nuxt 4 app as `html`, so
429
+ * it installed a package named `@reticlehq/react` (with `react` in its peer dependencies) into a Vue
430
+ * codebase — the reporter only continued after auditing our dist to confirm there are no React
431
+ * imports at runtime, which most people will not do. It then handed over a snippet guarded on
432
+ * `window.location.hostname === 'localhost'`, which fails twice over in Nuxt: `window` does not
433
+ * exist during SSR, and the dev host here was a hosts-file alias (required for the backend's
434
+ * white-label origin detection), so the guard was false and the connect never ran — no error, no log
435
+ * line, nothing to debug. And nothing said a running dev server does not pick up a new plugin.
436
+ *
437
+ * So: `import.meta.dev` (build-time, host-independent) instead of a hostname check, `.client.ts`
438
+ * instead of an SSR guard, the framework-neutral sensor instead of the React kit, the non-localhost
439
+ * flag named up front, and the restart said out loud.
440
+ */
441
+ export function nuxtManual(port, projectId) {
442
+ const base = connectArg(port, projectId);
443
+ const fields = '' === base ? '' : base.slice(1, -1).trim();
444
+ const connect = '' === fields ? 'reticle.connect()' : `reticle.connect({ ${fields} })`;
445
+ return `Nuxt owns its own Vite instance and renders its own HTML, so there is no vite.config to patch
446
+ and no index.html to inject into. Wire it with a dev-only CLIENT plugin, which is Nuxt's own idiom:
447
+
448
+ 1. Create ${NUXT_PLUGIN_PATH}:
449
+
450
+ export default defineNuxtPlugin(() => {
451
+ // import.meta.dev is the correct guard: it is resolved at build time, so it does not care
452
+ // what hostname you develop on. Do NOT guard on window.location.hostname === 'localhost' —
453
+ // that is false on any hosts-file alias or LAN address, and window does not exist in SSR.
454
+ if (!import.meta.dev) return
455
+ void import('@reticlehq/browser').then(({ reticle }) => {
456
+ ${connect}
457
+ })
458
+ })
459
+
460
+ The .client.ts suffix is load-bearing: it is what keeps this out of the server bundle.
461
+
462
+ 2. Restart the dev server. A dev server that is already running does not pick up a new plugin —
463
+ it will not appear in .nuxt/plugins/client.mjs, and the app will come up with no SDK at all.
464
+
465
+ 3. If your dev host is anything other than localhost (a hosts-file alias, a LAN IP, a tunnel), add
466
+ allowNonLocalhost: true to that connect call. Without it the SDK loads and then refuses, and the
467
+ only sign is one line in the browser console.
468
+
469
+ The package is @reticlehq/browser — the framework-neutral sensor. DOM, network, console, routing and
470
+ source file:line all work in Vue. What you do not get is React component identity, which is the only
471
+ thing the React adapter adds. There is no Nuxt app in this project's CI, so this path is UNVERIFIED:
472
+ if something does not work, please open an issue.`;
473
+ }
285
474
  /**
286
475
  * Root-level project config for Reticle. Written by `reticle init`; read by `reticle mcp` for the port
287
476
  * and by tooling for the stable projectId (the app's identity across port changes).
@@ -15,6 +15,16 @@ function reticlePluginCall(port) {
15
15
  const PLUGINS_ARRAY = /plugins\s*:\s*\[/;
16
16
  /** Matches an ES import statement (used to place our import after the last one). */
17
17
  const IMPORT_LINE = /^import\s.+from\s+['"][^'"]+['"];?\s*$/gm;
18
+ /**
19
+ * The opening `{` of the exported config object — `defineConfig({`, or a bare `export default {`.
20
+ * Used only when there is no `plugins` array to extend: the object is right there, so adding the
21
+ * key is the same edit as extending the array, and bailing sent a user whose config merely set
22
+ * `server.port` to a manual paste for a change we can make correctly.
23
+ *
24
+ * A config built by a call (`defineConfig(buildOptions())`) has no literal to extend and still
25
+ * bails — the rule stays "only edit a shape we can see whole".
26
+ */
27
+ const CONFIG_OBJECT = /(export\s+default\s+(?:defineConfig\s*\(\s*)?)\{/;
18
28
  /** Alias kept so existing call sites read in Vite terms; the vocabulary is shared (see patch-kind). */
19
29
  export const VitePatchKind = PatchKind;
20
30
  const NO_PLUGINS_REASON = "couldn't find a `plugins: [...]` array to extend";
@@ -41,12 +51,28 @@ function insertPlugin(source, port) {
41
51
  return `${match}${reticlePluginCall(port)},${separator}`;
42
52
  });
43
53
  }
54
+ /**
55
+ * Add a whole `plugins: [reticle()]` key to a config object that has none, matching the layout of
56
+ * the object it lands in: a multi-line object gets its own indented line, a one-liner stays inline.
57
+ */
58
+ function insertPluginsKey(source, port) {
59
+ return source.replace(CONFIG_OBJECT, (_match, prefix, offset) => {
60
+ const rest = source.slice(offset + _match.length);
61
+ const multiline = /^\s*\n/.test(rest);
62
+ const indent = /^\s*\n(\s*)\S/.exec(rest)?.[1] ?? ' ';
63
+ const key = `plugins: [${reticlePluginCall(port)}],`;
64
+ return multiline ? `${prefix}{\n${indent}${key}` : `${prefix}{ ${key}`;
65
+ });
66
+ }
44
67
  export function patchViteConfig(source, port) {
45
68
  if (source.includes(RETICLE_MARKER)) {
46
69
  return { kind: VitePatchKind.ALREADY };
47
70
  }
48
- if (!PLUGINS_ARRAY.test(source)) {
49
- return { kind: VitePatchKind.MANUAL, reason: NO_PLUGINS_REASON };
71
+ if (PLUGINS_ARRAY.test(source)) {
72
+ return { kind: VitePatchKind.APPLY, code: insertImport(insertPlugin(source, port)) };
73
+ }
74
+ if (CONFIG_OBJECT.test(source)) {
75
+ return { kind: VitePatchKind.APPLY, code: insertImport(insertPluginsKey(source, port)) };
50
76
  }
51
- return { kind: VitePatchKind.APPLY, code: insertImport(insertPlugin(source, port)) };
77
+ return { kind: VitePatchKind.MANUAL, reason: NO_PLUGINS_REASON };
52
78
  }
@@ -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)
@@ -161,7 +161,7 @@ export function describeLicense(now, env = process.env, baked = BAKED_ISSUER_PUB
161
161
  }
162
162
  : {
163
163
  status: 'eval',
164
- detail: 'evaluation mode — enterprise features run free (no issuer key configured)',
164
+ detail: 'evaluation mode: enterprise features run free (no issuer key configured)',
165
165
  };
166
166
  }
167
167
  const publicKey = loadPublicKey(pem);
@@ -7,6 +7,45 @@ export declare const MAX_RECONNECT_ATTEMPTS: number;
7
7
  export declare function reconnectDelayMs(attempt: number): number;
8
8
  /** JSON-RPC id the proxy uses for its own replayed `initialize` — never one the client could send. */
9
9
  export declare const RECONNECT_INITIALIZE_ID = "__reticle_proxy_reinit";
10
+ /**
11
+ * How much the proxy may append between size checks.
12
+ *
13
+ * Statting the file on every line would be a syscall per log line on a process that logs from a
14
+ * reconnect loop. Counting what we wrote costs nothing and is exact enough, because this process is
15
+ * the only writer of its own log. Well under the cap, so the cap is a cap rather than a suggestion.
16
+ */
17
+ export declare const PROXY_LOG_CHECK_BYTES: number;
18
+ /**
19
+ * Keep the proxy log under the same cap as the daemon log, without a stat per write.
20
+ *
21
+ * Returns the new "bytes since the last check" counter, so the accounting is testable without a
22
+ * filesystem. Rotation itself is `rotateDaemonLog` — the proxy log had no cap at all while the
23
+ * daemon log on the SAME machine stayed small, which is the whole of this defect.
24
+ */
25
+ export declare function accountProxyLogWrite(bytesSinceCheck: number, written: number, path: string, deps: {
26
+ fileSize(p: string): number;
27
+ renameFile(from: string, to: string): void;
28
+ }): number;
29
+ /**
30
+ * Reclaim a proxy log that a previous process already let run away. Returns the bytes reclaimed.
31
+ *
32
+ * Reported from the field: one machine's `proxy-4400.log` reached a third of a 460GB disk and broke
33
+ * builds, Docker and ordinary shell commands with ENOSPC. Nothing in Reticle degraded first, so the
34
+ * failure surfaced as the operating system refusing to write files. Files like that exist on real
35
+ * machines now, and the cap alone does not help them: the user should not have to find one with `du`.
36
+ *
37
+ * TRUNCATED IN PLACE, never renamed or unlinked. A rename moves the bytes without reclaiming a byte,
38
+ * and a file that a running process still holds open keeps its blocks allocated after an unlink
39
+ * until that handle closes — which is why the reporter recovered with `: > ~/.reticle/proxy-4400.log`
40
+ * and why that is the operation to copy here.
41
+ *
42
+ * Best-effort, in the same spirit as `rotateDaemonLog`: refusing to start the MCP server over
43
+ * housekeeping is strictly worse than a large file.
44
+ */
45
+ export declare function recoverOversizedProxyLog(path: string, deps: {
46
+ fileSize(p: string): number;
47
+ truncateFile(p: string): void;
48
+ }, max?: number): number;
10
49
  /** Name the proxy log after the port it serves. Called before anything can log. */
11
50
  export declare function setProxyLogPort(port: number): void;
12
51
  /** The proxy's own log file, so a silent drop leaves a readable trace the agent can go read. */
@@ -56,9 +95,36 @@ export declare class PendingRequests {
56
95
  * daemon under a live client left 20 of 20 concurrent calls hanging until the client's own timeout,
57
96
  * with the MCP server still perfectly alive. An agent cannot tell that apart from "still working".
58
97
  */
59
- export declare function streamLossReplies(pending: PendingRequests, reason: string): string[];
60
- /** The one shape every "the transport ate your call" answer takes, whichever leg died. */
61
- export declare function transportLossReply(id: unknown, reason: string): string;
98
+ export declare function streamLossReplies(pending: PendingRequests, reason: string, nextStep?: string): string[];
99
+ /**
100
+ * The one shape every "the transport ate your call" answer takes, whichever leg died.
101
+ *
102
+ * `nextStep` is appended when the caller knows one. Without it this reply describes a condition and
103
+ * stops there, which is all a first-run user got out of their first ever tool call.
104
+ */
105
+ export declare function transportLossReply(id: unknown, reason: string, nextStep?: string): string;
106
+ /**
107
+ * How long a request may sit in the queue, unsent, before the proxy answers it itself.
108
+ *
109
+ * The second population the stress test found. A request that was FORWARDED and lost is failed when
110
+ * the stream drops; one that never got forwarded — queued while dormant or reconnecting — was
111
+ * waiting for a session that, with a squatter holding the port, never arrives. It then hung until
112
+ * the client's own timeout, or forever if the client has none.
113
+ *
114
+ * Comfortably longer than a daemon restart (~1-2s) so a normal blip still flushes and succeeds, and
115
+ * well under the 60s an MCP client typically allows, so the answer comes from us with a reason
116
+ * rather than from their timer with none.
117
+ */
118
+ export declare const QUEUE_WAIT_MS = 20000;
119
+ /** Retry a first connect, or report it? Pure, so the window can be held against the queue's. */
120
+ export declare function shouldRetryFirstConnect(error: unknown, elapsedMs: number): boolean;
121
+ /**
122
+ * What to do about it, carried on the reply the queue expiry sends.
123
+ *
124
+ * A bare -32001 on the first call after an install says nothing a user can act on: not whether to
125
+ * wait, not whether to retry, not whether the install itself is broken.
126
+ */
127
+ export declare function queueExpiryNextStep(port: number): string;
62
128
  /**
63
129
  * Makes a reconnect invisible to the client.
64
130
  *