@reticlehq/server 2.5.0 → 2.6.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.
- package/SKILL.md +27 -10
- package/dist/bridge/bridge.d.ts +12 -0
- package/dist/bridge/bridge.js +32 -2
- package/dist/bridge/bridge.test-harness.d.ts +8 -0
- package/dist/bridge/bridge.test-harness.js +9 -1
- package/dist/cli/cli-doctor.d.ts +0 -5
- package/dist/cli/cli-doctor.js +71 -3
- package/dist/cli/cli-parse-feedback.d.ts +31 -0
- package/dist/cli/cli-parse-feedback.js +56 -0
- package/dist/cli/cli-parse.d.ts +5 -9
- package/dist/cli/cli-parse.js +7 -23
- package/dist/cli/doctor-daemon-line.d.ts +37 -0
- package/dist/cli/doctor-daemon-line.js +35 -0
- package/dist/cli/port-holder.d.ts +43 -0
- package/dist/cli/port-holder.js +99 -0
- package/dist/cli.js +131 -11
- package/dist/daemon/daemon-resilience.d.ts +24 -0
- package/dist/daemon/daemon-resilience.js +105 -17
- package/dist/daemon/daemon.js +26 -1
- package/dist/daemon/heartbeat.d.ts +115 -0
- package/dist/daemon/heartbeat.js +141 -0
- package/dist/daemon/idle-shutdown.d.ts +13 -5
- package/dist/daemon/idle-shutdown.js +14 -6
- package/dist/daemon/port-presence.d.ts +76 -0
- package/dist/daemon/port-presence.js +93 -0
- package/dist/daemon/state-dir.d.ts +7 -0
- package/dist/daemon/state-dir.js +59 -0
- package/dist/daemon/unclean-predecessor.d.ts +23 -0
- package/dist/daemon/unclean-predecessor.js +27 -0
- package/dist/events/predicate-eval.d.ts +10 -0
- package/dist/events/predicate-eval.js +24 -4
- package/dist/events/predicate-parse.d.ts +23 -0
- package/dist/events/predicate-parse.js +54 -0
- package/dist/events/predicate.d.ts +5 -0
- package/dist/events/predicate.js +17 -3
- package/dist/events/ring-buffer.js +1 -16
- package/dist/flows/flow-replay.d.ts +1 -1
- package/dist/flows/flow-replay.js +19 -2
- package/dist/flows/flow-step-runners.js +2 -3
- package/dist/flows/flow-tools.js +10 -14
- package/dist/flows/role-drift-reason.d.ts +20 -0
- package/dist/flows/role-drift-reason.js +39 -0
- package/dist/flows/suite-flakes.d.ts +33 -0
- package/dist/flows/suite-flakes.js +32 -0
- package/dist/honesty/blind-spots.d.ts +19 -0
- package/dist/honesty/blind-spots.js +33 -0
- package/dist/honesty/verified.d.ts +10 -1
- package/dist/honesty/verified.js +16 -0
- package/dist/http-server.js +9 -0
- package/dist/index.d.ts +1 -0
- package/dist/index.js +10 -1
- package/dist/init/agent-rules.d.ts +18 -0
- package/dist/init/agent-rules.js +22 -4
- package/dist/init/cra.d.ts +13 -0
- package/dist/init/cra.js +16 -0
- package/dist/init/detect.d.ts +6 -0
- package/dist/init/detect.js +8 -0
- package/dist/init/mcp-clients.d.ts +81 -0
- package/dist/init/mcp-clients.js +248 -0
- package/dist/init/node-io.js +8 -2
- package/dist/init/plan-framework.js +36 -2
- package/dist/init/plan.d.ts +14 -0
- package/dist/init/plan.js +73 -9
- package/dist/init/run.d.ts +18 -1
- package/dist/init/run.js +133 -16
- package/dist/init/windows-quote.d.ts +38 -0
- package/dist/init/windows-quote.js +70 -0
- package/dist/mcp/mcp-outage.d.ts +1 -0
- package/dist/mcp/mcp-outage.js +8 -1
- package/dist/mcp/mcp-proxy.d.ts +11 -1
- package/dist/mcp/mcp-proxy.js +30 -7
- package/dist/mcp/mcp.js +29 -2
- package/dist/pool/browser-pool.d.ts +9 -0
- package/dist/pool/browser-pool.js +38 -9
- package/dist/session/attachment-history.d.ts +46 -0
- package/dist/session/attachment-history.js +65 -0
- package/dist/session/no-session-diagnosis.d.ts +7 -0
- package/dist/session/no-session-diagnosis.js +53 -3
- package/dist/session/no-session-watch.d.ts +3 -1
- package/dist/session/no-session-watch.js +10 -2
- package/dist/session/pending-commands.js +1 -0
- package/dist/session/session-info.d.ts +17 -1
- package/dist/session/session-info.js +5 -1
- package/dist/session/session-manager.d.ts +3 -0
- package/dist/session/session-manager.js +72 -3
- package/dist/session/session.d.ts +1 -0
- package/dist/session/session.js +8 -0
- package/dist/telemetry/crash-cause.d.ts +47 -0
- package/dist/telemetry/crash-cause.js +104 -0
- package/dist/telemetry/daemon-telemetry.d.ts +25 -7
- package/dist/telemetry/daemon-telemetry.js +28 -13
- package/dist/telemetry/error-class.d.ts +26 -0
- package/dist/telemetry/error-class.js +52 -0
- package/dist/telemetry/feedback-cli.d.ts +7 -2
- package/dist/telemetry/feedback-cli.js +49 -5
- package/dist/telemetry/feedback-context.d.ts +1 -6
- package/dist/telemetry/feedback-context.js +64 -4
- package/dist/telemetry/git-facts.d.ts +7 -0
- package/dist/telemetry/git-facts.js +4 -4
- package/dist/telemetry/project-profile.js +2 -1
- package/dist/telemetry/session-metrics.d.ts +45 -3
- package/dist/telemetry/session-metrics.js +247 -9
- package/dist/telemetry/telemetry.d.ts +19 -1
- package/dist/telemetry/telemetry.js +31 -8
- package/dist/tools/act-tools.js +33 -6
- package/dist/tools/assert-verdict.d.ts +7 -1
- package/dist/tools/assert-verdict.js +15 -3
- package/dist/tools/browser-tools.js +8 -2
- package/dist/tools/contract-tools.d.ts +0 -5
- package/dist/tools/contract-tools.js +26 -0
- package/dist/tools/dynamic-tools.js +6 -1
- package/dist/tools/error-recovery.d.ts +14 -0
- package/dist/tools/error-recovery.js +83 -7
- package/dist/tools/feedback-invite.d.ts +48 -0
- package/dist/tools/feedback-invite.js +71 -0
- package/dist/tools/invoke-tool.d.ts +0 -6
- package/dist/tools/invoke-tool.js +69 -1
- package/dist/tools/navigate-arrival.d.ts +26 -0
- package/dist/tools/navigate-arrival.js +60 -0
- package/dist/tools/navigate-result.d.ts +15 -1
- package/dist/tools/navigate-result.js +17 -10
- package/dist/tools/observe-tools.js +22 -4
- package/dist/tools/real-input-attempt.js +10 -5
- package/dist/tools/tool-kit.d.ts +8 -0
- package/dist/tools/tool-surface.js +13 -0
- package/docs/README.md +55 -0
- package/docs/agent-cheatsheet.md +2 -2
- package/docs/benchmarks.md +1 -1
- package/docs/fixtures-dispatch-receiver.yml +76 -0
- package/docs/fixtures.md +17 -0
- package/docs/gate-plan.md +98 -0
- package/docs/gates.md +117 -0
- package/docs/local-registry.md +1 -1
- package/docs/matrix/2.5.0/claude-code-machine.json +19 -0
- package/docs/matrix/2.5.0/codex-machine.json +20 -0
- package/docs/matrix/2.5.0/cursor-machine.json +22 -0
- package/docs/matrix/2.5.0/gemini-machine.json +22 -0
- package/docs/matrix/2.5.0/opencode-machine.json +22 -0
- package/docs/matrix/2.5.0/vscode-machine.json +22 -0
- package/docs/matrix/2.5.0/windsurf-machine.json +22 -0
- package/docs/matrix/MATRIX.md +26 -0
- package/docs/matrix/README.md +59 -0
- package/docs/mint.json +1 -1
- package/docs/system-map.md +152 -0
- package/docs/telemetry-contract.md +2 -2
- package/docs/telemetry.md +7 -0
- package/package.json +2 -2
package/dist/index.js
CHANGED
|
@@ -66,6 +66,10 @@ export { BrowserPool, DEFAULT_LEASE_TTL_MS } from './pool/browser-pool.js';
|
|
|
66
66
|
export { playwrightLauncher, resolveMaxContexts } from './pool/playwright-launcher.js';
|
|
67
67
|
export { appendReticleParams } from './tools/lease-tools.js';
|
|
68
68
|
export { writePid, removePid, isRunning, logPath, readPid, isAlive } from './daemon/daemon.js';
|
|
69
|
+
// The daemon's own liveness vocabulary, exported so a GATE can read a daemon log back and say how
|
|
70
|
+
// that daemon ended. Without this the battery would re-implement the rule, and a guard that
|
|
71
|
+
// re-implements what it guards is insensitive to it.
|
|
72
|
+
export { classifyDaemonLife, DaemonEnd, DAEMON_HEARTBEAT_EVENT, DAEMON_HEARTBEAT_MS, } from './daemon/heartbeat.js';
|
|
69
73
|
export { scrollToFind } from './input/scroll-find.js';
|
|
70
74
|
export { CORE_TOOL_NAMES, TOOL_SURFACE, TOOL_PROFILE_ENV, filterTools, resolveToolSurface, } from './tools/tool-surface.js';
|
|
71
75
|
export { AnnotationStore } from './flows/annotation-store.js';
|
|
@@ -262,6 +266,9 @@ export async function start(options = {}) {
|
|
|
262
266
|
reticleRoot,
|
|
263
267
|
now,
|
|
264
268
|
bridgePort: port,
|
|
269
|
+
// The daemon's OWN project, so a tool can tell "this session is mine" from "this session
|
|
270
|
+
// belongs to a sibling app under the same daemon". contract_save refuses on the second.
|
|
271
|
+
...(activeProjectId === undefined ? {} : { projectId: activeProjectId }),
|
|
265
272
|
};
|
|
266
273
|
const profile = resolveToolSurface(options.toolProfile);
|
|
267
274
|
const server = createMcpServer(realInput !== undefined ? { ...deps, realInput } : deps, profile);
|
|
@@ -321,7 +328,9 @@ export async function startDaemon(options = {}) {
|
|
|
321
328
|
// never picked when the agent omits a sessionId. Explicit per-call scope/sessionId still overrides.
|
|
322
329
|
// Scope + the no-session diagnosis: "no browser session connected" is the error that ends most
|
|
323
330
|
// sessions, and the agent is told to check two things it cannot see. See no-session-diagnosis.ts.
|
|
324
|
-
|
|
331
|
+
// The pool reader is passed lazily: `pool` is assigned above but may be undefined when this
|
|
332
|
+
// daemon runs without one, and the diagnosis must degrade to the tab message rather than lie.
|
|
333
|
+
wireSessionScope(bridge.sessions, readProjectId(process.cwd()), port, () => pool?.reapedLeaseCount() ?? 0);
|
|
325
334
|
const { realInput, owned } = await resolveRealInput(options, () => shared.close(), makeNetworkDetailRouter(bridge, options.driveUrl));
|
|
326
335
|
const fs = createNodeFileSystem();
|
|
327
336
|
const reticleRoot = options.reticleRoot ?? join(process.cwd(), ReticleDir.ROOT);
|
|
@@ -4,6 +4,13 @@
|
|
|
4
4
|
* it with reticle". Registering the MCP server (mcp.ts / cursor.ts) only makes the tools AVAILABLE;
|
|
5
5
|
* this is the piece that makes the agent reach for them, every session, unprompted.
|
|
6
6
|
*
|
|
7
|
+
* It carries a SECOND standing instruction, and that one is not about this project at all: send
|
|
8
|
+
* feedback to the Reticle maintainers, from the moment instrumentation starts. The agent is the party
|
|
9
|
+
* that experiences Reticle, and the only one who can say what it cost — and by the time anyone would
|
|
10
|
+
* think to ask, the context that knew is gone. Written into the same block, because the moment the
|
|
11
|
+
* rule lands (init, before any framework wiring) is the moment the setup itself is being lived
|
|
12
|
+
* through, which is when the reports we hear least about are actually generated.
|
|
13
|
+
*
|
|
7
14
|
* The rule is framed as a definition-of-done (WHEN to verify, HOW, and the anti-reward-hacking guard),
|
|
8
15
|
* written into whichever instruction file the detected agent reads: `CLAUDE.md` (Claude Code),
|
|
9
16
|
* `.cursor/rules/reticle.mdc` (Cursor, `alwaysApply` so it stays in context), or `AGENTS.md` as the
|
|
@@ -15,6 +22,17 @@ export declare const CLAUDE_MD_PATH = "CLAUDE.md";
|
|
|
15
22
|
export declare const AGENTS_MD_PATH = "AGENTS.md";
|
|
16
23
|
/** Cursor project rule; the frontmatter `alwaysApply` keeps it in context on every turn. */
|
|
17
24
|
export declare const CURSOR_RULE_PATH = ".cursor/rules/reticle.mdc";
|
|
25
|
+
/**
|
|
26
|
+
* How the agent must INVOKE the CLI.
|
|
27
|
+
*
|
|
28
|
+
* `reticle init` installs the SDK packages, never the server — so there is no `reticle` on the
|
|
29
|
+
* agent's PATH, and every bare `reticle <cmd>` this rule named was an instruction that could not be
|
|
30
|
+
* followed. Reported from a field sweep: three of them, in the file the agent re-reads every session.
|
|
31
|
+
*
|
|
32
|
+
* `npx @reticlehq/server` is the same invocation the MCP registration uses, and it is derived from
|
|
33
|
+
* the package's own name so the two can never drift apart.
|
|
34
|
+
*/
|
|
35
|
+
export declare const CLI: string;
|
|
18
36
|
export declare const AgentRuleStatus: {
|
|
19
37
|
readonly APPLY: "apply";
|
|
20
38
|
readonly ALREADY: "already";
|
package/dist/init/agent-rules.js
CHANGED
|
@@ -4,6 +4,13 @@
|
|
|
4
4
|
* it with reticle". Registering the MCP server (mcp.ts / cursor.ts) only makes the tools AVAILABLE;
|
|
5
5
|
* this is the piece that makes the agent reach for them, every session, unprompted.
|
|
6
6
|
*
|
|
7
|
+
* It carries a SECOND standing instruction, and that one is not about this project at all: send
|
|
8
|
+
* feedback to the Reticle maintainers, from the moment instrumentation starts. The agent is the party
|
|
9
|
+
* that experiences Reticle, and the only one who can say what it cost — and by the time anyone would
|
|
10
|
+
* think to ask, the context that knew is gone. Written into the same block, because the moment the
|
|
11
|
+
* rule lands (init, before any framework wiring) is the moment the setup itself is being lived
|
|
12
|
+
* through, which is when the reports we hear least about are actually generated.
|
|
13
|
+
*
|
|
7
14
|
* The rule is framed as a definition-of-done (WHEN to verify, HOW, and the anti-reward-hacking guard),
|
|
8
15
|
* written into whichever instruction file the detected agent reads: `CLAUDE.md` (Claude Code),
|
|
9
16
|
* `.cursor/rules/reticle.mdc` (Cursor, `alwaysApply` so it stays in context), or `AGENTS.md` as the
|
|
@@ -30,7 +37,7 @@ const RULE_END = '<!-- reticle:end -->';
|
|
|
30
37
|
* `npx @reticlehq/server` is the same invocation the MCP registration uses, and it is derived from
|
|
31
38
|
* the package's own name so the two can never drift apart.
|
|
32
39
|
*/
|
|
33
|
-
const CLI = `${NPX} ${RETICLE_NPM_PACKAGE}`;
|
|
40
|
+
export const CLI = `${NPX} ${RETICLE_NPM_PACKAGE}`;
|
|
34
41
|
/** The rule body (markdown). States WHEN to verify, HOW, and never-weaken-a-check. */
|
|
35
42
|
const RULE_BODY = `## Verifying with Reticle
|
|
36
43
|
|
|
@@ -43,8 +50,19 @@ This app is instrumented by **Reticle** — an in-app verification layer exposed
|
|
|
43
50
|
- **If Reticle can't run** (no daemon, or this is not a running web app), say so — do not skip verification silently.
|
|
44
51
|
- **If a tool result carries \`version_skew\`, act on it before trusting anything else.** Two pieces of Reticle are on different wire contracts, so tools can misbehave in ways neither side reports — a green here is not evidence. The field says which pair disagrees and names the exact fix: run \`${CLI} stop\` and retry when the DAEMON is stale, or tell the human to install the matching SDK and restart their dev server when the PAGE is. Do that first, then redo whatever you were verifying.
|
|
45
52
|
- **If a tool result carries \`update_available\`, say so.** Reticle checked npm and a newer version exists. Tell the human the version numbers and offer to run \`${CLI} update\` — between tasks, never mid-verification, because it restarts the daemon and drops the session. Finish what you are doing first.
|
|
46
|
-
|
|
47
|
-
|
|
53
|
+
|
|
54
|
+
## Sending feedback to the Reticle team (first-class)
|
|
55
|
+
|
|
56
|
+
**Reporting your experience with Reticle is part of using Reticle, from the moment you start wiring it in — not something to do at the end if anything went wrong.** Reticle is built FOR agents, which makes you its actual user and the only one who can see what it costs you. That knowledge is gone when your context is, so it has to leave the turn it happened in.
|
|
57
|
+
|
|
58
|
+
- **Send it with \`reticle_feedback\`, the moment you notice.** One call, then carry straight on with your task. Do not batch reports, do not wait until the task is done, and do not decide something is too minor — the small awkward things are what nobody ever reports, so they never get fixed.
|
|
59
|
+
- **Everything counts, in both directions.** A tool that returned something wrong (\`bug\`), something Reticle could not observe at all (\`gap\`), a verdict you could not act on (\`ambiguity\`), something you wished existed (\`feature_request\`), something that works but costs too many calls or has an awkward shape (\`improvement\`).
|
|
60
|
+
- **Setup and installation count too, and are the easiest to lose.** Anything that went wrong while installing, wiring, or starting Reticle — a step in the docs that did not match reality, a plugin that had to be added by hand, a daemon that would not start, an OS-specific detour. You hit these before any verification runs, which is exactly why they usually go unreported.
|
|
61
|
+
- **When the tools are not reachable yet**, file the same report from the shell: \`${CLI} feedback --agent --kind <bug|gap|ambiguity|feature_request|improvement> "what happened"\`. It needs no daemon and no working install, so a Reticle that broke before it started can still be reported.
|
|
62
|
+
- **Write it so it can be acted on.** For a failure: what you called, what you expected, what you got, and the call trail in \`trace\`. For a request: the GOAL in \`need\` (not your guess at the solution), what improves in \`impact\`, and how you work around it today in \`currentApproach\` — that workaround is usually the most useful line in the report. Set \`model\` to the model you are running.
|
|
63
|
+
- **Working around a Reticle defect in silence is the one thing not to do.** A silent workaround is how the same defect survives to the next user.
|
|
64
|
+
- **Report defects in *Reticle*, not in the app under test.** A bug you find in this app is Reticle working as intended; that belongs in your report to the user.
|
|
65
|
+
- **Never include app source, secrets, user data, or customer records** in a report.`;
|
|
48
66
|
export const AgentRuleStatus = {
|
|
49
67
|
APPLY: 'apply',
|
|
50
68
|
ALREADY: 'already',
|
|
@@ -96,7 +114,7 @@ function refreshMarkedBlock(existing) {
|
|
|
96
114
|
/** The Cursor rule file (.mdc): `alwaysApply` keeps the rule in every turn's context. */
|
|
97
115
|
export function cursorRuleFile() {
|
|
98
116
|
return `---
|
|
99
|
-
description: Verify web features with Reticle after building them
|
|
117
|
+
description: Verify web features with Reticle after building them, and report Reticle problems back to its maintainers
|
|
100
118
|
alwaysApply: true
|
|
101
119
|
---
|
|
102
120
|
|
package/dist/init/cra.d.ts
CHANGED
|
@@ -29,6 +29,19 @@ export declare const TOKEN_VAR = "REACT_APP_RETICLE_TOKEN";
|
|
|
29
29
|
* withReticle); CRA has no such hook without ejecting, so this message is the fix.
|
|
30
30
|
*/
|
|
31
31
|
export declare const CRA_TOKEN_MISSING_NOTE: string;
|
|
32
|
+
/**
|
|
33
|
+
* Said as its own NOTICE line, beside the write — not inside it.
|
|
34
|
+
*
|
|
35
|
+
* The caveat has always been in the write step's `detail`, and the write step renders `[✓]`. But
|
|
36
|
+
* `SKILL.md` tells whoever reads the report: *"If every line is `✓`, `·` or `–`, skip to Step 4 and
|
|
37
|
+
* validate."* So the one fact that makes this install conditional sat in the one place the reading
|
|
38
|
+
* protocol says to ignore — which is exactly how it was reported to us as "4 OK marks and no
|
|
39
|
+
* warning". The words were on screen; the reader was following instructions.
|
|
40
|
+
*
|
|
41
|
+
* It cannot simply be promoted: `run.ts` only writes steps whose status is APPLY, so demoting the
|
|
42
|
+
* token step to NOTICE would stop it writing the token and break the install outright.
|
|
43
|
+
*/
|
|
44
|
+
export declare const CRA_TOKEN_PER_MACHINE_NOTICE: string;
|
|
32
45
|
/** Add the import after the last existing one, or null when it is already present. */
|
|
33
46
|
export declare function craImportPatch(source: string): string | null;
|
|
34
47
|
/** Set the token variable, or null when nothing needs to change. */
|
package/dist/init/cra.js
CHANGED
|
@@ -31,6 +31,22 @@ export const TOKEN_VAR = 'REACT_APP_RETICLE_TOKEN';
|
|
|
31
31
|
export const CRA_TOKEN_MISSING_NOTE = `${TOKEN_VAR} is not set, so Reticle cannot pair with the daemon. ` +
|
|
32
32
|
`The pairing token is per-machine and ${CRA_ENV_PATH} is gitignored by CRA's template, ` +
|
|
33
33
|
'so it does not survive a clone. Run `npx reticle init` in this project to write it for this machine.';
|
|
34
|
+
/**
|
|
35
|
+
* Said as its own NOTICE line, beside the write — not inside it.
|
|
36
|
+
*
|
|
37
|
+
* The caveat has always been in the write step's `detail`, and the write step renders `[✓]`. But
|
|
38
|
+
* `SKILL.md` tells whoever reads the report: *"If every line is `✓`, `·` or `–`, skip to Step 4 and
|
|
39
|
+
* validate."* So the one fact that makes this install conditional sat in the one place the reading
|
|
40
|
+
* protocol says to ignore — which is exactly how it was reported to us as "4 OK marks and no
|
|
41
|
+
* warning". The words were on screen; the reader was following instructions.
|
|
42
|
+
*
|
|
43
|
+
* It cannot simply be promoted: `run.ts` only writes steps whose status is APPLY, so demoting the
|
|
44
|
+
* token step to NOTICE would stop it writing the token and break the install outright.
|
|
45
|
+
*/
|
|
46
|
+
export const CRA_TOKEN_PER_MACHINE_NOTICE = `${CRA_ENV_PATH} is in CRA's own .gitignore, so the token just written does not travel. ` +
|
|
47
|
+
'It works on THIS machine and nowhere else: a teammate cloning the repo, CI, or a container ' +
|
|
48
|
+
'gets an app that boots, never pairs, and reports it only in the browser console. ' +
|
|
49
|
+
'Each developer runs `npx reticle init` once locally.';
|
|
34
50
|
/** Add the import after the last existing one, or null when it is already present. */
|
|
35
51
|
export function craImportPatch(source) {
|
|
36
52
|
if (source.includes(CRA_DEV_MODULE_IMPORT))
|
package/dist/init/detect.d.ts
CHANGED
|
@@ -62,6 +62,12 @@ export interface Detection {
|
|
|
62
62
|
}
|
|
63
63
|
/** Extract the leading major version from a semver range like "^19.0.0" or "19.1.1". */
|
|
64
64
|
export declare function parseMajor(range: string | undefined): number | undefined;
|
|
65
|
+
/**
|
|
66
|
+
* Whether an installed tree identifies the manager that built it. Callers use this to decide whether
|
|
67
|
+
* they still need weaker evidence — see `resolveLockfiles`, which stops inheriting an ancestor
|
|
68
|
+
* lockfile once the project's own tree can answer the question.
|
|
69
|
+
*/
|
|
70
|
+
export declare function namesAPackageManager(markers: ReadonlySet<string>): boolean;
|
|
65
71
|
export declare function detectPackageManager(lockfiles: ReadonlySet<string>, nodeModulesMarkers: ReadonlySet<string>): PackageManager;
|
|
66
72
|
export declare function detect(input: DetectInput): Detection;
|
|
67
73
|
interface InstallCommand {
|
package/dist/init/detect.js
CHANGED
|
@@ -75,6 +75,14 @@ function packageManagerFromNodeModules(markers) {
|
|
|
75
75
|
return pm;
|
|
76
76
|
return undefined;
|
|
77
77
|
}
|
|
78
|
+
/**
|
|
79
|
+
* Whether an installed tree identifies the manager that built it. Callers use this to decide whether
|
|
80
|
+
* they still need weaker evidence — see `resolveLockfiles`, which stops inheriting an ancestor
|
|
81
|
+
* lockfile once the project's own tree can answer the question.
|
|
82
|
+
*/
|
|
83
|
+
export function namesAPackageManager(markers) {
|
|
84
|
+
return packageManagerFromNodeModules(markers) !== undefined;
|
|
85
|
+
}
|
|
78
86
|
export function detectPackageManager(lockfiles, nodeModulesMarkers) {
|
|
79
87
|
if (lockfiles.has('pnpm-lock.yaml'))
|
|
80
88
|
return PackageManager.PNPM;
|
|
@@ -0,0 +1,81 @@
|
|
|
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 CODEX: "codex";
|
|
7
|
+
readonly GEMINI: "gemini";
|
|
8
|
+
readonly VSCODE: "vscode";
|
|
9
|
+
};
|
|
10
|
+
export type McpClient = (typeof McpClient)[keyof typeof McpClient];
|
|
11
|
+
/** Where the config lives. `cli` means the client owns registration and we shell out to it. */
|
|
12
|
+
export declare const ConfigScope: {
|
|
13
|
+
readonly HOME: "home";
|
|
14
|
+
readonly PROJECT: "project";
|
|
15
|
+
readonly CLI: "cli";
|
|
16
|
+
};
|
|
17
|
+
export type ConfigScope = (typeof ConfigScope)[keyof typeof ConfigScope];
|
|
18
|
+
/** How the file is written. `toml` is deliberately never auto-merged — see mergeClientConfig. */
|
|
19
|
+
export declare const ConfigFormat: {
|
|
20
|
+
readonly JSON: "json";
|
|
21
|
+
readonly TOML: "toml";
|
|
22
|
+
readonly CLI: "cli";
|
|
23
|
+
};
|
|
24
|
+
export type ConfigFormat = (typeof ConfigFormat)[keyof typeof ConfigFormat];
|
|
25
|
+
export interface ClientSpec {
|
|
26
|
+
id: McpClient;
|
|
27
|
+
label: string;
|
|
28
|
+
scope: ConfigScope;
|
|
29
|
+
/** Relative to `$HOME` for `home`, to the project root for `project`. Empty for `cli`. */
|
|
30
|
+
relPath: string;
|
|
31
|
+
format: ConfigFormat;
|
|
32
|
+
/** The object key holding the server map — `mcpServers`, `servers`, or `mcp`. */
|
|
33
|
+
serversKey: string;
|
|
34
|
+
/** Our entry, in THIS client's shape. */
|
|
35
|
+
entry: () => unknown;
|
|
36
|
+
/** Where the shape came from, so the next person can re-check it rather than trust this file. */
|
|
37
|
+
docs: string;
|
|
38
|
+
}
|
|
39
|
+
export declare const MCP_CLIENTS: readonly ClientSpec[];
|
|
40
|
+
export declare function clientSpec(id: McpClient): ClientSpec;
|
|
41
|
+
export declare const ClientMergeStatus: {
|
|
42
|
+
readonly APPLY: "apply";
|
|
43
|
+
readonly ALREADY: "already";
|
|
44
|
+
readonly MANUAL: "manual";
|
|
45
|
+
};
|
|
46
|
+
export type ClientMergeStatus = (typeof ClientMergeStatus)[keyof typeof ClientMergeStatus];
|
|
47
|
+
export interface ClientMergeResult {
|
|
48
|
+
status: ClientMergeStatus;
|
|
49
|
+
/** Full file content to write. Byte-identical to `existing` when not `apply`. */
|
|
50
|
+
content: string;
|
|
51
|
+
}
|
|
52
|
+
/**
|
|
53
|
+
* Merge our server into this client's config.
|
|
54
|
+
*
|
|
55
|
+
* TOML is never auto-merged. Editing TOML without a parser is how a config file gets corrupted, and
|
|
56
|
+
* a corrupted `~/.codex/config.toml` costs the user every server they had, not just ours. Printing
|
|
57
|
+
* the exact block is honest; writing a guess is not — so Codex returns MANUAL with a snippet.
|
|
58
|
+
*/
|
|
59
|
+
export declare function mergeClientConfig(spec: ClientSpec, existing: string | null): ClientMergeResult;
|
|
60
|
+
/**
|
|
61
|
+
* A block the user can paste, for a client we could not write.
|
|
62
|
+
*
|
|
63
|
+
* Every client gets one, including the ones we CAN write: when a merge comes back MANUAL because the
|
|
64
|
+
* file could not be parsed, the snippet is the only actionable thing left to give.
|
|
65
|
+
*/
|
|
66
|
+
export declare function clientSnippet(spec: ClientSpec): string;
|
|
67
|
+
/**
|
|
68
|
+
* The path whose existence means "this user has this client".
|
|
69
|
+
*
|
|
70
|
+
* Detection is deliberately conservative and one-directional: we write into a config a client
|
|
71
|
+
* ALREADY has, and never create `~/.gemini/` or `~/.codeium/` for somebody who does not use them.
|
|
72
|
+
* Littering a home directory to register a server nobody asked for is worse than printing a snippet.
|
|
73
|
+
*
|
|
74
|
+
* For a `home` client this is the directory above its config; for a `project` client it is the
|
|
75
|
+
* config file (or its directory) inside the repo, which is unambiguous evidence somebody uses it
|
|
76
|
+
* here. Same reasoning as Cursor's existing rule, which also accepts a project-level `.cursor/`
|
|
77
|
+
* because a fresh Cursor profile has not written `~/.cursor` yet.
|
|
78
|
+
*/
|
|
79
|
+
export declare function clientMarkerRelPath(spec: ClientSpec): string;
|
|
80
|
+
/** Clients `init` can wire by writing a file — everything but the CLI-registered one. */
|
|
81
|
+
export declare function fileBackedClients(): readonly ClientSpec[];
|
|
@@ -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
|
+
}
|
package/dist/init/node-io.js
CHANGED
|
@@ -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
|
-
/**
|
|
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(
|
|
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.
|
|
@@ -6,7 +6,7 @@
|
|
|
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
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';
|
|
12
12
|
import { StepStatus } from './plan.js';
|
|
@@ -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
|
-
|
|
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) {
|