@reticlehq/server 2.7.0 → 2.9.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/README.md +14 -5
- package/SKILL.md +130 -778
- package/dist/bridge/bridge.d.ts +23 -0
- package/dist/bridge/bridge.js +80 -2
- package/dist/bridge/bridge.test-harness.d.ts +5 -0
- package/dist/bridge/bridge.test-harness.js +6 -4
- package/dist/capsule/causal-summary.d.ts +36 -1
- package/dist/capsule/causal-summary.js +33 -7
- package/dist/capsule/divergence.js +7 -1
- package/dist/cli/chromium-hint.d.ts +33 -5
- package/dist/cli/chromium-hint.js +82 -12
- package/dist/cli/cli-doctor.js +65 -34
- package/dist/cli/cli-flow-commands.js +8 -8
- package/dist/cli/cli-kill.d.ts +58 -0
- package/dist/cli/cli-kill.js +162 -0
- package/dist/cli/cli-parse.d.ts +16 -1
- package/dist/cli/cli-parse.js +32 -9
- package/dist/cli/cli-port.d.ts +32 -0
- package/dist/cli/cli-port.js +82 -35
- package/dist/cli/cli-verify.d.ts +21 -0
- package/dist/cli/cli-verify.js +47 -3
- package/dist/cli/config-discovery.d.ts +35 -0
- package/dist/cli/config-discovery.js +163 -0
- package/dist/cli/doctor-daemon-line.js +4 -1
- package/dist/cli/doctor-project-line.d.ts +37 -0
- package/dist/cli/doctor-project-line.js +35 -0
- package/dist/cli/doctor-rows.d.ts +51 -0
- package/dist/cli/doctor-rows.js +54 -0
- package/dist/cli/doctor-sessions-line.d.ts +12 -0
- package/dist/cli/doctor-sessions-line.js +69 -0
- package/dist/cli/drive-attach.d.ts +99 -0
- package/dist/cli/drive-attach.js +203 -0
- package/dist/cli/drive-command.d.ts +13 -0
- package/dist/cli/drive-command.js +103 -0
- package/dist/cli/open-note.d.ts +20 -0
- package/dist/cli/open-note.js +37 -0
- package/dist/cli/port-holder.d.ts +10 -0
- package/dist/cli/port-holder.js +26 -2
- package/dist/cli/status-next-action.d.ts +37 -0
- package/dist/cli/status-next-action.js +61 -0
- package/dist/cli.js +119 -39
- package/dist/crawl/crawl-tools.js +12 -2
- package/dist/crawl/crawl.d.ts +13 -0
- package/dist/crawl/crawl.js +50 -2
- package/dist/events/contradictions.d.ts +46 -0
- package/dist/events/contradictions.js +35 -22
- package/dist/events/declared.d.ts +45 -0
- package/dist/events/declared.js +73 -0
- package/dist/events/echo-mismatch.d.ts +8 -2
- package/dist/events/echo-mismatch.js +33 -5
- package/dist/events/event-filters.d.ts +1 -1
- package/dist/events/event-filters.js +7 -4
- package/dist/events/predicate-eval.d.ts +17 -72
- package/dist/events/predicate-eval.js +117 -236
- package/dist/events/predicate-parse.js +31 -3
- package/dist/events/predicate-route.d.ts +32 -0
- package/dist/events/predicate-route.js +102 -0
- package/dist/events/predicate-schema.d.ts +99 -0
- package/dist/events/predicate-schema.js +309 -0
- package/dist/events/predicate.d.ts +8 -0
- package/dist/events/predicate.js +296 -22
- package/dist/flows/annotate-tools.js +3 -0
- package/dist/flows/annotate.js +18 -4
- package/dist/flows/coverage.d.ts +38 -5
- package/dist/flows/coverage.js +30 -4
- package/dist/flows/decision.d.ts +15 -0
- package/dist/flows/decision.js +1 -1
- package/dist/flows/flow-classify.js +1 -1
- package/dist/flows/flow-replay-run.js +8 -1
- package/dist/flows/flow-tools.js +21 -5
- package/dist/flows/heal-run.js +1 -1
- package/dist/flows/replay.js +3 -0
- package/dist/honesty/accepted-write.d.ts +3 -0
- package/dist/honesty/accepted-write.js +8 -3
- package/dist/honesty/already-true.js +15 -0
- package/dist/honesty/blind-spots.js +10 -2
- package/dist/honesty/honesty.d.ts +11 -0
- package/dist/honesty/honesty.js +1 -0
- package/dist/honesty/uncaptured-bodies.d.ts +4 -2
- package/dist/honesty/uncaptured-bodies.js +9 -2
- package/dist/honesty/unread-outcome.d.ts +8 -0
- package/dist/honesty/unread-outcome.js +36 -9
- package/dist/honesty/unsettled.d.ts +49 -0
- package/dist/honesty/unsettled.js +82 -0
- package/dist/honesty/verified.d.ts +36 -3
- package/dist/honesty/verified.js +54 -11
- package/dist/http-server.d.ts +19 -0
- package/dist/http-server.js +202 -4
- package/dist/impact/impact-recorder.d.ts +23 -0
- package/dist/impact/impact-recorder.js +76 -0
- package/dist/impact/impact-store.d.ts +47 -0
- package/dist/impact/impact-store.js +171 -0
- package/dist/index.d.ts +7 -0
- package/dist/index.js +47 -10
- package/dist/init/agent-rules.d.ts +36 -0
- package/dist/init/agent-rules.js +132 -22
- package/dist/init/astro-patch.d.ts +2 -1
- package/dist/init/astro-patch.js +38 -6
- package/dist/init/capabilities.d.ts +22 -0
- package/dist/init/capabilities.js +76 -0
- package/dist/init/confirm.d.ts +114 -0
- package/dist/init/confirm.js +165 -0
- package/dist/init/connect-steps.d.ts +2 -0
- package/dist/init/connect-steps.js +50 -0
- package/dist/init/cra.js +3 -2
- package/dist/init/csp-check.d.ts +38 -0
- package/dist/init/csp-check.js +92 -0
- package/dist/init/csp-doctor.d.ts +25 -0
- package/dist/init/csp-doctor.js +49 -0
- package/dist/init/cursor.js +6 -9
- package/dist/init/detect.d.ts +8 -0
- package/dist/init/detect.js +33 -3
- package/dist/init/mcp-clients.d.ts +1 -0
- package/dist/init/mcp-clients.js +51 -7
- package/dist/init/mcp.d.ts +15 -0
- package/dist/init/mcp.js +19 -0
- package/dist/init/non-js-project.js +29 -5
- package/dist/init/plan-framework.d.ts +10 -0
- package/dist/init/plan-framework.js +78 -16
- package/dist/init/plan.d.ts +47 -6
- package/dist/init/plan.js +228 -69
- package/dist/init/run.d.ts +24 -2
- package/dist/init/run.js +157 -45
- package/dist/init/slash-command.d.ts +1 -1
- package/dist/init/slash-command.js +54 -6
- package/dist/init/snippets.d.ts +45 -5
- package/dist/init/snippets.js +194 -42
- package/dist/init/vite-config.js +53 -5
- package/dist/input/network-mock-tools.d.ts +19 -0
- package/dist/input/network-mock-tools.js +37 -5
- package/dist/input/scroll-find.d.ts +6 -0
- package/dist/input/scroll-find.js +17 -2
- package/dist/input/scroll-tools.js +4 -0
- package/dist/journal/attach-journal.d.ts +9 -1
- package/dist/journal/attach-journal.js +13 -2
- package/dist/journal/journal-query.d.ts +13 -0
- package/dist/journal/journal-query.js +28 -3
- package/dist/journal/workspace-gitignore.d.ts +10 -0
- package/dist/journal/workspace-gitignore.js +59 -0
- package/dist/license/license.js +1 -1
- package/dist/mcp/mcp-proxy.d.ts +35 -30
- package/dist/mcp/mcp-proxy.js +306 -104
- package/dist/mcp/mcp.d.ts +10 -1
- package/dist/mcp/mcp.js +70 -34
- package/dist/mcp/proxy-handshake.d.ts +82 -1
- package/dist/mcp/proxy-handshake.js +109 -2
- package/dist/mcp/proxy-lifecycle.d.ts +11 -2
- package/dist/mcp/proxy-lifecycle.js +12 -3
- package/dist/mcp/proxy-log.d.ts +63 -0
- package/dist/mcp/proxy-log.js +132 -0
- package/dist/mcp/server-instructions.d.ts +17 -0
- package/dist/mcp/server-instructions.js +56 -0
- package/dist/mcp/shared-params.d.ts +27 -0
- package/dist/mcp/shared-params.js +36 -0
- package/dist/pool/browser-pool.d.ts +22 -2
- package/dist/pool/browser-pool.js +44 -4
- package/dist/project/learned-routes.d.ts +24 -0
- package/dist/project/learned-routes.js +63 -0
- package/dist/project/project-store.d.ts +6 -0
- package/dist/project/project-store.js +25 -1
- package/dist/project/reticle-dir.d.ts +2 -0
- package/dist/project/reticle-dir.js +1 -0
- package/dist/session/connection-memory.d.ts +59 -0
- package/dist/session/connection-memory.js +108 -0
- package/dist/session/dev-command.d.ts +26 -0
- package/dist/session/dev-command.js +89 -0
- package/dist/session/dev-server-probe.d.ts +68 -0
- package/dist/session/dev-server-probe.js +102 -7
- package/dist/session/last-act.d.ts +26 -0
- package/dist/session/last-act.js +35 -1
- package/dist/session/live-control-tools.js +42 -5
- package/dist/session/no-session-diagnosis.d.ts +67 -2
- package/dist/session/no-session-diagnosis.js +236 -27
- package/dist/session/no-session-next-action.d.ts +46 -0
- package/dist/session/no-session-next-action.js +85 -0
- package/dist/session/no-session-watch.d.ts +40 -1
- package/dist/session/no-session-watch.js +174 -8
- package/dist/session/pending-commands.d.ts +12 -0
- package/dist/session/pending-commands.js +13 -1
- package/dist/session/ref-provenance.d.ts +54 -0
- package/dist/session/ref-provenance.js +61 -0
- package/dist/session/session-lease.js +1 -1
- package/dist/session/session-manager.d.ts +4 -0
- package/dist/session/session-manager.js +35 -5
- package/dist/session/session.d.ts +10 -1
- package/dist/session/session.js +146 -5
- package/dist/telemetry/app-instrumented.d.ts +8 -0
- package/dist/telemetry/app-instrumented.js +10 -0
- package/dist/telemetry/automation-hint.d.ts +17 -0
- package/dist/telemetry/automation-hint.js +71 -0
- package/dist/telemetry/bug-found.d.ts +15 -17
- package/dist/telemetry/bug-found.js +52 -21
- package/dist/telemetry/cli-telemetry.js +7 -1
- package/dist/telemetry/daemon-telemetry.js +24 -0
- package/dist/telemetry/feedback.js +1 -1
- package/dist/telemetry/init-telemetry.js +9 -1
- package/dist/telemetry/install-source.d.ts +56 -0
- package/dist/telemetry/install-source.js +74 -0
- package/dist/telemetry/instrumentation-stall.d.ts +50 -0
- package/dist/telemetry/instrumentation-stall.js +90 -0
- package/dist/telemetry/run-telemetry.js +4 -1
- package/dist/telemetry/session-metrics.d.ts +8 -0
- package/dist/telemetry/session-metrics.js +15 -0
- package/dist/telemetry/telemetry.d.ts +7 -1
- package/dist/telemetry/telemetry.js +26 -2
- package/dist/telemetry/tool-refused.d.ts +19 -0
- package/dist/telemetry/tool-refused.js +55 -0
- package/dist/telemetry/verification-of.js +46 -7
- package/dist/temp-dir.d.ts +27 -0
- package/dist/temp-dir.js +46 -0
- package/dist/tools/act-tools.js +209 -28
- package/dist/tools/assert-grade.d.ts +1 -1
- package/dist/tools/assert-grade.js +27 -5
- package/dist/tools/assert-verdict.js +49 -7
- package/dist/tools/browser-tools.js +10 -0
- package/dist/tools/contract-tools.js +4 -0
- package/dist/tools/dynamic-tools.js +16 -7
- package/dist/tools/error-recovery.d.ts +10 -0
- package/dist/tools/error-recovery.js +73 -9
- package/dist/tools/feedback-invite.js +1 -1
- package/dist/tools/invoke-tool.js +159 -26
- package/dist/tools/lease-hint.d.ts +30 -8
- package/dist/tools/lease-hint.js +65 -14
- package/dist/tools/lease-tools.d.ts +40 -1
- package/dist/tools/lease-tools.js +181 -83
- package/dist/tools/navigate-result.js +1 -1
- package/dist/tools/observe-tools.js +10 -4
- package/dist/tools/observed-nothing.js +1 -1
- package/dist/tools/react-grace.d.ts +52 -0
- package/dist/tools/react-grace.js +90 -0
- package/dist/tools/read-tools.js +67 -5
- package/dist/tools/resolve-target.d.ts +33 -0
- package/dist/tools/resolve-target.js +53 -0
- package/dist/tools/settle-in-flight.d.ts +94 -0
- package/dist/tools/settle-in-flight.js +146 -0
- package/dist/tools/snapshot-delta.d.ts +10 -0
- package/dist/tools/snapshot-delta.js +194 -9
- package/dist/tools/tool-kit.d.ts +8 -0
- package/dist/tools/tool-surface.d.ts +60 -0
- package/dist/tools/tool-surface.js +86 -1
- package/dist/tools/tools.js +61 -8
- package/dist/tools/unadvertised-help.js +9 -6
- package/docs/README.md +55 -11
- package/docs/actions.mdx +120 -0
- package/docs/agent-cheatsheet.md +95 -29
- package/docs/architecture.md +52 -35
- package/docs/benchmarks.md +127 -75
- package/docs/best-practices.mdx +216 -0
- package/docs/capabilities.mdx +534 -0
- package/docs/cli/affected.mdx +53 -0
- package/docs/cli/capsules.mdx +70 -0
- package/docs/cli/cloud.mdx +153 -0
- package/docs/cli/doctor.mdx +109 -0
- package/docs/cli/drive.mdx +74 -0
- package/docs/cli/feedback.mdx +67 -0
- package/docs/cli/gate.mdx +64 -0
- package/docs/cli/help.mdx +94 -0
- package/docs/cli/hunt.mdx +73 -0
- package/docs/cli/identify.mdx +76 -0
- package/docs/cli/init.mdx +121 -0
- package/docs/cli/kill.mdx +91 -0
- package/docs/cli/license.mdx +48 -0
- package/docs/cli/mcp.mdx +61 -0
- package/docs/cli/open.mdx +74 -0
- package/docs/cli/restart.mdx +60 -0
- package/docs/cli/rollback.mdx +35 -0
- package/docs/cli/serve.mdx +69 -0
- package/docs/cli/status.mdx +88 -0
- package/docs/cli/stop.mdx +64 -0
- package/docs/cli/telemetry.mdx +57 -0
- package/docs/cli/update.mdx +52 -0
- package/docs/cli/verify.mdx +76 -0
- package/docs/cli/version.mdx +39 -0
- package/docs/cli/watch.mdx +62 -0
- package/docs/cli.mdx +241 -0
- package/docs/debugging.md +16 -10
- package/docs/deploy-checks.md +45 -12
- package/docs/desktop.mdx +384 -0
- package/docs/docs.json +364 -0
- package/docs/enterprise.md +22 -16
- package/docs/faq.mdx +102 -0
- package/docs/fixtures.md +13 -7
- package/docs/flows.md +29 -23
- package/docs/for-agents.md +71 -0
- package/docs/frameworks.mdx +238 -0
- package/docs/gate-plan.md +48 -42
- package/docs/gates.md +28 -22
- package/docs/getting-started.md +129 -77
- package/docs/human-control.md +24 -18
- package/docs/index.mdx +143 -0
- package/docs/install-agentic.mdx +224 -0
- package/docs/install-manual.mdx +196 -0
- package/docs/instrumentation.mdx +238 -0
- package/docs/integration-patterns.md +41 -35
- package/docs/local-registry.md +19 -13
- package/docs/multi-agent-testing.md +19 -13
- package/docs/packages/babel-plugin.mdx +79 -0
- package/docs/packages/browser.mdx +111 -0
- package/docs/packages/core.mdx +116 -0
- package/docs/packages/electron.mdx +85 -0
- package/docs/packages/eslint-plugin.mdx +86 -0
- package/docs/packages/next.mdx +89 -0
- package/docs/packages/react.mdx +91 -0
- package/docs/packages/server.mdx +98 -0
- package/docs/packages/tauri.mdx +63 -0
- package/docs/packages/test.mdx +119 -0
- package/docs/packages/vite-plugin.mdx +94 -0
- package/docs/packages.mdx +218 -0
- package/docs/platform-integration.md +35 -26
- package/docs/predicates.mdx +248 -0
- package/docs/quickstart.mdx +257 -0
- package/docs/recipes.mdx +282 -0
- package/docs/skill-file.mdx +107 -0
- package/docs/state-management.mdx +199 -0
- package/docs/style.css +14 -0
- package/docs/system-map.md +50 -33
- package/docs/telemetry-contract.md +106 -52
- package/docs/telemetry-events.mdx +475 -0
- package/docs/telemetry.md +63 -55
- package/docs/testing.md +44 -6
- package/docs/token-efficiency.md +37 -27
- package/docs/tools-act-and-wait.mdx +173 -0
- package/docs/tools-act-sequence.mdx +137 -0
- package/docs/tools-act.mdx +95 -0
- package/docs/tools-assert.mdx +129 -0
- package/docs/tools-console.mdx +63 -0
- package/docs/tools-inspect.mdx +88 -0
- package/docs/tools-navigate.mdx +99 -0
- package/docs/tools-network.mdx +119 -0
- package/docs/tools-observe.mdx +145 -0
- package/docs/tools-overview.mdx +160 -0
- package/docs/tools-query.mdx +126 -0
- package/docs/tools-session-and-feedback.mdx +112 -0
- package/docs/tools-sessions.mdx +94 -0
- package/docs/tools-snapshot.mdx +129 -0
- package/docs/tools-state.mdx +104 -0
- package/docs/tools-tools-and-run.mdx +156 -0
- package/docs/tools-wait-for.mdx +85 -0
- package/docs/troubleshooting.mdx +263 -0
- package/docs/usage.md +209 -165
- package/docs/vs-chrome-devtools-mcp.mdx +123 -0
- package/docs/vs-playwright-mcp.mdx +125 -0
- package/docs/vs-screenshots.mdx +137 -0
- package/docs/what-is-reticle.mdx +63 -0
- package/docs/why-reticle.mdx +109 -0
- package/package.json +5 -5
- package/docs/desktop-apps.md +0 -256
- package/docs/matrix/2.5.0/claude-code-machine.json +0 -19
- package/docs/matrix/2.5.0/codex-machine.json +0 -20
- package/docs/matrix/2.5.0/cursor-machine.json +0 -22
- package/docs/matrix/2.5.0/gemini-machine.json +0 -22
- package/docs/matrix/2.5.0/opencode-machine.json +0 -22
- package/docs/matrix/2.5.0/vscode-machine.json +0 -22
- package/docs/matrix/2.5.0/windsurf-machine.json +0 -22
- package/docs/matrix/MATRIX.md +0 -26
- package/docs/matrix/README.md +0 -59
- package/docs/mint.json +0 -61
|
@@ -0,0 +1,114 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* The step `init` never took: stay and watch for an app to connect.
|
|
3
|
+
*
|
|
4
|
+
* The install has two halves. Registering the MCP server gives the agent the tools; getting the SDK
|
|
5
|
+
* into a running page gives it something to look at. Almost everyone finishes the first, and nothing
|
|
6
|
+
* joined "init finished" to "an app connected" — so `init` exited 0 having written files, and the
|
|
7
|
+
* person left with a config file and no instrumented page was never told (#269).
|
|
8
|
+
*
|
|
9
|
+
* The capability existed in three places and was used together by none of them: the poll behind
|
|
10
|
+
* `reticle open`, the install gate (which calls this exact check "the only step that proves the
|
|
11
|
+
* install works"), and the event that measures whether it ever happened.
|
|
12
|
+
*
|
|
13
|
+
* WHY NO FLAG, and no dev-server management:
|
|
14
|
+
*
|
|
15
|
+
* - No flag, because the people this is for are the ones who do not know to ask. `--verify` would
|
|
16
|
+
* be typed by the users who already run `status` afterwards, which is exactly the cohort that is
|
|
17
|
+
* not being lost. The gate instead is whether a HUMAN IS AT THE TERMINAL: `init` waits when
|
|
18
|
+
* stdout is a TTY and skips the wait entirely otherwise, so a scripted or agent-driven `init`
|
|
19
|
+
* keeps today's cost and today's exit behaviour, and blocking never happens where blocking is
|
|
20
|
+
* wrong. The signal is the standard "am I being piped" one and needs no argument parsing, which
|
|
21
|
+
* also keeps `init`'s documented flag surface where it is.
|
|
22
|
+
* - It never starts anything. A dev server started by a long-lived daemon is invisible to the
|
|
23
|
+
* person whose machine it runs on and orphans when the daemon exits, so the daemon stays out of
|
|
24
|
+
* it — the AGENT starts one, under the guards in agent-rules.ts, because it is attributable and
|
|
25
|
+
* stoppable there. So the honest shape here is: confirm the app if it connects, otherwise name
|
|
26
|
+
* the ONE command that is outstanding and the one that proves it worked.
|
|
27
|
+
*/
|
|
28
|
+
import { InitConfirmation } from '@reticlehq/core';
|
|
29
|
+
import type { InitOutcome } from '@reticlehq/core';
|
|
30
|
+
/**
|
|
31
|
+
* How long a human is asked to wait.
|
|
32
|
+
*
|
|
33
|
+
* Long enough for a page that is already loading to finish dialling, short enough that somebody who
|
|
34
|
+
* has not restarted their dev server yet is not held hostage by a wait that cannot succeed — they
|
|
35
|
+
* have not typed the command that would make it succeed, and the message tells them which one.
|
|
36
|
+
*/
|
|
37
|
+
export declare const CONFIRM_WINDOW_MS = 12000;
|
|
38
|
+
export declare const CONFIRM_POLL_MS = 500;
|
|
39
|
+
export interface ConfirmDeps {
|
|
40
|
+
/** Session ids connected right now, or null when nothing is listening on the bridge port. */
|
|
41
|
+
listSessionIds: () => Promise<readonly string[] | null>;
|
|
42
|
+
now: () => number;
|
|
43
|
+
sleep: (ms: number) => Promise<void>;
|
|
44
|
+
windowMs?: number;
|
|
45
|
+
pollMs?: number;
|
|
46
|
+
}
|
|
47
|
+
/** The narrow slice of the init IO this needs — it prints, and nothing else. */
|
|
48
|
+
interface PrintIo {
|
|
49
|
+
print: (line: string) => void;
|
|
50
|
+
}
|
|
51
|
+
/** What `runInit` hands back; `outcome` is present only on a real (non-dry) run. */
|
|
52
|
+
interface ConfirmableResult {
|
|
53
|
+
ok: boolean;
|
|
54
|
+
applied: number;
|
|
55
|
+
manual: number;
|
|
56
|
+
outcome?: InitOutcome;
|
|
57
|
+
}
|
|
58
|
+
/**
|
|
59
|
+
* Poll until an app connects or the window runs out.
|
|
60
|
+
*
|
|
61
|
+
* A session that was ALREADY there when the wait began does not count, and this is the whole reason
|
|
62
|
+
* the check reads ids rather than a count. A daemon is shared across every project on the machine,
|
|
63
|
+
* so "a session exists" is satisfied by another app, by a stray tab, by yesterday's work — and
|
|
64
|
+
* confirming this install on somebody else's session is a false green in the one message a user
|
|
65
|
+
* reads to decide whether they are done. `reticle open` refuses the same way, for the same reason.
|
|
66
|
+
*
|
|
67
|
+
* The conservative direction is deliberate: an app that reconnects under an id it already held is
|
|
68
|
+
* reported as not-yet-connected, and the message it gets sends them to `status`, which shows it. A
|
|
69
|
+
* missed confirmation costs one command; a false one costs the whole point of confirming.
|
|
70
|
+
*/
|
|
71
|
+
export declare function confirmAppConnected(deps: ConfirmDeps): Promise<InitConfirmation>;
|
|
72
|
+
/**
|
|
73
|
+
* What the user reads, in the marks the rest of the report already uses: `⚠` means work left to do
|
|
74
|
+
* and nothing else, `ℹ` is worth reading but is not work, `✓` happened.
|
|
75
|
+
*
|
|
76
|
+
* Only NO_SESSION earns a `⚠`, and it earns it honestly: the app is wired and has not connected, so
|
|
77
|
+
* there is a command outstanding. A missing daemon is not a defect in the install — the daemon comes
|
|
78
|
+
* up when the agent loads the tools — so it is a notice.
|
|
79
|
+
*/
|
|
80
|
+
export declare function confirmationMessage(confirmation: InitConfirmation, port: number): string;
|
|
81
|
+
/**
|
|
82
|
+
* What a non-TTY run says instead of waiting.
|
|
83
|
+
*
|
|
84
|
+
* `init` deliberately does not block when nobody is at the terminal — a 12-second wait in a script
|
|
85
|
+
* is wrong, and an agent driving `init` through a shell is never a TTY. But silence was the wrong
|
|
86
|
+
* other half of that decision: the agent path IS the prescribed path (every skill and every README
|
|
87
|
+
* block tells an agent to run exactly this command), so the one message joining "init finished" to
|
|
88
|
+
* "an app connected" was withheld from most of the people who run it. They were left with files on
|
|
89
|
+
* disk, an exit code of 0, and nothing saying the page half had not happened.
|
|
90
|
+
*
|
|
91
|
+
* It claims nothing it did not check. It did not look for a session, so it does not say whether one
|
|
92
|
+
* exists — a one-shot look would be worse than none, because a daemon is shared across every project
|
|
93
|
+
* on the machine and "a session exists" is satisfied by somebody else's app, which is the false
|
|
94
|
+
* green `confirmAppConnected` refuses by design.
|
|
95
|
+
*/
|
|
96
|
+
export declare function unwatchedMessage(port: number): string;
|
|
97
|
+
/**
|
|
98
|
+
* Finish `init` on evidence rather than on a write.
|
|
99
|
+
*
|
|
100
|
+
* The telemetry report happens HERE rather than inside `runInit`, so the one `init_completed` event
|
|
101
|
+
* carries what was seen. Emitting inside `runInit` and again after the wait would double-count the
|
|
102
|
+
* funnel it exists to measure, and a second event kind for the same command would need joining back
|
|
103
|
+
* to the first before anybody could read either.
|
|
104
|
+
*/
|
|
105
|
+
export declare function confirmInstall(result: ConfirmableResult, io: PrintIo, deps: ConfirmDeps & {
|
|
106
|
+
interactive: boolean;
|
|
107
|
+
port: number;
|
|
108
|
+
}, report?: (outcome: InitOutcome) => void): Promise<void>;
|
|
109
|
+
/** The real deps: the daemon's own status endpoint, and whether a human is watching. */
|
|
110
|
+
export declare function nodeConfirmDeps(port?: number): ConfirmDeps & {
|
|
111
|
+
interactive: boolean;
|
|
112
|
+
port: number;
|
|
113
|
+
};
|
|
114
|
+
export {};
|
|
@@ -0,0 +1,165 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* The step `init` never took: stay and watch for an app to connect.
|
|
3
|
+
*
|
|
4
|
+
* The install has two halves. Registering the MCP server gives the agent the tools; getting the SDK
|
|
5
|
+
* into a running page gives it something to look at. Almost everyone finishes the first, and nothing
|
|
6
|
+
* joined "init finished" to "an app connected" — so `init` exited 0 having written files, and the
|
|
7
|
+
* person left with a config file and no instrumented page was never told (#269).
|
|
8
|
+
*
|
|
9
|
+
* The capability existed in three places and was used together by none of them: the poll behind
|
|
10
|
+
* `reticle open`, the install gate (which calls this exact check "the only step that proves the
|
|
11
|
+
* install works"), and the event that measures whether it ever happened.
|
|
12
|
+
*
|
|
13
|
+
* WHY NO FLAG, and no dev-server management:
|
|
14
|
+
*
|
|
15
|
+
* - No flag, because the people this is for are the ones who do not know to ask. `--verify` would
|
|
16
|
+
* be typed by the users who already run `status` afterwards, which is exactly the cohort that is
|
|
17
|
+
* not being lost. The gate instead is whether a HUMAN IS AT THE TERMINAL: `init` waits when
|
|
18
|
+
* stdout is a TTY and skips the wait entirely otherwise, so a scripted or agent-driven `init`
|
|
19
|
+
* keeps today's cost and today's exit behaviour, and blocking never happens where blocking is
|
|
20
|
+
* wrong. The signal is the standard "am I being piped" one and needs no argument parsing, which
|
|
21
|
+
* also keeps `init`'s documented flag surface where it is.
|
|
22
|
+
* - It never starts anything. A dev server started by a long-lived daemon is invisible to the
|
|
23
|
+
* person whose machine it runs on and orphans when the daemon exits, so the daemon stays out of
|
|
24
|
+
* it — the AGENT starts one, under the guards in agent-rules.ts, because it is attributable and
|
|
25
|
+
* stoppable there. So the honest shape here is: confirm the app if it connects, otherwise name
|
|
26
|
+
* the ONE command that is outstanding and the one that proves it worked.
|
|
27
|
+
*/
|
|
28
|
+
import { InitConfirmation, RETICLE_DEFAULT_PORT } from '@reticlehq/core';
|
|
29
|
+
import { fetchStatus, summarizeStatus } from '../cli/cli-launch.js';
|
|
30
|
+
import { reportInitOutcome } from '../telemetry/init-telemetry.js';
|
|
31
|
+
/**
|
|
32
|
+
* How long a human is asked to wait.
|
|
33
|
+
*
|
|
34
|
+
* Long enough for a page that is already loading to finish dialling, short enough that somebody who
|
|
35
|
+
* has not restarted their dev server yet is not held hostage by a wait that cannot succeed — they
|
|
36
|
+
* have not typed the command that would make it succeed, and the message tells them which one.
|
|
37
|
+
*/
|
|
38
|
+
export const CONFIRM_WINDOW_MS = 12_000;
|
|
39
|
+
export const CONFIRM_POLL_MS = 500;
|
|
40
|
+
/**
|
|
41
|
+
* Poll until an app connects or the window runs out.
|
|
42
|
+
*
|
|
43
|
+
* A session that was ALREADY there when the wait began does not count, and this is the whole reason
|
|
44
|
+
* the check reads ids rather than a count. A daemon is shared across every project on the machine,
|
|
45
|
+
* so "a session exists" is satisfied by another app, by a stray tab, by yesterday's work — and
|
|
46
|
+
* confirming this install on somebody else's session is a false green in the one message a user
|
|
47
|
+
* reads to decide whether they are done. `reticle open` refuses the same way, for the same reason.
|
|
48
|
+
*
|
|
49
|
+
* The conservative direction is deliberate: an app that reconnects under an id it already held is
|
|
50
|
+
* reported as not-yet-connected, and the message it gets sends them to `status`, which shows it. A
|
|
51
|
+
* missed confirmation costs one command; a false one costs the whole point of confirming.
|
|
52
|
+
*/
|
|
53
|
+
export async function confirmAppConnected(deps) {
|
|
54
|
+
const deadline = deps.now() + (deps.windowMs ?? CONFIRM_WINDOW_MS);
|
|
55
|
+
let before;
|
|
56
|
+
for (;;) {
|
|
57
|
+
const ids = await deps.listSessionIds();
|
|
58
|
+
// A port nobody is listening on cannot receive a session, and no amount of waiting changes that.
|
|
59
|
+
// One look settles it; polling on would spend the whole window to re-learn the first answer.
|
|
60
|
+
if (null === ids)
|
|
61
|
+
return InitConfirmation.NO_DAEMON;
|
|
62
|
+
const baseline = before;
|
|
63
|
+
if (baseline === undefined)
|
|
64
|
+
before = new Set(ids);
|
|
65
|
+
else if (ids.some((id) => !baseline.has(id)))
|
|
66
|
+
return InitConfirmation.CONNECTED;
|
|
67
|
+
if (deadline <= deps.now())
|
|
68
|
+
return InitConfirmation.NO_SESSION;
|
|
69
|
+
await deps.sleep(deps.pollMs ?? CONFIRM_POLL_MS);
|
|
70
|
+
}
|
|
71
|
+
}
|
|
72
|
+
const PROVE_COMMAND = '`npx @reticlehq/server status`';
|
|
73
|
+
/**
|
|
74
|
+
* What the user reads, in the marks the rest of the report already uses: `⚠` means work left to do
|
|
75
|
+
* and nothing else, `ℹ` is worth reading but is not work, `✓` happened.
|
|
76
|
+
*
|
|
77
|
+
* Only NO_SESSION earns a `⚠`, and it earns it honestly: the app is wired and has not connected, so
|
|
78
|
+
* there is a command outstanding. A missing daemon is not a defect in the install — the daemon comes
|
|
79
|
+
* up when the agent loads the tools — so it is a notice.
|
|
80
|
+
*/
|
|
81
|
+
export function confirmationMessage(confirmation, port) {
|
|
82
|
+
if (confirmation === InitConfirmation.CONNECTED) {
|
|
83
|
+
return (' [✓] an app is connected — that is the install finished, not just written.\n' +
|
|
84
|
+
' Ask your agent to drive a flow.');
|
|
85
|
+
}
|
|
86
|
+
if (confirmation === InitConfirmation.NO_DAEMON) {
|
|
87
|
+
return (` [ℹ] nothing is listening on port ${String(port)} yet, so no app could have connected and ` +
|
|
88
|
+
'this run did not wait for one.\n' +
|
|
89
|
+
' The daemon starts on its own when your agent loads the Reticle tools. After that, ' +
|
|
90
|
+
`${PROVE_COMMAND} confirms the app.`);
|
|
91
|
+
}
|
|
92
|
+
return (' [⚠] no app has connected yet — the files are written, the page half of the install is not ' +
|
|
93
|
+
'done.\n' +
|
|
94
|
+
' Restart your dev server and load the app in a browser — or ask your agent to, which it is ' +
|
|
95
|
+
'now told to do for you, in the background and with the command from your own scripts.\n' +
|
|
96
|
+
` ${PROVE_COMMAND} then confirms it, or says exactly why it has not connected.`);
|
|
97
|
+
}
|
|
98
|
+
/**
|
|
99
|
+
* What a non-TTY run says instead of waiting.
|
|
100
|
+
*
|
|
101
|
+
* `init` deliberately does not block when nobody is at the terminal — a 12-second wait in a script
|
|
102
|
+
* is wrong, and an agent driving `init` through a shell is never a TTY. But silence was the wrong
|
|
103
|
+
* other half of that decision: the agent path IS the prescribed path (every skill and every README
|
|
104
|
+
* block tells an agent to run exactly this command), so the one message joining "init finished" to
|
|
105
|
+
* "an app connected" was withheld from most of the people who run it. They were left with files on
|
|
106
|
+
* disk, an exit code of 0, and nothing saying the page half had not happened.
|
|
107
|
+
*
|
|
108
|
+
* It claims nothing it did not check. It did not look for a session, so it does not say whether one
|
|
109
|
+
* exists — a one-shot look would be worse than none, because a daemon is shared across every project
|
|
110
|
+
* on the machine and "a session exists" is satisfied by somebody else's app, which is the false
|
|
111
|
+
* green `confirmAppConnected` refuses by design.
|
|
112
|
+
*/
|
|
113
|
+
export function unwatchedMessage(port) {
|
|
114
|
+
return (' [ℹ] the files are written. An app CONNECTING is what finishes the install, and this run did ' +
|
|
115
|
+
'not wait to see one.\n' +
|
|
116
|
+
' Start the dev server, load the app in a browser, then ' +
|
|
117
|
+
`${PROVE_COMMAND} — it confirms the app, or says exactly why it has not connected.\n` +
|
|
118
|
+
` Bridge port ${String(port)}.`);
|
|
119
|
+
}
|
|
120
|
+
const WATCHING = (windowMs) => ` watching for an app to connect (up to ${String(Math.round(windowMs / 1000))}s) — Ctrl-C is safe, ` +
|
|
121
|
+
'everything above is already written.';
|
|
122
|
+
/**
|
|
123
|
+
* Finish `init` on evidence rather than on a write.
|
|
124
|
+
*
|
|
125
|
+
* The telemetry report happens HERE rather than inside `runInit`, so the one `init_completed` event
|
|
126
|
+
* carries what was seen. Emitting inside `runInit` and again after the wait would double-count the
|
|
127
|
+
* funnel it exists to measure, and a second event kind for the same command would need joining back
|
|
128
|
+
* to the first before anybody could read either.
|
|
129
|
+
*/
|
|
130
|
+
export async function confirmInstall(result, io, deps, report = reportInitOutcome) {
|
|
131
|
+
const outcome = result.outcome;
|
|
132
|
+
// No outcome means a dry run or an exit that already reported for itself (no package.json). A
|
|
133
|
+
// preview is not an install and must not land in the funnel as either a success or a failure.
|
|
134
|
+
if (outcome === undefined)
|
|
135
|
+
return;
|
|
136
|
+
if (!deps.interactive) {
|
|
137
|
+
// Still no waiting — see unwatchedMessage for why it speaks anyway.
|
|
138
|
+
io.print('');
|
|
139
|
+
io.print(unwatchedMessage(deps.port));
|
|
140
|
+
report(outcome);
|
|
141
|
+
return;
|
|
142
|
+
}
|
|
143
|
+
io.print('');
|
|
144
|
+
io.print(WATCHING(deps.windowMs ?? CONFIRM_WINDOW_MS));
|
|
145
|
+
const confirmation = await confirmAppConnected(deps);
|
|
146
|
+
io.print(confirmationMessage(confirmation, deps.port));
|
|
147
|
+
report({ ...outcome, confirmation });
|
|
148
|
+
}
|
|
149
|
+
/** The real deps: the daemon's own status endpoint, and whether a human is watching. */
|
|
150
|
+
export function nodeConfirmDeps(port = RETICLE_DEFAULT_PORT) {
|
|
151
|
+
return {
|
|
152
|
+
listSessionIds: async () => {
|
|
153
|
+
const payload = await fetchStatus(port);
|
|
154
|
+
// `fetchStatus` resolves undefined when nothing answered, which is precisely the "no daemon"
|
|
155
|
+
// case — a zero-session daemon and an absent one need opposite messages.
|
|
156
|
+
if (payload === undefined)
|
|
157
|
+
return null;
|
|
158
|
+
return summarizeStatus(payload).sessions.map((s) => s.sessionId);
|
|
159
|
+
},
|
|
160
|
+
now: () => Date.now(),
|
|
161
|
+
sleep: (ms) => new Promise((resolve) => setTimeout(resolve, ms)),
|
|
162
|
+
interactive: true === process.stdout.isTTY,
|
|
163
|
+
port,
|
|
164
|
+
};
|
|
165
|
+
}
|
|
@@ -0,0 +1,50 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Which `init` steps decide whether the app can dial the daemon.
|
|
3
|
+
*
|
|
4
|
+
* Lifted out of `plan.ts` when that file reached the size backstop. A cohesive unit on its own
|
|
5
|
+
* terms: one question, asked of a step title, with one consequence — a manual step in this set makes
|
|
6
|
+
* `init` exit non-zero instead of reporting a success over an app that can never connect.
|
|
7
|
+
*
|
|
8
|
+
* Matched on TITLES, which is the fragile part and worth stating: renaming a step in
|
|
9
|
+
* `plan-framework.ts` silently drops it out of this set and nothing goes red. `connect-steps.test.ts`
|
|
10
|
+
* pins the membership for exactly that reason.
|
|
11
|
+
*/
|
|
12
|
+
/**
|
|
13
|
+
* Titles of the steps WITHOUT which no session ever appears.
|
|
14
|
+
*
|
|
15
|
+
* A ⚠ on one of these is not a warning, it is a guaranteed failure: nothing performs the manual step,
|
|
16
|
+
* so the app will not connect and every Reticle tool will answer "no browser session connected".
|
|
17
|
+
* Reported from a field sweep, where the ⚠ count and "did it connect" were treated as independent
|
|
18
|
+
* signals and are not.
|
|
19
|
+
*/
|
|
20
|
+
const CONNECT_STEP_TITLES = new Set([
|
|
21
|
+
'Connect snippet',
|
|
22
|
+
'Connect snippet (CRA)',
|
|
23
|
+
'Connect snippet (Astro)',
|
|
24
|
+
'Connect snippet (Nuxt)',
|
|
25
|
+
'Reticle client hook',
|
|
26
|
+
'Reticle connect module',
|
|
27
|
+
'ReticleDev component',
|
|
28
|
+
// Writing the component and MOUNTING it are two steps, and only the write was here. A root layout
|
|
29
|
+
// whose shape `init` does not recognise leaves the component on disk and never rendered: the SDK
|
|
30
|
+
// is in the project, nothing imports it, and `init` exited 0 over an app that could not connect.
|
|
31
|
+
'Mount ReticleDev',
|
|
32
|
+
// NOT here, and the reason is worth keeping: 'Pairing token'.
|
|
33
|
+
//
|
|
34
|
+
// It is a genuine connect step — CRA inlines only REACT_APP_*, so without the token in the env
|
|
35
|
+
// file the bridge refuses every connection and the app boots, looks correct, and never pairs. But
|
|
36
|
+
// it goes MANUAL in exactly one situation: no daemon has ever run on this machine, so there is no
|
|
37
|
+
// token to inline. That is the FIRST CRA install on a fresh machine, i.e. the first-time user —
|
|
38
|
+
// and making that exit non-zero reports a broken install to the one person least able to tell
|
|
39
|
+
// that it is not.
|
|
40
|
+
//
|
|
41
|
+
// The real fix is for `init` to mint the token rather than only read it, which is what both the
|
|
42
|
+
// daemon and the Vite plugin already do. `runInit` is synchronous and the existing helpers are
|
|
43
|
+
// not, and `gate:install` scaffolds no CRA app, so that change would ship with no coverage of the
|
|
44
|
+
// path it changes. It is worth doing, and worth doing with a scaffold behind it.
|
|
45
|
+
'Vite plugin',
|
|
46
|
+
]);
|
|
47
|
+
/** True when this step is what makes the app dial the daemon. */
|
|
48
|
+
export function isConnectStep(title) {
|
|
49
|
+
return CONNECT_STEP_TITLES.has(title);
|
|
50
|
+
}
|
package/dist/init/cra.js
CHANGED
|
@@ -12,6 +12,7 @@
|
|
|
12
12
|
* `REACT_APP_`. So it travels through `.env.development.local`, which is CRA's own documented
|
|
13
13
|
* mechanism and is gitignored by CRA's own template.
|
|
14
14
|
*/
|
|
15
|
+
import { CLI } from './agent-rules.js';
|
|
15
16
|
/** The line added to `src/index.tsx`. Side-effect import: the module guards itself on NODE_ENV. */
|
|
16
17
|
export const CRA_DEV_MODULE_IMPORT = "import './reticle-dev';";
|
|
17
18
|
export const CRA_DEV_MODULE_PATH = 'src/reticle-dev.ts';
|
|
@@ -30,7 +31,7 @@ export const TOKEN_VAR = 'REACT_APP_RETICLE_TOKEN';
|
|
|
30
31
|
*/
|
|
31
32
|
export const CRA_TOKEN_MISSING_NOTE = `${TOKEN_VAR} is not set, so Reticle cannot pair with the daemon. ` +
|
|
32
33
|
`The pairing token is per-machine and ${CRA_ENV_PATH} is gitignored by CRA's template, ` +
|
|
33
|
-
|
|
34
|
+
`so it does not survive a clone. Run \`${CLI} init\` in this project to write it for this machine.`;
|
|
34
35
|
/**
|
|
35
36
|
* Said as its own NOTICE line, beside the write — not inside it.
|
|
36
37
|
*
|
|
@@ -46,7 +47,7 @@ export const CRA_TOKEN_MISSING_NOTE = `${TOKEN_VAR} is not set, so Reticle canno
|
|
|
46
47
|
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
48
|
'It works on THIS machine and nowhere else: a teammate cloning the repo, CI, or a container ' +
|
|
48
49
|
'gets an app that boots, never pairs, and reports it only in the browser console. ' +
|
|
49
|
-
|
|
50
|
+
`Each developer runs \`${CLI} init\` once locally.`;
|
|
50
51
|
/** Add the import after the last existing one, or null when it is already present. */
|
|
51
52
|
export function craImportPatch(source) {
|
|
52
53
|
if (source.includes(CRA_DEV_MODULE_IMPORT))
|
|
@@ -0,0 +1,38 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Does this app's Content-Security-Policy let the browser reach the Reticle bridge?
|
|
3
|
+
*
|
|
4
|
+
* Two independent field reports, both Next: `init` reported every step successful, the SDK mounted,
|
|
5
|
+
* the dial URL was correct, and the app never connected because a strict `connect-src` excluded
|
|
6
|
+
* `ws://localhost:<port>`. The browser blocks the WebSocket and says so in its own console, which
|
|
7
|
+
* nothing on the Reticle side reads — so `status` and `doctor` reported perfect health at an app
|
|
8
|
+
* that could not connect at all, forever. A setup step that exits 0 while leaving the user broken is
|
|
9
|
+
* the worst failure mode this repo has.
|
|
10
|
+
*
|
|
11
|
+
* A TEXT SCAN, deliberately. `headers()` is a function; `init` cannot execute a user's Next config
|
|
12
|
+
* without importing their whole app, and the thing worth finding is the policy string a developer
|
|
13
|
+
* typed — in `next.config.*`, in middleware, or in a `<meta http-equiv>` tag. The scan finds the
|
|
14
|
+
* string wherever it is written.
|
|
15
|
+
*
|
|
16
|
+
* NARROW on purpose: it fires only when a `connect-src` exists AND does not admit the bridge. No
|
|
17
|
+
* CSP, or one that already covers us, produces nothing. A warning that fires on a working setup
|
|
18
|
+
* costs precisely what a green check on a broken one costs, and this file exists because of the
|
|
19
|
+
* second kind.
|
|
20
|
+
*/
|
|
21
|
+
/**
|
|
22
|
+
* The problem with this app's `connect-src`, or undefined if there is not one.
|
|
23
|
+
*
|
|
24
|
+
* `https:` counts as a wildcard here only in the sense that a policy written that loosely will not
|
|
25
|
+
* be the thing blocking a dev WebSocket; the check stays quiet rather than arguing about it.
|
|
26
|
+
*/
|
|
27
|
+
export declare function cspConnectSrcProblem(text: string, port: number): string | undefined;
|
|
28
|
+
/** The exact addition to paste. Text to copy, not advice to interpret. */
|
|
29
|
+
export declare function devCspAddition(port: number): string;
|
|
30
|
+
/** The step title, named here so `doctor` and the plan cannot drift apart on what this check is called. */
|
|
31
|
+
export declare const CSP_STEP_TITLE = "Content-Security-Policy";
|
|
32
|
+
/**
|
|
33
|
+
* The first blocking policy among the sources `init` already has in hand, or undefined.
|
|
34
|
+
*
|
|
35
|
+
* At most one finding: a second copy of the same policy in a second file is the same problem, and
|
|
36
|
+
* two identical notices read as two problems.
|
|
37
|
+
*/
|
|
38
|
+
export declare function cspPlanProblem(sources: readonly (string | null | undefined)[], port: number): string | undefined;
|
|
@@ -0,0 +1,92 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Does this app's Content-Security-Policy let the browser reach the Reticle bridge?
|
|
3
|
+
*
|
|
4
|
+
* Two independent field reports, both Next: `init` reported every step successful, the SDK mounted,
|
|
5
|
+
* the dial URL was correct, and the app never connected because a strict `connect-src` excluded
|
|
6
|
+
* `ws://localhost:<port>`. The browser blocks the WebSocket and says so in its own console, which
|
|
7
|
+
* nothing on the Reticle side reads — so `status` and `doctor` reported perfect health at an app
|
|
8
|
+
* that could not connect at all, forever. A setup step that exits 0 while leaving the user broken is
|
|
9
|
+
* the worst failure mode this repo has.
|
|
10
|
+
*
|
|
11
|
+
* A TEXT SCAN, deliberately. `headers()` is a function; `init` cannot execute a user's Next config
|
|
12
|
+
* without importing their whole app, and the thing worth finding is the policy string a developer
|
|
13
|
+
* typed — in `next.config.*`, in middleware, or in a `<meta http-equiv>` tag. The scan finds the
|
|
14
|
+
* string wherever it is written.
|
|
15
|
+
*
|
|
16
|
+
* NARROW on purpose: it fires only when a `connect-src` exists AND does not admit the bridge. No
|
|
17
|
+
* CSP, or one that already covers us, produces nothing. A warning that fires on a working setup
|
|
18
|
+
* costs precisely what a green check on a broken one costs, and this file exists because of the
|
|
19
|
+
* second kind.
|
|
20
|
+
*/
|
|
21
|
+
/** Sources that admit any ws origin, so the bridge is already reachable. */
|
|
22
|
+
const WILDCARDS = ['*', 'ws:', 'wss:', 'ws://*', 'https:'];
|
|
23
|
+
/** The two hosts the SDK may dial. `localhost` and the IPv4 loopback are different CSP sources. */
|
|
24
|
+
function bridgeOrigins(port) {
|
|
25
|
+
return [`ws://localhost:${String(port)}`, `ws://127.0.0.1:${String(port)}`];
|
|
26
|
+
}
|
|
27
|
+
/**
|
|
28
|
+
* The `connect-src` directive's source list, or undefined when the text declares none.
|
|
29
|
+
*
|
|
30
|
+
* Matches to the end of the directive (`;`) or the end of the enclosing quoted string, which is how
|
|
31
|
+
* these are written in both a Next `headers()` value and a `<meta content="...">`.
|
|
32
|
+
*/
|
|
33
|
+
function connectSrcSources(text) {
|
|
34
|
+
// Stops at the directive separator or the closing double quote of the enclosing string. Single
|
|
35
|
+
// quotes are NOT terminators: `'self'` is a source, not the end of the list.
|
|
36
|
+
const match = /connect-src([^;"]*)/i.exec(text);
|
|
37
|
+
const list = match?.[1];
|
|
38
|
+
if (list === undefined)
|
|
39
|
+
return undefined;
|
|
40
|
+
return list
|
|
41
|
+
.split(/\s+/)
|
|
42
|
+
.map((source) => source.trim())
|
|
43
|
+
.filter((source) => source.length > 0);
|
|
44
|
+
}
|
|
45
|
+
/**
|
|
46
|
+
* The problem with this app's `connect-src`, or undefined if there is not one.
|
|
47
|
+
*
|
|
48
|
+
* `https:` counts as a wildcard here only in the sense that a policy written that loosely will not
|
|
49
|
+
* be the thing blocking a dev WebSocket; the check stays quiet rather than arguing about it.
|
|
50
|
+
*/
|
|
51
|
+
export function cspConnectSrcProblem(text, port) {
|
|
52
|
+
const sources = connectSrcSources(text);
|
|
53
|
+
if (sources === undefined || 0 === sources.length)
|
|
54
|
+
return undefined;
|
|
55
|
+
if (sources.some((source) => WILDCARDS.includes(source)))
|
|
56
|
+
return undefined;
|
|
57
|
+
const wanted = bridgeOrigins(port);
|
|
58
|
+
// BOTH, not either: the SDK picks its host from how the page was served, and a policy that admits
|
|
59
|
+
// one is a coin flip. A coin flip that fails is indistinguishable from every other silent
|
|
60
|
+
// non-connect, which is the whole cost being avoided here.
|
|
61
|
+
if (wanted.every((origin) => sources.includes(origin)))
|
|
62
|
+
return undefined;
|
|
63
|
+
const missing = wanted.filter((origin) => !sources.includes(origin));
|
|
64
|
+
return (`this app declares a Content-Security-Policy whose \`connect-src\` does not admit the Reticle ` +
|
|
65
|
+
`bridge: ${missing.join(' and ')} ${1 === missing.length ? 'is' : 'are'} missing. The browser ` +
|
|
66
|
+
`will block the WebSocket and report it in ITS console only — every check on the Reticle side ` +
|
|
67
|
+
`will pass while the app never connects. ${devCspAddition(port)}`);
|
|
68
|
+
}
|
|
69
|
+
/** The exact addition to paste. Text to copy, not advice to interpret. */
|
|
70
|
+
export function devCspAddition(port) {
|
|
71
|
+
return (`Add to \`connect-src\` in development only (e.g. behind ` +
|
|
72
|
+
`\`process.env.NODE_ENV === 'development'\`): ` +
|
|
73
|
+
`${bridgeOrigins(port).join(' ')}`);
|
|
74
|
+
}
|
|
75
|
+
/** The step title, named here so `doctor` and the plan cannot drift apart on what this check is called. */
|
|
76
|
+
export const CSP_STEP_TITLE = 'Content-Security-Policy';
|
|
77
|
+
/**
|
|
78
|
+
* The first blocking policy among the sources `init` already has in hand, or undefined.
|
|
79
|
+
*
|
|
80
|
+
* At most one finding: a second copy of the same policy in a second file is the same problem, and
|
|
81
|
+
* two identical notices read as two problems.
|
|
82
|
+
*/
|
|
83
|
+
export function cspPlanProblem(sources, port) {
|
|
84
|
+
for (const source of sources) {
|
|
85
|
+
if ('string' !== typeof source)
|
|
86
|
+
continue;
|
|
87
|
+
const problem = cspConnectSrcProblem(source, port);
|
|
88
|
+
if (problem !== undefined)
|
|
89
|
+
return problem;
|
|
90
|
+
}
|
|
91
|
+
return undefined;
|
|
92
|
+
}
|
|
@@ -0,0 +1,25 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* `doctor`'s web CSP check — the sibling of the desktop one next door.
|
|
3
|
+
*
|
|
4
|
+
* The desktop findings exist because a Tauri app with the default CSP runs perfectly and never
|
|
5
|
+
* connects. The WEB version of that failure had no check at all, and it arrived twice in one batch
|
|
6
|
+
* of field reports: two Next apps where `init` printed success for every step, the SDK mounted, the
|
|
7
|
+
* dial URL was right, and the browser silently refused the WebSocket because `connect-src` excluded
|
|
8
|
+
* the bridge. The violation is reported in the browser's console and nowhere else, so every check on
|
|
9
|
+
* this side stayed green while the app was permanently unreachable.
|
|
10
|
+
*
|
|
11
|
+
* Reads the files a CSP is actually written in, by name — a full source scan would be slower and
|
|
12
|
+
* would find policy strings in test fixtures and documentation.
|
|
13
|
+
*/
|
|
14
|
+
/** Read a project-relative file, or undefined when it is absent/unreadable. */
|
|
15
|
+
type ReadFile = (relative: string) => string | undefined;
|
|
16
|
+
export interface CspDiagnosis {
|
|
17
|
+
/** Which file to look at. */
|
|
18
|
+
file: string;
|
|
19
|
+
/** What is wrong, in one line. */
|
|
20
|
+
problem: string;
|
|
21
|
+
/** Exactly what to add — copy-pasteable, with the port actually in use. */
|
|
22
|
+
fix: string;
|
|
23
|
+
}
|
|
24
|
+
export declare function diagnoseWebCsp(read: ReadFile, port: number): CspDiagnosis[];
|
|
25
|
+
export {};
|
|
@@ -0,0 +1,49 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* `doctor`'s web CSP check — the sibling of the desktop one next door.
|
|
3
|
+
*
|
|
4
|
+
* The desktop findings exist because a Tauri app with the default CSP runs perfectly and never
|
|
5
|
+
* connects. The WEB version of that failure had no check at all, and it arrived twice in one batch
|
|
6
|
+
* of field reports: two Next apps where `init` printed success for every step, the SDK mounted, the
|
|
7
|
+
* dial URL was right, and the browser silently refused the WebSocket because `connect-src` excluded
|
|
8
|
+
* the bridge. The violation is reported in the browser's console and nowhere else, so every check on
|
|
9
|
+
* this side stayed green while the app was permanently unreachable.
|
|
10
|
+
*
|
|
11
|
+
* Reads the files a CSP is actually written in, by name — a full source scan would be slower and
|
|
12
|
+
* would find policy strings in test fixtures and documentation.
|
|
13
|
+
*/
|
|
14
|
+
import { cspConnectSrcProblem, devCspAddition } from './csp-check.js';
|
|
15
|
+
/**
|
|
16
|
+
* The places a Content-Security-Policy gets written in a JS app.
|
|
17
|
+
*
|
|
18
|
+
* Next config and middleware cover `headers()` and the edge-middleware style; the layouts and the
|
|
19
|
+
* plain `index.html` cover the `<meta http-equiv>` style. Both reported cases were in this list.
|
|
20
|
+
*/
|
|
21
|
+
const CSP_FILES = [
|
|
22
|
+
'next.config.js',
|
|
23
|
+
'next.config.mjs',
|
|
24
|
+
'next.config.ts',
|
|
25
|
+
'middleware.ts',
|
|
26
|
+
'middleware.js',
|
|
27
|
+
'src/middleware.ts',
|
|
28
|
+
'app/layout.tsx',
|
|
29
|
+
'app/layout.js',
|
|
30
|
+
'pages/_document.tsx',
|
|
31
|
+
'pages/_document.js',
|
|
32
|
+
'index.html',
|
|
33
|
+
'public/index.html',
|
|
34
|
+
'vercel.json',
|
|
35
|
+
'netlify.toml',
|
|
36
|
+
];
|
|
37
|
+
export function diagnoseWebCsp(read, port) {
|
|
38
|
+
const findings = [];
|
|
39
|
+
for (const file of CSP_FILES) {
|
|
40
|
+
const source = read(file);
|
|
41
|
+
if (source === undefined)
|
|
42
|
+
continue;
|
|
43
|
+
const problem = cspConnectSrcProblem(source, port);
|
|
44
|
+
if (problem === undefined)
|
|
45
|
+
continue;
|
|
46
|
+
findings.push({ file, problem, fix: devCspAddition(port) });
|
|
47
|
+
}
|
|
48
|
+
return findings;
|
|
49
|
+
}
|
package/dist/init/cursor.js
CHANGED
|
@@ -4,8 +4,7 @@
|
|
|
4
4
|
* We add the `reticle` server once at this global path so every project picks it up. Unparseable
|
|
5
5
|
* (jsonc/comment) files bail to manual rather than being rewritten.
|
|
6
6
|
*/
|
|
7
|
-
import { NPX, MCP_SERVER_NAME, npxServerArgs } from './mcp.js';
|
|
8
|
-
import { RETICLE_NPM_PACKAGE } from '../version/server-version.js';
|
|
7
|
+
import { NPX, MCP_SERVER_NAME, npxServerArgs, isReticleRegistration } from './mcp.js';
|
|
9
8
|
/** Path of Cursor's global MCP config, relative to the user's home directory. */
|
|
10
9
|
export const CURSOR_MCP_RELPATH = '.cursor/mcp.json';
|
|
11
10
|
/** The directory whose presence signals Cursor is installed for this user. */
|
|
@@ -46,19 +45,17 @@ function parseConfig(existing) {
|
|
|
46
45
|
*
|
|
47
46
|
* What IS repaired is a stale entry of our own shape — an old pin, a command that has moved — which
|
|
48
47
|
* presence-only idempotency reported as "already registered" and never fixed, so an upgrade could
|
|
49
|
-
* not repair the thing an upgrade exists to repair.
|
|
48
|
+
* not repair the thing an upgrade exists to repair. A WRONG entry counts as ours to repair too:
|
|
49
|
+
* `["@reticlehq/core","mcp"]` was reported from the field, and `core` has no MCP server in it.
|
|
50
50
|
*/
|
|
51
51
|
function leaveEntryAlone(existing) {
|
|
52
52
|
if (JSON.stringify(existing) === JSON.stringify(cursorServerEntry()))
|
|
53
53
|
return true;
|
|
54
54
|
if (typeof existing !== 'object' || null === existing)
|
|
55
55
|
return true;
|
|
56
|
-
const
|
|
57
|
-
const
|
|
58
|
-
|
|
59
|
-
Array.isArray(args) &&
|
|
60
|
-
args.some((arg) => 'string' === typeof arg && arg.includes(RETICLE_NPM_PACKAGE));
|
|
61
|
-
return !ours;
|
|
56
|
+
const record = existing;
|
|
57
|
+
const tokens = [record.command, record.args].flat().filter((v) => 'string' === typeof v);
|
|
58
|
+
return !isReticleRegistration(tokens);
|
|
62
59
|
}
|
|
63
60
|
export function mergeCursorConfig(existing) {
|
|
64
61
|
const parsed = parseConfig(existing);
|
package/dist/init/detect.d.ts
CHANGED
|
@@ -84,5 +84,13 @@ interface InstallCommand {
|
|
|
84
84
|
}
|
|
85
85
|
/** Build a dev-dependency install command for one or more packages (e.g. the kit + its build plugin). */
|
|
86
86
|
export declare function installCommandParts(pm: PackageManager, pkgs: string | readonly string[]): InstallCommand;
|
|
87
|
+
/**
|
|
88
|
+
* The install command as a human reads it — and deliberately NOT `installCommandParts` joined.
|
|
89
|
+
*
|
|
90
|
+
* This string is what the plan prints and what a user retypes when running the step by hand, so it
|
|
91
|
+
* must not carry the flags we add only to quieten our own child process. Built from `INSTALL_ARGS`
|
|
92
|
+
* directly for that reason; routing it through the parts would put `--no-audit --no-fund` in front of
|
|
93
|
+
* every reader and teach them our noise-suppression as if it were part of installing Reticle.
|
|
94
|
+
*/
|
|
87
95
|
export declare function installCommand(pm: PackageManager, pkgs: string | readonly string[]): string;
|
|
88
96
|
export {};
|