@reticlehq/server 2.5.0 → 2.7.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/SKILL.md +226 -26
- package/dist/bridge/bridge.d.ts +18 -1
- package/dist/bridge/bridge.js +59 -6
- package/dist/bridge/bridge.test-harness.d.ts +8 -0
- package/dist/bridge/bridge.test-harness.js +9 -1
- package/dist/cli/chromium-hint.d.ts +40 -0
- package/dist/cli/chromium-hint.js +75 -0
- package/dist/cli/cli-doctor.d.ts +0 -5
- package/dist/cli/cli-doctor.js +74 -14
- package/dist/cli/cli-launch.d.ts +1 -0
- package/dist/cli/cli-launch.js +4 -1
- package/dist/cli/cli-parse-feedback.d.ts +31 -0
- package/dist/cli/cli-parse-feedback.js +56 -0
- package/dist/cli/cli-parse.d.ts +5 -9
- package/dist/cli/cli-parse.js +7 -23
- package/dist/cli/cli-port.js +20 -1
- package/dist/cli/doctor-daemon-line.d.ts +37 -0
- package/dist/cli/doctor-daemon-line.js +44 -0
- package/dist/cli/port-holder.d.ts +43 -0
- package/dist/cli/port-holder.js +99 -0
- package/dist/cli.js +170 -19
- package/dist/daemon/daemon-resilience.d.ts +24 -0
- package/dist/daemon/daemon-resilience.js +105 -17
- package/dist/daemon/daemon.js +26 -1
- package/dist/daemon/heartbeat.d.ts +115 -0
- package/dist/daemon/heartbeat.js +141 -0
- package/dist/daemon/idle-shutdown.d.ts +13 -5
- package/dist/daemon/idle-shutdown.js +14 -6
- package/dist/daemon/loopback-alias.d.ts +32 -0
- package/dist/daemon/loopback-alias.js +80 -0
- package/dist/daemon/port-presence.d.ts +76 -0
- package/dist/daemon/port-presence.js +93 -0
- package/dist/daemon/state-dir.d.ts +7 -0
- package/dist/daemon/state-dir.js +59 -0
- package/dist/daemon/unclean-predecessor.d.ts +23 -0
- package/dist/daemon/unclean-predecessor.js +27 -0
- package/dist/events/predicate-eval.d.ts +17 -3
- package/dist/events/predicate-eval.js +183 -96
- package/dist/events/predicate-parse.d.ts +23 -0
- package/dist/events/predicate-parse.js +68 -0
- package/dist/events/predicate.d.ts +5 -0
- package/dist/events/predicate.js +17 -3
- package/dist/events/ring-buffer.d.ts +8 -0
- package/dist/events/ring-buffer.js +35 -17
- package/dist/flows/flow-replay.d.ts +1 -1
- package/dist/flows/flow-replay.js +19 -2
- package/dist/flows/flow-step-runners.js +2 -3
- package/dist/flows/flow-tools.js +10 -14
- package/dist/flows/role-drift-reason.d.ts +20 -0
- package/dist/flows/role-drift-reason.js +39 -0
- package/dist/flows/suite-flakes.d.ts +33 -0
- package/dist/flows/suite-flakes.js +32 -0
- package/dist/honesty/blind-spots.d.ts +19 -0
- package/dist/honesty/blind-spots.js +33 -0
- package/dist/honesty/honesty.d.ts +11 -0
- package/dist/honesty/honesty.js +9 -1
- package/dist/honesty/verified.d.ts +10 -1
- package/dist/honesty/verified.js +42 -3
- package/dist/http-server.js +20 -1
- package/dist/index.d.ts +1 -0
- package/dist/index.js +36 -3
- package/dist/init/agent-rules.d.ts +18 -0
- package/dist/init/agent-rules.js +28 -5
- package/dist/init/cra.d.ts +13 -0
- package/dist/init/cra.js +16 -0
- package/dist/init/detect.d.ts +14 -0
- package/dist/init/detect.js +23 -0
- package/dist/init/mcp-clients.d.ts +81 -0
- package/dist/init/mcp-clients.js +248 -0
- package/dist/init/node-io.js +8 -2
- package/dist/init/non-js-project.d.ts +27 -0
- package/dist/init/non-js-project.js +60 -0
- package/dist/init/plan-framework.d.ts +9 -0
- package/dist/init/plan-framework.js +55 -3
- package/dist/init/plan.d.ts +14 -0
- package/dist/init/plan.js +93 -12
- package/dist/init/run.d.ts +18 -1
- package/dist/init/run.js +169 -23
- package/dist/init/snippets.d.ts +20 -1
- package/dist/init/snippets.js +71 -6
- package/dist/init/windows-quote.d.ts +38 -0
- package/dist/init/windows-quote.js +70 -0
- package/dist/mcp/mcp-outage.d.ts +1 -0
- package/dist/mcp/mcp-outage.js +8 -1
- package/dist/mcp/mcp-proxy.d.ts +11 -1
- package/dist/mcp/mcp-proxy.js +39 -9
- package/dist/mcp/mcp.js +64 -2
- package/dist/mcp/proxy-lifecycle.d.ts +19 -0
- package/dist/mcp/proxy-lifecycle.js +21 -0
- package/dist/pool/browser-pool.d.ts +9 -0
- package/dist/pool/browser-pool.js +38 -9
- package/dist/pool/playwright-launcher.js +10 -2
- package/dist/session/attachment-history.d.ts +46 -0
- package/dist/session/attachment-history.js +65 -0
- 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 +11 -4
- package/dist/session/no-session-diagnosis.js +95 -14
- package/dist/session/no-session-watch.d.ts +3 -1
- package/dist/session/no-session-watch.js +10 -2
- package/dist/session/pending-commands.js +1 -0
- package/dist/session/review-store.d.ts +19 -0
- package/dist/session/review-store.js +34 -5
- package/dist/session/session-info.d.ts +17 -1
- package/dist/session/session-info.js +5 -1
- package/dist/session/session-manager.d.ts +12 -0
- package/dist/session/session-manager.js +83 -3
- package/dist/session/session.d.ts +9 -0
- package/dist/session/session.js +20 -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/crash-cause.d.ts +47 -0
- package/dist/telemetry/crash-cause.js +104 -0
- package/dist/telemetry/daemon-telemetry.d.ts +25 -7
- package/dist/telemetry/daemon-telemetry.js +32 -13
- package/dist/telemetry/error-class.d.ts +26 -0
- package/dist/telemetry/error-class.js +52 -0
- package/dist/telemetry/feedback-cli.d.ts +7 -2
- package/dist/telemetry/feedback-cli.js +49 -5
- package/dist/telemetry/feedback-context.d.ts +1 -6
- package/dist/telemetry/feedback-context.js +64 -4
- package/dist/telemetry/git-facts.d.ts +7 -0
- package/dist/telemetry/git-facts.js +4 -4
- package/dist/telemetry/project-profile.js +2 -1
- package/dist/telemetry/session-metrics.d.ts +45 -3
- package/dist/telemetry/session-metrics.js +247 -9
- package/dist/telemetry/telemetry.d.ts +21 -1
- package/dist/telemetry/telemetry.js +34 -9
- package/dist/telemetry/verification-of.js +26 -1
- package/dist/tools/act-tools.js +63 -14
- package/dist/tools/assert-verdict.d.ts +7 -1
- package/dist/tools/assert-verdict.js +22 -3
- package/dist/tools/browser-tools.js +8 -2
- package/dist/tools/contract-tools.d.ts +0 -5
- package/dist/tools/contract-tools.js +26 -0
- package/dist/tools/dynamic-tools.js +6 -1
- package/dist/tools/error-recovery.d.ts +14 -0
- package/dist/tools/error-recovery.js +89 -8
- package/dist/tools/feedback-invite.d.ts +48 -0
- package/dist/tools/feedback-invite.js +71 -0
- package/dist/tools/invoke-tool.d.ts +0 -6
- package/dist/tools/invoke-tool.js +76 -7
- package/dist/tools/navigate-arrival.d.ts +26 -0
- package/dist/tools/navigate-arrival.js +60 -0
- package/dist/tools/navigate-result.d.ts +15 -1
- package/dist/tools/navigate-result.js +17 -10
- package/dist/tools/observe-tools.js +22 -4
- package/dist/tools/real-input-attempt.js +10 -5
- package/dist/tools/tool-kit.d.ts +60 -5
- package/dist/tools/tool-kit.js +55 -24
- package/dist/tools/tool-surface.js +13 -0
- package/dist/tools/tools.js +22 -0
- 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 +55 -0
- package/docs/agent-cheatsheet.md +15 -3
- package/docs/benchmarks.md +1 -1
- package/docs/fixtures-dispatch-receiver.yml +76 -0
- package/docs/fixtures.md +17 -0
- package/docs/gate-plan.md +98 -0
- package/docs/gates.md +117 -0
- package/docs/local-registry.md +1 -1
- package/docs/matrix/2.5.0/claude-code-machine.json +19 -0
- package/docs/matrix/2.5.0/codex-machine.json +20 -0
- package/docs/matrix/2.5.0/cursor-machine.json +22 -0
- package/docs/matrix/2.5.0/gemini-machine.json +22 -0
- package/docs/matrix/2.5.0/opencode-machine.json +22 -0
- package/docs/matrix/2.5.0/vscode-machine.json +22 -0
- package/docs/matrix/2.5.0/windsurf-machine.json +22 -0
- package/docs/matrix/MATRIX.md +26 -0
- package/docs/matrix/README.md +59 -0
- package/docs/mint.json +1 -1
- package/docs/system-map.md +152 -0
- package/docs/telemetry-contract.md +15 -3
- package/docs/telemetry.md +16 -1
- package/docs/token-efficiency.md +2 -2
- package/package.json +2 -2
|
@@ -0,0 +1,75 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* A Chromium hint you can satisfy by following it.
|
|
3
|
+
*
|
|
4
|
+
* `npx playwright install chromium` looks obviously right and is quietly wrong. `npx` resolves the
|
|
5
|
+
* LATEST playwright, which pins a different browser revision than the playwright the daemon actually
|
|
6
|
+
* bundles — so the command downloads ~90 MiB of a build the daemon will not use, and the check still
|
|
7
|
+
* says missing afterwards. Reported from the field by an agent whose machine already had five
|
|
8
|
+
* chromium builds, none of them the wanted one, with no way to learn which one was wanted.
|
|
9
|
+
*
|
|
10
|
+
* Two things fix it, and they are the same two things any unsatisfiable check needs: pin the command
|
|
11
|
+
* to the version doing the asking, and say what was probed. A check that reports only its verdict
|
|
12
|
+
* cannot be argued with — the reader has no way to tell a missing browser from a bad lookup.
|
|
13
|
+
*/
|
|
14
|
+
import { existsSync } from 'node:fs';
|
|
15
|
+
import { createRequire } from 'node:module';
|
|
16
|
+
/**
|
|
17
|
+
* The install command, pinned when we know what to pin to.
|
|
18
|
+
*
|
|
19
|
+
* Unpinned is still the right fallback: a wrong-revision download is a bad outcome, but no command
|
|
20
|
+
* at all is a worse one.
|
|
21
|
+
*/
|
|
22
|
+
export function chromiumInstallCommand(playwrightVersion) {
|
|
23
|
+
const pin = playwrightVersion === undefined ? 'playwright' : `playwright@${playwrightVersion}`;
|
|
24
|
+
return `npx ${pin} install chromium`;
|
|
25
|
+
}
|
|
26
|
+
/**
|
|
27
|
+
* Ask the playwright THIS process would use, not the one `npx` would fetch.
|
|
28
|
+
*
|
|
29
|
+
* Both halves matter: `executablePath()` resolves the revision the daemon will actually launch, and
|
|
30
|
+
* the bundled package version is what makes the suggested command install that same revision.
|
|
31
|
+
*/
|
|
32
|
+
export async function probeChromium() {
|
|
33
|
+
try {
|
|
34
|
+
const { chromium } = await import('playwright');
|
|
35
|
+
const executablePath = chromium.executablePath();
|
|
36
|
+
return {
|
|
37
|
+
executablePath,
|
|
38
|
+
exists: existsSync(executablePath),
|
|
39
|
+
playwrightVersion: bundledPlaywrightVersion(),
|
|
40
|
+
};
|
|
41
|
+
}
|
|
42
|
+
catch {
|
|
43
|
+
// Playwright itself is absent — a different problem from a missing browser, and the hint says so.
|
|
44
|
+
return { exists: false };
|
|
45
|
+
}
|
|
46
|
+
}
|
|
47
|
+
/** The bundled playwright's version, or undefined if it cannot be read (the hint then goes unpinned). */
|
|
48
|
+
export function bundledPlaywrightVersion() {
|
|
49
|
+
try {
|
|
50
|
+
const require = createRequire(import.meta.url);
|
|
51
|
+
const pkg = require('playwright/package.json');
|
|
52
|
+
if ('object' === typeof pkg && null !== pkg && 'version' in pkg) {
|
|
53
|
+
const { version } = pkg;
|
|
54
|
+
if ('string' === typeof version)
|
|
55
|
+
return version;
|
|
56
|
+
}
|
|
57
|
+
}
|
|
58
|
+
catch {
|
|
59
|
+
/* an unreadable package.json is not worth failing a diagnostic over */
|
|
60
|
+
}
|
|
61
|
+
return undefined;
|
|
62
|
+
}
|
|
63
|
+
/** The doctor line for the Chromium check — verdict, what was probed, and how to satisfy it. */
|
|
64
|
+
export function chromiumHint(probe) {
|
|
65
|
+
if (probe.exists)
|
|
66
|
+
return '✓ installed';
|
|
67
|
+
const command = chromiumInstallCommand(probe.playwrightVersion);
|
|
68
|
+
// Naming the path is what turns "missing" from a verdict into evidence: a reader with browsers on
|
|
69
|
+
// disk can see immediately whether this is a missing install or a lookup pointed somewhere else
|
|
70
|
+
// (PLAYWRIGHT_BROWSERS_PATH, a different browsers root, a revision bump).
|
|
71
|
+
const probed = probe.executablePath === undefined
|
|
72
|
+
? 'the playwright package is not installed'
|
|
73
|
+
: `looked for ${probe.executablePath}`;
|
|
74
|
+
return `✗ missing — ${probed}; run: ${command}`;
|
|
75
|
+
}
|
package/dist/cli/cli-doctor.d.ts
CHANGED
|
@@ -1,6 +1 @@
|
|
|
1
|
-
/**
|
|
2
|
-
* `reticle doctor` — collapse the ~6 independent first-run failure modes into one command. Checks the
|
|
3
|
-
* Chromium install (the #1 silent failure), whether a daemon is up on the resolved bridge port, and
|
|
4
|
-
* reminds the user which port the app must dial. Human-readable to stdout (not the JSON log).
|
|
5
|
-
*/
|
|
6
1
|
export declare function handleDoctor(port: number): Promise<void>;
|
package/dist/cli/cli-doctor.js
CHANGED
|
@@ -1,32 +1,92 @@
|
|
|
1
|
-
import {
|
|
1
|
+
import { readFileSync } from 'node:fs';
|
|
2
2
|
import { join } from 'node:path';
|
|
3
3
|
import { ReticleEnv } from '@reticlehq/core';
|
|
4
|
-
import { readPid,
|
|
4
|
+
import { readPid, reticleStateHome } from '../daemon/daemon.js';
|
|
5
|
+
import { PortPresence, probePresence } from '../daemon/port-presence.js';
|
|
6
|
+
import { probeDaemon } from '../mcp/mcp-proxy.js';
|
|
7
|
+
import { fetchStatus } from './cli-launch.js';
|
|
8
|
+
import { daemonLine } from './doctor-daemon-line.js';
|
|
9
|
+
import { describeForeignHolder, findPortHolder } from './port-holder.js';
|
|
10
|
+
import { chromiumHint, probeChromium } from './chromium-hint.js';
|
|
11
|
+
import { spawnSync } from 'node:child_process';
|
|
12
|
+
import { SERVER_VERSION } from '../version/server-version.js';
|
|
13
|
+
import { CONTRACT_FINGERPRINT } from '@reticlehq/core';
|
|
5
14
|
import { diagnoseDesktop, isDesktopProject } from '../init/desktop-doctor.js';
|
|
6
15
|
/**
|
|
7
16
|
* `reticle doctor` — collapse the ~6 independent first-run failure modes into one command. Checks the
|
|
8
17
|
* Chromium install (the #1 silent failure), whether a daemon is up on the resolved bridge port, and
|
|
9
18
|
* reminds the user which port the app must dial. Human-readable to stdout (not the JSON log).
|
|
10
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 command people run when things are already broken; it must
|
|
25
|
+
* never be the reason `doctor` fails. `describeForeignHolder` handles the null by saying it could
|
|
26
|
+
* not identify the holder and naming our recorded pid, rather than asserting the holder is not ours
|
|
27
|
+
* — a claim it has no evidence for, and one that was wrong for every Windows user.
|
|
28
|
+
*/
|
|
29
|
+
function runCapture(command, args) {
|
|
30
|
+
try {
|
|
31
|
+
const result = spawnSync(command, [...args], { encoding: 'utf8', timeout: LOOKUP_TIMEOUT_MS });
|
|
32
|
+
return 0 === result.status && 'string' === typeof result.stdout ? result.stdout : null;
|
|
33
|
+
}
|
|
34
|
+
catch {
|
|
35
|
+
return null;
|
|
36
|
+
}
|
|
37
|
+
}
|
|
38
|
+
/** Bound on the holder lookup. A hung `lsof` must not hang the command that diagnoses hangs. */
|
|
39
|
+
const LOOKUP_TIMEOUT_MS = 2_000;
|
|
40
|
+
/** Narrow the `/status` payload to the two fields the daemon line reads. */
|
|
41
|
+
function asIdentity(payload) {
|
|
42
|
+
if (typeof payload !== 'object' || null === payload)
|
|
43
|
+
return {};
|
|
44
|
+
const record = payload;
|
|
45
|
+
const pick = (key) => {
|
|
46
|
+
const value = record[key];
|
|
47
|
+
return 'string' === typeof value && value.length > 0 ? value : undefined;
|
|
48
|
+
};
|
|
49
|
+
// Keys are OMITTED rather than set to undefined: `exactOptionalPropertyTypes` is on, and the
|
|
50
|
+
// distinction is the point — "this daemon did not say" is not the same as "this daemon said none".
|
|
51
|
+
const version = pick('version');
|
|
52
|
+
const contract = pick('contract');
|
|
53
|
+
return {
|
|
54
|
+
...(version === undefined ? {} : { version }),
|
|
55
|
+
...(contract === undefined ? {} : { contract }),
|
|
56
|
+
};
|
|
57
|
+
}
|
|
11
58
|
export async function handleDoctor(port) {
|
|
12
59
|
const line = (s) => {
|
|
13
60
|
process.stdout.write(`${s}\n`);
|
|
14
61
|
};
|
|
15
62
|
line('reticle doctor');
|
|
16
63
|
line(` node ${process.version}`);
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
? ' chromium ✓ installed'
|
|
22
|
-
: ' chromium ✗ missing — run: npx playwright install chromium');
|
|
23
|
-
}
|
|
24
|
-
catch {
|
|
25
|
-
line(' chromium ✗ missing — run: npx playwright install chromium');
|
|
26
|
-
}
|
|
64
|
+
line(` chromium ${chromiumHint(await probeChromium())}`);
|
|
65
|
+
// Ask the PORT, not just the pid file. "not running on :4400" has been printed about a port that
|
|
66
|
+
// was demonstrably occupied, which sends the reader to start a daemon that cannot bind. The three
|
|
67
|
+
// states are genuinely different problems with different fixes, so doctor names which one it is.
|
|
27
68
|
const pid = readPid(port);
|
|
28
|
-
|
|
29
|
-
|
|
69
|
+
const presence = await probePresence(port, { tcpOpen: probeDaemon, status: fetchStatus });
|
|
70
|
+
if (presence === PortPresence.DAEMON) {
|
|
71
|
+
// Name WHICH daemon. The /status payload already carries version + contract, and doctor was
|
|
72
|
+
// throwing both away — while skew is invisible everywhere else, reaching the agent as a bare
|
|
73
|
+
// -32000 naming no version. This is the command a human runs at exactly that moment.
|
|
74
|
+
const status = asIdentity(await fetchStatus(port));
|
|
75
|
+
const built = daemonLine(port, pid, status, {
|
|
76
|
+
version: SERVER_VERSION,
|
|
77
|
+
contract: CONTRACT_FINGERPRINT,
|
|
78
|
+
});
|
|
79
|
+
line(built.text);
|
|
80
|
+
if (built.skew !== undefined)
|
|
81
|
+
line(` version ✗ ${built.skew}`);
|
|
82
|
+
}
|
|
83
|
+
else if (presence === PortPresence.FOREIGN) {
|
|
84
|
+
// Name the holder when we can. `doctor` exists for exactly this moment, and "another process"
|
|
85
|
+
// leaves the reader to find a shell command themselves — the obvious one being the `lsof -ti`
|
|
86
|
+
// pipeline that also kills the agent's own MCP proxy.
|
|
87
|
+
// `pid` is what our pid file recorded for this port. When it matches the process actually
|
|
88
|
+
// holding it, this is our OWN daemon wedged, not a stranger — and the fix is different.
|
|
89
|
+
line(` daemon ✗ ${describeForeignHolder(port, findPortHolder(port, runCapture), pid)}`);
|
|
30
90
|
}
|
|
31
91
|
else {
|
|
32
92
|
line(` daemon ✗ not running on :${port} — your agent runs \`reticle mcp\` (or \`reticle serve\`)`);
|
package/dist/cli/cli-launch.d.ts
CHANGED
|
@@ -19,6 +19,7 @@ interface StatusSession {
|
|
|
19
19
|
export declare function summarizeStatus(payload: unknown): {
|
|
20
20
|
sessionCount: number;
|
|
21
21
|
sessions: StatusSession[];
|
|
22
|
+
why?: string;
|
|
22
23
|
};
|
|
23
24
|
/**
|
|
24
25
|
* Warn when the daemon already on this port is a different version than this CLI, and attach anyway.
|
package/dist/cli/cli-launch.js
CHANGED
|
@@ -15,6 +15,9 @@ export function summarizeStatus(payload) {
|
|
|
15
15
|
if (typeof payload !== 'object' || null === payload)
|
|
16
16
|
return { sessionCount: 0, sessions: [] };
|
|
17
17
|
const obj = payload;
|
|
18
|
+
// Carried through to the printed line: with no sessions this is the whole answer, and dropping it
|
|
19
|
+
// here would silently undo the reason it is on the wire.
|
|
20
|
+
const why = 'string' === typeof obj['why'] ? obj['why'] : undefined;
|
|
18
21
|
const raw = Array.isArray(obj['sessions']) ? obj['sessions'] : [];
|
|
19
22
|
const sessions = raw
|
|
20
23
|
.map((s) => {
|
|
@@ -34,7 +37,7 @@ export function summarizeStatus(payload) {
|
|
|
34
37
|
})
|
|
35
38
|
.filter((s) => s !== null);
|
|
36
39
|
const sessionCount = 'number' === typeof obj['sessionCount'] ? obj['sessionCount'] : sessions.length;
|
|
37
|
-
return { sessionCount, sessions };
|
|
40
|
+
return { sessionCount, sessions, ...(why === undefined ? {} : { why }) };
|
|
38
41
|
}
|
|
39
42
|
/** A string field off the /status body, or undefined on a daemon too old to report it. */
|
|
40
43
|
function statusField(payload, key) {
|
|
@@ -0,0 +1,31 @@
|
|
|
1
|
+
/** `reticle feedback --rating 4 "the words"` — the human half of the channel. */
|
|
2
|
+
export declare const RATING_FLAG = "--rating";
|
|
3
|
+
export declare const BUG_FLAG = "--bug";
|
|
4
|
+
/**
|
|
5
|
+
* The AGENT half of the same channel, for the phase where `reticle_feedback` does not exist yet.
|
|
6
|
+
*
|
|
7
|
+
* An agent that is still installing Reticle — or that just watched `init` fail, or a daemon refuse to
|
|
8
|
+
* start — has no MCP tools to file with, and that is exactly the report we most need: the failures
|
|
9
|
+
* that happen before the tool surface is reachable are invisible to every other path we have. This
|
|
10
|
+
* gives that agent the same shape the tool takes, over a command that needs nothing running.
|
|
11
|
+
*/
|
|
12
|
+
export declare const KIND_FLAG = "--kind";
|
|
13
|
+
export declare const AGENT_FLAG = "--agent";
|
|
14
|
+
/** Accepted `--kind` values, from core's enum — never a second hand-written list to drift from it. */
|
|
15
|
+
export declare const FEEDBACK_KINDS: readonly string[];
|
|
16
|
+
/** What `parseFeedbackArgs` yields: the parsed command, or a usage error to print. */
|
|
17
|
+
export type ParsedFeedback = {
|
|
18
|
+
kind: 'feedback';
|
|
19
|
+
text: string;
|
|
20
|
+
rating?: number;
|
|
21
|
+
/** The FeedbackKind name from `--kind`; absent means the legacy bug/experience split. */
|
|
22
|
+
feedbackKind?: string;
|
|
23
|
+
bug: boolean;
|
|
24
|
+
/** `--agent`: file as the agent, so agent reports never dilute the human rating signal. */
|
|
25
|
+
agent: boolean;
|
|
26
|
+
} | {
|
|
27
|
+
kind: 'error';
|
|
28
|
+
message: string;
|
|
29
|
+
};
|
|
30
|
+
/** Parse everything after `reticle feedback`. */
|
|
31
|
+
export declare function parseFeedbackArgs(rest: string[]): ParsedFeedback;
|
|
@@ -0,0 +1,56 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* The `reticle feedback` grammar — flags, kind validation, and the message-vs-flag split.
|
|
3
|
+
*
|
|
4
|
+
* Split out of `cli-parse.ts` when that file crossed the line cap. Cohesive on its own terms: this
|
|
5
|
+
* is the one command whose arguments are free text, so it is the only place in the grammar where
|
|
6
|
+
* "everything that is not a flag" is a rule with edge cases rather than a one-liner.
|
|
7
|
+
*/
|
|
8
|
+
import { FeedbackKind } from '@reticlehq/core';
|
|
9
|
+
/** `reticle feedback --rating 4 "the words"` — the human half of the channel. */
|
|
10
|
+
export const RATING_FLAG = '--rating';
|
|
11
|
+
export const BUG_FLAG = '--bug';
|
|
12
|
+
/**
|
|
13
|
+
* The AGENT half of the same channel, for the phase where `reticle_feedback` does not exist yet.
|
|
14
|
+
*
|
|
15
|
+
* An agent that is still installing Reticle — or that just watched `init` fail, or a daemon refuse to
|
|
16
|
+
* start — has no MCP tools to file with, and that is exactly the report we most need: the failures
|
|
17
|
+
* that happen before the tool surface is reachable are invisible to every other path we have. This
|
|
18
|
+
* gives that agent the same shape the tool takes, over a command that needs nothing running.
|
|
19
|
+
*/
|
|
20
|
+
export const KIND_FLAG = '--kind';
|
|
21
|
+
export const AGENT_FLAG = '--agent';
|
|
22
|
+
/** Accepted `--kind` values, from core's enum — never a second hand-written list to drift from it. */
|
|
23
|
+
export const FEEDBACK_KINDS = Object.values(FeedbackKind);
|
|
24
|
+
/** Parse everything after `reticle feedback`. */
|
|
25
|
+
export function parseFeedbackArgs(rest) {
|
|
26
|
+
const ratingAt = rest.indexOf(RATING_FLAG);
|
|
27
|
+
const rating = -1 === ratingAt ? undefined : Number(rest[ratingAt + 1]);
|
|
28
|
+
if (rating !== undefined && (!Number.isInteger(rating) || rating < 1 || rating > 5)) {
|
|
29
|
+
return { kind: 'error', message: `${RATING_FLAG} takes a whole number from 1 to 5` };
|
|
30
|
+
}
|
|
31
|
+
const kindAt = rest.indexOf(KIND_FLAG);
|
|
32
|
+
const feedbackKind = -1 === kindAt ? undefined : rest[kindAt + 1];
|
|
33
|
+
if (feedbackKind !== undefined && !FEEDBACK_KINDS.includes(feedbackKind)) {
|
|
34
|
+
return { kind: 'error', message: `${KIND_FLAG} takes one of: ${FEEDBACK_KINDS.join(', ')}` };
|
|
35
|
+
}
|
|
36
|
+
// Everything that is not a flag or a flag's VALUE is the message. Quoting is the user's job for
|
|
37
|
+
// shell reasons, but joining the remainder means an unquoted sentence still works. Both value
|
|
38
|
+
// positions have to be consumed: without that, `--kind bug "..."` filed a report whose text began
|
|
39
|
+
// with the word "bug".
|
|
40
|
+
const consumed = new Set([
|
|
41
|
+
...(-1 === ratingAt ? [] : [ratingAt, ratingAt + 1]),
|
|
42
|
+
...(-1 === kindAt ? [] : [kindAt, kindAt + 1]),
|
|
43
|
+
]);
|
|
44
|
+
const text = rest
|
|
45
|
+
.filter((arg, i) => !consumed.has(i) && !arg.startsWith('--'))
|
|
46
|
+
.join(' ')
|
|
47
|
+
.trim();
|
|
48
|
+
return {
|
|
49
|
+
kind: 'feedback',
|
|
50
|
+
text,
|
|
51
|
+
...(rating !== undefined ? { rating } : {}),
|
|
52
|
+
...(feedbackKind !== undefined ? { feedbackKind } : {}),
|
|
53
|
+
bug: rest.includes(BUG_FLAG),
|
|
54
|
+
agent: rest.includes(AGENT_FLAG),
|
|
55
|
+
};
|
|
56
|
+
}
|
package/dist/cli/cli-parse.d.ts
CHANGED
|
@@ -4,10 +4,9 @@
|
|
|
4
4
|
* file-size cap and keep the parser pure + unit-testable. Re-exported from cli.ts so existing
|
|
5
5
|
* imports are unchanged.
|
|
6
6
|
*/
|
|
7
|
-
|
|
8
|
-
|
|
9
|
-
export declare const
|
|
10
|
-
export declare const BUG_FLAG = "--bug";
|
|
7
|
+
import { type ParsedFeedback } from './cli-parse-feedback.js';
|
|
8
|
+
export { AGENT_FLAG, BUG_FLAG, FEEDBACK_KINDS, KIND_FLAG, RATING_FLAG, } from './cli-parse-feedback.js';
|
|
9
|
+
export declare const CLI_USAGE = "usage:\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]\n reticle status [--port N]\n reticle doctor [--port N] (diagnose setup: Chromium, daemon, port \u2014 one command)\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 \u2014 for debugging)\n reticle mcp [--port N] [--drive <url>] [--headless] (MCP stdio proxy \u2014 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 \u2014 status shows what's sent + the policy)\n reticle feedback [--rating 1-5] [--bug] \"message\" (tell us what worked and what didn't \u2014 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 \u2014 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 \u2014 batch work). --headed opts any of them in, --headless hides drive, CI hides it.";
|
|
11
10
|
/** The `reticle telemetry` sub-actions. Bare `reticle telemetry` means `status`. */
|
|
12
11
|
export declare const TelemetryAction: {
|
|
13
12
|
readonly STATUS: "status";
|
|
@@ -61,12 +60,9 @@ export type CliResult = {
|
|
|
61
60
|
} | {
|
|
62
61
|
kind: 'telemetry';
|
|
63
62
|
action: TelemetryAction;
|
|
64
|
-
} | {
|
|
63
|
+
} | Extract<ParsedFeedback, {
|
|
65
64
|
kind: 'feedback';
|
|
66
|
-
|
|
67
|
-
rating?: number;
|
|
68
|
-
bug: boolean;
|
|
69
|
-
} | {
|
|
65
|
+
}> | {
|
|
70
66
|
kind: 'identify';
|
|
71
67
|
context?: string;
|
|
72
68
|
company?: string;
|
package/dist/cli/cli-parse.js
CHANGED
|
@@ -4,6 +4,9 @@
|
|
|
4
4
|
* file-size cap and keep the parser pure + unit-testable. Re-exported from cli.ts so existing
|
|
5
5
|
* imports are unchanged.
|
|
6
6
|
*/
|
|
7
|
+
import { parseFeedbackArgs } from './cli-parse-feedback.js';
|
|
8
|
+
// Re-exported so every existing importer of these flags is unaffected by the file split.
|
|
9
|
+
export { AGENT_FLAG, BUG_FLAG, FEEDBACK_KINDS, KIND_FLAG, RATING_FLAG, } from './cli-parse-feedback.js';
|
|
7
10
|
export const CLI_USAGE = `usage:
|
|
8
11
|
reticle init [--dry-run] [--port N] [--no-mcp] [--no-install] [--app <dir>] (wire Reticle into the project in this directory)
|
|
9
12
|
--app picks WHICH app in a monorepo, when several are found
|
|
@@ -26,6 +29,8 @@ export const CLI_USAGE = `usage:
|
|
|
26
29
|
reticle license (show enterprise license status: active | eval | missing)
|
|
27
30
|
reticle telemetry [status|enable|disable] (anonymous usage metrics — status shows what's sent + the policy)
|
|
28
31
|
reticle feedback [--rating 1-5] [--bug] "message" (tell us what worked and what didn't — prints exactly what it sends)
|
|
32
|
+
reticle feedback --agent --kind <bug|gap|ambiguity|feature_request|improvement> "message"
|
|
33
|
+
(agents: file from anywhere, including a setup that never finished)
|
|
29
34
|
reticle identify --context company|side_project|open_source|learning [--company N] [--email E] [--forget]
|
|
30
35
|
(OPT-IN: tell us who you are, e.g. for support or an enterprise trial)
|
|
31
36
|
|
|
@@ -64,9 +69,6 @@ const COMPANY_FLAG = '--company';
|
|
|
64
69
|
const EMAIL_FLAG = '--email';
|
|
65
70
|
const CONTEXT_FLAG = '--context';
|
|
66
71
|
const FORGET_FLAG = '--forget';
|
|
67
|
-
/** `reticle feedback --rating 4 "the words"` — the human half of the channel. */
|
|
68
|
-
export const RATING_FLAG = '--rating';
|
|
69
|
-
export const BUG_FLAG = '--bug';
|
|
70
72
|
/** The `reticle telemetry` sub-actions. Bare `reticle telemetry` means `status`. */
|
|
71
73
|
export const TelemetryAction = {
|
|
72
74
|
STATUS: 'status',
|
|
@@ -496,26 +498,8 @@ export function parseCliArgs(argv, defaultPort, defaultHeadless = false) {
|
|
|
496
498
|
forget: rest.includes(FORGET_FLAG),
|
|
497
499
|
};
|
|
498
500
|
}
|
|
499
|
-
case FEEDBACK_COMMAND:
|
|
500
|
-
|
|
501
|
-
const rating = -1 === ratingAt ? undefined : Number(rest[ratingAt + 1]);
|
|
502
|
-
if (rating !== undefined && (!Number.isInteger(rating) || rating < 1 || rating > 5)) {
|
|
503
|
-
return { kind: 'error', message: `${RATING_FLAG} takes a whole number from 1 to 5` };
|
|
504
|
-
}
|
|
505
|
-
// Everything that is not a flag or the rating's value is the message. Quoting is the user's
|
|
506
|
-
// job for shell reasons, but joining the remainder means an unquoted sentence still works.
|
|
507
|
-
const consumed = new Set(-1 === ratingAt ? [] : [ratingAt, ratingAt + 1]);
|
|
508
|
-
const text = rest
|
|
509
|
-
.filter((arg, i) => !consumed.has(i) && !arg.startsWith('--'))
|
|
510
|
-
.join(' ')
|
|
511
|
-
.trim();
|
|
512
|
-
return {
|
|
513
|
-
kind: 'feedback',
|
|
514
|
-
text,
|
|
515
|
-
...(rating !== undefined ? { rating } : {}),
|
|
516
|
-
bug: rest.includes(BUG_FLAG),
|
|
517
|
-
};
|
|
518
|
-
}
|
|
501
|
+
case FEEDBACK_COMMAND:
|
|
502
|
+
return parseFeedbackArgs(rest);
|
|
519
503
|
case OPEN_COMMAND: {
|
|
520
504
|
const port = parsePortFlag(rest, defaultPort);
|
|
521
505
|
// The first non-flag arg is the url (optional — omitting reuses a connected tab).
|
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);
|
|
@@ -0,0 +1,37 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* What `doctor` says about the daemon it found — pure, so it can be tested without a socket.
|
|
3
|
+
*
|
|
4
|
+
* Doctor already stopped lying about the PORT: `probePresence` separates "a daemon is here" from "a
|
|
5
|
+
* stranger holds it" from "nothing is listening", which was the load-bearing half of the report. It
|
|
6
|
+
* still did not say WHICH daemon, though the `/status` payload it already fetches carries `version`
|
|
7
|
+
* and `contract` and it discarded both.
|
|
8
|
+
*
|
|
9
|
+
* That is the gap worth closing, because skew is invisible everywhere else: a CLI and a daemon on
|
|
10
|
+
* different contracts connect anyway and then disagree about behaviour, which reaches the agent as a
|
|
11
|
+
* bare `-32000` naming no version at all. Doctor is the command a human runs at exactly that moment.
|
|
12
|
+
*
|
|
13
|
+
* Same split as `classifyPort`/`probePresence`: the rule lives here and is tested; the fetching stays
|
|
14
|
+
* at the call site.
|
|
15
|
+
*/
|
|
16
|
+
/** The fields of `/status` this line cares about. Both optional — an old daemon reports neither. */
|
|
17
|
+
export interface DaemonIdentity {
|
|
18
|
+
version?: string;
|
|
19
|
+
contract?: string;
|
|
20
|
+
}
|
|
21
|
+
export interface DaemonLine {
|
|
22
|
+
/** The `daemon ✓ …` line itself. */
|
|
23
|
+
text: string;
|
|
24
|
+
/** A skew sentence to print underneath, when this CLI and that daemon disagree. */
|
|
25
|
+
skew?: string;
|
|
26
|
+
}
|
|
27
|
+
/**
|
|
28
|
+
* Build the daemon line for a port that answered `/status`.
|
|
29
|
+
*
|
|
30
|
+
* `pid` comes from the pid file and is genuinely optional — a daemon started by another user, or one
|
|
31
|
+
* whose pid file was cleaned up, still answers. Printing `(pid null)` would be worse than printing
|
|
32
|
+
* nothing, which is the same class of mistake as the port lie this command already fixed.
|
|
33
|
+
*/
|
|
34
|
+
export declare function daemonLine(port: number, pid: number | null, peer: DaemonIdentity, self: {
|
|
35
|
+
version: string;
|
|
36
|
+
contract: string;
|
|
37
|
+
}): DaemonLine;
|
|
@@ -0,0 +1,44 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* What `doctor` says about the daemon it found — pure, so it can be tested without a socket.
|
|
3
|
+
*
|
|
4
|
+
* Doctor already stopped lying about the PORT: `probePresence` separates "a daemon is here" from "a
|
|
5
|
+
* stranger holds it" from "nothing is listening", which was the load-bearing half of the report. It
|
|
6
|
+
* still did not say WHICH daemon, though the `/status` payload it already fetches carries `version`
|
|
7
|
+
* and `contract` and it discarded both.
|
|
8
|
+
*
|
|
9
|
+
* That is the gap worth closing, because skew is invisible everywhere else: a CLI and a daemon on
|
|
10
|
+
* different contracts connect anyway and then disagree about behaviour, which reaches the agent as a
|
|
11
|
+
* bare `-32000` naming no version at all. Doctor is the command a human runs at exactly that moment.
|
|
12
|
+
*
|
|
13
|
+
* Same split as `classifyPort`/`probePresence`: the rule lives here and is tested; the fetching stays
|
|
14
|
+
* at the call site.
|
|
15
|
+
*/
|
|
16
|
+
import { describeSkew } from '../version/version-skew.js';
|
|
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).';
|
|
27
|
+
/**
|
|
28
|
+
* Build the daemon line for a port that answered `/status`.
|
|
29
|
+
*
|
|
30
|
+
* `pid` comes from the pid file and is genuinely optional — a daemon started by another user, or one
|
|
31
|
+
* whose pid file was cleaned up, still answers. Printing `(pid null)` would be worse than printing
|
|
32
|
+
* nothing, which is the same class of mistake as the port lie this command already fixed.
|
|
33
|
+
*/
|
|
34
|
+
export function daemonLine(port, pid, peer, self) {
|
|
35
|
+
const parts = [];
|
|
36
|
+
if (null !== pid)
|
|
37
|
+
parts.push(`pid ${String(pid)}`);
|
|
38
|
+
if (peer.version !== undefined)
|
|
39
|
+
parts.push(`v${peer.version}`);
|
|
40
|
+
const detail = 0 === parts.length ? '' : ` (${parts.join(', ')})`;
|
|
41
|
+
const skew = describeSkew({ what: 'the daemon on this port', version: peer.version, contract: peer.contract, fix: FIX }, self);
|
|
42
|
+
const line = { text: ` daemon ✓ running on :${String(port)}${detail}` };
|
|
43
|
+
return skew === undefined ? line : { ...line, skew };
|
|
44
|
+
}
|
|
@@ -0,0 +1,43 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Who is holding the bridge port.
|
|
3
|
+
*
|
|
4
|
+
* `doctor` is what a user runs when the agent cannot reach the bridge, and the commonest real cause
|
|
5
|
+
* is that something else is on 4400. It already stopped saying "nothing is listening" for that case,
|
|
6
|
+
* but it did not say WHICH process — so the reader's next move was a shell command they had to know.
|
|
7
|
+
*
|
|
8
|
+
* Naming the holder is also the safe answer to a trap this project has hit for real: the obvious
|
|
9
|
+
* lookup, `lsof -ti tcp:4400 | xargs kill -9`, ALSO kills the agent's own `reticle mcp` proxy,
|
|
10
|
+
* because that proxy holds a client connection to the same port and `-ti` does not filter to
|
|
11
|
+
* listeners. Every invocation here is `-sTCP:LISTEN`, and the message never suggests `-ti`.
|
|
12
|
+
*
|
|
13
|
+
* The parser is pure and the lookup takes an injected exec, so the rule is tested on every platform
|
|
14
|
+
* while the shelling out stays at the call site.
|
|
15
|
+
*/
|
|
16
|
+
export interface PortHolder {
|
|
17
|
+
pid: number;
|
|
18
|
+
command: string;
|
|
19
|
+
}
|
|
20
|
+
/**
|
|
21
|
+
* The FIRST listener in `lsof -nP -iTCP:<port> -sTCP:LISTEN -F pc` output, or null.
|
|
22
|
+
*
|
|
23
|
+
* First, not "the only one": a dual-stack listener (IPv4 + IPv6) is reported twice, and on a port
|
|
24
|
+
* conflict the answer the reader needs is "something is there, here is what" rather than an
|
|
25
|
+
* exhaustive list. A second `p` record ends the first.
|
|
26
|
+
*
|
|
27
|
+
* Null on anything incomplete. Half an answer — a pid with no command — prints worse than the
|
|
28
|
+
* un-named message it would replace, which is the same mistake as the port lie this command fixed.
|
|
29
|
+
*/
|
|
30
|
+
export declare function parsePortHolder(stdout: string): PortHolder | null;
|
|
31
|
+
/** What `doctor` prints for a port held by something that is not a Reticle daemon. */
|
|
32
|
+
export declare function describeForeignHolder(port: number, holder: PortHolder | null,
|
|
33
|
+
/**
|
|
34
|
+
* The pid we recorded for this port, from the daemon pid file.
|
|
35
|
+
*
|
|
36
|
+
* When it matches the process actually holding the port, the port is held by OUR daemon — one
|
|
37
|
+
* that accepts TCP and never answers `/status`, i.e. wedged. That classifies as FOREIGN, which is
|
|
38
|
+
* right for "can anything bind this?" and wrong to say out loud: the reader goes hunting a
|
|
39
|
+
* stranger, and the fix that works (`reticle stop`, it respawns on the next call) is never named.
|
|
40
|
+
*/
|
|
41
|
+
ourPid?: number | null): string;
|
|
42
|
+
/** The lookup itself. `exec` is injected so the shelling out never runs in a unit test. */
|
|
43
|
+
export declare function findPortHolder(port: number, exec: (command: string, args: readonly string[]) => string | null): PortHolder | null;
|