@reticlehq/server 3.3.0 → 3.5.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/LICENSE-ENTERPRISE +1 -1
- package/README.md +3 -3
- package/SKILL.md +12 -14
- package/dist/chromium-launch-options.d.ts +4 -3
- package/dist/chromium-launch-options.js +6 -2
- package/dist/command/cli/adhoc-suite.d.ts +22 -0
- package/dist/command/cli/adhoc-suite.js +90 -19
- package/dist/command/cli/adhoc-verdict.d.ts +35 -0
- package/dist/command/cli/adhoc-verdict.js +90 -3
- package/dist/command/cli/answers/gate-hook-message.d.ts +6 -0
- package/dist/command/cli/answers/gate-hook-message.js +6 -0
- package/dist/command/cli/auth/cloud-keystore.d.ts +0 -2
- package/dist/command/cli/auth/cloud-keystore.js +0 -3
- package/dist/command/cli/auth/cloud-session.d.ts +6 -2
- package/dist/command/cli/auth/cloud-session.js +2 -0
- package/dist/command/cli/auth/device-flow.d.ts +2 -2
- package/dist/command/cli/cli-doctor.js +7 -5
- package/dist/command/cli/cli-flow-commands.d.ts +3 -6
- package/dist/command/cli/cli-flow-commands.js +67 -4
- package/dist/command/cli/cli-parse.d.ts +8 -1
- package/dist/command/cli/cli-parse.js +24 -4
- package/dist/command/cli/cli-update-commands.js +8 -1
- package/dist/command/cli/cli-verify.d.ts +24 -3
- package/dist/command/cli/cli-verify.js +54 -11
- package/dist/command/cli/cloud-cli.js +64 -2
- package/dist/command/cli/cloud-kit.js +3 -5
- package/dist/command/cli/cloud-login.d.ts +1 -1
- package/dist/command/cli/cloud-login.js +27 -8
- package/dist/command/cli/demo/demo-page.js +6 -3
- package/dist/command/cli/doctor/browser/chromium-hint.d.ts +5 -0
- package/dist/command/cli/doctor/browser/chromium-hint.js +5 -0
- package/dist/command/cli/launch/cli-launch.d.ts +8 -0
- package/dist/command/cli/launch/cli-launch.js +21 -9
- package/dist/command/cli/launch/sdk-version-match.d.ts +84 -0
- package/dist/command/cli/launch/sdk-version-match.js +206 -0
- package/dist/command/cli/lifecycle/daemon-lifecycle.js +2 -16
- package/dist/command/cli/ports/resolve/cli-port.d.ts +25 -0
- package/dist/command/cli/ports/resolve/cli-port.js +125 -15
- package/dist/command/cli/setup-mcp-cli.js +21 -13
- package/dist/command/cli/tutorial.d.ts +2 -2
- package/dist/command/cli/tutorial.js +21 -7
- package/dist/command/cli.js +79 -11
- package/dist/command/daemon/binding/daemon-status-probe.d.ts +14 -2
- package/dist/command/daemon/binding/daemon-status-probe.js +34 -6
- package/dist/command/daemon/daemon-resolve.d.ts +9 -0
- package/dist/command/daemon/daemon-resolve.js +1 -1
- package/dist/command/daemon/lifetime/daemon-usefulness.d.ts +7 -1
- package/dist/command/daemon/lifetime/daemon-usefulness.js +30 -7
- package/dist/command/dev/stale-issue-guard.d.ts +3 -4
- package/dist/command/dev/stale-issue-guard.js +8 -8
- package/dist/command/setup/bringup/connect-lease.d.ts +17 -0
- package/dist/command/setup/bringup/connect-lease.js +38 -0
- package/dist/command/setup/bringup/dev-server-files.d.ts +68 -0
- package/dist/command/setup/bringup/dev-server-files.js +131 -0
- package/dist/command/setup/bringup/dev-server-log-cap.d.ts +35 -0
- package/dist/command/setup/bringup/dev-server-log-cap.js +121 -0
- package/dist/command/setup/bringup/dev-server-wait.d.ts +2 -0
- package/dist/command/setup/bringup/dev-server-wait.js +1 -1
- package/dist/command/setup/bringup/ensure-daemon.d.ts +65 -4
- package/dist/command/setup/bringup/ensure-daemon.js +135 -16
- package/dist/command/setup/bringup/static-page-server.d.ts +35 -0
- package/dist/command/setup/bringup/static-page-server.js +151 -0
- package/dist/command/setup/desktop-shape.d.ts +4 -0
- package/dist/command/setup/desktop-shape.js +18 -2
- package/dist/command/setup/init/init-port.d.ts +15 -0
- package/dist/command/setup/init/init-port.js +44 -0
- package/dist/command/setup/init/init-runtime.d.ts +1 -1
- package/dist/command/setup/init/init-runtime.js +91 -9
- package/dist/command/setup/node-effects.d.ts +27 -14
- package/dist/command/setup/node-effects.js +86 -45
- package/dist/command/setup/probe/loopback-probe-urls.d.ts +7 -0
- package/dist/command/setup/probe/loopback-probe-urls.js +40 -0
- package/dist/command/setup/probe/page-probe.d.ts +13 -2
- package/dist/command/setup/probe/page-probe.js +18 -3
- package/dist/command/setup/run-setup.d.ts +28 -1
- package/dist/command/setup/run-setup.js +103 -15
- package/dist/command/setup/session-pick.d.ts +13 -0
- package/dist/command/setup/session-pick.js +20 -0
- package/dist/command/setup/setup-command.d.ts +2 -14
- package/dist/command/setup/setup-command.js +105 -31
- package/dist/command/setup/setup-mcp.d.ts +2 -0
- package/dist/command/setup/setup-mcp.js +17 -8
- package/dist/command/setup/terminal/wait-progress.d.ts +8 -0
- package/dist/command/setup/terminal/wait-progress.js +17 -0
- package/dist/command/version/peer-announce.d.ts +6 -2
- package/dist/command/version/peer-announce.js +8 -6
- package/dist/command/version/version-nudge.d.ts +17 -3
- package/dist/command/version/version-nudge.js +25 -4
- package/dist/features/crawl/crawl-empty.js +1 -1
- package/dist/features/crawl/crawl-tools.d.ts +6 -0
- package/dist/features/crawl/crawl-tools.js +57 -0
- package/dist/features/crawl/crawl.js +171 -157
- package/dist/features/exhaust/app-coverage.d.ts +33 -0
- package/dist/features/exhaust/app-coverage.js +31 -0
- package/dist/features/exhaust/code-coverage.d.ts +47 -0
- package/dist/features/exhaust/code-coverage.js +77 -0
- package/dist/features/exhaust/exhaust-run.d.ts +33 -0
- package/dist/features/exhaust/exhaust-run.js +82 -0
- package/dist/features/exhaust/explorer.d.ts +103 -0
- package/dist/features/exhaust/explorer.js +195 -0
- package/dist/features/exhaust/failure-branches.d.ts +33 -0
- package/dist/features/exhaust/failure-branches.js +92 -0
- package/dist/features/exhaust/ledger.d.ts +202 -0
- package/dist/features/exhaust/ledger.js +252 -0
- package/dist/features/exhaust/session-fold.d.ts +31 -0
- package/dist/features/exhaust/session-fold.js +79 -0
- package/dist/features/exhaust/session-port.d.ts +36 -0
- package/dist/features/exhaust/session-port.js +102 -0
- package/dist/features/harness/drive-report.d.ts +0 -9
- package/dist/features/harness/drive-report.js +18 -0
- package/dist/features/harness/driver.js +4 -7
- package/dist/features/harness/fill-values.d.ts +10 -0
- package/dist/features/harness/fill-values.js +50 -11
- package/dist/features/harness/jev-driver.js +30 -13
- package/dist/features/harness/openai-driver.d.ts +12 -0
- package/dist/features/harness/openai-driver.js +21 -12
- package/dist/features/harness/platform-config.js +5 -6
- package/dist/features/license/license.d.ts +2 -2
- package/dist/index.d.ts +9 -0
- package/dist/index.js +56 -19
- package/dist/judgement/capsule/capsule-store.d.ts +4 -4
- package/dist/judgement/domain/domain-model.js +3 -1
- package/dist/judgement/oracles/self-instrument.js +4 -2
- package/dist/judgement/runs/artifact/replay-mapping.d.ts +2 -2
- package/dist/judgement/runs/artifact/replay-mapping.js +44 -9
- package/dist/judgement/runs/verification-sync.d.ts +3 -1
- package/dist/judgement/runs/verification-sync.js +1 -1
- package/dist/language/flows/change/affected.d.ts +9 -0
- package/dist/language/flows/change/affected.js +35 -0
- package/dist/language/flows/change/gate.d.ts +20 -0
- package/dist/language/flows/change/gate.js +12 -1
- package/dist/language/flows/flow-author.d.ts +7 -0
- package/dist/language/flows/flow-author.js +19 -0
- package/dist/language/flows/flow-expect-grammar.js +1 -1
- package/dist/language/flows/flow-journey.d.ts +54 -0
- package/dist/language/flows/flow-journey.js +119 -0
- package/dist/language/flows/flow-replay-run.d.ts +2 -3
- package/dist/language/flows/flow-replay-run.js +13 -47
- package/dist/language/flows/flow-replay.d.ts +22 -8
- package/dist/language/flows/flow-replay.js +55 -9
- package/dist/language/flows/flow-result.d.ts +2 -0
- package/dist/language/flows/flow-result.js +25 -0
- package/dist/language/flows/flow-tools.js +17 -5
- package/dist/language/flows/flows.d.ts +10 -1
- package/dist/language/flows/flows.js +90 -14
- package/dist/language/flows/recording/replay-halt.js +3 -1
- package/dist/language/flows/recording/tape/recordings.d.ts +30 -1
- package/dist/language/flows/recording/tape/recordings.js +55 -1
- package/dist/language/flows/replay.d.ts +19 -0
- package/dist/language/flows/replay.js +42 -6
- package/dist/language/flows/verify-change-tools.js +5 -2
- package/dist/launch-chromium.d.ts +80 -0
- package/dist/launch-chromium.js +218 -0
- package/dist/memory/cloud/cloud-config.d.ts +11 -2
- package/dist/memory/cloud/cloud-config.js +47 -7
- package/dist/memory/cloud/cloud-sync.d.ts +4 -1
- package/dist/memory/cloud/cloud-sync.js +6 -9
- package/dist/memory/cloud/harness-offer.js +5 -6
- package/dist/memory/cloud/harness-switch.js +5 -6
- package/dist/memory/cloud/sync-cycle.d.ts +77 -3
- package/dist/memory/cloud/sync-cycle.js +325 -55
- package/dist/memory/cloud/sync-daemon.js +24 -20
- package/dist/memory/cloud/sync-disk.js +16 -2
- package/dist/memory/intent/intent-shard.d.ts +10 -32
- package/dist/memory/intent/intent-shard.js +0 -10
- package/dist/memory/intent/intent-tools.js +8 -3
- package/dist/memory/journal/drive-flow.d.ts +18 -13
- package/dist/memory/journal/drive-flow.js +45 -37
- package/dist/memory/journal/envelope.d.ts +8 -8
- package/dist/memory/journal/journal-query.d.ts +14 -0
- package/dist/memory/journal/journal-query.js +15 -0
- package/dist/memory/journal/on-disk/workspace-tiers.js +2 -0
- package/dist/memory/journal/session-end.d.ts +9 -1
- package/dist/memory/journal/session-end.js +24 -12
- package/dist/memory/project/artifact-root-resolver.d.ts +10 -0
- package/dist/memory/project/artifact-root-resolver.js +46 -19
- package/dist/memory/project/dir/reticle-dir.d.ts +2 -0
- package/dist/memory/project/dir/reticle-dir.js +1 -0
- package/dist/memory/project/project-tools.js +3 -1
- package/dist/portal/bridge/auth-failure-reason.d.ts +1 -1
- package/dist/portal/bridge/auth-failure-reason.js +4 -2
- package/dist/portal/bridge/bridge.d.ts +12 -3
- package/dist/portal/bridge/bridge.js +49 -3
- package/dist/portal/input/js-coverage.d.ts +25 -0
- package/dist/portal/input/js-coverage.js +30 -0
- package/dist/portal/input/real-input.d.ts +11 -0
- package/dist/portal/input/real-input.js +29 -4
- package/dist/portal/input/viewport-tools.js +16 -6
- package/dist/portal/pool/playwright-launcher.d.ts +6 -0
- package/dist/portal/pool/playwright-launcher.js +20 -13
- package/dist/portal/session/dev-server/dev-command.d.ts +2 -2
- package/dist/portal/session/dev-server/dev-command.js +30 -12
- package/dist/portal/session/dev-server/dev-server-probe.js +17 -1
- package/dist/portal/session/dev-server/route-status-probe.d.ts +24 -0
- package/dist/portal/session/dev-server/route-status-probe.js +57 -0
- package/dist/portal/session/facts/observed-state.d.ts +11 -0
- package/dist/portal/session/facts/observed-state.js +34 -0
- package/dist/portal/session/no-session-diagnosis.d.ts +29 -0
- package/dist/portal/session/no-session-diagnosis.js +62 -5
- package/dist/portal/session/no-session-next-action.d.ts +13 -0
- package/dist/portal/session/no-session-next-action.js +48 -9
- package/dist/portal/session/no-session-watch.d.ts +18 -0
- package/dist/portal/session/no-session-watch.js +88 -2
- package/dist/portal/session/session-health.js +3 -2
- package/dist/portal/session/session-manager.d.ts +20 -6
- package/dist/portal/session/session-manager.js +19 -3
- package/dist/portal/session/session-reaper.d.ts +28 -0
- package/dist/portal/session/session-reaper.js +49 -1
- package/dist/portal/session/session.d.ts +10 -8
- package/dist/portal/session/session.js +24 -32
- package/dist/surface/http-server.d.ts +3 -2
- package/dist/surface/http-server.js +8 -5
- package/dist/surface/mcp/mcp-proxy.d.ts +2 -2
- package/dist/surface/mcp/mcp-proxy.js +2 -2
- package/dist/surface/mcp/mcp.js +5 -3
- package/dist/surface/mcp/proxy/proxy-daemon-probe.d.ts +13 -1
- package/dist/surface/mcp/proxy/proxy-daemon-probe.js +27 -2
- package/dist/surface/mcp/proxy/proxy-log.js +3 -5
- package/dist/surface/mcp/server-instructions.js +9 -7
- package/dist/surface/tools/act/after-match.d.ts +17 -0
- package/dist/surface/tools/act/after-match.js +30 -0
- package/dist/surface/tools/act/durable.d.ts +69 -0
- package/dist/surface/tools/act/durable.js +139 -0
- package/dist/surface/tools/act/navigation/navigate-arrival.d.ts +13 -6
- package/dist/surface/tools/act/navigation/navigate-arrival.js +47 -10
- package/dist/surface/tools/act/navigation/navigate-result.d.ts +1 -12
- package/dist/surface/tools/act/navigation/navigate-result.js +21 -3
- package/dist/surface/tools/act/proved-controls.d.ts +9 -0
- package/dist/surface/tools/act/proved-controls.js +11 -0
- package/dist/surface/tools/act/sequence-grade.d.ts +5 -0
- package/dist/surface/tools/act/sequence-grade.js +32 -1
- package/dist/surface/tools/act-capsule.d.ts +7 -0
- package/dist/surface/tools/act-capsule.js +46 -1
- package/dist/surface/tools/act-sequence-tool.js +25 -8
- package/dist/surface/tools/act-tools.js +28 -52
- package/dist/surface/tools/assert/assert-grade.js +12 -1
- package/dist/surface/tools/assert/assert-verdict.js +2 -2
- package/dist/surface/tools/browser-tools.js +14 -2
- package/dist/surface/tools/coverage-tools.js +27 -1
- package/dist/surface/tools/dynamic-tools.js +29 -2
- package/dist/surface/tools/error-recovery.js +5 -6
- package/dist/surface/tools/feedback-invite.js +6 -4
- package/dist/surface/tools/harness-explore.d.ts +3 -2
- package/dist/surface/tools/harness-explore.js +12 -7
- package/dist/surface/tools/invoke-tool.js +12 -8
- package/dist/surface/tools/lease-tools.js +15 -6
- package/dist/surface/tools/live-call-text.js +4 -1
- package/dist/surface/tools/observe-tools.js +9 -1
- package/dist/surface/tools/tool-kit.d.ts +10 -0
- package/dist/surface/tools/tools.js +4 -0
- package/dist/telemetry/argument-shape.d.ts +0 -2
- package/dist/telemetry/argument-shape.js +1 -5
- package/dist/telemetry/error-class.d.ts +2 -3
- package/dist/telemetry/error-class.js +2 -3
- package/dist/telemetry/harness-drive.d.ts +0 -2
- package/dist/telemetry/harness-drive.js +0 -4
- package/dist/telemetry/onboarding-firsts.d.ts +2 -2
- package/dist/telemetry/onboarding-firsts.js +2 -2
- package/dist/wire-journal.d.ts +3 -0
- package/dist/wire-journal.js +4 -0
- package/dist/workspace-packages.d.ts +0 -2
- package/dist/workspace-packages.js +1 -9
- package/docs/README.md +4 -2
- package/docs/actions.mdx +3 -4
- package/docs/agent-cheatsheet.md +4 -4
- package/docs/architecture.md +6 -6
- package/docs/best-practices.mdx +1 -1
- package/docs/capabilities.mdx +32 -1
- package/docs/cli/cloud.mdx +20 -8
- package/docs/cli/doctor.mdx +4 -0
- package/docs/cli/feedback.mdx +2 -2
- package/docs/cli/gate.mdx +9 -4
- package/docs/cli/update.mdx +10 -0
- package/docs/cli.mdx +22 -7
- package/docs/desktop.mdx +14 -10
- package/docs/environment.mdx +87 -0
- package/docs/faq.mdx +18 -16
- package/docs/first-drive.md +5 -13
- package/docs/flows.md +21 -2
- package/docs/frameworks.mdx +29 -10
- package/docs/getting-started.md +15 -5
- package/docs/http-transport.md +6 -3
- package/docs/human-control.md +2 -2
- package/docs/index.mdx +16 -6
- package/docs/install-agentic.mdx +2 -2
- package/docs/install-manual.mdx +1 -1
- package/docs/integration-patterns.md +3 -3
- package/docs/multi-agent-testing.md +1 -1
- package/docs/packages/babel-plugin.mdx +1 -1
- package/docs/packages/browser.mdx +2 -2
- package/docs/packages/next.mdx +3 -0
- package/docs/packages/react.mdx +2 -2
- package/docs/packages/tauri.mdx +1 -1
- package/docs/packages/vite-plugin.mdx +4 -2
- package/docs/packages.mdx +9 -9
- package/docs/predicates.mdx +50 -5
- package/docs/quickstart.mdx +12 -10
- package/docs/recipes.mdx +2 -2
- package/docs/state-management.mdx +2 -2
- package/docs/system-map.md +1 -1
- package/docs/telemetry.md +1 -1
- package/docs/token-efficiency.md +3 -3
- package/docs/tools/act-and-wait.mdx +1 -0
- package/docs/tools/act-sequence.mdx +1 -1
- package/docs/tools/act.mdx +1 -1
- package/docs/tools/console.mdx +1 -1
- package/docs/tools/inspect.mdx +1 -1
- package/docs/tools/network.mdx +1 -1
- package/docs/tools/query.mdx +1 -1
- package/docs/tools/sessions.mdx +1 -1
- package/docs/tools/snapshot.mdx +1 -1
- package/docs/tools/state.mdx +1 -1
- package/docs/tools/wait-for.mdx +1 -1
- package/docs/usage.md +7 -7
- package/docs/use-cases.mdx +157 -0
- package/docs/vs-screenshots.mdx +3 -3
- package/docs/what-is-recorded.md +2 -2
- package/docs/what-is-reticle.mdx +17 -20
- package/docs/why-reticle.mdx +14 -21
- package/package.json +10 -9
- package/dist/judgement/capsule/minimize.d.ts +0 -12
- package/dist/judgement/capsule/minimize.js +0 -22
- package/dist/judgement/phenomena/phenomena.d.ts +0 -27
- package/dist/judgement/phenomena/phenomena.js +0 -101
- package/dist/language/flows/flow-report.d.ts +0 -22
- package/dist/language/flows/flow-report.js +0 -106
|
@@ -14,7 +14,7 @@
|
|
|
14
14
|
* and a real browser to find out what happens when neither works.
|
|
15
15
|
*/
|
|
16
16
|
import { judgeWait, portBusyMessage, QUIET_MEANS_HUNG_MS, urlToWatch, WaitVerdict, } from './bringup/dev-server-wait.js';
|
|
17
|
-
import { waitProgressLine } from './terminal/wait-progress.js';
|
|
17
|
+
import { connectProgressLine, waitProgressLine } from './terminal/wait-progress.js';
|
|
18
18
|
/**
|
|
19
19
|
* Windows gets longer to say something before silence counts as a wedge.
|
|
20
20
|
*
|
|
@@ -28,7 +28,7 @@ import { waitProgressLine } from './terminal/wait-progress.js';
|
|
|
28
28
|
*/
|
|
29
29
|
const WINDOWS_QUIET_MEANS_HUNG_MS = 3 * 60_000;
|
|
30
30
|
const WINDOWS_QUIET_MEANS_HUNG_MS_APPLIES = 'win32' === process.platform;
|
|
31
|
-
import { pickSession } from './session-pick.js';
|
|
31
|
+
import { liveSessionOfProject, pickSession } from './session-pick.js';
|
|
32
32
|
import { readPage, describePage, findingBeforeOpen, PageFinding, } from './probe/page-probe.js';
|
|
33
33
|
import { remainingSteps } from './remaining-steps.js';
|
|
34
34
|
import { AppShape, isDesktop, policyFor } from './desktop-shape.js';
|
|
@@ -40,11 +40,27 @@ import { AppShape, isDesktop, policyFor } from './desktop-shape.js';
|
|
|
40
40
|
const SDK_READY_WINDOW_MS = 15_000;
|
|
41
41
|
/** The wait left when no browser was opened: only a tab that is ALREADY loaded can still appear. */
|
|
42
42
|
const NO_BROWSER_GRACE_MS = 3_000;
|
|
43
|
+
/**
|
|
44
|
+
* How long a system browser that SAID it opened gets to produce a session before init proves the
|
|
45
|
+
* connect in a Reticle-owned browser instead. A launcher's exit is not a window: macOS `open` against
|
|
46
|
+
* a default browser that does not answer exits 1 only after the launch check has already counted it
|
|
47
|
+
* as opened, and init then waited out its whole connect budget over a correct install.
|
|
48
|
+
*/
|
|
49
|
+
const SYSTEM_BROWSER_GRACE_MS = 15_000;
|
|
43
50
|
export const SetupPhase = {
|
|
44
51
|
DEV_SERVER: 'dev-server',
|
|
45
52
|
CONNECT: 'connect',
|
|
46
53
|
DONE: 'done',
|
|
47
54
|
};
|
|
55
|
+
/** Hand a lease back. Best-effort: a lease that already expired is already gone. */
|
|
56
|
+
async function releaseLease(lease) {
|
|
57
|
+
try {
|
|
58
|
+
await lease?.release();
|
|
59
|
+
}
|
|
60
|
+
catch {
|
|
61
|
+
/* nothing to undo */
|
|
62
|
+
}
|
|
63
|
+
}
|
|
48
64
|
const asProgress = (input, o) => ({
|
|
49
65
|
initDone: true,
|
|
50
66
|
devServerUp: undefined !== o.url,
|
|
@@ -89,6 +105,10 @@ async function daemonWhy(fx) {
|
|
|
89
105
|
export async function runSetupPhases(input, fx) {
|
|
90
106
|
const notes = [];
|
|
91
107
|
const note = (line) => {
|
|
108
|
+
// The page is probed before the browser opens and again after the wait, and an unchanged page
|
|
109
|
+
// gave the reader the same paragraph twice in a row.
|
|
110
|
+
if (notes.at(-1) === line)
|
|
111
|
+
return;
|
|
92
112
|
notes.push(line);
|
|
93
113
|
fx.note(line);
|
|
94
114
|
};
|
|
@@ -139,6 +159,7 @@ export async function runSetupPhases(input, fx) {
|
|
|
139
159
|
serving,
|
|
140
160
|
quietForMs: fx.devServerQuietForMs(),
|
|
141
161
|
elapsedMs: fx.now() - startedAt,
|
|
162
|
+
budgetMs: input.startupBudgetMs,
|
|
142
163
|
quietMeansHungMs: WINDOWS_QUIET_MEANS_HUNG_MS_APPLIES
|
|
143
164
|
? WINDOWS_QUIET_MEANS_HUNG_MS
|
|
144
165
|
: QUIET_MEANS_HUNG_MS,
|
|
@@ -164,8 +185,12 @@ export async function runSetupPhases(input, fx) {
|
|
|
164
185
|
// Says BOTH were checked. A server that prints nothing but IS listening is the CRA case and
|
|
165
186
|
// must not be failed, so a reader has to be able to tell "we looked at the log" from "we
|
|
166
187
|
// looked at the log AND the ports".
|
|
167
|
-
note(
|
|
168
|
-
|
|
188
|
+
note((input.startupBudgetMs !== undefined && fx.now() - startedAt >= input.startupBudgetMs
|
|
189
|
+
? `The dev server did not become ready within ${String(input.startupBudgetMs)}ms. `
|
|
190
|
+
: undefined === watching
|
|
191
|
+
? 'The dev server neither printed a URL nor bound a port, so setup has nothing to open. '
|
|
192
|
+
: `The dev server stopped making progress before ${watching} became ready. `) +
|
|
193
|
+
'Check its log, increase --timeout for a slow build, or pass --url with the address it serves.');
|
|
169
194
|
return stop(input, SetupPhase.DEV_SERVER, {}, notes);
|
|
170
195
|
}
|
|
171
196
|
await fx.sleep(input.pollMs);
|
|
@@ -181,9 +206,13 @@ export async function runSetupPhases(input, fx) {
|
|
|
181
206
|
const requiredRuntime = isDesktop(input.shape) ? input.shape : undefined;
|
|
182
207
|
// A tab already on this URL is the answer. Opening another is how three sessions appear.
|
|
183
208
|
// `before` is empty so pickSession will take that tab rather than waiting for a new one.
|
|
209
|
+
//
|
|
210
|
+
// Not only when attached: a server init started itself (Next, Angular — nothing announces) has a
|
|
211
|
+
// url too, and a live tab of this project already on it is just as much the answer. Without this
|
|
212
|
+
// every re-run opened one more tab.
|
|
184
213
|
const alreadyConnected = attachedToExisting
|
|
185
214
|
? pickSession(listed, url, new Set(), requiredRuntime)
|
|
186
|
-
:
|
|
215
|
+
: liveSessionOfProject(listed, url, input.projectId, requiredRuntime);
|
|
187
216
|
const before = new Set(listed.map((s) => s.sessionId));
|
|
188
217
|
// Do not put a window in front of somebody until the page behind it can actually do something.
|
|
189
218
|
//
|
|
@@ -198,7 +227,12 @@ export async function runSetupPhases(input, fx) {
|
|
|
198
227
|
// proof that nothing can ever dial in.
|
|
199
228
|
const budgetMs = input.connectBudgetMs ?? Math.max(input.phaseTimeoutMs, policy.connectBudgetMs);
|
|
200
229
|
let openedBrowser = false;
|
|
201
|
-
|
|
230
|
+
let lease;
|
|
231
|
+
// `--no-open` used to skip this whole block, so nothing was opened and the run ended on "never
|
|
232
|
+
// dialled the bridge" over a correct install. With a lease to fall back on it is entered too, and
|
|
233
|
+
// only the system browser is skipped.
|
|
234
|
+
const canLease = undefined !== fx.openLease;
|
|
235
|
+
if (null === alreadyConnected && policy.openBrowser && (input.openBrowser || canLease)) {
|
|
202
236
|
// SERVED is the precondition, not SDK_PRESENT: a url that answers nothing is the only state
|
|
203
237
|
// where a window is certainly useless. Whether the SDK is in the served HTML is a DIFFERENT
|
|
204
238
|
// question — Vite injects a marker, while Nuxt, React Router, Astro, SvelteKit and CRA deliver
|
|
@@ -209,7 +243,7 @@ export async function runSetupPhases(input, fx) {
|
|
|
209
243
|
//
|
|
210
244
|
// So presence decides only WHEN: open the moment it appears (the fast path for the frameworks
|
|
211
245
|
// that inline it), otherwise once the short readiness window is out.
|
|
212
|
-
const finding = await findingBeforeOpen(() => fx.probePage(url), { now: () => fx.now(), sleep: (ms) => fx.sleep(ms) }, Math.min(SDK_READY_WINDOW_MS, budgetMs), input.pollMs);
|
|
246
|
+
const finding = await findingBeforeOpen(() => fx.probePage(url), { now: () => fx.now(), sleep: (ms) => fx.sleep(ms) }, Math.min(SDK_READY_WINDOW_MS, budgetMs), input.pollMs, input.htmlCarriesSdk);
|
|
213
247
|
if (PageFinding.NOT_SERVED === finding || PageFinding.TLS_REFUSED === finding) {
|
|
214
248
|
note(describePage(finding, url));
|
|
215
249
|
note('Not opening a browser: nothing answered that url, so the window could only show an error ' +
|
|
@@ -218,12 +252,23 @@ export async function runSetupPhases(input, fx) {
|
|
|
218
252
|
else {
|
|
219
253
|
// Said BEFORE the window appears, so somebody watching a page that stays inert has already
|
|
220
254
|
// been told which of the two it is.
|
|
221
|
-
if (PageFinding.SDK_MISSING === finding)
|
|
255
|
+
if (PageFinding.SDK_MISSING === finding && input.htmlCarriesSdk) {
|
|
222
256
|
note(describePage(finding, url));
|
|
257
|
+
}
|
|
223
258
|
// `= true` means ATTEMPTED, which is the most that is knowable — see openBrowser above for
|
|
224
259
|
// why the launcher's own exit code must not be read as "no window appeared".
|
|
225
|
-
await fx.openBrowser(url);
|
|
226
|
-
openedBrowser =
|
|
260
|
+
const launcherFailed = input.openBrowser ? await fx.openBrowser(url) : undefined;
|
|
261
|
+
openedBrowser = input.openBrowser;
|
|
262
|
+
if (undefined !== fx.openLease && (!input.openBrowser || undefined !== launcherFailed)) {
|
|
263
|
+
const owned = await fx.openLease(url);
|
|
264
|
+
if ('failed' in owned) {
|
|
265
|
+
note(`Could not open ${url} in a Reticle-owned browser either: ${owned.failed}`);
|
|
266
|
+
}
|
|
267
|
+
else {
|
|
268
|
+
lease = owned;
|
|
269
|
+
openedBrowser = true;
|
|
270
|
+
}
|
|
271
|
+
}
|
|
227
272
|
}
|
|
228
273
|
}
|
|
229
274
|
// Waiting the full budget for a session when nothing was opened to create one is dead time, and
|
|
@@ -237,6 +282,9 @@ export async function runSetupPhases(input, fx) {
|
|
|
237
282
|
? budgetMs
|
|
238
283
|
: Math.min(NO_BROWSER_GRACE_MS, budgetMs));
|
|
239
284
|
let session = alreadyConnected;
|
|
285
|
+
const waitStartedAt = fx.now();
|
|
286
|
+
let connectSpokeAtMs;
|
|
287
|
+
let leaseFallbackTried = false;
|
|
240
288
|
while (null === session) {
|
|
241
289
|
// On a desktop app, only the desktop window counts. AppShape and the runtime a page reports use
|
|
242
290
|
// the same three names, so the shape IS the requirement — see session-pick.
|
|
@@ -245,6 +293,32 @@ export async function runSetupPhases(input, fx) {
|
|
|
245
293
|
break;
|
|
246
294
|
if (deadline <= fx.now())
|
|
247
295
|
break;
|
|
296
|
+
if (!leaseFallbackTried &&
|
|
297
|
+
undefined === lease &&
|
|
298
|
+
undefined !== fx.openLease &&
|
|
299
|
+
openedBrowser &&
|
|
300
|
+
input.openBrowser &&
|
|
301
|
+
!isDesktop(input.shape) &&
|
|
302
|
+
fx.now() - waitStartedAt >= SYSTEM_BROWSER_GRACE_MS) {
|
|
303
|
+
leaseFallbackTried = true;
|
|
304
|
+
const owned = await fx.openLease(url);
|
|
305
|
+
if ('failed' in owned) {
|
|
306
|
+
note(`Could not open ${url} in a Reticle-owned browser either: ${owned.failed}`);
|
|
307
|
+
}
|
|
308
|
+
else {
|
|
309
|
+
lease = owned;
|
|
310
|
+
}
|
|
311
|
+
continue;
|
|
312
|
+
}
|
|
313
|
+
// Not in silence: a desktop run used to print one line and then nothing for ten minutes.
|
|
314
|
+
const waitedMs = fx.now() - waitStartedAt;
|
|
315
|
+
const progress = connectProgressLine(waitedMs, connectSpokeAtMs, deadline - waitStartedAt, policy.awaiting);
|
|
316
|
+
if (undefined !== progress) {
|
|
317
|
+
// To the person only: it is progress, not a finding, and in `notes` it would split the
|
|
318
|
+
// page description printed before and after this wait into two copies.
|
|
319
|
+
fx.note(progress);
|
|
320
|
+
connectSpokeAtMs = waitedMs;
|
|
321
|
+
}
|
|
248
322
|
await fx.sleep(input.pollMs);
|
|
249
323
|
}
|
|
250
324
|
if (null === session) {
|
|
@@ -275,8 +349,13 @@ export async function runSetupPhases(input, fx) {
|
|
|
275
349
|
fx.note(why.lead);
|
|
276
350
|
notes.push(why.full ?? why.lead);
|
|
277
351
|
}
|
|
278
|
-
|
|
352
|
+
const finding = readPage(await fx.probePage(url));
|
|
353
|
+
// Silent on a bundle-connect shape: its HTML never carries the SDK, so the absence says nothing.
|
|
354
|
+
if (PageFinding.SDK_MISSING !== finding || input.htmlCarriesSdk) {
|
|
355
|
+
note(describePage(finding, url, undefined !== why));
|
|
356
|
+
}
|
|
279
357
|
}
|
|
358
|
+
await releaseLease(lease);
|
|
280
359
|
return stop(input, SetupPhase.CONNECT, { url }, notes);
|
|
281
360
|
}
|
|
282
361
|
// ── connected, which is where ONBOARDING ends ────────────────────────────────────────────────
|
|
@@ -292,16 +371,25 @@ export async function runSetupPhases(input, fx) {
|
|
|
292
371
|
// Both routes, because `explore` needs a model: without ANTHROPIC_API_KEY it answers "No model
|
|
293
372
|
// configured to drive the app". Naming only that one hands the reader a dead end on any machine
|
|
294
373
|
// without a key, which is the same defect this release spent its time removing everywhere else.
|
|
295
|
-
//
|
|
374
|
+
// A heading and a numbered list rather than one paragraph: a reader scans for what to do next.
|
|
296
375
|
//
|
|
297
376
|
// This was a single 524-character note, measured on a real first run: eighty-three words, no
|
|
298
377
|
// break, naming `reticle_*` tools at a reader who had just typed `reticle init` in a terminal and
|
|
299
378
|
// has no such tools to call. The facts were right and the shape made them unreadable, which on
|
|
300
379
|
// the last line of onboarding is the same as not saying them.
|
|
301
|
-
note(
|
|
302
|
-
|
|
380
|
+
note(`✓ Connected. ${url} is instrumented. Onboarding is done; nothing is verified yet.`);
|
|
381
|
+
// Said, because "connected" otherwise reads as "your browser connected": the page that dialled
|
|
382
|
+
// was a headless one Reticle launched and has now closed, and nothing is open on this screen.
|
|
383
|
+
if (undefined !== lease && session.sessionId === lease.sessionId) {
|
|
384
|
+
note('The proof came from a Reticle-owned headless browser (now closed), not a window of yours — ' +
|
|
385
|
+
`open ${url} in your own browser to use the app.`);
|
|
386
|
+
}
|
|
387
|
+
await releaseLease(lease);
|
|
388
|
+
note('');
|
|
389
|
+
note('Next, prove one flow. Your agent does this:');
|
|
390
|
+
note(' 1. Drive one flow and end it with `reticle_act_and_wait` or `reticle_assert`. Those two ' +
|
|
303
391
|
'produce a verdict; nothing else does.');
|
|
304
|
-
note('Or hand over the whole drive: `reticle_verify { action: "explore", persona: "<who does ' +
|
|
392
|
+
note(' 2. Or hand over the whole drive: `reticle_verify { action: "explore", persona: "<who does ' +
|
|
305
393
|
'what>" }` records what it drove, so later runs replay with no model in the loop. Needs ' +
|
|
306
394
|
'ANTHROPIC_API_KEY.');
|
|
307
395
|
return {
|
|
@@ -21,7 +21,20 @@ export interface CandidateSession {
|
|
|
21
21
|
readonly hasCapabilities?: boolean;
|
|
22
22
|
/** Which shell answered. Absent on an SDK too old to report one — see requiredRuntime below. */
|
|
23
23
|
readonly runtime?: string;
|
|
24
|
+
/** The project the page reported. Absent on an SDK too old to report one. */
|
|
25
|
+
readonly projectId?: string;
|
|
24
26
|
}
|
|
27
|
+
/**
|
|
28
|
+
* A tab of THIS project on `url` that is demonstrably alive right now, or null.
|
|
29
|
+
*
|
|
30
|
+
* Stricter than `pickSession`, because it decides whether to open a window at all: every init run
|
|
31
|
+
* opened another tab even when this project already had a live one on the url, and the pile-up then
|
|
32
|
+
* made every tool called without a sessionId fail with "multiple sessions connected". Reused only on
|
|
33
|
+
* positive evidence — the page names this project, is visible, and was heard from inside the
|
|
34
|
+
* daemon's staleness threshold — because reusing a dead tab is the worse mistake: nothing opens and
|
|
35
|
+
* nothing connects.
|
|
36
|
+
*/
|
|
37
|
+
export declare function liveSessionOfProject(sessions: readonly CandidateSession[], url: string, projectId: string | undefined, requiredRuntime?: string): CandidateSession | null;
|
|
25
38
|
/**
|
|
26
39
|
* The session to drive, or null when nothing on this url qualifies.
|
|
27
40
|
*
|
|
@@ -10,9 +10,29 @@
|
|
|
10
10
|
* the oldest: a tab whose dev server died yesterday. The HUD then plays to a window nobody is
|
|
11
11
|
* watching, and the verdict describes a page the user cannot see.
|
|
12
12
|
*/
|
|
13
|
+
import { SESSION_HEALTH } from '@reticlehq/core';
|
|
13
14
|
/** Sorts a hidden tab after a visible one; among equals, the least stale first. */
|
|
14
15
|
const isLive = (s) => true !== s.hidden && true !== s.throttled;
|
|
15
16
|
const staleness = (s) => s.lastSeenMs ?? Number.POSITIVE_INFINITY;
|
|
17
|
+
/**
|
|
18
|
+
* A tab of THIS project on `url` that is demonstrably alive right now, or null.
|
|
19
|
+
*
|
|
20
|
+
* Stricter than `pickSession`, because it decides whether to open a window at all: every init run
|
|
21
|
+
* opened another tab even when this project already had a live one on the url, and the pile-up then
|
|
22
|
+
* made every tool called without a sessionId fail with "multiple sessions connected". Reused only on
|
|
23
|
+
* positive evidence — the page names this project, is visible, and was heard from inside the
|
|
24
|
+
* daemon's staleness threshold — because reusing a dead tab is the worse mistake: nothing opens and
|
|
25
|
+
* nothing connects.
|
|
26
|
+
*/
|
|
27
|
+
export function liveSessionOfProject(sessions, url, projectId, requiredRuntime) {
|
|
28
|
+
if (undefined === projectId || 0 === projectId.length)
|
|
29
|
+
return null;
|
|
30
|
+
const mine = sessions.filter((s) => projectId === s.projectId &&
|
|
31
|
+
true !== s.hidden &&
|
|
32
|
+
undefined !== s.lastSeenMs &&
|
|
33
|
+
s.lastSeenMs <= SESSION_HEALTH.STALE_THRESHOLD_MS);
|
|
34
|
+
return pickSession(mine, url, new Set(), requiredRuntime);
|
|
35
|
+
}
|
|
16
36
|
/**
|
|
17
37
|
* The session to drive, or null when nothing on this url qualifies.
|
|
18
38
|
*
|
|
@@ -27,22 +27,10 @@ interface SetupCommandInput extends Omit<SetupInput, 'shape'> {
|
|
|
27
27
|
readonly env: Readonly<Record<string, string>>;
|
|
28
28
|
/** Register the MCP server with the coding agents init does not itself reach. */
|
|
29
29
|
readonly registerAgents: boolean;
|
|
30
|
+
/** The bridge's pairing token, for the MCP transport a connect-proof lease is opened over. */
|
|
31
|
+
readonly pairingToken?: string | undefined;
|
|
30
32
|
}
|
|
31
|
-
/**
|
|
32
|
-
* Register with the agents `init` does not reach, and leave the skill where one loads skills from.
|
|
33
|
-
*
|
|
34
|
-
* init covers eight clients and only where it finds them installed, which leaves VS Code's USER
|
|
35
|
-
* scope unwritten — it exists on machines today, and init only ever writes the project-scope file,
|
|
36
|
-
* so a VS Code user has no tools outside the directory they ran init in.
|
|
37
|
-
*/
|
|
38
33
|
export declare function registerOtherAgents(print: (line: string) => void): void;
|
|
39
34
|
type SetupCommandResult = SetupOutcome;
|
|
40
|
-
/**
|
|
41
|
-
* Run the phases against the real world.
|
|
42
|
-
*
|
|
43
|
-
* The dev server is stopped on every ending except success, where it is handed to the user: an
|
|
44
|
-
* instrumented app they can watch is the deliverable, and killing it would leave them with config
|
|
45
|
-
* files and a dead tab.
|
|
46
|
-
*/
|
|
47
35
|
export declare function runSetupCommand(input: SetupCommandInput, print: (line: string) => void): Promise<SetupCommandResult>;
|
|
48
36
|
export {};
|
|
@@ -9,22 +9,25 @@ import { existsSync, readFileSync, writeFileSync } from 'node:fs';
|
|
|
9
9
|
import { homedir } from 'node:os';
|
|
10
10
|
import { join } from 'node:path';
|
|
11
11
|
import { planAgentConfigs } from './agent-configs.js';
|
|
12
|
-
import { AppShape, readShape } from './desktop-shape.js';
|
|
12
|
+
import { AppShape, describeShape, readShape } from './desktop-shape.js';
|
|
13
13
|
import { stopOnInterrupt } from './terminal/interrupt.js';
|
|
14
14
|
import { stopOnCrash, crashSentence } from './terminal/crash.js';
|
|
15
15
|
import { applyAgentPlan, applyAgentSkills } from './agent-writer.js';
|
|
16
16
|
import { ApprovalOutcome, grantAutoApproval } from './auto-approve.js';
|
|
17
17
|
import { agentIo } from './agent-io.js';
|
|
18
|
-
import { EnsureDaemon, ensureDaemon, nodeEnsureDaemonDeps } from './bringup/ensure-daemon.js';
|
|
18
|
+
import { EnsureDaemon, ensureDaemon, holdDaemon, nodeEnsureDaemonDeps, nodeRetireDeps, retireMovedDaemons, } from './bringup/ensure-daemon.js';
|
|
19
19
|
import { openInBrowser } from '../cli/launch/cli-launch.js';
|
|
20
|
+
import { openLeaseFor } from './bringup/connect-lease.js';
|
|
20
21
|
import { readProjectId } from '../cli/ports/resolve/cli-port.js';
|
|
21
|
-
import {
|
|
22
|
+
import { spawnSync } from 'node:child_process';
|
|
23
|
+
import { isAlive, reticleStateHome } from '../daemon/daemon.js';
|
|
22
24
|
import { readDevServers } from '../daemon/dev-servers.js';
|
|
23
25
|
import { urlOfExistingApp } from './probe/existing-app.js';
|
|
26
|
+
import { handedOverUrl, stopHandedOver } from './bringup/dev-server-files.js';
|
|
24
27
|
import { listSessions, OwnedDevServer, probePage } from './node-effects.js';
|
|
25
28
|
import { fetchStatus } from '../daemon/binding/daemon-status-probe.js';
|
|
26
|
-
import { summarizeStatus } from '../cli/launch/cli-launch.js';
|
|
27
|
-
import { runSetupPhases, } from './run-setup.js';
|
|
29
|
+
import { daemonSkew, summarizeStatus } from '../cli/launch/cli-launch.js';
|
|
30
|
+
import { runSetupPhases, SetupPhase, } from './run-setup.js';
|
|
28
31
|
import { noticeKey, readSaid, rememberSaid, unsaidNotices } from './agent-notice-memory.js';
|
|
29
32
|
/**
|
|
30
33
|
* The dev server a crash should take with it, if one is running right now.
|
|
@@ -84,13 +87,15 @@ function hasElectronDependency(dir) {
|
|
|
84
87
|
* scope unwritten — it exists on machines today, and init only ever writes the project-scope file,
|
|
85
88
|
* so a VS Code user has no tools outside the directory they ran init in.
|
|
86
89
|
*/
|
|
90
|
+
/** `agent`, `agents`. */
|
|
91
|
+
const agentWord = (n) => (1 === n ? 'agent' : 'agents');
|
|
87
92
|
export function registerOtherAgents(print) {
|
|
88
93
|
const platform = process.platform;
|
|
89
94
|
const home = homedir();
|
|
90
95
|
const results = applyAgentPlan(planAgentConfigs({ home, platform, exists: agentIo.exists, readFile: agentIo.readFile }), agentIo);
|
|
91
96
|
const wrote = results.filter((r) => 'created' === r.action || 'merged' === r.action);
|
|
92
97
|
if (0 < wrote.length) {
|
|
93
|
-
print(`
|
|
98
|
+
print(` ✓ ${String(wrote.length)} more ${agentWord(wrote.length)}: ${wrote.map((r) => r.name).join(', ')}`);
|
|
94
99
|
}
|
|
95
100
|
// A format we will not rewrite is somebody's to edit, so it has to be said rather than skipped --
|
|
96
101
|
// but said ONCE. Two commands call this function by design, so a `curl | sh` followed by
|
|
@@ -103,21 +108,21 @@ export function registerOtherAgents(print) {
|
|
|
103
108
|
.map((r) => ({ name: r.name, file: r.file, why: r.why }));
|
|
104
109
|
const fresh = unsaidNotices(readSaid(stateHome), manual);
|
|
105
110
|
for (const notice of fresh) {
|
|
106
|
-
print(
|
|
111
|
+
print(` ⚠ ${notice.name}: ${notice.why} — add the reticle entry to ${notice.file} by hand.`);
|
|
107
112
|
}
|
|
108
113
|
rememberSaid(stateHome, fresh.map((n) => noticeKey(n)));
|
|
109
114
|
const skills = applyAgentSkills(agentIo, { home, platform });
|
|
110
115
|
if (0 < skills.length)
|
|
111
|
-
print(`
|
|
116
|
+
print(` ✓ the /reticle skill, for ${String(skills.length)} ${agentWord(skills.length)}`);
|
|
112
117
|
// Registration alone still leaves an Accept dialog in front of every call, and a verification run
|
|
113
118
|
// makes dozens of them: the loop is only autonomous once the tools are pre-approved.
|
|
114
119
|
const approvals = grantAutoApproval(agentIo, { home, platform });
|
|
115
120
|
const granted = approvals.filter((a) => ApprovalOutcome.GRANTED === a.outcome);
|
|
116
121
|
if (0 < granted.length) {
|
|
117
|
-
print(`pre-approved
|
|
122
|
+
print(` ✓ tools pre-approved in ${granted.map((a) => a.name).join(', ')}, so no Accept prompt per call`);
|
|
118
123
|
}
|
|
119
124
|
for (const noted of approvals.filter((a) => undefined !== a.warn)) {
|
|
120
|
-
print(
|
|
125
|
+
print(` ⚠ ${noted.name}: ${String(noted.warn)}`);
|
|
121
126
|
}
|
|
122
127
|
}
|
|
123
128
|
/**
|
|
@@ -127,13 +132,70 @@ export function registerOtherAgents(print) {
|
|
|
127
132
|
* instrumented app they can watch is the deliverable, and killing it would leave them with config
|
|
128
133
|
* files and a dead tab.
|
|
129
134
|
*/
|
|
135
|
+
/** How long a stopped dev server gets to let go of its port before the new one is started. */
|
|
136
|
+
const HANDED_OVER_EXIT_MS = 10_000;
|
|
137
|
+
const HANDED_OVER_EXIT_POLL_MS = 100;
|
|
138
|
+
/** Stop the server a previous init handed over for `appDir`, and wait (bounded) for it to exit. */
|
|
139
|
+
async function restartHandedOverServer(appDir) {
|
|
140
|
+
let stoppedPid;
|
|
141
|
+
const stopped = stopHandedOver(reticleStateHome(), appDir, {
|
|
142
|
+
alive: isAlive,
|
|
143
|
+
kill: (pid) => {
|
|
144
|
+
stoppedPid = pid;
|
|
145
|
+
try {
|
|
146
|
+
// Its own process group (it was spawned detached), so what it started goes with it.
|
|
147
|
+
if ('win32' === process.platform) {
|
|
148
|
+
spawnSync('taskkill', ['/PID', String(pid), '/T', '/F']);
|
|
149
|
+
}
|
|
150
|
+
else {
|
|
151
|
+
process.kill(-pid, 'SIGTERM');
|
|
152
|
+
}
|
|
153
|
+
}
|
|
154
|
+
catch {
|
|
155
|
+
/* already gone */
|
|
156
|
+
}
|
|
157
|
+
},
|
|
158
|
+
});
|
|
159
|
+
const deadline = Date.now() + HANDED_OVER_EXIT_MS;
|
|
160
|
+
while (undefined !== stoppedPid && isAlive(stoppedPid) && Date.now() < deadline) {
|
|
161
|
+
await new Promise((done) => setTimeout(done, HANDED_OVER_EXIT_POLL_MS));
|
|
162
|
+
}
|
|
163
|
+
return stopped;
|
|
164
|
+
}
|
|
130
165
|
export async function runSetupCommand(input, print) {
|
|
131
166
|
if (input.registerAgents)
|
|
132
167
|
registerOtherAgents(print);
|
|
168
|
+
// A moved port first: this project's daemon on the OLD port would otherwise be the one the page
|
|
169
|
+
// finds and dials, while everything below waits on the new one.
|
|
170
|
+
const retired = await retireMovedDaemons(readProjectId(input.appDir), input.bridgePort, nodeRetireDeps());
|
|
171
|
+
for (const port of retired) {
|
|
172
|
+
print(`stopped this project's Reticle daemon on port ${String(port)}: the project now uses ` +
|
|
173
|
+
`${String(input.bridgePort)}, and the page would have kept dialling the old one.`);
|
|
174
|
+
}
|
|
175
|
+
// And the dev server a previous init left running, which may have read the old port once at
|
|
176
|
+
// startup (Next does) — so the attach below would keep a page dialling the daemon just stopped.
|
|
177
|
+
if (0 < retired.length && (await restartHandedOverServer(input.appDir))) {
|
|
178
|
+
print(`stopped the dev server a previous init started, so it restarts on port ${String(input.bridgePort)}.`);
|
|
179
|
+
}
|
|
133
180
|
// Before the app is booted, because the app's whole job from here is to dial this port. Without
|
|
134
181
|
// it the phases wait out their budget and then report the SDK as the thing that failed.
|
|
135
|
-
const daemon = await ensureDaemon(input.bridgePort, nodeEnsureDaemonDeps());
|
|
136
|
-
if (
|
|
182
|
+
const daemon = await ensureDaemon(input.bridgePort, nodeEnsureDaemonDeps((status) => undefined !== daemonSkew(status)));
|
|
183
|
+
if (undefined !== daemon.message)
|
|
184
|
+
print(daemon.message);
|
|
185
|
+
if (EnsureDaemon.SKEWED_IN_USE === daemon.state) {
|
|
186
|
+
// Stop here: every hello from the page would be refused, and waiting out the connect budget
|
|
187
|
+
// only to blame the page is the run this exists to prevent.
|
|
188
|
+
return {
|
|
189
|
+
ok: false,
|
|
190
|
+
reachedPhase: SetupPhase.DEV_SERVER,
|
|
191
|
+
flowSaved: false,
|
|
192
|
+
notes: undefined === daemon.message ? [] : [daemon.message],
|
|
193
|
+
fallback: [
|
|
194
|
+
`Once those sessions are done, free the port with \`npx @reticlehq/server kill --port ${String(input.bridgePort)}\` and re-run init.`,
|
|
195
|
+
],
|
|
196
|
+
};
|
|
197
|
+
}
|
|
198
|
+
if (EnsureDaemon.UNAVAILABLE === daemon.state) {
|
|
137
199
|
print(`could not start the Reticle daemon on port ${String(input.bridgePort)}, so nothing is listening for the app to connect to. Run \`npx @reticlehq/server serve --port ${String(input.bridgePort)}\` and try again.`);
|
|
138
200
|
}
|
|
139
201
|
// Which shell this is decides three things the phases would otherwise get wrong: opening a
|
|
@@ -145,7 +207,7 @@ export async function runSetupCommand(input, print) {
|
|
|
145
207
|
hasElectronDep: hasElectronDependency(input.appDir),
|
|
146
208
|
});
|
|
147
209
|
if (AppShape.WEB !== shape)
|
|
148
|
-
print(`detected
|
|
210
|
+
print(`detected ${describeShape(shape)}`);
|
|
149
211
|
const server = new OwnedDevServer();
|
|
150
212
|
const effects = {
|
|
151
213
|
startDevServer: (command, cwd) => {
|
|
@@ -153,10 +215,16 @@ export async function runSetupCommand(input, print) {
|
|
|
153
215
|
server.start(command, cwd, input.env);
|
|
154
216
|
return Promise.resolve();
|
|
155
217
|
},
|
|
156
|
-
|
|
218
|
+
// The server the plugin announced (Vite), or failing that the one a previous init handed over
|
|
219
|
+
// and recorded — the only way to know a Next or Angular server on the port is this project's own.
|
|
220
|
+
existingAppUrl: async () => urlOfExistingApp(readDevServers(reticleStateHome()), {
|
|
157
221
|
projectId: readProjectId(input.appDir),
|
|
158
222
|
root: input.appDir,
|
|
159
|
-
})
|
|
223
|
+
}) ??
|
|
224
|
+
(await handedOverUrl(reticleStateHome(), input.appDir, {
|
|
225
|
+
alive: isAlive,
|
|
226
|
+
answers: async (url) => (await probePage(url)).served,
|
|
227
|
+
})),
|
|
160
228
|
devServerOutput: () => server.output(),
|
|
161
229
|
devServerExited: () => server.exited(),
|
|
162
230
|
devServerQuietForMs: () => server.quietForMs(),
|
|
@@ -164,20 +232,16 @@ export async function runSetupCommand(input, print) {
|
|
|
164
232
|
probePage,
|
|
165
233
|
openBrowser: async (url) => {
|
|
166
234
|
const failure = await openInBrowser(url);
|
|
167
|
-
if (null
|
|
168
|
-
|
|
169
|
-
|
|
170
|
-
|
|
171
|
-
|
|
172
|
-
|
|
173
|
-
|
|
174
|
-
|
|
175
|
-
// says which url needs one rather than inventing a Reticle that can conjure it.
|
|
176
|
-
`open one: this asks the OS for your default browser, and it is a machine with none — ` +
|
|
177
|
-
`CI, a container, an SSH session, WSL with no host browser. Point any browser that can ` +
|
|
178
|
-
`reach it at ${url}; the session appears within a second of the page loading.`);
|
|
179
|
-
}
|
|
235
|
+
if (null === failure)
|
|
236
|
+
return undefined;
|
|
237
|
+
// It used to end "nothing else here will open one", which stopped being true once a
|
|
238
|
+
// Reticle-owned browser could always launch: run-setup opens a lease on this failure, so the
|
|
239
|
+
// reader is told what happens next rather than sent to find a browser by hand.
|
|
240
|
+
print(`could not open a browser (${failure}), so the connect proof will come from a ` +
|
|
241
|
+
`Reticle-owned headless browser instead. To use the app yourself, point any browser at ${url}.`);
|
|
242
|
+
return failure;
|
|
180
243
|
},
|
|
244
|
+
openLease: (url) => openLeaseFor(input.bridgePort, url, input.pairingToken),
|
|
181
245
|
listSessions: () => listSessions(input.bridgePort),
|
|
182
246
|
// `summarizeStatus` already narrows this payload for `reticle status`; reusing it here keeps one
|
|
183
247
|
// reader of the wire shape rather than two that can disagree about which key carries the reason.
|
|
@@ -207,16 +271,26 @@ export async function runSetupCommand(input, print) {
|
|
|
207
271
|
activeDevServerStop = () => {
|
|
208
272
|
server.stop();
|
|
209
273
|
};
|
|
274
|
+
// Hold the daemon for as long as this run waits on it. Its idle rule counted none of this as
|
|
275
|
+
// activity, and on a slow desktop build the daemon init had just started idled out mid-wait.
|
|
276
|
+
const releaseDaemon = holdDaemon(input.bridgePort);
|
|
210
277
|
try {
|
|
211
|
-
const outcome = await runSetupPhases({ ...input, shape }, effects);
|
|
278
|
+
const outcome = await runSetupPhases({ ...input, shape, projectId: readProjectId(input.appDir) }, effects);
|
|
212
279
|
// The app stays up only when there is something worth watching.
|
|
213
|
-
if (outcome.ok)
|
|
214
|
-
server.handOver();
|
|
280
|
+
if (outcome.ok) {
|
|
281
|
+
server.handOver(outcome.url);
|
|
282
|
+
// Its output is on a file now, not this terminal, so say where: a compile error that used to
|
|
283
|
+
// scroll past here is the first thing somebody will look for.
|
|
284
|
+
const log = server.logPath();
|
|
285
|
+
if (undefined !== log)
|
|
286
|
+
print(`The dev server keeps running; its output goes to ${log}`);
|
|
287
|
+
}
|
|
215
288
|
return {
|
|
216
289
|
...outcome,
|
|
217
290
|
};
|
|
218
291
|
}
|
|
219
292
|
finally {
|
|
293
|
+
releaseDaemon();
|
|
220
294
|
releaseSignals();
|
|
221
295
|
activeDevServerStop = null;
|
|
222
296
|
server.stop();
|
|
@@ -32,6 +32,8 @@ export interface SetupMcpResult {
|
|
|
32
32
|
readonly detected: readonly string[];
|
|
33
33
|
readonly registered: readonly string[];
|
|
34
34
|
readonly alreadyThere: readonly string[];
|
|
35
|
+
/** Automatic registration was attempted but failed. */
|
|
36
|
+
readonly failed: readonly string[];
|
|
35
37
|
/**
|
|
36
38
|
* Clients whose config this will NOT rewrite, and which therefore still need a human.
|
|
37
39
|
*
|
|
@@ -1,9 +1,10 @@
|
|
|
1
|
-
import { McpClient, MCP_CLIENTS, claudeAddCommand, claudeAvailableProbe,
|
|
1
|
+
import { McpClient, MCP_CLIENTS, claudeAddCommand, claudeAvailableProbe, claudeHasReticle, detectMcpClients, mergeClientConfig, clientSpec, ClientMergeStatus, } from '@reticlehq/init';
|
|
2
2
|
import { OnboardingPhase, OnboardingStepStatus, } from '@reticlehq/core/telemetry';
|
|
3
3
|
export function setupMcp(io) {
|
|
4
4
|
const detected = [];
|
|
5
5
|
const registered = [];
|
|
6
6
|
const alreadyThere = [];
|
|
7
|
+
const failed = [];
|
|
7
8
|
const manual = [];
|
|
8
9
|
/*
|
|
9
10
|
* Claude Code first, because nothing else can find it.
|
|
@@ -22,14 +23,16 @@ export function setupMcp(io) {
|
|
|
22
23
|
const available = claudeAvailableProbe();
|
|
23
24
|
if (io.runCli(available.command, available.args)) {
|
|
24
25
|
detected.push(McpClient.CLAUDE_CODE);
|
|
25
|
-
|
|
26
|
-
if (io
|
|
26
|
+
// Read from Claude's config: `claude mcp get` health-checks by launching the server.
|
|
27
|
+
if (claudeHasReticle(io, undefined)) {
|
|
27
28
|
alreadyThere.push(McpClient.CLAUDE_CODE);
|
|
28
29
|
}
|
|
29
30
|
else {
|
|
30
31
|
const cmd = claudeAddCommand();
|
|
31
32
|
if (io.runCli(cmd.command, cmd.args))
|
|
32
33
|
registered.push(McpClient.CLAUDE_CODE);
|
|
34
|
+
else
|
|
35
|
+
failed.push(McpClient.CLAUDE_CODE);
|
|
33
36
|
}
|
|
34
37
|
}
|
|
35
38
|
for (const client of detectMcpClients(io)) {
|
|
@@ -47,8 +50,13 @@ export function setupMcp(io) {
|
|
|
47
50
|
manual.push({ id: client.id, configPath: client.configPath, docs: spec.docs });
|
|
48
51
|
continue;
|
|
49
52
|
}
|
|
50
|
-
|
|
51
|
-
|
|
53
|
+
try {
|
|
54
|
+
io.writeFile(client.configPath, merged.content);
|
|
55
|
+
registered.push(client.id);
|
|
56
|
+
}
|
|
57
|
+
catch {
|
|
58
|
+
failed.push(client.id);
|
|
59
|
+
}
|
|
52
60
|
}
|
|
53
61
|
/*
|
|
54
62
|
* Both INSTALL steps, reported from the one place that knows the answer.
|
|
@@ -69,7 +77,8 @@ export function setupMcp(io) {
|
|
|
69
77
|
* SKIPPED when there was nothing to register with — not failed. Nothing went wrong; there was
|
|
70
78
|
* simply no agent here, and counting that as our failure would hide the ones that are.
|
|
71
79
|
*
|
|
72
|
-
* FAILED when
|
|
80
|
+
* FAILED when no detected client is usable, whether registration failed or needs a hand.
|
|
81
|
+
* That case used to fall through to COMPLETED,
|
|
73
82
|
* so the cohort with the tools registered NOWHERE was counted among the successful installs, and
|
|
74
83
|
* the one number that says "did this machine get Reticle" read yes for the people it read no
|
|
75
84
|
* for. Registering some and not others is still COMPLETED: the agent they are using may well be
|
|
@@ -79,11 +88,11 @@ export function setupMcp(io) {
|
|
|
79
88
|
? OnboardingStepStatus.SKIPPED
|
|
80
89
|
: 0 === registered.length && alreadyThere.length > 0
|
|
81
90
|
? OnboardingStepStatus.SKIPPED
|
|
82
|
-
: 0 === registered.length && 0 === alreadyThere.length
|
|
91
|
+
: 0 === registered.length && 0 === alreadyThere.length
|
|
83
92
|
? OnboardingStepStatus.FAILED
|
|
84
93
|
: OnboardingStepStatus.COMPLETED,
|
|
85
94
|
});
|
|
86
|
-
return { detected, registered, alreadyThere, manual };
|
|
95
|
+
return { detected, registered, alreadyThere, failed, manual };
|
|
87
96
|
}
|
|
88
97
|
/** Everything this machine could be asked about, for the report when nothing was found. */
|
|
89
98
|
export function knownClientLabels() {
|
|
@@ -26,3 +26,11 @@ export declare const WAIT_PROGRESS_EVERY_MS = 15000;
|
|
|
26
26
|
* without a line per poll burying the output it exists to explain.
|
|
27
27
|
*/
|
|
28
28
|
export declare function waitProgressLine(elapsedMs: number, watching: string | undefined, lastSpokeAtMs: number | undefined): string | undefined;
|
|
29
|
+
/**
|
|
30
|
+
* The same, for the wait on a session after the app is up.
|
|
31
|
+
*
|
|
32
|
+
* That wait was silent as well: a desktop init printed "Waiting for the app to launch and dial in."
|
|
33
|
+
* and then nothing for the whole of its budget. Naming what is awaited and how long remains is what
|
|
34
|
+
* lets somebody tell a slow start from a wedged one — and when to stop waiting themselves.
|
|
35
|
+
*/
|
|
36
|
+
export declare function connectProgressLine(elapsedMs: number, lastSpokeAtMs: number | undefined, budgetMs: number, awaiting: string): string | undefined;
|