@reticlehq/server 3.8.2 → 3.9.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/LICENSE +201 -104
- package/NOTICE +5 -0
- package/README.md +1 -1
- package/bin/launch.js +13 -0
- package/bin/package.json +3 -0
- package/bin/reticle.js +66 -8
- package/dist/chromium-launch-options.d.ts +25 -0
- package/dist/chromium-launch-options.js +21 -1
- package/dist/command/cli/adhoc-verdict.js +34 -7
- package/dist/command/cli/answers/gate-exit.d.ts +6 -2
- package/dist/command/cli/answers/gate-exit.js +12 -2
- package/dist/command/cli/ci-hint.d.ts +44 -0
- package/dist/command/cli/ci-hint.js +72 -0
- package/dist/command/cli/cli-doctor.d.ts +6 -1
- package/dist/command/cli/cli-doctor.js +45 -4
- package/dist/command/cli/cli-flow-commands.d.ts +7 -1
- package/dist/command/cli/cli-flow-commands.js +39 -17
- package/dist/command/cli/cli-kill.d.ts +21 -0
- package/dist/command/cli/cli-kill.js +46 -3
- package/dist/command/cli/cli-parse.d.ts +5 -2
- package/dist/command/cli/cli-parse.js +10 -5
- package/dist/command/cli/cli-usage.js +3 -2
- package/dist/command/cli/cli-verify.d.ts +6 -1
- package/dist/command/cli/cli-verify.js +32 -13
- package/dist/command/cli/doctor/browser/chromium-hint.d.ts +6 -1
- package/dist/command/cli/doctor/browser/chromium-hint.js +11 -5
- package/dist/command/cli/doctor/doctor-fix.d.ts +44 -0
- package/dist/command/cli/doctor/doctor-fix.js +89 -0
- package/dist/command/cli/doctor/doctor-launcher-line.d.ts +23 -0
- package/dist/command/cli/doctor/doctor-launcher-line.js +60 -0
- package/dist/command/cli/doctor/doctor-rows.d.ts +4 -0
- package/dist/command/cli/doctor/doctor-rows.js +4 -0
- package/dist/command/cli/doctor-repairs.d.ts +64 -0
- package/dist/command/cli/doctor-repairs.js +214 -0
- package/dist/command/cli/drive/drive-command.js +3 -7
- package/dist/command/cli/first-use-checks.d.ts +41 -0
- package/dist/command/cli/first-use-checks.js +127 -0
- package/dist/command/cli/first-verdict-node.d.ts +3 -0
- package/dist/command/cli/first-verdict-node.js +189 -0
- package/dist/command/cli/first-verdict.d.ts +126 -0
- package/dist/command/cli/first-verdict.js +267 -0
- package/dist/command/cli/launch/sdk-version-match.d.ts +3 -2
- package/dist/command/cli/launch/sdk-version-match.js +3 -2
- package/dist/command/cli/lifecycle/daemon-lifecycle.js +6 -5
- package/dist/command/cli/mcp-command.js +13 -4
- package/dist/command/cli/ports/port-holder.d.ts +2 -0
- package/dist/command/cli/ports/port-holder.js +11 -3
- package/dist/command/cli/setup-mcp-cli.d.ts +27 -1
- package/dist/command/cli/setup-mcp-cli.js +98 -23
- package/dist/command/cli/status/status-lines.d.ts +1 -0
- package/dist/command/cli/status/status-lines.js +50 -17
- package/dist/command/cli/status-fields.js +17 -3
- package/dist/command/cli/try-command.d.ts +7 -0
- package/dist/command/cli/try-command.js +11 -3
- package/dist/command/cli.js +70 -33
- package/dist/command/daemon/binding/daemon-status-probe.d.ts +3 -1
- package/dist/command/daemon/binding/daemon-status-probe.js +4 -2
- package/dist/command/daemon/daemon-argv.d.ts +9 -0
- package/dist/command/daemon/daemon-argv.js +11 -0
- package/dist/command/daemon/daemon.d.ts +10 -3
- package/dist/command/daemon/daemon.js +17 -6
- package/dist/command/daemon/lifetime/daemon-exit.d.ts +24 -0
- package/dist/command/daemon/lifetime/daemon-exit.js +39 -0
- package/dist/command/daemon/lifetime/death-cause.d.ts +15 -0
- package/dist/command/daemon/lifetime/death-cause.js +130 -0
- package/dist/command/daemon/lifetime/heartbeat.d.ts +7 -15
- package/dist/command/daemon/lifetime/heartbeat.js +4 -13
- package/dist/command/daemon/port-state.d.ts +14 -0
- package/dist/command/daemon/port-state.js +31 -0
- package/dist/command/setup/agent-configs.d.ts +2 -0
- package/dist/command/setup/agent-configs.js +42 -15
- package/dist/command/setup/agent-next-steps.d.ts +77 -0
- package/dist/command/setup/agent-next-steps.js +284 -0
- package/dist/command/setup/agent-writer.js +11 -6
- package/dist/command/setup/init/init-host.js +2 -0
- package/dist/command/setup/init/init-runtime.d.ts +6 -1
- package/dist/command/setup/init/init-runtime.js +57 -21
- package/dist/command/setup/mcp-launcher.d.ts +3 -0
- package/dist/command/setup/mcp-launcher.js +11 -0
- package/dist/command/setup/setup-command.js +30 -12
- package/dist/command/setup/setup-mcp.d.ts +8 -0
- package/dist/command/setup/setup-mcp.js +47 -20
- package/dist/command/setup/start-dev-server.d.ts +8 -0
- package/dist/command/setup/start-dev-server.js +43 -0
- package/dist/command/version/identity/server-version.d.ts +2 -0
- package/dist/command/version/identity/server-version.js +2 -0
- package/dist/command/version/version-skew.d.ts +7 -0
- package/dist/command/version/version-skew.js +13 -2
- package/dist/features/license/license.d.ts +11 -71
- package/dist/features/license/license.js +43 -136
- package/dist/features/visual/desktop-capture.d.ts +33 -0
- package/dist/features/visual/desktop-capture.js +94 -0
- package/dist/features/visual/step-shots.js +139 -35
- package/dist/features/visual/visual-tools.d.ts +0 -18
- package/dist/features/visual/visual-tools.js +2 -81
- package/dist/import-graph.js +5 -6
- package/dist/index.d.ts +3 -1
- package/dist/index.js +14 -5
- package/dist/judgement/runs/run-ledger.d.ts +7 -5
- package/dist/judgement/runs/run-ledger.js +98 -69
- package/dist/judgement/runs/verification-sync.d.ts +6 -3
- package/dist/judgement/runs/verification-sync.js +21 -10
- package/dist/language/flows/flow-store-for-session.js +7 -5
- package/dist/language/flows/recording/tape/recordings.d.ts +2 -13
- package/dist/language/flows/recording/tape/recordings.js +41 -44
- package/dist/language/flows/replay.d.ts +17 -7
- package/dist/language/flows/replay.js +95 -44
- package/dist/launch-chromium.d.ts +1 -0
- package/dist/launch-chromium.js +49 -12
- package/dist/memory/cloud/sync-screenshots.js +6 -1
- package/dist/portal/bridge/bridge.d.ts +13 -4
- package/dist/portal/bridge/bridge.js +10 -9
- package/dist/portal/input/network-mock.js +3 -2
- package/dist/portal/input/real-input.d.ts +15 -0
- package/dist/portal/input/real-input.js +59 -3
- package/dist/portal/input/viewport-tools.js +6 -3
- package/dist/portal/pool/browser-pool.d.ts +5 -1
- package/dist/portal/pool/browser-pool.js +32 -38
- package/dist/portal/pool/context-permissions.d.ts +20 -17
- package/dist/portal/pool/context-permissions.js +63 -1
- package/dist/portal/pool/csp-widen.d.ts +62 -0
- package/dist/portal/pool/csp-widen.js +132 -0
- package/dist/portal/pool/lease-page-error.d.ts +29 -0
- package/dist/portal/pool/lease-page-error.js +18 -0
- package/dist/portal/pool/page-error-journal.d.ts +57 -0
- package/dist/portal/pool/page-error-journal.js +181 -0
- package/dist/portal/pool/playwright-launcher.js +21 -8
- package/dist/portal/pool/pool-contract.d.ts +22 -0
- package/dist/portal/pool/pool-lease.d.ts +35 -2
- package/dist/portal/pool/pool-lease.js +23 -0
- package/dist/portal/pool/zero-install.js +69 -1
- package/dist/portal/session/dev-server/dev-command.d.ts +8 -0
- package/dist/portal/session/dev-server/dev-command.js +56 -8
- package/dist/portal/session/no-session-diagnosis.js +10 -3
- package/dist/portal/session/no-session-next-action.d.ts +12 -0
- package/dist/portal/session/no-session-next-action.js +44 -4
- package/dist/portal/session/no-session-watch.js +3 -1
- package/dist/portal/session/page-commands/undeclared-command.d.ts +9 -3
- package/dist/portal/session/page-commands/undeclared-command.js +16 -7
- package/dist/portal/session/recorded-gaps.d.ts +1 -1
- package/dist/portal/session/recorded-gaps.js +5 -2
- package/dist/portal/session/session-manager.js +40 -21
- package/dist/portal/session/session.js +4 -3
- package/dist/portal/session/zero-install-first-call.d.ts +20 -1
- package/dist/portal/session/zero-install-first-call.js +34 -2
- package/dist/start-wiring.d.ts +29 -2
- package/dist/start-wiring.js +73 -9
- package/dist/surface/mcp/mcp-proxy.js +10 -2
- package/dist/surface/mcp/mcp.js +5 -3
- package/dist/surface/mcp/peer/agents-connected.d.ts +6 -0
- package/dist/surface/mcp/peer/agents-connected.js +58 -0
- package/dist/surface/mcp/proxy/proxy-daemon-probe.js +17 -5
- package/dist/surface/tools/act/act-preflight.js +37 -17
- package/dist/surface/tools/act/act-target.js +3 -1
- package/dist/surface/tools/act/after-match.d.ts +7 -0
- package/dist/surface/tools/act/after-match.js +33 -0
- package/dist/surface/tools/act/resolve-target.d.ts +2 -0
- package/dist/surface/tools/act/resolve-target.js +35 -8
- package/dist/surface/tools/act/sequence-grade.d.ts +3 -1
- package/dist/surface/tools/act/sequence-grade.js +8 -1
- package/dist/surface/tools/act-sequence-tool.js +3 -0
- package/dist/surface/tools/act-tools.js +10 -10
- package/dist/surface/tools/assert/assert-verdict.js +28 -1
- package/dist/surface/tools/browser-tools.js +75 -7
- package/dist/surface/tools/dynamic-tools.js +2 -1
- package/dist/surface/tools/empty-session-list.d.ts +2 -0
- package/dist/surface/tools/empty-session-list.js +4 -2
- package/dist/surface/tools/error-recovery.js +43 -22
- package/dist/surface/tools/invoke-tool.js +27 -20
- package/dist/surface/tools/lease-hint.d.ts +7 -0
- package/dist/surface/tools/lease-hint.js +10 -3
- package/dist/surface/tools/lease-readiness.d.ts +21 -0
- package/dist/surface/tools/lease-readiness.js +47 -0
- package/dist/surface/tools/lease-seed.d.ts +2 -0
- package/dist/surface/tools/lease-seed.js +2 -1
- package/dist/surface/tools/lease-tools.js +43 -15
- package/dist/surface/tools/repeat-hint.d.ts +40 -0
- package/dist/surface/tools/repeat-hint.js +171 -0
- package/dist/surface/tools/run-report.d.ts +2 -2
- package/dist/surface/tools/run-report.js +27 -15
- package/dist/surface/tools/tool-kit.d.ts +12 -0
- package/dist/surface/tools/tool-kit.js +7 -0
- package/dist/surface/tools/tools.js +26 -13
- package/dist/telemetry/argument-shape.js +16 -7
- package/dist/telemetry/cli-telemetry.d.ts +10 -1
- package/dist/telemetry/cli-telemetry.js +15 -0
- package/dist/telemetry/init-telemetry.d.ts +2 -1
- package/dist/telemetry/init-telemetry.js +47 -2
- package/dist/telemetry/install-trace.js +23 -4
- package/dist/telemetry/onboarding-funnel.d.ts +19 -2
- package/dist/telemetry/onboarding-funnel.js +25 -3
- package/dist/telemetry/proxy-telemetry.d.ts +2 -1
- package/dist/telemetry/proxy-telemetry.js +2 -2
- package/dist/telemetry/session-metrics.d.ts +1 -1
- package/dist/telemetry/session-metrics.js +67 -27
- package/dist/telemetry/telemetry.d.ts +3 -1
- package/dist/telemetry/telemetry.js +60 -28
- package/dist/telemetry/tool-refused.d.ts +2 -2
- package/dist/telemetry/tool-refused.js +9 -2
- package/docs/architecture.md +3 -6
- package/docs/cli/doctor.mdx +41 -2
- package/docs/cli/help.mdx +4 -2
- package/docs/cli/license.mdx +4 -5
- package/docs/cli/setup.mdx +13 -2
- package/docs/cli/status.mdx +4 -0
- package/docs/cli.mdx +1 -1
- package/docs/deploy-checks.md +25 -0
- package/docs/desktop.mdx +1 -1
- package/docs/enterprise.md +15 -20
- package/docs/environment.mdx +1 -0
- package/docs/first-drive.md +1 -1
- package/docs/install-manual.mdx +16 -1
- package/docs/packages/engine.mdx +1 -1
- package/docs/packages/server.mdx +1 -1
- package/docs/packages/tauri.mdx +1 -1
- package/docs/packages/test.mdx +1 -1
- package/docs/packages.mdx +1 -1
- package/docs/platform-integration.md +1 -1
- package/docs/skill.md +1 -1
- package/docs/telemetry.md +4 -2
- package/package.json +13 -9
- package/LICENSE-ENTERPRISE +0 -37
- package/dist/features/ee/audit-log.d.ts +0 -14
- package/dist/features/ee/audit-log.js +0 -11
|
@@ -0,0 +1,77 @@
|
|
|
1
|
+
import { McpClient } from '@reticlehq/init';
|
|
2
|
+
import { AgentClient } from '@reticlehq/core/telemetry';
|
|
3
|
+
import type { SetupMcpResult } from './setup-mcp.js';
|
|
4
|
+
/** What registration did for one client. */
|
|
5
|
+
export declare const AgentReg: {
|
|
6
|
+
/** The Claude Code plugin carries Reticle. */
|
|
7
|
+
readonly PLUGIN: "plugin";
|
|
8
|
+
readonly ADDED: "added";
|
|
9
|
+
readonly ALREADY: "already";
|
|
10
|
+
readonly FAILED: "failed";
|
|
11
|
+
readonly MANUAL: "manual";
|
|
12
|
+
/** Found, and Reticle is not in its config. Read-only callers (`status`) only. */
|
|
13
|
+
readonly MISSING: "missing";
|
|
14
|
+
};
|
|
15
|
+
export type AgentReg = (typeof AgentReg)[keyof typeof AgentReg];
|
|
16
|
+
export interface AgentRow {
|
|
17
|
+
readonly id: McpClient;
|
|
18
|
+
readonly reg: AgentReg;
|
|
19
|
+
/** The config path for a hand edit. */
|
|
20
|
+
readonly path?: string;
|
|
21
|
+
/** The command to retry a failed CLI registration, with the launcher that run used. */
|
|
22
|
+
readonly retry?: string;
|
|
23
|
+
/** The plugin AND a user-scope `reticle` entry: the client has two servers. */
|
|
24
|
+
readonly duplicate?: boolean;
|
|
25
|
+
/** The entry is the floating npx form: the note that names the commands that replace it. */
|
|
26
|
+
readonly stale?: string;
|
|
27
|
+
}
|
|
28
|
+
/**
|
|
29
|
+
* The rows for one `setupMcp` run, in its order.
|
|
30
|
+
*
|
|
31
|
+
* @param launcher the installed bin that run registered (`SetupMcpIo.launcher`), so a retry line
|
|
32
|
+
* names the same command.
|
|
33
|
+
*/
|
|
34
|
+
export declare function agentRows(result: SetupMcpResult, launcher?: string): AgentRow[];
|
|
35
|
+
/** The "Your agents:" block: one line per client, each with its own next step. */
|
|
36
|
+
export declare function agentLines(rows: readonly AgentRow[]): string[];
|
|
37
|
+
/** The clients worth waiting for: the ones that have Reticle registered. */
|
|
38
|
+
export declare function awaitedClients(rows: readonly AgentRow[]): AgentClient[];
|
|
39
|
+
/** One agent, as `reticle status` reports it. */
|
|
40
|
+
export interface AgentStatus {
|
|
41
|
+
readonly name: string;
|
|
42
|
+
readonly connected: boolean;
|
|
43
|
+
/** The step still missing. Absent when connected. */
|
|
44
|
+
readonly next?: string;
|
|
45
|
+
/** The line a person reads: "Connected: Cursor ✓", or the name and the step still missing. */
|
|
46
|
+
readonly said: string;
|
|
47
|
+
}
|
|
48
|
+
/** Per client: connected, or the exact step still missing. */
|
|
49
|
+
export declare function agentStatuses(rows: readonly AgentRow[], connected: ReadonlySet<string>): AgentStatus[];
|
|
50
|
+
/** The lines the installer prints after its wait. */
|
|
51
|
+
export declare function connectionLines(rows: readonly AgentRow[], connected: ReadonlySet<string>): string[];
|
|
52
|
+
/** The clock the wait runs on. Injected, so a test does not wait. */
|
|
53
|
+
export interface WaitClock {
|
|
54
|
+
now(): number;
|
|
55
|
+
sleep(ms: number): Promise<void>;
|
|
56
|
+
}
|
|
57
|
+
/**
|
|
58
|
+
* Poll `read` until every client in `want` has connected, or `budgetMs` has passed. Returns the
|
|
59
|
+
* last read. Bounded: it never waits longer than the budget.
|
|
60
|
+
*/
|
|
61
|
+
export declare function waitForAgents(want: readonly string[], read: () => ReadonlySet<string>, clock: WaitClock, budgetMs: number, pollMs: number): Promise<ReadonlySet<string>>;
|
|
62
|
+
/** `$CLAUDE_CONFIG_DIR`, else home: where Claude Code keeps `.claude.json`. */
|
|
63
|
+
export declare function claudeStateDir(): string;
|
|
64
|
+
/** What `installedAgentRows` reads. It never writes. */
|
|
65
|
+
export interface AgentProbe {
|
|
66
|
+
exists(path: string): boolean;
|
|
67
|
+
readFile(path: string): string | null;
|
|
68
|
+
homeDir(): string;
|
|
69
|
+
}
|
|
70
|
+
/**
|
|
71
|
+
* The agents on this machine and whether Reticle is in each config, read only. For `reticle status`,
|
|
72
|
+
* which must not register anything and must not spawn a client CLI.
|
|
73
|
+
*
|
|
74
|
+
* @param claudeStateDir the directory that holds `.claude.json`: `$CLAUDE_CONFIG_DIR`, else home.
|
|
75
|
+
* @param launcher the installed bin a new registration would use, for the stale-entry fix.
|
|
76
|
+
*/
|
|
77
|
+
export declare function installedAgentRows(io: AgentProbe, projectDir: string | undefined, pluginActive: boolean, claudeStateDir?: string, launcher?: string): AgentRow[];
|
|
@@ -0,0 +1,284 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* One next step for each coding agent, and whether each one really connected.
|
|
3
|
+
*
|
|
4
|
+
* Registration writes a config. An agent reads that config only at a time the client decides, so
|
|
5
|
+
* "registered" is not "the agent has the tools". This file names, per client, the one step that
|
|
6
|
+
* loads the tools, and reads which agents did connect (`agents-connected.ts`) to say so.
|
|
7
|
+
*
|
|
8
|
+
* Only the Claude Code plugin gets "no restart". Its docs say `/reload-plugins` loads it. For every
|
|
9
|
+
* other client the step is the conservative one: their docs do not say a running session picks up a
|
|
10
|
+
* new server.
|
|
11
|
+
*/
|
|
12
|
+
import { join } from 'node:path';
|
|
13
|
+
import { homedir } from 'node:os';
|
|
14
|
+
import { claudeAddCommand, claudeHasReticle, claudeRemoveUserCommand, claudeStaleEntryNote, claudeUserEntryIsFloating, clientSpec, codexNamesOurServer, ConfigFormat, detectMcpClients, McpClient, } from '@reticlehq/init';
|
|
15
|
+
import { MCP_SERVER_NAME } from '@reticlehq/core';
|
|
16
|
+
import { AgentClient } from '@reticlehq/core/telemetry';
|
|
17
|
+
/** What registration did for one client. */
|
|
18
|
+
export const AgentReg = {
|
|
19
|
+
/** The Claude Code plugin carries Reticle. */
|
|
20
|
+
PLUGIN: 'plugin',
|
|
21
|
+
ADDED: 'added',
|
|
22
|
+
ALREADY: 'already',
|
|
23
|
+
FAILED: 'failed',
|
|
24
|
+
MANUAL: 'manual',
|
|
25
|
+
/** Found, and Reticle is not in its config. Read-only callers (`status`) only. */
|
|
26
|
+
MISSING: 'missing',
|
|
27
|
+
};
|
|
28
|
+
/** The one table of client to next step. Change a step here, nowhere else. */
|
|
29
|
+
const AGENT_STEPS = {
|
|
30
|
+
[McpClient.CLAUDE_CODE]: {
|
|
31
|
+
name: 'Claude Code',
|
|
32
|
+
agent: AgentClient.CLAUDE_CODE,
|
|
33
|
+
step: 'restart Claude Code, then /mcp shows reticle',
|
|
34
|
+
},
|
|
35
|
+
[McpClient.CURSOR]: {
|
|
36
|
+
name: 'Cursor',
|
|
37
|
+
agent: AgentClient.CURSOR,
|
|
38
|
+
step: 'restart Cursor, then turn Reticle on in Settings › MCP',
|
|
39
|
+
},
|
|
40
|
+
[McpClient.CODEX]: {
|
|
41
|
+
name: 'Codex',
|
|
42
|
+
agent: AgentClient.CODEX,
|
|
43
|
+
step: 'restart Codex, then /mcp shows reticle',
|
|
44
|
+
},
|
|
45
|
+
[McpClient.GEMINI]: {
|
|
46
|
+
name: 'Gemini CLI',
|
|
47
|
+
agent: AgentClient.GEMINI_CLI,
|
|
48
|
+
step: 'restart Gemini CLI, then /mcp shows reticle',
|
|
49
|
+
},
|
|
50
|
+
[McpClient.WINDSURF]: {
|
|
51
|
+
name: 'Windsurf',
|
|
52
|
+
agent: AgentClient.WINDSURF,
|
|
53
|
+
step: 'restart Windsurf, then turn Reticle on in its MCP settings',
|
|
54
|
+
},
|
|
55
|
+
[McpClient.VSCODE]: {
|
|
56
|
+
name: 'VS Code',
|
|
57
|
+
agent: AgentClient.VSCODE,
|
|
58
|
+
step: 'run "Developer: Reload Window" in VS Code',
|
|
59
|
+
},
|
|
60
|
+
[McpClient.OPENCODE]: {
|
|
61
|
+
name: 'OpenCode',
|
|
62
|
+
agent: AgentClient.OPENCODE,
|
|
63
|
+
step: 'restart OpenCode',
|
|
64
|
+
},
|
|
65
|
+
[McpClient.ANTIGRAVITY]: {
|
|
66
|
+
name: 'Antigravity',
|
|
67
|
+
agent: AgentClient.ANTIGRAVITY,
|
|
68
|
+
step: 'restart Antigravity',
|
|
69
|
+
},
|
|
70
|
+
};
|
|
71
|
+
const NextStepCopy = {
|
|
72
|
+
HEADING: 'Your agents:',
|
|
73
|
+
PLUGIN_ACTIVE: 'plugin active: type /reload-plugins if it is open',
|
|
74
|
+
PLUGIN_NOT_CONNECTED: 'start Claude Code, or type /reload-plugins in an open session',
|
|
75
|
+
ADDED: 'added: ',
|
|
76
|
+
ALREADY: 'already set up',
|
|
77
|
+
NOT_ADDED: 'not added: ',
|
|
78
|
+
FAILED: 'registration failed: ',
|
|
79
|
+
FAILED_FILE: 'check the permissions of its config file, then run `reticle setup mcp`',
|
|
80
|
+
MANUAL_HEAD: 'add the reticle entry to ',
|
|
81
|
+
MANUAL_TAIL: ' by hand',
|
|
82
|
+
MISSING: 'not added: run `reticle setup mcp`',
|
|
83
|
+
CONNECTED_HEAD: 'Connected: ',
|
|
84
|
+
CONNECTED_TAIL: ' ✓',
|
|
85
|
+
NOT_CONNECTED: ' not connected yet: ',
|
|
86
|
+
DUPLICATE: ' ⚠ a user-scope reticle server is registered too, so Claude Code has two. Remove it: ',
|
|
87
|
+
STALE: ' ⚠ ',
|
|
88
|
+
};
|
|
89
|
+
const NAME_COLUMN = Math.max(...Object.values(AGENT_STEPS).map((s) => s.name.length)) + 2;
|
|
90
|
+
/**
|
|
91
|
+
* The rows for one `setupMcp` run, in its order.
|
|
92
|
+
*
|
|
93
|
+
* @param launcher the installed bin that run registered (`SetupMcpIo.launcher`), so a retry line
|
|
94
|
+
* names the same command.
|
|
95
|
+
*/
|
|
96
|
+
export function agentRows(result, launcher) {
|
|
97
|
+
const known = (id) => id in AGENT_STEPS;
|
|
98
|
+
const rows = [];
|
|
99
|
+
for (const id of result.detected) {
|
|
100
|
+
if (!known(id)) {
|
|
101
|
+
continue;
|
|
102
|
+
}
|
|
103
|
+
if (result.pluginActive.includes(id)) {
|
|
104
|
+
rows.push(pluginRow(id, result.duplicates.includes(id)));
|
|
105
|
+
continue;
|
|
106
|
+
}
|
|
107
|
+
const manual = result.manual.find((m) => m.id === id);
|
|
108
|
+
if (manual !== undefined) {
|
|
109
|
+
rows.push({ id, reg: AgentReg.MANUAL, path: manual.configPath });
|
|
110
|
+
}
|
|
111
|
+
else if (result.failed.includes(id)) {
|
|
112
|
+
rows.push(McpClient.CLAUDE_CODE === id
|
|
113
|
+
? { id, reg: AgentReg.FAILED, retry: claudeAddCommand(launcher).display }
|
|
114
|
+
: { id, reg: AgentReg.FAILED });
|
|
115
|
+
}
|
|
116
|
+
else if (result.alreadyThere.includes(id)) {
|
|
117
|
+
rows.push(alreadyRow(id, result.stale.includes(id), launcher));
|
|
118
|
+
}
|
|
119
|
+
else if (result.registered.includes(id)) {
|
|
120
|
+
rows.push({ id, reg: AgentReg.ADDED });
|
|
121
|
+
}
|
|
122
|
+
}
|
|
123
|
+
return rows;
|
|
124
|
+
}
|
|
125
|
+
function alreadyRow(id, stale, launcher) {
|
|
126
|
+
return stale
|
|
127
|
+
? { id, reg: AgentReg.ALREADY, stale: claudeStaleEntryNote(launcher) }
|
|
128
|
+
: { id, reg: AgentReg.ALREADY };
|
|
129
|
+
}
|
|
130
|
+
function pluginRow(id, duplicate) {
|
|
131
|
+
return duplicate ? { id, reg: AgentReg.PLUGIN, duplicate } : { id, reg: AgentReg.PLUGIN };
|
|
132
|
+
}
|
|
133
|
+
function failedStep(row) {
|
|
134
|
+
return row.retry === undefined ? NextStepCopy.FAILED_FILE : `run \`${row.retry}\``;
|
|
135
|
+
}
|
|
136
|
+
/** What one row says before anything is known about the connection. */
|
|
137
|
+
function registrationText(row) {
|
|
138
|
+
const { step } = AGENT_STEPS[row.id];
|
|
139
|
+
switch (row.reg) {
|
|
140
|
+
case AgentReg.PLUGIN:
|
|
141
|
+
return NextStepCopy.PLUGIN_ACTIVE;
|
|
142
|
+
case AgentReg.ADDED:
|
|
143
|
+
return `${NextStepCopy.ADDED}${step}`;
|
|
144
|
+
case AgentReg.ALREADY:
|
|
145
|
+
return NextStepCopy.ALREADY;
|
|
146
|
+
case AgentReg.FAILED:
|
|
147
|
+
return `${NextStepCopy.FAILED}${failedStep(row)}`;
|
|
148
|
+
case AgentReg.MANUAL:
|
|
149
|
+
return `${NextStepCopy.NOT_ADDED}${NextStepCopy.MANUAL_HEAD}${row.path ?? ''}${NextStepCopy.MANUAL_TAIL}`;
|
|
150
|
+
case AgentReg.MISSING:
|
|
151
|
+
return NextStepCopy.MISSING;
|
|
152
|
+
}
|
|
153
|
+
}
|
|
154
|
+
/** The "Your agents:" block: one line per client, each with its own next step. */
|
|
155
|
+
export function agentLines(rows) {
|
|
156
|
+
if (0 === rows.length) {
|
|
157
|
+
return [];
|
|
158
|
+
}
|
|
159
|
+
return [
|
|
160
|
+
NextStepCopy.HEADING,
|
|
161
|
+
...rows.flatMap((row) => {
|
|
162
|
+
const lines = [` ${AGENT_STEPS[row.id].name.padEnd(NAME_COLUMN)}${registrationText(row)}`];
|
|
163
|
+
if (true === row.duplicate) {
|
|
164
|
+
lines.push(`${NextStepCopy.DUPLICATE}${claudeRemoveUserCommand()}`);
|
|
165
|
+
}
|
|
166
|
+
if (row.stale !== undefined) {
|
|
167
|
+
lines.push(`${NextStepCopy.STALE}${row.stale}`);
|
|
168
|
+
}
|
|
169
|
+
return lines;
|
|
170
|
+
}),
|
|
171
|
+
];
|
|
172
|
+
}
|
|
173
|
+
/** A row whose client has Reticle in its config, so a connection can be waited for. */
|
|
174
|
+
function isRegistered(row) {
|
|
175
|
+
return AgentReg.PLUGIN === row.reg || AgentReg.ADDED === row.reg || AgentReg.ALREADY === row.reg;
|
|
176
|
+
}
|
|
177
|
+
/** The clients worth waiting for: the ones that have Reticle registered. */
|
|
178
|
+
export function awaitedClients(rows) {
|
|
179
|
+
return rows.filter(isRegistered).map((row) => AGENT_STEPS[row.id].agent);
|
|
180
|
+
}
|
|
181
|
+
function missingStep(row) {
|
|
182
|
+
if (AgentReg.PLUGIN === row.reg) {
|
|
183
|
+
return NextStepCopy.PLUGIN_NOT_CONNECTED;
|
|
184
|
+
}
|
|
185
|
+
if (isRegistered(row)) {
|
|
186
|
+
return AGENT_STEPS[row.id].step;
|
|
187
|
+
}
|
|
188
|
+
return registrationText(row);
|
|
189
|
+
}
|
|
190
|
+
/** Per client: connected, or the exact step still missing. */
|
|
191
|
+
export function agentStatuses(rows, connected) {
|
|
192
|
+
return rows.map((row) => {
|
|
193
|
+
const { name, agent } = AGENT_STEPS[row.id];
|
|
194
|
+
if (isRegistered(row) && connected.has(agent)) {
|
|
195
|
+
return {
|
|
196
|
+
name,
|
|
197
|
+
connected: true,
|
|
198
|
+
said: `${NextStepCopy.CONNECTED_HEAD}${name}${NextStepCopy.CONNECTED_TAIL}`,
|
|
199
|
+
};
|
|
200
|
+
}
|
|
201
|
+
const next = missingStep(row);
|
|
202
|
+
const said = isRegistered(row)
|
|
203
|
+
? `${name}${NextStepCopy.NOT_CONNECTED}${next}`
|
|
204
|
+
: `${name} ${next}`;
|
|
205
|
+
return { name, connected: false, next, said };
|
|
206
|
+
});
|
|
207
|
+
}
|
|
208
|
+
/** The lines the installer prints after its wait. */
|
|
209
|
+
export function connectionLines(rows, connected) {
|
|
210
|
+
return agentStatuses(rows, connected).map((status) => status.said);
|
|
211
|
+
}
|
|
212
|
+
/**
|
|
213
|
+
* Poll `read` until every client in `want` has connected, or `budgetMs` has passed. Returns the
|
|
214
|
+
* last read. Bounded: it never waits longer than the budget.
|
|
215
|
+
*/
|
|
216
|
+
export async function waitForAgents(want, read, clock, budgetMs, pollMs) {
|
|
217
|
+
const start = clock.now();
|
|
218
|
+
let seen = read();
|
|
219
|
+
while (!want.every((client) => seen.has(client)) && clock.now() - start < budgetMs) {
|
|
220
|
+
await clock.sleep(pollMs);
|
|
221
|
+
seen = read();
|
|
222
|
+
}
|
|
223
|
+
return seen;
|
|
224
|
+
}
|
|
225
|
+
const CLAUDE_CONFIG_DIR_ENV = 'CLAUDE_CONFIG_DIR';
|
|
226
|
+
/** `$CLAUDE_CONFIG_DIR`, else home: where Claude Code keeps `.claude.json`. */
|
|
227
|
+
export function claudeStateDir() {
|
|
228
|
+
return process.env[CLAUDE_CONFIG_DIR_ENV] ?? homedir();
|
|
229
|
+
}
|
|
230
|
+
const CLAUDE_STATE_FILE = '.claude.json';
|
|
231
|
+
/**
|
|
232
|
+
* Does this client's config name a `reticle` server, in any shape?
|
|
233
|
+
*
|
|
234
|
+
* Presence, not "the entry we would write now". An entry that starts an installed launcher, or an
|
|
235
|
+
* older npx entry, still starts Reticle. `setup mcp` repairs a stale entry; `status` only reports
|
|
236
|
+
* whether one is there. A file that does not parse reads as absent.
|
|
237
|
+
*/
|
|
238
|
+
function configNamesReticle(spec, existing) {
|
|
239
|
+
if (ConfigFormat.TOML === spec.format) {
|
|
240
|
+
return codexNamesOurServer(existing, spec.serversKey);
|
|
241
|
+
}
|
|
242
|
+
if (null === existing) {
|
|
243
|
+
return false;
|
|
244
|
+
}
|
|
245
|
+
try {
|
|
246
|
+
const parsed = JSON.parse(existing);
|
|
247
|
+
const servers = 'object' === typeof parsed && null !== parsed
|
|
248
|
+
? parsed[spec.serversKey]
|
|
249
|
+
: undefined;
|
|
250
|
+
return 'object' === typeof servers && null !== servers && MCP_SERVER_NAME in servers;
|
|
251
|
+
}
|
|
252
|
+
catch {
|
|
253
|
+
return false;
|
|
254
|
+
}
|
|
255
|
+
}
|
|
256
|
+
/**
|
|
257
|
+
* The agents on this machine and whether Reticle is in each config, read only. For `reticle status`,
|
|
258
|
+
* which must not register anything and must not spawn a client CLI.
|
|
259
|
+
*
|
|
260
|
+
* @param claudeStateDir the directory that holds `.claude.json`: `$CLAUDE_CONFIG_DIR`, else home.
|
|
261
|
+
* @param launcher the installed bin a new registration would use, for the stale-entry fix.
|
|
262
|
+
*/
|
|
263
|
+
export function installedAgentRows(io, projectDir, pluginActive, claudeStateDir = io.homeDir(), launcher) {
|
|
264
|
+
const rows = [];
|
|
265
|
+
if (pluginActive) {
|
|
266
|
+
// A user-scope entry next to the plugin is a second server.
|
|
267
|
+
rows.push(pluginRow(McpClient.CLAUDE_CODE, claudeHasReticle(io, undefined, claudeStateDir)));
|
|
268
|
+
}
|
|
269
|
+
else if (claudeHasReticle(io, projectDir, claudeStateDir)) {
|
|
270
|
+
rows.push(alreadyRow(McpClient.CLAUDE_CODE, claudeUserEntryIsFloating(io, claudeStateDir), launcher));
|
|
271
|
+
}
|
|
272
|
+
else if (io.exists(join(claudeStateDir, CLAUDE_STATE_FILE))) {
|
|
273
|
+
rows.push({ id: McpClient.CLAUDE_CODE, reg: AgentReg.MISSING });
|
|
274
|
+
}
|
|
275
|
+
for (const client of detectMcpClients(io)) {
|
|
276
|
+
rows.push({
|
|
277
|
+
id: client.id,
|
|
278
|
+
reg: configNamesReticle(clientSpec(client.id), client.existing)
|
|
279
|
+
? AgentReg.ALREADY
|
|
280
|
+
: AgentReg.MISSING,
|
|
281
|
+
});
|
|
282
|
+
}
|
|
283
|
+
return rows;
|
|
284
|
+
}
|
|
@@ -13,8 +13,11 @@ import { AgentAction, AGENT_CLIENTS, joinFor, parentOf, } from './agent-configs.
|
|
|
13
13
|
/** Two spaces, because that is what every one of these files already uses. */
|
|
14
14
|
const INDENT = 2;
|
|
15
15
|
const RETICLE_KEY = 'reticle';
|
|
16
|
-
/**
|
|
17
|
-
|
|
16
|
+
/**
|
|
17
|
+
* A fresh Continue config. YAML is only ever created, never edited. The args are a JSON array,
|
|
18
|
+
* which is valid YAML flow syntax and keeps a Windows path escaped.
|
|
19
|
+
*/
|
|
20
|
+
const continueYaml = (entry) => `mcpServers:\n - name: ${RETICLE_KEY}\n type: stdio\n command: ${String(entry['command'])}\n args: ${JSON.stringify(entry['args'])}\n`;
|
|
18
21
|
/**
|
|
19
22
|
* Write what the plan says to write, and nothing else.
|
|
20
23
|
*
|
|
@@ -32,8 +35,8 @@ export function applyAgentPlan(plan, io) {
|
|
|
32
35
|
}
|
|
33
36
|
try {
|
|
34
37
|
io.mkdirp(parentOf(step.file));
|
|
35
|
-
if (AgentAction.CREATE_YAML === step.action) {
|
|
36
|
-
io.writeFile(step.file,
|
|
38
|
+
if (AgentAction.CREATE_YAML === step.action && undefined !== step.entry) {
|
|
39
|
+
io.writeFile(step.file, continueYaml(step.entry));
|
|
37
40
|
return { ...base, action: 'created' };
|
|
38
41
|
}
|
|
39
42
|
const key = step.key;
|
|
@@ -99,10 +102,12 @@ export function applyAgentSkills(io, where) {
|
|
|
99
102
|
const join = joinFor(where.platform);
|
|
100
103
|
for (const client of AGENT_CLIENTS) {
|
|
101
104
|
const dir = client.skillDir;
|
|
102
|
-
if (undefined === dir)
|
|
105
|
+
if (undefined === dir) {
|
|
103
106
|
continue;
|
|
104
|
-
|
|
107
|
+
}
|
|
108
|
+
if (!io.exists(join(where.home, client.marker[where.platform]))) {
|
|
105
109
|
continue;
|
|
110
|
+
}
|
|
106
111
|
try {
|
|
107
112
|
const target = join(where.home, dir);
|
|
108
113
|
io.mkdirp(target);
|
|
@@ -1,6 +1,7 @@
|
|
|
1
1
|
import { homedir } from 'node:os';
|
|
2
2
|
import { spanSync } from '../../../trace.js';
|
|
3
3
|
import { hookBinFor } from '../../cli/hook/hook-fast.js';
|
|
4
|
+
import { mcpLauncher } from '../mcp-launcher.js';
|
|
4
5
|
import { reportInitOutcome } from '../../../telemetry/init-telemetry.js';
|
|
5
6
|
import { reportOnboardingStep } from '../../../telemetry/onboarding-funnel.js';
|
|
6
7
|
import { defaultPairingTokenDir, readOrCreatePairingTokenSync, } from '../../../portal/bridge/pairing-token.js';
|
|
@@ -44,6 +45,7 @@ export function serverInitHost() {
|
|
|
44
45
|
hookBin() {
|
|
45
46
|
return hookBinFor(homedir(), process.platform);
|
|
46
47
|
},
|
|
48
|
+
mcpLauncher,
|
|
47
49
|
insideClaudeCode() {
|
|
48
50
|
return CLAUDE_CODE_MARKER_VALUE === process.env[CLAUDE_CODE_MARKER_ENV];
|
|
49
51
|
},
|
|
@@ -53,7 +53,12 @@ interface InitRuntimeArgs {
|
|
|
53
53
|
interface RuntimePrintIo {
|
|
54
54
|
readonly print: (line: string) => void;
|
|
55
55
|
}
|
|
56
|
+
/**
|
|
57
|
+
* Print the "Your agents:" block, and return the bounded wait for the agents to connect. `mayWait`
|
|
58
|
+
* false: the wait is a no-op. The caller adds the terminal check. See `agentsBlock`.
|
|
59
|
+
*/
|
|
60
|
+
export type AgentsBlockPort = (print: (line: string) => void, mayWait: boolean) => () => Promise<void>;
|
|
56
61
|
export declare function continueAfterInit(parsed: InitRuntimeArgs, result: InitResult, io: RuntimePrintIo, cwd: string,
|
|
57
62
|
/** init's ending once the connection is proved: coverage and the agent prompt. See `FirstFlowPort`. */
|
|
58
|
-
firstFlow?: FirstFlowPort): Promise<void>;
|
|
63
|
+
firstFlow?: FirstFlowPort, agentsBlock?: AgentsBlockPort): Promise<void>;
|
|
59
64
|
export {};
|
|
@@ -7,7 +7,7 @@
|
|
|
7
7
|
*/
|
|
8
8
|
import { existsSync, readFileSync, writeFileSync } from 'node:fs';
|
|
9
9
|
import { join } from 'node:path';
|
|
10
|
-
import { InitConfirmation, RETICLE_DEFAULT_PORT } from '@reticlehq/core';
|
|
10
|
+
import { InitConfirmation, OnboardingPhase, OnboardingStop, RETICLE_DEFAULT_PORT, } from '@reticlehq/core';
|
|
11
11
|
import { COMMUNITY_HINT, FEEDBACK_HINT, Framework, InitFailure, } from '@reticlehq/init';
|
|
12
12
|
import { confirmInstall, nodeConfirmDeps } from '../terminal/confirm.js';
|
|
13
13
|
import { writeLicenseKey } from '../license-key.js';
|
|
@@ -24,11 +24,14 @@ import { collectEnv, DEFAULT_PHASE_TIMEOUT_MS } from '../setup-options.js';
|
|
|
24
24
|
import { staticPageDevCommand } from '../bringup/static-page-server.js';
|
|
25
25
|
import { SetupPhase } from '../run-setup.js';
|
|
26
26
|
import { reportInitOutcome } from '../../../telemetry/init-telemetry.js';
|
|
27
|
+
import { reportOnboardingStop } from '../../../telemetry/onboarding-funnel.js';
|
|
27
28
|
/** Claude Code marks every process it spawns with this variable set to this value. */
|
|
28
29
|
const CLAUDE_CODE_ENV = 'CLAUDECODE';
|
|
29
30
|
const CLAUDE_CODE_MARKER = '1';
|
|
30
31
|
/** How often the runtime phases look again: fast enough not to be the wait, slow enough to be free. */
|
|
31
32
|
const POLL_MS = 250;
|
|
33
|
+
/** The step an agent without the `reticle_*` tools cannot take. */
|
|
34
|
+
const FIRST_LOOK_STEP = 'first_look';
|
|
32
35
|
/**
|
|
33
36
|
* The shapes whose served HTML carries the SDK marker: Vite injects it into index.html, and a plain
|
|
34
37
|
* HTML page has the snippet pasted in. Everything else connects from the JS bundle, where a fetch
|
|
@@ -47,8 +50,9 @@ const NEXT_APP_CONNECT = ['app', 'src/app'].flatMap((d) => ['tsx', 'jsx', 'js'].
|
|
|
47
50
|
* names chunks, not modules, so it stays silent.
|
|
48
51
|
*/
|
|
49
52
|
export function htmlCarriesSdk(framework, exists) {
|
|
50
|
-
if (HTML_CARRIES_SDK.has(framework))
|
|
53
|
+
if (HTML_CARRIES_SDK.has(framework)) {
|
|
51
54
|
return true;
|
|
55
|
+
}
|
|
52
56
|
return Framework.NEXT === framework && NEXT_APP_CONNECT.some(exists);
|
|
53
57
|
}
|
|
54
58
|
/** Registration is global MCP wiring, so both flags that disown it are read here. */
|
|
@@ -68,8 +72,9 @@ const licenseIo = {
|
|
|
68
72
|
* thing as an install working.
|
|
69
73
|
*/
|
|
70
74
|
function printRelaunch(parsed, io, cwd) {
|
|
71
|
-
if (true !== parsed.relaunch)
|
|
75
|
+
if (true !== parsed.relaunch) {
|
|
72
76
|
return;
|
|
77
|
+
}
|
|
73
78
|
io.print(relaunchDecision({
|
|
74
79
|
...(undefined === process.env['CLAUDE_CODE_SESSION_ID']
|
|
75
80
|
? {}
|
|
@@ -82,13 +87,15 @@ function printRelaunch(parsed, io, cwd) {
|
|
|
82
87
|
}
|
|
83
88
|
export async function continueAfterInit(parsed, result, io, cwd,
|
|
84
89
|
/** init's ending once the connection is proved: coverage and the agent prompt. See `FirstFlowPort`. */
|
|
85
|
-
firstFlow) {
|
|
90
|
+
firstFlow, agentsBlock) {
|
|
86
91
|
const port = parsed.port ?? RETICLE_DEFAULT_PORT;
|
|
87
92
|
const stop = () => {
|
|
88
|
-
if (true === parsed.keepGoing)
|
|
93
|
+
if (true === parsed.keepGoing) {
|
|
89
94
|
process.exitCode = 1;
|
|
90
|
-
|
|
95
|
+
}
|
|
96
|
+
else {
|
|
91
97
|
process.exit(1);
|
|
98
|
+
}
|
|
92
99
|
};
|
|
93
100
|
// Real installs persist the key whichever way setup ends. A preview must leave .env and
|
|
94
101
|
// .gitignore untouched, just as it leaves project wiring and agent configuration untouched.
|
|
@@ -114,16 +121,19 @@ firstFlow) {
|
|
|
114
121
|
registerOtherAgents(io.print);
|
|
115
122
|
}
|
|
116
123
|
return confirmInstall(result, io, nodeConfirmDeps(port)).then(() => {
|
|
117
|
-
if (true === parsed.json)
|
|
124
|
+
if (true === parsed.json) {
|
|
118
125
|
process.stdout.write(`${JSON.stringify(result, null, 2)}\n`);
|
|
119
|
-
|
|
126
|
+
}
|
|
127
|
+
if (!result.ok) {
|
|
120
128
|
stop();
|
|
129
|
+
}
|
|
121
130
|
});
|
|
122
131
|
}
|
|
123
132
|
// From here on init left the ask to us, so it is the last thing on every exit below.
|
|
124
133
|
const ask = (spaced = true) => {
|
|
125
|
-
if (spaced)
|
|
134
|
+
if (spaced) {
|
|
126
135
|
io.print('');
|
|
136
|
+
}
|
|
127
137
|
io.print(FEEDBACK_HINT);
|
|
128
138
|
};
|
|
129
139
|
// `init` deferred its outcome to this phase, so every exit below reports it, and only once: the
|
|
@@ -131,8 +141,9 @@ firstFlow) {
|
|
|
131
141
|
// would double the funnel this event exists to measure.
|
|
132
142
|
let outcomeReported = false;
|
|
133
143
|
const reportOnce = (init) => {
|
|
134
|
-
if (outcomeReported)
|
|
144
|
+
if (outcomeReported) {
|
|
135
145
|
return;
|
|
146
|
+
}
|
|
136
147
|
outcomeReported = true;
|
|
137
148
|
reportInitOutcome(init);
|
|
138
149
|
};
|
|
@@ -140,11 +151,13 @@ firstFlow) {
|
|
|
140
151
|
if (context === undefined) {
|
|
141
152
|
// Nothing was established, so there is nothing to run against. init has already said why, and
|
|
142
153
|
// the outcome it deferred (a failed dependency install, usually) is reported here or nowhere.
|
|
143
|
-
if (result.outcome !== undefined)
|
|
154
|
+
if (result.outcome !== undefined) {
|
|
144
155
|
reportOnce(result.outcome);
|
|
156
|
+
}
|
|
145
157
|
ask();
|
|
146
|
-
if (true === parsed.json)
|
|
158
|
+
if (true === parsed.json) {
|
|
147
159
|
process.stdout.write(`${JSON.stringify(result, null, 2)}\n`);
|
|
160
|
+
}
|
|
148
161
|
stop();
|
|
149
162
|
return;
|
|
150
163
|
}
|
|
@@ -188,6 +201,9 @@ firstFlow) {
|
|
|
188
201
|
// A Ctrl-C during the wait ends the process with no `finally`, so the outcome is reported from the
|
|
189
202
|
// signal itself. The send is detached, so it survives the exit that follows.
|
|
190
203
|
const releaseInterrupt = reportOnInterrupt(() => reportFailure(InitFailure.INTERRUPTED));
|
|
204
|
+
// The wait for the agents runs last, after the interrupt handler is gone: a Ctrl+C that skips the
|
|
205
|
+
// wait is not a failed init.
|
|
206
|
+
let waitForAgents = () => Promise.resolve();
|
|
191
207
|
return runSetupCommand({
|
|
192
208
|
appDir: context.appDir,
|
|
193
209
|
invokedAt: cwd,
|
|
@@ -217,10 +233,12 @@ firstFlow) {
|
|
|
217
233
|
// A plain page with no dev script gets Reticle's own static server, so the page is actually
|
|
218
234
|
// served and the connect can be proved, instead of stopping at "start the app yourself".
|
|
219
235
|
...(() => {
|
|
220
|
-
if (undefined !== context.devCommand)
|
|
236
|
+
if (undefined !== context.devCommand) {
|
|
221
237
|
return { devCommand: context.devCommand };
|
|
222
|
-
|
|
238
|
+
}
|
|
239
|
+
if (undefined !== parsed.url) {
|
|
223
240
|
return {};
|
|
241
|
+
}
|
|
224
242
|
const devCommand = staticPageDevCommand({
|
|
225
243
|
appDir: context.appDir,
|
|
226
244
|
isStaticPage: Framework.HTML === context.framework,
|
|
@@ -249,16 +267,18 @@ firstFlow) {
|
|
|
249
267
|
outcome.reachedPhase === SetupPhase.DEV_SERVER
|
|
250
268
|
? InitFailure.DEV_SERVER
|
|
251
269
|
: InitFailure.APP_CONNECTION;
|
|
252
|
-
if (outcome.stop !== undefined)
|
|
270
|
+
if (outcome.stop !== undefined) {
|
|
253
271
|
init.stop = outcome.stop;
|
|
272
|
+
}
|
|
254
273
|
}
|
|
255
274
|
reportOnce(init);
|
|
256
275
|
}
|
|
257
276
|
// One object, so an agent reads a result instead of interpreting a report.
|
|
258
277
|
if (true === parsed.json) {
|
|
259
278
|
process.stdout.write(`${JSON.stringify(outcome, null, 2)}\n`);
|
|
260
|
-
if (!outcome.ok)
|
|
279
|
+
if (!outcome.ok) {
|
|
261
280
|
stop();
|
|
281
|
+
}
|
|
262
282
|
return;
|
|
263
283
|
}
|
|
264
284
|
io.print('');
|
|
@@ -274,7 +294,18 @@ firstFlow) {
|
|
|
274
294
|
});
|
|
275
295
|
if (undefined !== restart) {
|
|
276
296
|
io.print(restart);
|
|
297
|
+
// The same block and wait as `reticle setup mcp`. No wait for a failed setup, or inside
|
|
298
|
+
// `reticle connect`, which signs in next.
|
|
299
|
+
if (agentsBlock !== undefined) {
|
|
300
|
+
waitForAgents = agentsBlock(io.print, outcome.ok && true !== parsed.keepGoing);
|
|
301
|
+
}
|
|
277
302
|
io.print('');
|
|
303
|
+
// The agent cannot take its first look until it restarts. Detached: init exits soon.
|
|
304
|
+
reportOnboardingStop({
|
|
305
|
+
phase: OnboardingPhase.ONBOARD,
|
|
306
|
+
step: FIRST_LOOK_STEP,
|
|
307
|
+
stop: OnboardingStop.AGENT_NOT_RESTARTED,
|
|
308
|
+
}, { detach: true });
|
|
278
309
|
}
|
|
279
310
|
if (outcome.ok && !outcome.flowSaved) {
|
|
280
311
|
// Success, and no flow. Saying "a flow was driven" here would replace a wrong exit code with
|
|
@@ -282,8 +313,9 @@ firstFlow) {
|
|
|
282
313
|
// sentence is read by a person deciding whether their app is verified. It is not.
|
|
283
314
|
// The connect already said "connected, nothing verified yet" and listed what to do next;
|
|
284
315
|
// repeating it here was the same sentence twice, three lines apart.
|
|
285
|
-
for (const [i, step] of outcome.fallback.entries())
|
|
316
|
+
for (const [i, step] of outcome.fallback.entries()) {
|
|
286
317
|
io.print(` ${String(i + 1)}. ${step}`);
|
|
318
|
+
}
|
|
287
319
|
// Already spaced by the blank line above when nothing was listed.
|
|
288
320
|
ask(0 < outcome.fallback.length);
|
|
289
321
|
io.print(COMMUNITY_HINT);
|
|
@@ -299,15 +331,17 @@ firstFlow) {
|
|
|
299
331
|
// A run that produced no verdict did not succeed, and the exit code is the one place a caller
|
|
300
332
|
// reads that without parsing anything.
|
|
301
333
|
io.print('⚠ setup did not finish. To carry on from here:');
|
|
302
|
-
for (const [i, step] of outcome.fallback.entries())
|
|
334
|
+
for (const [i, step] of outcome.fallback.entries()) {
|
|
303
335
|
io.print(` ${i + 1}. ${step}`);
|
|
336
|
+
}
|
|
304
337
|
ask();
|
|
305
338
|
stop();
|
|
306
339
|
}, (error) => {
|
|
307
340
|
reportFailure(InitFailure.RUNTIME_ERROR);
|
|
308
341
|
throw error;
|
|
309
342
|
})
|
|
310
|
-
.finally(releaseInterrupt)
|
|
343
|
+
.finally(releaseInterrupt)
|
|
344
|
+
.then(() => waitForAgents());
|
|
311
345
|
}
|
|
312
346
|
const INTERRUPT_SIGNALS = ['SIGINT', 'SIGTERM'];
|
|
313
347
|
/** 128 + the signal number, which is what a shell reports. */
|
|
@@ -319,14 +353,16 @@ function reportOnInterrupt(report) {
|
|
|
319
353
|
report();
|
|
320
354
|
// Before setup installs its own handler ours may be the only one, and a listener present
|
|
321
355
|
// means Node no longer exits on the signal by itself: without this, Ctrl-C would be ignored.
|
|
322
|
-
if (1 === process.listenerCount(signal))
|
|
356
|
+
if (1 === process.listenerCount(signal)) {
|
|
323
357
|
process.exit(INTERRUPT_EXIT_CODE[signal]);
|
|
358
|
+
}
|
|
324
359
|
};
|
|
325
360
|
process.on(signal, handler);
|
|
326
361
|
return { signal, handler };
|
|
327
362
|
});
|
|
328
363
|
return () => {
|
|
329
|
-
for (const { signal, handler } of handlers)
|
|
364
|
+
for (const { signal, handler } of handlers) {
|
|
330
365
|
process.off(signal, handler);
|
|
366
|
+
}
|
|
331
367
|
};
|
|
332
368
|
}
|
|
@@ -0,0 +1,11 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* The bin that agent configs start the MCP server from, when this CLI is an install that stays on
|
|
3
|
+
* disk. Undefined for npx's cache and other run-once copies: see `installedLauncher` in init.
|
|
4
|
+
*/
|
|
5
|
+
import { fileURLToPath } from 'node:url';
|
|
6
|
+
import { installedLauncher } from '@reticlehq/init';
|
|
7
|
+
/** `bin/reticle.js`, from `dist/command/setup/mcp-launcher.js`. The real path, not a symlink. */
|
|
8
|
+
export const CLI_BIN = fileURLToPath(new URL('../../../bin/reticle.js', import.meta.url));
|
|
9
|
+
export function mcpLauncher() {
|
|
10
|
+
return installedLauncher(CLI_BIN);
|
|
11
|
+
}
|