@reticlehq/init 2.14.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 (114) hide show
  1. package/LICENSE +105 -0
  2. package/NOTICE +5 -0
  3. package/README.md +29 -0
  4. package/dist/agent-rules.d.ts +116 -0
  5. package/dist/agent-rules.js +279 -0
  6. package/dist/app-choice.d.ts +29 -0
  7. package/dist/app-choice.js +39 -0
  8. package/dist/astro-host.d.ts +21 -0
  9. package/dist/astro-host.js +27 -0
  10. package/dist/astro-patch.d.ts +33 -0
  11. package/dist/astro-patch.js +103 -0
  12. package/dist/brace-scan.d.ts +24 -0
  13. package/dist/brace-scan.js +46 -0
  14. package/dist/capabilities.d.ts +40 -0
  15. package/dist/capabilities.js +167 -0
  16. package/dist/closing-hint.d.ts +35 -0
  17. package/dist/closing-hint.js +122 -0
  18. package/dist/codex-toml.d.ts +10 -0
  19. package/dist/codex-toml.js +144 -0
  20. package/dist/connect-steps.d.ts +55 -0
  21. package/dist/connect-steps.js +92 -0
  22. package/dist/cra.d.ts +82 -0
  23. package/dist/cra.js +183 -0
  24. package/dist/csp-check.d.ts +49 -0
  25. package/dist/csp-check.js +168 -0
  26. package/dist/csp-doctor.d.ts +32 -0
  27. package/dist/csp-doctor.js +62 -0
  28. package/dist/desktop-doctor.d.ts +46 -0
  29. package/dist/desktop-doctor.js +201 -0
  30. package/dist/detect.d.ts +225 -0
  31. package/dist/detect.js +334 -0
  32. package/dist/dev-script.d.ts +51 -0
  33. package/dist/dev-script.js +66 -0
  34. package/dist/dev-server-ports.d.ts +20 -0
  35. package/dist/dev-server-ports.js +45 -0
  36. package/dist/electron-patch.d.ts +11 -0
  37. package/dist/electron-patch.js +70 -0
  38. package/dist/electron-vite-patch.d.ts +12 -0
  39. package/dist/electron-vite-patch.js +100 -0
  40. package/dist/existing-config.d.ts +31 -0
  41. package/dist/existing-config.js +63 -0
  42. package/dist/format-generated.d.ts +18 -0
  43. package/dist/format-generated.js +48 -0
  44. package/dist/framework-adapter.d.ts +60 -0
  45. package/dist/framework-adapter.js +138 -0
  46. package/dist/host.d.ts +41 -0
  47. package/dist/host.js +21 -0
  48. package/dist/index.d.ts +25 -0
  49. package/dist/index.js +25 -0
  50. package/dist/init-failure.d.ts +19 -0
  51. package/dist/init-failure.js +18 -0
  52. package/dist/init-opt-out.d.ts +17 -0
  53. package/dist/init-opt-out.js +30 -0
  54. package/dist/install-hint.d.ts +9 -0
  55. package/dist/install-hint.js +41 -0
  56. package/dist/install-retries.d.ts +22 -0
  57. package/dist/install-retries.js +65 -0
  58. package/dist/install-source-config.d.ts +18 -0
  59. package/dist/install-source-config.js +34 -0
  60. package/dist/lockfiles.d.ts +14 -0
  61. package/dist/lockfiles.js +49 -0
  62. package/dist/mcp-clients.d.ts +94 -0
  63. package/dist/mcp-clients.js +327 -0
  64. package/dist/mcp-registered.d.ts +12 -0
  65. package/dist/mcp-registered.js +14 -0
  66. package/dist/mcp.d.ts +89 -0
  67. package/dist/mcp.js +129 -0
  68. package/dist/next-patch.d.ts +49 -0
  69. package/dist/next-patch.js +237 -0
  70. package/dist/node-io.d.ts +22 -0
  71. package/dist/node-io.js +150 -0
  72. package/dist/non-js-project.d.ts +44 -0
  73. package/dist/non-js-project.js +137 -0
  74. package/dist/nuxt-patch.d.ts +18 -0
  75. package/dist/nuxt-patch.js +33 -0
  76. package/dist/patch-kind.d.ts +20 -0
  77. package/dist/patch-kind.js +10 -0
  78. package/dist/plan-electron-vite.d.ts +16 -0
  79. package/dist/plan-electron-vite.js +72 -0
  80. package/dist/plan-framework.d.ts +122 -0
  81. package/dist/plan-framework.js +773 -0
  82. package/dist/plan.d.ts +312 -0
  83. package/dist/plan.js +650 -0
  84. package/dist/platform.d.ts +16 -0
  85. package/dist/platform.js +16 -0
  86. package/dist/preflight.d.ts +30 -0
  87. package/dist/preflight.js +52 -0
  88. package/dist/project-id.d.ts +12 -0
  89. package/dist/project-id.js +25 -0
  90. package/dist/refresh-rules.d.ts +40 -0
  91. package/dist/refresh-rules.js +51 -0
  92. package/dist/remember-project.d.ts +24 -0
  93. package/dist/remember-project.js +40 -0
  94. package/dist/run.d.ts +122 -0
  95. package/dist/run.js +762 -0
  96. package/dist/slash-command.d.ts +27 -0
  97. package/dist/slash-command.js +132 -0
  98. package/dist/snippets.d.ts +267 -0
  99. package/dist/snippets.js +898 -0
  100. package/dist/tanstack-start.d.ts +18 -0
  101. package/dist/tanstack-start.js +57 -0
  102. package/dist/version.d.ts +18 -0
  103. package/dist/version.js +23 -0
  104. package/dist/vite-config.d.ts +18 -0
  105. package/dist/vite-config.js +111 -0
  106. package/dist/vite-owning-config.d.ts +25 -0
  107. package/dist/vite-owning-config.js +219 -0
  108. package/dist/windows-quote.d.ts +38 -0
  109. package/dist/windows-quote.js +70 -0
  110. package/dist/workspace-apps.d.ts +40 -0
  111. package/dist/workspace-apps.js +188 -0
  112. package/dist/workspace-redirect.d.ts +22 -0
  113. package/dist/workspace-redirect.js +97 -0
  114. package/package.json +53 -0
