@reticlehq/server 2.3.0 → 2.4.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (43) hide show
  1. package/dist/bridge.js +43 -3
  2. package/dist/bridge.test-harness.d.ts +5 -0
  3. package/dist/bridge.test-harness.js +12 -0
  4. package/dist/cli-parse.d.ts +14 -2
  5. package/dist/cli-parse.js +40 -12
  6. package/dist/cli-port.d.ts +3 -0
  7. package/dist/cli-port.js +21 -0
  8. package/dist/cli.js +9 -2
  9. package/dist/flows/decision.d.ts +0 -6
  10. package/dist/flows/decision.js +47 -6
  11. package/dist/flows/flow-tools.js +14 -6
  12. package/dist/init/capabilities.d.ts +17 -0
  13. package/dist/init/capabilities.js +51 -0
  14. package/dist/init/detect.d.ts +25 -0
  15. package/dist/init/detect.js +65 -3
  16. package/dist/init/next-patch.d.ts +41 -0
  17. package/dist/init/next-patch.js +106 -0
  18. package/dist/init/node-io.js +25 -0
  19. package/dist/init/patch-kind.d.ts +20 -0
  20. package/dist/init/patch-kind.js +10 -0
  21. package/dist/init/plan-framework.d.ts +35 -0
  22. package/dist/init/plan-framework.js +217 -0
  23. package/dist/init/plan.d.ts +51 -3
  24. package/dist/init/plan.js +125 -125
  25. package/dist/init/run.d.ts +17 -0
  26. package/dist/init/run.js +207 -16
  27. package/dist/init/slash-command.d.ts +18 -0
  28. package/dist/init/slash-command.js +52 -0
  29. package/dist/init/snippets.d.ts +44 -3
  30. package/dist/init/snippets.js +152 -6
  31. package/dist/init/vite-config.d.ts +4 -10
  32. package/dist/init/vite-config.js +15 -7
  33. package/dist/input/driven-redaction.d.ts +15 -0
  34. package/dist/input/driven-redaction.js +76 -0
  35. package/dist/input/network-detail.d.ts +9 -3
  36. package/dist/input/network-detail.js +35 -15
  37. package/dist/mcp-proxy.d.ts +35 -3
  38. package/dist/mcp-proxy.js +174 -27
  39. package/dist/session/session-manager.js +6 -0
  40. package/dist/session/session.d.ts +6 -0
  41. package/dist/session/session.js +7 -0
  42. package/dist/tools/read-tools.js +4 -0
  43. package/package.json +2 -2
package/dist/init/run.js CHANGED
@@ -7,8 +7,13 @@ import { dirname, join } from 'node:path';
7
7
  import { detect, Framework } from './detect.js';
8
8
  import { DEPS_TARGET, MCP_TARGET, buildPlan, StepStatus, } from './plan.js';
9
9
  import { claudeAvailableProbe, claudeExistsProbe } from './mcp.js';
10
+ import { reticleDevLocation } from './next-patch.js';
11
+ import { scanTestids, storeHints } from './capabilities.js';
10
12
  import { CURSOR_DIR_RELPATH, CURSOR_MCP_RELPATH } from './cursor.js';
11
13
  import { deriveProjectId, packageName } from './project-id.js';
14
+ import { VITE_DEV_MODULE_PATH } from './snippets.js';
15
+ import { CLAUDE_COMMAND_PATH, CURSOR_COMMAND_PATH } from './slash-command.js';
16
+ import { SERVER_VERSION } from '../server-version.js';
12
17
  import { InitFailure, reportInitOutcome } from '../telemetry/init-telemetry.js';
13
18
  /** Lockfile basenames, in package-manager preference order (mirrors detect.ts). */
