@reticlehq/init 3.7.0 → 3.8.1
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/detect/detect.d.ts +9 -0
- package/dist/detect/detect.js +5 -1
- package/dist/detect/dev-server-ports.d.ts +2 -0
- package/dist/detect/dev-server-ports.js +20 -0
- package/dist/detect/prettier-config.d.ts +21 -0
- package/dist/detect/prettier-config.js +38 -0
- package/dist/detect/workspace-redirect.js +23 -8
- package/dist/diagnose/closing-hint.d.ts +5 -0
- package/dist/diagnose/closing-hint.js +18 -11
- package/dist/gather-plan-input.js +70 -18
- package/dist/host.d.ts +6 -0
- package/dist/index.d.ts +8 -2
- package/dist/index.js +8 -2
- package/dist/no-package-json.js +62 -14
- package/dist/patch/next-patch.d.ts +19 -11
- package/dist/patch/next-patch.js +33 -6
- package/dist/patch/prettier-ignore.d.ts +34 -0
- package/dist/patch/prettier-ignore.js +49 -0
- package/dist/patch/python-snippets.d.ts +22 -0
- package/dist/patch/python-snippets.js +113 -0
- package/dist/patch/snippets.d.ts +9 -30
- package/dist/patch/snippets.js +40 -103
- package/dist/patch/static-page.js +3 -5
- package/dist/patch/vite-config.js +11 -0
- package/dist/plan/connect-steps.d.ts +1 -0
- package/dist/plan/connect-steps.js +1 -0
- package/dist/plan/framework-adapter.d.ts +10 -0
- package/dist/plan/framework-adapter.js +120 -1
- package/dist/plan/notices.js +1 -1
- package/dist/plan/plan-framework.js +101 -32
- package/dist/plan/plan-types.d.ts +27 -0
- package/dist/plan/plan.d.ts +1 -10
- package/dist/plan/plan.js +128 -131
- package/dist/plan/stop-hook-step.d.ts +11 -0
- package/dist/plan/stop-hook-step.js +82 -6
- package/dist/project/agent-rules.d.ts +1 -1
- package/dist/project/agent-rules.js +8 -8
- package/dist/project/refresh-rules.js +2 -1
- package/dist/register/prompt-hooks.d.ts +123 -0
- package/dist/register/prompt-hooks.js +449 -0
- package/dist/register/slash-command.d.ts +1 -1
- package/dist/register/slash-command.js +5 -4
- package/dist/run-types.d.ts +4 -0
- package/dist/run.js +4 -1
- package/package.json +2 -2
|
@@ -8,6 +8,7 @@
|
|
|
8
8
|
import type { Detection, Framework, UiLibrary } from '../detect/detect.js';
|
|
9
9
|
import type { FoundStore } from '../detect/capabilities.js';
|
|
10
10
|
import type { McpClient } from '../register/mcp-clients.js';
|
|
11
|
+
import type { PromptHookTarget } from '../register/prompt-hooks.js';
|
|
11
12
|
/** Exported so the init telemetry can tell an MCP-registration failure from a dependency install. */
|
|
12
13
|
export declare const MCP_TARGET = "global (claude user scope)";
|
|
13
14
|
/** The step that runs the package manager — the other thing that commonly fails on a user's machine. */
|
|
@@ -115,6 +116,11 @@ export interface PlanInput {
|
|
|
115
116
|
* handing it an io would let any later step reach the disk from inside `buildPlan`.
|
|
116
117
|
*/
|
|
117
118
|
cspSources?: Readonly<Record<string, string | undefined>> | undefined;
|
|
119
|
+
/**
|
|
120
|
+
* The project's `.prettierignore` content, pre-read so the prettier-ignore step stays pure.
|
|
121
|
+
* Null when the file does not exist — `init` creates it only when the project uses Prettier.
|
|
122
|
+
*/
|
|
123
|
+
prettierIgnoreSource?: string | null | undefined;
|
|
118
124
|
/** Whether the `claude` CLI is installed (so we can register the MCP server globally). */
|
|
119
125
|
claudeCli: boolean;
|
|
120
126
|
/** Whether an `reticle` MCP server is already registered with Claude (any scope) — idempotency. */
|
|
@@ -217,6 +223,11 @@ export interface PlanInput {
|
|
|
217
223
|
* when there is no top-level layout and no single group renders `/` (#1538).
|
|
218
224
|
*/
|
|
219
225
|
nextLayoutCandidates?: readonly string[] | undefined;
|
|
226
|
+
/**
|
|
227
|
+
* The other route-group root layouts when one was chosen to mount into, that do not mount
|
|
228
|
+
* `<ReticleDev />` themselves: the routes under them render without it.
|
|
229
|
+
*/
|
|
230
|
+
nextUnmountedLayouts?: readonly string[] | undefined;
|
|
220
231
|
/** Where the dev-only connect component goes — never inside `pages/`, which routes on presence. */
|
|
221
232
|
nextReticleDevPath?: string | undefined;
|
|
222
233
|
/** What the mount file should import — a sibling for App Router, `../components/…` for Pages. */
|
|
@@ -325,6 +336,14 @@ export interface PlanInput {
|
|
|
325
336
|
* see reticleConfigStep for the port that survived every re-run because of it.
|
|
326
337
|
*/
|
|
327
338
|
reticleConfigSource?: string | null | undefined;
|
|
339
|
+
/**
|
|
340
|
+
* `--new-id`: the app's source files that quote the id being replaced. A hand-written
|
|
341
|
+
* `connect({ projectId: '…' })` still naming it sends this checkout's evidence to the original.
|
|
342
|
+
*/
|
|
343
|
+
copiedIdSources?: readonly {
|
|
344
|
+
path: string;
|
|
345
|
+
source: string;
|
|
346
|
+
}[] | undefined;
|
|
328
347
|
/** Current project-root CLAUDE.md content (for the idempotent agent-rule merge), or null/undefined. */
|
|
329
348
|
claudeMdContent?: string | null | undefined;
|
|
330
349
|
/** Current project-root AGENTS.md content (the cross-agent rule), or null/undefined. */
|
|
@@ -347,6 +366,12 @@ export interface PlanInput {
|
|
|
347
366
|
hooks?: boolean | undefined;
|
|
348
367
|
/** Current .claude/settings.json content, or null when absent (the hook is MERGED into it). */
|
|
349
368
|
claudeSettingsContent?: string | null | undefined;
|
|
369
|
+
/** Install the prompt-capture hook into `.claude/settings.json`: Claude Code is used here. */
|
|
370
|
+
claudePromptHook?: boolean | undefined;
|
|
371
|
+
/** Every OTHER agent found here with a prompt hook, and its config as it is now. */
|
|
372
|
+
promptHooks?: readonly PromptHookTarget[] | undefined;
|
|
373
|
+
/** See `InitHost.hookBin`. */
|
|
374
|
+
hookBin?: string | undefined;
|
|
350
375
|
/**
|
|
351
376
|
* Absolute directory the AGENT runs in, when that is not the app's directory.
|
|
352
377
|
*
|
|
@@ -369,6 +394,8 @@ export interface PlanInput {
|
|
|
369
394
|
install: boolean;
|
|
370
395
|
/** Stable project identity derived at init (package.json name + root). Baked into snippets/.reticle.json. */
|
|
371
396
|
projectId?: string;
|
|
397
|
+
/** `--new-id`: the config on disk takes `projectId` above in place of the id it was copied with. */
|
|
398
|
+
newId?: boolean;
|
|
372
399
|
/** The CLI's own version, pinned onto the SDK install so a stale registry cache cannot skew it. */
|
|
373
400
|
sdkVersion?: string;
|
|
374
401
|
};
|
package/dist/plan/plan.d.ts
CHANGED
|
@@ -3,18 +3,9 @@
|
|
|
3
3
|
* contents, produce an ordered list of steps — each marked apply / manual / already / skip. The
|
|
4
4
|
* runner performs the `write` side-effects; this module decides *what* should happen.
|
|
5
5
|
*/
|
|
6
|
-
import { Framework, UiLibrary } from '../detect/detect.js';
|
|
7
6
|
import { type Plan, type PlanInput } from './plan-types.js';
|
|
8
7
|
export { DEPS_TARGET, MCP_TARGET, StepStatus, type Step, type Plan, type PlanInput, } from './plan-types.js';
|
|
9
8
|
import { RETICLE_CONFIG_FILE } from '../detect/existing-config.js';
|
|
10
9
|
export { RETICLE_CONFIG_FILE };
|
|
11
|
-
|
|
12
|
-
* The dev-dependencies `reticle init` installs — kit (or sensor) first, build plugin next.
|
|
13
|
-
*
|
|
14
|
-
* `uiLibrary` matters because the framework does not always name the renderer. The rule below was
|
|
15
|
-
* already written for Nuxt, correctly, and applied only there: a Vue app on plain Vite is
|
|
16
|
-
* `Framework.VITE` and a SvelteKit app renders Svelte, and both were being handed the React kit
|
|
17
|
-
* right after `init` had detected the real UI library and said so on screen.
|
|
18
|
-
*/
|
|
19
|
-
export declare function frameworkPackages(framework: Framework, uiLibrary?: UiLibrary): readonly string[];
|
|
10
|
+
export { frameworkPackages } from './framework-adapter.js';
|
|
20
11
|
export declare function buildPlan(input: PlanInput): Plan;
|
package/dist/plan/plan.js
CHANGED
|
@@ -3,9 +3,6 @@
|
|
|
3
3
|
* contents, produce an ordered list of steps — each marked apply / manual / already / skip. The
|
|
4
4
|
* runner performs the `write` side-effects; this module decides *what* should happen.
|
|
5
5
|
*/
|
|
6
|
-
import { Framework, UiLibrary, installCommand, installCommandParts } from '../detect/detect.js';
|
|
7
|
-
import { installFailureHint } from '../diagnose/install-hint.js';
|
|
8
|
-
import { installRetries } from '../diagnose/install-retries.js';
|
|
9
6
|
import { claudeAddCommand, claudeProjectMcpJson, codexAddCommand, mcpManual, } from '../register/mcp.js';
|
|
10
7
|
import { codexNamesOurServer } from '../register/codex-toml.js';
|
|
11
8
|
import { mergeClientConfig, ClientMergeStatus, clientSnippet, clientSpec, CLAUDE_PROJECT_CONFIG, CLAUDE_PROJECT_SPEC, McpClient, } from '../register/mcp-clients.js';
|
|
@@ -14,44 +11,20 @@ import { mergeMarkedInstruction, reticleMdFile, cursorRuleFile, AgentRuleStatus,
|
|
|
14
11
|
import { cspStep } from '../diagnose/csp-step.js';
|
|
15
12
|
import { HTML_INDEX_PATH } from '../patch/static-page.js';
|
|
16
13
|
import { frameworkSteps } from './framework-adapter.js';
|
|
17
|
-
import {
|
|
14
|
+
import { installStep } from './framework-adapter.js';
|
|
18
15
|
import { MCP_TARGET, StepStatus } from './plan-types.js';
|
|
19
16
|
export { DEPS_TARGET, MCP_TARGET, StepStatus, } from './plan-types.js';
|
|
20
17
|
import { join } from 'node:path';
|
|
21
|
-
import { reticleConfigContent
|
|
18
|
+
import { reticleConfigContent } from '../patch/snippets.js';
|
|
22
19
|
import { configWithInstallSource } from '../project/install-source-config.js';
|
|
23
20
|
import { portMoveDetail, retargetConfigPort } from './port-steps.js';
|
|
24
21
|
import { containerisedStep, uiLibraryStep, webGlCanvasStep, windowsMcpNoteStep, } from './notices.js';
|
|
25
22
|
import { existingConfigProblem, projectIdOf, RETICLE_CONFIG_FILE, } from '../detect/existing-config.js';
|
|
26
|
-
import { CLAUDE_SETTINGS_PATH, claudeSettingsSteps } from './stop-hook-step.js';
|
|
23
|
+
import { CLAUDE_SETTINGS_PATH, claudeSettingsSteps, promptHookSteps } from './stop-hook-step.js';
|
|
24
|
+
import { PRETTIERIGNORE_PATH, RETICLE_IGNORE_ENTRY, ignoresReticle, withReticleIgnored, } from '../patch/prettier-ignore.js';
|
|
27
25
|
// Re-exported: it moved to the module that reads it, and every existing importer says `plan.js`.
|
|
28
26
|
export { RETICLE_CONFIG_FILE };
|
|
29
|
-
|
|
30
|
-
* Pin the SDK to the CLI's own version.
|
|
31
|
-
*
|
|
32
|
-
* `pnpm add -D @reticlehq/react` once installed an older release in one project while npm and yarn
|
|
33
|
-
* took the current one in the next — a stale registry metadata cache, invisible to everyone. A
|
|
34
|
-
* version-skewed SDK talking to a newer daemon is the `-32000` failure path: the app connects, the
|
|
35
|
-
* protocol disagrees, and nothing on either side names a version. Asking for the CLI's exact version
|
|
36
|
-
* makes the cache irrelevant, and a skewed pair impossible to install by accident.
|
|
37
|
-
*/
|
|
38
|
-
function pinnedPackages(packages, version) {
|
|
39
|
-
if (version === undefined || 0 === version.length)
|
|
40
|
-
return packages;
|
|
41
|
-
return packages.map((p) => `${p}@${version}`);
|
|
42
|
-
}
|
|
43
|
-
/**
|
|
44
|
-
* The dev-dependencies `reticle init` installs — kit (or sensor) first, build plugin next.
|
|
45
|
-
*
|
|
46
|
-
* `uiLibrary` matters because the framework does not always name the renderer. The rule below was
|
|
47
|
-
* already written for Nuxt, correctly, and applied only there: a Vue app on plain Vite is
|
|
48
|
-
* `Framework.VITE` and a SvelteKit app renders Svelte, and both were being handed the React kit
|
|
49
|
-
* right after `init` had detected the real UI library and said so on screen.
|
|
50
|
-
*/
|
|
51
|
-
export function frameworkPackages(framework, uiLibrary = UiLibrary.UNKNOWN) {
|
|
52
|
-
const kit = usesReactKit(uiLibrary, framework) ? RETICLE_REACT_KIT : RETICLE_BROWSER_SDK;
|
|
53
|
-
return FRAMEWORK_ADAPTERS[framework].packages(kit);
|
|
54
|
-
}
|
|
27
|
+
export { frameworkPackages } from './framework-adapter.js';
|
|
55
28
|
/**
|
|
56
29
|
* Where the agent-facing files go, which is not always where the app is.
|
|
57
30
|
*
|
|
@@ -67,8 +40,9 @@ function agentFile(input, relPath) {
|
|
|
67
40
|
}
|
|
68
41
|
const CLAUDE_MCP_TITLE = 'MCP server (Claude, global)';
|
|
69
42
|
function claudeMcpStep(input) {
|
|
70
|
-
if (!input.claudeCli)
|
|
43
|
+
if (!input.claudeCli) {
|
|
71
44
|
return null;
|
|
45
|
+
}
|
|
72
46
|
if (input.mcpExists) {
|
|
73
47
|
return {
|
|
74
48
|
title: CLAUDE_MCP_TITLE,
|
|
@@ -110,10 +84,12 @@ function otherClientSteps(input) {
|
|
|
110
84
|
* leaves it alone (ALREADY) or hands over the block (MANUAL), exactly as before.
|
|
111
85
|
*/
|
|
112
86
|
function codexCliStep(spec, configPath, existing, input) {
|
|
113
|
-
if (McpClient.CODEX !== spec.id || true !== input.codexCli)
|
|
87
|
+
if (McpClient.CODEX !== spec.id || true !== input.codexCli) {
|
|
114
88
|
return null;
|
|
115
|
-
|
|
89
|
+
}
|
|
90
|
+
if (codexNamesOurServer(existing, spec.serversKey)) {
|
|
116
91
|
return null;
|
|
92
|
+
}
|
|
117
93
|
const cmd = codexAddCommand();
|
|
118
94
|
return {
|
|
119
95
|
title: `MCP server (${spec.label})`,
|
|
@@ -160,8 +136,9 @@ function clientStep(spec, configPath, existing) {
|
|
|
160
136
|
* they get the notice instead.
|
|
161
137
|
*/
|
|
162
138
|
function claudeProjectStep(input) {
|
|
163
|
-
if (input.claudeCli || true !== input.insideClaudeCode)
|
|
139
|
+
if (input.claudeCli || true !== input.insideClaudeCode) {
|
|
164
140
|
return null;
|
|
141
|
+
}
|
|
165
142
|
const path = agentFile(input, CLAUDE_PROJECT_CONFIG);
|
|
166
143
|
return clientStep(CLAUDE_PROJECT_SPEC, path, input.claudeProjectConfig ?? null);
|
|
167
144
|
}
|
|
@@ -227,8 +204,9 @@ function mcpSteps(input) {
|
|
|
227
204
|
});
|
|
228
205
|
}
|
|
229
206
|
const windowsNote = windowsMcpNoteStep(input);
|
|
230
|
-
if (windowsNote !== null)
|
|
207
|
+
if (windowsNote !== null) {
|
|
231
208
|
steps.push(windowsNote);
|
|
209
|
+
}
|
|
232
210
|
return steps;
|
|
233
211
|
}
|
|
234
212
|
const SLASH_COMMAND_TITLE = 'The /reticle command';
|
|
@@ -240,8 +218,9 @@ const SLASH_COMMAND_TITLE = 'The /reticle command';
|
|
|
240
218
|
* our signature is left exactly as it is.
|
|
241
219
|
*/
|
|
242
220
|
function commandIsSettled(content) {
|
|
243
|
-
if (null === content || content === undefined)
|
|
221
|
+
if (null === content || content === undefined) {
|
|
244
222
|
return false;
|
|
223
|
+
}
|
|
245
224
|
return content === SLASH_COMMAND_BODY || !content.includes(SLASH_COMMAND_SIGNATURE);
|
|
246
225
|
}
|
|
247
226
|
/**
|
|
@@ -253,8 +232,9 @@ function commandIsSettled(content) {
|
|
|
253
232
|
* whole file is Reticle's, so a stale one is rewritten.
|
|
254
233
|
*/
|
|
255
234
|
function commandStepFor(path, present, content) {
|
|
256
|
-
if (!present)
|
|
235
|
+
if (!present) {
|
|
257
236
|
return null;
|
|
237
|
+
}
|
|
258
238
|
return commandIsSettled(content)
|
|
259
239
|
? {
|
|
260
240
|
title: SLASH_COMMAND_TITLE,
|
|
@@ -297,18 +277,34 @@ const AGENT_RULE_DETAIL = 'teach the agent to verify features with Reticle after
|
|
|
297
277
|
*/
|
|
298
278
|
function ruleWriteDetail(existing, file) {
|
|
299
279
|
const hasContent = existing !== null && existing !== undefined && existing.trim().length > 0;
|
|
300
|
-
if (!hasContent)
|
|
280
|
+
if (!hasContent) {
|
|
301
281
|
return `${AGENT_RULE_DETAIL} — creates ${file}`;
|
|
282
|
+
}
|
|
302
283
|
const added = markedBlock().split('\n').length - 1;
|
|
303
284
|
return (`${AGENT_RULE_DETAIL} — APPENDS ${String(added)} lines to your existing ${file}, ` +
|
|
304
285
|
'inside `reticle:begin`/`reticle:end` markers; delete the marked block to undo');
|
|
305
286
|
}
|
|
306
287
|
const RETICLE_MD_TITLE = 'Full agent rules';
|
|
307
288
|
const RETICLE_MD_DETAIL = 'the reference the always-loaded rule points at (when NOT to verify, recovery, feedback)';
|
|
289
|
+
/**
|
|
290
|
+
* A line that is only an import of AGENTS.md (`@AGENTS.md`, `@./AGENTS.md`). Claude Code reads the
|
|
291
|
+
* imported file as part of CLAUDE.md, so the rule `agentsMdStep` writes there already reaches it, and
|
|
292
|
+
* a second copy in CLAUDE.md would load the rule twice on every turn (#1680).
|
|
293
|
+
*/
|
|
294
|
+
const IMPORTS_AGENTS_MD = /^[ \t]*@(?:\.\/)?AGENTS\.md[ \t]*\r?$/m;
|
|
308
295
|
function claudeRuleStep(input) {
|
|
309
|
-
if (!input.claudeCli)
|
|
296
|
+
if (!input.claudeCli) {
|
|
310
297
|
return null;
|
|
298
|
+
}
|
|
311
299
|
const path = agentFile(input, CLAUDE_MD_PATH);
|
|
300
|
+
if (IMPORTS_AGENTS_MD.test(input.claudeMdContent ?? '')) {
|
|
301
|
+
return {
|
|
302
|
+
title: AGENT_RULE_TITLE,
|
|
303
|
+
target: path,
|
|
304
|
+
status: StepStatus.ALREADY,
|
|
305
|
+
detail: `CLAUDE.md imports ${AGENTS_MD_PATH}, so the Reticle rule written there reaches Claude Code through the import; no second copy`,
|
|
306
|
+
};
|
|
307
|
+
}
|
|
312
308
|
const r = mergeMarkedInstruction(input.claudeMdContent);
|
|
313
309
|
if (r.status === AgentRuleStatus.ALREADY) {
|
|
314
310
|
return {
|
|
@@ -334,10 +330,12 @@ function claudeRuleStep(input) {
|
|
|
334
330
|
*/
|
|
335
331
|
function cursorRuleStep(input) {
|
|
336
332
|
const cursorGlobal = (input.detectedClients ?? []).some((c) => c.id === McpClient.CURSOR);
|
|
337
|
-
if (!cursorGlobal && true !== input.cursorProjectPresent)
|
|
333
|
+
if (!cursorGlobal && true !== input.cursorProjectPresent) {
|
|
338
334
|
return null;
|
|
339
|
-
|
|
335
|
+
}
|
|
336
|
+
if (input.cursorProjectPresent !== true && input.claudeCli) {
|
|
340
337
|
return null;
|
|
338
|
+
}
|
|
341
339
|
// The whole file is Reticle's — init created it — so a stale one is REWRITTEN rather than merged.
|
|
342
340
|
// Comparing content is what makes the rule updatable; comparing existence made it permanent.
|
|
343
341
|
const path = agentFile(input, CURSOR_RULE_PATH);
|
|
@@ -458,95 +456,44 @@ function reticleMdStep(input) {
|
|
|
458
456
|
* noise.
|
|
459
457
|
*/
|
|
460
458
|
function agentRuleSteps(input) {
|
|
461
|
-
if (!input.options.mcp)
|
|
459
|
+
if (!input.options.mcp) {
|
|
462
460
|
return [];
|
|
461
|
+
}
|
|
463
462
|
return [...stepsForAgents(input, (a) => a.ruleStep), agentsMdStep(input), reticleMdStep(input)];
|
|
464
463
|
}
|
|
465
464
|
/**
|
|
466
|
-
*
|
|
467
|
-
*
|
|
468
|
-
* pnpm's `minimumReleaseAge` refuses any release younger than the configured window — a deliberate
|
|
469
|
-
* supply-chain policy, not a bug — with `ERR_PNPM_NO_MATURE_MATCHING_VERSION`. Unpinned it silently
|
|
470
|
-
* resolves to an OLDER version instead, which is how an app ends up running a stale SDK against a
|
|
471
|
-
* newer daemon: the connection succeeds, the protocol disagrees, and the failure surfaces as -32000
|
|
472
|
-
* with nothing naming a version. Pinning turns that into this loud failure, which is the better
|
|
473
|
-
* trade — but only if the message says what to do about it.
|
|
474
|
-
*/
|
|
475
|
-
/**
|
|
476
|
-
* Is this package already declared at the version we would pin?
|
|
465
|
+
* Keep `.reticle/` out of the project's `prettier --check`.
|
|
477
466
|
*
|
|
478
|
-
*
|
|
479
|
-
*
|
|
480
|
-
*
|
|
481
|
-
*
|
|
467
|
+
* When the project uses Prettier, `init` adds `.reticle/` to the project's `.prettierignore`.
|
|
468
|
+
* The nested `.reticle/.gitignore` does not help — Prettier only reads the ignore file of the
|
|
469
|
+
* directory it runs from — and matching Prettier's exact JSON output is a losing game, so
|
|
470
|
+
* excluding the directory is the root-cause fix (#1560).
|
|
482
471
|
*
|
|
483
|
-
*
|
|
484
|
-
*
|
|
472
|
+
* SKIP when the project does not use Prettier. ALREADY when `.reticle/` is already excluded.
|
|
473
|
+
* Otherwise APPLY: create the file, or append the entry to the existing one.
|
|
485
474
|
*/
|
|
486
|
-
function
|
|
487
|
-
if (
|
|
488
|
-
return
|
|
489
|
-
if (pinned === undefined || 0 === pinned.length)
|
|
490
|
-
return true;
|
|
491
|
-
return declared === pinned || `^${pinned}` === declared || `~${pinned}` === declared;
|
|
492
|
-
}
|
|
493
|
-
function installStep(input) {
|
|
494
|
-
const pm = input.detection.packageManager;
|
|
495
|
-
// `Detection.packageManagerCommand` (see `preflight.ts`, #1149). Falling back to `pm` covers every
|
|
496
|
-
// caller that never went through preflight (tests, and any future one).
|
|
497
|
-
const pmCommand = input.detection.packageManagerCommand ?? pm;
|
|
498
|
-
const packages = pinnedPackages(frameworkPackages(input.detection.framework, input.detection.uiLibrary), input.options.sdkVersion);
|
|
499
|
-
const command = installCommand(pm, packages, pmCommand);
|
|
500
|
-
/*
|
|
501
|
-
* A re-run over an already-wired project does NO dependency work.
|
|
502
|
-
*
|
|
503
|
-
* Reported from the field: `init` on an instrumented npm-workspaces project ran a pnpm dependency
|
|
504
|
-
* migration, which moved the existing `node_modules` aside and broke the dev server. Whatever
|
|
505
|
-
* manager gets chosen, the redundant install is the step that touches the tree — so the fix that
|
|
506
|
-
* matters is not running one that nothing needs. `init` is documented as idempotent; this is the
|
|
507
|
-
* step that was not.
|
|
508
|
-
*/
|
|
509
|
-
const declared = input.detection.dependencies ?? {};
|
|
510
|
-
const version = input.options.sdkVersion;
|
|
511
|
-
const names = frameworkPackages(input.detection.framework, input.detection.uiLibrary);
|
|
512
|
-
if (names.length > 0 && names.every((n) => alreadyDeclared(declared[n], version))) {
|
|
513
|
-
return {
|
|
514
|
-
title: 'Install dependencies',
|
|
515
|
-
target: 'package.json',
|
|
516
|
-
status: StepStatus.ALREADY,
|
|
517
|
-
detail: `${names.join(', ')} already declared${version === undefined || 0 === version.length ? '' : ` at ${version}`}`,
|
|
518
|
-
};
|
|
519
|
-
}
|
|
520
|
-
// Never install into a linked tree: the package manager replaces the link (a workspace's tree).
|
|
521
|
-
if (true === input.nodeModulesLinked) {
|
|
522
|
-
return {
|
|
523
|
-
title: 'Install dependencies',
|
|
524
|
-
target: 'package.json',
|
|
525
|
-
status: StepStatus.SKIP,
|
|
526
|
-
detail: `skipped: node_modules is a link to another tree, which an install here would replace; install them where it points: ${command}`,
|
|
527
|
-
};
|
|
475
|
+
function prettierIgnoreStep(input) {
|
|
476
|
+
if (true !== input.detection.usesPrettier) {
|
|
477
|
+
return null;
|
|
528
478
|
}
|
|
529
|
-
|
|
530
|
-
if (
|
|
479
|
+
const existing = input.prettierIgnoreSource ?? null;
|
|
480
|
+
if (null !== existing && ignoresReticle(existing)) {
|
|
531
481
|
return {
|
|
532
|
-
title: '
|
|
533
|
-
target:
|
|
534
|
-
status: StepStatus.
|
|
535
|
-
detail:
|
|
482
|
+
title: 'Prettier ignore',
|
|
483
|
+
target: PRETTIERIGNORE_PATH,
|
|
484
|
+
status: StepStatus.ALREADY,
|
|
485
|
+
detail: '.reticle/ already excluded from Prettier',
|
|
536
486
|
};
|
|
537
487
|
}
|
|
538
|
-
const
|
|
488
|
+
const content = withReticleIgnored(existing);
|
|
539
489
|
return {
|
|
540
|
-
title: '
|
|
541
|
-
target:
|
|
490
|
+
title: 'Prettier ignore',
|
|
491
|
+
target: PRETTIERIGNORE_PATH,
|
|
542
492
|
status: StepStatus.APPLY,
|
|
543
|
-
detail:
|
|
544
|
-
|
|
545
|
-
|
|
546
|
-
|
|
547
|
-
fallback: `${command}\n\n${installFailureHint(pm, pmCommand)}`,
|
|
548
|
-
},
|
|
549
|
-
retries: installRetries(pm, packages, frameworkPackages(input.detection.framework, input.detection.uiLibrary), input.options.sdkVersion, pmCommand),
|
|
493
|
+
detail: null === existing
|
|
494
|
+
? 'create .prettierignore excluding .reticle/ so `prettier --check` passes'
|
|
495
|
+
: 'add .reticle/ to .prettierignore so `prettier --check` passes',
|
|
496
|
+
write: { path: PRETTIERIGNORE_PATH, content, expect: [RETICLE_IGNORE_ENTRY] },
|
|
550
497
|
};
|
|
551
498
|
}
|
|
552
499
|
const RETICLE_CONFIG_TITLE = 'Reticle config';
|
|
@@ -565,12 +512,15 @@ const AGENT_ROOT_CONFIG_TITLE = 'Reticle config (agent root)';
|
|
|
565
512
|
* and the two files are byte-identical, so neither can disagree with the other about the port or the
|
|
566
513
|
* project identity.
|
|
567
514
|
*/
|
|
568
|
-
function agentRootConfigStep(input, content) {
|
|
515
|
+
function agentRootConfigStep(input, content, renamedFrom) {
|
|
569
516
|
const root = input.agentFileRoot;
|
|
570
|
-
if (root === undefined || 0 === root.length)
|
|
517
|
+
if (root === undefined || 0 === root.length) {
|
|
571
518
|
return [];
|
|
519
|
+
}
|
|
572
520
|
const path = agentFile(input, RETICLE_CONFIG_FILE);
|
|
573
|
-
const
|
|
521
|
+
const declared = projectIdOf(input.agentRootConfigSource);
|
|
522
|
+
// The root's copy of the id `--new-id` just replaced is this checkout's, not another app's.
|
|
523
|
+
const existingProject = declared === renamedFrom ? undefined : declared;
|
|
574
524
|
const wantedProject = projectIdOf(content);
|
|
575
525
|
/**
|
|
576
526
|
* A monorepo has more than one app and the root can only point at one of them.
|
|
@@ -634,7 +584,7 @@ function agentRootConfigStep(input, content) {
|
|
|
634
584
|
},
|
|
635
585
|
];
|
|
636
586
|
}
|
|
637
|
-
function reticleConfigStep(input, content, onDisk, movedFrom) {
|
|
587
|
+
function reticleConfigStep(input, content, onDisk, movedFrom, renamedFrom) {
|
|
638
588
|
if (true === input.reticleConfigExists) {
|
|
639
589
|
const problem = existingConfigProblem(onDisk);
|
|
640
590
|
if (problem !== undefined) {
|
|
@@ -650,15 +600,17 @@ function reticleConfigStep(input, content, onDisk, movedFrom) {
|
|
|
650
600
|
// The one thing a re-run can still learn: which channel the user actually arrived through.
|
|
651
601
|
// See configWithInstallSource — it only ever ADDS a field that is absent.
|
|
652
602
|
const backfilled = configWithInstallSource(onDisk, input.installSource);
|
|
653
|
-
if (backfilled !== undefined || movedFrom !== undefined) {
|
|
603
|
+
if (backfilled !== undefined || movedFrom !== undefined || renamedFrom !== undefined) {
|
|
654
604
|
const port = input.options.port;
|
|
655
605
|
return {
|
|
656
606
|
title: RETICLE_CONFIG_TITLE,
|
|
657
607
|
target: RETICLE_CONFIG_FILE,
|
|
658
608
|
status: StepStatus.APPLY,
|
|
659
|
-
detail:
|
|
660
|
-
?
|
|
661
|
-
:
|
|
609
|
+
detail: renamedFrom !== undefined
|
|
610
|
+
? `this checkout's own project id: ${String(input.options.projectId)} (was ${renamedFrom}, the id it was copied with)`
|
|
611
|
+
: movedFrom !== undefined && port !== undefined
|
|
612
|
+
? portMoveDetail(movedFrom, port)
|
|
613
|
+
: 'record which install route this project came through',
|
|
662
614
|
write: { path: RETICLE_CONFIG_FILE, content: backfilled ?? onDisk ?? content },
|
|
663
615
|
};
|
|
664
616
|
}
|
|
@@ -686,11 +638,27 @@ function reticleConfigStep(input, content, onDisk, movedFrom) {
|
|
|
686
638
|
* requested port, and a config still naming the old one is half of a split brain (see
|
|
687
639
|
* port-steps.ts). Only the port moves; the identity stays the one already on disk.
|
|
688
640
|
*/
|
|
641
|
+
/**
|
|
642
|
+
* `--new-id`: the config with `projectId` replaced by the one derived for THIS checkout, or null when
|
|
643
|
+
* there is nothing to replace. A copied repository keeps the original's `.reticle.json`, and init
|
|
644
|
+
* otherwise keeps a config on disk as it is, so nothing else gives the copy an id of its own.
|
|
645
|
+
*/
|
|
646
|
+
function withOwnProjectId(source, options) {
|
|
647
|
+
const id = options.projectId;
|
|
648
|
+
const from = projectIdOf(source);
|
|
649
|
+
if (true !== options.newId || id === undefined || from === undefined || from === id) {
|
|
650
|
+
return null;
|
|
651
|
+
}
|
|
652
|
+
const fields = JSON.parse(source);
|
|
653
|
+
return { code: `${JSON.stringify({ ...fields, projectId: id }, null, 2)}\n`, from };
|
|
654
|
+
}
|
|
689
655
|
function reticleConfigSteps(input) {
|
|
690
656
|
const existing = input.reticleConfigSource;
|
|
691
|
-
const
|
|
657
|
+
const copied = true === input.reticleConfigExists && null !== existing && existing !== undefined
|
|
692
658
|
? existing
|
|
693
659
|
: undefined;
|
|
660
|
+
const renamed = copied === undefined ? null : withOwnProjectId(copied, input.options);
|
|
661
|
+
const onDisk = renamed?.code ?? copied;
|
|
694
662
|
const moved = onDisk === undefined ? null : retargetConfigPort(onDisk, input.options.port);
|
|
695
663
|
const content = moved?.code ??
|
|
696
664
|
onDisk ??
|
|
@@ -699,12 +667,36 @@ function reticleConfigSteps(input) {
|
|
|
699
667
|
// config written before this field existed, and the two mean different things.
|
|
700
668
|
input.installSource);
|
|
701
669
|
return [
|
|
702
|
-
reticleConfigStep(input, content, moved?.code ??
|
|
703
|
-
...agentRootConfigStep(input, content),
|
|
670
|
+
reticleConfigStep(input, content, moved?.code ?? onDisk, moved?.from, renamed?.from),
|
|
671
|
+
...agentRootConfigStep(input, content, renamed?.from),
|
|
672
|
+
...(null === renamed ? [] : copiedIdSteps(input, renamed.from)),
|
|
704
673
|
];
|
|
705
674
|
}
|
|
675
|
+
/**
|
|
676
|
+
* `--new-id`: the old id where the app quotes it by hand, as in `connect({ projectId: '…' })`. Left
|
|
677
|
+
* there, the copy's evidence goes on into the original checkout. Only the whole quoted literal moves:
|
|
678
|
+
* the id is a hash-suffixed name nothing else spells.
|
|
679
|
+
*/
|
|
680
|
+
function copiedIdSteps(input, from) {
|
|
681
|
+
const id = input.options.projectId;
|
|
682
|
+
if (id === undefined) {
|
|
683
|
+
return [];
|
|
684
|
+
}
|
|
685
|
+
return (input.copiedIdSources ?? []).map(({ path, source }) => ({
|
|
686
|
+
title: RETICLE_CONFIG_TITLE,
|
|
687
|
+
target: path,
|
|
688
|
+
status: StepStatus.APPLY,
|
|
689
|
+
detail: `the project id the app names by hand: ${id} (was ${from})`,
|
|
690
|
+
write: {
|
|
691
|
+
path,
|
|
692
|
+
content: ['"', "'", '`'].reduce((code, q) => code.replaceAll(`${q}${from}${q}`, `${q}${id}${q}`), source),
|
|
693
|
+
expect: [id],
|
|
694
|
+
},
|
|
695
|
+
}));
|
|
696
|
+
}
|
|
706
697
|
export function buildPlan(input) {
|
|
707
698
|
const cspSteps = cspStep(input);
|
|
699
|
+
const prettierIgnore = prettierIgnoreStep(input);
|
|
708
700
|
const steps = [
|
|
709
701
|
...cspSteps,
|
|
710
702
|
...mcpSteps(input),
|
|
@@ -714,11 +706,15 @@ export function buildPlan(input) {
|
|
|
714
706
|
stop: true === input.hooks,
|
|
715
707
|
// Beside the CLAUDE.md rule: the same agent, the same consent to write its instruction files.
|
|
716
708
|
compact: input.options.mcp && input.claudeCli,
|
|
709
|
+
prompt: true === input.claudePromptHook,
|
|
710
|
+
bin: input.hookBin,
|
|
717
711
|
}),
|
|
712
|
+
...promptHookSteps(input.promptHooks ?? [], true === input.claudePromptHook, input.hookBin),
|
|
718
713
|
...uiLibraryStep(input),
|
|
719
714
|
...webGlCanvasStep(input),
|
|
720
715
|
installStep(input),
|
|
721
716
|
...reticleConfigSteps(input),
|
|
717
|
+
...(prettierIgnore !== null ? [prettierIgnore] : []),
|
|
722
718
|
];
|
|
723
719
|
// Both steps can patch index.html: the connect write must keep the CSP edit that runs first.
|
|
724
720
|
const patchedIndex = cspSteps.find((step) => step.write?.path === HTML_INDEX_PATH)?.write
|
|
@@ -727,7 +723,8 @@ export function buildPlan(input) {
|
|
|
727
723
|
// LAST, and a notice rather than an action: it is a statement about this machine's shape, and it
|
|
728
724
|
// only matters once every file above has been written. See containerised-dev-server.ts.
|
|
729
725
|
const container = containerisedStep(input);
|
|
730
|
-
if (container !== null)
|
|
726
|
+
if (container !== null) {
|
|
731
727
|
steps.push(container);
|
|
728
|
+
}
|
|
732
729
|
return { framework: input.detection.framework, uiLibrary: input.detection.uiLibrary, steps };
|
|
733
730
|
}
|
|
@@ -1,4 +1,5 @@
|
|
|
1
1
|
import { type Step } from './plan-types.js';
|
|
2
|
+
import { type PromptHookTarget } from '../register/prompt-hooks.js';
|
|
2
3
|
export declare const CLAUDE_SETTINGS_PATH = ".claude/settings.json";
|
|
3
4
|
export declare const STOP_HOOK_COMMAND = "npx @reticlehq/server report --hook";
|
|
4
5
|
export declare function stopHookStep(path: string, existing: string | null | undefined): Step;
|
|
@@ -20,4 +21,14 @@ export declare function compactHookStep(path: string, existing: string | null |
|
|
|
20
21
|
export declare function claudeSettingsSteps(path: string, existing: string | null | undefined, options: {
|
|
21
22
|
stop: boolean;
|
|
22
23
|
compact: boolean;
|
|
24
|
+
prompt?: boolean;
|
|
25
|
+
bin?: string | undefined;
|
|
23
26
|
}): Step[];
|
|
27
|
+
/**
|
|
28
|
+
* The consent line. Names all that the agent hooks keep, what leaves with a linked run and under
|
|
29
|
+
* which setting, and the one switch that stops all of it.
|
|
30
|
+
*/
|
|
31
|
+
export declare const PROMPT_CAPTURE_NOTICE: string;
|
|
32
|
+
export declare function promptHookStep(target: PromptHookTarget): Step;
|
|
33
|
+
/** The steps for every detected agent, then the one consent line when any hook is in place. */
|
|
34
|
+
export declare function promptHookSteps(targets: readonly PromptHookTarget[], claude: boolean, bin?: string): Step[];
|
|
@@ -1,12 +1,14 @@
|
|
|
1
1
|
/**
|
|
2
|
-
* `init --hooks`: the
|
|
2
|
+
* `init --hooks`: the Claude Code Stop hook, merged into `.claude/settings.json`.
|
|
3
3
|
*
|
|
4
4
|
* Opt-in, because it writes into a file the user and other tools own. When the agent stops without a
|
|
5
|
-
* single claim having held, the hook prints one line saying so
|
|
6
|
-
* itself is `reticle report --hook`, which reads the session journal on disk.
|
|
5
|
+
* single claim having held, the hook prints one line saying so and does not block. The line
|
|
6
|
+
* itself is `reticle report --hook`, which reads the session journal on disk. It blocks the stop in one
|
|
7
|
+
* case: a Harness drive the agent started still runs, so the agent stays to read its result.
|
|
7
8
|
*/
|
|
8
9
|
import { RETICLE_NPM_PACKAGE } from '../version.js';
|
|
9
10
|
import { StepStatus } from './plan-types.js';
|
|
11
|
+
import { PROMPT_HOOK_AGENTS, PromptHookAgent, PromptHookStatus, installPromptHook, promptHookCommand, } from '../register/prompt-hooks.js';
|
|
10
12
|
export const CLAUDE_SETTINGS_PATH = '.claude/settings.json';
|
|
11
13
|
export const STOP_HOOK_COMMAND = `npx ${RETICLE_NPM_PACKAGE} report --hook`;
|
|
12
14
|
const TITLE = 'Claude Code Stop hook';
|
|
@@ -48,7 +50,7 @@ export function stopHookStep(path, existing) {
|
|
|
48
50
|
title: TITLE,
|
|
49
51
|
target: path,
|
|
50
52
|
status: StepStatus.APPLY,
|
|
51
|
-
detail: 'print one line when the agent stops without a claim that held;
|
|
53
|
+
detail: 'print one line when the agent stops without a claim that held; blocks only while its Harness drive runs',
|
|
52
54
|
write: { path, content: `${JSON.stringify(merged, null, 2)}\n`, expect: [STOP_HOOK_COMMAND] },
|
|
53
55
|
};
|
|
54
56
|
}
|
|
@@ -116,7 +118,81 @@ export function claudeSettingsSteps(path, existing, options) {
|
|
|
116
118
|
steps.push(stop);
|
|
117
119
|
current = stop.write?.content ?? current;
|
|
118
120
|
}
|
|
119
|
-
if (options.compact)
|
|
120
|
-
|
|
121
|
+
if (options.compact) {
|
|
122
|
+
const compact = compactHookStep(path, current);
|
|
123
|
+
steps.push(compact);
|
|
124
|
+
current = compact.write?.content ?? current;
|
|
125
|
+
}
|
|
126
|
+
const claude = PROMPT_HOOK_AGENTS.find((agent) => PromptHookAgent.CLAUDE_CODE === agent.id);
|
|
127
|
+
if (true === options.prompt && claude !== undefined) {
|
|
128
|
+
steps.push(promptHookStep({
|
|
129
|
+
spec: claude,
|
|
130
|
+
path,
|
|
131
|
+
existing: current ?? null,
|
|
132
|
+
...(options.bin === undefined ? {} : { bin: options.bin }),
|
|
133
|
+
}));
|
|
134
|
+
}
|
|
121
135
|
return steps;
|
|
122
136
|
}
|
|
137
|
+
/**
|
|
138
|
+
* The prompt-capture hook, for each agent found. On by default, because the request is what a
|
|
139
|
+
* verdict is checked against and an agent forgets to relay it. `--no-prompt-capture` skips it;
|
|
140
|
+
* `"promptCapture": false` in `.reticle.json` keeps every agent hook from writing; `reticle hook uninstall`
|
|
141
|
+
* removes exactly what this added.
|
|
142
|
+
*/
|
|
143
|
+
const PROMPT_TITLE = 'Prompt capture';
|
|
144
|
+
const NOTICE_TARGET = '.reticle/request.json';
|
|
145
|
+
/**
|
|
146
|
+
* The consent line. Names all that the agent hooks keep, what leaves with a linked run and under
|
|
147
|
+
* which setting, and the one switch that stops all of it.
|
|
148
|
+
*/
|
|
149
|
+
export const PROMPT_CAPTURE_NOTICE = 'Reticle keeps these in .reticle/ in this project, with secrets removed: each prompt you send, the tool calls your agent makes (commands, file edits, web calls), its model, and its closing message for each turn. ' +
|
|
150
|
+
'Tool calls stay on this machine. ' +
|
|
151
|
+
'When the project is linked (reticle connect, or RETICLE_API_KEY in the environment), each synced run sends the model to your own workspace. ' +
|
|
152
|
+
'Your prompts and the closing messages also go there with the runs, and to the Harness, unless "shareRequests": false is in .reticle.json. ' +
|
|
153
|
+
'Stop all of it: "promptCapture": false in .reticle.json, or RETICLE_PROMPT_CAPTURE=0 on this machine.';
|
|
154
|
+
export function promptHookStep(target) {
|
|
155
|
+
const { spec, path, existing, bin } = target;
|
|
156
|
+
const title = `${PROMPT_TITLE}: ${spec.label}`;
|
|
157
|
+
const merged = installPromptHook(spec, existing, bin);
|
|
158
|
+
const then = spec.activation === undefined ? '' : `. ${spec.activation}`;
|
|
159
|
+
if (PromptHookStatus.ALREADY === merged.status) {
|
|
160
|
+
return {
|
|
161
|
+
title,
|
|
162
|
+
target: path,
|
|
163
|
+
status: StepStatus.ALREADY,
|
|
164
|
+
detail: `hook already installed${then}`,
|
|
165
|
+
};
|
|
166
|
+
}
|
|
167
|
+
if (PromptHookStatus.MANUAL === merged.status) {
|
|
168
|
+
return {
|
|
169
|
+
title,
|
|
170
|
+
target: path,
|
|
171
|
+
status: StepStatus.MANUAL,
|
|
172
|
+
detail: `${path} is not plain JSON, so it was left alone. Add a prompt-submit hook that runs \`${promptHookCommand(spec.id)}\` (${spec.docs})`,
|
|
173
|
+
};
|
|
174
|
+
}
|
|
175
|
+
return {
|
|
176
|
+
title,
|
|
177
|
+
target: path,
|
|
178
|
+
status: StepStatus.APPLY,
|
|
179
|
+
detail: `save each prompt, tool call, model and closing message to .reticle/; never blocks the agent${then}`,
|
|
180
|
+
write: { path, content: merged.content, expect: [promptHookCommand(spec.id)] },
|
|
181
|
+
};
|
|
182
|
+
}
|
|
183
|
+
/** The steps for every detected agent, then the one consent line when any hook is in place. */
|
|
184
|
+
export function promptHookSteps(targets, claude, bin) {
|
|
185
|
+
const steps = targets.map((target) => promptHookStep(bin === undefined ? target : { ...target, bin }));
|
|
186
|
+
if (0 === steps.length && !claude) {
|
|
187
|
+
return steps;
|
|
188
|
+
}
|
|
189
|
+
return [
|
|
190
|
+
...steps,
|
|
191
|
+
{
|
|
192
|
+
title: PROMPT_TITLE,
|
|
193
|
+
target: NOTICE_TARGET,
|
|
194
|
+
status: StepStatus.NOTICE,
|
|
195
|
+
detail: PROMPT_CAPTURE_NOTICE,
|
|
196
|
+
},
|
|
197
|
+
];
|
|
198
|
+
}
|