@reticlehq/server 2.8.0 → 2.10.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 (318) hide show
  1. package/SKILL.md +130 -813
  2. package/dist/bridge/bridge.d.ts +23 -0
  3. package/dist/bridge/bridge.js +80 -2
  4. package/dist/bridge/bridge.test-harness.d.ts +5 -0
  5. package/dist/bridge/bridge.test-harness.js +6 -4
  6. package/dist/capsule/capsule-store.d.ts +2 -2
  7. package/dist/capsule/causal-summary.d.ts +16 -1
  8. package/dist/capsule/causal-summary.js +2 -1
  9. package/dist/cli/cli-doctor.js +41 -13
  10. package/dist/cli/cli-flow-commands.js +8 -8
  11. package/dist/cli/cli-launch.d.ts +0 -17
  12. package/dist/cli/cli-launch.js +0 -28
  13. package/dist/cli/cli-parse.d.ts +1 -1
  14. package/dist/cli/cli-parse.js +1 -1
  15. package/dist/cli/cli-port.d.ts +24 -0
  16. package/dist/cli/cli-port.js +66 -35
  17. package/dist/cli/cli-verify.d.ts +21 -0
  18. package/dist/cli/cli-verify.js +45 -1
  19. package/dist/cli/config-discovery.d.ts +35 -0
  20. package/dist/cli/config-discovery.js +163 -0
  21. package/dist/cli/doctor-daemon-line.js +4 -1
  22. package/dist/cli/doctor-project-line.d.ts +37 -0
  23. package/dist/cli/doctor-project-line.js +35 -0
  24. package/dist/cli/doctor-rows.d.ts +51 -0
  25. package/dist/cli/doctor-rows.js +54 -0
  26. package/dist/cli/doctor-sessions-line.js +6 -3
  27. package/dist/cli/drive-attach.d.ts +99 -0
  28. package/dist/cli/drive-attach.js +203 -0
  29. package/dist/cli/drive-command.d.ts +13 -0
  30. package/dist/cli/drive-command.js +103 -0
  31. package/dist/cli/open-note.d.ts +20 -0
  32. package/dist/cli/open-note.js +37 -0
  33. package/dist/cli/status-next-action.d.ts +37 -0
  34. package/dist/cli/status-next-action.js +61 -0
  35. package/dist/cli.js +65 -70
  36. package/dist/crawl/crawl.d.ts +7 -2
  37. package/dist/crawl/crawl.js +52 -9
  38. package/dist/ee/audit-log.js +2 -2
  39. package/dist/events/body-failures.d.ts +2 -2
  40. package/dist/events/contradiction-folds.d.ts +45 -0
  41. package/dist/events/contradiction-folds.js +42 -0
  42. package/dist/events/contradictions.d.ts +51 -1
  43. package/dist/events/contradictions.js +26 -7
  44. package/dist/events/declared.d.ts +45 -0
  45. package/dist/events/declared.js +73 -0
  46. package/dist/events/echo-mismatch.d.ts +9 -3
  47. package/dist/events/echo-mismatch.js +33 -5
  48. package/dist/events/event-filters.d.ts +1 -1
  49. package/dist/events/event-filters.js +7 -4
  50. package/dist/events/predicate-eval.d.ts +17 -91
  51. package/dist/events/predicate-eval.js +85 -326
  52. package/dist/events/predicate-parse.js +31 -3
  53. package/dist/events/predicate-route.d.ts +32 -0
  54. package/dist/events/predicate-route.js +102 -0
  55. package/dist/events/predicate-schema.d.ts +99 -0
  56. package/dist/events/predicate-schema.js +309 -0
  57. package/dist/events/predicate.d.ts +8 -0
  58. package/dist/events/predicate.js +127 -5
  59. package/dist/events/stale-response.d.ts +2 -3
  60. package/dist/events/stale-response.js +49 -2
  61. package/dist/events/unit-mismatch.d.ts +2 -2
  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-replay-run.js +8 -1
  69. package/dist/flows/flow-tools.js +9 -1
  70. package/dist/flows/replay.js +3 -0
  71. package/dist/flows/verify-change-tools.js +1 -1
  72. package/dist/honesty/accepted-write.d.ts +3 -0
  73. package/dist/honesty/accepted-write.js +8 -3
  74. package/dist/honesty/already-true.js +15 -0
  75. package/dist/honesty/blind-spots.js +4 -0
  76. package/dist/honesty/honesty.d.ts +11 -0
  77. package/dist/honesty/honesty.js +1 -0
  78. package/dist/honesty/uncaptured-bodies.d.ts +4 -2
  79. package/dist/honesty/uncaptured-bodies.js +9 -2
  80. package/dist/honesty/unread-outcome.d.ts +8 -0
  81. package/dist/honesty/unread-outcome.js +36 -9
  82. package/dist/honesty/unsettled.d.ts +9 -0
  83. package/dist/honesty/unsettled.js +7 -3
  84. package/dist/honesty/verified.d.ts +29 -3
  85. package/dist/honesty/verified.js +67 -8
  86. package/dist/http-server.d.ts +19 -0
  87. package/dist/http-server.js +202 -4
  88. package/dist/impact/impact-recorder.d.ts +23 -0
  89. package/dist/impact/impact-recorder.js +76 -0
  90. package/dist/impact/impact-store.d.ts +47 -0
  91. package/dist/impact/impact-store.js +171 -0
  92. package/dist/index.d.ts +25 -0
  93. package/dist/index.js +60 -10
  94. package/dist/init/agent-rules.d.ts +20 -0
  95. package/dist/init/agent-rules.js +36 -0
  96. package/dist/init/astro-patch.js +27 -1
  97. package/dist/init/confirm.d.ts +21 -3
  98. package/dist/init/confirm.js +32 -5
  99. package/dist/init/connect-steps.d.ts +2 -0
  100. package/dist/init/connect-steps.js +50 -0
  101. package/dist/init/csp-check.d.ts +38 -0
  102. package/dist/init/csp-check.js +92 -0
  103. package/dist/init/csp-doctor.d.ts +25 -0
  104. package/dist/init/csp-doctor.js +49 -0
  105. package/dist/init/cursor.js +6 -9
  106. package/dist/init/detect.d.ts +8 -0
  107. package/dist/init/detect.js +33 -3
  108. package/dist/init/mcp-clients.d.ts +1 -0
  109. package/dist/init/mcp-clients.js +51 -7
  110. package/dist/init/mcp.d.ts +16 -2
  111. package/dist/init/mcp.js +23 -1
  112. package/dist/init/plan-framework.d.ts +10 -0
  113. package/dist/init/plan-framework.js +25 -1
  114. package/dist/init/plan.d.ts +7 -2
  115. package/dist/init/plan.js +66 -30
  116. package/dist/init/run.js +11 -5
  117. package/dist/init/slash-command.d.ts +1 -1
  118. package/dist/init/slash-command.js +12 -4
  119. package/dist/init/snippets.d.ts +1 -1
  120. package/dist/init/snippets.js +35 -7
  121. package/dist/init/vite-config.js +24 -2
  122. package/dist/input/network-mock-tools.js +8 -1
  123. package/dist/input/scroll-find.d.ts +6 -0
  124. package/dist/input/scroll-find.js +17 -2
  125. package/dist/input/scroll-tools.js +4 -0
  126. package/dist/journal/attach-journal.d.ts +9 -1
  127. package/dist/journal/attach-journal.js +13 -2
  128. package/dist/journal/journal-query.d.ts +13 -0
  129. package/dist/journal/journal-query.js +28 -3
  130. package/dist/journal/workspace-gitignore.d.ts +10 -0
  131. package/dist/journal/workspace-gitignore.js +59 -0
  132. package/dist/license/license.d.ts +69 -4
  133. package/dist/license/license.js +84 -15
  134. package/dist/mcp/mcp-proxy.d.ts +7 -93
  135. package/dist/mcp/mcp-proxy.js +183 -250
  136. package/dist/mcp/mcp.d.ts +27 -2
  137. package/dist/mcp/mcp.js +77 -36
  138. package/dist/mcp/proxy-daemon-probe.d.ts +8 -0
  139. package/dist/mcp/proxy-daemon-probe.js +48 -0
  140. package/dist/mcp/proxy-handshake.d.ts +82 -1
  141. package/dist/mcp/proxy-handshake.js +110 -3
  142. package/dist/mcp/proxy-lifecycle.d.ts +10 -1
  143. package/dist/mcp/proxy-lifecycle.js +11 -2
  144. package/dist/mcp/proxy-log.d.ts +63 -0
  145. package/dist/mcp/proxy-log.js +132 -0
  146. package/dist/mcp/server-instructions.d.ts +17 -0
  147. package/dist/mcp/server-instructions.js +56 -0
  148. package/dist/mcp/shared-params.d.ts +27 -0
  149. package/dist/mcp/shared-params.js +36 -0
  150. package/dist/mcp/sse-frame-parser.d.ts +19 -0
  151. package/dist/mcp/sse-frame-parser.js +41 -0
  152. package/dist/project/reticle-dir.d.ts +2 -0
  153. package/dist/project/reticle-dir.js +1 -0
  154. package/dist/session/connection-memory.d.ts +59 -0
  155. package/dist/session/connection-memory.js +108 -0
  156. package/dist/session/dev-command.d.ts +26 -0
  157. package/dist/session/dev-command.js +89 -0
  158. package/dist/session/dev-server-probe.d.ts +68 -0
  159. package/dist/session/dev-server-probe.js +102 -7
  160. package/dist/session/last-act.d.ts +26 -0
  161. package/dist/session/last-act.js +35 -1
  162. package/dist/session/live-control-tools.js +42 -5
  163. package/dist/session/no-session-diagnosis.d.ts +55 -2
  164. package/dist/session/no-session-diagnosis.js +199 -22
  165. package/dist/session/no-session-next-action.d.ts +46 -0
  166. package/dist/session/no-session-next-action.js +85 -0
  167. package/dist/session/no-session-watch.d.ts +40 -1
  168. package/dist/session/no-session-watch.js +172 -11
  169. package/dist/session/pending-commands.d.ts +12 -0
  170. package/dist/session/pending-commands.js +13 -1
  171. package/dist/session/session-manager.d.ts +4 -0
  172. package/dist/session/session-manager.js +35 -5
  173. package/dist/session/session-readiness.js +1 -1
  174. package/dist/session/session.d.ts +10 -1
  175. package/dist/session/session.js +146 -5
  176. package/dist/telemetry/app-instrumented.d.ts +8 -0
  177. package/dist/telemetry/app-instrumented.js +10 -0
  178. package/dist/telemetry/bug-found.d.ts +18 -0
  179. package/dist/telemetry/bug-found.js +14 -0
  180. package/dist/telemetry/daemon-telemetry.js +29 -0
  181. package/dist/telemetry/install-source.d.ts +20 -0
  182. package/dist/telemetry/install-source.js +34 -0
  183. package/dist/telemetry/instrumentation-stall.d.ts +57 -0
  184. package/dist/telemetry/instrumentation-stall.js +103 -0
  185. package/dist/telemetry/license-activation.d.ts +48 -0
  186. package/dist/telemetry/license-activation.js +57 -0
  187. package/dist/telemetry/session-metrics.d.ts +8 -0
  188. package/dist/telemetry/session-metrics.js +10 -0
  189. package/dist/telemetry/telemetry.d.ts +3 -1
  190. package/dist/telemetry/telemetry.js +23 -3
  191. package/dist/tools/act-tools.js +166 -29
  192. package/dist/tools/assert-verdict.js +32 -7
  193. package/dist/tools/browser-tools.js +10 -0
  194. package/dist/tools/dynamic-tools.js +17 -8
  195. package/dist/tools/error-recovery.js +10 -6
  196. package/dist/tools/feedback-tools.js +31 -6
  197. package/dist/tools/invoke-tool.d.ts +1 -1
  198. package/dist/tools/invoke-tool.js +30 -8
  199. package/dist/tools/lease-hint.d.ts +30 -8
  200. package/dist/tools/lease-hint.js +65 -14
  201. package/dist/tools/lease-tools.d.ts +11 -0
  202. package/dist/tools/lease-tools.js +141 -96
  203. package/dist/tools/observe-tools.js +9 -3
  204. package/dist/tools/react-grace.d.ts +4 -0
  205. package/dist/tools/react-grace.js +6 -3
  206. package/dist/tools/read-tools.js +47 -1
  207. package/dist/tools/resolve-target.d.ts +33 -0
  208. package/dist/tools/resolve-target.js +53 -0
  209. package/dist/tools/settle-in-flight.d.ts +15 -0
  210. package/dist/tools/settle-in-flight.js +42 -2
  211. package/dist/tools/snapshot-delta.d.ts +3 -0
  212. package/dist/tools/snapshot-delta.js +67 -6
  213. package/dist/tools/tool-kit.d.ts +38 -3
  214. package/dist/tools/tool-names.d.ts +2 -0
  215. package/dist/tools/tool-names.js +2 -0
  216. package/dist/tools/tool-surface.d.ts +60 -0
  217. package/dist/tools/tool-surface.js +84 -19
  218. package/dist/tools/tools.js +61 -5
  219. package/dist/tools/unadvertised-help.js +9 -6
  220. package/docs/README.md +1 -2
  221. package/docs/actions.mdx +41 -8
  222. package/docs/agent-cheatsheet.md +6 -4
  223. package/docs/architecture.md +6 -0
  224. package/docs/benchmarks.md +60 -12
  225. package/docs/best-practices.mdx +11 -6
  226. package/docs/capabilities.mdx +204 -57
  227. package/docs/cli/affected.mdx +6 -4
  228. package/docs/cli/capsules.mdx +9 -8
  229. package/docs/cli/cloud.mdx +30 -4
  230. package/docs/cli/doctor.mdx +41 -8
  231. package/docs/cli/drive.mdx +34 -15
  232. package/docs/cli/gate.mdx +6 -6
  233. package/docs/cli/help.mdx +8 -6
  234. package/docs/cli/hunt.mdx +20 -4
  235. package/docs/cli/identify.mdx +16 -2
  236. package/docs/cli/init.mdx +35 -8
  237. package/docs/cli/kill.mdx +5 -5
  238. package/docs/cli/license.mdx +1 -1
  239. package/docs/cli/open.mdx +6 -0
  240. package/docs/cli/restart.mdx +6 -6
  241. package/docs/cli/serve.mdx +10 -3
  242. package/docs/cli/status.mdx +29 -5
  243. package/docs/cli/stop.mdx +8 -0
  244. package/docs/cli/verify.mdx +20 -1
  245. package/docs/cli/version.mdx +1 -1
  246. package/docs/cli/watch.mdx +9 -2
  247. package/docs/cli.mdx +44 -22
  248. package/docs/debugging.md +3 -1
  249. package/docs/deploy-checks.md +31 -2
  250. package/docs/desktop.mdx +276 -80
  251. package/docs/docs.json +13 -12
  252. package/docs/enterprise.md +9 -3
  253. package/docs/faq.mdx +49 -35
  254. package/docs/fixtures.md +2 -0
  255. package/docs/flows.md +7 -5
  256. package/docs/for-agents.md +3 -3
  257. package/docs/frameworks.mdx +46 -7
  258. package/docs/gate-plan.md +4 -2
  259. package/docs/gates.md +8 -6
  260. package/docs/getting-started.md +53 -12
  261. package/docs/human-control.md +2 -0
  262. package/docs/index.mdx +43 -1
  263. package/docs/install-agentic.mdx +13 -4
  264. package/docs/install-manual.mdx +6 -4
  265. package/docs/instrumentation.mdx +7 -1
  266. package/docs/integration-patterns.md +3 -1
  267. package/docs/local-registry.md +4 -2
  268. package/docs/multi-agent-testing.md +3 -1
  269. package/docs/packages/babel-plugin.mdx +15 -6
  270. package/docs/packages/browser.mdx +2 -2
  271. package/docs/packages/core.mdx +26 -16
  272. package/docs/packages/electron.mdx +5 -7
  273. package/docs/packages/eslint-plugin.mdx +5 -3
  274. package/docs/packages/next.mdx +3 -5
  275. package/docs/packages/react.mdx +3 -3
  276. package/docs/packages/server.mdx +2 -2
  277. package/docs/packages/tauri.mdx +6 -6
  278. package/docs/packages/test.mdx +18 -7
  279. package/docs/packages/vite-plugin.mdx +4 -6
  280. package/docs/packages.mdx +14 -13
  281. package/docs/platform-integration.md +2 -0
  282. package/docs/predicates.mdx +76 -7
  283. package/docs/quickstart.mdx +51 -5
  284. package/docs/recipes.mdx +63 -23
  285. package/docs/skill-file.mdx +3 -1
  286. package/docs/state-management.mdx +2 -0
  287. package/docs/system-map.md +3 -1
  288. package/docs/telemetry-contract.md +33 -5
  289. package/docs/telemetry-events.mdx +63 -18
  290. package/docs/telemetry.md +8 -5
  291. package/docs/testing.md +35 -1
  292. package/docs/token-efficiency.md +8 -2
  293. package/docs/tools-act-and-wait.mdx +64 -14
  294. package/docs/tools-act-sequence.mdx +34 -17
  295. package/docs/tools-act.mdx +24 -11
  296. package/docs/tools-assert.mdx +69 -10
  297. package/docs/tools-console.mdx +11 -7
  298. package/docs/tools-inspect.mdx +11 -8
  299. package/docs/tools-navigate.mdx +41 -5
  300. package/docs/tools-network.mdx +42 -9
  301. package/docs/tools-observe.mdx +66 -10
  302. package/docs/tools-overview.mdx +26 -23
  303. package/docs/tools-query.mdx +52 -8
  304. package/docs/tools-session-and-feedback.mdx +35 -9
  305. package/docs/tools-sessions.mdx +39 -10
  306. package/docs/tools-snapshot.mdx +57 -3
  307. package/docs/tools-state.mdx +24 -1
  308. package/docs/tools-tools-and-run.mdx +55 -24
  309. package/docs/tools-wait-for.mdx +34 -15
  310. package/docs/troubleshooting.mdx +80 -10
  311. package/docs/usage.md +46 -24
  312. package/docs/vs-chrome-devtools-mcp.mdx +41 -0
  313. package/docs/vs-playwright-mcp.mdx +42 -2
  314. package/docs/vs-screenshots.mdx +42 -1
  315. package/docs/what-is-reticle.mdx +1 -1
  316. package/docs/why-reticle.mdx +3 -1
  317. package/package.json +5 -5
  318. package/docs/desktop-apps.md +0 -265
