@reticlehq/server 3.5.0 → 3.6.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 +1 -1
- package/SKILL.md +16 -8
- package/dist/chromium-launch-options.d.ts +3 -1
- package/dist/chromium-launch-options.js +2 -1
- package/dist/command/cli/adhoc-verdict.d.ts +1 -0
- package/dist/command/cli/adhoc-verdict.js +6 -3
- package/dist/command/cli/answers/open-note.d.ts +2 -0
- package/dist/command/cli/answers/open-note.js +22 -0
- package/dist/command/cli/cli-flow-commands.js +4 -8
- package/dist/command/cli/cli-parse.d.ts +1 -1
- package/dist/command/cli/cli-parse.js +10 -10
- package/dist/command/cli/cli-verify.d.ts +1 -1
- package/dist/command/cli/cli-verify.js +35 -25
- package/dist/command/cli/cloud-cli.js +51 -12
- package/dist/command/cli/cloud-login.d.ts +8 -0
- package/dist/command/cli/cloud-login.js +36 -2
- package/dist/command/cli/demo/demo-page.js +11 -3
- package/dist/command/cli/doctor/browser/chromium-hint.d.ts +46 -0
- package/dist/command/cli/doctor/browser/chromium-hint.js +99 -1
- package/dist/command/cli/drive/drive-attach.d.ts +27 -2
- package/dist/command/cli/drive/drive-attach.js +71 -4
- package/dist/command/cli/drive/drive-command.d.ts +2 -1
- package/dist/command/cli/drive/drive-command.js +13 -7
- package/dist/command/cli/lifecycle/daemon-lifecycle.js +4 -1
- package/dist/command/cli/ports/port-holder.d.ts +10 -0
- package/dist/command/cli/ports/port-holder.js +24 -0
- package/dist/command/cli/setup-mcp-cli.js +6 -4
- package/dist/command/cli/tutorial.d.ts +2 -2
- package/dist/command/cli/tutorial.js +6 -9
- package/dist/command/cli.d.ts +6 -0
- package/dist/command/cli.js +28 -22
- package/dist/command/daemon/binding/daemon-status-probe.d.ts +2 -0
- package/dist/command/daemon/binding/daemon-status-probe.js +1 -0
- package/dist/command/daemon/daemon-resolve.d.ts +14 -0
- package/dist/command/daemon/daemon-resolve.js +33 -2
- package/dist/command/setup/agent-writer.d.ts +1 -1
- package/dist/command/setup/agent-writer.js +3 -2
- package/dist/command/setup/bringup/connect-lease.d.ts +1 -0
- package/dist/command/setup/bringup/connect-lease.js +1 -0
- package/dist/command/setup/bringup/dev-server-wait.d.ts +8 -0
- package/dist/command/setup/bringup/dev-server-wait.js +19 -0
- package/dist/command/setup/init/init-port.d.ts +4 -0
- package/dist/command/setup/init/init-port.js +10 -0
- package/dist/command/setup/init/init-runtime.d.ts +10 -0
- package/dist/command/setup/init/init-runtime.js +18 -1
- package/dist/command/setup/node-effects.js +2 -1
- package/dist/command/setup/remaining-steps.js +2 -1
- package/dist/command/setup/run-setup.d.ts +5 -0
- package/dist/command/setup/run-setup.js +35 -27
- package/dist/command/setup/setup-command.d.ts +2 -1
- package/dist/command/setup/setup-command.js +2 -0
- package/dist/command/version/version-skew.js +11 -2
- package/dist/features/crawl/crawl-tools.js +4 -0
- package/dist/features/crawl/crawl.d.ts +6 -0
- package/dist/features/crawl/crawl.js +9 -1
- package/dist/features/exhaust/ledger.d.ts +7 -0
- package/dist/features/exhaust/ledger.js +35 -9
- package/dist/features/harness/app-findings.d.ts +2 -0
- package/dist/features/harness/app-findings.js +52 -0
- package/dist/features/harness/drive-report.d.ts +27 -1
- package/dist/features/harness/drive-report.js +162 -11
- package/dist/features/harness/drivers.d.ts +10 -14
- package/dist/features/harness/drivers.js +10 -14
- package/dist/features/harness/fill-values.d.ts +3 -73
- package/dist/features/harness/fill-values.js +3 -169
- package/dist/features/harness/goals.d.ts +21 -0
- package/dist/features/harness/goals.js +77 -0
- package/dist/features/harness/harness.d.ts +21 -0
- package/dist/features/harness/harness.js +69 -2
- package/dist/features/harness/platform/drive-target.d.ts +30 -0
- package/dist/features/harness/platform/drive-target.js +81 -0
- package/dist/features/harness/platform/local-apps.d.ts +94 -0
- package/dist/features/harness/platform/local-apps.js +204 -0
- package/dist/features/harness/platform/remote-drive.d.ts +180 -0
- package/dist/features/harness/platform/remote-drive.js +365 -0
- package/dist/features/harness/platform/script.d.ts +48 -0
- package/dist/features/harness/platform/script.js +128 -0
- package/dist/features/harness/platform/server-driver.d.ts +41 -0
- package/dist/features/harness/platform/server-driver.js +179 -0
- package/dist/features/harness/platform-config.d.ts +7 -0
- package/dist/features/harness/platform-config.js +14 -2
- package/dist/features/harness/script/app-controls.d.ts +5 -0
- package/dist/features/harness/script/app-controls.js +40 -0
- package/dist/features/harness/script/lane-ids.d.ts +14 -0
- package/dist/features/harness/script/lane-ids.js +16 -0
- package/dist/features/harness/script/script-run.d.ts +41 -0
- package/dist/features/harness/script/script-run.js +299 -0
- package/dist/features/license/license.d.ts +2 -2
- package/dist/features/visual/visual-tools.d.ts +10 -1
- package/dist/features/visual/visual-tools.js +40 -3
- package/dist/hooks/driven-by.d.ts +4 -0
- package/dist/hooks/driven-by.js +16 -0
- package/dist/hooks/hook-commands.js +15 -4
- package/dist/hooks/tool-log.d.ts +9 -0
- package/dist/hooks/tool-log.js +29 -0
- package/dist/index.d.ts +1 -0
- package/dist/index.js +76 -82
- package/dist/judgement/runs/artifact/build-verification-run.js +6 -0
- package/dist/judgement/runs/artifact/replay-mapping.d.ts +1 -1
- package/dist/judgement/runs/artifact/replay-mapping.js +16 -6
- package/dist/judgement/runs/artifact/run-store.d.ts +1 -1
- package/dist/judgement/runs/artifact/run-store.js +26 -1
- package/dist/judgement/runs/drive-run.d.ts +22 -0
- package/dist/judgement/runs/drive-run.js +99 -1
- package/dist/judgement/runs/repair-prompt.js +1 -1
- package/dist/judgement/runs/reticle-runner.js +8 -1
- package/dist/judgement/runs/verification-sync.d.ts +3 -2
- package/dist/judgement/runs/verification-sync.js +4 -3
- package/dist/language/flows/change/gate.d.ts +10 -0
- package/dist/language/flows/change/gate.js +22 -0
- package/dist/language/flows/decision.js +34 -0
- package/dist/language/flows/flow-anchor.d.ts +54 -8
- package/dist/language/flows/flow-anchor.js +88 -3
- package/dist/language/flows/flow-cross-step.d.ts +15 -0
- package/dist/language/flows/flow-cross-step.js +21 -0
- package/dist/language/flows/flow-learning.js +8 -11
- package/dist/language/flows/flow-mutate-tools.js +5 -3
- package/dist/language/flows/flow-replay-run.d.ts +23 -9
- package/dist/language/flows/flow-replay-run.js +101 -94
- package/dist/language/flows/flow-replay.d.ts +23 -1
- package/dist/language/flows/flow-replay.js +92 -19
- package/dist/language/flows/flow-scope.js +5 -1
- package/dist/language/flows/flow-step-runners.d.ts +20 -2
- package/dist/language/flows/flow-step-runners.js +236 -42
- package/dist/language/flows/flow-store-for-session.d.ts +2 -2
- package/dist/language/flows/flow-store-for-session.js +2 -2
- package/dist/language/flows/flow-tools.js +58 -34
- package/dist/language/flows/flows.js +5 -2
- package/dist/language/flows/heal-run.js +6 -4
- package/dist/language/flows/recording/flake.d.ts +2 -2
- package/dist/language/flows/recording/tape/recordings.d.ts +30 -4
- package/dist/language/flows/recording/tape/recordings.js +40 -8
- package/dist/language/flows/replay-from-hud.d.ts +20 -0
- package/dist/language/flows/replay-from-hud.js +51 -0
- package/dist/language/flows/replay.d.ts +9 -5
- package/dist/language/flows/replay.js +27 -9
- package/dist/language/flows/suite/suite-flakes.js +2 -0
- package/dist/language/flows/suite-selection.d.ts +2 -15
- package/dist/language/flows/suite-selection.js +4 -3
- package/dist/language/flows/verify-change-tools.js +8 -4
- package/dist/launch-chromium.d.ts +27 -5
- package/dist/launch-chromium.js +38 -8
- package/dist/memory/cloud/cloud-config.d.ts +12 -0
- package/dist/memory/cloud/cloud-config.js +15 -0
- package/dist/memory/cloud/cloud-sync.d.ts +17 -14
- package/dist/memory/cloud/cloud-sync.js +12 -1
- package/dist/memory/cloud/harness-config.d.ts +26 -1
- package/dist/memory/cloud/harness-config.js +99 -4
- package/dist/memory/cloud/hud-notices-source.d.ts +21 -0
- package/dist/memory/cloud/hud-notices-source.js +128 -0
- package/dist/memory/cloud/memory-scope.d.ts +144 -0
- package/dist/memory/cloud/memory-scope.js +205 -0
- package/dist/memory/cloud/sync-cycle.d.ts +12 -3
- package/dist/memory/cloud/sync-cycle.js +16 -3
- package/dist/memory/cloud/sync-daemon.js +7 -5
- package/dist/memory/cloud/sync-disk.d.ts +1 -1
- package/dist/memory/cloud/sync-disk.js +5 -8
- package/dist/memory/impact/account-watch.d.ts +19 -0
- package/dist/memory/impact/account-watch.js +34 -0
- package/dist/memory/impact/impact-recorder.d.ts +8 -0
- package/dist/memory/impact/impact-recorder.js +12 -2
- package/dist/memory/impact/impact-store.d.ts +16 -0
- package/dist/memory/impact/impact-store.js +142 -26
- package/dist/memory/intent/intent-shard.d.ts +2 -2
- package/dist/memory/intent/intent-tools.js +80 -2
- package/dist/memory/journal/envelope.d.ts +8 -8
- package/dist/memory/journal/journal-recorder.js +7 -1
- package/dist/memory/journal/on-disk/retention.d.ts +3 -0
- package/dist/memory/journal/on-disk/retention.js +5 -0
- package/dist/memory/journal/on-disk/startup-maintenance.js +3 -1
- package/dist/memory/journal/on-disk/workspace-tiers.js +7 -4
- package/dist/memory/journal/session-end.js +6 -4
- package/dist/memory/project/artifact-root-resolver.d.ts +26 -1
- package/dist/memory/project/artifact-root-resolver.js +94 -13
- package/dist/memory/project/artifact-root.d.ts +42 -5
- package/dist/memory/project/artifact-root.js +87 -8
- package/dist/memory/project/dir/reticle-dir.d.ts +4 -2
- package/dist/memory/project/dir/reticle-dir.js +3 -1
- package/dist/memory/project/project-tools.js +22 -3
- package/dist/memory/project/session-root.d.ts +24 -1
- package/dist/memory/project/session-root.js +41 -5
- package/dist/memory/project/sync-status.d.ts +39 -0
- package/dist/memory/project/sync-status.js +90 -0
- package/dist/memory/recall/memory-tools.js +2 -0
- package/dist/memory/recall/project-memory.d.ts +19 -0
- package/dist/memory/recall/project-memory.js +45 -6
- package/dist/portal/bridge/bridge.d.ts +3 -1
- package/dist/portal/bridge/bridge.js +19 -2
- package/dist/portal/bridge/pairing-token.d.ts +9 -0
- package/dist/portal/bridge/pairing-token.js +17 -0
- package/dist/portal/input/real-input.d.ts +81 -10
- package/dist/portal/input/real-input.js +207 -22
- package/dist/portal/input/viewport-tools.js +3 -3
- package/dist/portal/pool/browser-pool.d.ts +22 -9
- package/dist/portal/pool/browser-pool.js +70 -35
- package/dist/portal/pool/playwright-launcher.d.ts +13 -0
- package/dist/portal/pool/playwright-launcher.js +17 -1
- package/dist/portal/pool/pool-contract.d.ts +7 -4
- package/dist/portal/session/command-payload.d.ts +8 -0
- package/dist/portal/session/command-payload.js +19 -0
- package/dist/portal/session/dev-server/dev-command.d.ts +12 -0
- package/dist/portal/session/dev-server/dev-command.js +61 -12
- package/dist/portal/session/facts/departure-note.d.ts +22 -0
- package/dist/portal/session/facts/departure-note.js +47 -0
- package/dist/portal/session/facts/session-replaced.d.ts +5 -0
- package/dist/portal/session/facts/session-replaced.js +5 -0
- package/dist/portal/session/first-run-wiring.d.ts +42 -0
- package/dist/portal/session/first-run-wiring.js +132 -0
- package/dist/portal/session/human/notes-ledger.d.ts +20 -0
- package/dist/portal/session/human/notes-ledger.js +55 -0
- package/dist/portal/session/human/review-store.d.ts +12 -0
- package/dist/portal/session/human/review-store.js +18 -10
- package/dist/portal/session/live-control-tools.d.ts +0 -13
- package/dist/portal/session/live-control-tools.js +28 -3
- package/dist/portal/session/no-session-diagnosis.d.ts +7 -0
- package/dist/portal/session/no-session-diagnosis.js +15 -0
- package/dist/portal/session/no-session-next-action.d.ts +11 -0
- package/dist/portal/session/no-session-next-action.js +31 -5
- package/dist/portal/session/no-session-watch.d.ts +14 -1
- package/dist/portal/session/no-session-watch.js +63 -10
- package/dist/portal/session/presence/session-mirror.js +3 -0
- package/dist/portal/session/serving-directory.d.ts +27 -0
- package/dist/portal/session/serving-directory.js +45 -0
- package/dist/portal/session/session-health.d.ts +7 -0
- package/dist/portal/session/session-health.js +6 -0
- package/dist/portal/session/session-info.d.ts +8 -1
- package/dist/portal/session/session-info.js +6 -0
- package/dist/portal/session/session-manager.d.ts +21 -0
- package/dist/portal/session/session-manager.js +44 -8
- package/dist/portal/session/session-reaper.js +4 -1
- package/dist/portal/session/session-successor.d.ts +5 -0
- package/dist/portal/session/session-tools.js +26 -3
- package/dist/portal/session/session.d.ts +7 -2
- package/dist/portal/session/session.js +21 -18
- package/dist/start-options.d.ts +6 -0
- package/dist/status-payload.d.ts +10 -0
- package/dist/status-payload.js +23 -0
- package/dist/surface/http-server.js +11 -1
- package/dist/surface/mcp/mcp-proxy.d.ts +5 -0
- package/dist/surface/mcp/mcp-proxy.js +36 -5
- package/dist/surface/mcp/mcp.d.ts +11 -6
- package/dist/surface/mcp/mcp.js +27 -13
- package/dist/surface/mcp/server-instructions.d.ts +4 -2
- package/dist/surface/mcp/server-instructions.js +14 -4
- package/dist/surface/mcp/shared-params.js +2 -2
- package/dist/surface/tools/act/navigation/navigate-arrival.d.ts +8 -0
- package/dist/surface/tools/act/navigation/navigate-arrival.js +56 -2
- package/dist/surface/tools/act/settle-in-flight.js +25 -3
- package/dist/surface/tools/act-sequence-tool.js +2 -2
- package/dist/surface/tools/act-tools.js +23 -7
- package/dist/surface/tools/assert/assert-verdict.d.ts +1 -1
- package/dist/surface/tools/assert/assert-verdict.js +17 -2
- package/dist/surface/tools/browser-tools.js +8 -1
- package/dist/surface/tools/chat-drives.d.ts +26 -0
- package/dist/surface/tools/chat-drives.js +165 -0
- package/dist/surface/tools/empty-session-list.d.ts +41 -0
- package/dist/surface/tools/empty-session-list.js +80 -0
- package/dist/surface/tools/error-recovery.d.ts +6 -0
- package/dist/surface/tools/error-recovery.js +37 -4
- package/dist/surface/tools/explore-tools.d.ts +10 -1
- package/dist/surface/tools/explore-tools.js +82 -13
- package/dist/surface/tools/feedback-tools.js +14 -0
- package/dist/surface/tools/harness-explore.d.ts +96 -14
- package/dist/surface/tools/harness-explore.js +227 -206
- package/dist/surface/tools/harness-plan.d.ts +12 -0
- package/dist/surface/tools/harness-plan.js +31 -0
- package/dist/surface/tools/harness-script.d.ts +8 -0
- package/dist/surface/tools/harness-script.js +342 -0
- package/dist/surface/tools/harness-toolset.d.ts +14 -1
- package/dist/surface/tools/harness-toolset.js +77 -6
- package/dist/surface/tools/invoke-tool.js +52 -1
- package/dist/surface/tools/lease-seed.d.ts +29 -0
- package/dist/surface/tools/lease-seed.js +123 -0
- package/dist/surface/tools/lease-tools.d.ts +3 -23
- package/dist/surface/tools/lease-tools.js +30 -131
- package/dist/surface/tools/live-call-text.js +2 -2
- package/dist/surface/tools/next-step.d.ts +19 -0
- package/dist/surface/tools/next-step.js +159 -0
- package/dist/surface/tools/observe-tools.js +2 -4
- package/dist/surface/tools/platform-moment.d.ts +12 -0
- package/dist/surface/tools/platform-moment.js +168 -0
- package/dist/surface/tools/query-hint-schema.d.ts +7 -0
- package/dist/surface/tools/query-hint-schema.js +10 -0
- package/dist/surface/tools/read-tools.js +8 -2
- package/dist/surface/tools/real-input-attempt.js +96 -13
- package/dist/surface/tools/tool-kit.d.ts +15 -1
- package/dist/surface/tools/tool-kit.js +4 -6
- package/dist/surface/tools/tools.js +20 -48
- package/dist/telemetry/hud-metrics.d.ts +2 -0
- package/dist/telemetry/hud-metrics.js +17 -0
- package/dist/telemetry/telemetry.d.ts +12 -0
- package/dist/telemetry/telemetry.js +43 -14
- package/dist/telemetry/verification-of.js +1 -0
- package/dist/wire-journal.js +4 -1
- package/docs/README.md +1 -1
- package/docs/agent-cheatsheet.md +1 -1
- package/docs/autodrive.md +29 -25
- package/docs/cli/doctor.mdx +6 -0
- package/docs/cli/drive.mdx +8 -2
- package/docs/cli/init.mdx +11 -13
- package/docs/cli/status.mdx +5 -1
- package/docs/cli/version.mdx +3 -3
- package/docs/cli.mdx +1 -1
- package/docs/desktop.mdx +5 -1
- package/docs/environment.mdx +3 -7
- package/docs/getting-started.md +13 -11
- package/docs/human-control.md +1 -2
- package/docs/index.mdx +4 -3
- package/docs/install-manual.mdx +2 -2
- package/docs/packages/tauri.mdx +2 -2
- package/docs/packages/vite-plugin.mdx +22 -1
- package/docs/packages.mdx +1 -1
- package/docs/quickstart.mdx +17 -7
- package/docs/telemetry.md +1 -1
- package/docs/tools/session-and-feedback.mdx +9 -9
- package/docs/usage.md +5 -2
- package/docs/what-is-recorded.md +13 -3
- package/docs/what-is-reticle.mdx +3 -1
- package/package.json +8 -12
- package/dist/features/harness/driver.d.ts +0 -50
- package/dist/features/harness/driver.js +0 -236
- package/dist/features/harness/jev-driver.d.ts +0 -188
- package/dist/features/harness/jev-driver.js +0 -678
- package/dist/features/harness/openai-driver.d.ts +0 -58
- package/dist/features/harness/openai-driver.js +0 -206
- package/dist/memory/project/dir/fill-value-store.d.ts +0 -32
- package/dist/memory/project/dir/fill-value-store.js +0 -88
- package/docs/local-registry.md +0 -143
|
@@ -125,11 +125,11 @@ const linkAfterLogin = async (url, project, link) => {
|
|
|
125
125
|
* Browser device flow — the DEFAULT `reticle login` (like `gh auth login`): fetch a device + user code,
|
|
126
126
|
* open the browser to approve, then poll until the user confirms. No email to type, no code to copy back.
|
|
127
127
|
*/
|
|
128
|
-
const cmdLoginDevice = async (explicitUrl, project, link, autoLink) => {
|
|
128
|
+
const cmdLoginDevice = async (explicitUrl, project, link, autoLink, open = openBrowser) => {
|
|
129
129
|
const url = baseUrl(null, explicitUrl);
|
|
130
130
|
const started = DeviceStartSchema.parse(await api('POST', `${url}/v1/auth/device/start`, null, {}));
|
|
131
131
|
hint(`Opening ${started.verificationUri} — confirm this code in the browser: ${started.userCode}`);
|
|
132
|
-
|
|
132
|
+
open(started.verificationUriComplete);
|
|
133
133
|
const intervalMs = Math.max(1, started.interval) * 1000;
|
|
134
134
|
for (;;) {
|
|
135
135
|
await sleep(intervalMs);
|
|
@@ -200,3 +200,37 @@ export const cmdLogout = async (argv = []) => {
|
|
|
200
200
|
emit({ loggedOut: true, url });
|
|
201
201
|
return 0;
|
|
202
202
|
};
|
|
203
|
+
/** The daemon signs in for the panel and links nothing: its working directory is not the user's app. */
|
|
204
|
+
const NO_LINK = () => Promise.resolve(0);
|
|
205
|
+
/**
|
|
206
|
+
* The panel's Sign in: the same browser approval `reticle login` opens, started by the daemon, which
|
|
207
|
+
* owns `~/.reticle`. One flow at a time, and a second press while it is pending reopens its approval
|
|
208
|
+
* page rather than doing nothing: somebody who closed that tab must be able to get it back.
|
|
209
|
+
* `onSignedIn` runs when the approval lands so every open panel can repaint. A failure stays in the
|
|
210
|
+
* daemon log: the panel copied `reticle login` as it was clicked, which is the way through.
|
|
211
|
+
*/
|
|
212
|
+
export function hudSignIn(onSignedIn, start = (open) => cmdLoginDevice(undefined, undefined, NO_LINK, false, open), opener = openBrowser) {
|
|
213
|
+
let running = false;
|
|
214
|
+
let pending;
|
|
215
|
+
return () => {
|
|
216
|
+
if (running) {
|
|
217
|
+
if (pending !== undefined)
|
|
218
|
+
opener(pending);
|
|
219
|
+
return;
|
|
220
|
+
}
|
|
221
|
+
running = true;
|
|
222
|
+
void start((url) => {
|
|
223
|
+
pending = url;
|
|
224
|
+
opener(url);
|
|
225
|
+
})
|
|
226
|
+
.then((code) => {
|
|
227
|
+
if (0 === code)
|
|
228
|
+
onSignedIn();
|
|
229
|
+
})
|
|
230
|
+
.catch(() => undefined)
|
|
231
|
+
.finally(() => {
|
|
232
|
+
running = false;
|
|
233
|
+
pending = undefined;
|
|
234
|
+
});
|
|
235
|
+
};
|
|
236
|
+
}
|
|
@@ -30,7 +30,7 @@
|
|
|
30
30
|
import { fileURLToPath } from 'node:url';
|
|
31
31
|
import { createServer } from 'node:http';
|
|
32
32
|
import { dirname, join, relative, extname, sep } from 'node:path';
|
|
33
|
-
import { existsSync, readFileSync, readdirSync
|
|
33
|
+
import { existsSync, readFileSync, readdirSync } from 'node:fs';
|
|
34
34
|
/** Where the tour starts. Everything else on the page is reached from here. */
|
|
35
35
|
const SDK_ENTRY = '@reticlehq/browser';
|
|
36
36
|
/** The signal the demo app fires when it saves — the consequence the tour declares in advance. */
|
|
@@ -208,13 +208,21 @@ export function serveDemoPage(html, graph) {
|
|
|
208
208
|
continue;
|
|
209
209
|
const file = join(mount.dir, path.slice(mount.prefix.length));
|
|
210
210
|
// A request that climbed out of the mount is not served, whatever it points at.
|
|
211
|
-
if (!file.startsWith(mount.dir + sep)
|
|
211
|
+
if (!file.startsWith(mount.dir + sep))
|
|
212
|
+
break;
|
|
213
|
+
// One read, no prior exists/stat check: a missing file or a directory throws here, and there
|
|
214
|
+
// is no window between a check and the read for the path to change under it.
|
|
215
|
+
let body;
|
|
216
|
+
try {
|
|
217
|
+
body = readFileSync(file);
|
|
218
|
+
}
|
|
219
|
+
catch {
|
|
212
220
|
break;
|
|
213
221
|
}
|
|
214
222
|
res.writeHead(200, {
|
|
215
223
|
'content-type': '.js' === extname(file) ? JS_CONTENT_TYPE : 'application/octet-stream',
|
|
216
224
|
});
|
|
217
|
-
res.end(
|
|
225
|
+
res.end(body);
|
|
218
226
|
return;
|
|
219
227
|
}
|
|
220
228
|
res.writeHead(404);
|
|
@@ -48,7 +48,45 @@ export interface ChromiumProbe {
|
|
|
48
48
|
* `Microsoft Edge`) — see launch-chromium. Present only when `exists` is false and one was found.
|
|
49
49
|
*/
|
|
50
50
|
fallback?: string | undefined;
|
|
51
|
+
/** `executablePath` came from RETICLE_CHROMIUM_PATH, so the bundled build is not in play at all. */
|
|
52
|
+
configured?: boolean | undefined;
|
|
53
|
+
/** Why that configured executable cannot be launched. Present only when `exists` is false. */
|
|
54
|
+
problem?: ChromiumPathProblem | undefined;
|
|
51
55
|
}
|
|
56
|
+
/** What stops the executable RETICLE_CHROMIUM_PATH names from being launched. */
|
|
57
|
+
export declare const ChromiumPathProblem: {
|
|
58
|
+
readonly MISSING: "missing";
|
|
59
|
+
readonly NOT_A_FILE: "not-a-file";
|
|
60
|
+
readonly NOT_EXECUTABLE: "not-executable";
|
|
61
|
+
};
|
|
62
|
+
export type ChromiumPathProblem = (typeof ChromiumPathProblem)[keyof typeof ChromiumPathProblem];
|
|
63
|
+
/**
|
|
64
|
+
* What stops the file at `path` from being launched, or undefined when nothing does.
|
|
65
|
+
*
|
|
66
|
+
* Existing is not enough: Playwright cannot start a directory or a file without the executable bit,
|
|
67
|
+
* and its error for that reads like a missing install. Windows has no executable bit, so only the
|
|
68
|
+
* file check applies there.
|
|
69
|
+
*/
|
|
70
|
+
export declare function chromiumPathProblem(path: string, platform: NodeJS.Platform): ChromiumPathProblem | undefined;
|
|
71
|
+
/** The Chromium RETICLE_CHROMIUM_PATH names, or undefined when it is unset or blank. */
|
|
72
|
+
export declare function configuredChromiumPath(env: NodeJS.ProcessEnv): string | undefined;
|
|
73
|
+
/**
|
|
74
|
+
* What the probes report when RETICLE_CHROMIUM_PATH is set, or undefined when it is not. The user
|
|
75
|
+
* named the browser, so the bundled revision, the installed channels and the browsers root say
|
|
76
|
+
* nothing about whether a launch will work. Only the named file does.
|
|
77
|
+
*/
|
|
78
|
+
export declare function configuredChromiumProbe(env: NodeJS.ProcessEnv, pathProblem: (path: string) => ChromiumPathProblem | undefined): ChromiumProbe | undefined;
|
|
79
|
+
/**
|
|
80
|
+
* How every RETICLE_CHROMIUM_PATH refusal begins. Error recovery keys on it, so the variable's name
|
|
81
|
+
* is read as configuration rather than as a `reticle_*` tool the call got wrong.
|
|
82
|
+
*/
|
|
83
|
+
export declare const CHROMIUM_PATH_REFUSAL_PREFIX: string;
|
|
84
|
+
/**
|
|
85
|
+
* A RETICLE_CHROMIUM_PATH that cannot be launched. Worded apart from the missing-browser hint on
|
|
86
|
+
* purpose: that one sends the reader to install Playwright's build, which does nothing for a path
|
|
87
|
+
* typed wrong.
|
|
88
|
+
*/
|
|
89
|
+
export declare function configuredChromiumRefusal(path: string, problem: ChromiumPathProblem): string;
|
|
52
90
|
/** The install command, pinned when we know what to pin to. */
|
|
53
91
|
export declare function chromiumInstallCommand(playwrightVersion?: string): string;
|
|
54
92
|
/**
|
|
@@ -83,5 +121,13 @@ export declare function parseChromiumRevision(executablePath: string): {
|
|
|
83
121
|
export declare function probeChromium(): Promise<ChromiumProbe>;
|
|
84
122
|
/** The bundled playwright's version, or undefined if it cannot be read (the hint then goes unpinned). */
|
|
85
123
|
export declare function bundledPlaywrightVersion(): string | undefined;
|
|
124
|
+
/**
|
|
125
|
+
* Why the lease preflight refuses, or undefined when a launch can go ahead.
|
|
126
|
+
*
|
|
127
|
+
* "Chromium is not installed" is kept in the usual message because error-recovery routes it to the
|
|
128
|
+
* install fix. A RETICLE_CHROMIUM_PATH that cannot be launched is a wrong path, not a missing
|
|
129
|
+
* install, so it says only that and the install fix is never attached.
|
|
130
|
+
*/
|
|
131
|
+
export declare function chromiumPreflightRefusal(probe: ChromiumProbe): string | undefined;
|
|
86
132
|
/** The doctor line for the Chromium check — verdict, what was probed, and how to satisfy it. */
|
|
87
133
|
export declare function chromiumHint(probe: ChromiumProbe): string;
|
|
@@ -20,8 +20,86 @@
|
|
|
20
20
|
* resolves, so `PLAYWRIGHT_BROWSERS_PATH` and every platform default are honoured by construction
|
|
21
21
|
* rather than by a table of roots this file would have to keep in step with Playwright's.
|
|
22
22
|
*/
|
|
23
|
-
import { existsSync, readdirSync } from 'node:fs';
|
|
23
|
+
import { accessSync, constants as fsConstants, existsSync, readdirSync, statSync } from 'node:fs';
|
|
24
24
|
import { createRequire } from 'node:module';
|
|
25
|
+
import { ReticleEnv } from '@reticlehq/core';
|
|
26
|
+
/** What stops the executable RETICLE_CHROMIUM_PATH names from being launched. */
|
|
27
|
+
export const ChromiumPathProblem = {
|
|
28
|
+
MISSING: 'missing',
|
|
29
|
+
NOT_A_FILE: 'not-a-file',
|
|
30
|
+
NOT_EXECUTABLE: 'not-executable',
|
|
31
|
+
};
|
|
32
|
+
/**
|
|
33
|
+
* What stops the file at `path` from being launched, or undefined when nothing does.
|
|
34
|
+
*
|
|
35
|
+
* Existing is not enough: Playwright cannot start a directory or a file without the executable bit,
|
|
36
|
+
* and its error for that reads like a missing install. Windows has no executable bit, so only the
|
|
37
|
+
* file check applies there.
|
|
38
|
+
*/
|
|
39
|
+
export function chromiumPathProblem(path, platform) {
|
|
40
|
+
let isFile;
|
|
41
|
+
try {
|
|
42
|
+
isFile = statSync(path).isFile();
|
|
43
|
+
}
|
|
44
|
+
catch {
|
|
45
|
+
return ChromiumPathProblem.MISSING;
|
|
46
|
+
}
|
|
47
|
+
if (!isFile)
|
|
48
|
+
return ChromiumPathProblem.NOT_A_FILE;
|
|
49
|
+
if ('win32' === platform)
|
|
50
|
+
return undefined;
|
|
51
|
+
try {
|
|
52
|
+
accessSync(path, fsConstants.X_OK);
|
|
53
|
+
return undefined;
|
|
54
|
+
}
|
|
55
|
+
catch {
|
|
56
|
+
return ChromiumPathProblem.NOT_EXECUTABLE;
|
|
57
|
+
}
|
|
58
|
+
}
|
|
59
|
+
/** The Chromium RETICLE_CHROMIUM_PATH names, or undefined when it is unset or blank. */
|
|
60
|
+
export function configuredChromiumPath(env) {
|
|
61
|
+
const path = env[ReticleEnv.CHROMIUM_PATH]?.trim();
|
|
62
|
+
return path === undefined || 0 === path.length ? undefined : path;
|
|
63
|
+
}
|
|
64
|
+
/**
|
|
65
|
+
* What the probes report when RETICLE_CHROMIUM_PATH is set, or undefined when it is not. The user
|
|
66
|
+
* named the browser, so the bundled revision, the installed channels and the browsers root say
|
|
67
|
+
* nothing about whether a launch will work. Only the named file does.
|
|
68
|
+
*/
|
|
69
|
+
export function configuredChromiumProbe(env, pathProblem) {
|
|
70
|
+
const path = configuredChromiumPath(env);
|
|
71
|
+
if (path === undefined)
|
|
72
|
+
return undefined;
|
|
73
|
+
const problem = pathProblem(path);
|
|
74
|
+
return {
|
|
75
|
+
executablePath: path,
|
|
76
|
+
exists: problem === undefined,
|
|
77
|
+
configured: true,
|
|
78
|
+
...(problem === undefined ? {} : { problem }),
|
|
79
|
+
};
|
|
80
|
+
}
|
|
81
|
+
/**
|
|
82
|
+
* How every RETICLE_CHROMIUM_PATH refusal begins. Error recovery keys on it, so the variable's name
|
|
83
|
+
* is read as configuration rather than as a `reticle_*` tool the call got wrong.
|
|
84
|
+
*/
|
|
85
|
+
export const CHROMIUM_PATH_REFUSAL_PREFIX = `${ReticleEnv.CHROMIUM_PATH} points at `;
|
|
86
|
+
/** What each problem reads as, after the path. */
|
|
87
|
+
const CHROMIUM_PATH_PROBLEM_TEXT = {
|
|
88
|
+
[ChromiumPathProblem.MISSING]: 'which does not exist',
|
|
89
|
+
[ChromiumPathProblem.NOT_A_FILE]: 'which is not a file',
|
|
90
|
+
[ChromiumPathProblem.NOT_EXECUTABLE]: 'which is not executable',
|
|
91
|
+
};
|
|
92
|
+
const CHROMIUM_PATH_FIX = "fix the path, or unset it to use Playwright's own Chromium";
|
|
93
|
+
/** The doctor verdict for a RETICLE_CHROMIUM_PATH that can be launched. */
|
|
94
|
+
const CHROMIUM_PATH_IN_USE = (path) => `✓ using ${path} (${ReticleEnv.CHROMIUM_PATH})`;
|
|
95
|
+
/**
|
|
96
|
+
* A RETICLE_CHROMIUM_PATH that cannot be launched. Worded apart from the missing-browser hint on
|
|
97
|
+
* purpose: that one sends the reader to install Playwright's build, which does nothing for a path
|
|
98
|
+
* typed wrong.
|
|
99
|
+
*/
|
|
100
|
+
export function configuredChromiumRefusal(path, problem) {
|
|
101
|
+
return `${CHROMIUM_PATH_REFUSAL_PREFIX}${path}, ${CHROMIUM_PATH_PROBLEM_TEXT[problem]}; ${CHROMIUM_PATH_FIX}`;
|
|
102
|
+
}
|
|
25
103
|
/**
|
|
26
104
|
* A browsers-root entry that is a chromium build.
|
|
27
105
|
*
|
|
@@ -160,8 +238,28 @@ export function bundledPlaywrightVersion() {
|
|
|
160
238
|
}
|
|
161
239
|
return undefined;
|
|
162
240
|
}
|
|
241
|
+
/**
|
|
242
|
+
* Why the lease preflight refuses, or undefined when a launch can go ahead.
|
|
243
|
+
*
|
|
244
|
+
* "Chromium is not installed" is kept in the usual message because error-recovery routes it to the
|
|
245
|
+
* install fix. A RETICLE_CHROMIUM_PATH that cannot be launched is a wrong path, not a missing
|
|
246
|
+
* install, so it says only that and the install fix is never attached.
|
|
247
|
+
*/
|
|
248
|
+
export function chromiumPreflightRefusal(probe) {
|
|
249
|
+
if (probe.exists)
|
|
250
|
+
return undefined;
|
|
251
|
+
if (true === probe.configured && probe.executablePath !== undefined) {
|
|
252
|
+
return configuredChromiumRefusal(probe.executablePath, probe.problem ?? ChromiumPathProblem.MISSING);
|
|
253
|
+
}
|
|
254
|
+
return `Chromium is not installed for Playwright — ${chromiumHint(probe)}`;
|
|
255
|
+
}
|
|
163
256
|
/** The doctor line for the Chromium check — verdict, what was probed, and how to satisfy it. */
|
|
164
257
|
export function chromiumHint(probe) {
|
|
258
|
+
if (true === probe.configured && probe.executablePath !== undefined) {
|
|
259
|
+
return probe.exists
|
|
260
|
+
? CHROMIUM_PATH_IN_USE(probe.executablePath)
|
|
261
|
+
: `✗ ${configuredChromiumRefusal(probe.executablePath, probe.problem ?? ChromiumPathProblem.MISSING)}`;
|
|
262
|
+
}
|
|
165
263
|
if (probe.exists) {
|
|
166
264
|
// Naming the revision on the happy line too: it is the number the mismatch line talks about, and
|
|
167
265
|
// a reader comparing two machines has nothing to compare without it.
|
|
@@ -44,7 +44,7 @@ export declare function decideDriveMode(presence: PortPresence): DriveMode;
|
|
|
44
44
|
/** A driveable session the daemon opened on our behalf. */
|
|
45
45
|
interface DriveSession {
|
|
46
46
|
sessionId: string;
|
|
47
|
-
/** Whether the tab's SDK actually registered — false ⇒ the app may not embed @reticlehq/
|
|
47
|
+
/** Whether the tab's SDK actually registered — false ⇒ the app may not embed @reticlehq/browser. */
|
|
48
48
|
ready: boolean;
|
|
49
49
|
/** How long the session survives untouched. Absent from a daemon that does not report it. */
|
|
50
50
|
expiresInMs?: number;
|
|
@@ -76,8 +76,33 @@ export declare function readAttachResponse(port: number, url: string, res: {
|
|
|
76
76
|
}): DriveAttachResult;
|
|
77
77
|
/** Ask the daemon on `port` to open a driveable browser at `url`. */
|
|
78
78
|
export declare function requestDriveSession(port: number, url: string, post?: DrivePost): Promise<DriveAttachResult>;
|
|
79
|
+
/** Which app the attached page says it is, beside the project this directory names. */
|
|
80
|
+
export interface AttachedApp {
|
|
81
|
+
/** What the JSON line carries. Every value is printable, see `printable`. */
|
|
82
|
+
fields: {
|
|
83
|
+
/** The project the page reported when it connected. */
|
|
84
|
+
projectId?: string;
|
|
85
|
+
title?: string;
|
|
86
|
+
/** The `projectId` in this directory's `.reticle.json`, when there is one. */
|
|
87
|
+
expectedProjectId?: string;
|
|
88
|
+
};
|
|
89
|
+
/**
|
|
90
|
+
* The two project ids differ. Decided on the raw values, before either is made printable, so two
|
|
91
|
+
* ids that differ only in a control character still count as different.
|
|
92
|
+
*/
|
|
93
|
+
mismatch: boolean;
|
|
94
|
+
}
|
|
95
|
+
/** Nothing known about the app, as for a page that never connected. */
|
|
96
|
+
export declare const NO_ATTACHED_APP: AttachedApp;
|
|
97
|
+
/**
|
|
98
|
+
* Pick the attached session out of a `/status` payload. Pure.
|
|
99
|
+
*
|
|
100
|
+
* The drive answer is a lease result, which carries no project: the page reports that itself when
|
|
101
|
+
* it connects, and `/status` is where it is already listed.
|
|
102
|
+
*/
|
|
103
|
+
export declare function attachedApp(status: unknown, sessionId: string, expectedProjectId: string | undefined): AttachedApp;
|
|
79
104
|
/** The success line. Names the session, because that is the handle everything downstream needs. */
|
|
80
|
-
export declare function describeAttached(port: number, url: string, session: DriveSession): string;
|
|
105
|
+
export declare function describeAttached(port: number, url: string, session: DriveSession, app?: AttachedApp): string;
|
|
81
106
|
/**
|
|
82
107
|
* What an attach cannot honour: the window.
|
|
83
108
|
*
|
|
@@ -31,6 +31,7 @@
|
|
|
31
31
|
import * as http from 'node:http';
|
|
32
32
|
import { DRIVE_PATH, LOOPBACK_HOST } from '@reticlehq/core';
|
|
33
33
|
import { PortPresence } from '../../daemon/binding/port-presence.js';
|
|
34
|
+
import { summarizeStatus } from '../../daemon/binding/daemon-status-probe.js';
|
|
34
35
|
/** What `drive` should do about the port, given what is on it. */
|
|
35
36
|
export const DriveMode = {
|
|
36
37
|
/** Nothing is listening: bind it and run the foreground engine. */
|
|
@@ -134,18 +135,84 @@ export async function requestDriveSession(port, url, post = postJson) {
|
|
|
134
135
|
}
|
|
135
136
|
return readAttachResponse(port, url, res);
|
|
136
137
|
}
|
|
138
|
+
/** Nothing known about the app, as for a page that never connected. */
|
|
139
|
+
export const NO_ATTACHED_APP = { fields: {}, mismatch: false };
|
|
140
|
+
/**
|
|
141
|
+
* Control characters, and the 0x80 to 0x9f range some terminals also obey. Built with
|
|
142
|
+
* `fromCharCode` to keep them out of a regex literal (no-control-regex).
|
|
143
|
+
*/
|
|
144
|
+
const TERMINAL_CONTROLS = new RegExp(`[${String.fromCharCode(0)}-${String.fromCharCode(0x1f)}${String.fromCharCode(0x7f)}-${String.fromCharCode(0x9f)}]`, 'g');
|
|
145
|
+
/**
|
|
146
|
+
* A value safe to print. The project id and title come from the page, and the expected project id
|
|
147
|
+
* from a `.reticle.json` anyone can commit. All three are written straight to the terminal (and into
|
|
148
|
+
* the JSON line beside it), so they could otherwise move the cursor or retitle the window. Each
|
|
149
|
+
* control character becomes U+FFFD, so the reader can still see that something was there.
|
|
150
|
+
*/
|
|
151
|
+
function printable(value) {
|
|
152
|
+
return value.replace(TERMINAL_CONTROLS, '\uFFFD');
|
|
153
|
+
}
|
|
154
|
+
/**
|
|
155
|
+
* Pick the attached session out of a `/status` payload. Pure.
|
|
156
|
+
*
|
|
157
|
+
* The drive answer is a lease result, which carries no project: the page reports that itself when
|
|
158
|
+
* it connects, and `/status` is where it is already listed.
|
|
159
|
+
*/
|
|
160
|
+
export function attachedApp(status, sessionId, expectedProjectId) {
|
|
161
|
+
const row = summarizeStatus(status).sessions.find((s) => s.sessionId === sessionId);
|
|
162
|
+
return {
|
|
163
|
+
fields: {
|
|
164
|
+
...(row?.projectId === undefined ? {} : { projectId: printable(row.projectId) }),
|
|
165
|
+
...(row?.title === undefined ? {} : { title: printable(row.title) }),
|
|
166
|
+
...(expectedProjectId === undefined
|
|
167
|
+
? {}
|
|
168
|
+
: { expectedProjectId: printable(expectedProjectId) }),
|
|
169
|
+
},
|
|
170
|
+
// Only warned when both sides are known: a page with no project id proves nothing, and neither
|
|
171
|
+
// does a directory without a `.reticle.json`.
|
|
172
|
+
mismatch: row?.projectId !== undefined &&
|
|
173
|
+
expectedProjectId !== undefined &&
|
|
174
|
+
row.projectId !== expectedProjectId,
|
|
175
|
+
};
|
|
176
|
+
}
|
|
177
|
+
/** The fallback when the daemon sent no hint for a page that did not connect. */
|
|
178
|
+
const MSG_NOT_READY_HINT = 'Check that the app embeds @reticlehq/browser.';
|
|
179
|
+
/** How the attach line names the page's project. */
|
|
180
|
+
const PROJECT_LABEL = (projectId) => `project ${JSON.stringify(projectId)}`;
|
|
181
|
+
/** The page is a different project from the one this directory's `.reticle.json` names. */
|
|
182
|
+
const MSG_PROJECT_MISMATCH = (url, expected, actual) => `Warning: this directory's .reticle.json is ${PROJECT_LABEL(expected)}, but the page at ${url} ` +
|
|
183
|
+
`is ${PROJECT_LABEL(actual)}, so another app is probably serving that port. Check which dev ` +
|
|
184
|
+
'server is on it before trusting anything this session reports.';
|
|
185
|
+
/**
|
|
186
|
+
* The project and title, and a warning when this directory names a different project.
|
|
187
|
+
*
|
|
188
|
+
* In a monorepo the port you meant for one app is easily held by another, and the port, url and
|
|
189
|
+
* session id read the same either way.
|
|
190
|
+
*/
|
|
191
|
+
function describeApp(url, app) {
|
|
192
|
+
const { projectId, title, expectedProjectId } = app.fields;
|
|
193
|
+
const named = [
|
|
194
|
+
...(projectId === undefined ? [] : [PROJECT_LABEL(projectId)]),
|
|
195
|
+
...(title === undefined ? [] : [JSON.stringify(title)]),
|
|
196
|
+
];
|
|
197
|
+
const label = 0 === named.length ? '' : ` (${named.join(', ')})`;
|
|
198
|
+
const warning = app.mismatch && projectId !== undefined && expectedProjectId !== undefined
|
|
199
|
+
? ` ${MSG_PROJECT_MISMATCH(url, expectedProjectId, projectId)}`
|
|
200
|
+
: '';
|
|
201
|
+
return { label, warning };
|
|
202
|
+
}
|
|
137
203
|
/** The success line. Names the session, because that is the handle everything downstream needs. */
|
|
138
|
-
export function describeAttached(port, url, session) {
|
|
204
|
+
export function describeAttached(port, url, session, app = NO_ATTACHED_APP) {
|
|
139
205
|
const life = session.expiresInMs === undefined
|
|
140
206
|
? ''
|
|
141
207
|
: ` It is released after ${String(Math.round(session.expiresInMs / 1000))}s untouched; any ` +
|
|
142
208
|
'tool call against it resets that.';
|
|
143
209
|
if (!session.ready) {
|
|
144
210
|
return (`the Reticle daemon on :${String(port)} opened ${url} as session ${session.sessionId}, but ` +
|
|
145
|
-
`the page did not connect to the bridge. ${session.hint ??
|
|
211
|
+
`the page did not connect to the bridge. ${session.hint ?? MSG_NOT_READY_HINT}`);
|
|
146
212
|
}
|
|
147
|
-
|
|
148
|
-
|
|
213
|
+
const { label, warning } = describeApp(url, app);
|
|
214
|
+
return (`attached to the Reticle daemon on :${String(port)} — it is driving ${url}${label} as session ` +
|
|
215
|
+
`${session.sessionId}, which every Reticle tool can address.${life}${warning}`);
|
|
149
216
|
}
|
|
150
217
|
/**
|
|
151
218
|
* What an attach cannot honour: the window.
|
|
@@ -6,8 +6,9 @@
|
|
|
6
6
|
* place. The pure parts (which mode, and every sentence a user reads) live next door in
|
|
7
7
|
* drive-attach.ts, which is where the reasoning for attaching is written down.
|
|
8
8
|
*/
|
|
9
|
+
/** `projectId` is the one in this directory's `.reticle.json`, read by the caller. */
|
|
9
10
|
export declare function handleDrive(parsed: {
|
|
10
11
|
port: number;
|
|
11
12
|
driveUrl: string;
|
|
12
13
|
headless: boolean;
|
|
13
|
-
}): void;
|
|
14
|
+
}, projectId?: string): void;
|
|
@@ -13,9 +13,10 @@ import { probeDaemon } from '../../../surface/mcp/mcp-proxy.js';
|
|
|
13
13
|
import { readPid } from '../../daemon/daemon.js';
|
|
14
14
|
import { fetchStatus } from '../../daemon/binding/daemon-status-probe.js';
|
|
15
15
|
import { captureLookup, findPortHolder } from '../ports/port-holder.js';
|
|
16
|
-
import { DriveMode, decideDriveMode, describeAttached, driveForeignHolder, driveHeadlessOnAttach, driveRaceLost, requestDriveSession, } from './drive-attach.js';
|
|
17
|
-
|
|
18
|
-
|
|
16
|
+
import { DriveMode, NO_ATTACHED_APP, attachedApp, decideDriveMode, describeAttached, driveForeignHolder, driveHeadlessOnAttach, driveRaceLost, requestDriveSession, } from './drive-attach.js';
|
|
17
|
+
/** `projectId` is the one in this directory's `.reticle.json`, read by the caller. */
|
|
18
|
+
export function handleDrive(parsed, projectId) {
|
|
19
|
+
void driveWithHonestConflict(parsed, projectId);
|
|
19
20
|
}
|
|
20
21
|
/**
|
|
21
22
|
* Decide who gets the port BEFORE binding it — and when the answer is "the daemon", ask it instead.
|
|
@@ -29,7 +30,7 @@ export function handleDrive(parsed) {
|
|
|
29
30
|
* because the MCP proxy respawns a daemon into the gap. So a healthy daemon is now ATTACHED to —
|
|
30
31
|
* see cli/drive/drive-attach.ts.
|
|
31
32
|
*/
|
|
32
|
-
async function driveWithHonestConflict(parsed) {
|
|
33
|
+
async function driveWithHonestConflict(parsed, projectId) {
|
|
33
34
|
const presence = await probePresence(parsed.port, { tcpOpen: probeDaemon, status: fetchStatus });
|
|
34
35
|
const mode = decideDriveMode(presence);
|
|
35
36
|
if (DriveMode.REFUSE === mode) {
|
|
@@ -43,7 +44,7 @@ async function driveWithHonestConflict(parsed) {
|
|
|
43
44
|
return;
|
|
44
45
|
}
|
|
45
46
|
if (DriveMode.ATTACH === mode) {
|
|
46
|
-
await attachToRunningDaemon(parsed.port, parsed.driveUrl, parsed.headless);
|
|
47
|
+
await attachToRunningDaemon(parsed.port, parsed.driveUrl, parsed.headless, projectId);
|
|
47
48
|
return;
|
|
48
49
|
}
|
|
49
50
|
// The probe cannot close the race: something can take the port between here and the bind, and the
|
|
@@ -81,7 +82,7 @@ async function driveWithHonestConflict(parsed) {
|
|
|
81
82
|
* against it renews its lease), and a foreground hold here could only keep it alive by faking tool
|
|
82
83
|
* calls at it.
|
|
83
84
|
*/
|
|
84
|
-
async function attachToRunningDaemon(port, url, headless) {
|
|
85
|
+
async function attachToRunningDaemon(port, url, headless, projectId) {
|
|
85
86
|
const result = await requestDriveSession(port, url);
|
|
86
87
|
if (!result.ok) {
|
|
87
88
|
log('reticle_drive_attach_failed', { port, url, reason: result.reason });
|
|
@@ -89,13 +90,18 @@ async function attachToRunningDaemon(port, url, headless) {
|
|
|
89
90
|
process.exit(1);
|
|
90
91
|
return;
|
|
91
92
|
}
|
|
93
|
+
// Only a page that connected has reported its project; one that never dialled in is not listed.
|
|
94
|
+
const app = result.session.ready
|
|
95
|
+
? attachedApp(await fetchStatus(port), result.session.sessionId, projectId)
|
|
96
|
+
: NO_ATTACHED_APP;
|
|
92
97
|
log('reticle_drive_attached', {
|
|
93
98
|
port,
|
|
94
99
|
url,
|
|
95
100
|
sessionId: result.session.sessionId,
|
|
96
101
|
ready: result.session.ready,
|
|
102
|
+
...app.fields,
|
|
97
103
|
});
|
|
98
|
-
process.stderr.write(`${describeAttached(port, url, result.session)}\n`);
|
|
104
|
+
process.stderr.write(`${describeAttached(port, url, result.session, app)}\n`);
|
|
99
105
|
// The command's own default is headed, and this path cannot deliver that. Said out loud rather
|
|
100
106
|
// than left for the user to discover by watching a window that never opens.
|
|
101
107
|
if (!headless)
|
|
@@ -16,7 +16,7 @@
|
|
|
16
16
|
*/
|
|
17
17
|
import { log } from '../../../log.js';
|
|
18
18
|
import { RETICLE_VERIFY_DEFAULT_PORT } from '../../../index.js';
|
|
19
|
-
import { verifyEndpointMismatch } from '../../../status-payload.js';
|
|
19
|
+
import { strandedClientsNote, verifyEndpointMismatch } from '../../../status-payload.js';
|
|
20
20
|
import { probeDaemon, waitForDaemonBind } from '../../../surface/mcp/mcp-proxy.js';
|
|
21
21
|
import { stateDirProblem } from '../../daemon/state-dir.js';
|
|
22
22
|
import { readPid, isAlive, removePid, spawnDaemon, logPath, reticleStateHome, } from '../../daemon/daemon.js';
|
|
@@ -168,6 +168,9 @@ export function handleStop(port, quiet) {
|
|
|
168
168
|
* anything at all, and a single `ok: false` cannot tell them which happened.
|
|
169
169
|
*/
|
|
170
170
|
export async function handleRestart(port, force) {
|
|
171
|
+
const stranded = strandedClientsNote(await fetchStatus(port));
|
|
172
|
+
if (stranded !== undefined)
|
|
173
|
+
process.stderr.write(`${stranded}\n`);
|
|
171
174
|
const freed = await runKill(port, force);
|
|
172
175
|
if (!freed) {
|
|
173
176
|
log('reticle_restart_aborted', {
|
|
@@ -51,3 +51,13 @@ export declare function describeForeignHolder(port: number, holder: PortHolder |
|
|
|
51
51
|
ourPid?: number | null): string;
|
|
52
52
|
/** The lookup itself. `exec` is injected so the shelling out never runs in a unit test. */
|
|
53
53
|
export declare function findPortHolder(port: number, exec: (command: string, args: readonly string[]) => string | null): PortHolder | null;
|
|
54
|
+
/** The path in `lsof -a -p <pid> -d cwd -Fn` output, or null when it printed none. */
|
|
55
|
+
export declare function parseProcessCwd(stdout: string): string | null;
|
|
56
|
+
/**
|
|
57
|
+
* The working directory of whatever LISTENS on `port`, or null.
|
|
58
|
+
*
|
|
59
|
+
* How the daemon tells which checkout served a page when every git worktree declares the same
|
|
60
|
+
* projectId: the dev server runs inside exactly one of them. Null wherever `lsof` cannot answer —
|
|
61
|
+
* Windows, a slim container — and the caller then keeps the answer it had without this.
|
|
62
|
+
*/
|
|
63
|
+
export declare function findListenerCwd(port: number, exec: (command: string, args: readonly string[]) => string | null): string | null;
|
|
@@ -121,3 +121,27 @@ export function findPortHolder(port, exec) {
|
|
|
121
121
|
const out = exec('lsof', ['-nP', `-iTCP:${String(port)}`, '-sTCP:LISTEN', '-F', 'pc']);
|
|
122
122
|
return null === out ? null : parsePortHolder(out);
|
|
123
123
|
}
|
|
124
|
+
/** `lsof -Fn` emits the file name on its own line, prefixed `n`. */
|
|
125
|
+
const NAME_FIELD = 'n';
|
|
126
|
+
/** The path in `lsof -a -p <pid> -d cwd -Fn` output, or null when it printed none. */
|
|
127
|
+
export function parseProcessCwd(stdout) {
|
|
128
|
+
for (const raw of stdout.split('\n')) {
|
|
129
|
+
if (raw.startsWith(NAME_FIELD) && raw.length > NAME_FIELD.length)
|
|
130
|
+
return raw.slice(1);
|
|
131
|
+
}
|
|
132
|
+
return null;
|
|
133
|
+
}
|
|
134
|
+
/**
|
|
135
|
+
* The working directory of whatever LISTENS on `port`, or null.
|
|
136
|
+
*
|
|
137
|
+
* How the daemon tells which checkout served a page when every git worktree declares the same
|
|
138
|
+
* projectId: the dev server runs inside exactly one of them. Null wherever `lsof` cannot answer —
|
|
139
|
+
* Windows, a slim container — and the caller then keeps the answer it had without this.
|
|
140
|
+
*/
|
|
141
|
+
export function findListenerCwd(port, exec) {
|
|
142
|
+
const holder = findPortHolder(port, exec);
|
|
143
|
+
if (null === holder)
|
|
144
|
+
return null;
|
|
145
|
+
const out = exec('lsof', ['-a', '-p', String(holder.pid), '-d', 'cwd', '-Fn']);
|
|
146
|
+
return null === out ? null : parseProcessCwd(out);
|
|
147
|
+
}
|
|
@@ -35,14 +35,16 @@ export function handleSetupMcp(reportStep, standalone = true) {
|
|
|
35
35
|
const result = setupMcp(io);
|
|
36
36
|
if (0 === result.detected.length) {
|
|
37
37
|
// Even with no file-backed client, the broader set may still have somewhere to write.
|
|
38
|
-
registerOtherAgents((line) => {
|
|
38
|
+
const preRegistered = registerOtherAgents((line) => {
|
|
39
39
|
io.print(line);
|
|
40
40
|
});
|
|
41
41
|
// Named rather than a bare "none found". A person whose agent IS installed needs to know
|
|
42
42
|
// whether we looked for it at all, and the list is the difference between "we did not find
|
|
43
|
-
// yours" and "we do not support yours".
|
|
44
|
-
|
|
45
|
-
io.print(
|
|
43
|
+
// yours" and "we do not support yours". When the broader set was written, saying "none found"
|
|
44
|
+
// right under its tick read as a contradiction: those agents are pre-registered, not found.
|
|
45
|
+
io.print(0 < preRegistered
|
|
46
|
+
? `None of ${knownClientLabels().join(', ')} is installed here yet. The agents above are pre-registered and have Reticle's tools once installed; install one of the others, then run \`reticle setup mcp\` again.`
|
|
47
|
+
: `No coding agent config found. Looked for: ${knownClientLabels().join(', ')}. Install one, then run \`reticle setup mcp\` again.`);
|
|
46
48
|
return;
|
|
47
49
|
}
|
|
48
50
|
for (const id of result.registered)
|
|
@@ -84,8 +84,8 @@ export declare function tutorialNextSteps(audience: TutorialAudience, restartAge
|
|
|
84
84
|
* on `process.stdout.isTTY` does not work: that is `undefined` through every pipe, so the tour never
|
|
85
85
|
* prints. A heuristic that silently answers "nobody is watching" is worse than no heuristic.
|
|
86
86
|
*
|
|
87
|
-
* It ends with the tour's OWN closing
|
|
88
|
-
*
|
|
87
|
+
* It ends with the tour's OWN closing. Installing changes no project: the app is wired the first time
|
|
88
|
+
* an agent uses Reticle in it (see first-run-wiring.ts).
|
|
89
89
|
*/
|
|
90
90
|
export declare function installClosing(registered?: boolean): string;
|
|
91
91
|
export declare function renderTutorial(audience: TutorialAudience): string;
|
|
@@ -95,8 +95,8 @@ export function tutorialNextSteps(audience, restartAgent = false) {
|
|
|
95
95
|
const lines = TutorialAudience.AGENT === audience
|
|
96
96
|
? [
|
|
97
97
|
'Next:',
|
|
98
|
-
'
|
|
99
|
-
'
|
|
98
|
+
' reticle_session list # the first call wires this project: Reticle runs init itself',
|
|
99
|
+
' # and names every file it changed — tell the user which',
|
|
100
100
|
' then run the four steps above against it, and report the verdict',
|
|
101
101
|
]
|
|
102
102
|
: [
|
|
@@ -104,12 +104,9 @@ export function tutorialNextSteps(audience, restartAgent = false) {
|
|
|
104
104
|
...(restartAgent
|
|
105
105
|
? [' restart your agent # it loads new tools only when it starts']
|
|
106
106
|
: []),
|
|
107
|
-
' cd <your project> && reticle init # wire Reticle into the app',
|
|
108
|
-
' npm run dev # then load it in a browser',
|
|
109
|
-
' reticle status # confirms the app connected, or says why not',
|
|
110
107
|
'',
|
|
111
108
|
// One line: the terminal wraps it at the reader's width, not ours.
|
|
112
|
-
'
|
|
109
|
+
'Ask your agent to verify one real flow in your project. Nothing in your app changed at install: the first time the agent uses Reticle there, Reticle wires the app, starts it, and tells you which files it changed.',
|
|
113
110
|
];
|
|
114
111
|
return lines.join('\n');
|
|
115
112
|
}
|
|
@@ -120,8 +117,8 @@ export function tutorialNextSteps(audience, restartAgent = false) {
|
|
|
120
117
|
* on `process.stdout.isTTY` does not work: that is `undefined` through every pipe, so the tour never
|
|
121
118
|
* prints. A heuristic that silently answers "nobody is watching" is worse than no heuristic.
|
|
122
119
|
*
|
|
123
|
-
* It ends with the tour's OWN closing
|
|
124
|
-
*
|
|
120
|
+
* It ends with the tour's OWN closing. Installing changes no project: the app is wired the first time
|
|
121
|
+
* an agent uses Reticle in it (see first-run-wiring.ts).
|
|
125
122
|
*/
|
|
126
123
|
export function installClosing(registered = true) {
|
|
127
124
|
// One line a step. The reasons are for `reticle tutorial`; right after an install they turned a
|
|
@@ -140,7 +137,7 @@ export function installClosing(registered = true) {
|
|
|
140
137
|
* Reticle" points at a panel on screen; straight after an install there is none to point at.
|
|
141
138
|
*/
|
|
142
139
|
const INSTALL_SAYS = {
|
|
143
|
-
connect: '
|
|
140
|
+
connect: 'The first time your agent uses Reticle in a project, it wires the app and a Reticle panel appears in it: your proof the SDK is connected.',
|
|
144
141
|
};
|
|
145
142
|
export function renderTutorial(audience) {
|
|
146
143
|
const steps = tutorialScript(audience)
|
package/dist/command/cli.d.ts
CHANGED
|
@@ -2,5 +2,11 @@
|
|
|
2
2
|
import { parseCliArgs, CLI_USAGE } from './cli/cli-parse.js';
|
|
3
3
|
export { parseCliArgs, CLI_USAGE };
|
|
4
4
|
export type { CliResult } from './cli/cli-parse.js';
|
|
5
|
+
/**
|
|
6
|
+
* Report status to the terminal. `--json` writes one structured line to STDOUT so it can be piped
|
|
7
|
+
* into `jq`; the human-readable block goes to stdout too. `log()` stays on stderr (the MCP
|
|
8
|
+
* transport lives on stdout), so `--json` must not go through it.
|
|
9
|
+
*/
|
|
10
|
+
export declare function reportStatus(fields: Record<string, unknown>, json: boolean): void;
|
|
5
11
|
export declare function handleStatus(port: number, json?: boolean): Promise<void>;
|
|
6
12
|
export declare function main(): void;
|