@reticlehq/server 2.5.0 → 2.7.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 (182) hide show
  1. package/SKILL.md +226 -26
  2. package/dist/bridge/bridge.d.ts +18 -1
  3. package/dist/bridge/bridge.js +59 -6
  4. package/dist/bridge/bridge.test-harness.d.ts +8 -0
  5. package/dist/bridge/bridge.test-harness.js +9 -1
  6. package/dist/cli/chromium-hint.d.ts +40 -0
  7. package/dist/cli/chromium-hint.js +75 -0
  8. package/dist/cli/cli-doctor.d.ts +0 -5
  9. package/dist/cli/cli-doctor.js +74 -14
  10. package/dist/cli/cli-launch.d.ts +1 -0
  11. package/dist/cli/cli-launch.js +4 -1
  12. package/dist/cli/cli-parse-feedback.d.ts +31 -0
  13. package/dist/cli/cli-parse-feedback.js +56 -0
  14. package/dist/cli/cli-parse.d.ts +5 -9
  15. package/dist/cli/cli-parse.js +7 -23
  16. package/dist/cli/cli-port.js +20 -1
  17. package/dist/cli/doctor-daemon-line.d.ts +37 -0
  18. package/dist/cli/doctor-daemon-line.js +44 -0
  19. package/dist/cli/port-holder.d.ts +43 -0
  20. package/dist/cli/port-holder.js +99 -0
  21. package/dist/cli.js +170 -19
  22. package/dist/daemon/daemon-resilience.d.ts +24 -0
  23. package/dist/daemon/daemon-resilience.js +105 -17
  24. package/dist/daemon/daemon.js +26 -1
  25. package/dist/daemon/heartbeat.d.ts +115 -0
  26. package/dist/daemon/heartbeat.js +141 -0
  27. package/dist/daemon/idle-shutdown.d.ts +13 -5
  28. package/dist/daemon/idle-shutdown.js +14 -6
  29. package/dist/daemon/loopback-alias.d.ts +32 -0
  30. package/dist/daemon/loopback-alias.js +80 -0
  31. package/dist/daemon/port-presence.d.ts +76 -0
  32. package/dist/daemon/port-presence.js +93 -0
  33. package/dist/daemon/state-dir.d.ts +7 -0
  34. package/dist/daemon/state-dir.js +59 -0
  35. package/dist/daemon/unclean-predecessor.d.ts +23 -0
  36. package/dist/daemon/unclean-predecessor.js +27 -0
  37. package/dist/events/predicate-eval.d.ts +17 -3
  38. package/dist/events/predicate-eval.js +183 -96
  39. package/dist/events/predicate-parse.d.ts +23 -0
  40. package/dist/events/predicate-parse.js +68 -0
  41. package/dist/events/predicate.d.ts +5 -0
  42. package/dist/events/predicate.js +17 -3
  43. package/dist/events/ring-buffer.d.ts +8 -0
  44. package/dist/events/ring-buffer.js +35 -17
  45. package/dist/flows/flow-replay.d.ts +1 -1
  46. package/dist/flows/flow-replay.js +19 -2
  47. package/dist/flows/flow-step-runners.js +2 -3
  48. package/dist/flows/flow-tools.js +10 -14
  49. package/dist/flows/role-drift-reason.d.ts +20 -0
  50. package/dist/flows/role-drift-reason.js +39 -0
  51. package/dist/flows/suite-flakes.d.ts +33 -0
  52. package/dist/flows/suite-flakes.js +32 -0
  53. package/dist/honesty/blind-spots.d.ts +19 -0
  54. package/dist/honesty/blind-spots.js +33 -0
  55. package/dist/honesty/honesty.d.ts +11 -0
  56. package/dist/honesty/honesty.js +9 -1
  57. package/dist/honesty/verified.d.ts +10 -1
  58. package/dist/honesty/verified.js +42 -3
  59. package/dist/http-server.js +20 -1
  60. package/dist/index.d.ts +1 -0
  61. package/dist/index.js +36 -3
  62. package/dist/init/agent-rules.d.ts +18 -0
  63. package/dist/init/agent-rules.js +28 -5
  64. package/dist/init/cra.d.ts +13 -0
  65. package/dist/init/cra.js +16 -0
  66. package/dist/init/detect.d.ts +14 -0
  67. package/dist/init/detect.js +23 -0
  68. package/dist/init/mcp-clients.d.ts +81 -0
  69. package/dist/init/mcp-clients.js +248 -0
  70. package/dist/init/node-io.js +8 -2
  71. package/dist/init/non-js-project.d.ts +27 -0
  72. package/dist/init/non-js-project.js +60 -0
  73. package/dist/init/plan-framework.d.ts +9 -0
  74. package/dist/init/plan-framework.js +55 -3
  75. package/dist/init/plan.d.ts +14 -0
  76. package/dist/init/plan.js +93 -12
  77. package/dist/init/run.d.ts +18 -1
  78. package/dist/init/run.js +169 -23
  79. package/dist/init/snippets.d.ts +20 -1
  80. package/dist/init/snippets.js +71 -6
  81. package/dist/init/windows-quote.d.ts +38 -0
  82. package/dist/init/windows-quote.js +70 -0
  83. package/dist/mcp/mcp-outage.d.ts +1 -0
  84. package/dist/mcp/mcp-outage.js +8 -1
  85. package/dist/mcp/mcp-proxy.d.ts +11 -1
  86. package/dist/mcp/mcp-proxy.js +39 -9
  87. package/dist/mcp/mcp.js +64 -2
  88. package/dist/mcp/proxy-lifecycle.d.ts +19 -0
  89. package/dist/mcp/proxy-lifecycle.js +21 -0
  90. package/dist/pool/browser-pool.d.ts +9 -0
  91. package/dist/pool/browser-pool.js +38 -9
  92. package/dist/pool/playwright-launcher.js +10 -2
  93. package/dist/session/attachment-history.d.ts +46 -0
  94. package/dist/session/attachment-history.js +65 -0
  95. package/dist/session/live-control-tools.js +39 -2
  96. package/dist/session/live-control.d.ts +7 -9
  97. package/dist/session/live-control.js +34 -1
  98. package/dist/session/no-session-diagnosis.d.ts +11 -4
  99. package/dist/session/no-session-diagnosis.js +95 -14
  100. package/dist/session/no-session-watch.d.ts +3 -1
  101. package/dist/session/no-session-watch.js +10 -2
  102. package/dist/session/pending-commands.js +1 -0
  103. package/dist/session/review-store.d.ts +19 -0
  104. package/dist/session/review-store.js +34 -5
  105. package/dist/session/session-info.d.ts +17 -1
  106. package/dist/session/session-info.js +5 -1
  107. package/dist/session/session-manager.d.ts +12 -0
  108. package/dist/session/session-manager.js +83 -3
  109. package/dist/session/session.d.ts +9 -0
  110. package/dist/session/session.js +20 -0
  111. package/dist/status-payload.d.ts +13 -1
  112. package/dist/status-payload.js +6 -1
  113. package/dist/telemetry/app-instrumented.d.ts +37 -0
  114. package/dist/telemetry/app-instrumented.js +54 -0
  115. package/dist/telemetry/crash-cause.d.ts +47 -0
  116. package/dist/telemetry/crash-cause.js +104 -0
  117. package/dist/telemetry/daemon-telemetry.d.ts +25 -7
  118. package/dist/telemetry/daemon-telemetry.js +32 -13
  119. package/dist/telemetry/error-class.d.ts +26 -0
  120. package/dist/telemetry/error-class.js +52 -0
  121. package/dist/telemetry/feedback-cli.d.ts +7 -2
  122. package/dist/telemetry/feedback-cli.js +49 -5
  123. package/dist/telemetry/feedback-context.d.ts +1 -6
  124. package/dist/telemetry/feedback-context.js +64 -4
  125. package/dist/telemetry/git-facts.d.ts +7 -0
  126. package/dist/telemetry/git-facts.js +4 -4
  127. package/dist/telemetry/project-profile.js +2 -1
  128. package/dist/telemetry/session-metrics.d.ts +45 -3
  129. package/dist/telemetry/session-metrics.js +247 -9
  130. package/dist/telemetry/telemetry.d.ts +21 -1
  131. package/dist/telemetry/telemetry.js +34 -9
  132. package/dist/telemetry/verification-of.js +26 -1
  133. package/dist/tools/act-tools.js +63 -14
  134. package/dist/tools/assert-verdict.d.ts +7 -1
  135. package/dist/tools/assert-verdict.js +22 -3
  136. package/dist/tools/browser-tools.js +8 -2
  137. package/dist/tools/contract-tools.d.ts +0 -5
  138. package/dist/tools/contract-tools.js +26 -0
  139. package/dist/tools/dynamic-tools.js +6 -1
  140. package/dist/tools/error-recovery.d.ts +14 -0
  141. package/dist/tools/error-recovery.js +89 -8
  142. package/dist/tools/feedback-invite.d.ts +48 -0
  143. package/dist/tools/feedback-invite.js +71 -0
  144. package/dist/tools/invoke-tool.d.ts +0 -6
  145. package/dist/tools/invoke-tool.js +76 -7
  146. package/dist/tools/navigate-arrival.d.ts +26 -0
  147. package/dist/tools/navigate-arrival.js +60 -0
  148. package/dist/tools/navigate-result.d.ts +15 -1
  149. package/dist/tools/navigate-result.js +17 -10
  150. package/dist/tools/observe-tools.js +22 -4
  151. package/dist/tools/real-input-attempt.js +10 -5
  152. package/dist/tools/tool-kit.d.ts +60 -5
  153. package/dist/tools/tool-kit.js +55 -24
  154. package/dist/tools/tool-surface.js +13 -0
  155. package/dist/tools/tools.js +22 -0
  156. package/dist/visual/capture-cleanup.d.ts +16 -0
  157. package/dist/visual/capture-cleanup.js +39 -0
  158. package/dist/visual/visual-tools.d.ts +18 -0
  159. package/dist/visual/visual-tools.js +24 -5
  160. package/docs/README.md +55 -0
  161. package/docs/agent-cheatsheet.md +15 -3
  162. package/docs/benchmarks.md +1 -1
  163. package/docs/fixtures-dispatch-receiver.yml +76 -0
  164. package/docs/fixtures.md +17 -0
  165. package/docs/gate-plan.md +98 -0
  166. package/docs/gates.md +117 -0
  167. package/docs/local-registry.md +1 -1
  168. package/docs/matrix/2.5.0/claude-code-machine.json +19 -0
  169. package/docs/matrix/2.5.0/codex-machine.json +20 -0
  170. package/docs/matrix/2.5.0/cursor-machine.json +22 -0
  171. package/docs/matrix/2.5.0/gemini-machine.json +22 -0
  172. package/docs/matrix/2.5.0/opencode-machine.json +22 -0
  173. package/docs/matrix/2.5.0/vscode-machine.json +22 -0
  174. package/docs/matrix/2.5.0/windsurf-machine.json +22 -0
  175. package/docs/matrix/MATRIX.md +26 -0
  176. package/docs/matrix/README.md +59 -0
  177. package/docs/mint.json +1 -1
  178. package/docs/system-map.md +152 -0
  179. package/docs/telemetry-contract.md +15 -3
  180. package/docs/telemetry.md +16 -1
  181. package/docs/token-efficiency.md +2 -2
  182. package/package.json +2 -2