@@ -0,0 +1,59 @@
1
+ import { join } from 'node:path';
2
+ import { ReticleDir } from '@reticlehq/core';
3
+ /**
4
+ * The `.reticle/` entries that are LOCAL state, and must never be committed by accident.
5
+ *
6
+ * Split from the durable half deliberately, and the split is the whole design: `contract.json`,
7
+ * `flows/`, `baselines/` and `capsules/` are meant to be checked in — a flow that cannot be shared
8
+ * is not a regression check anybody else can run — while everything below is per-machine churn.
9
+ *
10
+ * The reason this matters beyond tidiness: a session journal carries URLs, request and response
11
+ * bodies, and DOM text from the app under test. `.reticle/` is created by the daemon rather than by
12
+ * the user, so it arrives untracked and unexplained, and one `git add -A` puts an app's traffic into
13
+ * a shared repository. Reticle created the directory; the ignore for it is Reticle's to write.
14
+ */
15
+ const TRANSIENT = [
16
+ `${ReticleDir.SESSIONS_SUBDIR}/`,
17
+ `${ReticleDir.RUNS_SUBDIR}/`,
18
+ `${ReticleDir.VISUAL_SUBDIR}/`,
19
+ ReticleDir.PROJECT_FILE,
20
+ ReticleDir.AMBIENT_FILE,
21
+ ReticleDir.ENVELOPES_FILE,
22
+ ReticleDir.FLAKE_FILE,
23
+ ReticleDir.TIERS_FILE,
24
+ '*.log',
25
+ '*.tmp',
26
+ ];
27
+ const HEADER = [
28
+ '# Written by Reticle, and only about the directory Reticle owns.',
29
+ '#',
30
+ '# The entries below are per-machine state — session journals, run artifacts, learned envelopes.',
31
+ '# Journals contain URLs, request and response bodies and page text from the app under test, so',
32
+ '# they are worth keeping out of a shared repository on their own account.',
33
+ '#',
34
+ `# Deliberately NOT ignored: ${ReticleDir.CONTRACT_FILE}, ${ReticleDir.FLOWS_SUBDIR}/, ${ReticleDir.BASELINES_SUBDIR}/ and`,
35
+ `# ${ReticleDir.CAPSULES_SUBDIR}/. Those are meant to be committed — a saved flow only earns its keep`,
36
+ '# when a teammate or CI can replay it.',
37
+ '#',
38
+ '# Edit freely: Reticle writes this once and never rewrites it.',
39
+ ];
40
+ /**
41
+ * Write `.reticle/.gitignore` if it is not already there.
42
+ *
43
+ * Never overwrites — once the file exists it belongs to whoever edited it last, and silently
44
+ * reverting somebody's change to a file we placed in their repository is worse than not having
45
+ * written it. Best-effort throughout: this runs on the daemon's start path, and a workspace that
46
+ * cannot be written must not stop a daemon coming up. Nothing about verification depends on it.
47
+ */
48
+ export async function ensureWorkspaceGitignore(fs, root) {
49
+ const path = join(root, '.gitignore');
50
+ try {
51
+ if (await fs.exists(path))
52
+ return;
53
+ await fs.mkdir(root);
54
+ await fs.writeFile(path, `${[...HEADER, '', ...TRANSIENT].join('\n')}\n`);
55
+ }
56
+ catch {
57
+ /* a workspace we cannot write is not a reason to fail a session */
58
+ }
59
+ }
@@ -12,6 +12,7 @@
12
12
  */