14
19
  const LOCKFILE_NAMES = [
@@ -43,8 +48,60 @@ export function resolveLockfiles(rootFiles, cwd, io) {
43
48
  return set;
44
49
  }
45
50
  const PACKAGE_JSON = 'package.json';
46
- const NEXT_RETICLE_DEV = 'app/reticle-dev.tsx';
51
+ /**
52
+ * Root-layout candidates, App Router only. `--src-dir` apps keep theirs under `src/app`, and the
53
+ * ReticleDev component has to land NEXT TO the layout or the relative import it generates is dead.
54
+ */
55
+ const NEXT_LAYOUT_CANDIDATES = [
56
+ 'app/layout.tsx',
57
+ 'app/layout.jsx',
58
+ 'app/layout.js',
59
+ 'src/app/layout.tsx',
60
+ 'src/app/layout.jsx',
61
+ 'src/app/layout.js',
62
+ ];
63
+ /**
64
+ * Pages Router mount points, checked only when there is no App Router layout. A Pages app has no
65
+ * `app/` directory at all, so writing the component there produced a file nothing imported.
66
+ */
67
+ const NEXT_PAGES_APP_CANDIDATES = [
68
+ 'pages/_app.tsx',
69
+ 'pages/_app.jsx',
70
+ 'pages/_app.js',
71
+ 'src/pages/_app.tsx',
72
+ 'src/pages/_app.jsx',
73
+ 'src/pages/_app.js',
74
+ ];
47
75
  const SVELTEKIT_HOOKS = 'src/hooks.client.ts';
76
+ /** Where an app's components live. Bounded on purpose — this is a hint, not an index of the repo. */
77
+ const SOURCE_DIRS = ['src', 'src/components', 'src/pages', 'app', 'components'];
78
+ const SOURCE_FILE = /\.(tsx|jsx|ts|js|svelte|vue|astro)$/;
79
+ /** Files read for the testid scan. A capabilities block is a hint; reading a whole repo for it is not. */
80
+ const MAX_SCANNED_FILES = 200;
81
+ /** Read a bounded set of the app's source files, for the `data-testid` scan. */
82
+ function readSourceFiles(io) {
83
+ const out = [];
84
+ for (const dir of SOURCE_DIRS) {
85
+ for (const name of io.listFiles(dir)) {
86
+ if (!SOURCE_FILE.test(name))
87
+ continue;
88
+ const content = io.readFile(`${dir}/${name}`);
89
+ if (content !== null)
90
+ out.push(content);
91
+ if (out.length >= MAX_SCANNED_FILES)
92
+ return out;
93
+ }
94
+ }
95
+ return out;
96
+ }
97
+ /** Direct dependency names, for naming the state libraries an app actually has. */
98
+ function dependencyNames(pkg) {
99
+ const p = (pkg ?? {});
100
+ return new Set([
101
+ ...Object.keys(p['dependencies'] ?? {}),
102
+ ...Object.keys(p['devDependencies'] ?? {}),
103
+ ]);
104
+ }
48
105
  const VITE_CONFIG_CANDIDATES = [
49
106
  'vite.config.ts',
50
107
  'vite.config.js',
@@ -57,11 +114,17 @@ const NEXT_CONFIG_CANDIDATES = [
57
114
  'next.config.ts',
58
115
  'next.config.cjs',
59
116
  ];
117
+ /**
118
+ * `⚠` means WORK LEFT TO DO and nothing else — it is what an agent (and the release gate) counts to
119
+ * decide whether the install finished. A notice gets its own mark so "steps remaining" can reach zero
120
+ * on a working install that happens to be on an ungated stack.
121
+ */
60
122
  const STATUS_SYMBOL = {
61
123
  [StepStatus.APPLY]: '✓',
62
124
  [StepStatus.MANUAL]: '⚠',
63
125
  [StepStatus.ALREADY]: '·',
64
126
  [StepStatus.SKIP]: '–',
127
+ [StepStatus.NOTICE]: 'ℹ',
65
128
  };
66
129
  function firstPresent(files, candidates) {
67
130
  for (const c of candidates)
@@ -79,6 +142,8 @@ function gatherPlanInput(options, io, pkgRaw) {
79
142
  configFiles: rootFiles,
80
143
  // Walk up for the lockfile so a monorepo sub-package picks the workspace's package manager.
81
144
  lockfiles: resolveLockfiles(rootFiles, options.cwd, io),
145
+ // An already-installed tree names its own manager, which matters when no lockfile is committed.
146
+ nodeModulesMarkers: new Set(io.listFiles('node_modules')),
82
147
  };
83
148
  const detection = detect(detectInput);
84
149
  const vitePath = firstPresent(rootFiles, VITE_CONFIG_CANDIDATES);
@@ -94,51 +159,121 @@ function gatherPlanInput(options, io, pkgRaw) {
94
159
  const cursorConfigPath = `${io.homeDir()}/${CURSOR_MCP_RELPATH}`;
95
160
  const cursorPresent = options.mcp && io.exists(cursorDir);
96
161
  const cursorConfig = cursorPresent ? io.readFile(cursorConfigPath) : null;
162
+ const nextConfigFile = firstPresent(rootFiles, NEXT_CONFIG_CANDIDATES);
163
+ // App Router first; a Pages Router app has no layout, and its mount point is pages/_app.
164
+ const layoutPath = (NEXT_LAYOUT_CANDIDATES.find((p) => io.exists(p)) ??
165
+ NEXT_PAGES_APP_CANDIDATES.find((p) => io.exists(p))) ??
166
+ null;
167
+ const layoutSource = layoutPath === null ? null : io.readFile(layoutPath);
168
+ // Where the component goes depends on WHICH router mounts it: `pages/` routes on presence, so a
169
+ // component there becomes a broken route; `app/` routes on filename, so a sibling is inert.
170
+ const devLocation = reticleDevLocation(layoutPath ?? 'app/layout.tsx', detection.typescript);
97
171
  return {
98
172
  detection,
99
173
  claudeCli,
100
174
  mcpExists,
101
175
  cursorPresent,
176
+ cursorProjectPresent: io.exists(CURSOR_DIR_RELPATH),
102
177
  cursorConfig,
103
178
  cursorConfigPath,
104
179
  viteConfig,
105
- nextConfigFile: firstPresent(rootFiles, NEXT_CONFIG_CANDIDATES),
106
- nextReticleDevExists: io.exists(NEXT_RETICLE_DEV),
180
+ nextConfigFile,
181
+ nextConfigSource: nextConfigFile === null ? null : io.readFile(nextConfigFile),
182
+ nextLayout: layoutPath !== null && layoutSource !== null
183
+ ? { path: layoutPath, source: layoutSource }
184
+ : null,
185
+ // Capabilities: scanned, never asked for. Bounded — a hint for the agent, not a repo index.
186
+ testids: scanTestids(readSourceFiles(io)),
187
+ storeHints: storeHints(dependencyNames(pkg)),
188
+ viteDevModuleExists: io.exists(VITE_DEV_MODULE_PATH),
189
+ nextReticleDevPath: devLocation.path,
190
+ nextReticleDevImport: devLocation.importSpecifier,
191
+ nextReticleDevExists: io.exists(devLocation.path),
107
192
  svelteKitHooksExists: io.exists(SVELTEKIT_HOOKS),
108
193
  reticleConfigExists: io.exists('.reticle.json'),
109
194
  // Read the agent instruction files so the rule merge stays idempotent across re-runs.
110
195
  claudeMdContent: io.readFile('CLAUDE.md'),
111
196
  agentsMdContent: io.readFile('AGENTS.md'),
112
197
  cursorRuleExists: io.exists('.cursor/rules/reticle.mdc'),
113
- options: { port: options.port, mcp: options.mcp, install: options.install, projectId },
198
+ claudeCommandExists: io.exists(CLAUDE_COMMAND_PATH),
199
+ cursorCommandExists: io.exists(CURSOR_COMMAND_PATH),
200
+ options: {
201
+ port: options.port,
202
+ mcp: options.mcp,
203
+ install: options.install,
204
+ projectId,
205
+ // The SDK must match the CLI asking for it — see pinnedPackages.
206
+ sdkVersion: SERVER_VERSION,
207
+ },
114
208
  };
115
209
  }
210
+ /** Where workspace tooling conventionally puts packages. */
211
+ const WORKSPACE_PARENTS = ['apps', 'packages'];
212
+ /** Deps that mark a directory as a runnable web app even when it has no bundler config file. */
213
+ const APP_DEPS = ['next', 'vite'];
214
+ function looksLikeApp(dir, io) {
215
+ const pkgRaw = io.readFile(`${dir}/${PACKAGE_JSON}`);
216
+ if (pkgRaw === null)
217
+ return false;
218
+ const configs = [...VITE_CONFIG_CANDIDATES, ...NEXT_CONFIG_CANDIDATES];
219
+ if (configs.some((c) => io.exists(`${dir}/${c}`)))
220
+ return true;
221
+ // `next.config` is optional in Next, so the dependency list is the other half of the signal.
222
+ return APP_DEPS.some((d) => pkgRaw.includes(`"${d}"`));
223
+ }
224
+ /**
225
+ * App directories under a workspace root.
226
+ *
227
+ * Running `reticle init` at the repo root is what people actually do, and in a monorepo the app is a
228
+ * directory down — so init detected "no framework", printed a wall of manual HTML instructions, and
229
+ * would have installed the SDK into the ROOT package.json. It already walks UP for the lockfile, so
230
+ * it knows it is in a workspace; this is the matching walk DOWN.
231
+ */
232
+ export function findWorkspaceApps(io) {
233
+ const found = [];
234
+ for (const parent of WORKSPACE_PARENTS) {
235
+ for (const name of io.listDirs(parent)) {
236
+ const dir = `${parent}/${name}`;
237
+ if (looksLikeApp(dir, io))
238
+ found.push(dir);
239
+ }
240
+ }
241
+ return found;
242
+ }
116
243
  function restartHint(framework) {
117
244
  if (framework === Framework.NEXT)
118
245
  return 'Restart `next dev`, then ask your agent: "List Reticle sessions".';
119
246
  if (framework === Framework.VITE)
120
247
  return 'Restart `vite`, then ask your agent: "List Reticle sessions".';
248
+ if (framework === Framework.ASTRO)
249
+ return 'Restart `astro dev`, then ask your agent: "List Reticle sessions".';
121
250
  if (framework === Framework.SVELTEKIT)
122
251
  return 'Restart your dev server (`npm run dev`), then ask your agent: "List Reticle sessions".';
123
252
  return 'Reload your app on localhost, then ask your agent: "List Reticle sessions".';
124
253
  }
125
- function report(plan, dryRun, failed, io) {
254
+ const SKIPPED_DETAIL = 'skipped — the dependency install above failed, and wiring the app to a package that is not ' +
255
+ 'installed stops it booting. Run that install, then re-run `reticle init`.';
256
+ function report(plan, dryRun, failed, skipped, io) {
126
257
  io.print(dryRun ? 'reticle init (dry run — no files written)' : 'reticle init');
127
258
  io.print('');
128
259
  let applied = 0;
129
260
  let manual = 0;
130
261
  for (const s of plan.steps) {
131
262
  // A side effect that failed to apply is reported as a manual step with its fallback command.
132
- const downgraded = failed.has(s.target);
263
+ const downgraded = failed.has(s.target) || skipped.has(s.target);
133
264
  const status = downgraded ? StepStatus.MANUAL : s.status;
134
- const detail = downgraded && s.exec !== undefined
135
- ? `step failed — run manually: ${s.exec.fallback}`
136
- : s.detail;
265
+ const detail = skipped.has(s.target)
266
+ ? SKIPPED_DETAIL
267
+ : downgraded && s.exec !== undefined
268
+ ? `step failed — run manually: ${s.exec.fallback}`
269
+ : s.detail;
137
270
  io.print(` [${STATUS_SYMBOL[status]}] ${s.title} → ${s.target}`);
138
271
  if (status === StepStatus.APPLY)
139
272
  applied++;
140
- if (status === StepStatus.MANUAL) {
141
- manual++;
273
+ if (status === StepStatus.MANUAL || status === StepStatus.NOTICE) {
274
+ // A notice prints in full like a manual step — it is worth reading — but is NOT counted as work.
275
+ if (status === StepStatus.MANUAL)
276
+ manual++;
142
277
  for (const line of detail.split('\n'))
143
278
  io.print(` ${line}`);
144
279
  }
@@ -150,18 +285,35 @@ function report(plan, dryRun, failed, io) {
150
285
  io.print(restartHint(plan.framework));
151
286
  return { ok: true, applied, manual };
152
287
  }
153
- /** Perform the apply-step side effects; return the targets whose side effect failed. */
288
+ /**
289
+ * Perform the apply-step side effects; return the targets whose side effect failed.
290
+ *
291
+ * Steps run in plan order, which puts the dependency install BEFORE everything that imports what it
292
+ * installs. If it fails, the wiring is skipped rather than applied: patching `next.config.ts` to
293
+ * import a `@reticlehq/next` that was never installed takes the dev server down with
294
+ * MODULE_NOT_FOUND, so the app stops booting *because* Reticle was installed. A skipped step is a
295
+ * message; a half-wired app is a broken project.
296
+ */
154
297
  function applyEffects(plan, io) {
155
298
  const failed = new Set();
299
+ const skipped = new Set();
300
+ let installFailed = false;
156
301
  for (const s of plan.steps) {
157
302
  if (s.status !== StepStatus.APPLY)
158
303
  continue;
304
+ if (installFailed && s.dependsOnInstall === true) {
305
+ skipped.add(s.target);
306
+ continue;
307
+ }
159
308
  if (s.write !== undefined)
160
309
  io.writeFile(s.write.path, s.write.content);
161
- if (s.exec !== undefined && !io.exec(s.exec.command, s.exec.args))
310
+ if (s.exec !== undefined && !io.exec(s.exec.command, s.exec.args)) {
162
311
  failed.add(s.target);
312
+ if (s.target === DEPS_TARGET)
313
+ installFailed = true;
314
+ }
163
315
  }
164
- return failed;
316
+ return { failed, skipped };
165
317
  }
166
318
  /**
167
319
  * Which STEP failed, from the step targets — not from an error string, which would carry paths.
@@ -178,6 +330,39 @@ function classifyInitFailure(failed) {
178
330
  return InitFailure.MCP_REGISTRATION;
179
331
  return InitFailure.OTHER;
180
332
  }
333
+ const AMBIGUOUS_HEADER = 'Several apps found in this workspace. Re-run `reticle init` inside the one you want:';
334
+ /**
335
+ * When the current directory is a workspace root with no app of its own, wire the app instead of the
336
+ * root. One candidate is wired silently (there is nothing to ask about); several are listed, because
337
+ * guessing which app someone meant is worse than one line of output.
338
+ * Returns null when there is nothing to redirect to — the caller then proceeds here as before.
339
+ */
340
+ function redirectToWorkspaceApp(options, io, pkgRaw) {
341
+ if (options.redirected === true)
342
+ return null;
343
+ const pkg = JSON.parse(pkgRaw);
344
+ const rootFiles = new Set(io.rootFiles());
345
+ const here = detect({
346
+ pkg: typeof pkg === 'object' && pkg !== null ? pkg : {},
347
+ configFiles: rootFiles,
348
+ lockfiles: new Set(),
349
+ });
350
+ if (here.framework !== Framework.HTML)
351
+ return null; // this directory IS the app
352
+ const apps = findWorkspaceApps(io);
353
+ const target = apps.length === 1 ? apps[0] : undefined;
354
+ if (target === undefined) {
355
+ if (apps.length === 0)
356
+ return null; // not a workspace — fall through to the normal HTML plan
357
+ io.print(AMBIGUOUS_HEADER);
358
+ for (const a of apps)
359
+ io.print(` ${a}`);
360
+ return { ok: false, applied: 0, manual: apps.length };
361
+ }
362
+ io.print(`No app in this directory — wiring ${target} instead.`);
363
+ io.print('');
364
+ return runInit({ ...options, cwd: join(options.cwd, target), redirected: true }, io.scoped(target));
365
+ }
181
366
  export function runInit(options, io) {
182
367
  const pkgRaw = io.readFile(PACKAGE_JSON);
183
368
  if (pkgRaw === null) {
@@ -187,9 +372,15 @@ export function runInit(options, io) {
187
372
  reportInitOutcome({ ok: false, reason: InitFailure.NO_PACKAGE_JSON });
188
373
  return { ok: false, applied: 0, manual: 0 };
189
374
  }
375
+ const redirected = redirectToWorkspaceApp(options, io, pkgRaw);
376
+ if (redirected !== null)
377
+ return redirected;
190
378
  const plan = buildPlan(gatherPlanInput(options, io, pkgRaw));
191
- const failed = options.dryRun ? new Set() : applyEffects(plan, io);
192
- const result = report(plan, options.dryRun, failed, io);
379
+ const effects = options.dryRun
380
+ ? { failed: new Set(), skipped: new Set() }
381
+ : applyEffects(plan, io);
382
+ const { failed, skipped } = effects;
383
+ const result = report(plan, options.dryRun, failed, skipped, io);
193
384
  // A dry run is a preview, not an outcome — reporting it would inflate both success and failure.
194
385
  if (!options.dryRun) {
195
386
  reportInitOutcome({
@@ -0,0 +1,18 @@
1
+ /**
2
+ * The `/reticle` slash command — the entry point the docs have been promising and nothing created.
3
+ *
4
+ * SKILL.md tells the user "Type `/reticle` anytime to verify the app" in three places, and `init`
5
+ * never wrote the file that makes it exist. So the one obvious way in was a command that silently did
6
+ * nothing in every tool. Claude Code and Cursor both read custom commands from a markdown file per
7
+ * command; the body is the prompt the agent receives.
8
+ *
9
+ * Deliberately SCOPED to one flow. A user installing Reticle has an existing app with many flows, and
10
+ * an agent told to "verify the app" will try to instrument all of them — slow, noisy, and nothing to
11
+ * see for minutes. One flow, driven where the human can watch it, is the whole difference between a
12
+ * two-minute first impression and an afternoon.
13
+ */
14
+ /** Claude Code reads project commands from `.claude/commands/<name>.md`. */
15
+ export declare const CLAUDE_COMMAND_PATH = ".claude/commands/reticle.md";
16
+ /** Cursor reads them from `.cursor/commands/<name>.md`. */
17
+ export declare const CURSOR_COMMAND_PATH = ".cursor/commands/reticle.md";
18
+ export declare const SLASH_COMMAND_BODY = "---\ndescription: Verify this app in the browser with Reticle \u2014 drive one real flow and report what happened.\n---\n\nVerify this running app with Reticle. Drive it; do not read the code and guess.\n\n## Pick ONE flow\n\nNot the whole app. Pick the single most important flow you can complete in a handful of steps \u2014 the\none a user would do first (sign in, search, add to cart, submit the form). If the user named a flow or\njust changed some code, use that one instead.\n\nState which flow you picked in one line before you start.\n\n## Drive it where the human can see\n\n1. `reticle_sessions` \u2014 find the connected tab. None? Tell the user to run their dev server and open\n the app, then stop. Reticle never starts a dev server.\n2. `reticle_snapshot` \u2014 read the accessibility tree and locate the elements the flow needs. Elements\n are addressable by role and name; you do **not** need `data-testid` to drive them.\n3. Walk the flow with `reticle_act_and_wait`, one step at a time. Narrate each step first \u2014 the human\n is watching the page, and the HUD shows what you say.\n4. After each step, check the effect with `reticle_assert` \u2014 not just that the click dispatched, but\n that the thing it was supposed to do actually happened.\n5. Finish with `reticle_console` and `reticle_network` to catch errors the DOM does not show.\n\n## Report\n\n- What you drove, step by step, and what each step actually produced.\n- Anything broken, with the `file:line` Reticle gave you.\n- If a step could not be verified, say so plainly. \"Unknown\" is a real answer; a green verdict you\n cannot back is not.\n\nDo not weaken or skip an assertion to make the run pass \u2014 that is a finding, not a fix.\n";
@@ -0,0 +1,52 @@
1
+ /**
2
+ * The `/reticle` slash command — the entry point the docs have been promising and nothing created.
3
+ *
4
+ * SKILL.md tells the user "Type `/reticle` anytime to verify the app" in three places, and `init`
5
+ * never wrote the file that makes it exist. So the one obvious way in was a command that silently did
6
+ * nothing in every tool. Claude Code and Cursor both read custom commands from a markdown file per
7
+ * command; the body is the prompt the agent receives.
8
+ *
9
+ * Deliberately SCOPED to one flow. A user installing Reticle has an existing app with many flows, and
10
+ * an agent told to "verify the app" will try to instrument all of them — slow, noisy, and nothing to
11
+ * see for minutes. One flow, driven where the human can watch it, is the whole difference between a
12
+ * two-minute first impression and an afternoon.
13
+ */
14
+ /** Claude Code reads project commands from `.claude/commands/<name>.md`. */
15
+ export const CLAUDE_COMMAND_PATH = '.claude/commands/reticle.md';
16
+ /** Cursor reads them from `.cursor/commands/<name>.md`. */
17
+ export const CURSOR_COMMAND_PATH = '.cursor/commands/reticle.md';
18
+ export const SLASH_COMMAND_BODY = `---
19
+ description: Verify this app in the browser with Reticle — drive one real flow and report what happened.
20
+ ---
21
+
22
+ Verify this running app with Reticle. Drive it; do not read the code and guess.
23
+
24
+ ## Pick ONE flow
25
+
26
+ Not the whole app. Pick the single most important flow you can complete in a handful of steps — the
27
+ one a user would do first (sign in, search, add to cart, submit the form). If the user named a flow or
28
+ just changed some code, use that one instead.
29
+
30
+ State which flow you picked in one line before you start.
31
+
32
+ ## Drive it where the human can see
33
+
34
+ 1. \`reticle_sessions\` — find the connected tab. None? Tell the user to run their dev server and open
35
+ the app, then stop. Reticle never starts a dev server.
36
+ 2. \`reticle_snapshot\` — read the accessibility tree and locate the elements the flow needs. Elements
37
+ are addressable by role and name; you do **not** need \`data-testid\` to drive them.
38
+ 3. Walk the flow with \`reticle_act_and_wait\`, one step at a time. Narrate each step first — the human
39
+ is watching the page, and the HUD shows what you say.
40
+ 4. After each step, check the effect with \`reticle_assert\` — not just that the click dispatched, but
41
+ that the thing it was supposed to do actually happened.
42
+ 5. Finish with \`reticle_console\` and \`reticle_network\` to catch errors the DOM does not show.
43
+
44
+ ## Report
45
+
46
+ - What you drove, step by step, and what each step actually produced.
47
+ - Anything broken, with the \`file:line\` Reticle gave you.
48
+ - If a step could not be verified, say so plainly. "Unknown" is a real answer; a green verdict you
49
+ cannot back is not.
50
+
51
+ Do not weaken or skip an assertion to make the run pass — that is a finding, not a fix.
52
+ `;
@@ -6,8 +6,42 @@
6
6
  export declare function viteManual(port: number | undefined): string;
7
7
  /** Next.js config wrap — always printed (we never auto-rewrite next.config). */
8
8
  export declare function nextConfigManual(configFile: string): string;
9
- /** The dev-only client component that connects Reticle after hydration. */
10
- export declare function nextReticleDevFile(port: number | undefined, projectId?: string): string;
9
+ /**
10
+ * The dev-only client component that connects Reticle after hydration.
11
+ *
12
+ * The token is not optional. The bridge requires a pairing token even on localhost, and unlike Vite
13
+ * (where the plugin injects it) a Next app has to carry it through `withReticle`, which publishes it
14
+ * as `NEXT_PUBLIC_RETICLE_TOKEN`. This file used to connect with only a projectId, so every Next
15
+ * setup ended at `bridge refused the connection: authentication failed` and no session ever appeared.
16
+ */
17
+ export declare function nextReticleDevFile(port: number | undefined, projectId?: string, testids?: readonly string[], stores?: readonly string[]): string;
18
+ /**
19
+ * Astro connect instructions.
20
+ *
21
+ * Astro is Vite-based but SSRs its own HTML, so the plugin's index.html injection never fires, and
22
+ * `vite` is not a direct dependency — so this used to fall through to the generic HTML advice, which
23
+ * tells you to add a connect to an "entry module" Astro does not have, or to bundle the SDK with
24
+ * esbuild. Both are wrong for Astro, and following either gets you nothing.
25
+ *
26
+ * Astro bundles a page `<script>`, so the bare import resolves there. The token has to be inlined by
27
+ * the config because there is no plugin in the page's path to inject it, and `build.target` has to be
28
+ * raised or Astro down-levels the modern SDK bundle and dies on a destructuring transform.
29
+ */
30
+ export declare function astroManual(port: number | undefined, projectId?: string): string;
31
+ /**
32
+ * The app-side dev module the Vite plugin imports by convention.
33
+ *
34
+ * `registerCapabilities` tells the agent what it can drive without guessing; `registerStore` is the
35
+ * one that matters most and the one we cannot write for you — detecting that an app depends on
36
+ * zustand is easy, knowing which module exports the store instance is not, and a wrong import here
37
+ * breaks the module everything else hangs off. So the store lines are generated COMMENTED, naming
38
+ * the libraries actually found in package.json, with the exact call to uncomment.
39
+ */
40
+ export declare function viteDevModuleFile(testids: readonly string[], stores: readonly string[]): string;
41
+ /** Where that module goes. Matches @reticlehq/vite-plugin's convention list. */
42
+ export declare const VITE_DEV_MODULE_PATH = "src/reticle-dev.ts";
43
+ /** Default root-layout path, used when no layout was found on disk (reporting only). */
44
+ export declare const NEXT_LAYOUT_PATH = "app/layout.tsx";
11
45
  /** Mount instruction for the root layout. */
12
46
  export declare const NEXT_LAYOUT_MANUAL = "Mount <ReticleDev /> in your root layout (app/layout.tsx), dev-only:\n\n import { ReticleDev } from './reticle-dev';\n // inside <body>:\n {process.env.NODE_ENV === 'development' ? <ReticleDev /> : null}";
13
47
  /**
@@ -23,7 +57,14 @@ export declare const SVELTEKIT_HOOKS_PATH = "src/hooks.client.ts";
23
57
  * Said to the user's face rather than discovered later. React, Next, Remix and Astro each have an
24
58
  * app and a CI gate; SvelteKit has neither, so "it generated some wiring" is not evidence it works.
25
59
  */
26
- export declare const UNVERIFIED_FRAMEWORK_NOTE = "Reticle has no SvelteKit app and no CI gate for one, so this wiring is untested \u2014 it may work, but nothing proves it and nothing will tell us if it breaks. Supported and gated today: Vite + React, Next.js, Remix, Astro, and plain HTML. If the hook does not register a session, please open an issue.";
60
+ /**
61
+ * Said out loud when the app does not render through React. The SDK is framework-agnostic — DOM,
62
+ * network, console and routing all still work — but `@reticlehq/react` is a React adapter, so
63
+ * component names and source mapping do not, and no CI gate covers this stack. Reporting all-green
64
+ * here is the one thing this project exists not to do.
65
+ */
66
+ export declare function unverifiedUiLibraryNote(library: string): string;
67
+ export declare const UNVERIFIED_FRAMEWORK_NOTE = "Reticle has no SvelteKit app and no CI gate for one, so this wiring is untested \u2014 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.";
27
68
  /**
28
69
  * Dev-only client hook that connects Reticle in a SvelteKit app. SvelteKit renders through app.html and
29
70
  * never triggers Vite's index.html injection (verified), so the standard plugin can't auto-connect —