@reticlehq/server 3.4.0 → 3.5.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/command/cli/auth/cloud-session.d.ts +4 -4
- package/dist/command/cli/auth/device-flow.d.ts +2 -2
- package/dist/command/cli/cli-parse.d.ts +1 -1
- package/dist/command/cli/cli-parse.js +3 -1
- package/dist/command/cli/cloud-cli.js +59 -0
- package/dist/command/cli/cloud-login.d.ts +1 -1
- package/dist/command/cli/cloud-login.js +8 -7
- package/dist/command/cli/setup-mcp-cli.js +9 -1
- package/dist/command/cli.js +17 -1
- package/dist/command/setup/bringup/dev-server-wait.d.ts +2 -0
- package/dist/command/setup/bringup/dev-server-wait.js +1 -1
- package/dist/command/setup/init/init-runtime.js +53 -8
- package/dist/command/setup/node-effects.js +6 -1
- package/dist/command/setup/run-setup.d.ts +2 -0
- package/dist/command/setup/run-setup.js +7 -2
- package/dist/command/setup/setup-mcp.d.ts +2 -0
- package/dist/command/setup/setup-mcp.js +14 -5
- package/dist/command/version/peer-announce.d.ts +6 -2
- package/dist/command/version/peer-announce.js +8 -6
- package/dist/command/version/version-nudge.d.ts +17 -3
- package/dist/command/version/version-nudge.js +25 -4
- package/dist/features/harness/fill-values.d.ts +4 -0
- package/dist/features/harness/fill-values.js +47 -11
- package/dist/features/harness/openai-driver.d.ts +12 -0
- package/dist/features/harness/openai-driver.js +17 -5
- package/dist/features/license/license.d.ts +2 -2
- package/dist/index.js +17 -9
- package/dist/judgement/capsule/capsule-store.d.ts +4 -4
- package/dist/memory/cloud/sync-cycle.d.ts +9 -0
- package/dist/memory/cloud/sync-cycle.js +29 -2
- package/dist/memory/intent/intent-tools.js +8 -3
- package/dist/memory/journal/envelope.d.ts +8 -8
- package/dist/memory/journal/journal-query.d.ts +14 -0
- package/dist/memory/journal/journal-query.js +15 -0
- package/dist/memory/project/artifact-root-resolver.d.ts +10 -0
- package/dist/memory/project/artifact-root-resolver.js +46 -19
- package/dist/portal/bridge/bridge.d.ts +4 -3
- package/dist/portal/bridge/bridge.js +6 -1
- package/dist/portal/input/viewport-tools.js +16 -6
- package/dist/portal/session/session.d.ts +5 -8
- package/dist/portal/session/session.js +9 -13
- package/dist/surface/http-server.d.ts +1 -1
- package/dist/surface/http-server.js +4 -3
- package/dist/surface/mcp/mcp.js +1 -1
- package/dist/surface/mcp/proxy/proxy-log.js +3 -5
- package/dist/surface/tools/act-tools.js +2 -2
- package/dist/surface/tools/dynamic-tools.js +29 -2
- package/dist/surface/tools/error-recovery.js +1 -2
- package/dist/surface/tools/harness-explore.js +5 -1
- package/dist/surface/tools/invoke-tool.js +12 -8
- package/dist/surface/tools/tool-kit.d.ts +10 -0
- package/dist/telemetry/argument-shape.js +1 -1
- package/dist/telemetry/onboarding-firsts.d.ts +2 -2
- package/dist/telemetry/onboarding-firsts.js +2 -2
- package/docs/actions.mdx +1 -2
- package/docs/cli/cloud.mdx +6 -4
- package/docs/cli.mdx +13 -7
- package/docs/desktop.mdx +1 -1
- package/docs/getting-started.md +12 -2
- package/docs/install-agentic.mdx +1 -1
- package/docs/install-manual.mdx +1 -1
- package/docs/packages/next.mdx +3 -0
- package/docs/packages/tauri.mdx +1 -1
- package/docs/packages/vite-plugin.mdx +2 -0
- package/docs/packages.mdx +1 -1
- package/docs/quickstart.mdx +11 -9
- package/docs/tools/act.mdx +1 -1
- package/docs/usage.md +0 -1
- package/package.json +5 -5
|
@@ -21,17 +21,17 @@ declare const SessionSchema: z.ZodObject<{
|
|
|
21
21
|
/** Who signed in. Absent in sessions written before it was recorded. */
|
|
22
22
|
email: z.ZodOptional<z.ZodString>;
|
|
23
23
|
}, "strip", z.ZodTypeAny, {
|
|
24
|
-
url: string;
|
|
25
24
|
token: string;
|
|
25
|
+
url: string;
|
|
26
26
|
orgName: string;
|
|
27
|
-
email?: string | undefined;
|
|
28
27
|
orgId?: string | undefined;
|
|
28
|
+
email?: string | undefined;
|
|
29
29
|
}, {
|
|
30
|
-
url: string;
|
|
31
30
|
token: string;
|
|
31
|
+
url: string;
|
|
32
32
|
orgName: string;
|
|
33
|
-
email?: string | undefined;
|
|
34
33
|
orgId?: string | undefined;
|
|
34
|
+
email?: string | undefined;
|
|
35
35
|
}>;
|
|
36
36
|
export type Session = z.infer<typeof SessionSchema>;
|
|
37
37
|
/** Trailing slashes are not identity: `https://x/` and `https://x` are one host. */
|
|
@@ -53,18 +53,18 @@ export declare const DevicePollSchema: z.ZodObject<{
|
|
|
53
53
|
}>>;
|
|
54
54
|
}, "strip", z.ZodTypeAny, {
|
|
55
55
|
status: string;
|
|
56
|
+
token?: string | undefined;
|
|
56
57
|
org?: {
|
|
57
58
|
name: string;
|
|
58
59
|
id?: string | undefined;
|
|
59
60
|
} | undefined;
|
|
60
|
-
token?: string | undefined;
|
|
61
61
|
}, {
|
|
62
62
|
status: string;
|
|
63
|
+
token?: string | undefined;
|
|
63
64
|
org?: {
|
|
64
65
|
name: string;
|
|
65
66
|
id?: string | undefined;
|
|
66
67
|
} | undefined;
|
|
67
|
-
token?: string | undefined;
|
|
68
68
|
}>;
|
|
69
69
|
/**
|
|
70
70
|
* Best-effort open the approval page in the default browser.
|
|
@@ -15,7 +15,7 @@ export { AGENT_FLAG, BUG_FLAG, FEEDBACK_KINDS, KIND_FLAG, RATING_FLAG, } from '.
|
|
|
15
15
|
* telling readers to do exactly that on 110 lines before it was caught, so the invocation now leads
|
|
16
16
|
* the usage block rather than being a footnote somewhere else.
|
|
17
17
|
*/
|
|
18
|
-
export declare const CLI_USAGE = "usage: npx @reticlehq/server <command> (or `reticle <command>` once the bin is on your PATH)\n\n reticle init [--dry-run] [--port N] [--no-mcp] [--no-install] [--app <dir>]\n [--env KEY=VALUE]... [--files-only] [--hooks] (wire Reticle into the project in this directory)\n init is ONBOARDING: it wires the project, boots the app and proves a session\n connected. It does not drive. The FIRST RUN is the stage that proves a flow:\n reticle verify <url> --explore --persona \"<who does what>\", or the same thing\n from an agent as reticle_verify { action: \"explore\", persona: \"\u2026\" }\n --app picks WHICH app in a monorepo, when several are found\n --env is what the app needs to reach a usable state: the key from\n .env.example, the mock backend, the variable that skips an auth wall.\n Repeatable, and a value may contain spaces and equals signs\n --files-only writes the files and stops, which is what init did before it\n learned to boot the app and prove the install works\n --relaunch prints the exact command that restarts THIS conversation with\n the tools loaded, so the restart is not a chore handed to a human. It\n refuses when the session id has no transcript behind it, because\n --resume on an empty id opens a blank conversation that looks like\n success. Works with --files-only\n --license writes the key to .env and keeps .env out of git\n --json puts the result on stdout, so an agent reads one object\n --no-open / --no-agents / --url / --timeout are the runtime dials: CI, a\n headless box, or an app you already run\n --no-mcp skips MORE than the server registration: also the agent rule files\n (CLAUDE.md / AGENTS.md / .cursor) and the /reticle command, because all three\n only make sense once the tools are reachable.\n reticle serve [--port N] [--drive <url>] [--headless] [--http] [--http-port N] [--http-token T]\n reticle stop [--port N] [--quiet] (stop the daemon we started, by its recorded pid)\n reticle kill [--port N] [--force] (free the port by its LISTENER, never the agent's mcp proxy)\n reticle restart [--port N] [--force] (kill, then start a daemon and wait for a real bind)\n reticle status [--port N] [--json] (a readable block; --json for the event)\n reticle doctor [--port N] (one command to diagnose setup: Chromium, daemon, port)\n reticle open [url] [--port N] (show the app: reuse the connected tab, else open one)\n reticle verify <url> [--port N] [--headed] [--timeout N] [--storage-state <file>] [--session-id <id>] (one-shot: drive the URL, verify saved flows, exit 0=pass)\n [--explore] [--persona <who>] (no saved flows? let Reticle drive the app itself and record them)\n [--select <label>] (repeatable: verify only flows carrying these labels \u2014 no model, exit 0=pass)\n [--expect '<json predicate>' | --expect-file <path>] (one verdict, no saved\n flows needed. --expect-file is the form no shell can mangle: cmd.exe keeps the\n single quotes the docs show, and PowerShell strips the inner double quotes, and\n a predicate that does not parse produces no verdict at all \u2014 asks the daemon that\n is already running, so nothing is bound and nothing is stopped. It NEEDS that\n daemon: with none on the port it refuses and names the flag, rather than reporting\n on your saved flows instead. Cannot be combined with --storage-state, which this\n path has nowhere to load. This is the path when your client never loaded the\n reticle_* tools. exit 0 ONLY on verified:\"yes\" \u2014 \"unknown\" is not a pass)\n reticle affected [--since <ref>] [file...] (which saved flows must re-verify for the changed files)\n reticle gate [--since <ref>] [--accept-coverage] [file...] (exit non-zero unless passing artifacts cover the affected flows)\n reticle report [--session <id>] [--hook] (what the latest session claimed, and what held)\n reticle setup mcp (register the MCP server with your agents; the installer's registration half)\n reticle capsules (list the saved fail-to-pass bug capsules in .reticle/capsules)\n reticle hunt <dir> (aggregate a directory of crawl reports into one false-green rate)\n reticle watch [url] (on save, report which saved flows must re-verify)\n reticle drive <url> [--headless] (foreground mode, for debugging)\n reticle mcp [--port N] [--drive <url>] [--headless] (MCP stdio proxy; auto-starts daemon if needed)\n reticle update (install the latest server version and restart)\n reticle rollback (restore the previous server version and restart)\n reticle license (show enterprise license status: active | eval | missing)\n reticle telemetry [status|enable|disable] (anonymous usage metrics; status shows what's sent + the policy)\n reticle feedback [--rating 1-5] [--bug] \"message\" (tell us what worked and what didn't; prints exactly what it sends)\n reticle feedback --agent --kind <bug|gap|ambiguity|feature_request|improvement|experience> \"message\"\n (agents: file from anywhere, including a setup that never finished)\n reticle identify --context company|side_project|open_source|learning [--company N] [--email E] [--forget]\n (OPT-IN: tell us who you are, e.g. for support or an enterprise trial)\n\nCloud (link this project to Reticle; runs/flows recorded on the dashboard):\n reticle login [--url <u>] [--email <e>] [--code <c>] [--org <n>]\n (browser device flow by default; --email mails a code)\n reticle logout [--url <u>] (sign out of ONE host; others stay signed in)\n reticle link [--project <name|id>] (bind this repo: mints a scoped key, writes .reticle/cloud.json)\n reticle whoami (who am I signed in as, and is this repo attached?)\n reticle project <ls|create <name>> (list or create cloud projects)\n reticle config [--runs on|off] [--memory on|off] [--flows on|off] [--verify local|server]\n reticle push | sync [--watch] (one sync cycle: send the difference, collect decisions)\n reticle runs | regression | share <runId> (read cloud state; regression exits 3 if any flow broke)\n\n'drive' shows the browser; everything else is hidden (serve/mcp own the pool behind leases, replay\nand the spec runner: batch work). --headed opts any of them in, --headless hides drive, CI hides it.";
|
|
18
|
+
export declare const CLI_USAGE = "usage: npx @reticlehq/server <command> (or `reticle <command>` once the bin is on your PATH)\n\n reticle init [--dry-run] [--port N] [--no-mcp] [--no-install] [--app <dir>]\n [--env KEY=VALUE]... [--files-only] [--hooks] (wire Reticle into the project in this directory)\n init is ONBOARDING: it wires the project, boots the app and proves a session\n connected. It does not drive. The FIRST RUN is the stage that proves a flow:\n reticle verify <url> --explore --persona \"<who does what>\", or the same thing\n from an agent as reticle_verify { action: \"explore\", persona: \"\u2026\" }\n --app picks WHICH app in a monorepo, when several are found\n --env is what the app needs to reach a usable state: the key from\n .env.example, the mock backend, the variable that skips an auth wall.\n Repeatable, and a value may contain spaces and equals signs\n --files-only writes the files and stops, which is what init did before it\n learned to boot the app and prove the install works\n --relaunch prints the exact command that restarts THIS conversation with\n the tools loaded, so the restart is not a chore handed to a human. It\n refuses when the session id has no transcript behind it, because\n --resume on an empty id opens a blank conversation that looks like\n success. Works with --files-only\n --license writes the key to .env and keeps .env out of git\n --json puts the result on stdout, so an agent reads one object\n --no-open / --no-agents / --url / --timeout are the runtime dials: CI, a\n headless box, or an app you already run\n --no-mcp skips MORE than the server registration: also the agent rule files\n (CLAUDE.md / AGENTS.md / .cursor) and the /reticle command, because all three\n only make sense once the tools are reachable.\n reticle serve [--port N] [--drive <url>] [--headless] [--http] [--http-port N] [--http-token T]\n reticle stop [--port N] [--quiet] (stop the daemon we started, by its recorded pid)\n reticle kill [--port N] [--force] (free the port by its LISTENER, never the agent's mcp proxy)\n reticle restart [--port N] [--force] (kill, then start a daemon and wait for a real bind)\n reticle status [--port N] [--json] (a readable block; --json for the event)\n reticle doctor [--port N] (one command to diagnose setup: Chromium, daemon, port)\n reticle open [url] [--port N] (show the app: reuse the connected tab, else open one)\n reticle verify <url> [--port N] [--headed] [--timeout N] [--storage-state <file>] [--session-id <id>] (one-shot: drive the URL, verify saved flows, exit 0=pass)\n [--explore] [--persona <who>] (no saved flows? let Reticle drive the app itself and record them)\n [--select <label>] (repeatable: verify only flows carrying these labels \u2014 no model, exit 0=pass)\n [--expect '<json predicate>' | --expect-file <path>] (one verdict, no saved\n flows needed. --expect-file is the form no shell can mangle: cmd.exe keeps the\n single quotes the docs show, and PowerShell strips the inner double quotes, and\n a predicate that does not parse produces no verdict at all \u2014 asks the daemon that\n is already running, so nothing is bound and nothing is stopped. It NEEDS that\n daemon: with none on the port it refuses and names the flag, rather than reporting\n on your saved flows instead. Cannot be combined with --storage-state, which this\n path has nowhere to load. This is the path when your client never loaded the\n reticle_* tools. exit 0 ONLY on verified:\"yes\" \u2014 \"unknown\" is not a pass)\n reticle affected [--since <ref>] [file...] (which saved flows must re-verify for the changed files)\n reticle gate [--since <ref>] [--accept-coverage] [file...] (exit non-zero unless passing artifacts cover the affected flows)\n reticle report [--session <id>] [--hook] (what the latest session claimed, and what held)\n reticle setup mcp (register the MCP server with your agents; the installer's registration half)\n reticle capsules (list the saved fail-to-pass bug capsules in .reticle/capsules)\n reticle hunt <dir> (aggregate a directory of crawl reports into one false-green rate)\n reticle watch [url] (on save, report which saved flows must re-verify)\n reticle drive <url> [--headless] (foreground mode, for debugging)\n reticle mcp [--port N] [--drive <url>] [--headless] (MCP stdio proxy; auto-starts daemon if needed)\n reticle update (install the latest server version and restart)\n reticle rollback (restore the previous server version and restart)\n reticle license (show enterprise license status: active | eval | missing)\n reticle telemetry [status|enable|disable] (anonymous usage metrics; status shows what's sent + the policy)\n reticle feedback [--rating 1-5] [--bug] \"message\" (tell us what worked and what didn't; prints exactly what it sends)\n reticle feedback --agent --kind <bug|gap|ambiguity|feature_request|improvement|experience> \"message\"\n (agents: file from anywhere, including a setup that never finished)\n reticle identify --context company|side_project|open_source|learning [--company N] [--email E] [--forget]\n (OPT-IN: tell us who you are, e.g. for support or an enterprise trial)\n\nCloud (link this project to Reticle; runs/flows recorded on the dashboard):\n reticle connect [--project <name|id>] [--url <cloud origin>]\n (wire this app if needed, sign in, link, send local history)\n reticle login [--url <u>] [--email <e>] [--code <c>] [--org <n>]\n (browser device flow by default; --email mails a code)\n reticle logout [--url <u>] (sign out of ONE host; others stay signed in)\n reticle link [--project <name|id>] (bind this repo: mints a scoped key, writes .reticle/cloud.json)\n reticle whoami (who am I signed in as, and is this repo attached?)\n reticle project <ls|create <name>> (list or create cloud projects)\n reticle config [--runs on|off] [--memory on|off] [--flows on|off] [--verify local|server]\n reticle push | sync [--watch] (one sync cycle: send the difference, collect decisions)\n reticle runs | regression | share <runId> (read cloud state; regression exits 3 if any flow broke)\n\n'drive' shows the browser; everything else is hidden (serve/mcp own the pool behind leases, replay\nand the spec runner: batch work). --headed opts any of them in, --headless hides drive, CI hides it.";
|
|
19
19
|
/** The `reticle telemetry` sub-actions. Bare `reticle telemetry` means `status`. */
|
|
20
20
|
export declare const TelemetryAction: {
|
|
21
21
|
readonly STATUS: "status";
|
|
@@ -85,6 +85,8 @@ export const CLI_USAGE = `usage: npx @reticlehq/server <command> (or \`reticl
|
|
|
85
85
|
(OPT-IN: tell us who you are, e.g. for support or an enterprise trial)
|
|
86
86
|
|
|
87
87
|
Cloud (link this project to Reticle; runs/flows recorded on the dashboard):
|
|
88
|
+
reticle connect [--project <name|id>] [--url <cloud origin>]
|
|
89
|
+
(wire this app if needed, sign in, link, send local history)
|
|
88
90
|
reticle login [--url <u>] [--email <e>] [--code <c>] [--org <n>]
|
|
89
91
|
(browser device flow by default; --email mails a code)
|
|
90
92
|
reticle logout [--url <u>] (sign out of ONE host; others stay signed in)
|
|
@@ -170,7 +172,7 @@ const KNOWN_COMMANDS = new Set([
|
|
|
170
172
|
FEEDBACK_COMMAND,
|
|
171
173
|
IDENTIFY_COMMAND,
|
|
172
174
|
DAEMON_INNER_COMMAND,
|
|
173
|
-
|
|
175
|
+
'connect',
|
|
174
176
|
'login',
|
|
175
177
|
'logout',
|
|
176
178
|
'whoami',
|
|
@@ -42,6 +42,7 @@ import { diskSink, diskSource, readCloudIssues, readCloudState } from '../../mem
|
|
|
42
42
|
*/
|
|
43
43
|
const CLOUD_COMMANDS = new Set([
|
|
44
44
|
'login',
|
|
45
|
+
'connect',
|
|
45
46
|
'logout',
|
|
46
47
|
'whoami',
|
|
47
48
|
'link',
|
|
@@ -438,6 +439,62 @@ const cmdLink = async (argv) => {
|
|
|
438
439
|
}
|
|
439
440
|
return 0;
|
|
440
441
|
};
|
|
442
|
+
/**
|
|
443
|
+
* `reticle connect` finishes the cloud half of first-run setup in one invocation. The CLI entry
|
|
444
|
+
* initializes an unwired app before calling this verb. A saved login is checked against this exact
|
|
445
|
+
* host before reuse; an expired one opens the normal browser approval flow. Login does not auto-link
|
|
446
|
+
* here because this command links once, to the project the caller named, then sends local history.
|
|
447
|
+
*/
|
|
448
|
+
const cmdConnect = async (argv) => {
|
|
449
|
+
for (let i = 0; i < argv.length; i += 2) {
|
|
450
|
+
const flag = argv[i];
|
|
451
|
+
const value = argv[i + 1];
|
|
452
|
+
if ((flag !== '--project' && flag !== '--url') ||
|
|
453
|
+
value === undefined ||
|
|
454
|
+
0 === value.length ||
|
|
455
|
+
value.startsWith('--')) {
|
|
456
|
+
err('usage: reticle connect [--project <name|id>] [--url <cloud origin>]');
|
|
457
|
+
return 2;
|
|
458
|
+
}
|
|
459
|
+
}
|
|
460
|
+
if (!(await createNodeFileSystem().exists(join(process.cwd(), RETICLE_CONFIG_BASENAME)))) {
|
|
461
|
+
err('this app is not wired yet — run `reticle init` in its directory, then `reticle connect`');
|
|
462
|
+
return 2;
|
|
463
|
+
}
|
|
464
|
+
const f = flags(argv);
|
|
465
|
+
const url = baseUrl(null, f['url']);
|
|
466
|
+
if (undefined === apiKeyFrom(process.env)) {
|
|
467
|
+
const session = await readSessionFor(url);
|
|
468
|
+
let signedIn = false;
|
|
469
|
+
if (session !== null) {
|
|
470
|
+
try {
|
|
471
|
+
await api('GET', `${url}/v1/me`, session.token);
|
|
472
|
+
signedIn = true;
|
|
473
|
+
}
|
|
474
|
+
catch {
|
|
475
|
+
hint('saved sign-in needs renewal — opening browser approval');
|
|
476
|
+
}
|
|
477
|
+
}
|
|
478
|
+
if (!signedIn) {
|
|
479
|
+
const code = await cmdLogin(['--url', url], cmdLink, false);
|
|
480
|
+
if (code !== 0)
|
|
481
|
+
return code;
|
|
482
|
+
}
|
|
483
|
+
}
|
|
484
|
+
const linkArgs = [
|
|
485
|
+
'--url',
|
|
486
|
+
url,
|
|
487
|
+
...(f['project'] === undefined ? [] : ['--project', f['project']]),
|
|
488
|
+
];
|
|
489
|
+
const linked = await cmdLink(linkArgs);
|
|
490
|
+
if (linked !== 0)
|
|
491
|
+
return linked;
|
|
492
|
+
const pushed = await cmdPush();
|
|
493
|
+
if (pushed !== 0)
|
|
494
|
+
return pushed;
|
|
495
|
+
hint('Connected. Open your coding agent and ask it to verify one flow in your app.');
|
|
496
|
+
return 0;
|
|
497
|
+
};
|
|
441
498
|
/** `reticle config [--runs on|off] [--memory on|off] [--flows on|off] [--verify local|server]`. */
|
|
442
499
|
const cmdConfig = async (argv) => {
|
|
443
500
|
const f = flags(argv);
|
|
@@ -661,6 +718,8 @@ export const runCloudCommand = async (argv) => {
|
|
|
661
718
|
// The linker is passed in rather than imported by cloud-login: login ends by linking and
|
|
662
719
|
// link needs a session, so the cycle is broken at this one call site.
|
|
663
720
|
return await cmdLogin(rest, cmdLink);
|
|
721
|
+
case 'connect':
|
|
722
|
+
return await cmdConnect(rest);
|
|
664
723
|
case 'logout':
|
|
665
724
|
return await cmdLogout(rest);
|
|
666
725
|
case 'whoami':
|
|
@@ -4,7 +4,7 @@ type Linker = (argv: readonly string[]) => Promise<number>;
|
|
|
4
4
|
* `reticle login` — browser device flow by default; `--email <e>` (or a positional email) keeps the
|
|
5
5
|
* headless two-step code path for CI/servers where opening a browser makes no sense.
|
|
6
6
|
*/
|
|
7
|
-
export declare const cmdLogin: (argv: readonly string[], link: Linker) => Promise<number>;
|
|
7
|
+
export declare const cmdLogin: (argv: readonly string[], link: Linker, autoLink?: boolean) => Promise<number>;
|
|
8
8
|
/** `reticle logout` — forget the cached session token (per-project keys under credentials.json stay). */
|
|
9
9
|
/**
|
|
10
10
|
* `reticle logout` — sign out of ONE host, not of everywhere.
|
|
@@ -48,7 +48,7 @@ const signedInEmail = async (url, token) => {
|
|
|
48
48
|
}
|
|
49
49
|
};
|
|
50
50
|
/** Persist a session token under ~/.reticle and print the next step. Shared by both login paths. */
|
|
51
|
-
const writeSession = async (url, token, orgName, orgId, project, link, email) => {
|
|
51
|
+
const writeSession = async (url, token, orgName, orgId, project, link, email, autoLink = true) => {
|
|
52
52
|
await mkdir(join(home(), SESSIONS_DIR), { recursive: true });
|
|
53
53
|
// Who signed in, so a flow saved on this machine can say which person made it (FlowFile.author).
|
|
54
54
|
const who = email ?? (await signedInEmail(url, token));
|
|
@@ -65,7 +65,8 @@ const writeSession = async (url, token, orgName, orgId, project, link, email) =>
|
|
|
65
65
|
await writeFile(sessionPath(home(), url), body);
|
|
66
66
|
await writeFile(join(home(), SESSION_FILE), body);
|
|
67
67
|
emit({ loggedIn: orgName, session: join(home(), SESSION_FILE) });
|
|
68
|
-
|
|
68
|
+
if (autoLink)
|
|
69
|
+
await linkAfterLogin(url, project, link);
|
|
69
70
|
};
|
|
70
71
|
/**
|
|
71
72
|
* Finish the job when the shell is already standing in an unlinked Reticle project.
|
|
@@ -124,7 +125,7 @@ const linkAfterLogin = async (url, project, link) => {
|
|
|
124
125
|
* Browser device flow — the DEFAULT `reticle login` (like `gh auth login`): fetch a device + user code,
|
|
125
126
|
* open the browser to approve, then poll until the user confirms. No email to type, no code to copy back.
|
|
126
127
|
*/
|
|
127
|
-
const cmdLoginDevice = async (explicitUrl, project, link) => {
|
|
128
|
+
const cmdLoginDevice = async (explicitUrl, project, link, autoLink) => {
|
|
128
129
|
const url = baseUrl(null, explicitUrl);
|
|
129
130
|
const started = DeviceStartSchema.parse(await api('POST', `${url}/v1/auth/device/start`, null, {}));
|
|
130
131
|
hint(`Opening ${started.verificationUri} — confirm this code in the browser: ${started.userCode}`);
|
|
@@ -134,7 +135,7 @@ const cmdLoginDevice = async (explicitUrl, project, link) => {
|
|
|
134
135
|
await sleep(intervalMs);
|
|
135
136
|
const poll = DevicePollSchema.parse(await api('POST', `${url}/v1/auth/device/token`, null, { deviceCode: started.deviceCode }));
|
|
136
137
|
if ('approved' === poll.status && poll.token !== undefined && poll.org !== undefined) {
|
|
137
|
-
await writeSession(url, poll.token, poll.org.name, poll.org.id, project, link);
|
|
138
|
+
await writeSession(url, poll.token, poll.org.name, poll.org.id, project, link, undefined, autoLink);
|
|
138
139
|
return 0;
|
|
139
140
|
}
|
|
140
141
|
if ('pending' === poll.status) {
|
|
@@ -154,12 +155,12 @@ const cmdLoginDevice = async (explicitUrl, project, link) => {
|
|
|
154
155
|
* `reticle login` — browser device flow by default; `--email <e>` (or a positional email) keeps the
|
|
155
156
|
* headless two-step code path for CI/servers where opening a browser makes no sense.
|
|
156
157
|
*/
|
|
157
|
-
export const cmdLogin = async (argv, link) => {
|
|
158
|
+
export const cmdLogin = async (argv, link, autoLink = true) => {
|
|
158
159
|
const f = flags(argv);
|
|
159
160
|
const positional = argv[0] !== undefined && !argv[0].startsWith('--') ? argv[0] : undefined;
|
|
160
161
|
const email = f['email'] ?? positional;
|
|
161
162
|
if (email === undefined)
|
|
162
|
-
return cmdLoginDevice(f['url'], f['project'], link);
|
|
163
|
+
return cmdLoginDevice(f['url'], f['project'], link, autoLink);
|
|
163
164
|
const org = f['org'];
|
|
164
165
|
const url = baseUrl(null, f['url']);
|
|
165
166
|
let code = f['code'];
|
|
@@ -177,7 +178,7 @@ export const cmdLogin = async (argv, link) => {
|
|
|
177
178
|
code = requested.devCode;
|
|
178
179
|
}
|
|
179
180
|
const parsed = LoginSchema.parse(await api('POST', `${url}/v1/auth/login`, null, { email, code }));
|
|
180
|
-
await writeSession(url, parsed.token, parsed.org.name, parsed.org.id, f['project'], link, email);
|
|
181
|
+
await writeSession(url, parsed.token, parsed.org.name, parsed.org.id, f['project'], link, email, autoLink);
|
|
181
182
|
return 0;
|
|
182
183
|
};
|
|
183
184
|
/** `reticle logout` — forget the cached session token (per-project keys under credentials.json stay). */
|
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
import { existsSync, mkdirSync, readFileSync, writeFileSync } from 'node:fs';
|
|
2
2
|
import { dirname } from 'node:path';
|
|
3
3
|
import { homedir } from 'node:os';
|
|
4
|
-
import { probeCli, terminalWidth, wrapForTerminal } from '@reticlehq/init';
|
|
4
|
+
import { claudeAddCommand, McpClient, probeCli, terminalWidth, wrapForTerminal, } from '@reticlehq/init';
|
|
5
5
|
import { installClosing, tutorialShownSteps } from './tutorial.js';
|
|
6
6
|
import { setupMcp, knownClientLabels } from '../setup/setup-mcp.js';
|
|
7
7
|
import { reportInstallSteps } from '../setup/setup-install.js';
|
|
@@ -49,6 +49,14 @@ export function handleSetupMcp(reportStep, standalone = true) {
|
|
|
49
49
|
io.print(` ✓ ${id}`);
|
|
50
50
|
for (const id of result.alreadyThere)
|
|
51
51
|
io.print(` ✓ ${id} (already set up)`);
|
|
52
|
+
for (const id of result.failed) {
|
|
53
|
+
io.print(` ⚠ ${id}: Reticle registration failed; this agent may not have the tools.`);
|
|
54
|
+
io.print(id === McpClient.CLAUDE_CODE
|
|
55
|
+
? ` Run this command to see the error and retry: ${claudeAddCommand().display}`
|
|
56
|
+
: ' Check your agent config file permissions, then retry: reticle setup mcp');
|
|
57
|
+
}
|
|
58
|
+
if (result.failed.length > 0)
|
|
59
|
+
process.exitCode = 1;
|
|
52
60
|
// Said, not skipped. These are the clients whose format we will not rewrite, and they used to be
|
|
53
61
|
// counted as registered: the line below is the difference between a user who adds four lines to
|
|
54
62
|
// one file and a user who believes they are set up and has no tools.
|
package/dist/command/cli.js
CHANGED
|
@@ -79,7 +79,7 @@ async function handleInit(parsed) {
|
|
|
79
79
|
daemonPresent: async (p) => presenceIsUsable(await probePresence(p, { tcpOpen: probeDaemon, status: fetchStatus })),
|
|
80
80
|
pickPort: (p) => pickDaemonPortToBind(p),
|
|
81
81
|
});
|
|
82
|
-
const io = buildNodeIo(cwd, serverInitHost());
|
|
82
|
+
const io = buildNodeIo(cwd, serverInitHost(), { stderr: true === parsed.json });
|
|
83
83
|
const result = runInit({
|
|
84
84
|
cwd,
|
|
85
85
|
port,
|
|
@@ -587,6 +587,22 @@ export function main() {
|
|
|
587
587
|
process.stdout.write(`${CLI_USAGE}\n`);
|
|
588
588
|
return;
|
|
589
589
|
}
|
|
590
|
+
if ('connect' === argv[0]) {
|
|
591
|
+
void (async () => {
|
|
592
|
+
// A fresh project gets the same install, dev-server handover and browser proof as `init`.
|
|
593
|
+
// Its failure exits non-zero before any cloud binding can claim this repo is ready.
|
|
594
|
+
if (readProjectId(process.cwd()) === undefined) {
|
|
595
|
+
await handleInit({ port: undefined, mcp: true, dryRun: false, install: true });
|
|
596
|
+
}
|
|
597
|
+
return runCloudCommand(argv);
|
|
598
|
+
})()
|
|
599
|
+
.then((code) => process.exit(code))
|
|
600
|
+
.catch((cause) => {
|
|
601
|
+
process.stderr.write(`reticle connect: ${cause instanceof Error ? cause.message : String(cause)}\n`);
|
|
602
|
+
process.exit(1);
|
|
603
|
+
});
|
|
604
|
+
return;
|
|
605
|
+
}
|
|
590
606
|
void runCloudCommand(argv).then((code) => process.exit(code));
|
|
591
607
|
return;
|
|
592
608
|
}
|
|
@@ -25,6 +25,8 @@ export interface WaitFacts {
|
|
|
25
25
|
readonly quietForMs: number;
|
|
26
26
|
/** Milliseconds since the wait began. */
|
|
27
27
|
readonly elapsedMs: number;
|
|
28
|
+
/** Explicit caller limit; otherwise a busy build keeps the default ceiling. */
|
|
29
|
+
readonly budgetMs?: number | undefined;
|
|
28
30
|
/**
|
|
29
31
|
* How long silence may mean "still starting" on THIS machine. Defaults to QUIET_MEANS_HUNG_MS.
|
|
30
32
|
*
|
|
@@ -38,7 +38,7 @@ export function judgeWait(facts) {
|
|
|
38
38
|
return WaitVerdict.READY;
|
|
39
39
|
if (facts.launcherExited)
|
|
40
40
|
return WaitVerdict.DEAD;
|
|
41
|
-
if (facts.elapsedMs >= WAIT_CEILING_MS)
|
|
41
|
+
if (facts.elapsedMs >= (facts.budgetMs ?? WAIT_CEILING_MS))
|
|
42
42
|
return WaitVerdict.HUNG;
|
|
43
43
|
// Output IS progress. Silence is what distinguishes a build from a wedge.
|
|
44
44
|
return facts.quietForMs >= (facts.quietMeansHungMs ?? QUIET_MEANS_HUNG_MS)
|
|
@@ -6,8 +6,8 @@
|
|
|
6
6
|
* every command harder to read.
|
|
7
7
|
*/
|
|
8
8
|
import { existsSync, readFileSync, writeFileSync } from 'node:fs';
|
|
9
|
-
import { RETICLE_DEFAULT_PORT } from '@reticlehq/core';
|
|
10
|
-
import { FEEDBACK_HINT, Framework } from '@reticlehq/init';
|
|
9
|
+
import { InitConfirmation, RETICLE_DEFAULT_PORT } from '@reticlehq/core';
|
|
10
|
+
import { FEEDBACK_HINT, Framework, InitFailure } from '@reticlehq/init';
|
|
11
11
|
import { confirmInstall, nodeConfirmDeps } from '../terminal/confirm.js';
|
|
12
12
|
import { writeLicenseKey } from '../license-key.js';
|
|
13
13
|
import { registerOtherAgents, runSetupCommand } from '../setup-command.js';
|
|
@@ -20,6 +20,8 @@ import { probeDaemon } from '../../../surface/mcp/proxy/proxy-daemon-probe.js';
|
|
|
20
20
|
import { fetchStatus } from '../../daemon/binding/daemon-status-probe.js';
|
|
21
21
|
import { collectEnv, DEFAULT_PHASE_TIMEOUT_MS } from '../setup-options.js';
|
|
22
22
|
import { staticPageDevCommand } from '../bringup/static-page-server.js';
|
|
23
|
+
import { SetupPhase } from '../run-setup.js';
|
|
24
|
+
import { reportInitOutcome } from '../../../telemetry/init-telemetry.js';
|
|
23
25
|
/** How often the runtime phases look again: fast enough not to be the wait, slow enough to be free. */
|
|
24
26
|
const POLL_MS = 250;
|
|
25
27
|
/**
|
|
@@ -59,9 +61,9 @@ function printRelaunch(parsed, io, cwd) {
|
|
|
59
61
|
}
|
|
60
62
|
export async function continueAfterInit(parsed, result, io, cwd) {
|
|
61
63
|
const port = parsed.port ?? RETICLE_DEFAULT_PORT;
|
|
62
|
-
//
|
|
63
|
-
//
|
|
64
|
-
if (undefined !== parsed.licenseKey) {
|
|
64
|
+
// Real installs persist the key whichever way setup ends. A preview must leave .env and
|
|
65
|
+
// .gitignore untouched, just as it leaves project wiring and agent configuration untouched.
|
|
66
|
+
if (!parsed.dryRun && undefined !== parsed.licenseKey) {
|
|
65
67
|
const written = writeLicenseKey(cwd, parsed.licenseKey, licenseIo);
|
|
66
68
|
io.print(written.message);
|
|
67
69
|
}
|
|
@@ -79,9 +81,12 @@ export async function continueAfterInit(parsed, result, io, cwd) {
|
|
|
79
81
|
// has: nothing reaches back into a machine that installed Reticle a version ago, so the
|
|
80
82
|
// upgrade path is re-running init, and the light form of init has to be enough to carry it.
|
|
81
83
|
// A dry run writes nothing anywhere, including here.
|
|
82
|
-
if (true === parsed.filesOnly && wantsAgents(parsed))
|
|
84
|
+
if (!parsed.dryRun && true === parsed.filesOnly && wantsAgents(parsed)) {
|
|
83
85
|
registerOtherAgents(io.print);
|
|
86
|
+
}
|
|
84
87
|
return confirmInstall(result, io, nodeConfirmDeps(port)).then(() => {
|
|
88
|
+
if (true === parsed.json)
|
|
89
|
+
process.stdout.write(`${JSON.stringify(result, null, 2)}\n`);
|
|
85
90
|
if (!result.ok)
|
|
86
91
|
process.exit(1);
|
|
87
92
|
});
|
|
@@ -96,8 +101,15 @@ export async function continueAfterInit(parsed, result, io, cwd) {
|
|
|
96
101
|
if (context === undefined) {
|
|
97
102
|
// Nothing was established, so there is nothing to run against. init has already said why.
|
|
98
103
|
ask();
|
|
104
|
+
if (true === parsed.json)
|
|
105
|
+
process.stdout.write(`${JSON.stringify(result, null, 2)}\n`);
|
|
99
106
|
process.exit(1);
|
|
100
107
|
}
|
|
108
|
+
const reportFailure = (reason) => {
|
|
109
|
+
if (result.outcome !== undefined) {
|
|
110
|
+
reportInitOutcome({ ...result.outcome, ok: false, reason });
|
|
111
|
+
}
|
|
112
|
+
};
|
|
101
113
|
// A PENDING connect step is not a reason to skip the runtime phase, and treating it as one made
|
|
102
114
|
// `init` stop with "paste this snippet" while never looking at the app.
|
|
103
115
|
//
|
|
@@ -113,10 +125,17 @@ export async function continueAfterInit(parsed, result, io, cwd) {
|
|
|
113
125
|
// Before the connect wait, never after: a bridge held by a stranger makes a session impossible,
|
|
114
126
|
// so going ahead spends the entire budget and then reports what reads as an instrumentation
|
|
115
127
|
// problem — the one place that is fine. See bridge-port.ts.
|
|
116
|
-
const refusal = bridgeOccupied(await probePresence(port, { tcpOpen: probeDaemon, status: fetchStatus })
|
|
128
|
+
const refusal = bridgeOccupied(await probePresence(port, { tcpOpen: probeDaemon, status: fetchStatus }).catch((error) => {
|
|
129
|
+
reportFailure(InitFailure.RUNTIME_ERROR);
|
|
130
|
+
throw error;
|
|
131
|
+
}), port);
|
|
117
132
|
if (refusal !== undefined) {
|
|
133
|
+
reportFailure(InitFailure.BRIDGE_OCCUPIED);
|
|
118
134
|
io.print(refusal);
|
|
119
135
|
ask();
|
|
136
|
+
if (true === parsed.json) {
|
|
137
|
+
process.stdout.write(`${JSON.stringify({ ok: false, reason: InitFailure.BRIDGE_OCCUPIED }, null, 2)}\n`);
|
|
138
|
+
}
|
|
120
139
|
process.exit(1);
|
|
121
140
|
}
|
|
122
141
|
return runSetupCommand({
|
|
@@ -136,7 +155,10 @@ export async function continueAfterInit(parsed, result, io, cwd) {
|
|
|
136
155
|
// keeps deciding, so nobody who passed nothing waits less than they used to.
|
|
137
156
|
...(undefined === parsed.timeoutSeconds
|
|
138
157
|
? {}
|
|
139
|
-
: {
|
|
158
|
+
: {
|
|
159
|
+
connectBudgetMs: parsed.timeoutSeconds * 1000,
|
|
160
|
+
startupBudgetMs: parsed.timeoutSeconds * 1000,
|
|
161
|
+
}),
|
|
140
162
|
pollMs: POLL_MS,
|
|
141
163
|
htmlCarriesSdk: HTML_CARRIES_SDK.has(context.framework),
|
|
142
164
|
// A plain page with no dev script gets Reticle's own static server, so the page is actually
|
|
@@ -153,6 +175,26 @@ export async function continueAfterInit(parsed, result, io, cwd) {
|
|
|
153
175
|
})(),
|
|
154
176
|
...(undefined === parsed.url ? {} : { suppliedUrl: parsed.url }),
|
|
155
177
|
}, (line) => io.print(line)).then((outcome) => {
|
|
178
|
+
if (result.outcome !== undefined) {
|
|
179
|
+
const init = {
|
|
180
|
+
...result.outcome,
|
|
181
|
+
ok: outcome.ok,
|
|
182
|
+
};
|
|
183
|
+
if (outcome.ok) {
|
|
184
|
+
delete init.reason;
|
|
185
|
+
init.confirmation = InitConfirmation.CONNECTED;
|
|
186
|
+
}
|
|
187
|
+
else {
|
|
188
|
+
// A failed startup does not establish whether a daemon or instrumented page exists.
|
|
189
|
+
// Keep the watcher's no_daemon/no_session/no_page classifications for observed facts.
|
|
190
|
+
delete init.confirmation;
|
|
191
|
+
init.reason =
|
|
192
|
+
outcome.reachedPhase === SetupPhase.DEV_SERVER
|
|
193
|
+
? InitFailure.DEV_SERVER
|
|
194
|
+
: InitFailure.APP_CONNECTION;
|
|
195
|
+
}
|
|
196
|
+
reportInitOutcome(init);
|
|
197
|
+
}
|
|
156
198
|
// One object, so an agent reads a result instead of interpreting a report.
|
|
157
199
|
if (true === parsed.json) {
|
|
158
200
|
process.stdout.write(`${JSON.stringify(outcome, null, 2)}\n`);
|
|
@@ -201,5 +243,8 @@ export async function continueAfterInit(parsed, result, io, cwd) {
|
|
|
201
243
|
io.print(` ${i + 1}. ${step}`);
|
|
202
244
|
ask();
|
|
203
245
|
process.exit(1);
|
|
246
|
+
}, (error) => {
|
|
247
|
+
reportFailure(InitFailure.RUNTIME_ERROR);
|
|
248
|
+
throw error;
|
|
204
249
|
});
|
|
205
250
|
}
|
|
@@ -23,6 +23,8 @@ import { reticleStateHome } from '../daemon/daemon.js';
|
|
|
23
23
|
import { descendants, parseLsofPorts, parseNetstatListeners, parseWmicProcesses, } from './terminal/listeners.js';
|
|
24
24
|
import { isAddressMiss, loopbackProbeUrls } from './probe/loopback-probe-urls.js';
|
|
25
25
|
const WINDOWS = 'win32' === process.platform;
|
|
26
|
+
/** Astro otherwise daemonizes automatically when it detects an agent terminal. */
|
|
27
|
+
const ASTRO_DEV_BACKGROUND_ENV = 'ASTRO_DEV_BACKGROUND';
|
|
26
28
|
/** A page fetch that is slow is a page fetch that failed, for our purposes. */
|
|
27
29
|
const PROBE_TIMEOUT_MS = 5_000;
|
|
28
30
|
/** The supervisor that owns the server's output and keeps its log bounded. */
|
|
@@ -64,7 +66,10 @@ export class OwnedDevServer {
|
|
|
64
66
|
detached: !WINDOWS,
|
|
65
67
|
// Nothing of ours: the supervisor holds the server's pipes, and init holds none of its.
|
|
66
68
|
stdio: 'ignore',
|
|
67
|
-
|
|
69
|
+
// Reticle already provides the detached supervisor. Astro's agent auto-backgrounding
|
|
70
|
+
// would escape its process group and leave the recorded handover PID pointing at a dead
|
|
71
|
+
// launcher, so re-init and cleanup could no longer find or stop the actual server.
|
|
72
|
+
env: { ...process.env, ...env, [ASTRO_DEV_BACKGROUND_ENV]: '1' },
|
|
68
73
|
});
|
|
69
74
|
this.log = log;
|
|
70
75
|
this.cwd = cwd;
|
|
@@ -132,6 +132,8 @@ export interface SetupInput {
|
|
|
132
132
|
* before setup could say what was wrong.
|
|
133
133
|
*/
|
|
134
134
|
readonly connectBudgetMs?: number | undefined;
|
|
135
|
+
/** Explicit --timeout for startup; absent preserves the adaptive build wait. */
|
|
136
|
+
readonly startupBudgetMs?: number | undefined;
|
|
135
137
|
readonly openBrowser: boolean;
|
|
136
138
|
/** Web, Electron or Tauri. Desktop changes three things; see desktop-shape.ts. */
|
|
137
139
|
readonly shape: AppShape;
|
|
@@ -159,6 +159,7 @@ export async function runSetupPhases(input, fx) {
|
|
|
159
159
|
serving,
|
|
160
160
|
quietForMs: fx.devServerQuietForMs(),
|
|
161
161
|
elapsedMs: fx.now() - startedAt,
|
|
162
|
+
budgetMs: input.startupBudgetMs,
|
|
162
163
|
quietMeansHungMs: WINDOWS_QUIET_MEANS_HUNG_MS_APPLIES
|
|
163
164
|
? WINDOWS_QUIET_MEANS_HUNG_MS
|
|
164
165
|
: QUIET_MEANS_HUNG_MS,
|
|
@@ -184,8 +185,12 @@ export async function runSetupPhases(input, fx) {
|
|
|
184
185
|
// Says BOTH were checked. A server that prints nothing but IS listening is the CRA case and
|
|
185
186
|
// must not be failed, so a reader has to be able to tell "we looked at the log" from "we
|
|
186
187
|
// looked at the log AND the ports".
|
|
187
|
-
note(
|
|
188
|
-
|
|
188
|
+
note((input.startupBudgetMs !== undefined && fx.now() - startedAt >= input.startupBudgetMs
|
|
189
|
+
? `The dev server did not become ready within ${String(input.startupBudgetMs)}ms. `
|
|
190
|
+
: undefined === watching
|
|
191
|
+
? 'The dev server neither printed a URL nor bound a port, so setup has nothing to open. '
|
|
192
|
+
: `The dev server stopped making progress before ${watching} became ready. `) +
|
|
193
|
+
'Check its log, increase --timeout for a slow build, or pass --url with the address it serves.');
|
|
189
194
|
return stop(input, SetupPhase.DEV_SERVER, {}, notes);
|
|
190
195
|
}
|
|
191
196
|
await fx.sleep(input.pollMs);
|
|
@@ -32,6 +32,8 @@ export interface SetupMcpResult {
|
|
|
32
32
|
readonly detected: readonly string[];
|
|
33
33
|
readonly registered: readonly string[];
|
|
34
34
|
readonly alreadyThere: readonly string[];
|
|
35
|
+
/** Automatic registration was attempted but failed. */
|
|
36
|
+
readonly failed: readonly string[];
|
|
35
37
|
/**
|
|
36
38
|
* Clients whose config this will NOT rewrite, and which therefore still need a human.
|
|
37
39
|
*
|
|
@@ -4,6 +4,7 @@ export function setupMcp(io) {
|
|
|
4
4
|
const detected = [];
|
|
5
5
|
const registered = [];
|
|
6
6
|
const alreadyThere = [];
|
|
7
|
+
const failed = [];
|
|
7
8
|
const manual = [];
|
|
8
9
|
/*
|
|
9
10
|
* Claude Code first, because nothing else can find it.
|
|
@@ -30,6 +31,8 @@ export function setupMcp(io) {
|
|
|
30
31
|
const cmd = claudeAddCommand();
|
|
31
32
|
if (io.runCli(cmd.command, cmd.args))
|
|
32
33
|
registered.push(McpClient.CLAUDE_CODE);
|
|
34
|
+
else
|
|
35
|
+
failed.push(McpClient.CLAUDE_CODE);
|
|
33
36
|
}
|
|
34
37
|
}
|
|
35
38
|
for (const client of detectMcpClients(io)) {
|
|
@@ -47,8 +50,13 @@ export function setupMcp(io) {
|
|
|
47
50
|
manual.push({ id: client.id, configPath: client.configPath, docs: spec.docs });
|
|
48
51
|
continue;
|
|
49
52
|
}
|
|
50
|
-
|
|
51
|
-
|
|
53
|
+
try {
|
|
54
|
+
io.writeFile(client.configPath, merged.content);
|
|
55
|
+
registered.push(client.id);
|
|
56
|
+
}
|
|
57
|
+
catch {
|
|
58
|
+
failed.push(client.id);
|
|
59
|
+
}
|
|
52
60
|
}
|
|
53
61
|
/*
|
|
54
62
|
* Both INSTALL steps, reported from the one place that knows the answer.
|
|
@@ -69,7 +77,8 @@ export function setupMcp(io) {
|
|
|
69
77
|
* SKIPPED when there was nothing to register with — not failed. Nothing went wrong; there was
|
|
70
78
|
* simply no agent here, and counting that as our failure would hide the ones that are.
|
|
71
79
|
*
|
|
72
|
-
* FAILED when
|
|
80
|
+
* FAILED when no detected client is usable, whether registration failed or needs a hand.
|
|
81
|
+
* That case used to fall through to COMPLETED,
|
|
73
82
|
* so the cohort with the tools registered NOWHERE was counted among the successful installs, and
|
|
74
83
|
* the one number that says "did this machine get Reticle" read yes for the people it read no
|
|
75
84
|
* for. Registering some and not others is still COMPLETED: the agent they are using may well be
|
|
@@ -79,11 +88,11 @@ export function setupMcp(io) {
|
|
|
79
88
|
? OnboardingStepStatus.SKIPPED
|
|
80
89
|
: 0 === registered.length && alreadyThere.length > 0
|
|
81
90
|
? OnboardingStepStatus.SKIPPED
|
|
82
|
-
: 0 === registered.length && 0 === alreadyThere.length
|
|
91
|
+
: 0 === registered.length && 0 === alreadyThere.length
|
|
83
92
|
? OnboardingStepStatus.FAILED
|
|
84
93
|
: OnboardingStepStatus.COMPLETED,
|
|
85
94
|
});
|
|
86
|
-
return { detected, registered, alreadyThere, manual };
|
|
95
|
+
return { detected, registered, alreadyThere, failed, manual };
|
|
87
96
|
}
|
|
88
97
|
/** Everything this machine could be asked about, for the report when nothing was found. */
|
|
89
98
|
export function knownClientLabels() {
|
|
@@ -1,4 +1,8 @@
|
|
|
1
1
|
export declare const PEER_VERSION_PARAM = "peerVersion";
|
|
2
2
|
export declare const PEER_CONTRACT_PARAM = "peerContract";
|
|
3
|
-
/**
|
|
4
|
-
|
|
3
|
+
/**
|
|
4
|
+
* Compare an attaching agent process against this daemon: the skew to tell THAT connection, or
|
|
5
|
+
* undefined when they agree. Returned rather than queued daemon-wide, because it is true only for the
|
|
6
|
+
* agent that announced it (#1136).
|
|
7
|
+
*/
|
|
8
|
+
export declare function agentPeerSkew(version: string | null, contract: string | null): string | undefined;
|
|
@@ -13,12 +13,15 @@
|
|
|
13
13
|
*/
|
|
14
14
|
import { CONTRACT_FINGERPRINT } from '@reticlehq/core';
|
|
15
15
|
import { SERVER_VERSION } from './identity/server-version.js';
|
|
16
|
-
import { daemonFix, describeSkew
|
|
17
|
-
import { noteVersionSkew } from './version-nudge.js';
|
|
16
|
+
import { daemonFix, describeSkew } from './version-skew.js';
|
|
18
17
|
export const PEER_VERSION_PARAM = 'peerVersion';
|
|
19
18
|
export const PEER_CONTRACT_PARAM = 'peerContract';
|
|
20
|
-
/**
|
|
21
|
-
|
|
19
|
+
/**
|
|
20
|
+
* Compare an attaching agent process against this daemon: the skew to tell THAT connection, or
|
|
21
|
+
* undefined when they agree. Returned rather than queued daemon-wide, because it is true only for the
|
|
22
|
+
* agent that announced it (#1136).
|
|
23
|
+
*/
|
|
24
|
+
export function agentPeerSkew(version, contract) {
|
|
22
25
|
const skew = describeSkew({
|
|
23
26
|
what: "the agent's MCP server",
|
|
24
27
|
version: version ?? undefined,
|
|
@@ -27,6 +30,5 @@ export function noteAgentPeer(version, contract) {
|
|
|
27
30
|
// MCP server, so the daemon is the newer half whenever the announcing agent is behind.
|
|
28
31
|
fix: daemonFix(SERVER_VERSION, version ?? undefined),
|
|
29
32
|
}, { version: SERVER_VERSION, contract: CONTRACT_FINGERPRINT });
|
|
30
|
-
|
|
31
|
-
noteVersionSkew(SkewPair.DAEMON, skew);
|
|
33
|
+
return skew;
|
|
32
34
|
}
|