13
13
  import { type KeyObject } from 'node:crypto';
14
14
  import { z } from 'zod';
15
+ import { LicenseActivation } from '@reticlehq/core';
15
16
  /** Outcome of verifying a license key. */
16
17
  export declare const LicenseStatus: {
17
18
  readonly VALID: "valid";
@@ -21,18 +22,31 @@ export declare const LicenseStatus: {
21
22
  readonly EXPIRED: "expired";
22
23
  };
23
24
  export type LicenseStatus = (typeof LicenseStatus)[keyof typeof LicenseStatus];
24
- /** The signed claims inside a key. `exp` is epoch ms; `features` (optional) scopes which ee features unlock. */
25
+ /**
26
+ * The signed claims inside a key. `exp` is epoch ms; `features` (optional) scopes which ee features
27
+ * unlock.
28
+ *
29
+ * `lid` is the STABLE LICENSE ID and the only safe join key for per-customer usage: `org` is a display
30
+ * name a human typed at signing time, so two customers called "Acme" merge into one and a customer who
31
+ * renames splits into two. Every key carries one (minted by the issuer, `scripts/issue-license.mjs`),
32
+ * which is why it is required rather than optional — an optional id is one that goes missing on exactly
33
+ * the keys somebody later needs to attribute, and a key without it should be re-issued, not silently
34
+ * counted as nobody.
35
+ */
25
36
  declare const LicensePayloadSchema: z.ZodObject<{
37
+ lid: z.ZodString;
26
38
  org: z.ZodString;
27
39
  plan: z.ZodString;
28
40
  exp: z.ZodNumber;
29
41
  features: z.ZodOptional<z.ZodArray<z.ZodString, "many">>;
30
42
  }, "strip", z.ZodTypeAny, {
43
+ lid: string;
31
44
  org: string;
32
45
  plan: string;
33
46
  exp: number;
34
47
  features?: string[] | undefined;
35
48
  }, {
49
+ lid: string;
36
50
  org: string;
37
51
  plan: string;
38
52
  exp: number;
@@ -42,13 +56,44 @@ export type LicensePayload = z.infer<typeof LicensePayloadSchema>;
42
56
  type LicenseCheck = {
43
57
  status: typeof LicenseStatus.VALID;
44
58
  payload: LicensePayload;
59
+ }
60
+ /**
61
+ * EXPIRED carries the payload; the other failures cannot. The line is the SIGNATURE, not the
62
+ * clock: an expired key was genuinely issued by us and its claims are exactly as trustworthy as
63
+ * they were yesterday, so the licence id is safe to use. A malformed or wrongly-signed key has no
64
+ * verified claims at all, and naming a customer from one would mean trusting whatever was pasted.
65
+ */
66
+ | {
67
+ status: typeof LicenseStatus.EXPIRED;
68
+ payload: LicensePayload;
45
69
  } | {
46
- status: Exclude<LicenseStatus, typeof LicenseStatus.VALID>;
70
+ status: Exclude<LicenseStatus, typeof LicenseStatus.VALID | typeof LicenseStatus.EXPIRED>;
47
71
  };
48
72
  /** Sign a payload into a license key — the ISSUER side (Reticle's private key). Exposed for the issuer tool + tests. */
49
73
  export declare function signLicenseKey(payload: LicensePayload, privateKey: KeyObject): string;
50
74
  /** Verify a key against the issuer public key. Never throws — returns a structured status. */
51
75
  export declare function verifyLicenseKey(key: string | undefined, publicKey: KeyObject, now: number): LicenseCheck;
76
+ /**
77
+ * Where to get a key. A named constant because it appears in the gate's error AND in what
78
+ * `reticle license` prints, and a contact address that drifts between the two is a support request
79
+ * that reaches nobody.
80
+ */
81
+ export declare const LICENSE_CONTACT = "hey@reticle.sh";
82
+ /**
83
+ * The features a licence gates in THIS build. The single source: `assertEnterprise` callers name a
84
+ * member rather than a string literal, and `reticle license` reports the list, so what the CLI claims
85
+ * is gated is derived from what is actually gated rather than a second list somebody maintains.
86
+ *
87
+ * It is deliberately short. Reticle's roadmap for enterprise is longer (SSO, SCIM, RBAC, multi-org,
88
+ * policy gates), and none of it is here yet — printing a roadmap as though it were shipped is how a
89
+ * buyer discovers on day two that the thing they paid for does not exist. What is listed is what the
90
+ * running build will actually refuse without a key.
91
+ */
92
+ export declare const EnterpriseFeature: {
93
+ /** Recording an audit event. */
94
+ readonly AUDIT_LOG: "audit-log";
95
+ };
96
+ export type EnterpriseFeature = (typeof EnterpriseFeature)[keyof typeof EnterpriseFeature];
52
97
  /** Thrown when a production-mode enterprise feature is used without a valid license. */
53
98
  export declare class EnterpriseLicenseError extends Error {
54
99
  readonly feature: string;
@@ -71,8 +116,28 @@ export declare function assertEnterprise(feature: string, ctx: GateContext): voi
71
116
  export declare const LICENSE_KEY_ENV = "RETICLE_LICENSE_KEY";
72
117
  export declare const LICENSE_PUBLIC_KEY_ENV = "RETICLE_LICENSE_PUBLIC_KEY";
73
118
  /** The human-facing state of enterprise activation on this machine (what `reticle license status` shows). */
74
- interface LicenseReport {
75
- status: 'active' | 'missing' | 'invalid' | 'expired' | 'eval';
119
+ export interface LicenseReport {
120
+ /**
121
+ * Typed against core's closed list rather than re-listing the strings here: this status is reported
122
+ * on every telemetry event, so a member added in one place and not the other is a silently
123
+ * miscounted column (telemetry contract, rule 4).
124
+ */
125
+ status: LicenseActivation;
126
+ /** The stable license id — what usage is attributed to. Present only when `status` is `active`. */
127
+ licenseId?: string;
128
+ /**
129
+ * What a licence unlocks in THIS build, and where to get one. Always reported, including on an
130
+ * unlicensed install: `reticle license` was the one command that talks about licensing and it named
131
+ * neither, so it told an interested reader to set a variable without saying what it would unlock or
132
+ * how to obtain it. A dead end at exactly the moment somebody is asking.
133
+ */
134
+ gated: readonly string[];
135
+ contact: string;
136
+ /**
137
+ * The key is valid, and scoped to features this build does not gate, so it unlocks nothing here.
138
+ * Present only when true: a `false` on every healthy licence is noise on the common case.
139
+ */
140
+ coversNothingHere?: boolean;
76
141
  org?: string;
77
142
  plan?: string;
78
143
  expiresAt?: number;
@@ -12,6 +12,7 @@
12
12
  */
13
13
  import { createPublicKey, sign as edSign, verify as edVerify } from 'node:crypto';
14
14
  import { z } from 'zod';
15
+ import { LicenseActivation } from '@reticlehq/core';
15
16
  /** Outcome of verifying a license key. */
16
17
  export const LicenseStatus = {
17
18
  VALID: 'valid',
@@ -20,8 +21,19 @@ export const LicenseStatus = {
20
21
  BAD_SIGNATURE: 'bad-signature',
21
22
  EXPIRED: 'expired',
22
23
  };
23
- /** The signed claims inside a key. `exp` is epoch ms; `features` (optional) scopes which ee features unlock. */
24
+ /**
25
+ * The signed claims inside a key. `exp` is epoch ms; `features` (optional) scopes which ee features
26
+ * unlock.
27
+ *
28
+ * `lid` is the STABLE LICENSE ID and the only safe join key for per-customer usage: `org` is a display
29
+ * name a human typed at signing time, so two customers called "Acme" merge into one and a customer who
30
+ * renames splits into two. Every key carries one (minted by the issuer, `scripts/issue-license.mjs`),
31
+ * which is why it is required rather than optional — an optional id is one that goes missing on exactly
32
+ * the keys somebody later needs to attribute, and a key without it should be re-issued, not silently
33
+ * counted as nobody.
34
+ */
24
35
  const LicensePayloadSchema = z.object({
36
+ lid: z.string().min(1),
25
37
  org: z.string(),
26
38
  plan: z.string(),
27
39
  exp: z.number(),
@@ -68,15 +80,35 @@ export function verifyLicenseKey(key, publicKey, now) {
68
80
  if (!signatureOk)
69
81
  return { status: LicenseStatus.BAD_SIGNATURE };
70
82
  if (payload.exp <= now)
71
- return { status: LicenseStatus.EXPIRED };
83
+ return { status: LicenseStatus.EXPIRED, payload };
72
84
  return { status: LicenseStatus.VALID, payload };
73
85
  }
86
+ /**
87
+ * Where to get a key. A named constant because it appears in the gate's error AND in what
88
+ * `reticle license` prints, and a contact address that drifts between the two is a support request
89
+ * that reaches nobody.
90
+ */
91
+ export const LICENSE_CONTACT = 'hey@reticle.sh';
92
+ /**
93
+ * The features a licence gates in THIS build. The single source: `assertEnterprise` callers name a
94
+ * member rather than a string literal, and `reticle license` reports the list, so what the CLI claims
95
+ * is gated is derived from what is actually gated rather than a second list somebody maintains.
96
+ *
97
+ * It is deliberately short. Reticle's roadmap for enterprise is longer (SSO, SCIM, RBAC, multi-org,
98
+ * policy gates), and none of it is here yet — printing a roadmap as though it were shipped is how a
99
+ * buyer discovers on day two that the thing they paid for does not exist. What is listed is what the
100
+ * running build will actually refuse without a key.
101
+ */
102
+ export const EnterpriseFeature = {
103
+ /** Recording an audit event. */
104
+ AUDIT_LOG: 'audit-log',
105
+ };
74
106
  /** Thrown when a production-mode enterprise feature is used without a valid license. */
75
107
  export class EnterpriseLicenseError extends Error {
76
108
  feature;
77
109
  reason;
78
110
  constructor(feature, reason) {
79
- super(`Reticle Enterprise feature "${feature}" requires a valid license (${reason}). Contact hey@reticle.sh.`);
111
+ super(`Reticle Enterprise feature "${feature}" requires a valid license (${reason}). Contact ${LICENSE_CONTACT}.`);
80
112
  this.name = 'EnterpriseLicenseError';
81
113
  this.feature = feature;
82
114
  this.reason = reason;
@@ -122,7 +154,7 @@ export const LICENSE_PUBLIC_KEY_ENV = 'RETICLE_LICENSE_PUBLIC_KEY';
122
154
  * never setting RETICLE_LICENSE_PUBLIC_KEY, because the baked key takes precedence. The public key is
123
155
  * safe to ship openly — it can only verify licenses, never mint them (that needs the private key).
124
156
  */
125
- const BAKED_ISSUER_PUBLIC_KEY_PEM = '';
157
+ const BAKED_ISSUER_PUBLIC_KEY_PEM = "-----BEGIN PUBLIC KEY-----\nMCowBQYDK2VwAyEAcg+u61Fs5pwDHcYxFGhaxVaQoUbAdyl+RUdSViZyxDY=\n-----END PUBLIC KEY-----";
126
158
  /**
127
159
  * Resolve the issuer public-key PEM enforcement uses: the baked-in release key wins, so it can't be
128
160
  * turned off from the environment. Only when nothing is baked (dev/repo) does the env var apply — that
@@ -134,6 +166,15 @@ function resolveIssuerPublicKeyPem(env, baked = BAKED_ISSUER_PUBLIC_KEY_PEM) {
134
166
  const envPem = env[LICENSE_PUBLIC_KEY_ENV];
135
167
  return envPem !== undefined && envPem.length > 0 ? envPem : undefined;
136
168
  }
169
+ /**
170
+ * The two fields every report carries, spread into each branch. One object rather than a field on each
171
+ * return, because the value that must never go missing is the one on the branch nobody was thinking
172
+ * about — and the branch an unlicensed reader lands on is exactly that branch.
173
+ */
174
+ const LICENSE_OFFER = {
175
+ gated: Object.values(EnterpriseFeature),
176
+ contact: LICENSE_CONTACT,
177
+ };
137
178
  function loadPublicKey(pem) {
138
179
  if (pem === undefined || 0 === pem.length)
139
180
  return undefined;
@@ -156,42 +197,70 @@ export function describeLicense(now, env = process.env, baked = BAKED_ISSUER_PUB
156
197
  // is off, and assertEnterpriseFromEnv now denies rather than unlocking. Say so loudly here too.
157
198
  return isProductionEnv(env)
158
199
  ? {
159
- status: 'invalid',
200
+ ...LICENSE_OFFER,
201
+ status: LicenseActivation.INVALID,
160
202
  detail: 'MISCONFIGURED — running in production with no issuer key baked; enterprise features are DENIED (rebuild with BAKED_ISSUER_PUBLIC_KEY_PEM stamped in)',
161
203
  }
162
204
  : {
163
- status: 'eval',
205
+ ...LICENSE_OFFER,
206
+ status: LicenseActivation.EVAL,
164
207
  detail: 'evaluation mode: enterprise features run free (no issuer key configured)',
165
208
  };
166
209
  }
167
210
  const publicKey = loadPublicKey(pem);
168
211
  if (publicKey === undefined)
169
- return { status: 'invalid', detail: `${LICENSE_PUBLIC_KEY_ENV} is not a valid public key` };
212
+ return {
213
+ ...LICENSE_OFFER,
214
+ status: LicenseActivation.INVALID,
215
+ detail: `${LICENSE_PUBLIC_KEY_ENV} is not a valid public key`,
216
+ };
170
217
  const check = verifyLicenseKey(env[LICENSE_KEY_ENV], publicKey, now);
171
218
  if (check.status === LicenseStatus.VALID) {
172
- const { org, plan, exp, features } = check.payload;
219
+ const { lid, org, plan, exp, features } = check.payload;
220
+ // A key scoped to features this build does not gate is `active` and unlocks nothing. Both halves
221
+ // were already on screen (`features` against `gated`) and nothing joined them, so the one word a
222
+ // customer actually reads told them they were fine while every gated call refused them.
223
+ const coversNothingHere = features !== undefined && !features.some((f) => LICENSE_OFFER.gated.includes(f));
224
+ const detail = coversNothingHere
225
+ ? `licensed to ${org} (${plan}), expires ${new Date(exp).toISOString()}. This key covers ${features?.join(', ')}, and nothing this build gates (${LICENSE_OFFER.gated.join(', ')}) is included. Contact ${LICENSE_CONTACT}`
226
+ : `licensed to ${org} (${plan}), expires ${new Date(exp).toISOString()}`;
173
227
  return {
174
- status: 'active',
228
+ ...LICENSE_OFFER,
229
+ status: LicenseActivation.ACTIVE,
230
+ licenseId: lid,
175
231
  org,
176
232
  plan,
177
233
  expiresAt: exp,
178
234
  ...(features !== undefined ? { features } : {}),
179
- detail: `licensed to ${org} (${plan}), expires ${new Date(exp).toISOString()}`,
235
+ ...(coversNothingHere ? { coversNothingHere } : {}),
236
+ detail,
180
237
  };
181
238
  }
182
239
  if (check.status === LicenseStatus.MISSING) {
183
240
  return {
184
- status: 'missing',
185
- detail: `set ${LICENSE_KEY_ENV} to activate enterprise features in production`,
241
+ ...LICENSE_OFFER,
242
+ status: LicenseActivation.MISSING,
243
+ detail: `set ${LICENSE_KEY_ENV} to activate enterprise features in production. ` +
244
+ `Request a key from ${LICENSE_CONTACT}`,
186
245
  };
187
246
  }
188
247
  if (check.status === LicenseStatus.EXPIRED) {
248
+ // The id rides an expired key so a lapse can be attributed to a CUSTOMER. Without it, telemetry
249
+ // reports that somebody's licence ran out and cannot say whose, which is not a renewal signal.
189
250
  return {
190
- status: 'expired',
191
- detail: 'license expired — renew to keep using enterprise features',
251
+ ...LICENSE_OFFER,
252
+ status: LicenseActivation.EXPIRED,
253
+ licenseId: check.payload.lid,
254
+ org: check.payload.org,
255
+ expiresAt: check.payload.exp,
256
+ detail: `license expired. Renew with ${LICENSE_CONTACT} to keep using enterprise features`,
192
257
  };
193
258
  }
194
- return { status: 'invalid', detail: `license key rejected (${check.status})` };
259
+ return {
260
+ ...LICENSE_OFFER,
261
+ status: LicenseActivation.INVALID,
262
+ detail: `license key rejected (${check.status}). ${LICENSE_CONTACT} can re-issue it`,
263
+ };
195
264
  }
196
265
  /** True in a production runtime (NODE_ENV=production) — where a missing issuer key is a mis-built
197
266
  * release, not an eval session, so the gate must fail CLOSED rather than run features free. */
@@ -1,3 +1,10 @@
1
+ export { SseFrameParser, type SseFrame } from './sse-frame-parser.js';
2
+ export { probeDaemon, waitForDaemon } from './proxy-daemon-probe.js';
3
+ /**
4
+ * Re-exported so every existing caller (and every spec) keeps importing the proxy's log from the
5
+ * proxy. Where the code lives is a file-size decision; where it is imported from is an API.
6
+ */
7
+ export { accountProxyLogWrite, proxyLog, proxyLogPath, PROXY_LOG_CHECK_BYTES, recoverOversizedProxyLog, setProxyLogPort, } from './proxy-log.js';
1
8
  /**
2
9
  * Overridable so the survival spec can reach exhaustion in seconds instead of minutes — the same
3
10
  * reason the idle windows take an env override. A positive integer only; anything else falls back,
@@ -7,62 +14,6 @@ export declare const MAX_RECONNECT_ATTEMPTS: number;
7
14
  export declare function reconnectDelayMs(attempt: number): number;
8
15
  /** JSON-RPC id the proxy uses for its own replayed `initialize` — never one the client could send. */
9
16
  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;
49
- /** Name the proxy log after the port it serves. Called before anything can log. */
50
- export declare function setProxyLogPort(port: number): void;
51
- /** The proxy's own log file, so a silent drop leaves a readable trace the agent can go read. */
52
- export declare function proxyLogPath(port?: number | undefined): string;
53
- /**
54
- * Log to stderr (which the agent host usually swallows) AND to ~/.reticle/proxy-<port>.log, which it
55
- * does not. A dropped MCP connection is invisible from the agent's side — no message, no exit code —
56
- * so the one thing that makes it diagnosable at all is a file somebody can read afterwards.
57
- *
58
- * EXPORTED because the crash handlers need it. `installProxyResilience` was wired to the bare stderr
59
- * logger, so the proxy's own uncaught exceptions and unhandled rejections — the exact events that
60
- * present to a human as "the MCP server disconnected" — were written to a stream the editor throws
61
- * away. The handler ran, the process kept serving, and the reason went nowhere. Reported as "the
62
- * proxy has no log file… when it dies the diagnostic dies with it", which was half right: the file
63
- * existed, and the one path that most needed it was not using it.
64
- */
65
- export declare function proxyLog(event: string, fields?: Record<string, unknown>): void;
66
17
  /**
67
18
  * JSON-RPC requests forwarded on behalf of the client that have not been answered yet.
68
19
  *
@@ -143,33 +94,6 @@ export declare class HandshakeReplay {
143
94
  /** True when this inbound daemon line is the echo of our replayed handshake and must not be forwarded. */
144
95
  shouldSuppressInbound(line: string): boolean;
145
96
  }
146
- /** One parsed Server-Sent-Events frame: the event name (defaulted to "message") and its data. */
147
- interface SseFrame {
148
- event: string;
149
- data: string;
150
- }
151
- /**
152
- * Incremental SSE frame parser for the MCP front door.
153
- *
154
- * A single SSE field can split across TCP reads (`da` in one chunk, `ta: {...}` in the next), and a
155
- * server may send CRLF or bare CR line endings — so the framing is stateful and edge-case-prone, yet it
156
- * carried every MCP message the agent sends and was only ever exercised end-to-end. Pulled out of the
157
- * socket handler as a pure, chunk-fed parser so those boundaries are unit-testable: feed raw chunks,
158
- * get back each complete frame (a blank line terminates a frame; `event:` names it, `data:` lines
159
- * accumulate newline-joined; `id:`/`retry:`/comments are ignored — not needed for the bridge).
160
- */
161
- export declare class SseFrameParser {
162
- #private;
163
- push(chunk: string): SseFrame[];
164
- }
165
- /**
166
- * Returns true if something is already listening on the reticle port.
167
- * Uses a plain TCP probe so we don't create a side-effectful SSE session
168
- * inside the daemon just to check reachability.
169
- */
170
- export declare function probeDaemon(port: number): Promise<boolean>;
171
- /** Poll until the daemon's HTTP port accepts connections or the deadline is reached. */
172
- export declare function waitForDaemon(port: number): Promise<void>;
173
97
  /**
174
98
  * Turn an `endpoint` frame's data into a POST target, or null when there is nothing usable in it.
175
99
  *
@@ -181,15 +105,6 @@ export declare function waitForDaemon(port: number): Promise<void>;
181
105
  * to reconnect and how to answer what it cannot send.
182
106
  */
183
107
  export declare function buildSessionUrl(rawData: string, port: number): string | null;
184
- /**
185
- * Bridge stdio ↔ SSE: connects to the running daemon's MCP endpoint and forwards
186
- * Claude Code's stdin/stdout JSON-RPC through it. Never resolves — runs until stdin closes.
187
- *
188
- * A dropped SSE stream used to exit the process, which is what made the agent's `reticle_*` tools
189
- * vanish mid-session with no message and no way for the agent to get them back — only a human running
190
- * `/mcp` could. The daemon demonstrably stays up across these drops, so the stream ending is a
191
- * transport event, not a shutdown: reconnect to it and replay the handshake instead of dying.
192
- */
193
108
  export declare function startMcpProxy(port: number,
194
109
  /**
195
110
  * Bring a daemon back before reconnecting. Without it the proxy retried a DEAD port until the
@@ -197,4 +112,3 @@ export declare function startMcpProxy(port: number,
197
112
  * failure mode for a daemon that crashed, was stopped, or shut itself down as idle.
198
113
  */
199
114
  ensureDaemon?: () => Promise<void>): Promise<never>;
200
- export {};