@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.
- package/dist/bridge.js +43 -3
- package/dist/bridge.test-harness.d.ts +5 -0
- package/dist/bridge.test-harness.js +12 -0
- package/dist/cli-parse.d.ts +14 -2
- package/dist/cli-parse.js +40 -12
- package/dist/cli-port.d.ts +3 -0
- package/dist/cli-port.js +21 -0
- package/dist/cli.js +9 -2
- package/dist/flows/decision.d.ts +0 -6
- package/dist/flows/decision.js +47 -6
- package/dist/flows/flow-tools.js +14 -6
- package/dist/init/capabilities.d.ts +17 -0
- package/dist/init/capabilities.js +51 -0
- package/dist/init/detect.d.ts +25 -0
- package/dist/init/detect.js +65 -3
- package/dist/init/next-patch.d.ts +41 -0
- package/dist/init/next-patch.js +106 -0
- package/dist/init/node-io.js +25 -0
- package/dist/init/patch-kind.d.ts +20 -0
- package/dist/init/patch-kind.js +10 -0
- package/dist/init/plan-framework.d.ts +35 -0
- package/dist/init/plan-framework.js +217 -0
- package/dist/init/plan.d.ts +51 -3
- package/dist/init/plan.js +125 -125
- package/dist/init/run.d.ts +17 -0
- package/dist/init/run.js +207 -16
- package/dist/init/slash-command.d.ts +18 -0
- package/dist/init/slash-command.js +52 -0
- package/dist/init/snippets.d.ts +44 -3
- package/dist/init/snippets.js +152 -6
- package/dist/init/vite-config.d.ts +4 -10
- package/dist/init/vite-config.js +15 -7
- package/dist/input/driven-redaction.d.ts +15 -0
- package/dist/input/driven-redaction.js +76 -0
- package/dist/input/network-detail.d.ts +9 -3
- package/dist/input/network-detail.js +35 -15
- package/dist/mcp-proxy.d.ts +35 -3
- package/dist/mcp-proxy.js +174 -27
- package/dist/session/session-manager.js +6 -0
- package/dist/session/session.d.ts +6 -0
- package/dist/session/session.js +7 -0
- package/dist/tools/read-tools.js +4 -0
- 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
|
-
|
|
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
|
|
106
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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 =
|
|
135
|
-
?
|
|
136
|
-
: s.
|
|
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
|
-
/**
|
|
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
|
|
192
|
-
|
|
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
|
+
`;
|
package/dist/init/snippets.d.ts
CHANGED
|
@@ -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
|
-
/**
|
|
10
|
-
|
|
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
|
-
|
|
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 —
|