@reticlehq/init 3.2.0 → 3.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 (93) hide show
  1. package/dist/detect/capabilities.d.ts +4 -1
  2. package/dist/detect/capabilities.js +45 -9
  3. package/dist/detect/detect.d.ts +58 -4
  4. package/dist/detect/detect.js +117 -7
  5. package/dist/detect/dev-script.d.ts +26 -2
  6. package/dist/detect/dev-script.js +80 -7
  7. package/dist/detect/non-js-project.d.ts +1 -1
  8. package/dist/detect/non-js-project.js +6 -1
  9. package/dist/detect/workspace-apps.d.ts +21 -0
  10. package/dist/detect/workspace-apps.js +111 -12
  11. package/dist/detect/workspace-redirect.d.ts +2 -1
  12. package/dist/detect/workspace-redirect.js +28 -2
  13. package/dist/diagnose/containerised-dev-server.d.ts +12 -4
  14. package/dist/diagnose/containerised-dev-server.js +36 -4
  15. package/dist/diagnose/csp-check.d.ts +12 -6
  16. package/dist/diagnose/csp-check.js +61 -13
  17. package/dist/diagnose/csp-doctor.d.ts +33 -2
  18. package/dist/diagnose/csp-doctor.js +103 -6
  19. package/dist/diagnose/csp-step.d.ts +27 -0
  20. package/dist/diagnose/csp-step.js +86 -0
  21. package/dist/diagnose/desktop-doctor.d.ts +1 -1
  22. package/dist/diagnose/desktop-doctor.js +8 -2
  23. package/dist/diagnose/install-hint.d.ts +9 -1
  24. package/dist/diagnose/install-hint.js +17 -7
  25. package/dist/diagnose/install-retries.d.ts +18 -2
  26. package/dist/diagnose/install-retries.js +50 -3
  27. package/dist/diagnose/terminal-wrap.d.ts +18 -0
  28. package/dist/diagnose/terminal-wrap.js +63 -0
  29. package/dist/host.d.ts +6 -0
  30. package/dist/host.js +3 -0
  31. package/dist/index.d.ts +8 -3
  32. package/dist/index.js +8 -3
  33. package/dist/no-package-json.d.ts +10 -0
  34. package/dist/no-package-json.js +121 -0
  35. package/dist/node-io.d.ts +9 -5
  36. package/dist/node-io.js +25 -7
  37. package/dist/patch/angular.d.ts +68 -0
  38. package/dist/patch/angular.js +216 -0
  39. package/dist/patch/astro-patch.d.ts +48 -13
  40. package/dist/patch/astro-patch.js +151 -43
  41. package/dist/patch/cra.d.ts +0 -2
  42. package/dist/patch/cra.js +0 -2
  43. package/dist/patch/electron-patch.d.ts +10 -1
  44. package/dist/patch/electron-patch.js +79 -27
  45. package/dist/patch/electron-vite-patch.js +1 -1
  46. package/dist/patch/nuxt-patch.d.ts +1 -1
  47. package/dist/patch/nuxt-patch.js +2 -2
  48. package/dist/patch/nuxt-snippets.d.ts +44 -0
  49. package/dist/patch/nuxt-snippets.js +135 -0
  50. package/dist/patch/remix.d.ts +24 -0
  51. package/dist/patch/remix.js +59 -0
  52. package/dist/patch/snippets.d.ts +58 -57
  53. package/dist/patch/snippets.js +181 -202
  54. package/dist/patch/static-page.d.ts +39 -0
  55. package/dist/patch/static-page.js +102 -0
  56. package/dist/patch/tanstack-start.d.ts +36 -8
  57. package/dist/patch/tanstack-start.js +118 -29
  58. package/dist/patch/vite-config.d.ts +5 -0
  59. package/dist/patch/vite-config.js +8 -0
  60. package/dist/patch/vite-env-types.d.ts +9 -0
  61. package/dist/patch/vite-env-types.js +19 -0
  62. package/dist/patch/vite-owning-config.d.ts +47 -0
  63. package/dist/patch/vite-owning-config.js +104 -19
  64. package/dist/plan/connect-steps.d.ts +10 -3
  65. package/dist/plan/connect-steps.js +22 -3
  66. package/dist/plan/framework-adapter.d.ts +5 -6
  67. package/dist/plan/framework-adapter.js +105 -11
  68. package/dist/plan/notices.js +4 -3
  69. package/dist/plan/plan-angular.d.ts +6 -0
  70. package/dist/plan/plan-angular.js +89 -0
  71. package/dist/plan/plan-electron-vite.d.ts +6 -3
  72. package/dist/plan/plan-electron-vite.js +39 -8
  73. package/dist/plan/plan-framework.d.ts +9 -47
  74. package/dist/plan/plan-framework.js +114 -323
  75. package/dist/plan/plan-types.d.ts +47 -2
  76. package/dist/plan/plan-vite.d.ts +67 -0
  77. package/dist/plan/plan-vite.js +256 -0
  78. package/dist/plan/plan.js +155 -69
  79. package/dist/plan/port-steps.d.ts +45 -0
  80. package/dist/plan/port-steps.js +119 -0
  81. package/dist/plan/preflight.d.ts +17 -3
  82. package/dist/plan/preflight.js +53 -23
  83. package/dist/plan/stop-hook-step.d.ts +4 -0
  84. package/dist/plan/stop-hook-step.js +49 -0
  85. package/dist/project/agent-rules.js +2 -0
  86. package/dist/register/mcp-clients.d.ts +13 -0
  87. package/dist/register/mcp-clients.js +22 -0
  88. package/dist/register/mcp.d.ts +26 -5
  89. package/dist/register/mcp.js +64 -8
  90. package/dist/run-types.d.ts +10 -0
  91. package/dist/run.js +132 -78
  92. package/package.json +3 -4
  93. package/NOTICE +0 -5
