@reticlehq/server 2.6.0 → 2.7.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/SKILL.md +201 -18
- package/dist/bridge/bridge.d.ts +6 -1
- package/dist/bridge/bridge.js +27 -4
- package/dist/cli/chromium-hint.d.ts +40 -0
- package/dist/cli/chromium-hint.js +75 -0
- package/dist/cli/cli-doctor.js +3 -11
- package/dist/cli/cli-launch.d.ts +1 -0
- package/dist/cli/cli-launch.js +4 -1
- package/dist/cli/cli-port.js +20 -1
- package/dist/cli/doctor-daemon-line.js +10 -1
- package/dist/cli.js +40 -9
- package/dist/daemon/daemon-resilience.js +2 -2
- package/dist/daemon/loopback-alias.d.ts +32 -0
- package/dist/daemon/loopback-alias.js +80 -0
- package/dist/events/predicate-eval.d.ts +7 -3
- package/dist/events/predicate-eval.js +159 -92
- package/dist/events/predicate-parse.d.ts +1 -1
- package/dist/events/predicate-parse.js +18 -4
- package/dist/events/ring-buffer.d.ts +8 -0
- package/dist/events/ring-buffer.js +34 -1
- package/dist/honesty/honesty.d.ts +11 -0
- package/dist/honesty/honesty.js +9 -1
- package/dist/honesty/verified.js +26 -3
- package/dist/http-server.js +11 -1
- package/dist/index.js +26 -2
- package/dist/init/agent-rules.js +6 -1
- package/dist/init/detect.d.ts +8 -0
- package/dist/init/detect.js +15 -0
- package/dist/init/non-js-project.d.ts +27 -0
- package/dist/init/non-js-project.js +60 -0
- package/dist/init/plan-framework.d.ts +9 -0
- package/dist/init/plan-framework.js +19 -1
- package/dist/init/plan.js +20 -3
- package/dist/init/run.js +38 -9
- package/dist/init/snippets.d.ts +20 -1
- package/dist/init/snippets.js +71 -6
- package/dist/mcp/mcp-proxy.js +10 -3
- package/dist/mcp/mcp.js +35 -0
- package/dist/mcp/proxy-lifecycle.d.ts +19 -0
- package/dist/mcp/proxy-lifecycle.js +21 -0
- package/dist/pool/playwright-launcher.js +10 -2
- package/dist/session/live-control-tools.js +39 -2
- package/dist/session/live-control.d.ts +7 -9
- package/dist/session/live-control.js +34 -1
- package/dist/session/no-session-diagnosis.d.ts +4 -4
- package/dist/session/no-session-diagnosis.js +60 -29
- package/dist/session/review-store.d.ts +19 -0
- package/dist/session/review-store.js +34 -5
- package/dist/session/session-manager.d.ts +9 -0
- package/dist/session/session-manager.js +11 -0
- package/dist/session/session.d.ts +8 -0
- package/dist/session/session.js +12 -0
- package/dist/status-payload.d.ts +13 -1
- package/dist/status-payload.js +6 -1
- package/dist/telemetry/app-instrumented.d.ts +37 -0
- package/dist/telemetry/app-instrumented.js +54 -0
- package/dist/telemetry/daemon-telemetry.d.ts +4 -4
- package/dist/telemetry/daemon-telemetry.js +8 -4
- package/dist/telemetry/session-metrics.d.ts +3 -3
- package/dist/telemetry/session-metrics.js +4 -4
- package/dist/telemetry/telemetry.d.ts +3 -1
- package/dist/telemetry/telemetry.js +3 -1
- package/dist/telemetry/verification-of.js +26 -1
- package/dist/tools/act-tools.js +32 -10
- package/dist/tools/assert-verdict.js +7 -0
- package/dist/tools/error-recovery.js +6 -1
- package/dist/tools/invoke-tool.js +11 -10
- package/dist/tools/tool-kit.d.ts +52 -5
- package/dist/tools/tool-kit.js +55 -24
- package/dist/tools/tool-surface.js +9 -9
- package/dist/tools/tools.js +22 -0
- package/dist/visual/capture-cleanup.d.ts +16 -0
- package/dist/visual/capture-cleanup.js +39 -0
- package/dist/visual/visual-tools.d.ts +18 -0
- package/dist/visual/visual-tools.js +24 -5
- package/docs/agent-cheatsheet.md +14 -2
- package/docs/gate-plan.md +1 -1
- package/docs/gates.md +1 -1
- package/docs/telemetry-contract.md +13 -1
- package/docs/telemetry.md +9 -1
- package/docs/token-efficiency.md +2 -2
- package/package.json +2 -2
|
@@ -5,6 +5,7 @@
|
|
|
5
5
|
* block. Harnesses gate on it (e.g. "grade ≥ net AND integrity clean"). This is pure composition +
|
|
6
6
|
* assessment — all five components already exist individually; wiring it onto results is a later step.
|
|
7
7
|
*/
|
|
8
|
+
import type { CaptureLoss } from '@reticlehq/core';
|
|
8
9
|
/** Assertion grade, strongest first — the tier the verdict actually proved. */
|
|
9
10
|
export declare const HonestyGrade: {
|
|
10
11
|
readonly SIGNAL: "signal";
|
|
@@ -23,6 +24,15 @@ interface HonestyInputs {
|
|
|
23
24
|
coveragePartial?: boolean;
|
|
24
25
|
truncated?: boolean;
|
|
25
26
|
blindSpots?: readonly string[];
|
|
27
|
+
/**
|
|
28
|
+
* The machine-readable half of `issues` — WHICH kind of loss, from core's closed `CaptureLoss`.
|
|
29
|
+
*
|
|
30
|
+
* `issues` is prose built for an agent to read, and prose is not something a dashboard can group
|
|
31
|
+
* by: `unclean_capture` became a large share of all field verdicts with no way to ask which of its three
|
|
32
|
+
* causes produced them. Passed by the caller rather than derived here, because only the caller
|
|
33
|
+
* knows whether a blind-spot note came from the browser transport or from a boundary in the page.
|
|
34
|
+
*/
|
|
35
|
+
losses?: readonly CaptureLoss[];
|
|
26
36
|
}
|
|
27
37
|
export interface HonestyBlock {
|
|
28
38
|
grade: HonestyGrade;
|
|
@@ -40,6 +50,7 @@ export interface HonestyBlock {
|
|
|
40
50
|
integrity: {
|
|
41
51
|
clean: boolean;
|
|
42
52
|
issues: string[];
|
|
53
|
+
losses?: CaptureLoss[];
|
|
43
54
|
};
|
|
44
55
|
}
|
|
45
56
|
export declare function buildHonestyBlock(inputs: HonestyInputs): HonestyBlock;
|
package/dist/honesty/honesty.js
CHANGED
|
@@ -46,7 +46,15 @@ export function buildHonestyBlock(inputs) {
|
|
|
46
46
|
? {}
|
|
47
47
|
: { envelope: { samples, sufficient: samples >= MIN_ENVELOPE_SAMPLES } }),
|
|
48
48
|
coverage: { ...(pct === undefined ? {} : { pct }), partial },
|
|
49
|
-
integrity: {
|
|
49
|
+
integrity: {
|
|
50
|
+
clean: 0 === issues.length,
|
|
51
|
+
issues,
|
|
52
|
+
// Only when something was actually lost. An empty array beside `clean: true` would be a field
|
|
53
|
+
// that is always present and always says nothing, which is how a signal gets tuned out.
|
|
54
|
+
...(0 === issues.length || undefined === inputs.losses || 0 === inputs.losses.length
|
|
55
|
+
? {}
|
|
56
|
+
: { losses: [...new Set(inputs.losses)] }),
|
|
57
|
+
},
|
|
50
58
|
};
|
|
51
59
|
}
|
|
52
60
|
/** Whether a verdict's honesty block clears the harness's bar. Reasons list every failed criterion. */
|
package/dist/honesty/verified.js
CHANGED
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import { Verified, VerifiedReason } from '@reticlehq/core';
|
|
1
|
+
import { Verified, VerifiedReason, isAbsenceDerived } from '@reticlehq/core';
|
|
2
2
|
import { HonestyGrade } from './honesty.js';
|
|
3
3
|
export function decideVerified(inputs) {
|
|
4
4
|
const { pass, honesty, contradictions = [], settled, outcomePending, outcomeUnread } = inputs;
|
|
@@ -41,14 +41,37 @@ export function decideVerified(inputs) {
|
|
|
41
41
|
// this product exists to catch is a green assertion sitting on top of a failed write. Measured on
|
|
42
42
|
// the bench app — `ui-advanced-request-failed` arrived with verdict.pass true and every other
|
|
43
43
|
// channel agreeing. Letting `pass` win there would report exactly the false green being detected.
|
|
44
|
-
|
|
45
|
-
|
|
44
|
+
// ...but ONLY for contradictions we positively OBSERVED. A kind inferred from the absence of
|
|
45
|
+
// evidence — the request had not settled, the response had not been applied yet — is a statement
|
|
46
|
+
// about when we stopped looking, not about whether the action worked, and the window closes the
|
|
47
|
+
// moment the predicate first passes. Reproduced on the bench app: `auth:granted` fired with
|
|
48
|
+
// matching data, state changed, the token was stored, the capture was clean and the grade was
|
|
49
|
+
// `signal`, and the verdict was still NO because one POST was in flight. That inverts the grade
|
|
50
|
+
// hierarchy — a timing observation beat a consequence observation — and it is the shape of half of
|
|
51
|
+
// every `no` verdict in the field. See ABSENCE_DERIVED_CONTRADICTIONS for why a false negative
|
|
52
|
+
// costs more than it looks: it makes an agent redo work that succeeded, or stop believing the
|
|
53
|
+
// verdict channel, which is the product.
|
|
54
|
+
const observed = contradictions.filter((c) => !isAbsenceDerived(c.kind));
|
|
55
|
+
if (observed.length > 0) {
|
|
56
|
+
const kinds = observed.map((c) => c.kind).join(', ');
|
|
46
57
|
return {
|
|
47
58
|
verified: Verified.NO,
|
|
48
59
|
verifiedReason: VerifiedReason.CONTRADICTED,
|
|
49
60
|
because: `channels disagree about this action (${kinds}) even though the assertion passed`,
|
|
50
61
|
};
|
|
51
62
|
}
|
|
63
|
+
// Absence-derived only: report it, do not assert failure. UNKNOWN is the honest answer — Reticle
|
|
64
|
+
// drove the app and could not yet tell. The finding still rides out in `contradictions`, so an
|
|
65
|
+
// agent that wants to wait and re-check has everything it needs.
|
|
66
|
+
if (contradictions.length > 0) {
|
|
67
|
+
const kinds = contradictions.map((c) => c.kind).join(', ');
|
|
68
|
+
return {
|
|
69
|
+
verified: Verified.UNKNOWN,
|
|
70
|
+
verifiedReason: VerifiedReason.UNSETTLED,
|
|
71
|
+
because: `the assertion held, but this window closed before the app finished (${kinds}) — ` +
|
|
72
|
+
`re-check, or assert a consequence that settles`,
|
|
73
|
+
};
|
|
74
|
+
}
|
|
52
75
|
// Below the contradiction clause, and for the same reason a contradiction outranks a dirty
|
|
53
76
|
// capture: evidence AGAINST beats absence of evidence, whichever absence it is. Both can hold at
|
|
54
77
|
// once — assert `{ text: 'Saved' }` that was already on screen while the write 500s — and ordering
|
package/dist/http-server.js
CHANGED
|
@@ -150,8 +150,18 @@ export function createSharedServer(options = {}) {
|
|
|
150
150
|
transports.clear();
|
|
151
151
|
await new Promise((resolve, reject) => {
|
|
152
152
|
httpServer.close((err) => {
|
|
153
|
-
|
|
153
|
+
// A server that never listened is already closed, and saying so is not an error worth
|
|
154
|
+
// propagating. It matters because this close() runs on the CLEANUP path of a failed
|
|
155
|
+
// `--drive`: when the app URL is unreachable, the daemon tears down before `listen()` has
|
|
156
|
+
// been called, Node rejects with ERR_SERVER_NOT_RUNNING, and that rejection REPLACES the
|
|
157
|
+
// real cause. The user's app is down and the message they read is "Server is not running",
|
|
158
|
+
// which blames Reticle for their dev server — traced live, where swallowing this turned the
|
|
159
|
+
// same failure back into "net::ERR_CONNECTION_REFUSED at http://localhost:4312/".
|
|
160
|
+
if (err !== undefined &&
|
|
161
|
+
err !== null &&
|
|
162
|
+
'ERR_SERVER_NOT_RUNNING' !== err.code) {
|
|
154
163
|
reject(err);
|
|
164
|
+
}
|
|
155
165
|
else
|
|
156
166
|
resolve();
|
|
157
167
|
});
|
package/dist/index.js
CHANGED
|
@@ -5,6 +5,8 @@ import { originOf } from './session/session-manager.js';
|
|
|
5
5
|
import { setBrowserMode, BrowserMode } from './telemetry/browser-mode.js';
|
|
6
6
|
import { replayNamedFlow } from './flows/flow-tools.js';
|
|
7
7
|
import { createSharedServer } from './http-server.js';
|
|
8
|
+
import { openLoopbackAlias } from './daemon/loopback-alias.js';
|
|
9
|
+
import { reportAppInstrumented } from './telemetry/app-instrumented.js';
|
|
8
10
|
import { resolveBridgeSecurityWithAutoToken } from './bridge/bridge-security.js';
|
|
9
11
|
import { Bridge } from './bridge/bridge.js';
|
|
10
12
|
import { BaselineStore } from './project/baselines.js';
|
|
@@ -14,6 +16,7 @@ import { buildFlowChips } from './flows/flow-scope.js';
|
|
|
14
16
|
import { ProjectStore } from './project/project-store.js';
|
|
15
17
|
import { AnnotationStore } from './flows/annotation-store.js';
|
|
16
18
|
import { createNodeFileSystem } from './project/fs-port.js';
|
|
19
|
+
import { cleanupCaptureDirectories } from './visual/capture-cleanup.js';
|
|
17
20
|
import { ReticleRunner } from './runs/reticle-runner.js';
|
|
18
21
|
import { createRunnerPort } from './runs/runner-port.js';
|
|
19
22
|
import { RunStore } from './runs/run-store.js';
|
|
@@ -287,6 +290,7 @@ export async function start(options = {}) {
|
|
|
287
290
|
...(security.token !== undefined ? { token: security.token } : {}),
|
|
288
291
|
close: async () => {
|
|
289
292
|
reaper.stop();
|
|
293
|
+
await cleanupCaptureDirectories();
|
|
290
294
|
leaseReaper?.stop();
|
|
291
295
|
await pool?.shutdown();
|
|
292
296
|
await owned?.dispose();
|
|
@@ -309,7 +313,9 @@ export async function startDaemon(options = {}) {
|
|
|
309
313
|
// mirror rejection so a port collision can't surface as an unhandled promise rejection.
|
|
310
314
|
void bridge.ready.catch(() => undefined);
|
|
311
315
|
// `reticle status` GETs this for a live, at-a-glance view of connected tabs + their health.
|
|
312
|
-
|
|
316
|
+
// The same diagnosis agents get on an empty `reticle_sessions`, so `reticle status` — the
|
|
317
|
+
// most-run command in the field — stops answering "sessionCount: 0" and nothing else.
|
|
318
|
+
shared.attachStatus(() => statusPayload(bridge.sessions.count(), bridge.sessions.list(), bridge.sessions.noSessionHint()));
|
|
313
319
|
// Agent-independent presence: the daemon outlives any single agent, so when the LAST agent's MCP
|
|
314
320
|
// connection drops (it stopped, or is waiting on the human), end every session and push a clear
|
|
315
321
|
// "go to your terminal" notice to the panel — the human is on the browser and must not lose a typed
|
|
@@ -389,6 +395,16 @@ export async function startDaemon(options = {}) {
|
|
|
389
395
|
// connecting session's project (a shared daemon serves many apps; each panel shows only its own
|
|
390
396
|
// flows + legacy untagged ones). Each chip carries a `start` hint (the first step's testid anchor)
|
|
391
397
|
// so the HUD shows a flow only on the page it can begin from — the panel re-scopes per route.
|
|
398
|
+
// The first instrumented app of this daemon run — the funnel step nothing could measure before.
|
|
399
|
+
// Inside the existing session-ready hook rather than beside it: this fires exactly when a page
|
|
400
|
+
// carrying the SDK has completed its handshake, which is the definition of "instrumented", and
|
|
401
|
+
// `reportAppInstrumented` is idempotent so later sessions cost a boolean check.
|
|
402
|
+
bridge.attachSessionReady(() => {
|
|
403
|
+
reportAppInstrumented({
|
|
404
|
+
initialized: readProjectId(process.cwd()) !== undefined,
|
|
405
|
+
agentAttached: agentConnected,
|
|
406
|
+
});
|
|
407
|
+
});
|
|
392
408
|
bridge.attachSessionReady((session) => {
|
|
393
409
|
flows
|
|
394
410
|
.list(session.projectId)
|
|
@@ -417,7 +433,13 @@ export async function startDaemon(options = {}) {
|
|
|
417
433
|
shared.httpServer.once('listening', onListening);
|
|
418
434
|
shared.httpServer.listen(port, security.host ?? LOOPBACK_HOST);
|
|
419
435
|
});
|
|
420
|
-
|
|
436
|
+
// `localhost` is a name with two answers, and Windows/Chrome tries the IPv6 one first. Only alias
|
|
437
|
+
// when we are on the default loopback bind: an explicit RETICLE_HOST is a deliberate choice about
|
|
438
|
+
// reachability, and quietly adding a second listener to it would be the opposite of deliberate.
|
|
439
|
+
const loopbackAlias = security.host === undefined
|
|
440
|
+
? await openLoopbackAlias(port)
|
|
441
|
+
: { opened: false, close: undefined };
|
|
442
|
+
log('mcp_daemon_started', { port, loopbackAlias: loopbackAlias.opened });
|
|
421
443
|
return {
|
|
422
444
|
bridge,
|
|
423
445
|
...(realInput !== undefined ? { realInput } : {}),
|
|
@@ -431,10 +453,12 @@ export async function startDaemon(options = {}) {
|
|
|
431
453
|
agentAttached: () => agentConnected,
|
|
432
454
|
close: async () => {
|
|
433
455
|
reaper.stop();
|
|
456
|
+
await cleanupCaptureDirectories();
|
|
434
457
|
const vh = verifyHttp;
|
|
435
458
|
if (vh !== undefined)
|
|
436
459
|
await new Promise((resolve) => vh.server.close(() => resolve()));
|
|
437
460
|
leaseReaper.stop();
|
|
461
|
+
await loopbackAlias.close?.();
|
|
438
462
|
await pool.shutdown();
|
|
439
463
|
await owned?.dispose();
|
|
440
464
|
await bridge.close();
|
package/dist/init/agent-rules.js
CHANGED
|
@@ -44,10 +44,15 @@ const RULE_BODY = `## Verifying with Reticle
|
|
|
44
44
|
This app is instrumented by **Reticle** — an in-app verification layer exposed as \`reticle_*\` MCP tools and the \`${CLI}\` CLI (run it through npx — Reticle's server is not installed into this project). Verifying is part of "done", not an optional extra.
|
|
45
45
|
|
|
46
46
|
- **When:** after you build or change any user-facing feature, flow, or UI, verify it with Reticle **before** telling the user it is complete. Reading the code or passing unit tests is not enough — drive the real running app.
|
|
47
|
-
- **How:** drive the
|
|
47
|
+
- **How:** drive the flow with \`reticle_act_and_wait({ ref, action, until })\` — it names the consequence you expect BEFORE the action, which is the difference between a check and a rationalisation. Batch a multi-step flow (a login, a form) into one \`reticle_act_sequence\` rather than one round trip per field. Read the surrounding evidence with \`reticle_snapshot\`, \`reticle_state\`, \`reticle_network\`, \`reticle_console\`.
|
|
48
|
+
- **Only \`reticle_act_and_wait\` and \`reticle_assert\` produce a verdict.** \`reticle_act\` and everything else move or read the app and prove nothing, so a session that ends without one of those two has no result however many tools it used. In practice \`reticle_act\` is reached for far more often and proves nothing; \`act_and_wait\` is where the defects actually get caught.
|
|
49
|
+
- **\`verified: "unknown"\` is not a pass.** It means Reticle drove the app and could not tell what happened; \`verifiedReason\` says which of eleven clauses decided that and what to do about it. Report it as unknown — never as working.
|
|
50
|
+
- **If a tool result carries \`verify_next\`, do what it says next.** It means you have driven this page several times without asking for a verdict, so nothing you have done so far is proven.
|
|
48
51
|
- **Covered flows:** run \`${CLI} gate\` — it reports which recorded flows the changed files affect and whether they still pass.
|
|
49
52
|
- **Never weaken a check to make it green.** Downgrading, skipping, or deleting an assertion is a finding, not a fix.
|
|
50
53
|
- **If Reticle can't run** (no daemon, or this is not a running web app), say so — do not skip verification silently.
|
|
54
|
+
- **If the \`reticle_*\` tools are missing entirely, they have not been loaded yet — not broken.** \`init\` registers the MCP server, but a client reads its tool list at startup and does not re-read it. Ask the human to reload their MCP tools (\`/mcp\` in Claude Code, or reload the window in Cursor/VS Code), wait, then carry on. \`npx ${RETICLE_NPM_PACKAGE} status\` works meanwhile and says whether the daemon is up and whether the app has connected.
|
|
55
|
+
- **Setup is not finished until one real flow has been driven and produced a verdict.** \`init\` exiting 0 means files were written; the tools appearing means your client can reach a daemon; a listed session means the app dialled in. None of those is evidence that anything works. If you are blocked before a verdict, say which step and what you tried — never report a clean install you did not verify.
|
|
51
56
|
- **If a tool result carries \`version_skew\`, act on it before trusting anything else.** Two pieces of Reticle are on different wire contracts, so tools can misbehave in ways neither side reports — a green here is not evidence. The field says which pair disagrees and names the exact fix: run \`${CLI} stop\` and retry when the DAEMON is stale, or tell the human to install the matching SDK and restart their dev server when the PAGE is. Do that first, then redo whatever you were verifying.
|
|
52
57
|
- **If a tool result carries \`update_available\`, say so.** Reticle checked npm and a newer version exists. Tell the human the version numbers and offer to run \`${CLI} update\` — between tasks, never mid-verification, because it restarts the daemon and drops the session. Finish what you are doing first.
|
|
53
58
|
|
package/dist/init/detect.d.ts
CHANGED
|
@@ -4,6 +4,14 @@
|
|
|
4
4
|
*/
|
|
5
5
|
export declare const Framework: {
|
|
6
6
|
readonly NEXT: "next";
|
|
7
|
+
/**
|
|
8
|
+
* Nuxt owns its own Vite instance and has no `vite.config`, so it used to fall all the way through
|
|
9
|
+
* to HTML — and then be handed a React kit and a connect snippet guarded on
|
|
10
|
+
* `window.location.hostname === 'localhost'`, which cannot work in a Vue app (the guard throws
|
|
11
|
+
* during SSR, and never runs at all on a non-localhost dev host). Detected in its own right so it
|
|
12
|
+
* gets the package and the recipe that actually fit it.
|
|
13
|
+
*/
|
|
14
|
+
readonly NUXT: "nuxt";
|
|
7
15
|
readonly VITE: "vite";
|
|
8
16
|
readonly SVELTEKIT: "sveltekit";
|
|
9
17
|
readonly ASTRO: "astro";
|
package/dist/init/detect.js
CHANGED
|
@@ -4,6 +4,14 @@
|
|
|
4
4
|
*/
|
|
5
5
|
export const Framework = {
|
|
6
6
|
NEXT: 'next',
|
|
7
|
+
/**
|
|
8
|
+
* Nuxt owns its own Vite instance and has no `vite.config`, so it used to fall all the way through
|
|
9
|
+
* to HTML — and then be handed a React kit and a connect snippet guarded on
|
|
10
|
+
* `window.location.hostname === 'localhost'`, which cannot work in a Vue app (the guard throws
|
|
11
|
+
* during SSR, and never runs at all on a non-localhost dev host). Detected in its own right so it
|
|
12
|
+
* gets the package and the recipe that actually fit it.
|
|
13
|
+
*/
|
|
14
|
+
NUXT: 'nuxt',
|
|
7
15
|
VITE: 'vite',
|
|
8
16
|
SVELTEKIT: 'sveltekit',
|
|
9
17
|
ASTRO: 'astro',
|
|
@@ -32,6 +40,7 @@ export const UiLibrary = {
|
|
|
32
40
|
const NEXT_CONFIGS = ['next.config.js', 'next.config.mjs', 'next.config.ts', 'next.config.cjs'];
|
|
33
41
|
const VITE_CONFIGS = ['vite.config.js', 'vite.config.ts', 'vite.config.mjs', 'vite.config.mts'];
|
|
34
42
|
const SVELTE_CONFIGS = ['svelte.config.js', 'svelte.config.ts', 'svelte.config.mjs'];
|
|
43
|
+
const NUXT_CONFIGS = ['nuxt.config.ts', 'nuxt.config.js', 'nuxt.config.mjs'];
|
|
35
44
|
const ASTRO_CONFIGS = [
|
|
36
45
|
'astro.config.mjs',
|
|
37
46
|
'astro.config.js',
|
|
@@ -98,6 +107,12 @@ function detectFramework(input) {
|
|
|
98
107
|
if (depVersion(pkg, 'next') !== undefined || hasAnyConfig(configFiles, NEXT_CONFIGS)) {
|
|
99
108
|
return Framework.NEXT;
|
|
100
109
|
}
|
|
110
|
+
// Nuxt before Vite: Nuxt apps can carry a `vite` dependency and even a vite.config, but the
|
|
111
|
+
// generic Vite path would wire a plugin into a config Nuxt does not read, and inject connect into
|
|
112
|
+
// an index.html Nuxt does not serve.
|
|
113
|
+
if (depVersion(pkg, 'nuxt') !== undefined || hasAnyConfig(configFiles, NUXT_CONFIGS)) {
|
|
114
|
+
return Framework.NUXT;
|
|
115
|
+
}
|
|
101
116
|
// SvelteKit is Vite-based but renders through app.html, so the Vite plugin's index.html injection
|
|
102
117
|
// never fires (verified) — it needs a manual client connect. Check BEFORE the generic Vite branch.
|
|
103
118
|
if (depVersion(pkg, '@sveltejs/kit') !== undefined || hasAnyConfig(configFiles, SVELTE_CONFIGS)) {
|
|
@@ -0,0 +1,27 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Telling a Python (or Ruby, or Go, or PHP) developer why Reticle cannot wire their app.
|
|
3
|
+
*
|
|
4
|
+
* `reticle init` answered "No package.json found here, and no app directory beneath it either",
|
|
5
|
+
* which reads as *you are in the wrong directory* — so an agent on a Streamlit app went looking for
|
|
6
|
+
* a directory that does not exist, then went looking for a standalone browser bundle to inject by
|
|
7
|
+
* hand, and only worked out the real answer several steps later.
|
|
8
|
+
*
|
|
9
|
+
* The real answer is not about directories at all: Reticle's SDK has to be imported by the app's own
|
|
10
|
+
* JS build, and a server-rendered Python app has no JS build to import it from. That is a property
|
|
11
|
+
* of the project, it is knowable from one look at the filesystem, and saying it plainly costs the
|
|
12
|
+
* reader nothing while the directory message costs them the whole investigation.
|
|
13
|
+
*
|
|
14
|
+
* Deliberately a hint, not a refusal: a Django or Rails app with a `frontend/` package.json is a
|
|
15
|
+
* perfectly ordinary Reticle install, and this only ever speaks when there is no package.json to be
|
|
16
|
+
* found anywhere.
|
|
17
|
+
*/
|
|
18
|
+
/** The ecosystem this directory looks like, or undefined when nothing says. */
|
|
19
|
+
export declare function detectNonJsEcosystem(exists: (file: string) => boolean): string | undefined;
|
|
20
|
+
/**
|
|
21
|
+
* What `init` should say when there is no package.json.
|
|
22
|
+
*
|
|
23
|
+
* Two genuinely different situations behind one old sentence: a JS developer standing in the wrong
|
|
24
|
+
* directory, and a developer whose project is not JavaScript at all. The first needs a path; the
|
|
25
|
+
* second needs to know that no path exists and why.
|
|
26
|
+
*/
|
|
27
|
+
export declare function noPackageJsonMessage(exists: (file: string) => boolean): string;
|
|
@@ -0,0 +1,60 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Telling a Python (or Ruby, or Go, or PHP) developer why Reticle cannot wire their app.
|
|
3
|
+
*
|
|
4
|
+
* `reticle init` answered "No package.json found here, and no app directory beneath it either",
|
|
5
|
+
* which reads as *you are in the wrong directory* — so an agent on a Streamlit app went looking for
|
|
6
|
+
* a directory that does not exist, then went looking for a standalone browser bundle to inject by
|
|
7
|
+
* hand, and only worked out the real answer several steps later.
|
|
8
|
+
*
|
|
9
|
+
* The real answer is not about directories at all: Reticle's SDK has to be imported by the app's own
|
|
10
|
+
* JS build, and a server-rendered Python app has no JS build to import it from. That is a property
|
|
11
|
+
* of the project, it is knowable from one look at the filesystem, and saying it plainly costs the
|
|
12
|
+
* reader nothing while the directory message costs them the whole investigation.
|
|
13
|
+
*
|
|
14
|
+
* Deliberately a hint, not a refusal: a Django or Rails app with a `frontend/` package.json is a
|
|
15
|
+
* perfectly ordinary Reticle install, and this only ever speaks when there is no package.json to be
|
|
16
|
+
* found anywhere.
|
|
17
|
+
*/
|
|
18
|
+
/** Marker files that identify an ecosystem, and what to call it. */
|
|
19
|
+
const ECOSYSTEM_MARKERS = [
|
|
20
|
+
['requirements.txt', 'Python'],
|
|
21
|
+
['pyproject.toml', 'Python'],
|
|
22
|
+
['Pipfile', 'Python'],
|
|
23
|
+
['setup.py', 'Python'],
|
|
24
|
+
['manage.py', 'Python (Django)'],
|
|
25
|
+
['Gemfile', 'Ruby'],
|
|
26
|
+
['go.mod', 'Go'],
|
|
27
|
+
['composer.json', 'PHP'],
|
|
28
|
+
['Cargo.toml', 'Rust'],
|
|
29
|
+
['pom.xml', 'Java'],
|
|
30
|
+
['build.gradle', 'Java'],
|
|
31
|
+
['mix.exs', 'Elixir'],
|
|
32
|
+
];
|
|
33
|
+
/** The ecosystem this directory looks like, or undefined when nothing says. */
|
|
34
|
+
export function detectNonJsEcosystem(exists) {
|
|
35
|
+
for (const [marker, name] of ECOSYSTEM_MARKERS) {
|
|
36
|
+
if (exists(marker))
|
|
37
|
+
return name;
|
|
38
|
+
}
|
|
39
|
+
return undefined;
|
|
40
|
+
}
|
|
41
|
+
/**
|
|
42
|
+
* What `init` should say when there is no package.json.
|
|
43
|
+
*
|
|
44
|
+
* Two genuinely different situations behind one old sentence: a JS developer standing in the wrong
|
|
45
|
+
* directory, and a developer whose project is not JavaScript at all. The first needs a path; the
|
|
46
|
+
* second needs to know that no path exists and why.
|
|
47
|
+
*/
|
|
48
|
+
export function noPackageJsonMessage(exists) {
|
|
49
|
+
const ecosystem = detectNonJsEcosystem(exists);
|
|
50
|
+
if (ecosystem === undefined) {
|
|
51
|
+
return ('No package.json found here, and no app directory beneath it either. Run `reticle init` from ' +
|
|
52
|
+
"your app's directory, or from a repo root that contains it.");
|
|
53
|
+
}
|
|
54
|
+
return (`This looks like a ${ecosystem} project, and there is no package.json here or in any app ` +
|
|
55
|
+
'directory beneath it. Reticle instruments a page by having its SDK imported into the app’s own ' +
|
|
56
|
+
'JavaScript build — so a server-rendered app with no JS build has nowhere to load it from, and ' +
|
|
57
|
+
'there is no directory you could run this from that would change that. ' +
|
|
58
|
+
'If this project does have a JS front end (a `frontend/`, `client/` or `assets/` directory with ' +
|
|
59
|
+
'its own package.json), run `reticle init` there, or point at it with `--app <dir>`.');
|
|
60
|
+
}
|
|
@@ -38,6 +38,15 @@ export declare function nextSteps(input: PlanInput): Step[];
|
|
|
38
38
|
* bundler never processes for modules. Reported from a real cra-redux-saga app.
|
|
39
39
|
*/
|
|
40
40
|
export declare function craSteps(input: PlanInput): Step[];
|
|
41
|
+
/**
|
|
42
|
+
* Nuxt: one manual step carrying the whole recipe, and no pretence of more.
|
|
43
|
+
*
|
|
44
|
+
* There is no Nuxt app in `apps/` and no CI gate for one, so an auto-written plugin would be a
|
|
45
|
+
* support claim nothing backs — the same reasoning SvelteKit already carries. What this DOES fix is
|
|
46
|
+
* everything that made the previous fall-through actively wrong: Nuxt was classified as `html`, so
|
|
47
|
+
* it was handed the React kit and a localhost-guarded snippet that cannot run in a Vue app.
|
|
48
|
+
*/
|
|
49
|
+
export declare function nuxtSteps(input: PlanInput): Step[];
|
|
41
50
|
export declare function svelteKitSteps(input: PlanInput): Step[];
|
|
42
51
|
/**
|
|
43
52
|
* Astro: the config define + build target, and the connect script in ONE layout.
|
|
@@ -8,7 +8,7 @@ import { patchNextConfig, patchRootLayout, patchPagesApp } from './next-patch.js
|
|
|
8
8
|
import { patchAstroConfig, patchAstroLayout } from './astro-patch.js';
|
|
9
9
|
import { CRA_DEV_MODULE_IMPORT, CRA_DEV_MODULE_PATH, CRA_ENV_PATH, CRA_TOKEN_PER_MACHINE_NOTICE, TOKEN_VAR, craDevModuleFile, craEnvPatch, craImportPatch, } from './cra.js';
|
|
10
10
|
import { PatchKind } from './patch-kind.js';
|
|
11
|
-
import { viteManual, NEXT_LAYOUT_MANUAL, NEXT_LAYOUT_PATH, viteDevModuleFile, VITE_DEV_MODULE_PATH, nextReticleDevFile, NEXT_RETICLE_DEV_PATH, nextConfigManual, svelteKitHooksFile, SVELTEKIT_HOOKS_PATH, UNVERIFIED_FRAMEWORK_NOTE, astroManual, } from './snippets.js';
|
|
11
|
+
import { viteManual, NEXT_LAYOUT_MANUAL, NEXT_LAYOUT_PATH, viteDevModuleFile, VITE_DEV_MODULE_PATH, nextReticleDevFile, NEXT_RETICLE_DEV_PATH, nextConfigManual, svelteKitHooksFile, SVELTEKIT_HOOKS_PATH, UNVERIFIED_FRAMEWORK_NOTE, astroManual, nuxtManual, NUXT_PLUGIN_PATH, } from './snippets.js';
|
|
12
12
|
import { StepStatus } from './plan.js';
|
|
13
13
|
/** What adding `reticle()` to a Vite config buys, which differs by framework. */
|
|
14
14
|
export const VITE_PLUGIN_DETAIL = {
|
|
@@ -291,6 +291,24 @@ export function craSteps(input) {
|
|
|
291
291
|
});
|
|
292
292
|
return steps;
|
|
293
293
|
}
|
|
294
|
+
/**
|
|
295
|
+
* Nuxt: one manual step carrying the whole recipe, and no pretence of more.
|
|
296
|
+
*
|
|
297
|
+
* There is no Nuxt app in `apps/` and no CI gate for one, so an auto-written plugin would be a
|
|
298
|
+
* support claim nothing backs — the same reasoning SvelteKit already carries. What this DOES fix is
|
|
299
|
+
* everything that made the previous fall-through actively wrong: Nuxt was classified as `html`, so
|
|
300
|
+
* it was handed the React kit and a localhost-guarded snippet that cannot run in a Vue app.
|
|
301
|
+
*/
|
|
302
|
+
export function nuxtSteps(input) {
|
|
303
|
+
return [
|
|
304
|
+
{
|
|
305
|
+
title: 'Connect snippet (Nuxt)',
|
|
306
|
+
target: NUXT_PLUGIN_PATH,
|
|
307
|
+
status: StepStatus.MANUAL,
|
|
308
|
+
detail: nuxtManual(input.options.port, input.options.projectId),
|
|
309
|
+
},
|
|
310
|
+
];
|
|
311
|
+
}
|
|
294
312
|
export function svelteKitSteps(input) {
|
|
295
313
|
const unverified = {
|
|
296
314
|
title: 'SvelteKit is UNVERIFIED',
|
package/dist/init/plan.js
CHANGED
|
@@ -9,13 +9,15 @@ import { mergeCursorConfig, CursorMergeStatus, cursorServerEntry } from './curso
|
|
|
9
9
|
import { mergeClientConfig, ClientMergeStatus, clientSnippet, clientSpec, } from './mcp-clients.js';
|
|
10
10
|
import { CLAUDE_COMMAND_PATH, CURSOR_COMMAND_PATH, SLASH_COMMAND_BODY, SLASH_COMMAND_SIGNATURE, } from './slash-command.js';
|
|
11
11
|
import { mergeMarkedInstruction, cursorRuleFile, AgentRuleStatus, CLAUDE_MD_PATH, AGENTS_MD_PATH, CURSOR_RULE_PATH, } from './agent-rules.js';
|
|
12
|
-
import { viteSteps, nextSteps, craSteps, svelteKitSteps, astroSteps, VITE_PLUGIN_DETAIL, } from './plan-framework.js';
|
|
12
|
+
import { viteSteps, nextSteps, craSteps, svelteKitSteps, nuxtSteps, astroSteps, VITE_PLUGIN_DETAIL, } from './plan-framework.js';
|
|
13
13
|
import { htmlManual, reticleConfigContent, unverifiedUiLibraryNote } from './snippets.js';
|
|
14
14
|
// An app dev installs exactly the audience-scoped browser-side dependencies — never the retired
|
|
15
15
|
// `@reticlehq/core` umbrella (which dragged the Node MCP server + ws into every app). The kit is the
|
|
16
16
|
// framework adapter (it re-exports the browser sensor), paired with that framework's dev-only build
|
|
17
17
|
// plugin for source mapping + connect injection.
|
|
18
18
|
const RETICLE_REACT_KIT = '@reticlehq/react';
|
|
19
|
+
/** The framework-neutral sensor, for stacks the React adapter has nothing to attach to. */
|
|
20
|
+
const RETICLE_BROWSER_SDK = '@reticlehq/browser';
|
|
19
21
|
const RETICLE_VITE_PLUGIN = '@reticlehq/vite-plugin';
|
|
20
22
|
const RETICLE_NEXT_PLUGIN = '@reticlehq/next';
|
|
21
23
|
/**
|
|
@@ -41,6 +43,11 @@ export function frameworkPackages(framework) {
|
|
|
41
43
|
case Framework.SVELTEKIT:
|
|
42
44
|
// SvelteKit builds on Vite; until a dedicated Svelte kit exists it uses the Vite build plugin.
|
|
43
45
|
return [RETICLE_REACT_KIT, RETICLE_VITE_PLUGIN];
|
|
46
|
+
case Framework.NUXT:
|
|
47
|
+
// The framework-neutral sensor, NOT the React kit. Nuxt renders Vue, and installing a package
|
|
48
|
+
// named @reticlehq/react — with `react` in its peer dependencies — into a Vue codebase is the
|
|
49
|
+
// single thing most likely to make someone abandon the setup, whether or not it works.
|
|
50
|
+
return [RETICLE_BROWSER_SDK];
|
|
44
51
|
case Framework.ASTRO:
|
|
45
52
|
// Astro owns its own Vite instance and renders its own HTML, so there is no config for the
|
|
46
53
|
// plugin to attach to — the kit alone, connected from a page <script> (see astroManual).
|
|
@@ -68,6 +75,7 @@ const CONNECT_STEP_TITLES = new Set([
|
|
|
68
75
|
'Connect snippet',
|
|
69
76
|
'Connect snippet (CRA)',
|
|
70
77
|
'Connect snippet (Astro)',
|
|
78
|
+
'Connect snippet (Nuxt)',
|
|
71
79
|
'Reticle client hook',
|
|
72
80
|
'Reticle connect module',
|
|
73
81
|
'ReticleDev component',
|
|
@@ -484,8 +492,14 @@ function reticleConfigStep(input) {
|
|
|
484
492
|
*/
|
|
485
493
|
function uiLibraryStep(input) {
|
|
486
494
|
const lib = input.detection.uiLibrary;
|
|
487
|
-
|
|
495
|
+
// Nuxt and SvelteKit each carry their own unverified wording inside the recipe, so a second,
|
|
496
|
+
// more generic notice beside it would only argue with the specific one.
|
|
497
|
+
const framework = input.detection.framework;
|
|
498
|
+
if (lib === UiLibrary.REACT ||
|
|
499
|
+
framework === Framework.SVELTEKIT ||
|
|
500
|
+
framework === Framework.NUXT) {
|
|
488
501
|
return [];
|
|
502
|
+
}
|
|
489
503
|
if (lib === UiLibrary.UNKNOWN)
|
|
490
504
|
return [];
|
|
491
505
|
return [
|
|
@@ -518,6 +532,9 @@ export function buildPlan(input) {
|
|
|
518
532
|
else if (input.detection.framework === Framework.CRA) {
|
|
519
533
|
steps.push(...craSteps(input));
|
|
520
534
|
}
|
|
535
|
+
else if (input.detection.framework === Framework.NUXT) {
|
|
536
|
+
steps.push(...nuxtSteps(input));
|
|
537
|
+
}
|
|
521
538
|
else if (input.detection.framework === Framework.SVELTEKIT) {
|
|
522
539
|
steps.push(...svelteKitSteps(input));
|
|
523
540
|
// The Vite plugin as well as the client hook. `init` already INSTALLS @reticlehq/vite-plugin for
|
|
@@ -530,7 +547,7 @@ export function buildPlan(input) {
|
|
|
530
547
|
title: 'Connect snippet',
|
|
531
548
|
target: 'index.html',
|
|
532
549
|
status: StepStatus.MANUAL,
|
|
533
|
-
detail: htmlManual(input.options.port, input.options.projectId),
|
|
550
|
+
detail: htmlManual(input.options.port, input.options.projectId, input.pairingToken),
|
|
534
551
|
});
|
|
535
552
|
}
|
|
536
553
|
return { framework: input.detection.framework, steps };
|
package/dist/init/run.js
CHANGED
|
@@ -4,6 +4,7 @@
|
|
|
4
4
|
* access goes through `InitIo` so the orchestration is unit-testable with an in-memory IO.
|
|
5
5
|
*/
|
|
6
6
|
import { dirname, join } from 'node:path';
|
|
7
|
+
import { noPackageJsonMessage } from './non-js-project.js';
|
|
7
8
|
import { spanSync } from '../trace.js';
|
|
8
9
|
import { readFileSync } from 'node:fs';
|
|
9
10
|
import { homedir } from 'node:os';
|
|
@@ -402,16 +403,39 @@ export const FEEDBACK_HINT = 'Anything wrong, missing, or awkward — in this se
|
|
|
402
403
|
'one thing that decides what gets fixed:\n' +
|
|
403
404
|
` ${CLI} feedback --agent --kind <bug|gap|ambiguity|feature_request|improvement> "what happened" (agents)\n` +
|
|
404
405
|
` ${CLI} feedback "what worked, what didn't" (humans)`;
|
|
405
|
-
function
|
|
406
|
+
function devServerRestart(framework) {
|
|
406
407
|
if (framework === Framework.NEXT)
|
|
407
|
-
return 'Restart `next dev
|
|
408
|
+
return 'Restart `next dev`';
|
|
408
409
|
if (framework === Framework.VITE)
|
|
409
|
-
return 'Restart `vite
|
|
410
|
+
return 'Restart `vite`';
|
|
410
411
|
if (framework === Framework.ASTRO)
|
|
411
|
-
return 'Restart `astro dev
|
|
412
|
+
return 'Restart `astro dev`';
|
|
412
413
|
if (framework === Framework.SVELTEKIT)
|
|
413
|
-
return 'Restart your dev server (`npm run dev`)
|
|
414
|
-
return 'Reload your app on localhost
|
|
414
|
+
return 'Restart your dev server (`npm run dev`)';
|
|
415
|
+
return 'Reload your app on localhost';
|
|
416
|
+
}
|
|
417
|
+
/**
|
|
418
|
+
* The last two lines a user reads, and the order matters.
|
|
419
|
+
*
|
|
420
|
+
* This used to say only "Restart <dev server>, then ask your agent: List Reticle sessions" — and
|
|
421
|
+
* asking the agent was the one thing that could not work yet. `init` registers the MCP server, but
|
|
422
|
+
* an agent client reads its tool list when it STARTS and never re-reads it, so the session that
|
|
423
|
+
* just ran `init` has no `reticle_*` tools however clean the install was. The user follows the
|
|
424
|
+
* instruction, the agent answers "unknown tool", and the obvious conclusion is that the install
|
|
425
|
+
* failed.
|
|
426
|
+
*
|
|
427
|
+
* So the reload is named FIRST, before the sentence that depends on it. Omitted entirely when this
|
|
428
|
+
* run did not register MCP (`--no-mcp`), where it would be advice about something we did not do.
|
|
429
|
+
*/
|
|
430
|
+
function restartHint(framework, mcpRegistered) {
|
|
431
|
+
const dev = `${devServerRestart(framework)}.`;
|
|
432
|
+
if (!mcpRegistered)
|
|
433
|
+
return `${dev} Then ask your agent: "List Reticle sessions".`;
|
|
434
|
+
return (`${dev}\n` +
|
|
435
|
+
"Then reload your agent's MCP tools — `/mcp` in Claude Code, or reload the window in " +
|
|
436
|
+
'Cursor/VS Code.\n' +
|
|
437
|
+
'The tools only appear after that: your agent read its tool list before Reticle existed.\n' +
|
|
438
|
+
'Then ask it: "List Reticle sessions".');
|
|
415
439
|
}
|
|
416
440
|
const SKIPPED_DETAIL = 'skipped — the dependency install above failed, and wiring the app to a package that is not ' +
|
|
417
441
|
'installed stops it booting. Run that install, then re-run `reticle init`.';
|
|
@@ -470,7 +494,7 @@ function report(plan, dryRun, failed, skipped, degraded, io, projectDir) {
|
|
|
470
494
|
'"no browser session connected" until then. Everything else is already in place.');
|
|
471
495
|
io.print('');
|
|
472
496
|
}
|
|
473
|
-
io.print(restartHint(plan.framework));
|
|
497
|
+
io.print(restartHint(plan.framework, wasMcpRegistered(resolvedStatus(plan, MCP_TARGET, failed, skipped))));
|
|
474
498
|
return { ok: !connectPending, applied, manual };
|
|
475
499
|
}
|
|
476
500
|
/**
|
|
@@ -655,8 +679,13 @@ function runInitSteps(options, io) {
|
|
|
655
679
|
if (redirectedEarly !== null)
|
|
656
680
|
return redirectedEarly;
|
|
657
681
|
if (null === pkgRaw) {
|
|
658
|
-
io.print(
|
|
659
|
-
|
|
682
|
+
io.print(
|
|
683
|
+
// Two genuinely different situations used to share one sentence: a JS developer in the wrong
|
|
684
|
+
// directory, and a project that is not JavaScript at all. The second reads the old wording as
|
|
685
|
+
// a path problem and goes looking for a directory that cannot exist — reported from a
|
|
686
|
+
// Streamlit app, where the search continued into hunting for a browser bundle to inject by
|
|
687
|
+
// hand before the real answer surfaced.
|
|
688
|
+
noPackageJsonMessage((file) => io.exists(join(options.cwd, file))));
|
|
660
689
|
// The onboarding funnel had NO instrumentation, so a setup that died here was indistinguishable
|
|
661
690
|
// from someone who never ran the command — the two failure modes with the most different fixes.
|
|
662
691
|
reportInitOutcome({ ok: false, reason: InitFailure.NO_PACKAGE_JSON });
|
package/dist/init/snippets.d.ts
CHANGED
|
@@ -47,7 +47,7 @@ export declare const NEXT_LAYOUT_MANUAL = "Mount <ReticleDev /> in your root lay
|
|
|
47
47
|
* where a bare `@reticlehq/react` import resolves. A bare import in a plain index.html does NOT resolve in
|
|
48
48
|
* the browser, so we never tell a bundled app to do that (the old advice silently failed for CRA).
|
|
49
49
|
*/
|
|
50
|
-
export declare function htmlManual(port: number | undefined, projectId?: string): string;
|
|
50
|
+
export declare function htmlManual(port: number | undefined, projectId?: string, pairingToken?: string): string;
|
|
51
51
|
export declare const NEXT_RETICLE_DEV_PATH = "app/reticle-dev.tsx";
|
|
52
52
|
export declare const SVELTEKIT_HOOKS_PATH = "src/hooks.client.ts";
|
|
53
53
|
/**
|
|
@@ -68,6 +68,25 @@ export declare const UNVERIFIED_FRAMEWORK_NOTE = "Reticle has no SvelteKit app a
|
|
|
68
68
|
* a client hook is the reliable path. SvelteKit runs src/hooks.client.ts on the client at startup.
|
|
69
69
|
*/
|
|
70
70
|
export declare function svelteKitHooksFile(port: number | undefined, projectId?: string): string;
|
|
71
|
+
/** Where a Nuxt dev-only client plugin belongs. `.client` keeps it out of SSR; Nuxt auto-registers it. */
|
|
72
|
+
export declare const NUXT_PLUGIN_PATH = "app/plugins/reticle.client.ts";
|
|
73
|
+
/**
|
|
74
|
+
* The Nuxt recipe, written out in full because every trap in it is one somebody actually hit.
|
|
75
|
+
*
|
|
76
|
+
* Reported from the field, in the order they were hit: `init` classified a Nuxt 4 app as `html`, so
|
|
77
|
+
* it installed a package named `@reticlehq/react` (with `react` in its peer dependencies) into a Vue
|
|
78
|
+
* codebase — the reporter only continued after auditing our dist to confirm there are no React
|
|
79
|
+
* imports at runtime, which most people will not do. It then handed over a snippet guarded on
|
|
80
|
+
* `window.location.hostname === 'localhost'`, which fails twice over in Nuxt: `window` does not
|
|
81
|
+
* exist during SSR, and the dev host here was a hosts-file alias (required for the backend's
|
|
82
|
+
* white-label origin detection), so the guard was false and the connect never ran — no error, no log
|
|
83
|
+
* line, nothing to debug. And nothing said a running dev server does not pick up a new plugin.
|
|
84
|
+
*
|
|
85
|
+
* So: `import.meta.dev` (build-time, host-independent) instead of a hostname check, `.client.ts`
|
|
86
|
+
* instead of an SSR guard, the framework-neutral sensor instead of the React kit, the non-localhost
|
|
87
|
+
* flag named up front, and the restart said out loud.
|
|
88
|
+
*/
|
|
89
|
+
export declare function nuxtManual(port: number | undefined, projectId?: string): string;
|
|
71
90
|
/**
|
|
72
91
|
* Root-level project config for Reticle. Written by `reticle init`; read by `reticle mcp` for the port
|
|
73
92
|
* and by tooling for the stable projectId (the app's identity across port changes).
|