@@ -0,0 +1,94 @@
1
+ export declare const McpClient: {
2
+ readonly CLAUDE_CODE: "claude-code";
3
+ readonly CURSOR: "cursor";
4
+ readonly WINDSURF: "windsurf";
5
+ readonly OPENCODE: "opencode";
6
+ readonly ANTIGRAVITY: "antigravity";
7
+ readonly CODEX: "codex";
8
+ readonly GEMINI: "gemini";
9
+ readonly VSCODE: "vscode";
10
+ };
11
+ export type McpClient = (typeof McpClient)[keyof typeof McpClient];
12
+ /** Where the config lives. `cli` means the client owns registration and we shell out to it. */
13
+ export declare const ConfigScope: {
14
+ readonly HOME: "home";
15
+ readonly PROJECT: "project";
16
+ readonly CLI: "cli";
17
+ };
18
+ export type ConfigScope = (typeof ConfigScope)[keyof typeof ConfigScope];
19
+ /** How the file is written. `toml` is deliberately never auto-merged — see mergeClientConfig. */
20
+ export declare const ConfigFormat: {
21
+ readonly JSON: "json";
22
+ readonly TOML: "toml";
23
+ readonly CLI: "cli";
24
+ };
25
+ export type ConfigFormat = (typeof ConfigFormat)[keyof typeof ConfigFormat];
26
+ export interface ClientSpec {
27
+ id: McpClient;
28
+ label: string;
29
+ scope: ConfigScope;
30
+ /** Relative to `$HOME` for `home`, to the project root for `project`. Empty for `cli`. */
31
+ relPath: string;
32
+ format: ConfigFormat;
33
+ /** The object key holding the server map — `mcpServers`, `servers`, or `mcp`. */
34
+ serversKey: string;
35
+ /** Our entry, in THIS client's shape. */
36
+ entry: () => unknown;
37
+ /** Where the shape came from, so the next person can re-check it rather than trust this file. */
38
+ docs: string;
39
+ }
40
+ export declare const MCP_CLIENTS: readonly ClientSpec[];
41
+ export declare function clientSpec(id: McpClient): ClientSpec;
42
+ export declare const ClientMergeStatus: {
43
+ readonly APPLY: "apply";
44
+ readonly ALREADY: "already";
45
+ readonly MANUAL: "manual";
46
+ };
47
+ export type ClientMergeStatus = (typeof ClientMergeStatus)[keyof typeof ClientMergeStatus];
48
+ interface ClientMergeResult {
49
+ status: ClientMergeStatus;
50
+ /** Full file content to write. Byte-identical to `existing` when not `apply`. */
51
+ content: string;
52
+ }
53
+ /**
54
+ * Merge our server into this client's config.
55
+ *
56
+ * TOML is never auto-merged. Editing TOML without a parser is how a config file gets corrupted, and
57
+ * a corrupted `~/.codex/config.toml` costs the user every server they had, not just ours. Printing
58
+ * the exact block is honest; writing a guess is not — so Codex returns MANUAL with a snippet.
59
+ *
60
+ * Not writing it is no reason not to READ it, though. Reporting `add this by hand` to somebody who
61
+ * already has the table is a warning they must not act on, and the setup contract makes a `⚠`
62
+ * blocking (#681). Detection is read-only and cannot corrupt anything.
63
+ */
64
+ export declare function mergeClientConfig(spec: ClientSpec, existing: string | null): ClientMergeResult;
65
+ /**
66
+ * A block the user can paste, for a client we could not write.
67
+ *
68
+ * Every client gets one, including the ones we CAN write: when a merge comes back MANUAL because the
69
+ * file could not be parsed, the snippet is the only actionable thing left to give.
70
+ */
71
+ export declare function clientSnippet(spec: ClientSpec): string;
72
+ /**
73
+ * The path whose existence means "this user has this client".
74
+ *
75
+ * Detection is deliberately conservative and one-directional: we write into a config a client
76
+ * ALREADY has, and never create `~/.gemini/` or `~/.codeium/` for somebody who does not use them.
77
+ * Littering a home directory to register a server nobody asked for is worse than printing a snippet.
78
+ *
79
+ * For a `home` client this is the directory above its config; for a `project` client it is the
80
+ * config file (or its directory) inside the repo, which is unambiguous evidence somebody uses it
81
+ * here. Same reasoning as Cursor's existing rule, which also accepts a project-level `.cursor/`
82
+ * because a fresh Cursor profile has not written `~/.cursor` yet.
83
+ */
84
+ export declare function clientMarkerRelPath(spec: ClientSpec): string;
85
+ /**
86
+ * Project-relative directory whose presence signals Cursor is in use for this repo.
87
+ *
88
+ * A fresh Cursor profile has not written `~/.cursor` yet, so the home marker alone misses real
89
+ * users. A project-level `.cursor/` is unambiguous evidence somebody uses Cursor here.
90
+ */
91
+ export declare const CURSOR_PROJECT_MARKER = ".cursor";
92
+ /** Clients `init` can wire by writing a file — everything but the CLI-registered one. */
93
+ export declare function fileBackedClients(): readonly ClientSpec[];
94
+ export {};
@@ -0,0 +1,327 @@
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, isReticleRegistration } from './mcp.js';
22
+ import { codexServerTokens } from './codex-toml.js';
23
+ export const McpClient = {
24
+ CLAUDE_CODE: 'claude-code',
25
+ CURSOR: 'cursor',
26
+ WINDSURF: 'windsurf',
27
+ OPENCODE: 'opencode',
28
+ ANTIGRAVITY: 'antigravity',
29
+ CODEX: 'codex',
30
+ GEMINI: 'gemini',
31
+ VSCODE: 'vscode',
32
+ };
33
+ /** Where the config lives. `cli` means the client owns registration and we shell out to it. */
34
+ export const ConfigScope = {
35
+ HOME: 'home',
36
+ PROJECT: 'project',
37
+ CLI: 'cli',
38
+ };
39
+ /** How the file is written. `toml` is deliberately never auto-merged — see mergeClientConfig. */
40
+ export const ConfigFormat = {
41
+ JSON: 'json',
42
+ TOML: 'toml',
43
+ CLI: 'cli',
44
+ };
45
+ /** The `{ command, args }` entry Cursor, Windsurf, Gemini and VS Code all take. */
46
+ function commandArgsEntry() {
47
+ return { command: NPX, args: npxServerArgs() };
48
+ }
49
+ /**
50
+ * OpenCode's entry. `command` is a single ARRAY containing the executable and its arguments, and
51
+ * `enabled` is explicit — a server registered but disabled is the quietest possible failure.
52
+ */
53
+ function openCodeEntry() {
54
+ return { type: 'local', command: [NPX, ...npxServerArgs()], enabled: true };
55
+ }
56
+ export const MCP_CLIENTS = [
57
+ {
58
+ id: McpClient.CLAUDE_CODE,
59
+ label: 'Claude Code',
60
+ scope: ConfigScope.CLI,
61
+ relPath: '',
62
+ format: ConfigFormat.CLI,
63
+ serversKey: 'mcpServers',
64
+ entry: commandArgsEntry,
65
+ docs: 'registered via `claude mcp add -s user` — see init/mcp.ts',
66
+ },
67
+ {
68
+ id: McpClient.CURSOR,
69
+ label: 'Cursor',
70
+ scope: ConfigScope.HOME,
71
+ relPath: '.cursor/mcp.json',
72
+ format: ConfigFormat.JSON,
73
+ serversKey: 'mcpServers',
74
+ entry: commandArgsEntry,
75
+ docs: 'https://docs.cursor.com — global ~/.cursor/mcp.json, or ./.cursor/mcp.json per project',
76
+ },
77
+ {
78
+ id: McpClient.WINDSURF,
79
+ label: 'Windsurf',
80
+ scope: ConfigScope.HOME,
81
+ relPath: '.codeium/windsurf/mcp_config.json',
82
+ format: ConfigFormat.JSON,
83
+ serversKey: 'mcpServers',
84
+ entry: commandArgsEntry,
85
+ docs: 'Windsurf docs — ~/.codeium/windsurf/mcp_config.json (same shape as Cursor, different path)',
86
+ },
87
+ {
88
+ id: McpClient.GEMINI,
89
+ label: 'Gemini CLI',
90
+ scope: ConfigScope.HOME,
91
+ relPath: '.gemini/settings.json',
92
+ format: ConfigFormat.JSON,
93
+ serversKey: 'mcpServers',
94
+ entry: commandArgsEntry,
95
+ docs: 'https://google-gemini.github.io/gemini-cli/docs/tools/mcp-server.html',
96
+ },
97
+ {
98
+ id: McpClient.VSCODE,
99
+ label: 'VS Code (Copilot)',
100
+ scope: ConfigScope.PROJECT,
101
+ relPath: '.vscode/mcp.json',
102
+ format: ConfigFormat.JSON,
103
+ // NOT `mcpServers`. Writing that key here registers nothing and reports success.
104
+ serversKey: 'servers',
105
+ entry: commandArgsEntry,
106
+ docs: 'VS Code MCP docs — .vscode/mcp.json, key is `servers`',
107
+ },
108
+ {
109
+ id: McpClient.OPENCODE,
110
+ label: 'OpenCode',
111
+ /**
112
+ * HOME, not PROJECT — OpenCode is installed globally and configured globally, like Cursor.
113
+ *
114
+ * It was project-scoped as `opencode.json`, and registration is gated on the marker already
115
+ * existing, so a project that had never written one was skipped in SILENCE: the user has OpenCode
116
+ * installed, `init` says nothing about it, and the tools never appear. In the field every OpenCode
117
+ * user connected a client and produced zero tool calls and zero app connections, which is what
118
+ * "you were never wired" looks like from outside.
119
+ *
120
+ * Path verified against a real install (OpenCode 1.3.17), not recalled. The extension is `.jsonc`,
121
+ * which the old project marker could not have matched even where a project config existed. JSONC
122
+ * permits comments, and a config that genuinely has them fails to parse — which `mergeClientConfig`
123
+ * already answers with MANUAL and a printed block, rather than rewriting somebody's file and
124
+ * dropping their comments.
125
+ */
126
+ scope: ConfigScope.HOME,
127
+ relPath: '.config/opencode/opencode.jsonc',
128
+ format: ConfigFormat.JSON,
129
+ serversKey: 'mcp',
130
+ entry: openCodeEntry,
131
+ docs: 'https://opencode.ai/docs/config — `mcp` key, type:"local", command is an array',
132
+ },
133
+ {
134
+ id: McpClient.ANTIGRAVITY,
135
+ label: 'Antigravity',
136
+ /**
137
+ * Nine users connected an MCP client through Antigravity and drove nothing at all.
138
+ *
139
+ * `init` did not know this client, so they hand-wired a registration - the connections are in the
140
+ * field data, so they got that far - and then never ran `init`, never instrumented an app, and
141
+ * never called a tool. Nothing in that state tells anyone there is a second half.
142
+ *
143
+ * Path and shape from Google's documentation rather than recall: `mcpServers`, at
144
+ * `~/.gemini/config/mcp_config.json`, and one file serves the 2.0 IDE, the CLI and the SDK. Same
145
+ * `command`/`args` object Cursor, Windsurf and Gemini CLI already use.
146
+ *
147
+ * It shares the `~/.gemini` tree with Gemini CLI, which is why the marker matters: registration is
148
+ * gated on the marker DIRECTORY, and these resolve to `.gemini/config` and `.gemini` respectively,
149
+ * so having one installed cannot make `init` write a config for the other.
150
+ *
151
+ * NOT verified against a running install. The path is documented, the merge is the shared JSON
152
+ * path that every other client here uses, and an unparseable config still degrades to a printed
153
+ * block rather than a rewrite.
154
+ */
155
+ scope: ConfigScope.HOME,
156
+ relPath: '.gemini/config/mcp_config.json',
157
+ format: ConfigFormat.JSON,
158
+ serversKey: 'mcpServers',
159
+ entry: commandArgsEntry,
160
+ docs: 'https://antigravity.google/docs/mcp — ~/.gemini/config/mcp_config.json, `mcpServers` key',
161
+ },
162
+ {
163
+ id: McpClient.CODEX,
164
+ label: 'Codex CLI',
165
+ scope: ConfigScope.HOME,
166
+ relPath: '.codex/config.toml',
167
+ format: ConfigFormat.TOML,
168
+ serversKey: 'mcp_servers',
169
+ entry: commandArgsEntry,
170
+ docs: 'https://developers.openai.com/codex/mcp — ~/.codex/config.toml, [mcp_servers.<name>]',
171
+ },
172
+ ];
173
+ export function clientSpec(id) {
174
+ const found = MCP_CLIENTS.find((spec) => spec.id === id);
175
+ if (found === undefined)
176
+ throw new Error(`no MCP client spec for '${id}'`);
177
+ return found;
178
+ }
179
+ export const ClientMergeStatus = {
180
+ APPLY: 'apply',
181
+ ALREADY: 'already',
182
+ MANUAL: 'manual',
183
+ };
184
+ function parseConfig(existing) {
185
+ if (null === existing || 0 === existing.trim().length)
186
+ return { ok: true, config: {} };
187
+ try {
188
+ const parsed = JSON.parse(existing);
189
+ // Valid JSON that is not a PLAIN OBJECT — `[]`, `3`, `"x"`, `null` — must not fall through to an
190
+ // empty config, because the file then gets rewritten wholesale. Unparseable JSON was always
191
+ // handled conservatively; this adjacent case destroyed the file instead.
192
+ if (typeof parsed !== 'object' || null === parsed || Array.isArray(parsed))
193
+ return { ok: false };
194
+ return { ok: true, config: parsed };
195
+ }
196
+ catch {
197
+ return { ok: false };
198
+ }
199
+ }
200
+ /**
201
+ * Should this entry be left exactly as it is?
202
+ *
203
+ * Two different reasons to leave one alone, and only one is "it is correct":
204
+ * - it already matches what we would write; or
205
+ * - it is not an entry WE wrote. Somebody pointing `reticle` at their own local build made a
206
+ * deliberate choice, and replacing it with the published npx command breaks a setup to fix a
207
+ * problem they do not have.
208
+ *
209
+ * What IS repaired is a stale entry of our own shape — an old pin, a moved command — which
210
+ * presence-only idempotency reported as "already registered" and never fixed, so an upgrade could
211
+ * not repair the thing an upgrade exists to repair — and a WRONG one, naming a Reticle package that
212
+ * has no MCP server in it (see isReticleRegistration).
213
+ */
214
+ function leaveEntryAlone(existing, spec) {
215
+ if (JSON.stringify(existing) === JSON.stringify(spec.entry()))
216
+ return true;
217
+ if (typeof existing !== 'object' || null === existing)
218
+ return true;
219
+ const record = existing;
220
+ // Both shapes carry the package name somewhere in a string; find it without assuming which field.
221
+ const tokens = [record.command, record.args].flat().filter((v) => 'string' === typeof v);
222
+ return !isReticleRegistration(tokens);
223
+ }
224
+ /**
225
+ * Is this TOML config's existing entry one we should leave exactly as it is?
226
+ *
227
+ * The TOML half of {@link leaveEntryAlone}, over the strings the config declares rather than a
228
+ * parsed object. `false` when the table is absent, when it is a Reticle registration that has
229
+ * drifted from what we would write, and when the shape was not one the scanner reads — every one of
230
+ * those keeps today's MANUAL answer, and the snippet is then the actionable thing to print.
231
+ */
232
+ function tomlEntryIsSettled(spec, existing) {
233
+ const tokens = codexServerTokens(existing, spec.serversKey);
234
+ if (0 === tokens.length)
235
+ return false;
236
+ const expected = [NPX, ...npxServerArgs()];
237
+ // Same strings, and no others: an entry carrying an extra flag is not the one we would write.
238
+ if (tokens.length === expected.length && expected.every((t) => tokens.includes(t)))
239
+ return true;
240
+ // Not ours to touch. Somebody pointing `reticle` at their own local build made a deliberate
241
+ // choice, and telling them to hand-edit it back is the same wrong answer as overwriting it.
242
+ return !isReticleRegistration(tokens);
243
+ }
244
+ /**
245
+ * Merge our server into this client's config.
246
+ *
247
+ * TOML is never auto-merged. Editing TOML without a parser is how a config file gets corrupted, and
248
+ * a corrupted `~/.codex/config.toml` costs the user every server they had, not just ours. Printing
249
+ * the exact block is honest; writing a guess is not — so Codex returns MANUAL with a snippet.
250
+ *
251
+ * Not writing it is no reason not to READ it, though. Reporting `add this by hand` to somebody who
252
+ * already has the table is a warning they must not act on, and the setup contract makes a `⚠`
253
+ * blocking (#681). Detection is read-only and cannot corrupt anything.
254
+ */
255
+ export function mergeClientConfig(spec, existing) {
256
+ if (spec.format !== ConfigFormat.JSON) {
257
+ if (ConfigFormat.TOML === spec.format && tomlEntryIsSettled(spec, existing)) {
258
+ return { status: ClientMergeStatus.ALREADY, content: existing ?? '' };
259
+ }
260
+ return { status: ClientMergeStatus.MANUAL, content: existing ?? '' };
261
+ }
262
+ const parsed = parseConfig(existing);
263
+ if (!parsed.ok)
264
+ return { status: ClientMergeStatus.MANUAL, content: existing ?? '' };
265
+ const config = parsed.config;
266
+ const current = config[spec.serversKey];
267
+ const servers = 'object' === typeof current && null !== current && !Array.isArray(current)
268
+ ? current
269
+ : {};
270
+ if (Object.prototype.hasOwnProperty.call(servers, MCP_SERVER_NAME) &&
271
+ leaveEntryAlone(servers[MCP_SERVER_NAME], spec)) {
272
+ return { status: ClientMergeStatus.ALREADY, content: existing ?? '' };
273
+ }
274
+ // Only OUR key is replaced; every other server and top-level key is carried through untouched.
275
+ const merged = {
276
+ ...config,
277
+ [spec.serversKey]: { ...servers, [MCP_SERVER_NAME]: spec.entry() },
278
+ };
279
+ return { status: ClientMergeStatus.APPLY, content: `${JSON.stringify(merged, null, 2)}\n` };
280
+ }
281
+ /**
282
+ * A block the user can paste, for a client we could not write.
283
+ *
284
+ * Every client gets one, including the ones we CAN write: when a merge comes back MANUAL because the
285
+ * file could not be parsed, the snippet is the only actionable thing left to give.
286
+ */
287
+ export function clientSnippet(spec) {
288
+ if (spec.format === ConfigFormat.TOML) {
289
+ const args = npxServerArgs()
290
+ .map((a) => `"${a}"`)
291
+ .join(', ');
292
+ return `[mcp_servers.${MCP_SERVER_NAME}]\ncommand = "${NPX}"\nargs = [${args}]\n`;
293
+ }
294
+ if (spec.format === ConfigFormat.CLI) {
295
+ return `claude mcp add -s user ${MCP_SERVER_NAME} -- ${NPX} ${npxServerArgs().join(' ')}\n`;
296
+ }
297
+ return `${JSON.stringify({ [spec.serversKey]: { [MCP_SERVER_NAME]: spec.entry() } }, null, 2)}\n`;
298
+ }
299
+ /**
300
+ * The path whose existence means "this user has this client".
301
+ *
302
+ * Detection is deliberately conservative and one-directional: we write into a config a client
303
+ * ALREADY has, and never create `~/.gemini/` or `~/.codeium/` for somebody who does not use them.
304
+ * Littering a home directory to register a server nobody asked for is worse than printing a snippet.
305
+ *
306
+ * For a `home` client this is the directory above its config; for a `project` client it is the
307
+ * config file (or its directory) inside the repo, which is unambiguous evidence somebody uses it
308
+ * here. Same reasoning as Cursor's existing rule, which also accepts a project-level `.cursor/`
309
+ * because a fresh Cursor profile has not written `~/.cursor` yet.
310
+ */
311
+ export function clientMarkerRelPath(spec) {
312
+ if (spec.scope === ConfigScope.CLI)
313
+ return '';
314
+ const parts = spec.relPath.split('/');
315
+ return parts.length > 1 ? parts.slice(0, -1).join('/') : spec.relPath;
316
+ }
317
+ /**
318
+ * Project-relative directory whose presence signals Cursor is in use for this repo.
319
+ *
320
+ * A fresh Cursor profile has not written `~/.cursor` yet, so the home marker alone misses real
321
+ * users. A project-level `.cursor/` is unambiguous evidence somebody uses Cursor here.
322
+ */
323
+ export const CURSOR_PROJECT_MARKER = '.cursor';
324
+ /** Clients `init` can wire by writing a file — everything but the CLI-registered one. */
325
+ export function fileBackedClients() {
326
+ return MCP_CLIENTS.filter((spec) => spec.scope !== ConfigScope.CLI);
327
+ }
@@ -0,0 +1,12 @@
1
+ /**
2
+ * Did `reticle init` actually register the MCP server?
3
+ *
4
+ * This was `!failed.has(MCP_TARGET)` — the negation of one narrow failure set — so a step that was
5
+ * SKIPPED (`--no-mcp`) or left MANUAL (the user has to run a command themselves) reported success,
6
+ * because neither is a failure. Under `--no-mcp` every run claimed registration.
7
+ *
8
+ * It is the onboarding funnel's most important field, and it was wrong in the flattering direction
9
+ * on exactly the runs least likely to have a working install.
10
+ */
11
+ import { StepStatus } from './plan.js';
12
+ export declare function wasMcpRegistered(status: StepStatus | undefined): boolean;
@@ -0,0 +1,14 @@
1
+ /**
2
+ * Did `reticle init` actually register the MCP server?
3
+ *
4
+ * This was `!failed.has(MCP_TARGET)` — the negation of one narrow failure set — so a step that was
5
+ * SKIPPED (`--no-mcp`) or left MANUAL (the user has to run a command themselves) reported success,
6
+ * because neither is a failure. Under `--no-mcp` every run claimed registration.
7
+ *
8
+ * It is the onboarding funnel's most important field, and it was wrong in the flattering direction
9
+ * on exactly the runs least likely to have a working install.
10
+ */
11
+ import { StepStatus } from './plan.js';
12
+ export function wasMcpRegistered(status) {
13
+ return status === StepStatus.APPLY || status === StepStatus.ALREADY;
14
+ }
package/dist/mcp.d.ts ADDED
@@ -0,0 +1,89 @@
1
+ /**
2
+ * Global (user-scope) MCP registration for `reticle init`. The bridge + MCP server is a single
3
+ * process that serves every project, so it is registered ONCE at user scope — not per-project via
4
+ * a checked-in `.mcp.json`. We shell out to the official `claude mcp add -s user` CLI rather than
5
+ * hand-editing `~/.claude.json` (a large stateful file). When the `claude` CLI is absent we print a
6
+ * manual instruction instead.
7
+ *
8
+ * The registration is intentionally PORTLESS — `npx @reticlehq/server mcp`, never `--port N`. A single
9
+ * global entry serves every project, so baking a port into it would pin every project to one port
10
+ * and defeat per-project isolation. Instead `reticle mcp` resolves the port at runtime from the
11
+ * project's `.reticle.json` in its CWD (see cli-port.ts). The port belongs to the project, not the
12
+ * global agent config.
13
+ */
14
+ export { MCP_SERVER_NAME } from '@reticlehq/core';
15
+ /**
16
+ * The registered command is STILL bare `npx` on every platform, deliberately.
17
+ *
18
+ * Windows is most of Reticle's users and, when this was written, had no CI. There is Windows CI
19
+ * now, so the `cmd /c` fallback is testable. Defaulting the majority platform to that launch
20
+ * command is still the riskier change: `.cmd` has spawn caveats in some hosts, and a registration
21
+ * that works on the machines we can test can still fail in the hosts we cannot. So the fallback
22
+ * stays documented rather than defaulted.
23
+ *
24
+ * What DID change is when it is printed. `mcpManual` used to run only when the `claude` CLI was
25
+ * absent. The reported Windows install had `claude mcp add` succeed, so the one paragraph that
26
+ * would have unblocked zero tools never appeared. `mcpWindowsNote` is that paragraph, and init
27
+ * prints it on every Windows run — including the ones that look like a clean registration.
28
+ */
29
+ export declare const NPX = "npx";
30
+ /**
31
+ * Args after `npx` that launch the bridge: `@reticlehq/server mcp`. Portless — the port comes from
32
+ * the project's `.reticle.json` at runtime, so one global entry works for every project.
33
+ *
34
+ * DELIBERATELY UNPINNED, and the trade is worth stating. `reticle init` pins the SDK to the CLI's
35
+ * exact version, so on release day the app can be on the new one while npx serves a cached older
36
+ * build — a real skew, reported from the field. Pinning this entry would remove that window.
37
+ *
38
+ * It would also freeze the agent's MCP server at whatever version was installed the day `init` ran,
39
+ * for as long as that entry survives — and `reticle update` upgrades the CLI, not a global agent
40
+ * config. Reticle's biggest measured problem is fixes not reaching people (2.4.0 reached zero users
41
+ * before its nudge existed), and a permanent pin makes that worse for every install, to close a
42
+ * window that lasts until the next npx cache miss.
43
+ *
44
+ * So it stays unpinned, and the skew is handled where it actually shows up: the contract fingerprint
45
+ * makes a real mismatch loud on the next tool result (see version-skew), and a stale entry of our own
46
+ * shape is now repaired on re-run rather than reported "already registered" (see mcp-clients.ts).
47
+ */
48
+ export declare function npxServerArgs(): string[];
49
+ /**
50
+ * Is this existing registration one WE are responsible for?
51
+ *
52
+ * Deliberately the SCOPE and not the server package. An entry naming any `@reticlehq/*` package is
53
+ * ours to repair — including one naming the WRONG package, which is the case this widened for:
54
+ * `args: ["@reticlehq/core","mcp"]` was reported from the field, where `core` has no `mcp` bin, so
55
+ * the client showed the server errored with zero tools while `init` reported it already wired and
56
+ * left it alone on every re-run. Every `reticle_*` tool was blocked until a human edited that file.
57
+ *
58
+ * An entry that names no Reticle package at all — a local build, a wrapper script — is still someone
59
+ * else's deliberate choice and is left exactly as it is.
60
+ *
61
+ * @param tokens every string in the entry's command + args, in any of the client shapes.
62
+ */
63
+ export declare function isReticleRegistration(tokens: readonly string[]): boolean;
64
+ interface ClaudeAddCommand {
65
+ command: string;
66
+ args: string[];
67
+ /** Human-readable form of the same command, for reports and manual fallback. */
68
+ display: string;
69
+ }
70
+ /** `claude mcp add reticle -s user -- npx @reticlehq/server mcp` — registers globally for all projects (portless). */
71
+ export declare function claudeAddCommand(): ClaudeAddCommand;
72
+ /** Probe args that tell us whether an `reticle` server already exists in any scope (exit 0 = exists). */
73
+ export declare function claudeExistsProbe(): {
74
+ command: string;
75
+ args: string[];
76
+ };
77
+ /** Probe args for whether the `claude` CLI is installed at all. */
78
+ export declare function claudeAvailableProbe(): {
79
+ command: string;
80
+ args: string[];
81
+ };
82
+ /**
83
+ * The Windows `cmd /c npx` registration. Printed on every Windows init, not only when the `claude`
84
+ * CLI is missing — a successful `claude mcp add` of bare `npx` is exactly the failure that reported
85
+ * as a clean install with zero tools.
86
+ */
87
+ export declare function mcpWindowsNote(): string;
88
+ /** Printed when the `claude` CLI isn't available — register Reticle globally once, by hand. */
89
+ export declare function mcpManual(): string;
package/dist/mcp.js ADDED
@@ -0,0 +1,129 @@
1
+ /**
2
+ * Global (user-scope) MCP registration for `reticle init`. The bridge + MCP server is a single
3
+ * process that serves every project, so it is registered ONCE at user scope — not per-project via
4
+ * a checked-in `.mcp.json`. We shell out to the official `claude mcp add -s user` CLI rather than
5
+ * hand-editing `~/.claude.json` (a large stateful file). When the `claude` CLI is absent we print a
6
+ * manual instruction instead.
7
+ *
8
+ * The registration is intentionally PORTLESS — `npx @reticlehq/server mcp`, never `--port N`. A single
9
+ * global entry serves every project, so baking a port into it would pin every project to one port
10
+ * and defeat per-project isolation. Instead `reticle mcp` resolves the port at runtime from the
11
+ * project's `.reticle.json` in its CWD (see cli-port.ts). The port belongs to the project, not the
12
+ * global agent config.
13
+ */
14
+ import { RETICLE_NPM_PACKAGE } from './version.js';
15
+ // The name is a wire identity and lives in the contract; re-exported here so the registration writers
16
+ // below (and their tests) keep reading it from the module that USES it.
17
+ export { MCP_SERVER_NAME } from '@reticlehq/core';
18
+ import { MCP_SERVER_NAME } from '@reticlehq/core';
19
+ /**
20
+ * The registered command is STILL bare `npx` on every platform, deliberately.
21
+ *
22
+ * Windows is most of Reticle's users and, when this was written, had no CI. There is Windows CI
23
+ * now, so the `cmd /c` fallback is testable. Defaulting the majority platform to that launch
24
+ * command is still the riskier change: `.cmd` has spawn caveats in some hosts, and a registration
25
+ * that works on the machines we can test can still fail in the hosts we cannot. So the fallback
26
+ * stays documented rather than defaulted.
27
+ *
28
+ * What DID change is when it is printed. `mcpManual` used to run only when the `claude` CLI was
29
+ * absent. The reported Windows install had `claude mcp add` succeed, so the one paragraph that
30
+ * would have unblocked zero tools never appeared. `mcpWindowsNote` is that paragraph, and init
31
+ * prints it on every Windows run — including the ones that look like a clean registration.
32
+ */
33
+ export const NPX = 'npx';
34
+ // The `reticle` bin lives in the server package, so `npx <pkg> mcp` runs the bridge without the
35
+ // retired `core` umbrella. Sourced from the single derived identity so it can never drift.
36
+ const RETICLE_PACKAGE = RETICLE_NPM_PACKAGE;
37
+ const MCP_SUBCOMMAND = 'mcp';
38
+ const CLAUDE_CLI = 'claude';
39
+ /** Windows `cmd /c` — the spawn that PowerShell's execution policy does not gate. */
40
+ const WINDOWS_SHELL = 'cmd';
41
+ const WINDOWS_SHELL_C = '/c';
42
+ /**
43
+ * Args after `npx` that launch the bridge: `@reticlehq/server mcp`. Portless — the port comes from
44
+ * the project's `.reticle.json` at runtime, so one global entry works for every project.
45
+ *
46
+ * DELIBERATELY UNPINNED, and the trade is worth stating. `reticle init` pins the SDK to the CLI's
47
+ * exact version, so on release day the app can be on the new one while npx serves a cached older
48
+ * build — a real skew, reported from the field. Pinning this entry would remove that window.
49
+ *
50
+ * It would also freeze the agent's MCP server at whatever version was installed the day `init` ran,
51
+ * for as long as that entry survives — and `reticle update` upgrades the CLI, not a global agent
52
+ * config. Reticle's biggest measured problem is fixes not reaching people (2.4.0 reached zero users
53
+ * before its nudge existed), and a permanent pin makes that worse for every install, to close a
54
+ * window that lasts until the next npx cache miss.
55
+ *
56
+ * So it stays unpinned, and the skew is handled where it actually shows up: the contract fingerprint
57
+ * makes a real mismatch loud on the next tool result (see version-skew), and a stale entry of our own
58
+ * shape is now repaired on re-run rather than reported "already registered" (see mcp-clients.ts).
59
+ */
60
+ export function npxServerArgs() {
61
+ return [RETICLE_PACKAGE, MCP_SUBCOMMAND];
62
+ }
63
+ /** The npm scope every Reticle package shares — the marker of a registration we are entitled to fix. */
64
+ const RETICLE_SCOPE = '@reticlehq/';
65
+ /**
66
+ * Is this existing registration one WE are responsible for?
67
+ *
68
+ * Deliberately the SCOPE and not the server package. An entry naming any `@reticlehq/*` package is
69
+ * ours to repair — including one naming the WRONG package, which is the case this widened for:
70
+ * `args: ["@reticlehq/core","mcp"]` was reported from the field, where `core` has no `mcp` bin, so
71
+ * the client showed the server errored with zero tools while `init` reported it already wired and
72
+ * left it alone on every re-run. Every `reticle_*` tool was blocked until a human edited that file.
73
+ *
74
+ * An entry that names no Reticle package at all — a local build, a wrapper script — is still someone
75
+ * else's deliberate choice and is left exactly as it is.
76
+ *
77
+ * @param tokens every string in the entry's command + args, in any of the client shapes.
78
+ */
79
+ export function isReticleRegistration(tokens) {
80
+ return tokens.includes(NPX) && tokens.some((t) => t.includes(RETICLE_SCOPE));
81
+ }
82
+ /** The full `npx …` invocation — the tail after `claude mcp add … --`. */
83
+ function serverInvocation() {
84
+ return [NPX, ...npxServerArgs()];
85
+ }
86
+ /** `claude mcp add reticle -s user -- npx @reticlehq/server mcp` — registers globally for all projects (portless). */
87
+ export function claudeAddCommand() {
88
+ const tail = serverInvocation();
89
+ const args = [MCP_SUBCOMMAND, 'add', MCP_SERVER_NAME, '-s', 'user', '--', ...tail];
90
+ return { command: CLAUDE_CLI, args, display: `${CLAUDE_CLI} ${args.join(' ')}` };
91
+ }
92
+ /** Probe args that tell us whether an `reticle` server already exists in any scope (exit 0 = exists). */
93
+ export function claudeExistsProbe() {
94
+ // NO `-s`: `claude mcp get` takes no options at all, so passing one exits 1 with "unknown option
95
+ // '-s'" — the probe answered "not registered" on EVERY machine, init then ran `claude mcp add`,
96
+ // which exits 1 with "already exists", and a re-run reported `[⚠] step failed` plus a manual
97
+ // command that fails the same way. A false positive from a project-scoped entry costs one skipped
98
+ // registration; the flag cost every re-run a failed step.
99
+ return { command: CLAUDE_CLI, args: [MCP_SUBCOMMAND, 'get', MCP_SERVER_NAME] };
100
+ }
101
+ /** Probe args for whether the `claude` CLI is installed at all. */
102
+ export function claudeAvailableProbe() {
103
+ return { command: CLAUDE_CLI, args: ['--version'] };
104
+ }
105
+ /**
106
+ * The Windows `cmd /c npx` registration. Printed on every Windows init, not only when the `claude`
107
+ * CLI is missing — a successful `claude mcp add` of bare `npx` is exactly the failure that reported
108
+ * as a clean install with zero tools.
109
+ */
110
+ export function mcpWindowsNote() {
111
+ return `On Windows, if the agent cannot start Reticle because npx is blocked ("running scripts is disabled
112
+ on this system" — a PowerShell execution policy), register it through cmd instead, which that policy
113
+ does not gate:
114
+
115
+ "${MCP_SERVER_NAME}": { "command": "${WINDOWS_SHELL}", "args": ${JSON.stringify([WINDOWS_SHELL_C, NPX, ...serverInvocation().slice(1)])} }`;
116
+ }
117
+ /** Printed when the `claude` CLI isn't available — register Reticle globally once, by hand. */
118
+ export function mcpManual() {
119
+ const tail = serverInvocation().join(' ');
120
+ return `Register the Reticle MCP server ONCE, globally (so every project gets it):
121
+
122
+ ${CLAUDE_CLI} ${MCP_SUBCOMMAND} add ${MCP_SERVER_NAME} -s user -- ${tail}
123
+
124
+ Or, for another agent, add this to its global MCP config (e.g. Cursor's ~/.cursor/mcp.json):
125
+
126
+ "${MCP_SERVER_NAME}": { "command": "${NPX}", "args": ${JSON.stringify(serverInvocation().slice(1))} }
127
+
128
+ ${mcpWindowsNote()}`;
129
+ }