@@ -0,0 +1,49 @@
1
+ /**
2
+ * `init --hooks`: the print-only Claude Code Stop hook, merged into `.claude/settings.json`.
3
+ *
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; it never blocks the agent. The line
6
+ * itself is `reticle report --hook`, which reads the session journal on disk.
7
+ */
8
+ import { RETICLE_NPM_PACKAGE } from '../version.js';
9
+ import { StepStatus } from './plan-types.js';
10
+ export const CLAUDE_SETTINGS_PATH = '.claude/settings.json';
11
+ export const STOP_HOOK_COMMAND = `npx ${RETICLE_NPM_PACKAGE} report --hook`;
12
+ const TITLE = 'Claude Code Stop hook';
13
+ const STOP_EVENT = 'Stop';
14
+ const HOOK_ENTRY = { hooks: [{ type: 'command', command: STOP_HOOK_COMMAND }] };
15
+ const isObject = (value) => 'object' === typeof value && null !== value && !Array.isArray(value);
16
+ /** The settings as an object, or undefined when the text is not one. */
17
+ function parseSettings(text) {
18
+ try {
19
+ const parsed = JSON.parse(text);
20
+ return isObject(parsed) ? parsed : undefined;
21
+ }
22
+ catch {
23
+ return undefined;
24
+ }
25
+ }
26
+ export function stopHookStep(path, existing) {
27
+ const settings = null === existing || undefined === existing ? {} : parseSettings(existing);
28
+ if (settings === undefined) {
29
+ return {
30
+ title: TITLE,
31
+ target: path,
32
+ status: StepStatus.MANUAL,
33
+ detail: `${CLAUDE_SETTINGS_PATH} is not a JSON object, so it was left alone. Add a Stop hook running \`${STOP_HOOK_COMMAND}\``,
34
+ };
35
+ }
36
+ const hooks = isObject(settings['hooks']) ? settings['hooks'] : {};
37
+ const stop = Array.isArray(hooks[STOP_EVENT]) ? hooks[STOP_EVENT] : [];
38
+ if (JSON.stringify(stop).includes(STOP_HOOK_COMMAND)) {
39
+ return { title: TITLE, target: path, status: StepStatus.ALREADY, detail: 'hook already installed' };
40
+ }
41
+ const merged = { ...settings, hooks: { ...hooks, [STOP_EVENT]: [...stop, HOOK_ENTRY] } };
42
+ return {
43
+ title: TITLE,
44
+ target: path,
45
+ status: StepStatus.APPLY,
46
+ detail: 'print one line when the agent stops without a claim that held; never blocks it',
47
+ write: { path, content: `${JSON.stringify(merged, null, 2)}\n`, expect: [STOP_HOOK_COMMAND] },
48
+ };
49
+ }
@@ -19,6 +19,7 @@
19
19
  */