@@ -0,0 +1,248 @@
1
+ /**
2
+ * Where each agent keeps its MCP config, and how to write ours into it.
3
+ *
4
+ * `init` knew two clients: Claude Code (a CLI shell-out) and Cursor (a JSON merge). Everything else
5
+ * got a printed snippet. That is the blocker under the whole release matrix — a per-client release
6
+ * artifact for a client `init` cannot wire measures whether somebody pasted JSON correctly, not
7
+ * whether the product works.
8
+ *
9
+ * Every path and shape below was read from the client's own documentation rather than recalled,
10
+ * because they differ in ways that look like they should not:
11
+ *
12
+ * Cursor / Windsurf / Gemini `mcpServers: { name: { command, args } }` — one shape, three paths
13
+ * VS Code `servers:` — a different key entirely
14
+ * OpenCode `mcp: { name: { type, command: [cmd, ...args] } }` — an ARRAY
15
+ * Codex TOML
16
+ *
17
+ * A single "write mcp.json" assuming the Cursor shape would produce a file three of these ignore:
18
+ * an install that reports success and registers nothing, which is the exact failure class this
19
+ * repo's install gate exists to catch.
20
+ */
21
+ import { NPX, MCP_SERVER_NAME, npxServerArgs } from './mcp.js';
22
+ import { RETICLE_NPM_PACKAGE } from '../version/server-version.js';
23
+ export const McpClient = {
24
+ CLAUDE_CODE: 'claude-code',
25
+ CURSOR: 'cursor',
26
+ WINDSURF: 'windsurf',
27
+ OPENCODE: 'opencode',
28
+ CODEX: 'codex',
29
+ GEMINI: 'gemini',
30
+ VSCODE: 'vscode',
31
+ };
32
+ /** Where the config lives. `cli` means the client owns registration and we shell out to it. */
33
+ export const ConfigScope = {
34
+ HOME: 'home',
35
+ PROJECT: 'project',
36
+ CLI: 'cli',
37
+ };
38
+ /** How the file is written. `toml` is deliberately never auto-merged — see mergeClientConfig. */
39
+ export const ConfigFormat = {
40
+ JSON: 'json',
41
+ TOML: 'toml',
42
+ CLI: 'cli',
43
+ };
44
+ /** The `{ command, args }` entry Cursor, Windsurf, Gemini and VS Code all take. */
45
+ function commandArgsEntry() {
46
+ return { command: NPX, args: npxServerArgs() };
47
+ }
48
+ /**
49
+ * OpenCode's entry. `command` is a single ARRAY containing the executable and its arguments, and
50
+ * `enabled` is explicit — a server registered but disabled is the quietest possible failure.
51
+ */
52
+ function openCodeEntry() {
53
+ return { type: 'local', command: [NPX, ...npxServerArgs()], enabled: true };
54
+ }
55
+ export const MCP_CLIENTS = [
56
+ {
57
+ id: McpClient.CLAUDE_CODE,
58
+ label: 'Claude Code',
59
+ scope: ConfigScope.CLI,
60
+ relPath: '',
61
+ format: ConfigFormat.CLI,
62
+ serversKey: 'mcpServers',
63
+ entry: commandArgsEntry,
64
+ docs: 'registered via `claude mcp add -s user` — see init/mcp.ts',
65
+ },
66
+ {
67
+ id: McpClient.CURSOR,
68
+ label: 'Cursor',
69
+ scope: ConfigScope.HOME,
70
+ relPath: '.cursor/mcp.json',
71
+ format: ConfigFormat.JSON,
72
+ serversKey: 'mcpServers',
73
+ entry: commandArgsEntry,
74
+ docs: 'https://docs.cursor.com — global ~/.cursor/mcp.json, or ./.cursor/mcp.json per project',
75
+ },
76
+ {
77
+ id: McpClient.WINDSURF,
78
+ label: 'Windsurf',
79
+ scope: ConfigScope.HOME,
80
+ relPath: '.codeium/windsurf/mcp_config.json',
81
+ format: ConfigFormat.JSON,
82
+ serversKey: 'mcpServers',
83
+ entry: commandArgsEntry,
84
+ docs: 'Windsurf docs — ~/.codeium/windsurf/mcp_config.json (same shape as Cursor, different path)',
85
+ },
86
+ {
87
+ id: McpClient.GEMINI,
88
+ label: 'Gemini CLI',
89
+ scope: ConfigScope.HOME,
90
+ relPath: '.gemini/settings.json',
91
+ format: ConfigFormat.JSON,
92
+ serversKey: 'mcpServers',
93
+ entry: commandArgsEntry,
94
+ docs: 'https://google-gemini.github.io/gemini-cli/docs/tools/mcp-server.html',
95
+ },
96
+ {
97
+ id: McpClient.VSCODE,
98
+ label: 'VS Code (Copilot)',
99
+ scope: ConfigScope.PROJECT,
100
+ relPath: '.vscode/mcp.json',
101
+ format: ConfigFormat.JSON,
102
+ // NOT `mcpServers`. Writing that key here registers nothing and reports success.
103
+ serversKey: 'servers',
104
+ entry: commandArgsEntry,
105
+ docs: 'VS Code MCP docs — .vscode/mcp.json, key is `servers`',
106
+ },
107
+ {
108
+ id: McpClient.OPENCODE,
109
+ label: 'OpenCode',
110
+ scope: ConfigScope.PROJECT,
111
+ relPath: 'opencode.json',
112
+ format: ConfigFormat.JSON,
113
+ serversKey: 'mcp',
114
+ entry: openCodeEntry,
115
+ docs: 'https://opencode.ai/docs/config — `mcp` key, type:"local", command is an array',
116
+ },
117
+ {
118
+ id: McpClient.CODEX,
119
+ label: 'Codex CLI',
120
+ scope: ConfigScope.HOME,
121
+ relPath: '.codex/config.toml',
122
+ format: ConfigFormat.TOML,
123
+ serversKey: 'mcp_servers',
124
+ entry: commandArgsEntry,
125
+ docs: 'https://developers.openai.com/codex/mcp — ~/.codex/config.toml, [mcp_servers.<name>]',
126
+ },
127
+ ];
128
+ export function clientSpec(id) {
129
+ const found = MCP_CLIENTS.find((spec) => spec.id === id);
130
+ if (found === undefined)
131
+ throw new Error(`no MCP client spec for '${id}'`);
132
+ return found;
133
+ }
134
+ export const ClientMergeStatus = {
135
+ APPLY: 'apply',
136
+ ALREADY: 'already',
137
+ MANUAL: 'manual',
138
+ };
139
+ function parseConfig(existing) {
140
+ if (null === existing || 0 === existing.trim().length)
141
+ return { ok: true, config: {} };
142
+ try {
143
+ const parsed = JSON.parse(existing);
144
+ // Valid JSON that is not a PLAIN OBJECT — `[]`, `3`, `"x"`, `null` — must not fall through to an
145
+ // empty config, because the file then gets rewritten wholesale. Unparseable JSON was always
146
+ // handled conservatively; this adjacent case destroyed the file instead.
147
+ if (typeof parsed !== 'object' || null === parsed || Array.isArray(parsed))
148
+ return { ok: false };
149
+ return { ok: true, config: parsed };
150
+ }
151
+ catch {
152
+ return { ok: false };
153
+ }
154
+ }
155
+ /**
156
+ * Should this entry be left exactly as it is?
157
+ *
158
+ * Two different reasons to leave one alone, and only one is "it is correct":
159
+ * - it already matches what we would write; or
160
+ * - it is not an entry WE wrote. Somebody pointing `reticle` at their own local build made a
161
+ * deliberate choice, and replacing it with the published npx command breaks a setup to fix a
162
+ * problem they do not have.
163
+ *
164
+ * What IS repaired is a stale entry of our own shape — an old pin, a moved command — which
165
+ * presence-only idempotency reported as "already registered" and never fixed, so an upgrade could
166
+ * not repair the thing an upgrade exists to repair.
167
+ */
168
+ function leaveEntryAlone(existing, spec) {
169
+ if (JSON.stringify(existing) === JSON.stringify(spec.entry()))
170
+ return true;
171
+ if (typeof existing !== 'object' || null === existing)
172
+ return true;
173
+ const record = existing;
174
+ // Both shapes carry the package name somewhere in a string; find it without assuming which field.
175
+ const tokens = [record.command, record.args].flat().filter((v) => 'string' === typeof v);
176
+ const ours = tokens.some((t) => t === NPX) && tokens.some((t) => t.includes(RETICLE_NPM_PACKAGE));
177
+ return !ours;
178
+ }
179
+ /**
180
+ * Merge our server into this client's config.
181
+ *
182
+ * TOML is never auto-merged. Editing TOML without a parser is how a config file gets corrupted, and
183
+ * a corrupted `~/.codex/config.toml` costs the user every server they had, not just ours. Printing
184
+ * the exact block is honest; writing a guess is not — so Codex returns MANUAL with a snippet.
185
+ */
186
+ export function mergeClientConfig(spec, existing) {
187
+ if (spec.format !== ConfigFormat.JSON) {
188
+ return { status: ClientMergeStatus.MANUAL, content: existing ?? '' };
189
+ }
190
+ const parsed = parseConfig(existing);
191
+ if (!parsed.ok)
192
+ return { status: ClientMergeStatus.MANUAL, content: existing ?? '' };
193
+ const config = parsed.config;
194
+ const current = config[spec.serversKey];
195
+ const servers = 'object' === typeof current && null !== current && !Array.isArray(current)
196
+ ? current
197
+ : {};
198
+ if (Object.prototype.hasOwnProperty.call(servers, MCP_SERVER_NAME) &&
199
+ leaveEntryAlone(servers[MCP_SERVER_NAME], spec)) {
200
+ return { status: ClientMergeStatus.ALREADY, content: existing ?? '' };
201
+ }
202
+ // Only OUR key is replaced; every other server and top-level key is carried through untouched.
203
+ const merged = {
204
+ ...config,
205
+ [spec.serversKey]: { ...servers, [MCP_SERVER_NAME]: spec.entry() },
206
+ };
207
+ return { status: ClientMergeStatus.APPLY, content: `${JSON.stringify(merged, null, 2)}\n` };
208
+ }
209
+ /**
210
+ * A block the user can paste, for a client we could not write.
211
+ *
212
+ * Every client gets one, including the ones we CAN write: when a merge comes back MANUAL because the
213
+ * file could not be parsed, the snippet is the only actionable thing left to give.
214
+ */
215
+ export function clientSnippet(spec) {
216
+ if (spec.format === ConfigFormat.TOML) {
217
+ const args = npxServerArgs()
218
+ .map((a) => `"${a}"`)
219
+ .join(', ');
220
+ return `[mcp_servers.${MCP_SERVER_NAME}]\ncommand = "${NPX}"\nargs = [${args}]\n`;
221
+ }
222
+ if (spec.format === ConfigFormat.CLI) {
223
+ return `claude mcp add -s user ${MCP_SERVER_NAME} -- ${NPX} ${npxServerArgs().join(' ')}\n`;
224
+ }
225
+ return `${JSON.stringify({ [spec.serversKey]: { [MCP_SERVER_NAME]: spec.entry() } }, null, 2)}\n`;
226
+ }
227
+ /**
228
+ * The path whose existence means "this user has this client".
229
+ *
230
+ * Detection is deliberately conservative and one-directional: we write into a config a client
231
+ * ALREADY has, and never create `~/.gemini/` or `~/.codeium/` for somebody who does not use them.
232
+ * Littering a home directory to register a server nobody asked for is worse than printing a snippet.
233
+ *
234
+ * For a `home` client this is the directory above its config; for a `project` client it is the
235
+ * config file (or its directory) inside the repo, which is unambiguous evidence somebody uses it
236
+ * here. Same reasoning as Cursor's existing rule, which also accepts a project-level `.cursor/`
237
+ * because a fresh Cursor profile has not written `~/.cursor` yet.
238
+ */
239
+ export function clientMarkerRelPath(spec) {
240
+ if (spec.scope === ConfigScope.CLI)
241
+ return '';
242
+ const parts = spec.relPath.split('/');
243
+ return parts.length > 1 ? parts.slice(0, -1).join('/') : spec.relPath;
244
+ }
245
+ /** Clients `init` can wire by writing a file — everything but the CLI-registered one. */
246
+ export function fileBackedClients() {
247
+ return MCP_CLIENTS.filter((spec) => spec.scope !== ConfigScope.CLI);
248
+ }
@@ -8,6 +8,7 @@ import { NodePlatform } from '../platform.js';
8
8
  import { join, dirname, isAbsolute } from 'node:path';
