@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
package/dist/init/snippets.js
CHANGED
|
@@ -2,7 +2,29 @@
|
|
|
2
2
|
* Generated file contents and copy-paste snippets for `reticle init`. Kept as named constants/builders
|
|
3
3
|
* so the runner never inlines free strings.
|
|
4
4
|
*/
|
|
5
|
-
import { RETICLE_DEFAULT_PORT, bridgeWsUrl } from '@reticlehq/core';
|
|
5
|
+
import { RETICLE_DEFAULT_PORT, ReticleDir, bridgeWsUrl } from '@reticlehq/core';
|
|
6
|
+
import { UiLibrary } from './detect.js';
|
|
7
|
+
import { SERVER_VERSION } from '../version/server-version.js';
|
|
8
|
+
/**
|
|
9
|
+
* The SDK as one import a plain page can actually resolve.
|
|
10
|
+
*
|
|
11
|
+
* `init` used to tell static-HTML users that a page with no build step could not load the SDK, and
|
|
12
|
+
* to go and stand up a bundler. That is true of a BARE specifier and false of a URL, and the
|
|
13
|
+
* difference was the whole road for every server-rendered app we hear from: FastAPI, Flask, Django,
|
|
14
|
+
* Streamlit, Rails. Proven end to end before this shipped, on a page served by `python3 -m
|
|
15
|
+
* http.server`: a session connected, a snapshot returned, and two act_and_wait calls came back
|
|
16
|
+
* `verified: "yes"`.
|
|
17
|
+
*
|
|
18
|
+
* jsDelivr rather than esm.sh, measured: a third of the requests and a third of the bytes for the
|
|
19
|
+
* same result. `/+esm` is what makes it work, and the bare package URL does NOT: every file in
|
|
20
|
+
* `dist` carries bare specifiers for `@reticlehq/core` and `@testing-library/dom`, so an unbundled
|
|
21
|
+
* entry point dies on the first import. That is also why adding `unpkg`/`jsdelivr` fields to
|
|
22
|
+
* package.json would not help.
|
|
23
|
+
*
|
|
24
|
+
* PINNED to this server's version on purpose. A floating import upgrades the page SDK underneath a
|
|
25
|
+
* daemon that did not move, which is `version_skew` arriving by a route nothing checks.
|
|
26
|
+
*/
|
|
27
|
+
const CDN_SDK_URL = `https://cdn.jsdelivr.net/npm/@reticlehq/browser@${SERVER_VERSION}/+esm`;
|
|
6
28
|
/**
|
|
7
29
|
* The connect argument literal: a non-default port adds a `url`, and a projectId is always passed
|
|
8
30
|
* (so the app is identifiable across port changes). Empty string only when neither applies.
|
|
@@ -16,18 +38,76 @@ function connectArg(port, projectId) {
|
|
|
16
38
|
parts.push(`projectId: '${projectId}'`);
|
|
17
39
|
return parts.length > 0 ? `{ ${parts.join(', ')} }` : '';
|
|
18
40
|
}
|
|
41
|
+
/**
|
|
42
|
+
* The same literal, with the pairing token folded in.
|
|
43
|
+
*
|
|
44
|
+
* The token belongs INSIDE the call the user pastes. Every other stack has a build step to inline
|
|
45
|
+
* it (the Vite plugin's `define`, Next's NEXT_PUBLIC_*, Astro's config, CRA's .env); the hand-wired
|
|
46
|
+
* paths have none, so `init` inlines the literal it already read. Without it the bridge closes the
|
|
47
|
+
* socket with AUTH_FAILED and no session ever appears: see Bridge's hello handler.
|
|
48
|
+
*
|
|
49
|
+
* An empty token is omitted rather than emitted as `token: ''`. A daemon that could not write to
|
|
50
|
+
* $HOME runs without auth and trusts loopback, and an empty string would fail the comparison against
|
|
51
|
+
* one that does hold a token.
|
|
52
|
+
*/
|
|
53
|
+
export function connectArgWithToken(port, projectId, pairingToken) {
|
|
54
|
+
const base = connectArg(port, projectId);
|
|
55
|
+
if (pairingToken === undefined || 0 === pairingToken.length)
|
|
56
|
+
return base;
|
|
57
|
+
const inner = base.length > 0 ? base.slice(1, -1).trim() : '';
|
|
58
|
+
return `{ ${[inner, `token: '${pairingToken}'`].filter((p) => p.length > 0).join(', ')} }`;
|
|
59
|
+
}
|
|
60
|
+
/**
|
|
61
|
+
* Which SDK package the GENERATED code should import, and whether `install()` applies.
|
|
62
|
+
*
|
|
63
|
+
* This has to agree with `frameworkPackages`, and it did not. That function was changed so a Vue or
|
|
64
|
+
* Svelte app installs `@reticlehq/browser` instead of the React adapter — correctly — while every
|
|
65
|
+
* generated connect snippet still said `import('@reticlehq/react')`. A SvelteKit app would have
|
|
66
|
+
* installed the sensor and then run a hook importing a package that is not there.
|
|
67
|
+
*
|
|
68
|
+
* `install()` is the React adapter's, not the sensor's: `@reticlehq/browser` exports `reticle` and no
|
|
69
|
+
* `install`, so swapping the specifier alone would trade a missing module for a missing export.
|
|
70
|
+
*/
|
|
71
|
+
export function sdkImport(uiLibrary) {
|
|
72
|
+
const react = uiLibrary !== UiLibrary.VUE && uiLibrary !== UiLibrary.SVELTE;
|
|
73
|
+
return react
|
|
74
|
+
? { specifier: '@reticlehq/react', usesInstall: true }
|
|
75
|
+
: { specifier: '@reticlehq/browser', usesInstall: false };
|
|
76
|
+
}
|
|
77
|
+
/**
|
|
78
|
+
* The framework plugin to show ALONGSIDE reticle() in the example, so the ordering is clear.
|
|
79
|
+
*
|
|
80
|
+
* It used to be `react()` unconditionally, which is what a Vue app was shown — a plugin it does not
|
|
81
|
+
* have, four lines after `init` had correctly detected Vue and said so. The example exists to show
|
|
82
|
+
* that `reticle()` goes last, not to tell anyone which UI framework they are using.
|
|
83
|
+
*/
|
|
84
|
+
function frameworkPluginExample(uiLibrary) {
|
|
85
|
+
switch (uiLibrary) {
|
|
86
|
+
case UiLibrary.VUE:
|
|
87
|
+
return 'vue()';
|
|
88
|
+
case UiLibrary.SVELTE:
|
|
89
|
+
return 'svelte()';
|
|
90
|
+
case UiLibrary.REACT:
|
|
91
|
+
case UiLibrary.PREACT:
|
|
92
|
+
return 'react()';
|
|
93
|
+
default:
|
|
94
|
+
// Nothing detected: name no plugin rather than invent one. The reader keeps whatever they have.
|
|
95
|
+
return '/* your existing plugins */';
|
|
96
|
+
}
|
|
97
|
+
}
|
|
19
98
|
/** The Vite-config snippet printed when we can't safely auto-patch the config. */
|
|
20
|
-
export function viteManual(port) {
|
|
99
|
+
export function viteManual(port, uiLibrary = UiLibrary.UNKNOWN) {
|
|
21
100
|
const call = port === undefined ? 'reticle()' : `reticle({ port: ${String(port)} })`;
|
|
22
101
|
return `Add the Reticle plugin to your Vite config:
|
|
23
102
|
|
|
24
103
|
import { reticle } from '@reticlehq/vite-plugin';
|
|
25
104
|
|
|
26
105
|
export default defineConfig({
|
|
27
|
-
plugins: [
|
|
106
|
+
plugins: [${frameworkPluginExample(uiLibrary)}, ${call}],
|
|
28
107
|
});
|
|
29
108
|
|
|
30
|
-
|
|
109
|
+
Keep \`reticle()\` LAST so it sees the output of your other plugins. It only applies during \`vite\`
|
|
110
|
+
(dev) — it is dropped from \`vite build\`.`;
|
|
31
111
|
}
|
|
32
112
|
/** Next.js config wrap — always printed (we never auto-rewrite next.config). */
|
|
33
113
|
export function nextConfigManual(configFile) {
|
|
@@ -45,21 +125,27 @@ export function nextConfigManual(configFile) {
|
|
|
45
125
|
* as `NEXT_PUBLIC_RETICLE_TOKEN`. This file used to connect with only a projectId, so every Next
|
|
46
126
|
* setup ended at `bridge refused the connection: authentication failed` and no session ever appeared.
|
|
47
127
|
*/
|
|
48
|
-
export function nextReticleDevFile(port, projectId, testids = [], stores = []) {
|
|
128
|
+
export function nextReticleDevFile(port, projectId, testids = [], stores = [], found = []) {
|
|
49
129
|
const base = connectArg(port, projectId);
|
|
50
130
|
const fields = '' === base ? '' : `${base.slice(1, -1).trim()}, `;
|
|
51
131
|
const ids = testids.map((t) => `'${t}'`).join(', ');
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
|
|
132
|
+
// Same rule as the Vite module: a store we found is registered outright, and the commented hint
|
|
133
|
+
// survives only for the libraries we can name but not wire.
|
|
134
|
+
const storeImports = found.map((s) => `import { ${s.ident} } from '${s.importPath}';`).join('\n');
|
|
135
|
+
const storeBlock = found.length > 0
|
|
136
|
+
? found.map((s) => ` registerStore('${s.key}', ${s.ident});`).join('\n')
|
|
137
|
+
: 0 === stores.length
|
|
138
|
+
? ' // No state library detected. If you add one, register it here — see node_modules/@reticlehq/server/docs/usage.md.'
|
|
139
|
+
: stores.map((h) => ` // import your store, then: ${h}`).join('\n');
|
|
55
140
|
return `'use client';
|
|
56
141
|
import { useEffect } from 'react';
|
|
142
|
+
${storeImports.length > 0 ? storeImports : ''}
|
|
57
143
|
|
|
58
144
|
/** Dev-only: connect Reticle + install the React adapter, after hydration. */
|
|
59
145
|
export function ReticleDev() {
|
|
60
146
|
useEffect(() => {
|
|
61
147
|
if (process.env.NODE_ENV !== 'development') return;
|
|
62
|
-
void import('@reticlehq/react').then(({ reticle, install, registerCapabilities }) => {
|
|
148
|
+
void import('@reticlehq/react').then(({ reticle, install, registerCapabilities${found.length > 0 ? ', registerStore' : ''} }) => {
|
|
63
149
|
install();
|
|
64
150
|
// Both provided by withReticle() in next.config. The bridge rejects a connect with no token;
|
|
65
151
|
// the root makes source paths repo-relative instead of absolute.
|
|
@@ -73,9 +159,9 @@ export function ReticleDev() {
|
|
|
73
159
|
// form wires \`subscribe\` too, so every mutation emits a diff; the getter form is read-only.
|
|
74
160
|
${storeBlock}
|
|
75
161
|
registerCapabilities({
|
|
76
|
-
testids: [${ids}],${0 === testids.length ? ' // none found
|
|
162
|
+
testids: [${ids}],${0 === testids.length ? ' // none found; add data-testid to your key elements' : ''}
|
|
77
163
|
signals: [], // names you pass to reticle.signal()
|
|
78
|
-
stores: [], // the keys you registered above
|
|
164
|
+
stores: [${found.map((s) => `'${s.key}'`).join(', ')}], // the keys you registered above
|
|
79
165
|
});
|
|
80
166
|
});
|
|
81
167
|
}, []);
|
|
@@ -171,15 +257,29 @@ Start the daemon BEFORE \`astro dev\`, so the token file exists when the config
|
|
|
171
257
|
* breaks the module everything else hangs off. So the store lines are generated COMMENTED, naming
|
|
172
258
|
* the libraries actually found in package.json, with the exact call to uncomment.
|
|
173
259
|
*/
|
|
174
|
-
export function viteDevModuleFile(testids, stores) {
|
|
260
|
+
export function viteDevModuleFile(testids, stores, found = [], uiLibrary = UiLibrary.REACT) {
|
|
261
|
+
// The specifier has to be the package `frameworkPackages` installed. This file is the Vite path —
|
|
262
|
+
// the commonest install there is — and it was hardcoded to `@reticlehq/react` while a Vue or
|
|
263
|
+
// Svelte app was being given `@reticlehq/browser`, so the generated file imported something that
|
|
264
|
+
// was not there. Caught by running `init` against a pristine Vue app, not by any gate.
|
|
265
|
+
//
|
|
266
|
+
// The sensor exports `registerCapabilities` and `registerStore` (and the store adapters) just as
|
|
267
|
+
// the React kit does; the only thing it lacks is `install()`, which this file never called.
|
|
268
|
+
const sdk = sdkImport(uiLibrary);
|
|
175
269
|
const ids = testids.map((t) => `'${t}'`).join(', ');
|
|
176
|
-
|
|
177
|
-
|
|
178
|
-
|
|
179
|
-
|
|
270
|
+
// A store we FOUND is imported and registered outright — the whole point of the file. The hints
|
|
271
|
+
// stay only for the libraries we can name but not wire (they need an argument we cannot infer).
|
|
272
|
+
const storeImports = found.map((s) => `import { ${s.ident} } from '${s.importPath}';`).join('\n');
|
|
273
|
+
const storeBlock = found.length > 0
|
|
274
|
+
? found.map((s) => ` registerStore('${s.key}', ${s.ident});`).join('\n')
|
|
275
|
+
: 0 === stores.length
|
|
276
|
+
? ' // No state library detected. If you add one, register it here — see node_modules/@reticlehq/server/docs/usage.md.'
|
|
277
|
+
: stores.map((h) => ` // import your store, then: ${h}`).join('\n');
|
|
278
|
+
const registerImport = found.length > 0 ? 'registerCapabilities, registerStore' : 'registerCapabilities';
|
|
279
|
+
return `// Dev-only. Imported automatically by @reticlehq/vite-plugin, so you do not need to import it.
|
|
180
280
|
// Self-guards on import.meta.env.DEV, so it is a no-op in a production build.
|
|
181
|
-
import {
|
|
182
|
-
|
|
281
|
+
import { ${registerImport} } from '${sdk.specifier}';
|
|
282
|
+
${storeImports.length > 0 ? `${storeImports}\n` : ''}
|
|
183
283
|
if (import.meta.env.DEV) {
|
|
184
284
|
// ── Start with ONE flow. ─────────────────────────────────────────────────────────────────────
|
|
185
285
|
// You do not need to describe the whole app to get value, and trying to is the slow path. Register
|
|
@@ -193,9 +293,9 @@ if (import.meta.env.DEV) {
|
|
|
193
293
|
${storeBlock}
|
|
194
294
|
|
|
195
295
|
registerCapabilities({
|
|
196
|
-
testids: [${ids}],${0 === testids.length ? ' // none found
|
|
296
|
+
testids: [${ids}],${0 === testids.length ? ' // none found; add data-testid to your key elements' : ''}
|
|
197
297
|
signals: [], // names you pass to reticle.signal()
|
|
198
|
-
stores: [], // the keys you registered above
|
|
298
|
+
stores: [${found.map((s) => `'${s.key}'`).join(', ')}], // the keys you registered above
|
|
199
299
|
});
|
|
200
300
|
}
|
|
201
301
|
`;
|
|
@@ -216,17 +316,23 @@ export const NEXT_LAYOUT_MANUAL = `Mount <ReticleDev /> in your root layout (app
|
|
|
216
316
|
* where a bare `@reticlehq/react` import resolves. A bare import in a plain index.html does NOT resolve in
|
|
217
317
|
* the browser, so we never tell a bundled app to do that (the old advice silently failed for CRA).
|
|
218
318
|
*/
|
|
319
|
+
/**
|
|
320
|
+
* The whole install, for a page with no build step, as one block a person can paste.
|
|
321
|
+
*
|
|
322
|
+
* Extracted so the `no package.json` exit can print the SAME snippet `htmlManual` offers. That exit
|
|
323
|
+
* is the one every server-rendered app reaches (FastAPI, Flask, Django, Rails, Streamlit), it is
|
|
324
|
+
* where `init` stops, and until now it stopped with an explanation instead of an answer. A message
|
|
325
|
+
* that says "add the snippet below" and then prints no snippet is the same defect wearing the
|
|
326
|
+
* opposite sign, so the two share a builder rather than a copy.
|
|
327
|
+
*/
|
|
328
|
+
export function staticPageSnippet(connectArgLiteral) {
|
|
329
|
+
return ` <script type="module">
|
|
330
|
+
import { reticle } from '${CDN_SDK_URL}';
|
|
331
|
+
reticle.connect(${connectArgLiteral});
|
|
332
|
+
</script>`;
|
|
333
|
+
}
|
|
219
334
|
export function htmlManual(port, projectId, pairingToken) {
|
|
220
|
-
const
|
|
221
|
-
// The token belongs INSIDE the call the user pastes. Every other stack has a build step to inline
|
|
222
|
-
// it (the Vite plugin's `define`, Next's NEXT_PUBLIC_*, Astro's config, CRA's .env); this path has
|
|
223
|
-
// none, so `init` inlines the literal it already read. Without it the bridge closes the socket
|
|
224
|
-
// with AUTH_FAILED and no session ever appears — see Bridge's hello handler.
|
|
225
|
-
const withToken = pairingToken === undefined || 0 === pairingToken.length
|
|
226
|
-
? base
|
|
227
|
-
: `{ ${[base.length > 0 ? base.slice(1, -1).trim() : '', `token: '${pairingToken}'`]
|
|
228
|
-
.filter((p) => p.length > 0)
|
|
229
|
-
.join(', ')} }`;
|
|
335
|
+
const withToken = connectArgWithToken(port, projectId, pairingToken);
|
|
230
336
|
const tokenNote = pairingToken === undefined || 0 === pairingToken.length
|
|
231
337
|
? ''
|
|
232
338
|
: `\n\n The \`token\` is this machine's pairing token, read from ~/.reticle/pairing-token. Keep it: the
|
|
@@ -237,16 +343,26 @@ export function htmlManual(port, projectId, pairingToken) {
|
|
|
237
343
|
• Bundled app (Create React App, webpack, Parcel, Vue/Svelte CLI, etc.) — add to your ENTRY module
|
|
238
344
|
(e.g. src/index.js or src/main.js), where '@reticlehq/react' resolves through your bundler:
|
|
239
345
|
|
|
240
|
-
if (
|
|
346
|
+
if (process.env.NODE_ENV !== 'production') {
|
|
241
347
|
void import('@reticlehq/react').then(({ reticle, install }) => {
|
|
242
348
|
install();
|
|
243
349
|
reticle.connect(${withToken});
|
|
244
350
|
});
|
|
245
351
|
}${tokenNote}
|
|
246
352
|
|
|
247
|
-
• Plain
|
|
248
|
-
|
|
249
|
-
|
|
353
|
+
• Plain HTML with NO build step (FastAPI, Flask, Django, Rails, Streamlit, a hand-written page) —
|
|
354
|
+
paste this into the page, in a template you only serve in development. There is nothing to
|
|
355
|
+
install: no npm, no bundler, no package.json.
|
|
356
|
+
|
|
357
|
+
${staticPageSnippet(withToken)}
|
|
358
|
+
|
|
359
|
+
Serving the app on something other than localhost (a hosts-file alias, a LAN IP, a container, a
|
|
360
|
+
tunnel)? You need TWO things, not one: \`allowNonLocalhost: true\` AND a pairing token, passed as
|
|
361
|
+
\`token\` on the same connect. The flag alone is NOT sufficient — off localhost the SDK refuses
|
|
362
|
+
without a token as well, and that refusal is page-side, so the daemon sees only silence and every
|
|
363
|
+
\`reticle doctor\` check still passes. The token is the one in \`~/.reticle/pairing-token\` (the
|
|
364
|
+
build plugins read the same file). Without both, the SDK says so in the browser console only, so
|
|
365
|
+
from here it looks exactly like nothing happened.`;
|
|
250
366
|
}
|
|
251
367
|
export const NEXT_RETICLE_DEV_PATH = 'app/reticle-dev.tsx';
|
|
252
368
|
export const SVELTEKIT_HOOKS_PATH = 'src/hooks.client.ts';
|
|
@@ -261,7 +377,15 @@ export const SVELTEKIT_HOOKS_PATH = 'src/hooks.client.ts';
|
|
|
261
377
|
* here is the one thing this project exists not to do.
|
|
262
378
|
*/
|
|
263
379
|
export function unverifiedUiLibraryNote(library) {
|
|
264
|
-
|
|
380
|
+
// Preact is not in the same position as Vue or Svelte and must not be told it is. The React
|
|
381
|
+
// adapter reaches Preact through `preact/compat`, which is what `docs/frameworks.mdx` has always
|
|
382
|
+
// said, so telling a Preact reader they get no component identity contradicts our own docs and
|
|
383
|
+
// talks them out of a package that is the right one for them. It is still ungated, which is the
|
|
384
|
+
// honest caveat, and #129 is the issue for closing that.
|
|
385
|
+
const identity = 'preact' === library
|
|
386
|
+
? 'React component identity — component names and stacks — comes from `@reticlehq/react`, which reaches Preact through `preact/compat`. That path is not covered by a CI gate here, so treat it as expected-to-work rather than proven.'
|
|
387
|
+
: 'What `@reticlehq/react` adds and you will NOT get is React component identity: component names and component stacks.';
|
|
388
|
+
return `Detected a ${library} app. Reticle's DOM, network, console and state tools work here. ${'vue' === library ? 'Source `file:line` does NOT come through: the build plugin stamps JSX and, separately, Svelte components, and a Vue single-file component is neither — measured, a Svelte counter reports `src/lib/Counter.svelte:5` and the same drive on Vue reports no source at all.' : 'Source `file:line` does too — the build plugin stamps it for this library (measured on preact and svelte).'} ${identity} ${'vue' === library ? 'The install gate scaffolds a Vue app from scratch on every change, so this SETUP is proven; no gate drives a Vue app to a verdict, so the drive is not.' : `No CI gate covers ${library}.`} Driven on every change: Vite + React, Next.js, Remix, Astro. If something doesn't work, please open an issue.`;
|
|
265
389
|
}
|
|
266
390
|
export const UNVERIFIED_FRAMEWORK_NOTE = 'Reticle has no SvelteKit app and no CI gate for one, so this wiring is untested — it may work, but nothing proves it and nothing will tell us if it breaks. Supported and gated today: Vite + React, Next.js, Remix and Astro. If the hook does not register a session, please open an issue.';
|
|
267
391
|
/**
|
|
@@ -269,14 +393,18 @@ export const UNVERIFIED_FRAMEWORK_NOTE = 'Reticle has no SvelteKit app and no CI
|
|
|
269
393
|
* never triggers Vite's index.html injection (verified), so the standard plugin can't auto-connect —
|
|
270
394
|
* a client hook is the reliable path. SvelteKit runs src/hooks.client.ts on the client at startup.
|
|
271
395
|
*/
|
|
272
|
-
export function svelteKitHooksFile(port, projectId) {
|
|
396
|
+
export function svelteKitHooksFile(port, projectId, uiLibrary = UiLibrary.SVELTE) {
|
|
397
|
+
// SvelteKit is Svelte, so this defaults to the sensor rather than the React adapter — and the
|
|
398
|
+
// import here MUST match what `frameworkPackages` installed, or the hook loads a package that is
|
|
399
|
+
// not in node_modules. See sdkImport.
|
|
400
|
+
const sdk = sdkImport(uiLibrary);
|
|
273
401
|
const base = connectArg(port, projectId);
|
|
274
402
|
const fields = '' === base ? '' : `${base.slice(1, -1).trim()}, `;
|
|
275
403
|
return `// Dev-only: connect Reticle on the client. SvelteKit renders via app.html, so the Vite-plugin
|
|
276
404
|
// index.html injection doesn't fire — connect from this client hook instead.
|
|
277
405
|
if (import.meta.env.DEV) {
|
|
278
|
-
void import('
|
|
279
|
-
install();
|
|
406
|
+
void import('${sdk.specifier}').then(({ reticle${sdk.usesInstall ? ', install' : ''} }) => {
|
|
407
|
+
${sdk.usesInstall ? 'install();' : '// No React adapter here: the sensor has no install() to call.'}
|
|
280
408
|
// The bridge requires the pairing token even on localhost. Nothing in a browser can read the
|
|
281
409
|
// file it lives in, so @reticlehq/vite-plugin inlines it here at build time. Without it the
|
|
282
410
|
// console reads "bridge refused the connection: authentication failed" and no session appears.
|
|
@@ -338,9 +466,23 @@ and no index.html to inject into. Wire it with a dev-only CLIENT plugin, which i
|
|
|
338
466
|
2. Restart the dev server. A dev server that is already running does not pick up a new plugin —
|
|
339
467
|
it will not appear in .nuxt/plugins/client.mjs, and the app will come up with no SDK at all.
|
|
340
468
|
|
|
341
|
-
3. If your dev host is anything other than localhost (a hosts-file alias, a LAN IP, a tunnel),
|
|
342
|
-
|
|
343
|
-
|
|
469
|
+
3. If your dev host is anything other than localhost (a hosts-file alias, a LAN IP, a tunnel), that
|
|
470
|
+
connect call needs TWO additions, not one: allowNonLocalhost: true AND a pairing token passed as
|
|
471
|
+
token. The flag alone is NOT sufficient off localhost. The token is the one in
|
|
472
|
+
~/.reticle/pairing-token. Without both, the SDK loads and then refuses, and the only sign is one
|
|
473
|
+
line in the browser console.
|
|
474
|
+
|
|
475
|
+
4. Add this to nuxt.config, so the dev server does not watch Reticle's own journal:
|
|
476
|
+
|
|
477
|
+
vite: { server: { watch: { ignored: [/(^|[\\\\/])\\.${ReticleDir.ROOT.slice(1)}([\\\\/]|$)/] } } }
|
|
478
|
+
|
|
479
|
+
Reticle journals every session into ${ReticleDir.ROOT}/ in your project root, rewriting one file
|
|
480
|
+
in it continuously while a session is live. Nuxt's dev server watches that root, so without this
|
|
481
|
+
it sees each write as a project file changing and full-reloads the page — which reconnects the
|
|
482
|
+
SDK, which produces the next write. The loop runs several times a second and looks like anything
|
|
483
|
+
except what it is: refs go stale, actions die mid-flight, and the session appears to flap.
|
|
484
|
+
It is a RegExp rather than a glob on purpose — chokidar dropped glob support in v4, so a
|
|
485
|
+
double-star pattern here is accepted and matches nothing.
|
|
344
486
|
|
|
345
487
|
The package is @reticlehq/browser — the framework-neutral sensor. DOM, network, console, routing and
|
|
346
488
|
source file:line all work in Vue. What you do not get is React component identity, which is the only
|
|
@@ -351,11 +493,21 @@ if something does not work, please open an issue.`;
|
|
|
351
493
|
* Root-level project config for Reticle. Written by `reticle init`; read by `reticle mcp` for the port
|
|
352
494
|
* and by tooling for the stable projectId (the app's identity across port changes).
|
|
353
495
|
*/
|
|
354
|
-
export function reticleConfigContent(framework, port, projectId) {
|
|
496
|
+
export function reticleConfigContent(framework, port, projectId, installSource) {
|
|
355
497
|
const fields = { framework };
|
|
356
498
|
if (projectId !== undefined && projectId.length > 0)
|
|
357
499
|
fields['projectId'] = projectId;
|
|
358
500
|
if (port !== undefined && port !== RETICLE_DEFAULT_PORT)
|
|
359
501
|
fields['port'] = port;
|
|
502
|
+
// How this install arrived, recorded HERE because it is a property of the install and the only
|
|
503
|
+
// moment anything knows it is the moment it happens. It reaches us as an environment variable set
|
|
504
|
+
// by whichever channel ran the install, and an environment variable is gone by the next command,
|
|
505
|
+
// so every event after this one reported `unknown` and the question "which channel actually
|
|
506
|
+
// converts" could not be asked at all. Written once, read for the life of the project.
|
|
507
|
+
//
|
|
508
|
+
// A closed vocabulary, narrowed before it gets here, so this can never carry a path or a URL.
|
|
509
|
+
if (installSource !== undefined && installSource.length > 0) {
|
|
510
|
+
fields['installSource'] = installSource;
|
|
511
|
+
}
|
|
360
512
|
return `${JSON.stringify(fields, null, 2)}\n`;
|
|
361
513
|
}
|
package/dist/init/vite-config.js
CHANGED
|
@@ -7,14 +7,46 @@
|
|
|
7
7
|
import { PatchKind } from './patch-kind.js';
|
|
8
8
|
export const VITE_IMPORT = "import { reticle } from '@reticlehq/vite-plugin';";
|
|
9
9
|
const RETICLE_MARKER = '@reticlehq/vite-plugin';
|
|
10
|
-
/**
|
|
10
|
+
/**
|
|
11
|
+
* The `reticle(...)` call — the bridge port so the injected connect targets it, and body capture.
|
|
12
|
+
*
|
|
13
|
+
* `captureNetworkBodies` is here rather than in the SDK's defaults, deliberately. Without it a write
|
|
14
|
+
* that answers 2xx with a body nobody recorded grades `unknown / outcome_unread`, because a 200
|
|
15
|
+
* describes the transport and not the result — so the single bug class this product exists to catch is
|
|
16
|
+
* unreachable on a default install. Measured on a real payments UI: a refund posted rupees into a
|
|
17
|
+
* paise field, the server answered 200 having refunded a hundredth of it, the page rendered the amount
|
|
18
|
+
* the user had typed rather than the amount that came back, and every DOM-level check passed. An agent
|
|
19
|
+
* asked to verify that flow had to edit the app's own vite config mid-task to see the payload, and
|
|
20
|
+
* then tell its human to undo the edit.
|
|
21
|
+
*
|
|
22
|
+
* Written into the USER'S config, not switched on inside the SDK, and the difference is the point. A
|
|
23
|
+
* body is the one part of a request that routinely carries personal data: the credential classes are
|
|
24
|
+
* redacted before anything is journalled — tokens, cookies, card numbers, cvv, ssn — but an address or
|
|
25
|
+
* an email is not, and nothing here should decide that for someone silently. In the config it is one
|
|
26
|
+
* visible line they can read, keep, or delete, and an SDK that updates underneath them never starts
|
|
27
|
+
* recording more than it did yesterday.
|
|
28
|
+
*/
|
|
11
29
|
function reticlePluginCall(port) {
|
|
12
|
-
|
|
30
|
+
const options = [
|
|
31
|
+
...(port === undefined ? [] : [`port: ${String(port)}`]),
|
|
32
|
+
'captureNetworkBodies: true',
|
|
33
|
+
];
|
|
34
|
+
return `reticle({ ${options.join(', ')} })`;
|
|
13
35
|
}
|
|
14
36
|
/** Matches the start of a `plugins: [` array literal. */
|
|
15
37
|
const PLUGINS_ARRAY = /plugins\s*:\s*\[/;
|
|
16
38
|
/** Matches an ES import statement (used to place our import after the last one). */
|
|
17
39
|
const IMPORT_LINE = /^import\s.+from\s+['"][^'"]+['"];?\s*$/gm;
|
|
40
|
+
/**
|
|
41
|
+
* The opening `{` of the exported config object — `defineConfig({`, or a bare `export default {`.
|
|
42
|
+
* Used only when there is no `plugins` array to extend: the object is right there, so adding the
|
|
43
|
+
* key is the same edit as extending the array, and bailing sent a user whose config merely set
|
|
44
|
+
* `server.port` to a manual paste for a change we can make correctly.
|
|
45
|
+
*
|
|
46
|
+
* A config built by a call (`defineConfig(buildOptions())`) has no literal to extend and still
|
|
47
|
+
* bails — the rule stays "only edit a shape we can see whole".
|
|
48
|
+
*/
|
|
49
|
+
const CONFIG_OBJECT = /(export\s+default\s+(?:defineConfig\s*\(\s*)?)\{/;
|
|
18
50
|
/** Alias kept so existing call sites read in Vite terms; the vocabulary is shared (see patch-kind). */
|
|
19
51
|
export const VitePatchKind = PatchKind;
|
|
20
52
|
const NO_PLUGINS_REASON = "couldn't find a `plugins: [...]` array to extend";
|
|
@@ -41,12 +73,28 @@ function insertPlugin(source, port) {
|
|
|
41
73
|
return `${match}${reticlePluginCall(port)},${separator}`;
|
|
42
74
|
});
|
|
43
75
|
}
|
|
76
|
+
/**
|
|
77
|
+
* Add a whole `plugins: [reticle()]` key to a config object that has none, matching the layout of
|
|
78
|
+
* the object it lands in: a multi-line object gets its own indented line, a one-liner stays inline.
|
|
79
|
+
*/
|
|
80
|
+
function insertPluginsKey(source, port) {
|
|
81
|
+
return source.replace(CONFIG_OBJECT, (_match, prefix, offset) => {
|
|
82
|
+
const rest = source.slice(offset + _match.length);
|
|
83
|
+
const multiline = /^\s*\n/.test(rest);
|
|
84
|
+
const indent = /^\s*\n(\s*)\S/.exec(rest)?.[1] ?? ' ';
|
|
85
|
+
const key = `plugins: [${reticlePluginCall(port)}],`;
|
|
86
|
+
return multiline ? `${prefix}{\n${indent}${key}` : `${prefix}{ ${key}`;
|
|
87
|
+
});
|
|
88
|
+
}
|
|
44
89
|
export function patchViteConfig(source, port) {
|
|
45
90
|
if (source.includes(RETICLE_MARKER)) {
|
|
46
91
|
return { kind: VitePatchKind.ALREADY };
|
|
47
92
|
}
|
|
48
|
-
if (
|
|
49
|
-
return { kind: VitePatchKind.
|
|
93
|
+
if (PLUGINS_ARRAY.test(source)) {
|
|
94
|
+
return { kind: VitePatchKind.APPLY, code: insertImport(insertPlugin(source, port)) };
|
|
95
|
+
}
|
|
96
|
+
if (CONFIG_OBJECT.test(source)) {
|
|
97
|
+
return { kind: VitePatchKind.APPLY, code: insertImport(insertPluginsKey(source, port)) };
|
|
50
98
|
}
|
|
51
|
-
return { kind: VitePatchKind.
|
|
99
|
+
return { kind: VitePatchKind.MANUAL, reason: NO_PLUGINS_REASON };
|
|
52
100
|
}
|
|
@@ -1,2 +1,21 @@
|
|
|
1
|
+
import type { MockRule } from './network-mock.js';
|
|
1
2
|
import type { ToolDef } from '../tools/tools.js';
|
|
3
|
+
/**
|
|
4
|
+
* Narrow the validated tool args into MockRule[], omitting undefined keys (exactOptionalPropertyTypes).
|
|
5
|
+
*
|
|
6
|
+
* THROWS on a malformed rule rather than falling back to `[]`, because `[]` is not a neutral value
|
|
7
|
+
* here — it is the documented way to clear every active mock. A rule with a wrong field name used to
|
|
8
|
+
* produce exactly the call that turns mocking OFF, and the handler then reported `applied: true`
|
|
9
|
+
* with `count: 0`. For a tool whose entire job is forcing an error state, that means the agent
|
|
10
|
+
* checks the app's failure handling against the real backend, sees the happy path, and reports that
|
|
11
|
+
* the error state works.
|
|
12
|
+
*
|
|
13
|
+
* Same rule as the action arguments: an argument that could not be understood is refused by name,
|
|
14
|
+
* never reinterpreted into a destructive default. A valueless `fill` used to wipe the field and
|
|
15
|
+
* report ok; it throws now, for this reason.
|
|
16
|
+
*
|
|
17
|
+
* `undefined` and `[]` still mean "clear" — those are deliberate, documented requests, not failures
|
|
18
|
+
* to parse.
|
|
19
|
+
*/
|
|
20
|
+
export declare function toRules(value: unknown): MockRule[];
|
|
2
21
|
export declare const NETWORK_MOCK_TOOLS: ToolDef[];
|
|
@@ -26,11 +26,36 @@ const ruleShape = z.object({
|
|
|
26
26
|
.optional()
|
|
27
27
|
.describe('Simulate a network failure (offline) instead of a response.'),
|
|
28
28
|
});
|
|
29
|
-
/**
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
|
|
29
|
+
/**
|
|
30
|
+
* Narrow the validated tool args into MockRule[], omitting undefined keys (exactOptionalPropertyTypes).
|
|
31
|
+
*
|
|
32
|
+
* THROWS on a malformed rule rather than falling back to `[]`, because `[]` is not a neutral value
|
|
33
|
+
* here — it is the documented way to clear every active mock. A rule with a wrong field name used to
|
|
34
|
+
* produce exactly the call that turns mocking OFF, and the handler then reported `applied: true`
|
|
35
|
+
* with `count: 0`. For a tool whose entire job is forcing an error state, that means the agent
|
|
36
|
+
* checks the app's failure handling against the real backend, sees the happy path, and reports that
|
|
37
|
+
* the error state works.
|
|
38
|
+
*
|
|
39
|
+
* Same rule as the action arguments: an argument that could not be understood is refused by name,
|
|
40
|
+
* never reinterpreted into a destructive default. A valueless `fill` used to wipe the field and
|
|
41
|
+
* report ok; it throws now, for this reason.
|
|
42
|
+
*
|
|
43
|
+
* `undefined` and `[]` still mean "clear" — those are deliberate, documented requests, not failures
|
|
44
|
+
* to parse.
|
|
45
|
+
*/
|
|
46
|
+
export function toRules(value) {
|
|
47
|
+
if (value === undefined)
|
|
33
48
|
return [];
|
|
49
|
+
const parsed = z.array(ruleShape).safeParse(value);
|
|
50
|
+
if (!parsed.success) {
|
|
51
|
+
const problem = parsed.error.issues
|
|
52
|
+
.map((i) => `${0 === i.path.length ? 'mocks' : `mocks.${i.path.join('.')}`}: ${i.message}`)
|
|
53
|
+
.join('; ');
|
|
54
|
+
throw new Error(`reticle_network_mock could not read \`mocks\` (${problem}). Nothing was applied and mocking ` +
|
|
55
|
+
'was left unchanged — an unreadable rule is NOT treated as a request to clear. A rule is ' +
|
|
56
|
+
'{ urlContains, method?, status?, body?, contentType?, delayMs?, abort? }; pass `clear: true` ' +
|
|
57
|
+
'or an empty array if you meant to turn mocking off.');
|
|
58
|
+
}
|
|
34
59
|
return parsed.data.map((r) => {
|
|
35
60
|
const rule = { urlContains: r.urlContains };
|
|
36
61
|
if (r.method !== undefined)
|
|
@@ -56,7 +81,14 @@ export const NETWORK_MOCK_TOOLS = [
|
|
|
56
81
|
'states without touching the backend ("verify the app handles a failed payment"). Pass `mocks` ' +
|
|
57
82
|
'(first matching rule wins); pass an empty array or `clear: true` to turn mocking off.',
|
|
58
83
|
inputSchema: {
|
|
59
|
-
|
|
84
|
+
// The rule fields each carry their own `.describe()` on `ruleShape`, but the params
|
|
85
|
+
// view reads only the top-level description of each entry, so those are flattened
|
|
86
|
+
// away before an agent sees them. Naming the shape here is what keeps the tool
|
|
87
|
+
// callable from its own description instead of by guessing and failing first.
|
|
88
|
+
mocks: z
|
|
89
|
+
.array(ruleShape)
|
|
90
|
+
.optional()
|
|
91
|
+
.describe('Interception rules, first match wins: { urlContains, method?, status?, body?, contentType?, delayMs?, abort? }. Omit or pass [] to clear.'),
|
|
60
92
|
clear: z.boolean().optional().describe('Clear all active mocks (same as mocks: []).'),
|
|
61
93
|
...sessionIdShape,
|
|
62
94
|
},
|
|
@@ -28,6 +28,12 @@ export interface ScrollFindResult {
|
|
|
28
28
|
* help). false ⇒ stopped at the maxScrolls budget (more rows may exist further down).
|
|
29
29
|
*/
|
|
30
30
|
exhausted: boolean;
|
|
31
|
+
/**
|
|
32
|
+
* Present only when informative: the document itself did not scroll (nothing scrollable), so the
|
|
33
|
+
* target is likely inside a list with its own scroll container. A genuine exhaustion — reaching
|
|
34
|
+
* the real end of a scrolled container — stays quiet.
|
|
35
|
+
*/
|
|
36
|
+
note?: string;
|
|
31
37
|
}
|
|
32
38
|
/**
|
|
33
39
|
* Reveal an element that a windowed/virtualized list has not mounted yet. Queries
|
|
@@ -12,6 +12,21 @@ async function queryFirst(session, q) {
|
|
|
12
12
|
return asRecord(elements[0]);
|
|
13
13
|
return undefined;
|
|
14
14
|
}
|
|
15
|
+
/**
|
|
16
|
+
* A note attached to `{ found: false, exhausted: true }` only when the failure is
|
|
17
|
+
* actually informative: the document itself had nothing to scroll (scrolled:false with
|
|
18
|
+
* no explicit container), so the target is likely inside a list with its own scroll
|
|
19
|
+
* container. Reaching the genuine end of a scrolled container stays quiet.
|
|
20
|
+
*/
|
|
21
|
+
function exhaustNote(q, data) {
|
|
22
|
+
if (q.container !== undefined)
|
|
23
|
+
return {};
|
|
24
|
+
if (false !== data['scrolled'])
|
|
25
|
+
return {};
|
|
26
|
+
return {
|
|
27
|
+
note: "The document did not scroll — the target may be inside a list with its own scroll container. Pass that container's ref as `container`.",
|
|
28
|
+
};
|
|
29
|
+
}
|
|
15
30
|
/**
|
|
16
31
|
* Reveal an element that a windowed/virtualized list has not mounted yet. Queries
|
|
17
32
|
* once (it may already be visible), then scrolls the container ~one viewport at a time, re-querying
|
|
@@ -39,7 +54,7 @@ export async function scrollToFind(session, q, opts = {}) {
|
|
|
39
54
|
// Fall through to linear refinement from current position (already near the target).
|
|
40
55
|
const data = asRecord(sr.result);
|
|
41
56
|
if (true === data['atEnd'] || false === data['scrolled']) {
|
|
42
|
-
return { found: false, scrolls, exhausted: true };
|
|
57
|
+
return { found: false, scrolls, exhausted: true, ...exhaustNote(q, data) };
|
|
43
58
|
}
|
|
44
59
|
}
|
|
45
60
|
for (let i = 0; i < max; i += 1) {
|
|
@@ -51,7 +66,7 @@ export async function scrollToFind(session, q, opts = {}) {
|
|
|
51
66
|
return { found: true, element: hit, scrolls, exhausted: false };
|
|
52
67
|
// Reached the bottom or the container would not move — no more rows to reveal.
|
|
53
68
|
if (true === data['atEnd'] || false === data['scrolled']) {
|
|
54
|
-
return { found: false, scrolls, exhausted: true };
|
|
69
|
+
return { found: false, scrolls, exhausted: true, ...exhaustNote(q, data) };
|
|
55
70
|
}
|
|
56
71
|
}
|
|
57
72
|
return { found: false, scrolls, exhausted: false }; // spent the budget; more may lie further down
|
|
@@ -52,6 +52,10 @@ export const SCROLL_TOOLS = [
|
|
|
52
52
|
element: z.object({ ref: z.string(), role: z.string(), name: z.string() }).optional(),
|
|
53
53
|
scrolls: z.number(),
|
|
54
54
|
exhausted: z.boolean(),
|
|
55
|
+
note: z
|
|
56
|
+
.string()
|
|
57
|
+
.optional()
|
|
58
|
+
.describe('Present only when informative: the document itself did not scroll, so the target may be inside a list with its own scroll container — pass its ref as `container`.'),
|
|
55
59
|
},
|
|
56
60
|
handler: (deps, args) => {
|
|
57
61
|
const session = deps.sessions.resolve(asString(args['sessionId']));
|
|
@@ -15,8 +15,16 @@ interface JournalAttachDeps {
|
|
|
15
15
|
}
|
|
16
16
|
/**
|
|
17
17
|
* Build the per-session journal attachment the bridge fires on session creation. Off when disabled;
|
|
18
|
-
*
|
|
18
|
+
* skips a session whose id is not a safe path segment (never crashes the live session over a
|
|
19
19
|
* journaling concern). The recorder's clock is the session's own elapsed time.
|
|
20
|
+
*
|
|
21
|
+
* That skip used to be SILENT, which is the part that mattered. The id becomes a directory name, so
|
|
22
|
+
* one that is not a safe path segment cannot be journalled — but the session itself connects and
|
|
23
|
+
* drives perfectly well, so the only observable difference is that the durable causal record for it
|
|
24
|
+
* does not exist. Every query that reads back through the journal then returns nothing, and nothing
|
|
25
|
+
* anywhere says why. An app self-assigning an unusual id gets a quietly degraded session; a hostile
|
|
26
|
+
* one gets the same, which is why this is worth a line either way. Nothing escapes the directory —
|
|
27
|
+
* the guard holds, and that is exactly why the failure is invisible rather than loud.
|
|
20
28
|
*/
|
|
21
29
|
export declare function makeJournalAttach(deps: JournalAttachDeps): (session: JournalTarget) => void;
|
|
22
30
|
export {};
|