20
20
  import { NPX } from '../register/mcp.js';
21
21
  import { RETICLE_NPM_PACKAGE } from '../version.js';
22
+ import { DISCOVERY_CALL_URL, DISCOVERY_EMAIL_COMMAND } from '@reticlehq/core';
22
23
  /** Project-root instruction files the agent re-reads every session. */
23
24
  export const CLAUDE_MD_PATH = 'CLAUDE.md';
24
25
  export const AGENTS_MD_PATH = 'AGENTS.md';
@@ -206,6 +207,7 @@ Finish that file when you see it. It is what lets you check what the app BELIEVE
206
207
  - **Working around a Reticle defect in silence is the one thing not to do.** A silent workaround is how the same defect survives to the next user.
207
208
  - **Report defects in *Reticle*, not in the app under test.** A bug you find in this app is Reticle working as intended; that one belongs in your report to the user.
208
209
  - **Never include app source, secrets, user data, or customer records** in a report.
210
+ - **When the person you work with is stuck, or Reticle got in their way, offer them a call with the founder** (${DISCOVERY_CALL_URL}) or leaving an email with \`${DISCOVERY_EMAIL_COMMAND}\`. That conversation is where Reticle's direction comes from. Offer it; never book a call or submit an email for them.
209
211
 
210
212
  ## More
211
213
 
@@ -37,6 +37,19 @@ export interface ClientSpec {
37
37
  /** Where the shape came from, so the next person can re-check it rather than trust this file. */
38
38
  docs: string;
39
39
  }
40
+ /**
41
+ * Claude Code's project-scope MCP config, which needs no CLI on PATH.
42
+ *
43
+ * Named here beside the client specs because it IS one of Claude Code's two config routes, and a
44
+ * free string for it is the reason nobody noticed the other route existed when the CLI one failed.
45
+ */
46
+ export declare const CLAUDE_PROJECT_CONFIG = ".mcp.json";
47
+ /**
48
+ * Claude Code's project-scope registration, as a client spec so it goes through the same merge
49
+ * (already / manual / apply) as every other client. Not in `MCP_CLIENTS`: that list is DETECTED by
50
+ * its config existing, and this one is only ever written from inside Claude Code (see plan.ts).
51
+ */
52
+ export declare const CLAUDE_PROJECT_SPEC: ClientSpec;
40
53
  export declare const MCP_CLIENTS: readonly ClientSpec[];
41
54
  export declare function clientSpec(id: McpClient): ClientSpec;
42
55
  export declare const ClientMergeStatus: {
@@ -53,6 +53,28 @@ function commandArgsEntry() {
53
53
  function openCodeEntry() {
54
54
  return { type: 'local', command: [NPX, ...npxServerArgs()], enabled: true };
55
55
  }
56
+ /**
57
+ * Claude Code's project-scope MCP config, which needs no CLI on PATH.
58
+ *
59
+ * Named here beside the client specs because it IS one of Claude Code's two config routes, and a
60
+ * free string for it is the reason nobody noticed the other route existed when the CLI one failed.
61
+ */
62
+ export const CLAUDE_PROJECT_CONFIG = '.mcp.json';
63
+ /**
64
+ * Claude Code's project-scope registration, as a client spec so it goes through the same merge
65
+ * (already / manual / apply) as every other client. Not in `MCP_CLIENTS`: that list is DETECTED by
66
+ * its config existing, and this one is only ever written from inside Claude Code (see plan.ts).
67
+ */
68
+ export const CLAUDE_PROJECT_SPEC = {
69
+ id: McpClient.CLAUDE_CODE,
70
+ label: 'Claude Code, project',
71
+ scope: ConfigScope.PROJECT,
72
+ relPath: CLAUDE_PROJECT_CONFIG,
73
+ format: ConfigFormat.JSON,
74
+ serversKey: 'mcpServers',
75
+ entry: commandArgsEntry,
76
+ docs: 'https://docs.claude.com/en/docs/claude-code/mcp: project scope, .mcp.json at the project root',
77
+ };
56
78
  export const MCP_CLIENTS = [
57
79
  {
58
80
  id: McpClient.CLAUDE_CODE,
@@ -66,11 +66,24 @@ interface ClaudeAddCommand {
66
66
  }
67
67
  /** `claude mcp add reticle -s user -- npx @reticlehq/server mcp` — registers globally for all projects (portless). */
68
68
  export declare function claudeAddCommand(): ClaudeAddCommand;
69
- /** Probe args that tell us whether an `reticle` server already exists in any scope (exit 0 = exists). */
70
- export declare function claudeExistsProbe(): {
71
- command: string;
72
- args: string[];
73
- };
69
+ /**
70
+ * Is a `reticle` server already registered with Claude Code, in any scope this project sees?
71
+ *
72
+ * READ FROM CLAUDE'S OWN CONFIG, never asked of the CLI. `claude mcp get reticle` health-checks the
73
+ * entry by LAUNCHING it — `npx @reticlehq/server mcp` in the project — which starts a daemon on
74
+ * whatever `.reticle.json` says before `init` has rewritten it. A re-run moving the port brought the
75
+ * old-port daemon back about two seconds after it was stopped. `claude mcp list` health-checks
76
+ * every server the same way.
77
+ *
78
+ * The three scopes `claude mcp add` writes: user (`mcpServers` at the top of the state file), local
79
+ * (`projects[<dir>].mcpServers`, keyed by absolute path) and project (`<dir>/.mcp.json`). Without a
80
+ * project directory only user scope is looked at. An unreadable file answers "not registered",
81
+ * which costs at worst a `claude mcp add` that reports the entry already exists.
82
+ */
83
+ export declare function claudeHasReticle(io: {
84
+ readFile(path: string): string | null;
85
+ homeDir(): string;
86
+ }, projectDir: string | undefined, configDir?: string | undefined): boolean;
74
87
  /** Probe args for whether the `claude` CLI is installed at all. */
75
88
  export declare function claudeAvailableProbe(): {
76
89
  command: string;
@@ -83,4 +96,12 @@ export declare function claudeAvailableProbe(): {
83
96
  */
84
97
  export declare function mcpWindowsNote(): string;
85
98
  /** Printed when the `claude` CLI isn't available — register Reticle globally once, by hand. */
99
+ /**
100
+ * Claude Code's PROJECT-scope config, which needs no CLI on PATH.
101
+ *
102
+ * The remedy a reporter had to work out for themselves: inside a Claude Code VS Code extension
103
+ * session the `claude` binary is not on PATH, so every CLI-based route is shut and `.mcp.json` in
104
+ * the project root is the one that still works.
105
+ */
106
+ export declare function claudeProjectMcpJson(): string;
86
107
  export declare function mcpManual(): string;
@@ -11,11 +11,13 @@
11
11
  * project's `.reticle.json` in its CWD (see cli-port.ts). The port belongs to the project, not the
12
12
  * global agent config.
13
13
  */
14
+ import { join } from 'node:path';
14
15
  import { RETICLE_NPM_PACKAGE } from '../version.js';
15
16
  // The name is a wire identity and lives in the contract; re-exported here so the registration writers
16
17
  // below (and their tests) keep reading it from the module that USES it.
17
18
  export { MCP_SERVER_NAME } from '@reticlehq/core';
18
19
  import { MCP_SERVER_NAME } from '@reticlehq/core';
20
+ import { CLAUDE_PROJECT_CONFIG } from './mcp-clients.js';
19
21
  /**
20
22
  * The registered command is STILL bare `npx` on every platform, deliberately.
21
23
  *
@@ -86,14 +88,53 @@ export function claudeAddCommand() {
86
88
  const args = [MCP_SUBCOMMAND, 'add', MCP_SERVER_NAME, '-s', 'user', '--', ...tail];
87
89
  return { command: CLAUDE_CLI, args, display: `${CLAUDE_CLI} ${args.join(' ')}` };
88
90
  }
89
- /** Probe args that tell us whether an `reticle` server already exists in any scope (exit 0 = exists). */
90
- export function claudeExistsProbe() {
91
- // NO `-s`: `claude mcp get` takes no options at all, so passing one exits 1 with "unknown option
92
- // '-s'" — the probe answered "not registered" on EVERY machine, init then ran `claude mcp add`,
93
- // which exits 1 with "already exists", and a re-run reported `[⚠] step failed` plus a manual
94
- // command that fails the same way. A false positive from a project-scoped entry costs one skipped
95
- // registration; the flag cost every re-run a failed step.
96
- return { command: CLAUDE_CLI, args: [MCP_SUBCOMMAND, 'get', MCP_SERVER_NAME] };
91
+ /** Where Claude Code keeps its state: `$CLAUDE_CONFIG_DIR/.claude.json`, else `~/.claude.json`. */
92
+ const CLAUDE_STATE_FILE = '.claude.json';
93
+ /** `mcpServers` holds a `reticle` entry. */
94
+ function namesReticle(scope) {
95
+ if ('object' !== typeof scope || null === scope)
96
+ return false;
97
+ const servers = scope.mcpServers;
98
+ return 'object' === typeof servers && null !== servers && MCP_SERVER_NAME in servers;
99
+ }
100
+ function parsed(source) {
101
+ if (null === source)
102
+ return undefined;
103
+ try {
104
+ return JSON.parse(source);
105
+ }
106
+ catch {
107
+ return undefined;
108
+ }
109
+ }
110
+ /**
111
+ * Is a `reticle` server already registered with Claude Code, in any scope this project sees?
112
+ *
113
+ * READ FROM CLAUDE'S OWN CONFIG, never asked of the CLI. `claude mcp get reticle` health-checks the
114
+ * entry by LAUNCHING it — `npx @reticlehq/server mcp` in the project — which starts a daemon on
115
+ * whatever `.reticle.json` says before `init` has rewritten it. A re-run moving the port brought the
116
+ * old-port daemon back about two seconds after it was stopped. `claude mcp list` health-checks
117
+ * every server the same way.
118
+ *
119
+ * The three scopes `claude mcp add` writes: user (`mcpServers` at the top of the state file), local
120
+ * (`projects[<dir>].mcpServers`, keyed by absolute path) and project (`<dir>/.mcp.json`). Without a
121
+ * project directory only user scope is looked at. An unreadable file answers "not registered",
122
+ * which costs at worst a `claude mcp add` that reports the entry already exists.
123
+ */
124
+ export function claudeHasReticle(io, projectDir, configDir = process.env['CLAUDE_CONFIG_DIR']) {
125
+ const state = parsed(io.readFile(join(configDir ?? io.homeDir(), CLAUDE_STATE_FILE)));
126
+ if (namesReticle(state))
127
+ return true;
128
+ if (projectDir === undefined)
129
+ return false;
130
+ const projects = 'object' === typeof state && null !== state
131
+ ? state.projects
132
+ : undefined;
133
+ if ('object' === typeof projects && null !== projects) {
134
+ if (namesReticle(projects[projectDir]))
135
+ return true;
136
+ }
137
+ return namesReticle(parsed(io.readFile(join(projectDir, CLAUDE_PROJECT_CONFIG))));
97
138
  }
98
139
  /** Probe args for whether the `claude` CLI is installed at all. */
99
140
  export function claudeAvailableProbe() {
@@ -112,12 +153,27 @@ does not gate:
112
153
  "${MCP_SERVER_NAME}": { "command": "${WINDOWS_SHELL}", "args": ${JSON.stringify([WINDOWS_SHELL_C, NPX, ...serverInvocation().slice(1)])} }`;