9
9
  import { homedir } from 'node:os';
10
10
  import { spawnSync } from 'node:child_process';
11
+ import { windowsShellArg } from './windows-quote.js';
11
12
  /**
12
13
  * `shell: true` ONLY where it earns its keep.
13
14
  *
@@ -19,11 +20,16 @@ import { spawnSync } from 'node:child_process';
19
20
  function shellOpt() {
20
21
  return NodePlatform.WINDOWS === process.platform ? { shell: true } : {};
21
22
  }
22
- /** Under a shell, quote what the shell would otherwise split. Windows only, where shell is required. */
23
+ /**
24
+ * Under a shell, quote what the shell would otherwise split or interpret. Windows only, where
25
+ * `shell: true` is required (see shellOpt). The rule itself lives in windows-quote.ts as a pure
26
+ * function so it can be tested on every platform — keeping it inside this branch is why it was
27
+ * wrong for as long as it was.
28
+ */
23
29
  function shellSafe(args) {
24
30
  if (process.platform !== NodePlatform.WINDOWS)
25
31
  return [...args];
26
- return args.map((arg) => (/[\s"]/.test(arg) ? `"${arg.replace(/"/g, '\\"')}"` : arg));
32
+ return args.map(windowsShellArg);
27
33
  }
28
34
  export function buildNodeIo(cwd) {
29
35
  // Project-relative by default; absolute paths (e.g. ~/.cursor/mcp.json) pass through unchanged.
@@ -0,0 +1,27 @@
1
+ /**
2
+ * Telling a Python (or Ruby, or Go, or PHP) developer why Reticle cannot wire their app.
3
+ *
4
+ * `reticle init` answered "No package.json found here, and no app directory beneath it either",
5
+ * which reads as *you are in the wrong directory* — so an agent on a Streamlit app went looking for
6
+ * a directory that does not exist, then went looking for a standalone browser bundle to inject by
7
+ * hand, and only worked out the real answer several steps later.
8
+ *
9
+ * The real answer is not about directories at all: Reticle's SDK has to be imported by the app's own
10
+ * JS build, and a server-rendered Python app has no JS build to import it from. That is a property
11
+ * of the project, it is knowable from one look at the filesystem, and saying it plainly costs the
12
+ * reader nothing while the directory message costs them the whole investigation.
13
+ *
14
+ * Deliberately a hint, not a refusal: a Django or Rails app with a `frontend/` package.json is a
15
+ * perfectly ordinary Reticle install, and this only ever speaks when there is no package.json to be
16
+ * found anywhere.
17
+ */
18
+ /** The ecosystem this directory looks like, or undefined when nothing says. */
19
+ export declare function detectNonJsEcosystem(exists: (file: string) => boolean): string | undefined;
20
+ /**
21
+ * What `init` should say when there is no package.json.
22
+ *
23
+ * Two genuinely different situations behind one old sentence: a JS developer standing in the wrong
24
+ * directory, and a developer whose project is not JavaScript at all. The first needs a path; the
25
+ * second needs to know that no path exists and why.
26
+ */
27
+ export declare function noPackageJsonMessage(exists: (file: string) => boolean): string;
@@ -0,0 +1,60 @@
1
+ /**
2
+ * Telling a Python (or Ruby, or Go, or PHP) developer why Reticle cannot wire their app.
3
+ *
4
+ * `reticle init` answered "No package.json found here, and no app directory beneath it either",
5
+ * which reads as *you are in the wrong directory* — so an agent on a Streamlit app went looking for
6
+ * a directory that does not exist, then went looking for a standalone browser bundle to inject by
7
+ * hand, and only worked out the real answer several steps later.
8
+ *
9
+ * The real answer is not about directories at all: Reticle's SDK has to be imported by the app's own
10
+ * JS build, and a server-rendered Python app has no JS build to import it from. That is a property
11
+ * of the project, it is knowable from one look at the filesystem, and saying it plainly costs the
12
+ * reader nothing while the directory message costs them the whole investigation.
13
+ *
14
+ * Deliberately a hint, not a refusal: a Django or Rails app with a `frontend/` package.json is a
15
+ * perfectly ordinary Reticle install, and this only ever speaks when there is no package.json to be
16
+ * found anywhere.
17
+ */
18
+ /** Marker files that identify an ecosystem, and what to call it. */
19
+ const ECOSYSTEM_MARKERS = [
20
+ ['requirements.txt', 'Python'],
21
+ ['pyproject.toml', 'Python'],
22
+ ['Pipfile', 'Python'],
23
+ ['setup.py', 'Python'],
24
+ ['manage.py', 'Python (Django)'],
25
+ ['Gemfile', 'Ruby'],
26
+ ['go.mod', 'Go'],
27
+ ['composer.json', 'PHP'],
28
+ ['Cargo.toml', 'Rust'],
29
+ ['pom.xml', 'Java'],
30
+ ['build.gradle', 'Java'],
31
+ ['mix.exs', 'Elixir'],
32
+ ];
33
+ /** The ecosystem this directory looks like, or undefined when nothing says. */
34
+ export function detectNonJsEcosystem(exists) {
35
+ for (const [marker, name] of ECOSYSTEM_MARKERS) {
36
+ if (exists(marker))
37
+ return name;
38
+ }
39
+ return undefined;
40
+ }
41
+ /**
42
+ * What `init` should say when there is no package.json.
43
+ *
44
+ * Two genuinely different situations behind one old sentence: a JS developer standing in the wrong
45
+ * directory, and a developer whose project is not JavaScript at all. The first needs a path; the
46
+ * second needs to know that no path exists and why.
47
+ */
48
+ export function noPackageJsonMessage(exists) {
49
+ const ecosystem = detectNonJsEcosystem(exists);
50
+ if (ecosystem === undefined) {
51
+ return ('No package.json found here, and no app directory beneath it either. Run `reticle init` from ' +
52
+ "your app's directory, or from a repo root that contains it.");
53
+ }
54
+ return (`This looks like a ${ecosystem} project, and there is no package.json here or in any app ` +
55
+ 'directory beneath it. Reticle instruments a page by having its SDK imported into the app’s own ' +
56
+ 'JavaScript build — so a server-rendered app with no JS build has nowhere to load it from, and ' +
57
+ 'there is no directory you could run this from that would change that. ' +
58
+ 'If this project does have a JS front end (a `frontend/`, `client/` or `assets/` directory with ' +
59
+ 'its own package.json), run `reticle init` there, or point at it with `--app <dir>`.');
60
+ }
@@ -38,6 +38,15 @@ export declare function nextSteps(input: PlanInput): Step[];
38
38
  * bundler never processes for modules. Reported from a real cra-redux-saga app.
39
39
  */
40
40
  export declare function craSteps(input: PlanInput): Step[];
41
+ /**
42
+ * Nuxt: one manual step carrying the whole recipe, and no pretence of more.
43
+ *
44
+ * There is no Nuxt app in `apps/` and no CI gate for one, so an auto-written plugin would be a
45
+ * support claim nothing backs — the same reasoning SvelteKit already carries. What this DOES fix is
46
+ * everything that made the previous fall-through actively wrong: Nuxt was classified as `html`, so
47
+ * it was handed the React kit and a localhost-guarded snippet that cannot run in a Vue app.
48
+ */
49
+ export declare function nuxtSteps(input: PlanInput): Step[];
41
50
  export declare function svelteKitSteps(input: PlanInput): Step[];
42
51
  /**
43
52
  * Astro: the config define + build target, and the connect script in ONE layout.
@@ -6,9 +6,9 @@
6
6
  import { patchViteConfig, VitePatchKind } from './vite-config.js';
7
7
  import { patchNextConfig, patchRootLayout, patchPagesApp } from './next-patch.js';
8
8
  import { patchAstroConfig, patchAstroLayout } from './astro-patch.js';
9
- import { CRA_DEV_MODULE_IMPORT, CRA_DEV_MODULE_PATH, CRA_ENV_PATH, TOKEN_VAR, craDevModuleFile, craEnvPatch, craImportPatch, } from './cra.js';
9
+ import { CRA_DEV_MODULE_IMPORT, CRA_DEV_MODULE_PATH, CRA_ENV_PATH, CRA_TOKEN_PER_MACHINE_NOTICE, TOKEN_VAR, craDevModuleFile, craEnvPatch, craImportPatch, } from './cra.js';
10
10
  import { PatchKind } from './patch-kind.js';
11
- import { viteManual, NEXT_LAYOUT_MANUAL, NEXT_LAYOUT_PATH, viteDevModuleFile, VITE_DEV_MODULE_PATH, nextReticleDevFile, NEXT_RETICLE_DEV_PATH, nextConfigManual, svelteKitHooksFile, SVELTEKIT_HOOKS_PATH, UNVERIFIED_FRAMEWORK_NOTE, astroManual, } from './snippets.js';
11
+ import { viteManual, NEXT_LAYOUT_MANUAL, NEXT_LAYOUT_PATH, viteDevModuleFile, VITE_DEV_MODULE_PATH, nextReticleDevFile, NEXT_RETICLE_DEV_PATH, nextConfigManual, svelteKitHooksFile, SVELTEKIT_HOOKS_PATH, UNVERIFIED_FRAMEWORK_NOTE, astroManual, nuxtManual, NUXT_PLUGIN_PATH, } from './snippets.js';
12
12
  import { StepStatus } from './plan.js';
13
13
  /** What adding `reticle()` to a Vite config buys, which differs by framework. */
14
14
  export const VITE_PLUGIN_DETAIL = {
@@ -46,6 +46,7 @@ function capabilitiesStep(input) {
46
46
  const found = testids.length > 0
47
47
  ? `${String(testids.length)} data-testid values`
48
48
  : 'no data-testid values yet';
49
+ const nothingToRegister = 0 === testids.length && 0 === stores.length;
49
50
  return [
50
51
  {
51
52
  title: CAPABILITIES_TITLE,
@@ -55,6 +56,28 @@ function capabilitiesStep(input) {
55
56
  write: { path: VITE_DEV_MODULE_PATH, content: viteDevModuleFile(testids, stores) },
56
57
  dependsOnInstall: true,
57
58
  },
59
+ // The write is real; what it registers is not. `registerCapabilities({ testids: [], signals: [],
60
+ // stores: [] })` registers nothing, so `hasCapabilities` stays false — correctly — and the ✓
61
+ // above reads as if the feature is on. Reported as exactly that confusion: "hasCapabilities false
62
+ // on every session while init said ✓ Capabilities + store".
63
+ //
64
+ // Beside the write rather than replacing it, for two reasons: only APPLY steps are written
65
+ // (run.ts), and SKILL.md tells the reader to skip ✓ lines — so a caveat carried on the ✓ is a
66
+ // caveat nobody reads.
67
+ ...(nothingToRegister
68
+ ? [
69
+ {
70
+ title: 'Capabilities are empty until you edit that file',
71
+ target: VITE_DEV_MODULE_PATH,
72
+ status: StepStatus.NOTICE,
73
+ detail: 'the module was written but registers nothing — no data-testid values were found and ' +
74
+ 'no state library was detected, so `hasCapabilities` stays false and reticle_state ' +
75
+ 'has nothing to read. Add data-testid to the elements your main flow touches, or ' +
76
+ 'register a store in that file, and the agent gains what the app BELIEVES rather than ' +
77
+ 'only what it rendered.',
78
+ },
79
+ ]
80
+ : []),
58
81
  ];
59
82
  }
60
83
  export function viteSteps(input, detail = VITE_PLUGIN_DETAIL.VITE) {
@@ -219,6 +242,14 @@ export function craSteps(input) {
219
242
  detail: `set ${TOKEN_VAR} (the only channel CRA inlines) — ${CRA_ENV_PATH} is gitignored, so each teammate must run \`reticle init\` on their own machine`,
220
243
  write: { path: CRA_ENV_PATH, content: env },
221
244
  });
245
+ // Beside the write, not inside it. A ✓ line is one SKILL.md tells the reader to skip, and this
246
+ // is the fact that decides whether the install works for anyone but the person running it.
247
+ steps.push({
248
+ title: 'Pairing token is per-machine',
249
+ target: CRA_ENV_PATH,
250
+ status: StepStatus.NOTICE,
251
+ detail: CRA_TOKEN_PER_MACHINE_NOTICE,
252
+ });
222
253
  }
223
254
  else if ('' === token) {
224
255
  // No daemon has ever run here, so there is no token to inline. Omitting the step entirely made
@@ -227,7 +258,10 @@ export function craSteps(input) {
227
258
  title: 'Pairing token',
228
259
  target: CRA_ENV_PATH,
229
260
  status: StepStatus.MANUAL,
230
- detail: `no pairing token yet — run \`reticle start\`, then \`reticle init\` again to write ${TOKEN_VAR}`,
261
+ // `reticle serve`, not `reticle start` — the latter is not a verb this CLI dispatches, and
262
+ // this message is read by someone whose app boots and never pairs. Handing them a command
263
+ // that errors is a second dead end on the first.
264
+ detail: `no pairing token yet — the daemon writes one on first run. Start it with \`reticle serve\` (or let your agent run \`reticle mcp\`), then \`reticle init\` again to write ${TOKEN_VAR}`,
231
265
  });
232
266
  }
233
267
  if (null === entry) {
@@ -257,6 +291,24 @@ export function craSteps(input) {
257
291
  });
258
292
  return steps;
259
293
  }
294
+ /**
295
+ * Nuxt: one manual step carrying the whole recipe, and no pretence of more.
296
+ *
297
+ * There is no Nuxt app in `apps/` and no CI gate for one, so an auto-written plugin would be a
298
+ * support claim nothing backs — the same reasoning SvelteKit already carries. What this DOES fix is
299
+ * everything that made the previous fall-through actively wrong: Nuxt was classified as `html`, so
300
+ * it was handed the React kit and a localhost-guarded snippet that cannot run in a Vue app.
301
+ */
302
+ export function nuxtSteps(input) {
303
+ return [
304
+ {
305
+ title: 'Connect snippet (Nuxt)',
306
+ target: NUXT_PLUGIN_PATH,
307
+ status: StepStatus.MANUAL,
308
+ detail: nuxtManual(input.options.port, input.options.projectId),
309
+ },
310
+ ];
311
+ }
260
312
  export function svelteKitSteps(input) {
261
313
  const unverified = {
262
314
  title: 'SvelteKit is UNVERIFIED',
@@ -4,6 +4,7 @@
4
4
  * runner performs the `write` side-effects; this module decides *what* should happen.
5
5
  */
6
6
  import { Framework, type Detection } from './detect.js';
7
+ import { type McpClient } from './mcp-clients.js';
7
8
  /**
8
9
  * Pin the SDK to the CLI's own version.
9
10
  *
@@ -14,6 +15,8 @@ import { Framework, type Detection } from './detect.js';
14
15
  * irrelevant, and a skewed pair impossible to install by accident.
15
16
  */
16
17
  export declare function pinnedPackages(packages: readonly string[], version: string | undefined): readonly string[];
18
+ /** The dev-dependencies `reticle init` installs for a given framework — kit first, build plugin next. */
19
+ export declare function frameworkPackages(framework: Framework): readonly string[];
17
20
  /** Exported so the init telemetry can tell an MCP-registration failure from a dependency install. */
18
21
  export declare const MCP_TARGET = "global (claude user scope)";
19
22
  /** True when this step is what makes the app dial the daemon. */
@@ -90,6 +93,17 @@ export interface PlanInput {
90
93
  cursorConfig: string | null;
91
94
  /** Absolute path of ~/.cursor/mcp.json (the write target). */
92
95
  cursorConfigPath: string;
96
+ /**
97
+ * Every OTHER MCP client detected on this machine, with its config path and current content.
98
+ *
99
+ * Claude Code and Cursor keep their own steps — Claude registers through its CLI, and Cursor's
100
+ * predates this. Everything else is uniform: read the file, merge, write.
101
+ */
102
+ detectedClients?: readonly {
103
+ id: McpClient;
104
+ configPath: string;
105
+ existing: string | null;
106
+ }[] | undefined;
93
107
  /** Discovered Vite config: its path + source, or null if none found. */
94
108
  viteConfig: {
95
109
  path: string;