@reticlehq/server 2.6.0 → 2.8.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/README.md +14 -5
- package/SKILL.md +251 -33
- package/dist/bridge/bridge.d.ts +6 -1
- package/dist/bridge/bridge.js +27 -4
- package/dist/capsule/causal-summary.d.ts +20 -0
- package/dist/capsule/causal-summary.js +31 -6
- package/dist/capsule/divergence.js +7 -1
- package/dist/cli/chromium-hint.d.ts +68 -0
- package/dist/cli/chromium-hint.js +145 -0
- package/dist/cli/cli-doctor.js +31 -36
- package/dist/cli/cli-kill.d.ts +58 -0
- package/dist/cli/cli-kill.js +162 -0
- package/dist/cli/cli-launch.d.ts +18 -0
- package/dist/cli/cli-launch.js +32 -1
- package/dist/cli/cli-parse.d.ts +16 -1
- package/dist/cli/cli-parse.js +32 -9
- package/dist/cli/cli-port.d.ts +8 -0
- package/dist/cli/cli-port.js +36 -1
- package/dist/cli/cli-verify.js +2 -2
- package/dist/cli/doctor-daemon-line.js +10 -1
- package/dist/cli/doctor-sessions-line.d.ts +12 -0
- package/dist/cli/doctor-sessions-line.js +66 -0
- package/dist/cli/port-holder.d.ts +10 -0
- package/dist/cli/port-holder.js +26 -2
- package/dist/cli.js +139 -23
- package/dist/crawl/crawl-tools.js +12 -2
- package/dist/crawl/crawl.d.ts +13 -0
- package/dist/crawl/crawl.js +50 -2
- package/dist/daemon/daemon-resilience.js +2 -2
- package/dist/daemon/loopback-alias.d.ts +32 -0
- package/dist/daemon/loopback-alias.js +80 -0
- package/dist/events/contradictions.d.ts +14 -0
- package/dist/events/contradictions.js +26 -17
- package/dist/events/predicate-eval.d.ts +26 -3
- package/dist/events/predicate-eval.js +284 -95
- package/dist/events/predicate-parse.d.ts +1 -1
- package/dist/events/predicate-parse.js +18 -4
- package/dist/events/predicate.js +170 -18
- package/dist/events/ring-buffer.d.ts +8 -0
- package/dist/events/ring-buffer.js +34 -1
- package/dist/flows/flow-classify.js +1 -1
- package/dist/flows/flow-tools.js +12 -4
- package/dist/flows/heal-run.js +1 -1
- package/dist/honesty/blind-spots.js +6 -2
- package/dist/honesty/honesty.d.ts +11 -0
- package/dist/honesty/honesty.js +9 -1
- package/dist/honesty/unsettled.d.ts +40 -0
- package/dist/honesty/unsettled.js +78 -0
- package/dist/honesty/verified.d.ts +7 -0
- package/dist/honesty/verified.js +27 -4
- package/dist/http-server.js +11 -1
- package/dist/index.js +29 -2
- package/dist/init/agent-rules.d.ts +16 -0
- package/dist/init/agent-rules.js +96 -17
- package/dist/init/astro-patch.d.ts +2 -1
- package/dist/init/astro-patch.js +11 -5
- package/dist/init/capabilities.d.ts +22 -0
- package/dist/init/capabilities.js +76 -0
- package/dist/init/confirm.d.ts +96 -0
- package/dist/init/confirm.js +138 -0
- package/dist/init/cra.js +3 -2
- package/dist/init/detect.d.ts +8 -0
- package/dist/init/detect.js +15 -0
- package/dist/init/non-js-project.d.ts +27 -0
- package/dist/init/non-js-project.js +84 -0
- package/dist/init/plan-framework.d.ts +9 -0
- package/dist/init/plan-framework.js +72 -16
- package/dist/init/plan.d.ts +40 -4
- package/dist/init/plan.js +183 -43
- package/dist/init/run.d.ts +24 -2
- package/dist/init/run.js +181 -46
- package/dist/init/slash-command.d.ts +1 -1
- package/dist/init/slash-command.js +46 -6
- package/dist/init/snippets.d.ts +64 -5
- package/dist/init/snippets.js +220 -31
- package/dist/init/vite-config.js +29 -3
- package/dist/input/network-mock-tools.d.ts +19 -0
- package/dist/input/network-mock-tools.js +29 -4
- package/dist/license/license.js +1 -1
- package/dist/mcp/mcp-proxy.d.ts +69 -3
- package/dist/mcp/mcp-proxy.js +215 -23
- package/dist/mcp/mcp.js +58 -7
- package/dist/mcp/proxy-lifecycle.d.ts +20 -1
- package/dist/mcp/proxy-lifecycle.js +22 -1
- package/dist/pool/browser-pool.d.ts +22 -2
- package/dist/pool/browser-pool.js +44 -4
- package/dist/pool/playwright-launcher.js +10 -2
- package/dist/project/learned-routes.d.ts +24 -0
- package/dist/project/learned-routes.js +63 -0
- package/dist/project/project-store.d.ts +6 -0
- package/dist/project/project-store.js +25 -1
- package/dist/session/live-control-tools.js +39 -2
- package/dist/session/live-control.d.ts +7 -9
- package/dist/session/live-control.js +34 -1
- package/dist/session/no-session-diagnosis.d.ts +23 -4
- package/dist/session/no-session-diagnosis.js +128 -44
- package/dist/session/no-session-watch.js +12 -0
- package/dist/session/ref-provenance.d.ts +54 -0
- package/dist/session/ref-provenance.js +61 -0
- package/dist/session/review-store.d.ts +19 -0
- package/dist/session/review-store.js +34 -5
- package/dist/session/session-lease.js +1 -1
- package/dist/session/session-manager.d.ts +9 -0
- package/dist/session/session-manager.js +11 -0
- package/dist/session/session.d.ts +8 -0
- package/dist/session/session.js +12 -0
- package/dist/status-payload.d.ts +13 -1
- package/dist/status-payload.js +6 -1
- package/dist/telemetry/app-instrumented.d.ts +37 -0
- package/dist/telemetry/app-instrumented.js +54 -0
- package/dist/telemetry/automation-hint.d.ts +17 -0
- package/dist/telemetry/automation-hint.js +71 -0
- package/dist/telemetry/bug-found.d.ts +0 -20
- package/dist/telemetry/bug-found.js +38 -21
- package/dist/telemetry/cli-telemetry.js +7 -1
- package/dist/telemetry/daemon-telemetry.d.ts +4 -4
- package/dist/telemetry/daemon-telemetry.js +8 -4
- package/dist/telemetry/feedback.js +1 -1
- package/dist/telemetry/init-telemetry.js +9 -1
- package/dist/telemetry/install-source.d.ts +36 -0
- package/dist/telemetry/install-source.js +40 -0
- package/dist/telemetry/run-telemetry.js +4 -1
- package/dist/telemetry/session-metrics.d.ts +3 -3
- package/dist/telemetry/session-metrics.js +9 -4
- package/dist/telemetry/telemetry.d.ts +7 -1
- package/dist/telemetry/telemetry.js +17 -2
- package/dist/telemetry/tool-refused.d.ts +19 -0
- package/dist/telemetry/tool-refused.js +55 -0
- package/dist/telemetry/verification-of.js +72 -8
- package/dist/temp-dir.d.ts +27 -0
- package/dist/temp-dir.js +46 -0
- package/dist/tools/act-tools.js +77 -11
- package/dist/tools/assert-grade.d.ts +1 -1
- package/dist/tools/assert-grade.js +27 -5
- package/dist/tools/assert-verdict.js +25 -1
- package/dist/tools/contract-tools.js +4 -0
- package/dist/tools/dynamic-tools.js +1 -1
- package/dist/tools/error-recovery.d.ts +10 -0
- package/dist/tools/error-recovery.js +70 -5
- package/dist/tools/feedback-invite.js +1 -1
- package/dist/tools/invoke-tool.js +149 -32
- package/dist/tools/lease-tools.d.ts +29 -1
- package/dist/tools/lease-tools.js +58 -5
- package/dist/tools/navigate-result.js +1 -1
- package/dist/tools/observe-tools.js +1 -1
- package/dist/tools/observed-nothing.js +1 -1
- package/dist/tools/react-grace.d.ts +48 -0
- package/dist/tools/react-grace.js +87 -0
- package/dist/tools/read-tools.js +20 -4
- package/dist/tools/settle-in-flight.d.ts +79 -0
- package/dist/tools/settle-in-flight.js +106 -0
- package/dist/tools/snapshot-delta.d.ts +7 -0
- package/dist/tools/snapshot-delta.js +130 -6
- package/dist/tools/tool-kit.d.ts +52 -5
- package/dist/tools/tool-kit.js +55 -24
- package/dist/tools/tool-surface.js +28 -9
- package/dist/tools/tools.js +38 -4
- package/dist/visual/capture-cleanup.d.ts +16 -0
- package/dist/visual/capture-cleanup.js +39 -0
- package/dist/visual/visual-tools.d.ts +18 -0
- package/dist/visual/visual-tools.js +24 -5
- package/docs/README.md +56 -11
- package/docs/actions.mdx +87 -0
- package/docs/agent-cheatsheet.md +102 -26
- package/docs/architecture.md +46 -35
- package/docs/benchmarks.md +76 -72
- package/docs/best-practices.mdx +211 -0
- package/docs/capabilities.mdx +387 -0
- package/docs/cli/affected.mdx +51 -0
- package/docs/cli/capsules.mdx +69 -0
- package/docs/cli/cloud.mdx +127 -0
- package/docs/cli/doctor.mdx +76 -0
- package/docs/cli/drive.mdx +55 -0
- package/docs/cli/feedback.mdx +67 -0
- package/docs/cli/gate.mdx +64 -0
- package/docs/cli/help.mdx +92 -0
- package/docs/cli/hunt.mdx +57 -0
- package/docs/cli/identify.mdx +62 -0
- package/docs/cli/init.mdx +94 -0
- package/docs/cli/kill.mdx +91 -0
- package/docs/cli/license.mdx +48 -0
- package/docs/cli/mcp.mdx +61 -0
- package/docs/cli/open.mdx +68 -0
- package/docs/cli/restart.mdx +60 -0
- package/docs/cli/rollback.mdx +35 -0
- package/docs/cli/serve.mdx +62 -0
- package/docs/cli/status.mdx +64 -0
- package/docs/cli/stop.mdx +56 -0
- package/docs/cli/telemetry.mdx +57 -0
- package/docs/cli/update.mdx +52 -0
- package/docs/cli/verify.mdx +57 -0
- package/docs/cli/version.mdx +39 -0
- package/docs/cli/watch.mdx +55 -0
- package/docs/cli.mdx +219 -0
- package/docs/debugging.md +13 -9
- package/docs/deploy-checks.md +15 -11
- package/docs/desktop-apps.md +49 -40
- package/docs/desktop.mdx +188 -0
- package/docs/docs.json +363 -0
- package/docs/enterprise.md +20 -16
- package/docs/faq.mdx +88 -0
- package/docs/fixtures.md +11 -7
- package/docs/flows.md +26 -22
- package/docs/for-agents.md +71 -0
- package/docs/frameworks.mdx +199 -0
- package/docs/gate-plan.md +45 -41
- package/docs/gates.md +23 -19
- package/docs/getting-started.md +76 -65
- package/docs/human-control.md +22 -18
- package/docs/index.mdx +101 -0
- package/docs/install-agentic.mdx +215 -0
- package/docs/install-manual.mdx +194 -0
- package/docs/instrumentation.mdx +232 -0
- package/docs/integration-patterns.md +38 -34
- package/docs/local-registry.md +16 -12
- package/docs/multi-agent-testing.md +16 -12
- package/docs/packages/babel-plugin.mdx +70 -0
- package/docs/packages/browser.mdx +111 -0
- package/docs/packages/core.mdx +106 -0
- package/docs/packages/electron.mdx +87 -0
- package/docs/packages/eslint-plugin.mdx +84 -0
- package/docs/packages/next.mdx +91 -0
- package/docs/packages/react.mdx +91 -0
- package/docs/packages/server.mdx +98 -0
- package/docs/packages/tauri.mdx +63 -0
- package/docs/packages/test.mdx +108 -0
- package/docs/packages/vite-plugin.mdx +96 -0
- package/docs/packages.mdx +217 -0
- package/docs/platform-integration.md +33 -26
- package/docs/predicates.mdx +179 -0
- package/docs/quickstart.mdx +211 -0
- package/docs/recipes.mdx +242 -0
- package/docs/skill-file.mdx +105 -0
- package/docs/state-management.mdx +197 -0
- package/docs/style.css +14 -0
- package/docs/system-map.md +47 -32
- package/docs/telemetry-contract.md +101 -46
- package/docs/telemetry-events.mdx +430 -0
- package/docs/telemetry.md +63 -50
- package/docs/testing.md +10 -6
- package/docs/token-efficiency.md +30 -26
- package/docs/tools-act-and-wait.mdx +123 -0
- package/docs/tools-act-sequence.mdx +120 -0
- package/docs/tools-act.mdx +82 -0
- package/docs/tools-assert.mdx +70 -0
- package/docs/tools-console.mdx +59 -0
- package/docs/tools-inspect.mdx +85 -0
- package/docs/tools-navigate.mdx +63 -0
- package/docs/tools-network.mdx +86 -0
- package/docs/tools-observe.mdx +89 -0
- package/docs/tools-overview.mdx +157 -0
- package/docs/tools-query.mdx +82 -0
- package/docs/tools-session-and-feedback.mdx +91 -0
- package/docs/tools-sessions.mdx +65 -0
- package/docs/tools-snapshot.mdx +75 -0
- package/docs/tools-state.mdx +81 -0
- package/docs/tools-tools-and-run.mdx +125 -0
- package/docs/tools-wait-for.mdx +66 -0
- package/docs/troubleshooting.mdx +193 -0
- package/docs/usage.md +175 -153
- package/docs/vs-chrome-devtools-mcp.mdx +82 -0
- package/docs/vs-playwright-mcp.mdx +85 -0
- package/docs/vs-screenshots.mdx +96 -0
- package/docs/what-is-reticle.mdx +63 -0
- package/docs/why-reticle.mdx +107 -0
- package/package.json +2 -2
- package/docs/matrix/2.5.0/claude-code-machine.json +0 -19
- package/docs/matrix/2.5.0/codex-machine.json +0 -20
- package/docs/matrix/2.5.0/cursor-machine.json +0 -22
- package/docs/matrix/2.5.0/gemini-machine.json +0 -22
- package/docs/matrix/2.5.0/opencode-machine.json +0 -22
- package/docs/matrix/2.5.0/vscode-machine.json +0 -22
- package/docs/matrix/2.5.0/windsurf-machine.json +0 -22
- package/docs/matrix/MATRIX.md +0 -26
- package/docs/matrix/README.md +0 -59
- package/docs/mint.json +0 -61
package/dist/cli/cli-launch.d.ts
CHANGED
|
@@ -1,3 +1,4 @@
|
|
|
1
|
+
import { PortPresence } from '../daemon/port-presence.js';
|
|
1
2
|
/**
|
|
2
3
|
* CLI launch + status helpers — the daemon-introspection (`reticle status`) and the one-command
|
|
3
4
|
* "show me the app" flow (`reticle open`). Split out of cli.ts so that file stays under the size cap.
|
|
@@ -19,6 +20,7 @@ interface StatusSession {
|
|
|
19
20
|
export declare function summarizeStatus(payload: unknown): {
|
|
20
21
|
sessionCount: number;
|
|
21
22
|
sessions: StatusSession[];
|
|
23
|
+
why?: string;
|
|
22
24
|
};
|
|
23
25
|
/**
|
|
24
26
|
* Warn when the daemon already on this port is a different version than this CLI, and attach anyway.
|
|
@@ -28,6 +30,22 @@ export declare function summarizeStatus(payload: unknown): {
|
|
|
28
30
|
* another agent's session on a version bump, which is worse than a loud line.
|
|
29
31
|
*/
|
|
30
32
|
export declare function warnOnDaemonSkew(port: number): Promise<void>;
|
|
33
|
+
/**
|
|
34
|
+
* Why `reticle drive` cannot have the port, or undefined when it can. Pure.
|
|
35
|
+
*
|
|
36
|
+
* `drive` bound the port unconditionally, and a daemon already listening on it is the NORMAL state
|
|
37
|
+
* once any MCP client has started one. The bind died with a raw `node:net` stack trace: the one
|
|
38
|
+
* failure in this CLI that answered a user with a Node core dump rather than a structured Reticle
|
|
39
|
+
* line naming the port. It is also the command Reticle itself recommends — `reticle_sessions`
|
|
40
|
+
* returns "run `reticle drive <url>`" whenever a tab is throttled, and a throttled tab nearly always
|
|
41
|
+
* coexists with the running daemon that makes the bind impossible.
|
|
42
|
+
*
|
|
43
|
+
* The `reticle_lease` suggestion is deliberately first: it needs no free port, it is what the
|
|
44
|
+
* daemon's own no-session diagnostic already points at, and it works while the collision stands.
|
|
45
|
+
*/
|
|
46
|
+
export declare function drivePortConflict(presence: PortPresence, port: number, holder?: {
|
|
47
|
+
ourPid: number | null;
|
|
48
|
+
}): string | undefined;
|
|
31
49
|
/** GET the daemon's /status JSON. Resolves to the parsed body, or undefined on any failure. */
|
|
32
50
|
export declare function fetchStatus(port: number): Promise<unknown>;
|
|
33
51
|
/** What `reticle open` should do: reuse an already-connected tab, open a new one, or ask for a url. */
|
package/dist/cli/cli-launch.js
CHANGED
|
@@ -6,6 +6,7 @@ import { describeSkew, DAEMON_FIX } from '../version/version-skew.js';
|
|
|
6
6
|
import { CONTRACT_FINGERPRINT } from '@reticlehq/core';
|
|
7
7
|
import { SERVER_VERSION } from '../version/server-version.js';
|
|
8
8
|
import { log } from '../log.js';
|
|
9
|
+
import { PortPresence, describePresence } from '../daemon/port-presence.js';
|
|
9
10
|
/**
|
|
10
11
|
* Reduce the daemon's /status JSON to the compact view `reticle status` prints. Pure: narrows the
|
|
11
12
|
* untrusted wire payload (never `any`) and tolerates a missing/partial body so a malformed response
|
|
@@ -15,6 +16,9 @@ export function summarizeStatus(payload) {
|
|
|
15
16
|
if (typeof payload !== 'object' || null === payload)
|
|
16
17
|
return { sessionCount: 0, sessions: [] };
|
|
17
18
|
const obj = payload;
|
|
19
|
+
// Carried through to the printed line: with no sessions this is the whole answer, and dropping it
|
|
20
|
+
// here would silently undo the reason it is on the wire.
|
|
21
|
+
const why = 'string' === typeof obj['why'] ? obj['why'] : undefined;
|
|
18
22
|
const raw = Array.isArray(obj['sessions']) ? obj['sessions'] : [];
|
|
19
23
|
const sessions = raw
|
|
20
24
|
.map((s) => {
|
|
@@ -34,7 +38,7 @@ export function summarizeStatus(payload) {
|
|
|
34
38
|
})
|
|
35
39
|
.filter((s) => s !== null);
|
|
36
40
|
const sessionCount = 'number' === typeof obj['sessionCount'] ? obj['sessionCount'] : sessions.length;
|
|
37
|
-
return { sessionCount, sessions };
|
|
41
|
+
return { sessionCount, sessions, ...(why === undefined ? {} : { why }) };
|
|
38
42
|
}
|
|
39
43
|
/** A string field off the /status body, or undefined on a daemon too old to report it. */
|
|
40
44
|
function statusField(payload, key) {
|
|
@@ -61,6 +65,33 @@ export async function warnOnDaemonSkew(port) {
|
|
|
61
65
|
if (skew !== undefined)
|
|
62
66
|
log('reticle_daemon_skew', { port, warning: skew });
|
|
63
67
|
}
|
|
68
|
+
/**
|
|
69
|
+
* Why `reticle drive` cannot have the port, or undefined when it can. Pure.
|
|
70
|
+
*
|
|
71
|
+
* `drive` bound the port unconditionally, and a daemon already listening on it is the NORMAL state
|
|
72
|
+
* once any MCP client has started one. The bind died with a raw `node:net` stack trace: the one
|
|
73
|
+
* failure in this CLI that answered a user with a Node core dump rather than a structured Reticle
|
|
74
|
+
* line naming the port. It is also the command Reticle itself recommends — `reticle_sessions`
|
|
75
|
+
* returns "run `reticle drive <url>`" whenever a tab is throttled, and a throttled tab nearly always
|
|
76
|
+
* coexists with the running daemon that makes the bind impossible.
|
|
77
|
+
*
|
|
78
|
+
* The `reticle_lease` suggestion is deliberately first: it needs no free port, it is what the
|
|
79
|
+
* daemon's own no-session diagnostic already points at, and it works while the collision stands.
|
|
80
|
+
*/
|
|
81
|
+
export function drivePortConflict(presence, port, holder) {
|
|
82
|
+
if (presence !== PortPresence.DAEMON && presence !== PortPresence.FOREIGN)
|
|
83
|
+
return undefined;
|
|
84
|
+
if (PortPresence.FOREIGN === presence)
|
|
85
|
+
return describePresence(presence, port);
|
|
86
|
+
const pid = holder?.ourPid;
|
|
87
|
+
const who = pid !== null && pid !== undefined
|
|
88
|
+
? `a Reticle daemon (pid ${String(pid)}) is already serving :${String(port)}`
|
|
89
|
+
: `a Reticle daemon is already serving :${String(port)}`;
|
|
90
|
+
return (`${who}, so \`drive\` cannot bind that port. Use the daemon that is already there — its ` +
|
|
91
|
+
'`reticle_lease` tool opens a browser Reticle drives itself and needs no free port — or stop ' +
|
|
92
|
+
'it with `reticle stop` and retry, or drive on another port with `RETICLE_PORT=N` (`drive` ' +
|
|
93
|
+
'takes the port from the environment or .reticle.json, not from a flag).');
|
|
94
|
+
}
|
|
64
95
|
/** How long the daemon /status probe waits before giving up — a local loopback call is near-instant. */
|
|
65
96
|
const STATUS_PROBE_TIMEOUT_MS = 1000;
|
|
66
97
|
/** GET the daemon's /status JSON. Resolves to the parsed body, or undefined on any failure. */
|
package/dist/cli/cli-parse.d.ts
CHANGED
|
@@ -6,7 +6,14 @@
|
|
|
6
6
|
*/
|
|
7
7
|
import { type ParsedFeedback } from './cli-parse-feedback.js';
|
|
8
8
|
export { AGENT_FLAG, BUG_FLAG, FEEDBACK_KINDS, KIND_FLAG, RATING_FLAG, } from './cli-parse-feedback.js';
|
|
9
|
-
|
|
9
|
+
/**
|
|
10
|
+
* The lines below say `reticle <command>`, which is the bin this package installs and is correct
|
|
11
|
+
* once it is on PATH. Copied into `npx`, it is not: `npx reticle` resolves the PACKAGE named
|
|
12
|
+
* `reticle`, which belongs to somebody else, and npx will happily fetch and run it. The docs were
|
|
13
|
+
* telling readers to do exactly that on 110 lines before it was caught, so the invocation now leads
|
|
14
|
+
* the usage block rather than being a footnote somewhere else.
|
|
15
|
+
*/
|
|
16
|
+
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>] (wire Reticle into the project in this directory)\n --app picks WHICH app in a monorepo, when several are found\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]\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> [--headed] [--timeout N] [--storage-state <file>] (one-shot: drive the URL, verify saved flows, exit 0=pass)\n reticle affected [--since <ref>] [file...] (which saved flows must re-verify for the changed files)\n reticle gate [--since <ref>] [file...] (exit non-zero unless passing artifacts cover the affected flows)\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> \"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 Cloud; runs/flows recorded on the dashboard):\n reticle login --email <e> [--code <c>] [--org <n>] (sign in: mails a code, then exchanges it)\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 (send local run artifacts to the dashboard)\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.";
|
|
10
17
|
/** The `reticle telemetry` sub-actions. Bare `reticle telemetry` means `status`. */
|
|
11
18
|
export declare const TelemetryAction: {
|
|
12
19
|
readonly STATUS: "status";
|
|
@@ -52,6 +59,14 @@ export type CliResult = {
|
|
|
52
59
|
kind: 'stop';
|
|
53
60
|
port: number;
|
|
54
61
|
quiet: boolean;
|
|
62
|
+
} | {
|
|
63
|
+
kind: 'kill';
|
|
64
|
+
port: number;
|
|
65
|
+
force: boolean;
|
|
66
|
+
} | {
|
|
67
|
+
kind: 'restart';
|
|
68
|
+
port: number;
|
|
69
|
+
force: boolean;
|
|
55
70
|
} | {
|
|
56
71
|
kind: 'status';
|
|
57
72
|
port: number;
|
package/dist/cli/cli-parse.js
CHANGED
|
@@ -5,36 +5,47 @@
|
|
|
5
5
|
* imports are unchanged.
|
|
6
6
|
*/
|
|
7
7
|
import { parseFeedbackArgs } from './cli-parse-feedback.js';
|
|
8
|
+
import { FORCE_FLAG } from './cli-kill.js';
|
|
8
9
|
// Re-exported so every existing importer of these flags is unaffected by the file split.
|
|
9
10
|
export { AGENT_FLAG, BUG_FLAG, FEEDBACK_KINDS, KIND_FLAG, RATING_FLAG, } from './cli-parse-feedback.js';
|
|
10
|
-
|
|
11
|
+
/**
|
|
12
|
+
* The lines below say `reticle <command>`, which is the bin this package installs and is correct
|
|
13
|
+
* once it is on PATH. Copied into `npx`, it is not: `npx reticle` resolves the PACKAGE named
|
|
14
|
+
* `reticle`, which belongs to somebody else, and npx will happily fetch and run it. The docs were
|
|
15
|
+
* telling readers to do exactly that on 110 lines before it was caught, so the invocation now leads
|
|
16
|
+
* the usage block rather than being a footnote somewhere else.
|
|
17
|
+
*/
|
|
18
|
+
export const CLI_USAGE = `usage: npx @reticlehq/server <command> (or \`reticle <command>\` once the bin is on your PATH)
|
|
19
|
+
|
|
11
20
|
reticle init [--dry-run] [--port N] [--no-mcp] [--no-install] [--app <dir>] (wire Reticle into the project in this directory)
|
|
12
21
|
--app picks WHICH app in a monorepo, when several are found
|
|
13
22
|
--no-mcp skips MORE than the server registration: also the agent rule files
|
|
14
23
|
(CLAUDE.md / AGENTS.md / .cursor) and the /reticle command, because all three
|
|
15
24
|
only make sense once the tools are reachable.
|
|
16
25
|
reticle serve [--port N] [--drive <url>] [--headless] [--http] [--http-port N] [--http-token T]
|
|
17
|
-
reticle stop [--port N] [--quiet]
|
|
26
|
+
reticle stop [--port N] [--quiet] (stop the daemon we started, by its recorded pid)
|
|
27
|
+
reticle kill [--port N] [--force] (free the port by its LISTENER, never the agent's mcp proxy)
|
|
28
|
+
reticle restart [--port N] [--force] (kill, then start a daemon and wait for a real bind)
|
|
18
29
|
reticle status [--port N]
|
|
19
|
-
reticle doctor [--port N] (diagnose setup: Chromium, daemon, port
|
|
30
|
+
reticle doctor [--port N] (one command to diagnose setup: Chromium, daemon, port)
|
|
20
31
|
reticle open [url] [--port N] (show the app: reuse the connected tab, else open one)
|
|
21
32
|
reticle verify <url> [--headed] [--timeout N] [--storage-state <file>] (one-shot: drive the URL, verify saved flows, exit 0=pass)
|
|
22
33
|
reticle affected [--since <ref>] [file...] (which saved flows must re-verify for the changed files)
|
|
23
34
|
reticle gate [--since <ref>] [file...] (exit non-zero unless passing artifacts cover the affected flows)
|
|
24
35
|
reticle watch [url] (on save, report which saved flows must re-verify)
|
|
25
|
-
reticle drive <url> [--headless] (foreground mode
|
|
26
|
-
reticle mcp [--port N] [--drive <url>] [--headless] (MCP stdio proxy
|
|
36
|
+
reticle drive <url> [--headless] (foreground mode, for debugging)
|
|
37
|
+
reticle mcp [--port N] [--drive <url>] [--headless] (MCP stdio proxy; auto-starts daemon if needed)
|
|
27
38
|
reticle update (install the latest server version and restart)
|
|
28
39
|
reticle rollback (restore the previous server version and restart)
|
|
29
40
|
reticle license (show enterprise license status: active | eval | missing)
|
|
30
|
-
reticle telemetry [status|enable|disable] (anonymous usage metrics
|
|
31
|
-
reticle feedback [--rating 1-5] [--bug] "message" (tell us what worked and what didn't
|
|
41
|
+
reticle telemetry [status|enable|disable] (anonymous usage metrics; status shows what's sent + the policy)
|
|
42
|
+
reticle feedback [--rating 1-5] [--bug] "message" (tell us what worked and what didn't; prints exactly what it sends)
|
|
32
43
|
reticle feedback --agent --kind <bug|gap|ambiguity|feature_request|improvement> "message"
|
|
33
44
|
(agents: file from anywhere, including a setup that never finished)
|
|
34
45
|
reticle identify --context company|side_project|open_source|learning [--company N] [--email E] [--forget]
|
|
35
46
|
(OPT-IN: tell us who you are, e.g. for support or an enterprise trial)
|
|
36
47
|
|
|
37
|
-
Cloud (link this project to Reticle Cloud
|
|
48
|
+
Cloud (link this project to Reticle Cloud; runs/flows recorded on the dashboard):
|
|
38
49
|
reticle login --email <e> [--code <c>] [--org <n>] (sign in: mails a code, then exchanges it)
|
|
39
50
|
reticle link [--project <name|id>] (bind this repo: mints a scoped key, writes .reticle/cloud.json)
|
|
40
51
|
reticle whoami (who am I signed in as, and is this repo attached?)
|
|
@@ -44,10 +55,12 @@ Cloud (link this project to Reticle Cloud — runs/flows recorded on the dashboa
|
|
|
44
55
|
reticle runs | regression | share <runId> (read cloud state; regression exits 3 if any flow broke)
|
|
45
56
|
|
|
46
57
|
'drive' shows the browser; everything else is hidden (serve/mcp own the pool behind leases, replay
|
|
47
|
-
and the spec runner
|
|
58
|
+
and the spec runner: batch work). --headed opts any of them in, --headless hides drive, CI hides it.`;
|
|
48
59
|
const INIT_COMMAND = 'init';
|
|
49
60
|
const SERVE_COMMAND = 'serve';
|
|
50
61
|
const STOP_COMMAND = 'stop';
|
|
62
|
+
const KILL_COMMAND = 'kill';
|
|
63
|
+
const RESTART_COMMAND = 'restart';
|
|
51
64
|
const STATUS_COMMAND = 'status';
|
|
52
65
|
const OPEN_COMMAND = 'open';
|
|
53
66
|
const DRIVE_COMMAND = 'drive';
|
|
@@ -90,6 +103,8 @@ const KNOWN_COMMANDS = new Set([
|
|
|
90
103
|
INIT_COMMAND,
|
|
91
104
|
SERVE_COMMAND,
|
|
92
105
|
STOP_COMMAND,
|
|
106
|
+
KILL_COMMAND,
|
|
107
|
+
RESTART_COMMAND,
|
|
93
108
|
STATUS_COMMAND,
|
|
94
109
|
OPEN_COMMAND,
|
|
95
110
|
DRIVE_COMMAND,
|
|
@@ -463,6 +478,14 @@ export function parseCliArgs(argv, defaultPort, defaultHeadless = false) {
|
|
|
463
478
|
const quiet = rest.includes(QUIET_FLAG);
|
|
464
479
|
return { kind: 'stop', port, quiet };
|
|
465
480
|
}
|
|
481
|
+
case KILL_COMMAND: {
|
|
482
|
+
const port = parsePortFlag(rest, defaultPort);
|
|
483
|
+
return { kind: 'kill', port, force: rest.includes(FORCE_FLAG) };
|
|
484
|
+
}
|
|
485
|
+
case RESTART_COMMAND: {
|
|
486
|
+
const port = parsePortFlag(rest, defaultPort);
|
|
487
|
+
return { kind: 'restart', port, force: rest.includes(FORCE_FLAG) };
|
|
488
|
+
}
|
|
466
489
|
case STATUS_COMMAND: {
|
|
467
490
|
const port = parsePortFlag(rest, defaultPort);
|
|
468
491
|
return { kind: 'status', port };
|
package/dist/cli/cli-port.d.ts
CHANGED
|
@@ -44,3 +44,11 @@ export declare function readJournalEnabled(cwd: string, env: string | undefined)
|
|
|
44
44
|
* Pass `portFlag` when the user explicitly supplied --port; pass `undefined` to fall through.
|
|
45
45
|
*/
|
|
46
46
|
export declare function resolvePort(portFlag: number | undefined, envPort: number | undefined, projectPort: number | undefined, defaultPort: number): number;
|
|
47
|
+
/**
|
|
48
|
+
* Detect a port mismatch between the daemon and the project's SDK configuration.
|
|
49
|
+
*
|
|
50
|
+
* Returns a diagnostic string when the daemon port differs from what `.reticle.json` declares.
|
|
51
|
+
* The SDK reads `.reticle.json` at build/connect time, so a mismatch means the app will dial a
|
|
52
|
+
* port nothing is listening on — the silent no-connect that #261 documents.
|
|
53
|
+
*/
|
|
54
|
+
export declare function diagnosePortMismatch(daemonPort: number, projectPort: number | undefined): string | undefined;
|
package/dist/cli/cli-port.js
CHANGED
|
@@ -20,7 +20,26 @@ import { readFileSync } from 'node:fs';
|
|
|
20
20
|
* the confusion got manufactured in the first place.
|
|
21
21
|
*/
|
|
22
22
|
export const DEV_SERVER_PORTS = new Set([
|
|
23
|
-
3000,
|
|
23
|
+
3000,
|
|
24
|
+
3001,
|
|
25
|
+
4200,
|
|
26
|
+
4321,
|
|
27
|
+
5000,
|
|
28
|
+
5173,
|
|
29
|
+
5174,
|
|
30
|
+
8000,
|
|
31
|
+
8080,
|
|
32
|
+
8100,
|
|
33
|
+
9000,
|
|
34
|
+
// Added after the scan reported "nothing is listening" at people whose app was plainly running.
|
|
35
|
+
// A narrow list is not just unhelpful, it is CONFIDENTLY wrong — the message states absence as a
|
|
36
|
+
// fact — so the defaults of the frameworks agents actually meet belong in it.
|
|
37
|
+
1420, // Tauri
|
|
38
|
+
3100, // Next, second instance
|
|
39
|
+
4173, // vite preview
|
|
40
|
+
5175, // Vite, third instance
|
|
41
|
+
7860, // Gradio
|
|
42
|
+
8501, // Streamlit
|
|
24
43
|
]);
|
|
25
44
|
export function isLikelyDevServerPort(port) {
|
|
26
45
|
return DEV_SERVER_PORTS.has(port);
|
|
@@ -101,3 +120,19 @@ export function readJournalEnabled(cwd, env) {
|
|
|
101
120
|
export function resolvePort(portFlag, envPort, projectPort, defaultPort) {
|
|
102
121
|
return portFlag ?? envPort ?? projectPort ?? defaultPort;
|
|
103
122
|
}
|
|
123
|
+
/**
|
|
124
|
+
* Detect a port mismatch between the daemon and the project's SDK configuration.
|
|
125
|
+
*
|
|
126
|
+
* Returns a diagnostic string when the daemon port differs from what `.reticle.json` declares.
|
|
127
|
+
* The SDK reads `.reticle.json` at build/connect time, so a mismatch means the app will dial a
|
|
128
|
+
* port nothing is listening on — the silent no-connect that #261 documents.
|
|
129
|
+
*/
|
|
130
|
+
export function diagnosePortMismatch(daemonPort, projectPort) {
|
|
131
|
+
if (projectPort === undefined)
|
|
132
|
+
return undefined;
|
|
133
|
+
if (projectPort === daemonPort)
|
|
134
|
+
return undefined;
|
|
135
|
+
return (`.reticle.json says "port": ${String(projectPort)} but this daemon is on :${String(daemonPort)}` +
|
|
136
|
+
` — the SDK will dial :${String(projectPort)} and never connect. ` +
|
|
137
|
+
`Either start the daemon with --port ${String(projectPort)}, or update .reticle.json to match.`);
|
|
138
|
+
}
|
package/dist/cli/cli-verify.js
CHANGED
|
@@ -30,10 +30,10 @@ const DEFAULT_SESSION_TIMEOUT_MS = 15_000;
|
|
|
30
30
|
const SESSION_POLL_MS = 250;
|
|
31
31
|
const DEFAULT_PROJECT_NAME = 'app';
|
|
32
32
|
const VERIFY_AGENT_ID = 'reticle-cli';
|
|
33
|
-
const MSG_NO_SESSION = 'No app connected
|
|
33
|
+
const MSG_NO_SESSION = 'No app connected: Reticle drove the URL but no @reticlehq/browser session dialed back.\n' +
|
|
34
34
|
' Make sure the SDK is in the build and reticle.connect() runs on the preview page' +
|
|
35
35
|
' (for a non-localhost preview: allowNonLocalhost + a pairing token).';
|
|
36
|
-
const MSG_NO_FLOWS = 'No saved flows to verify (.reticle/flows is empty)
|
|
36
|
+
const MSG_NO_FLOWS = 'No saved flows to verify (.reticle/flows is empty), so refusing to report a pass for verifying nothing.\n' +
|
|
37
37
|
' Flows are recorded interactively by an agent (reticle_record{action:"start"} → act → reticle_flow_save via the\n' +
|
|
38
38
|
' MCP tools), then committed to .reticle/flows/. In CI, check those files in and re-run `reticle verify`.';
|
|
39
39
|
const MSG_VERIFY_PREFIX = 'verify failed: ';
|
|
@@ -14,7 +14,16 @@
|
|
|
14
14
|
* at the call site.
|
|
15
15
|
*/
|
|
16
16
|
import { describeSkew } from '../version/version-skew.js';
|
|
17
|
-
|
|
17
|
+
/**
|
|
18
|
+
* `reticle stop`, not `reticle kill`.
|
|
19
|
+
*
|
|
20
|
+
* The first version of this said `reticle kill`, which this CLI does not dispatch — it is a proposal
|
|
21
|
+
* (#114), described as a gap in docs/system-map.md, and I read it there as if it shipped. A remedy
|
|
22
|
+
* naming a command that errors is a second dead end handed to someone already stuck, on the one
|
|
23
|
+
* command they run when they are confused.
|
|
24
|
+
*/
|
|
25
|
+
const FIX = 'Restart it so both ends are the same build: `reticle stop`, then let your agent reconnect (it ' +
|
|
26
|
+
'spawns a fresh daemon on the next tool call).';
|
|
18
27
|
/**
|
|
19
28
|
* Build the daemon line for a port that answered `/status`.
|
|
20
29
|
*
|
|
@@ -0,0 +1,12 @@
|
|
|
1
|
+
/** What this reads off `/status`. Everything is optional — an older daemon may send none of it. */
|
|
2
|
+
export interface SessionFacts {
|
|
3
|
+
sessionCount?: unknown;
|
|
4
|
+
why?: unknown;
|
|
5
|
+
}
|
|
6
|
+
export interface SessionsLine {
|
|
7
|
+
/** The `sessions` row itself. */
|
|
8
|
+
text: string;
|
|
9
|
+
/** The diagnosis to print underneath, when there is one and it is not contradicted by a session. */
|
|
10
|
+
why?: string;
|
|
11
|
+
}
|
|
12
|
+
export declare function sessionsLine(facts: SessionFacts): SessionsLine;
|
|
@@ -0,0 +1,66 @@
|
|
|
1
|
+
import { SELF_RECOVERING_MARKER } from '../session/no-session-diagnosis.js';
|
|
2
|
+
/**
|
|
3
|
+
* The line `doctor` was missing: is anything actually CONNECTED?
|
|
4
|
+
*
|
|
5
|
+
* `doctor` collapses the first-run failure modes into one command, and it checked every one of them
|
|
6
|
+
* except the one the funnel actually stalls on. A user who has installed the SDK, wired the plugin
|
|
7
|
+
* and started the daemon runs `doctor`, gets node, chromium, daemon, bridge port and desktop wiring
|
|
8
|
+
* all ✓, and is no better off — because the thing that is wrong is that no page has ever dialled in,
|
|
9
|
+
* and nothing on that screen mentions pages.
|
|
10
|
+
*
|
|
11
|
+
* The daemon has always known. `/status` carries `sessionCount`, the session list, and `why` — the
|
|
12
|
+
* same no-session diagnosis an agent gets from an empty `reticle_sessions`, which is careful about
|
|
13
|
+
* what it can and cannot prove. `reticle status` prints it. `doctor` fetched the identical payload
|
|
14
|
+
* to read `version` and `contract` off it and dropped the rest on the floor.
|
|
15
|
+
*
|
|
16
|
+
* So this invents no new diagnosis. It surfaces one that already exists, on the command a stuck human
|
|
17
|
+
* runs, which is the difference between a checklist that ends in "everything is fine" and one that
|
|
18
|
+
* ends in a next action.
|
|
19
|
+
*
|
|
20
|
+
* Pure and payload-shaped rather than reaching for the bridge: `doctor` runs in a separate process
|
|
21
|
+
* from the daemon and can only ever know what `/status` told it.
|
|
22
|
+
*/
|
|
23
|
+
/**
|
|
24
|
+
* The diagnosis is written for an AGENT, and `doctor` has a human in front of it.
|
|
25
|
+
*
|
|
26
|
+
* Every branch of it ends by telling the reader to call `reticle_sessions` again, which is right for
|
|
27
|
+
* the caller it was written for and useless to somebody in a shell: an MCP tool is not something you
|
|
28
|
+
* can type. Printing it unchanged would make `doctor` end on a recovery its reader cannot perform,
|
|
29
|
+
* which is the same defect as the aged-out lease message that told an agent to ask a human to reopen
|
|
30
|
+
* a tab that never existed.
|
|
31
|
+
*
|
|
32
|
+
* So the last sentence is swapped for the shell equivalent of itself. Only that sentence — the rest
|
|
33
|
+
* of the diagnosis is about the app and reads the same to either audience.
|
|
34
|
+
*/
|
|
35
|
+
const HUMAN_RETRY = 'Then run `reticle doctor` again — it will appear within a second of the page loading.';
|
|
36
|
+
function forAHuman(why) {
|
|
37
|
+
// Imported, not re-typed: a copy of a sentence that has to match another file's byte-for-byte is a
|
|
38
|
+
// drift bug with a delay on it, and the failure is silent — the swap simply stops happening and
|
|
39
|
+
// doctor goes back to telling a human to call an MCP tool.
|
|
40
|
+
return why.replace(SELF_RECOVERING_MARKER, HUMAN_RETRY);
|
|
41
|
+
}
|
|
42
|
+
function countOf(facts) {
|
|
43
|
+
const raw = facts.sessionCount;
|
|
44
|
+
return 'number' === typeof raw && Number.isFinite(raw) && raw >= 0 ? raw : undefined;
|
|
45
|
+
}
|
|
46
|
+
export function sessionsLine(facts) {
|
|
47
|
+
const count = countOf(facts);
|
|
48
|
+
if (count === undefined) {
|
|
49
|
+
// An older daemon that does not report it. Saying "0 connected" here would be a claim about the
|
|
50
|
+
// app made from a payload that never mentioned the app — the exact kind of confident wrong
|
|
51
|
+
// sentence the no-session diagnosis exists to avoid.
|
|
52
|
+
return { text: ' sessions ? this daemon did not report connected pages (older build)' };
|
|
53
|
+
}
|
|
54
|
+
if (count > 0) {
|
|
55
|
+
const plural = 1 === count ? 'page' : 'pages';
|
|
56
|
+
return { text: ` sessions ✓ ${String(count)} ${plural} connected` };
|
|
57
|
+
}
|
|
58
|
+
const why = 'string' === typeof facts.why && facts.why.length > 0 ? forAHuman(facts.why) : undefined;
|
|
59
|
+
return {
|
|
60
|
+
// Deliberately ✗ rather than a neutral note. Zero connected pages is why somebody is running
|
|
61
|
+
// doctor, and a ✓-coloured checklist that hides the one failing thing is how the old output
|
|
62
|
+
// convinced people their setup was fine.
|
|
63
|
+
text: ' sessions ✗ no page has connected to this daemon',
|
|
64
|
+
...(why === undefined ? {} : { why }),
|
|
65
|
+
};
|
|
66
|
+
}
|
|
@@ -17,6 +17,16 @@ export interface PortHolder {
|
|
|
17
17
|
pid: number;
|
|
18
18
|
command: string;
|
|
19
19
|
}
|
|
20
|
+
/**
|
|
21
|
+
* Run a lookup and return its stdout, or null if it could not run.
|
|
22
|
+
*
|
|
23
|
+
* Null on ANY failure — no lsof (every Windows user, a slim container), a non-zero exit, a throw.
|
|
24
|
+
* This is a diagnostic nicety inside the commands people run when things are already broken; it must
|
|
25
|
+
* never be the reason one of them fails. Every caller handles the null by saying it could not
|
|
26
|
+
* identify the holder, rather than asserting the holder is not ours — a claim it has no evidence
|
|
27
|
+
* for, and one that was wrong for every Windows user.
|
|
28
|
+
*/
|
|
29
|
+
export declare function captureLookup(command: string, args: readonly string[]): string | null;
|
|
20
30
|
/**
|
|
21
31
|
* The FIRST listener in `lsof -nP -iTCP:<port> -sTCP:LISTEN -F pc` output, or null.
|
|
22
32
|
*
|
package/dist/cli/port-holder.js
CHANGED
|
@@ -13,6 +13,27 @@
|
|
|
13
13
|
* The parser is pure and the lookup takes an injected exec, so the rule is tested on every platform
|
|
14
14
|
* while the shelling out stays at the call site.
|
|
15
15
|
*/
|
|
16
|
+
import { spawnSync } from 'node:child_process';
|
|
17
|
+
/** Bound on the holder lookup. A hung `lsof` must not hang the commands that diagnose hangs. */
|
|
18
|
+
const LOOKUP_TIMEOUT_MS = 2_000;
|
|
19
|
+
/**
|
|
20
|
+
* Run a lookup and return its stdout, or null if it could not run.
|
|
21
|
+
*
|
|
22
|
+
* Null on ANY failure — no lsof (every Windows user, a slim container), a non-zero exit, a throw.
|
|
23
|
+
* This is a diagnostic nicety inside the commands people run when things are already broken; it must
|
|
24
|
+
* never be the reason one of them fails. Every caller handles the null by saying it could not
|
|
25
|
+
* identify the holder, rather than asserting the holder is not ours — a claim it has no evidence
|
|
26
|
+
* for, and one that was wrong for every Windows user.
|
|
27
|
+
*/
|
|
28
|
+
export function captureLookup(command, args) {
|
|
29
|
+
try {
|
|
30
|
+
const result = spawnSync(command, [...args], { encoding: 'utf8', timeout: LOOKUP_TIMEOUT_MS });
|
|
31
|
+
return 0 === result.status && 'string' === typeof result.stdout ? result.stdout : null;
|
|
32
|
+
}
|
|
33
|
+
catch {
|
|
34
|
+
return null;
|
|
35
|
+
}
|
|
36
|
+
}
|
|
16
37
|
/** `lsof -F pc` emits one field per line, prefixed by its selector: `p<pid>`, `c<command>`. */
|
|
17
38
|
const PID_FIELD = 'p';
|
|
18
39
|
const COMMAND_FIELD = 'c';
|
|
@@ -59,8 +80,11 @@ export function describeForeignHolder(port, holder,
|
|
|
59
80
|
* stranger, and the fix that works (`reticle stop`, it respawns on the next call) is never named.
|
|
60
81
|
*/
|
|
61
82
|
ourPid) {
|
|
62
|
-
|
|
63
|
-
|
|
83
|
+
// Naming the command matters more than naming the process. "Stop that process" describes an
|
|
84
|
+
// outcome and leaves the reader to invent a route to it, and the route they invent is the `-ti`
|
|
85
|
+
// pipeline that takes their agent's proxy down with the holder.
|
|
86
|
+
const tail = 'a daemon cannot bind it. Free it with `reticle kill --force` (it signals the listener only, ' +
|
|
87
|
+
"never the agent's mcp proxy), or run Reticle on a different port (`--port`), then retry.";
|
|
64
88
|
if (null === holder) {
|
|
65
89
|
/**
|
|
66
90
|
* We could not identify the holder — no `lsof` (every Windows user, and 35% of them by the
|