113
154
  }
114
155
  /** Printed when the `claude` CLI isn't available — register Reticle globally once, by hand. */
156
+ /**
157
+ * Claude Code's PROJECT-scope config, which needs no CLI on PATH.
158
+ *
159
+ * The remedy a reporter had to work out for themselves: inside a Claude Code VS Code extension
160
+ * session the `claude` binary is not on PATH, so every CLI-based route is shut and `.mcp.json` in
161
+ * the project root is the one that still works.
162
+ */
163
+ export function claudeProjectMcpJson() {
164
+ const [command, ...args] = serverInvocation();
165
+ return `${CLAUDE_PROJECT_CONFIG} (project root):
166
+
167
+ { "mcpServers": { "${MCP_SERVER_NAME}": { "command": "${command ?? NPX}", "args": ${JSON.stringify(args)} } } }`;
168
+ }
115
169
  export function mcpManual() {
116
170
  const tail = serverInvocation().join(' ');
117
171
  return `Register the Reticle MCP server ONCE, globally (so every project gets it):
118
172
 
119
173
  ${CLAUDE_CLI} ${MCP_SUBCOMMAND} add ${MCP_SERVER_NAME} -s user -- ${tail}
120
174
 
175
+ Or, in Claude Code with no \`claude\` on PATH (a VS Code extension session), write ${claudeProjectMcpJson()}
176
+
121
177
  Or, for another agent, add this to its global MCP config (e.g. Cursor's ~/.cursor/mcp.json):
122
178
 
123
179
  "${MCP_SERVER_NAME}": { "command": "${NPX}", "args": ${JSON.stringify(serverInvocation().slice(1))} }
@@ -10,6 +10,8 @@ import type { InitOutcome } from '@reticlehq/core/telemetry';
10
10
  export interface InitOptions {
11
11
  /** `--capture-bodies`: write `captureNetworkBodies: true` into the app's config. Off by default (#705). */
12
12
  captureBodies?: boolean | undefined;
13
+ /** `--hooks`: also install the print-only Claude Code Stop hook. Off by default. */
14
+ hooks?: boolean | undefined;
13
15
  cwd: string;
14
16
  port: number | undefined;
15
17
  mcp: boolean;
@@ -76,6 +78,14 @@ export interface InitIo {
76
78
  scoped(relPath: string): InitIo;
77
79
  /** Runs a subprocess to completion (inherits stdio); returns true on exit code 0. */
78
80
  exec(command: string, args: readonly string[]): boolean;
81
+ /**
82
+ * Runs a subprocess quietly and returns its combined output — for a failure whose text names its
83
+ * cause (see explainInstallFailure). Optional: without it the generic failure text stands.
84
+ */
85
+ capture?(command: string, args: readonly string[]): {
86
+ ok: boolean;
87
+ output: string;
88
+ };
79
89
  /** Runs a subprocess quietly (no stdio) for a yes/no check; returns true on exit code 0. */
80
90
  probe(command: string, args: readonly string[]): boolean;
81
91
  /** Can this process write into the project root — see preflight.ts. */