@reticlehq/server 3.1.0 → 3.3.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/SKILL.md +30 -28
- package/dist/command/cli/adhoc-suite.d.ts +19 -0
- package/dist/command/cli/adhoc-suite.js +88 -0
- package/dist/command/cli/adhoc-verdict.d.ts +23 -2
- package/dist/command/cli/adhoc-verdict.js +18 -10
- package/dist/command/cli/auth/device-flow.d.ts +2 -2
- package/dist/command/cli/cli-doctor.js +37 -5
- package/dist/command/cli/cli-flow-commands.d.ts +2 -1
- package/dist/command/cli/cli-kill.js +1 -1
- package/dist/command/cli/cli-parse-grammar.d.ts +7 -0
- package/dist/command/cli/cli-parse-grammar.js +7 -0
- package/dist/command/cli/cli-parse-verify.d.ts +2 -5
- package/dist/command/cli/cli-parse-verify.js +86 -13
- package/dist/command/cli/cli-parse.d.ts +8 -1
- package/dist/command/cli/cli-parse.js +38 -7
- package/dist/command/cli/cli-verify.d.ts +59 -0
- package/dist/command/cli/cli-verify.js +131 -19
- package/dist/command/cli/cloud-cli.js +7 -6
- package/dist/command/cli/cloud-kit.d.ts +0 -2
- package/dist/command/cli/cloud-kit.js +3 -7
- package/dist/command/cli/config/config-discovery.d.ts +12 -0
- package/dist/command/cli/config/config-discovery.js +19 -0
- package/dist/command/cli/doctor/doctor-rows.d.ts +12 -0
- package/dist/command/cli/doctor/doctor-rows.js +12 -0
- package/dist/command/cli/doctor/doctor-sessions-line.d.ts +2 -1
- package/dist/command/cli/drive/drive-command.js +1 -1
- package/dist/command/cli/launch/cli-launch.d.ts +3 -23
- package/dist/command/cli/launch/cli-launch.js +29 -70
- package/dist/command/cli/lifecycle/daemon-lifecycle.d.ts +33 -0
- package/dist/command/cli/lifecycle/daemon-lifecycle.js +198 -0
- package/dist/command/cli/mcp-command.js +5 -3
- package/dist/command/cli/ports/resolve/cli-port.d.ts +15 -1
- package/dist/command/cli/ports/resolve/cli-port.js +50 -2
- package/dist/command/cli/report-command.d.ts +16 -0
- package/dist/command/cli/report-command.js +97 -0
- package/dist/command/cli/status/status-lines.d.ts +28 -0
- package/dist/command/cli/status/status-lines.js +117 -0
- package/dist/command/cli.d.ts +1 -1
- package/dist/command/cli.js +29 -177
- package/dist/command/daemon/binding/daemon-status-probe.d.ts +32 -0
- package/dist/command/daemon/binding/daemon-status-probe.js +93 -0
- package/dist/command/daemon/binding/loopback-reach.d.ts +42 -0
- package/dist/command/daemon/binding/loopback-reach.js +63 -0
- package/dist/command/daemon/binding/port-presence.d.ts +1 -1
- package/dist/command/daemon/daemon-resilience.d.ts +13 -1
- package/dist/command/daemon/daemon-resilience.js +12 -1
- package/dist/command/daemon/daemon.d.ts +2 -11
- package/dist/command/daemon/daemon.js +8 -11
- package/dist/command/daemon/idle-shutdown.d.ts +2 -1
- package/dist/command/daemon/lifetime/heartbeat.d.ts +8 -0
- package/dist/command/daemon/lifetime/heartbeat.js +10 -0
- package/dist/command/daemon/lifetime/log-cap.d.ts +68 -0
- package/dist/command/daemon/lifetime/log-cap.js +115 -0
- package/dist/command/setup/agent-notice-memory.d.ts +16 -0
- package/dist/command/setup/agent-notice-memory.js +65 -0
- package/dist/command/setup/bringup/dev-server-wait.d.ts +1 -0
- package/dist/command/setup/bringup/dev-server-wait.js +16 -0
- package/dist/command/setup/bringup/ensure-daemon.js +1 -1
- package/dist/command/setup/init/init-host.js +8 -0
- package/dist/command/setup/init/init-runtime.js +13 -1
- package/dist/command/setup/node-effects.js +1 -1
- package/dist/command/setup/probe/page-probe.js +9 -2
- package/dist/command/setup/run-setup.d.ts +25 -7
- package/dist/command/setup/run-setup.js +51 -9
- package/dist/command/setup/setup-command.js +31 -3
- package/dist/command/setup/terminal/confirm.js +2 -1
- package/dist/command/update/updater.d.ts +27 -0
- package/dist/command/update/updater.js +35 -2
- package/dist/features/crawl/crawl-tools.js +3 -1
- package/dist/features/crawl/crawl.d.ts +2 -0
- package/dist/features/crawl/crawl.js +3 -1
- package/dist/features/harness/drive-report.d.ts +73 -0
- package/dist/features/harness/drive-report.js +196 -0
- package/dist/features/harness/driver.js +29 -8
- package/dist/features/harness/drivers.d.ts +19 -0
- package/dist/features/harness/drivers.js +19 -0
- package/dist/features/harness/fill-values.d.ts +67 -0
- package/dist/features/harness/fill-values.js +153 -0
- package/dist/features/harness/jev-driver.d.ts +188 -0
- package/dist/features/harness/jev-driver.js +661 -0
- package/dist/features/harness/openai-driver.d.ts +46 -0
- package/dist/features/harness/openai-driver.js +197 -0
- package/dist/features/harness/platform-config.d.ts +62 -0
- package/dist/features/harness/platform-config.js +97 -0
- package/dist/features/visual/visual-tools.js +26 -2
- package/dist/index.d.ts +1 -1
- package/dist/index.js +80 -130
- package/dist/judgement/capsule/capsule-store.d.ts +31 -2
- package/dist/judgement/capsule/capsule-store.js +58 -6
- package/dist/judgement/capsule/capsule.js +5 -0
- package/dist/judgement/capsule/causal-summary.d.ts +13 -1
- package/dist/judgement/capsule/causal-summary.js +7 -0
- package/dist/judgement/domain/domain-model.js +38 -9
- package/dist/judgement/domain/domain-tools.js +4 -2
- package/dist/judgement/outcome/assertion-integrity.d.ts +3 -3
- package/dist/judgement/phenomena/phenomena.d.ts +2 -1
- package/dist/judgement/runs/artifact/gap-report.d.ts +17 -0
- package/dist/judgement/runs/artifact/gap-report.js +46 -0
- package/dist/judgement/runs/artifact/gap-summary.d.ts +58 -0
- package/dist/judgement/runs/artifact/gap-summary.js +85 -0
- package/dist/judgement/runs/artifact/render-report.js +4 -1
- package/dist/judgement/runs/artifact/to-artifact.d.ts +7 -2
- package/dist/judgement/runs/artifact/to-artifact.js +1 -1
- package/dist/judgement/runs/context-tools.js +21 -3
- package/dist/judgement/runs/drive-run.js +7 -2
- package/dist/judgement/runs/verification-sync.d.ts +2 -2
- package/dist/judgement/runs/verification-sync.js +9 -2
- package/dist/language/flows/affected-tools.js +6 -3
- package/dist/language/flows/annotate-notes/annotate.d.ts +0 -15
- package/dist/language/flows/annotate-notes/annotate.js +66 -18
- package/dist/language/flows/flow-anchor.d.ts +12 -0
- package/dist/language/flows/flow-anchor.js +20 -0
- package/dist/language/flows/flow-expect-grammar.d.ts +2 -2
- package/dist/language/flows/flow-expect-grammar.js +92 -11
- package/dist/language/flows/flow-intent.js +1 -1
- package/dist/language/flows/flow-learning.d.ts +3 -0
- package/dist/language/flows/flow-learning.js +39 -8
- package/dist/language/flows/flow-replay-run.d.ts +55 -10
- package/dist/language/flows/flow-replay-run.js +217 -58
- package/dist/language/flows/flow-replay-types.d.ts +9 -0
- package/dist/language/flows/flow-replay.d.ts +29 -11
- package/dist/language/flows/flow-replay.js +221 -203
- package/dist/language/flows/flow-result.d.ts +18 -1
- package/dist/language/flows/flow-result.js +18 -2
- package/dist/language/flows/flow-step-runners.d.ts +26 -10
- package/dist/language/flows/flow-step-runners.js +20 -4
- package/dist/language/flows/flow-store-for-session.d.ts +2 -1
- package/dist/language/flows/flow-success.d.ts +21 -6
- package/dist/language/flows/flow-success.js +50 -138
- package/dist/language/flows/flow-tools.js +8 -4
- package/dist/language/flows/flows.d.ts +15 -10
- package/dist/language/flows/flows.js +17 -4
- package/dist/language/flows/heal.d.ts +9 -1
- package/dist/language/flows/heal.js +24 -2
- package/dist/language/flows/learned-sources.d.ts +21 -0
- package/dist/language/flows/learned-sources.js +84 -0
- package/dist/language/flows/narrow-write.d.ts +4 -4
- package/dist/language/flows/recording/recording-backtrack.d.ts +0 -7
- package/dist/language/flows/recording/recording-backtrack.js +1 -1
- package/dist/language/flows/recording/replay-halt.d.ts +2 -1
- package/dist/language/flows/recording/tape/recordings.d.ts +2 -2
- package/dist/language/flows/replay.js +13 -9
- package/dist/language/flows/stores/annotation-store.d.ts +5 -5
- package/dist/language/flows/stores/assertion-tiers-store.js +2 -3
- package/dist/language/flows/stores/flake-store.js +2 -3
- package/dist/language/flows/suite/coverage.d.ts +1 -1
- package/dist/language/flows/suite/parallel-suite.d.ts +2 -1
- package/dist/language/flows/suite-selection.d.ts +15 -1
- package/dist/language/flows/suite-selection.js +1 -1
- package/dist/language/flows/verify-change-tools.js +21 -4
- package/dist/memory/cloud/account-state.d.ts +1 -1
- package/dist/memory/cloud/account-state.js +5 -4
- package/dist/memory/cloud/cloud-config.d.ts +9 -0
- package/dist/memory/cloud/cloud-config.js +9 -0
- package/dist/memory/cloud/cloud-sync.d.ts +7 -4
- package/dist/memory/cloud/cloud-sync.js +11 -6
- package/dist/memory/cloud/harness-config.d.ts +12 -0
- package/dist/memory/cloud/harness-config.js +58 -0
- package/dist/memory/cloud/harness-offer.d.ts +44 -0
- package/dist/memory/cloud/harness-offer.js +146 -0
- package/dist/memory/cloud/harness-switch.d.ts +11 -0
- package/dist/memory/cloud/harness-switch.js +53 -0
- package/dist/memory/cloud/sync-cycle.d.ts +12 -0
- package/dist/memory/cloud/sync-cycle.js +29 -4
- package/dist/memory/cloud/sync-daemon.d.ts +35 -2
- package/dist/memory/cloud/sync-daemon.js +45 -0
- package/dist/memory/cloud/sync-disk.js +11 -6
- package/dist/memory/impact/impact-recorder.d.ts +8 -0
- package/dist/memory/impact/impact-recorder.js +2 -1
- package/dist/memory/impact/impact-store.d.ts +5 -0
- package/dist/memory/impact/impact-store.js +25 -0
- package/dist/memory/intent/inline-intent.js +8 -3
- package/dist/memory/intent/intent-shard-store.d.ts +33 -47
- package/dist/memory/intent/intent-shard-store.js +253 -141
- package/dist/memory/intent/intent-shard.d.ts +22 -23
- package/dist/memory/intent/intent-shard.js +1 -1
- package/dist/memory/intent/intent-store.d.ts +13 -8
- package/dist/memory/intent/intent-store.js +46 -90
- package/dist/memory/intent/intent-tools.js +34 -23
- package/dist/memory/journal/ambient-store.js +2 -3
- package/dist/memory/journal/attach-journal.d.ts +11 -0
- package/dist/memory/journal/attach-journal.js +1 -1
- package/dist/memory/journal/deviation-report.js +4 -1
- package/dist/memory/journal/deviation-service.js +10 -2
- package/dist/memory/journal/drive-flow.d.ts +2 -2
- package/dist/memory/journal/drive-flow.js +21 -12
- package/dist/memory/journal/envelope-store.js +2 -3
- package/dist/memory/journal/journal-query.d.ts +35 -1
- package/dist/memory/journal/journal-query.js +46 -0
- package/dist/memory/journal/journal-recorder.d.ts +71 -2
- package/dist/memory/journal/journal-recorder.js +45 -7
- package/dist/memory/journal/on-disk/retain-policy.d.ts +34 -0
- package/dist/memory/journal/on-disk/retain-policy.js +44 -0
- package/dist/memory/journal/on-disk/retention.d.ts +91 -2
- package/dist/memory/journal/on-disk/retention.js +181 -11
- package/dist/memory/journal/on-disk/startup-maintenance.d.ts +42 -0
- package/dist/memory/journal/on-disk/startup-maintenance.js +14 -0
- package/dist/memory/journal/on-disk/workspace-gitignore.d.ts +0 -2
- package/dist/memory/journal/on-disk/workspace-gitignore.js +6 -38
- package/dist/memory/journal/on-disk/workspace-tiers.d.ts +52 -0
- package/dist/memory/journal/on-disk/workspace-tiers.js +88 -0
- package/dist/memory/journal/route-template.d.ts +22 -0
- package/dist/memory/journal/route-template.js +44 -0
- package/dist/memory/journal/self-observation.d.ts +3 -0
- package/dist/memory/journal/self-observation.js +67 -0
- package/dist/memory/journal/session-end.d.ts +17 -4
- package/dist/memory/journal/session-end.js +85 -7
- package/dist/memory/journal/session-journal.d.ts +122 -5
- package/dist/memory/journal/session-journal.js +489 -32
- package/dist/memory/project/artifact-root-resolver.d.ts +3 -0
- package/dist/memory/project/artifact-root-resolver.js +89 -0
- package/dist/memory/project/artifact-root.d.ts +39 -2
- package/dist/memory/project/artifact-root.js +58 -1
- package/dist/memory/project/baselines.d.ts +1 -1
- package/dist/memory/project/dir/fill-value-store.d.ts +32 -0
- package/dist/memory/project/dir/fill-value-store.js +88 -0
- package/dist/memory/project/dir/reticle-dir.d.ts +15 -5
- package/dist/memory/project/dir/reticle-dir.js +11 -0
- package/dist/memory/project/fs/fs-port.d.ts +22 -1
- package/dist/memory/project/fs/fs-port.js +72 -5
- package/dist/memory/project/fs/write-atomic.d.ts +26 -0
- package/dist/memory/project/fs/write-atomic.js +48 -0
- package/dist/memory/project/learned-routes.d.ts +16 -2
- package/dist/memory/project/learned-routes.js +26 -10
- package/dist/memory/project/project-for-root.d.ts +30 -0
- package/dist/memory/project/project-for-root.js +43 -0
- package/dist/memory/project/project-tools.js +10 -5
- package/dist/memory/project/session-root.d.ts +35 -6
- package/dist/memory/project/session-root.js +70 -8
- package/dist/memory/recall/prior/connection-memory.d.ts +6 -0
- package/dist/memory/recall/prior/connection-memory.js +8 -0
- package/dist/memory/recall/registered-projects.d.ts +2 -1
- package/dist/portal/bridge/auth-failure-reason.d.ts +9 -0
- package/dist/portal/bridge/auth-failure-reason.js +20 -3
- package/dist/portal/bridge/bridge.d.ts +6 -1
- package/dist/portal/bridge/bridge.js +36 -2
- package/dist/portal/input/real-input.js +2 -1
- package/dist/portal/pool/browser-pool.d.ts +10 -0
- package/dist/portal/pool/browser-pool.js +10 -0
- package/dist/portal/pool/playwright-launcher.js +1 -0
- package/dist/portal/pool/pool-contract.d.ts +2 -0
- package/dist/portal/pool/storage-seed.d.ts +2 -1
- package/dist/portal/pool/zero-install.d.ts +27 -0
- package/dist/portal/pool/zero-install.js +54 -0
- package/dist/portal/realm/web-realm.js +6 -0
- package/dist/portal/session/dev-server/dev-server-probe.d.ts +2 -1
- package/dist/portal/session/dev-server/served-document.d.ts +21 -0
- package/dist/portal/session/dev-server/served-document.js +57 -0
- package/dist/portal/session/facts/background-traffic.d.ts +9 -0
- package/dist/portal/session/facts/background-traffic.js +25 -0
- package/dist/portal/session/facts/session-replaced.d.ts +19 -0
- package/dist/portal/session/facts/session-replaced.js +22 -0
- package/dist/portal/session/live-control-tools.js +48 -3
- package/dist/portal/session/no-session-diagnosis.d.ts +1 -0
- package/dist/portal/session/no-session-diagnosis.js +67 -60
- package/dist/portal/session/no-session-next-action.js +3 -2
- package/dist/portal/session/no-session-watch.js +14 -10
- package/dist/portal/session/presence/attachment-history.d.ts +13 -2
- package/dist/portal/session/rebind.d.ts +18 -0
- package/dist/portal/session/rebind.js +38 -0
- package/dist/portal/session/session-health.d.ts +61 -0
- package/dist/portal/session/session-health.js +40 -1
- package/dist/portal/session/session-info.d.ts +6 -0
- package/dist/portal/session/session-manager.d.ts +13 -0
- package/dist/portal/session/session-manager.js +42 -3
- package/dist/portal/session/session-successor.d.ts +11 -0
- package/dist/portal/session/session-successor.js +14 -1
- package/dist/portal/session/session-verdict-facts.d.ts +28 -0
- package/dist/portal/session/session-verdict-facts.js +15 -0
- package/dist/portal/session/session.d.ts +26 -10
- package/dist/portal/session/session.js +48 -49
- package/dist/status-payload.d.ts +12 -1
- package/dist/status-payload.js +4 -1
- package/dist/surface/http-server.js +3 -5
- package/dist/surface/mcp/faults/mcp-outage.d.ts +2 -1
- package/dist/surface/mcp/faults/mcp-outage.js +20 -4
- package/dist/surface/mcp/mcp-proxy.d.ts +10 -1
- package/dist/surface/mcp/mcp-proxy.js +23 -12
- package/dist/surface/mcp/mcp.d.ts +10 -1
- package/dist/surface/mcp/mcp.js +59 -29
- package/dist/surface/mcp/proxy/proxy-daemon-probe.d.ts +15 -2
- package/dist/surface/mcp/proxy/proxy-daemon-probe.js +22 -5
- package/dist/surface/mcp/proxy/proxy-log.d.ts +5 -11
- package/dist/surface/mcp/proxy/proxy-log.js +7 -22
- package/dist/surface/mcp/proxy-idle-exit.d.ts +2 -1
- package/dist/surface/mcp/server-instructions.js +12 -1
- package/dist/surface/tools/act/act-preflight.js +6 -1
- package/dist/surface/tools/act/already-true.d.ts +40 -0
- package/dist/surface/tools/act/already-true.js +36 -0
- package/dist/surface/tools/act/navigation/navigate-arrival.d.ts +6 -0
- package/dist/surface/tools/act/navigation/navigate-arrival.js +13 -0
- package/dist/surface/tools/act/sequence-grade.js +13 -0
- package/dist/surface/tools/act/settle-in-flight.d.ts +6 -2
- package/dist/surface/tools/act/settle-in-flight.js +29 -6
- package/dist/surface/tools/act-capsule.js +49 -8
- package/dist/surface/tools/act-tools.js +25 -24
- package/dist/surface/tools/args/numeric-bounds.d.ts +15 -2
- package/dist/surface/tools/args/numeric-bounds.js +30 -3
- package/dist/surface/tools/assert/assert-grade.js +14 -2
- package/dist/surface/tools/assert/assert-verdict.js +74 -21
- package/dist/surface/tools/browser-tools.js +4 -3
- package/dist/surface/tools/coverage-tools.js +5 -2
- package/dist/surface/tools/dynamic-tools.js +43 -46
- package/dist/surface/tools/error-recovery.d.ts +6 -0
- package/dist/surface/tools/error-recovery.js +15 -0
- package/dist/surface/tools/explore-tools.js +80 -7
- package/dist/surface/tools/feedback-tools.d.ts +2 -1
- package/dist/surface/tools/feedback-tools.js +50 -3
- package/dist/surface/tools/first-sentence.d.ts +24 -0
- package/dist/surface/tools/first-sentence.js +0 -0
- package/dist/surface/tools/harness-explore.d.ts +125 -1
- package/dist/surface/tools/harness-explore.js +401 -15
- package/dist/surface/tools/harness-plan.d.ts +76 -0
- package/dist/surface/tools/harness-plan.js +156 -0
- package/dist/surface/tools/harness-toolset.js +12 -1
- package/dist/surface/tools/invoke-tool.js +82 -9
- package/dist/surface/tools/lease-hint.d.ts +8 -0
- package/dist/surface/tools/lease-hint.js +10 -0
- package/dist/surface/tools/lease-tools.d.ts +16 -28
- package/dist/surface/tools/lease-tools.js +53 -5
- package/dist/surface/tools/lineage-tools.js +1 -1
- package/dist/surface/tools/live-call-text.d.ts +6 -0
- package/dist/surface/tools/live-call-text.js +36 -2
- package/dist/surface/tools/observe-tools.js +69 -15
- package/dist/surface/tools/read/query-windowed.d.ts +1 -1
- package/dist/surface/tools/read/query-windowed.js +1 -1
- package/dist/surface/tools/read-tools.js +1 -2
- package/dist/surface/tools/real-input-attempt.d.ts +0 -5
- package/dist/surface/tools/real-input-attempt.js +1 -1
- package/dist/surface/tools/tool-kit.d.ts +23 -3
- package/dist/surface/tools/tool-kit.js +6 -1
- package/dist/surface/tools/tool-surface.js +17 -5
- package/dist/surface/tools/tools-helpers.d.ts +8 -7
- package/dist/surface/tools/tools-helpers.js +11 -13
- package/dist/surface/tools/tools.js +70 -30
- package/dist/telemetry/bug-found.js +32 -2
- package/dist/telemetry/feedback-context.js +3 -3
- package/dist/telemetry/feedback-local.d.ts +1 -0
- package/dist/telemetry/feedback-local.js +1 -3
- package/dist/telemetry/feedback.d.ts +1 -1
- package/dist/telemetry/harness-drive.d.ts +6 -0
- package/dist/telemetry/harness-drive.js +39 -0
- package/dist/telemetry/hud-metrics.d.ts +32 -0
- package/dist/telemetry/hud-metrics.js +95 -0
- package/dist/telemetry/mcp-connection.d.ts +14 -1
- package/dist/telemetry/mcp-connection.js +17 -2
- package/dist/telemetry/onboarding-firsts.d.ts +30 -1
- package/dist/telemetry/onboarding-firsts.js +54 -12
- package/dist/telemetry/session-metrics.d.ts +5 -0
- package/dist/telemetry/session-metrics.js +15 -1
- package/dist/telemetry/verification-of.js +31 -3
- package/dist/wire-journal.d.ts +31 -0
- package/dist/wire-journal.js +100 -0
- package/docs/README.md +1 -0
- package/docs/agent-cheatsheet.md +7 -3
- package/docs/autodrive.md +42 -2
- package/docs/benchmarks.md +1 -1
- package/docs/capabilities.mdx +1 -1
- package/docs/cli/cloud.mdx +11 -9
- package/docs/cli/doctor.mdx +1 -0
- package/docs/cli/init.mdx +1 -0
- package/docs/cli/report.mdx +44 -0
- package/docs/cli/status.mdx +35 -20
- package/docs/cli.mdx +8 -18
- package/docs/desktop.mdx +1 -1
- package/docs/faq.mdx +1 -1
- package/docs/flows.md +11 -0
- package/docs/frameworks.mdx +113 -1
- package/docs/install-agentic.mdx +36 -0
- package/docs/install-manual.mdx +1 -1
- package/docs/packages/tauri.mdx +1 -1
- package/docs/packages.mdx +1 -1
- package/docs/predicates.mdx +144 -4
- package/docs/system-map.md +6 -4
- package/docs/telemetry.md +2 -1
- package/docs/token-efficiency.md +2 -2
- package/docs/tools/overview.mdx +1 -1
- package/docs/tools/query.mdx +10 -10
- package/docs/troubleshooting.mdx +8 -2
- package/docs/usage.md +2 -2
- package/docs/what-is-recorded.md +62 -0
- package/package.json +5 -5
- package/dist/judgement/outcome/predicate-to-expect.d.ts +0 -44
- package/dist/judgement/outcome/predicate-to-expect.js +0 -148
|
@@ -3,8 +3,9 @@
|
|
|
3
3
|
* These are thin clients over the `/v1` API; the logic is server-side and these verbs just surface it.
|
|
4
4
|
* Credentials live under `~/.reticle`: `session.json` (human token from `reticle login`) and
|
|
5
5
|
* `credentials.json` (per-project api keys from `reticle link`). The non-secret repo binding + sync policy
|
|
6
|
-
* is `<repo>/.reticle/cloud.json`. Auth for a command = `
|
|
6
|
+
* is `<repo>/.reticle/cloud.json`. Auth for a command = `RETICLE_API_KEY` env (agent) OR the login token.
|
|
7
7
|
*/
|
|
8
|
+
import { apiKeyFrom } from '@reticlehq/core';
|
|
8
9
|
import { mkdir, writeFile } from 'node:fs/promises';
|
|
9
10
|
import { setTimeout as sleep } from 'node:timers/promises';
|
|
10
11
|
import { homedir } from 'node:os';
|
|
@@ -188,8 +189,8 @@ const cmdProject = async (argv) => {
|
|
|
188
189
|
// Name BOTH hosts when there is a session for a different one. "Run reticle login" on its own is
|
|
189
190
|
// baffling to somebody who just did — the useful fact is that they logged in somewhere else.
|
|
190
191
|
err(null !== active && normalizeUrl(active.url) !== url
|
|
191
|
-
? `signed in to ${normalizeUrl(active.url)}, but this command targets ${url} — run \`reticle login --url ${url}\`, or set
|
|
192
|
-
: `not signed in to ${url} — run \`reticle login --url ${url}\`, or set
|
|
192
|
+
? `signed in to ${normalizeUrl(active.url)}, but this command targets ${url} — run \`reticle login --url ${url}\`, or set RETICLE_API_KEY`
|
|
193
|
+
: `not signed in to ${url} — run \`reticle login --url ${url}\`, or set RETICLE_API_KEY`);
|
|
193
194
|
return 2;
|
|
194
195
|
}
|
|
195
196
|
const sub = argv[0];
|
|
@@ -232,7 +233,7 @@ const cmdProject = async (argv) => {
|
|
|
232
233
|
};
|
|
233
234
|
/**
|
|
234
235
|
* `reticle link [--project <id>]` — bind THIS repo to a cloud project. With a login token it MINTS a
|
|
235
|
-
* project-scoped key (no pasting); with a pre-set
|
|
236
|
+
* project-scoped key (no pasting); with a pre-set RETICLE_API_KEY it resolves the key's project via
|
|
236
237
|
* whoami. Writes the non-secret binding to <repo>/.reticle/cloud.json and the secret key to
|
|
237
238
|
* ~/.reticle/credentials.json (keyed by projectId).
|
|
238
239
|
*/
|
|
@@ -241,7 +242,7 @@ const cmdLink = async (argv) => {
|
|
|
241
242
|
const url = baseUrl(await readSession(), f['url']);
|
|
242
243
|
// Same rule as every other authed verb: the token is looked up by the host it will be sent to.
|
|
243
244
|
const session = await readSessionFor(url);
|
|
244
|
-
const envKey = process.env
|
|
245
|
+
const envKey = apiKeyFrom(process.env);
|
|
245
246
|
/*
|
|
246
247
|
* Read the existing binding FIRST, because it decides which project a bare `link` targets.
|
|
247
248
|
* Re-running `link` is ordinary — rotating a key, repointing an environment — and it must land in
|
|
@@ -361,7 +362,7 @@ const cmdLink = async (argv) => {
|
|
|
361
362
|
}
|
|
362
363
|
}
|
|
363
364
|
else {
|
|
364
|
-
err('run `reticle login` first, or set
|
|
365
|
+
err('run `reticle login` first, or set RETICLE_API_KEY to link with an existing key');
|
|
365
366
|
return 2;
|
|
366
367
|
}
|
|
367
368
|
const reticleDir = join(process.cwd(), RETICLE_DIR);
|
|
@@ -1,6 +1,4 @@
|
|
|
1
1
|
import { type Session } from './auth/cloud-session.js';
|
|
2
|
-
/** Where `reticle login` dials when nothing says otherwise: the hosted service. */
|
|
3
|
-
export declare const DEFAULT_URL = "https://app.reticle.sh";
|
|
4
2
|
export declare const RETICLE_DIR = ".reticle";
|
|
5
3
|
export declare const SESSION_FILE = "session.json";
|
|
6
4
|
export declare const CREDENTIALS_FILE = "credentials.json";
|
|
@@ -8,6 +8,7 @@
|
|
|
8
8
|
* `cloud-login` is the first to move — and it is a real seam rather than a file-size dodge: nothing
|
|
9
9
|
* here knows what a project, a credential or a link is.
|
|
10
10
|
*/
|
|
11
|
+
import { apiKeyFrom } from '@reticlehq/core';
|
|
11
12
|
import { readFile } from 'node:fs/promises';
|
|
12
13
|
import { homedir } from 'node:os';
|
|
13
14
|
import { join } from 'node:path';
|
|
@@ -15,7 +16,7 @@ import { z } from 'zod';
|
|
|
15
16
|
import { cloudFetch } from '../../memory/cloud/cloud-sync.js';
|
|
16
17
|
import { normalizeUrl, readSessionFrom, resolveSessionFor, sessionPath, } from './auth/cloud-session.js';
|
|
17
18
|
/** Where `reticle login` dials when nothing says otherwise: the hosted service. */
|
|
18
|
-
|
|
19
|
+
const DEFAULT_URL = 'https://app.reticle.sh';
|
|
19
20
|
export const RETICLE_DIR = '.reticle';
|
|
20
21
|
export const SESSION_FILE = 'session.json';
|
|
21
22
|
export const CREDENTIALS_FILE = 'credentials.json';
|
|
@@ -83,12 +84,7 @@ export const baseUrl = (session, explicit) => {
|
|
|
83
84
|
return DEFAULT_URL;
|
|
84
85
|
};
|
|
85
86
|
/** Bearer for a command: an explicit api key (agent) wins, else the human login token. */
|
|
86
|
-
export const bearer = (session) =>
|
|
87
|
-
const key = process.env['RETICLE_CLOUD_KEY'];
|
|
88
|
-
if (key !== undefined && key.length > 0)
|
|
89
|
-
return key;
|
|
90
|
-
return session?.token ?? null;
|
|
91
|
-
};
|
|
87
|
+
export const bearer = (session) => apiKeyFrom(process.env) ?? session?.token ?? null;
|
|
92
88
|
/** One `/v1` call. Throws a friendly Error on a non-2xx so the command surfaces it and exits 1. */
|
|
93
89
|
export const api = async (method, url, token, body) => {
|
|
94
90
|
const headers = {};
|
|
@@ -32,5 +32,17 @@ export interface ConfigDiscovery {
|
|
|
32
32
|
/** Every directory checked, in the order checked. The answer to "are you sure?". */
|
|
33
33
|
searched: string[];
|
|
34
34
|
}
|
|
35
|
+
/**
|
|
36
|
+
* Has this directory ever been through `init` — is there a `.reticle.json` in it?
|
|
37
|
+
*
|
|
38
|
+
* A single directory, not a walk: the caller is asking whether THIS tree invited Reticle in, which
|
|
39
|
+
* is what decides whether a session with no resolvable project may write here at all. An ancestor's
|
|
40
|
+
* config says nothing about that, so the up-walk `discoverProjectConfigs` does would answer a
|
|
41
|
+
* different question and answer it yes too often.
|
|
42
|
+
*
|
|
43
|
+
* Lives here so the question stays in the directory that owns the config file, rather than every
|
|
44
|
+
* caller importing the basename and re-deriving the join.
|
|
45
|
+
*/
|
|
46
|
+
export declare function hasProjectConfig(directory: string): boolean;
|
|
35
47
|
export declare function discoverProjectConfigs(cwd: string): ConfigDiscovery;
|
|
36
48
|
export {};
|
|
@@ -114,6 +114,25 @@ function firstSegment(glob) {
|
|
|
114
114
|
const [first] = glob.split('/');
|
|
115
115
|
return first ?? '';
|
|
116
116
|
}
|
|
117
|
+
/**
|
|
118
|
+
* Has this directory ever been through `init` — is there a `.reticle.json` in it?
|
|
119
|
+
*
|
|
120
|
+
* A single directory, not a walk: the caller is asking whether THIS tree invited Reticle in, which
|
|
121
|
+
* is what decides whether a session with no resolvable project may write here at all. An ancestor's
|
|
122
|
+
* config says nothing about that, so the up-walk `discoverProjectConfigs` does would answer a
|
|
123
|
+
* different question and answer it yes too often.
|
|
124
|
+
*
|
|
125
|
+
* Lives here so the question stays in the directory that owns the config file, rather than every
|
|
126
|
+
* caller importing the basename and re-deriving the join.
|
|
127
|
+
*/
|
|
128
|
+
export function hasProjectConfig(directory) {
|
|
129
|
+
try {
|
|
130
|
+
return existsSync(join(directory, RETICLE_CONFIG_BASENAME));
|
|
131
|
+
}
|
|
132
|
+
catch {
|
|
133
|
+
return false;
|
|
134
|
+
}
|
|
135
|
+
}
|
|
117
136
|
export function discoverProjectConfigs(cwd) {
|
|
118
137
|
const searched = [];
|
|
119
138
|
const found = [];
|
|
@@ -40,6 +40,18 @@ export declare const DoctorRow: {
|
|
|
40
40
|
* verification." Every row in this list checks a COMPONENT; none checked a LINK.
|
|
41
41
|
*/
|
|
42
42
|
readonly AGENT_LINK: "agent link";
|
|
43
|
+
/**
|
|
44
|
+
* Which of `localhost`'s two answers reaches the bridge, printed only when they disagree.
|
|
45
|
+
*
|
|
46
|
+
* The daemon binds `127.0.0.1` and `loopback-alias.ts` forwards `[::1]` to it, best-effort —
|
|
47
|
+
* a machine with IPv6 off, or something already holding `[::1]:port`, leaves the daemon serving
|
|
48
|
+
* IPv4 alone and must not be fatal. But `localhost` is a NAME, and Windows Chrome tries the IPv6
|
|
49
|
+
* answer first, so on that combination the SDK's documented default URL cannot reach a daemon that
|
|
50
|
+
* is running perfectly. The only record of the alias failing was a field on one JSON log line.
|
|
51
|
+
*
|
|
52
|
+
* Conditional like `version`: a healthy machine answers on both and prints nothing.
|
|
53
|
+
*/
|
|
54
|
+
readonly LOOPBACK: "loopback";
|
|
43
55
|
readonly BRIDGE_PORT: "bridge port";
|
|
44
56
|
readonly PORT_CHECK: "port check";
|
|
45
57
|
readonly SIBLING: "sibling";
|
|
@@ -40,6 +40,18 @@ export const DoctorRow = {
|
|
|
40
40
|
* verification." Every row in this list checks a COMPONENT; none checked a LINK.
|
|
41
41
|
*/
|
|
42
42
|
AGENT_LINK: 'agent link',
|
|
43
|
+
/**
|
|
44
|
+
* Which of `localhost`'s two answers reaches the bridge, printed only when they disagree.
|
|
45
|
+
*
|
|
46
|
+
* The daemon binds `127.0.0.1` and `loopback-alias.ts` forwards `[::1]` to it, best-effort —
|
|
47
|
+
* a machine with IPv6 off, or something already holding `[::1]:port`, leaves the daemon serving
|
|
48
|
+
* IPv4 alone and must not be fatal. But `localhost` is a NAME, and Windows Chrome tries the IPv6
|
|
49
|
+
* answer first, so on that combination the SDK's documented default URL cannot reach a daemon that
|
|
50
|
+
* is running perfectly. The only record of the alias failing was a field on one JSON log line.
|
|
51
|
+
*
|
|
52
|
+
* Conditional like `version`: a healthy machine answers on both and prints nothing.
|
|
53
|
+
*/
|
|
54
|
+
LOOPBACK: 'loopback',
|
|
43
55
|
BRIDGE_PORT: 'bridge port',
|
|
44
56
|
PORT_CHECK: 'port check',
|
|
45
57
|
SIBLING: 'sibling',
|
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
/** What this reads off `/status`. Everything is optional — an older daemon may send none of it. */
|
|
2
|
-
|
|
2
|
+
interface SessionFacts {
|
|
3
3
|
sessionCount?: unknown;
|
|
4
4
|
why?: unknown;
|
|
5
5
|
}
|
|
@@ -10,3 +10,4 @@ export interface SessionsLine {
|
|
|
10
10
|
why?: string;
|
|
11
11
|
}
|
|
12
12
|
export declare function sessionsLine(facts: SessionFacts): SessionsLine;
|
|
13
|
+
export {};
|
|
@@ -11,7 +11,7 @@ import { log } from '../../../log.js';
|
|
|
11
11
|
import { probePresence, describePresence } from '../../daemon/binding/port-presence.js';
|
|
12
12
|
import { probeDaemon } from '../../../surface/mcp/mcp-proxy.js';
|
|
13
13
|
import { readPid } from '../../daemon/daemon.js';
|
|
14
|
-
import { fetchStatus } from '
|
|
14
|
+
import { fetchStatus } from '../../daemon/binding/daemon-status-probe.js';
|
|
15
15
|
import { captureLookup, findPortHolder } from '../ports/port-holder.js';
|
|
16
16
|
import { DriveMode, decideDriveMode, describeAttached, driveForeignHolder, driveHeadlessOnAttach, driveRaceLost, requestDriveSession, } from './drive-attach.js';
|
|
17
17
|
export function handleDrive(parsed) {
|
|
@@ -3,26 +3,7 @@
|
|
|
3
3
|
* "show me the app" flow (`reticle open`). Split out of cli.ts so that file stays under the size cap.
|
|
4
4
|
* The decision logic is pure (unit-tested); the IO (fetch, OS browser launch) is injected/isolated.
|
|
5
5
|
*/
|
|
6
|
-
|
|
7
|
-
interface StatusSession {
|
|
8
|
-
sessionId: string;
|
|
9
|
-
url: string;
|
|
10
|
-
throttled: boolean;
|
|
11
|
-
stale: boolean;
|
|
12
|
-
pendingMarks: number;
|
|
13
|
-
/** The tab is attached and answering nothing — see Session.unresponsive. Absent on an older daemon. */
|
|
14
|
-
unresponsive?: true;
|
|
15
|
-
}
|
|
16
|
-
/**
|
|
17
|
-
* Reduce the daemon's /status JSON to the compact view `reticle status` prints. Pure: narrows the
|
|
18
|
-
* untrusted wire payload (never `any`) and tolerates a missing/partial body so a malformed response
|
|
19
|
-
* degrades to "running, 0 sessions" instead of throwing.
|
|
20
|
-
*/
|
|
21
|
-
export declare function summarizeStatus(payload: unknown): {
|
|
22
|
-
sessionCount: number;
|
|
23
|
-
sessions: StatusSession[];
|
|
24
|
-
why?: string;
|
|
25
|
-
};
|
|
6
|
+
export { summarizeStatus } from '../../daemon/binding/daemon-status-probe.js';
|
|
26
7
|
/**
|
|
27
8
|
* Warn when the daemon already on this port is a different version than this CLI, and attach anyway.
|
|
28
9
|
*
|
|
@@ -31,8 +12,6 @@ export declare function summarizeStatus(payload: unknown): {
|
|
|
31
12
|
* another agent's session on a version bump, which is worse than a loud line.
|
|
32
13
|
*/
|
|
33
14
|
export declare function warnOnDaemonSkew(port: number): Promise<void>;
|
|
34
|
-
/** GET the daemon's /status JSON. Resolves to the parsed body, or undefined on any failure. */
|
|
35
|
-
export declare function fetchStatus(port: number): Promise<unknown>;
|
|
36
15
|
/** What `reticle open` should do: reuse an already-connected tab, open a new one, or ask for a url. */
|
|
37
16
|
type OpenDecision = {
|
|
38
17
|
action: 'reuse';
|
|
@@ -53,6 +32,7 @@ type OpenDecision = {
|
|
|
53
32
|
* Decide what `reticle open [url]` does, given the currently-connected tabs. Pure.
|
|
54
33
|
* - no url + a tab connected → reuse it (the app is already open; don't spawn a duplicate).
|
|
55
34
|
* - no url + nothing connected → ask for one.
|
|
35
|
+
* - a HIDDEN tab is not a candidate: the url is opened instead (its own url, when none was given).
|
|
56
36
|
* - url + a tab already AT that url → reuse it (idempotent — re-running never piles up tabs).
|
|
57
37
|
* - url + a tab on that origin but another page → keep it, and say so (`left-as-is`).
|
|
58
38
|
* - url + no matching tab → open it.
|
|
@@ -67,6 +47,7 @@ type OpenDecision = {
|
|
|
67
47
|
export declare function decideOpen(all: {
|
|
68
48
|
url: string;
|
|
69
49
|
unresponsive?: boolean;
|
|
50
|
+
hidden?: boolean;
|
|
70
51
|
}[], url: string | undefined): OpenDecision;
|
|
71
52
|
/** The OS command that opens a URL in the default browser, per platform. Pure — unit-tested. */
|
|
72
53
|
export declare function openCommand(url: string, platform: NodeJS.Platform): {
|
|
@@ -89,4 +70,3 @@ export declare function openInBrowser(url: string, platform?: NodeJS.Platform, r
|
|
|
89
70
|
* by spawning a real launcher on a developer machine.
|
|
90
71
|
*/
|
|
91
72
|
export declare function launcherFailure(code: number | null, signal: NodeJS.Signals | null): string | null;
|
|
92
|
-
export {};
|
|
@@ -1,46 +1,19 @@
|
|
|
1
|
-
import * as http from 'node:http';
|
|
2
1
|
import { NodePlatform } from '../../../machine/platform.js';
|
|
3
2
|
import { spawn } from 'node:child_process';
|
|
4
|
-
import { isOpaqueOrigin
|
|
3
|
+
import { isOpaqueOrigin } from '@reticlehq/core';
|
|
5
4
|
import { daemonFix, describeSkew } from '../../version/version-skew.js';
|
|
6
5
|
import { CONTRACT_FINGERPRINT } from '@reticlehq/core';
|
|
7
6
|
import { SERVER_VERSION } from '../../version/identity/server-version.js';
|
|
8
7
|
import { log } from '../../../log.js';
|
|
9
|
-
import {
|
|
8
|
+
import { fetchStatus } from '../../daemon/binding/daemon-status-probe.js';
|
|
10
9
|
/**
|
|
11
|
-
*
|
|
12
|
-
*
|
|
13
|
-
*
|
|
10
|
+
* CLI launch + status helpers — the daemon-introspection (`reticle status`) and the one-command
|
|
11
|
+
* "show me the app" flow (`reticle open`). Split out of cli.ts so that file stays under the size cap.
|
|
12
|
+
* The decision logic is pure (unit-tested); the IO (fetch, OS browser launch) is injected/isolated.
|
|
14
13
|
*/
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
const obj = payload;
|
|
19
|
-
// Carried through to the printed line: with no sessions this is the whole answer, and dropping it
|
|
20
|
-
// here would silently undo the reason it is on the wire.
|
|
21
|
-
const why = 'string' === typeof obj['why'] ? obj['why'] : undefined;
|
|
22
|
-
const raw = Array.isArray(obj['sessions']) ? obj['sessions'] : [];
|
|
23
|
-
const sessions = raw
|
|
24
|
-
.map((s) => {
|
|
25
|
-
if (typeof s !== 'object' || null === s)
|
|
26
|
-
return null;
|
|
27
|
-
const r = s;
|
|
28
|
-
const sessionId = 'string' === typeof r['sessionId'] ? r['sessionId'] : '';
|
|
29
|
-
if ('' === sessionId)
|
|
30
|
-
return null;
|
|
31
|
-
return {
|
|
32
|
-
sessionId,
|
|
33
|
-
url: 'string' === typeof r['url'] ? r['url'] : '',
|
|
34
|
-
throttled: true === r['throttled'],
|
|
35
|
-
stale: true === r['stale'],
|
|
36
|
-
pendingMarks: 'number' === typeof r['pendingMarks'] ? r['pendingMarks'] : 0,
|
|
37
|
-
...(true === r['unresponsive'] ? { unresponsive: true } : {}),
|
|
38
|
-
};
|
|
39
|
-
})
|
|
40
|
-
.filter((s) => s !== null);
|
|
41
|
-
const sessionCount = 'number' === typeof obj['sessionCount'] ? obj['sessionCount'] : sessions.length;
|
|
42
|
-
return { sessionCount, sessions, ...(why === undefined ? {} : { why }) };
|
|
43
|
-
}
|
|
14
|
+
// Parsing the daemon's /status payload lives beside the probe that fetches it; re-exported here for
|
|
15
|
+
// the callers that already import it from this module.
|
|
16
|
+
export { summarizeStatus } from '../../daemon/binding/daemon-status-probe.js';
|
|
44
17
|
/** A string field off the /status body, or undefined on a daemon too old to report it. */
|
|
45
18
|
function statusField(payload, key) {
|
|
46
19
|
if (typeof payload !== 'object' || null === payload)
|
|
@@ -68,39 +41,6 @@ export async function warnOnDaemonSkew(port) {
|
|
|
68
41
|
if (skew !== undefined)
|
|
69
42
|
log('reticle_daemon_skew', { port, warning: skew });
|
|
70
43
|
}
|
|
71
|
-
/** How long the daemon /status probe waits before giving up — a local loopback call is near-instant. */
|
|
72
|
-
const STATUS_PROBE_TIMEOUT_MS = 1000;
|
|
73
|
-
/** GET the daemon's /status JSON. Resolves to the parsed body, or undefined on any failure. */
|
|
74
|
-
export function fetchStatus(port) {
|
|
75
|
-
return new Promise((resolve) => {
|
|
76
|
-
const req = http.get({
|
|
77
|
-
host: LOOPBACK_HOST,
|
|
78
|
-
port,
|
|
79
|
-
path: STATUS_PATH,
|
|
80
|
-
timeout: STATUS_PROBE_TIMEOUT_MS,
|
|
81
|
-
// Shares the proxy's keep-alive agent: the proxy calls this on every reconnect, and a socket
|
|
82
|
-
// per probe is the same churn the POST leg was fixed for.
|
|
83
|
-
agent: loopbackAgent,
|
|
84
|
-
}, (res) => {
|
|
85
|
-
let body = '';
|
|
86
|
-
res.setEncoding('utf8');
|
|
87
|
-
res.on('data', (chunk) => (body += chunk));
|
|
88
|
-
res.on('end', () => {
|
|
89
|
-
try {
|
|
90
|
-
resolve(JSON.parse(body));
|
|
91
|
-
}
|
|
92
|
-
catch {
|
|
93
|
-
resolve(undefined);
|
|
94
|
-
}
|
|
95
|
-
});
|
|
96
|
-
});
|
|
97
|
-
req.on('error', () => resolve(undefined));
|
|
98
|
-
req.on('timeout', () => {
|
|
99
|
-
req.destroy();
|
|
100
|
-
resolve(undefined);
|
|
101
|
-
});
|
|
102
|
-
});
|
|
103
|
-
}
|
|
104
44
|
function sameOrigin(a, b) {
|
|
105
45
|
try {
|
|
106
46
|
const origin = new URL(a).origin;
|
|
@@ -118,6 +58,7 @@ function sameOrigin(a, b) {
|
|
|
118
58
|
* Decide what `reticle open [url]` does, given the currently-connected tabs. Pure.
|
|
119
59
|
* - no url + a tab connected → reuse it (the app is already open; don't spawn a duplicate).
|
|
120
60
|
* - no url + nothing connected → ask for one.
|
|
61
|
+
* - a HIDDEN tab is not a candidate: the url is opened instead (its own url, when none was given).
|
|
121
62
|
* - url + a tab already AT that url → reuse it (idempotent — re-running never piles up tabs).
|
|
122
63
|
* - url + a tab on that origin but another page → keep it, and say so (`left-as-is`).
|
|
123
64
|
* - url + no matching tab → open it.
|
|
@@ -133,10 +74,28 @@ export function decideOpen(all, url) {
|
|
|
133
74
|
// A tab that has stopped answering is not a tab you can be handed. `open` is the command a caller
|
|
134
75
|
// reaches for to RECOVER, and reusing the wedged one left no way out short of killing the daemon:
|
|
135
76
|
// ending the session only flips a flag on the record, it does not make the page answer.
|
|
136
|
-
|
|
77
|
+
//
|
|
78
|
+
// A HIDDEN tab is excluded for the same reason. `open` means "show me the app", and a backgrounded
|
|
79
|
+
// tab is the one definition of an app nobody is being shown: the browser starves its timers, rAF
|
|
80
|
+
// and pointer gestures, so checks that depend on them come back inconclusive even where the
|
|
81
|
+
// behaviour works. Reported from the field as `open` answering `reusing` for a hidden tab and
|
|
82
|
+
// three checks that could never resolve. Opening the url instead costs one foreground tab and the
|
|
83
|
+
// hidden one is left exactly where it was — nothing is navigated out from under anybody.
|
|
84
|
+
//
|
|
85
|
+
// HIDDEN, not `throttled`: throttled is `hidden || stale`, and a stale-but-visible tab is usually
|
|
86
|
+
// a quiet page that drives fine. Excluding those would pile up a duplicate tab per run, which is
|
|
87
|
+
// the thing the origin match below exists to prevent.
|
|
88
|
+
const sessions = all.filter((s) => true !== s.unresponsive && true !== s.hidden);
|
|
137
89
|
if (url === undefined) {
|
|
138
90
|
const first = sessions[0];
|
|
139
|
-
|
|
91
|
+
if (first !== undefined)
|
|
92
|
+
return { action: 'reuse', url: first.url };
|
|
93
|
+
// Nothing visible to hand back, but a hidden tab still knows the url the caller would have
|
|
94
|
+
// typed. Opening it is the whole request; `need-url` would ask for something already in hand.
|
|
95
|
+
const backgrounded = all.find((s) => true === s.hidden);
|
|
96
|
+
return backgrounded !== undefined
|
|
97
|
+
? { action: 'open', url: backgrounded.url }
|
|
98
|
+
: { action: 'need-url' };
|
|
140
99
|
}
|
|
141
100
|
const exact = sessions.find((s) => s.url === url);
|
|
142
101
|
if (exact !== undefined)
|
|
@@ -0,0 +1,33 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Starting, stopping and restarting the daemon — the three commands that decide whether a `reticle`
|
|
3
|
+
* process exists, and the waiting each of them has to do to answer honestly.
|
|
4
|
+
*
|
|
5
|
+
* Lifted out of `cli.ts` for the reason `cli-launch.ts` was before it: that file sat at exactly the
|
|
6
|
+
* thousand-line cap, so the next line added to it — to any command, related or not — failed the
|
|
7
|
+
* build. This is the largest group in it that is one idea rather than several.
|
|
8
|
+
*
|
|
9
|
+
* The idea is: a lifecycle command must report what is actually the case, and the only way to know
|
|
10
|
+
* that is to wait and look. `serve` waits for the daemon to ANSWER before claiming it started,
|
|
11
|
+
* because the child binds asynchronously and a parent that reports the spawn reports a guess.
|
|
12
|
+
* `stop` waits for the process to be gone and escalates when it is not, because a daemon that
|
|
13
|
+
* ignores SIGTERM is the one situation `stop` exists for. `restart` reports its two halves
|
|
14
|
+
* separately, because "killed nothing" and "killed it but could not start a new one" leave the user
|
|
15
|
+
* in different places and one exit code cannot say which.
|
|
16
|
+
*/
|
|
17
|
+
export declare function handleServe(parsed: {
|
|
18
|
+
port: number;
|
|
19
|
+
driveUrl?: string;
|
|
20
|
+
headless: boolean;
|
|
21
|
+
http: boolean;
|
|
22
|
+
httpPort?: number;
|
|
23
|
+
httpToken?: string;
|
|
24
|
+
}): void;
|
|
25
|
+
export declare function handleStop(port: number, quiet: boolean): void;
|
|
26
|
+
/**
|
|
27
|
+
* `reticle restart` — free the port, then put a daemon back on it and prove it bound.
|
|
28
|
+
*
|
|
29
|
+
* The two halves are reported separately on purpose. A restart that killed the old daemon and then
|
|
30
|
+
* could not start a new one leaves the user in a different place from one that could not kill
|
|
31
|
+
* anything at all, and a single `ok: false` cannot tell them which happened.
|
|
32
|
+
*/
|
|
33
|
+
export declare function handleRestart(port: number, force: boolean): Promise<void>;
|
|
@@ -0,0 +1,198 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Starting, stopping and restarting the daemon — the three commands that decide whether a `reticle`
|
|
3
|
+
* process exists, and the waiting each of them has to do to answer honestly.
|
|
4
|
+
*
|
|
5
|
+
* Lifted out of `cli.ts` for the reason `cli-launch.ts` was before it: that file sat at exactly the
|
|
6
|
+
* thousand-line cap, so the next line added to it — to any command, related or not — failed the
|
|
7
|
+
* build. This is the largest group in it that is one idea rather than several.
|
|
8
|
+
*
|
|
9
|
+
* The idea is: a lifecycle command must report what is actually the case, and the only way to know
|
|
10
|
+
* that is to wait and look. `serve` waits for the daemon to ANSWER before claiming it started,
|
|
11
|
+
* because the child binds asynchronously and a parent that reports the spawn reports a guess.
|
|
12
|
+
* `stop` waits for the process to be gone and escalates when it is not, because a daemon that
|
|
13
|
+
* ignores SIGTERM is the one situation `stop` exists for. `restart` reports its two halves
|
|
14
|
+
* separately, because "killed nothing" and "killed it but could not start a new one" leave the user
|
|
15
|
+
* in different places and one exit code cannot say which.
|
|
16
|
+
*/
|
|
17
|
+
import { log } from '../../../log.js';
|
|
18
|
+
import { RETICLE_VERIFY_DEFAULT_PORT } from '../../../index.js';
|
|
19
|
+
import { verifyEndpointMismatch } from '../../../status-payload.js';
|
|
20
|
+
import { probeDaemon } from '../../../surface/mcp/mcp-proxy.js';
|
|
21
|
+
import { stateDirProblem } from '../../daemon/state-dir.js';
|
|
22
|
+
import { readPid, isAlive, removePid, spawnDaemon, logPath, reticleStateHome, } from '../../daemon/daemon.js';
|
|
23
|
+
import { readDaemonStartupCause } from '../../daemon/lifetime/startup-failure.js';
|
|
24
|
+
import { PortPresence, probePresence, describePresence, } from '../../daemon/binding/port-presence.js';
|
|
25
|
+
import { daemonSpawnArgs } from '../daemon-start-options.js';
|
|
26
|
+
import { fetchStatus } from '../../daemon/binding/daemon-status-probe.js';
|
|
27
|
+
import { runKill } from '../cli-kill.js';
|
|
28
|
+
export function handleServe(parsed) {
|
|
29
|
+
void serveWithHonestExit(parsed);
|
|
30
|
+
}
|
|
31
|
+
/**
|
|
32
|
+
* Report the BIND, not the spawn.
|
|
33
|
+
*
|
|
34
|
+
* `serve` used to log `reticle_daemon_spawned` and exit 0 unconditionally, while the child died at
|
|
35
|
+
* `reticle_daemon_start_failed` with an EADDRINUSE nothing joined back to the parent. Three surfaces
|
|
36
|
+
* then disagreed about what was true and none of them named the port.
|
|
37
|
+
*
|
|
38
|
+
* Two changes, both about saying what is actually the case: refuse up front when the port is held by
|
|
39
|
+
* something that is not a Reticle daemon (a spawn there cannot succeed), and after spawning, wait
|
|
40
|
+
* for the daemon to actually answer before claiming it started.
|
|
41
|
+
*/
|
|
42
|
+
async function serveWithHonestExit(parsed) {
|
|
43
|
+
const presence = await probePresence(parsed.port, {
|
|
44
|
+
tcpOpen: probeDaemon,
|
|
45
|
+
status: fetchStatus,
|
|
46
|
+
});
|
|
47
|
+
if (presence === PortPresence.DAEMON) {
|
|
48
|
+
// A daemon that is already up was started with ITS flags, not these — `serve` only attaches.
|
|
49
|
+
// Exiting 0 here regardless is how `--http-port` came to be accepted and ignored (#687): the
|
|
50
|
+
// running daemon kept serving whatever it was started with, and the flag vanished without a
|
|
51
|
+
// word. Ask the daemon which verify port it actually serves and refuse when it is not the one
|
|
52
|
+
// requested — a flag that cannot be honoured must say so, not report success.
|
|
53
|
+
if (parsed.http) {
|
|
54
|
+
const mismatch = verifyEndpointMismatch(await fetchStatus(parsed.port), parsed.httpPort ?? RETICLE_VERIFY_DEFAULT_PORT);
|
|
55
|
+
if (mismatch !== undefined) {
|
|
56
|
+
log('reticle_daemon_start_refused', { port: parsed.port, reason: mismatch });
|
|
57
|
+
process.stderr.write(`${mismatch}\n`);
|
|
58
|
+
process.exit(1);
|
|
59
|
+
return;
|
|
60
|
+
}
|
|
61
|
+
}
|
|
62
|
+
log('reticle_daemon_already_running', { port: parsed.port });
|
|
63
|
+
return;
|
|
64
|
+
}
|
|
65
|
+
if (presence === PortPresence.FOREIGN) {
|
|
66
|
+
log('reticle_daemon_start_refused', {
|
|
67
|
+
port: parsed.port,
|
|
68
|
+
presence,
|
|
69
|
+
reason: describePresence(presence, parsed.port),
|
|
70
|
+
});
|
|
71
|
+
process.stderr.write(`${describePresence(presence, parsed.port)}\n`);
|
|
72
|
+
process.exit(1);
|
|
73
|
+
return;
|
|
74
|
+
}
|
|
75
|
+
const scriptPath = process.argv[1];
|
|
76
|
+
if (scriptPath === undefined) {
|
|
77
|
+
log('reticle_serve_no_script', {});
|
|
78
|
+
process.exit(1);
|
|
79
|
+
return;
|
|
80
|
+
}
|
|
81
|
+
const daemonArgs = daemonSpawnArgs(parsed);
|
|
82
|
+
const startupStartedAt = Date.now();
|
|
83
|
+
spawnDaemon(process.execPath, scriptPath, daemonArgs, parsed.port);
|
|
84
|
+
// The child binds asynchronously and, when it cannot, exits 1 long after this process would have
|
|
85
|
+
// reported success. Wait for it to ANSWER — `/status` responding is the only evidence a daemon
|
|
86
|
+
// exists that does not come from the pid file the child may never have earned.
|
|
87
|
+
const bound = await waitForPresence(parsed.port, PortPresence.DAEMON, SERVE_BIND_TIMEOUT_MS);
|
|
88
|
+
if (!bound) {
|
|
89
|
+
const settled = await probePresence(parsed.port, { tcpOpen: probeDaemon, status: fetchStatus });
|
|
90
|
+
const startupCause = readDaemonStartupCause(logPath(parsed.port), startupStartedAt);
|
|
91
|
+
log('reticle_daemon_start_failed_parent', {
|
|
92
|
+
port: parsed.port,
|
|
93
|
+
presence: settled,
|
|
94
|
+
reason: describePresence(settled, parsed.port),
|
|
95
|
+
log: logPath(parsed.port),
|
|
96
|
+
stateDir: stateDirProblem(reticleStateHome()) === undefined ? 'writable' : 'unwritable',
|
|
97
|
+
...(startupCause === undefined ? {} : { cause: startupCause }),
|
|
98
|
+
});
|
|
99
|
+
// Name the CAUSE when we can see it. An unwritable state directory is a first-run failure mode
|
|
100
|
+
// (locked-down home, read-only container mount, managed profile) that produced only "nothing is
|
|
101
|
+
// listening" — the symptom restated — and then pointed at a log INSIDE that directory, which
|
|
102
|
+
// cannot exist. Reproduced with `chmod 555 ~/.reticle`.
|
|
103
|
+
const dirProblem = stateDirProblem(reticleStateHome());
|
|
104
|
+
process.stderr.write(dirProblem === undefined
|
|
105
|
+
? startupCause === undefined
|
|
106
|
+
? `the daemon did not come up on :${String(parsed.port)} — ${describePresence(settled, parsed.port)}\n` +
|
|
107
|
+
`see ${logPath(parsed.port)}\n`
|
|
108
|
+
: `the daemon did not come up on :${String(parsed.port)}.\n${startupCause}\n` +
|
|
109
|
+
`see ${logPath(parsed.port)}\n`
|
|
110
|
+
: `the daemon did not come up on :${String(parsed.port)}.\n${dirProblem}\n`);
|
|
111
|
+
process.exit(1);
|
|
112
|
+
return;
|
|
113
|
+
}
|
|
114
|
+
log('reticle_daemon_spawned', { port: parsed.port, ...(parsed.http ? { http: true } : {}) });
|
|
115
|
+
}
|
|
116
|
+
/** How long `serve` waits for the child to bind. Generous: a cold daemon start is seconds. */
|
|
117
|
+
const SERVE_BIND_TIMEOUT_MS = 15_000;
|
|
118
|
+
const PRESENCE_POLL_MS = 150;
|
|
119
|
+
/** How long a daemon gets to shut down on its own before `stop` stops asking politely. */
|
|
120
|
+
const GRACEFUL_STOP_MS = 5000;
|
|
121
|
+
/** How long after SIGKILL before we accept the process is not ours to kill and say so. */
|
|
122
|
+
const FORCED_STOP_MS = 2000;
|
|
123
|
+
async function waitForPresence(port, want, timeoutMs) {
|
|
124
|
+
const deadline = Date.now() + timeoutMs;
|
|
125
|
+
for (;;) {
|
|
126
|
+
const presence = await probePresence(port, { tcpOpen: probeDaemon, status: fetchStatus });
|
|
127
|
+
if (presence === want)
|
|
128
|
+
return true;
|
|
129
|
+
if (Date.now() >= deadline)
|
|
130
|
+
return false;
|
|
131
|
+
await new Promise((resolve) => setTimeout(resolve, PRESENCE_POLL_MS));
|
|
132
|
+
}
|
|
133
|
+
}
|
|
134
|
+
export function handleStop(port, quiet) {
|
|
135
|
+
const pid = readPid(port);
|
|
136
|
+
if (null === pid || !isAlive(pid)) {
|
|
137
|
+
removePid(port);
|
|
138
|
+
if (!quiet)
|
|
139
|
+
log('reticle_daemon_not_running', { port });
|
|
140
|
+
return;
|
|
141
|
+
}
|
|
142
|
+
process.kill(pid, 'SIGTERM');
|
|
143
|
+
const started = Date.now();
|
|
144
|
+
let escalated = false;
|
|
145
|
+
const poll = setInterval(() => {
|
|
146
|
+
if (!isAlive(pid)) {
|
|
147
|
+
clearInterval(poll);
|
|
148
|
+
removePid(port);
|
|
149
|
+
if (!quiet)
|
|
150
|
+
log('reticle_daemon_stopped', { port, pid, ...(escalated ? { escalated } : {}) });
|
|
151
|
+
return;
|
|
152
|
+
}
|
|
153
|
+
const waited = Date.now() - started;
|
|
154
|
+
// A daemon that ignores SIGTERM is WEDGED — which is the one situation `stop` exists for. Giving
|
|
155
|
+
// up here left the port held, the pid file stale, and a human running `kill -9` by hand at the
|
|
156
|
+
// first two minutes of setup. Escalate instead: graceful first, then unconditional.
|
|
157
|
+
if (!escalated && waited > GRACEFUL_STOP_MS) {
|
|
158
|
+
escalated = true;
|
|
159
|
+
try {
|
|
160
|
+
process.kill(pid, 'SIGKILL');
|
|
161
|
+
}
|
|
162
|
+
catch {
|
|
163
|
+
// Already gone between the liveness check and here — the next tick reports it stopped.
|
|
164
|
+
}
|
|
165
|
+
return;
|
|
166
|
+
}
|
|
167
|
+
if (waited > GRACEFUL_STOP_MS + FORCED_STOP_MS) {
|
|
168
|
+
clearInterval(poll);
|
|
169
|
+
// Surviving SIGKILL means the pid is not ours to kill (permissions) or is an unkillable
|
|
170
|
+
// zombie. Neither is retryable, and both need the pid named so a human can act on it.
|
|
171
|
+
if (!quiet)
|
|
172
|
+
log('reticle_daemon_stop_timeout', { port, pid, escalated });
|
|
173
|
+
process.exit(1);
|
|
174
|
+
}
|
|
175
|
+
}, 100);
|
|
176
|
+
}
|
|
177
|
+
/**
|
|
178
|
+
* `reticle restart` — free the port, then put a daemon back on it and prove it bound.
|
|
179
|
+
*
|
|
180
|
+
* The two halves are reported separately on purpose. A restart that killed the old daemon and then
|
|
181
|
+
* could not start a new one leaves the user in a different place from one that could not kill
|
|
182
|
+
* anything at all, and a single `ok: false` cannot tell them which happened.
|
|
183
|
+
*/
|
|
184
|
+
export async function handleRestart(port, force) {
|
|
185
|
+
const freed = await runKill(port, force);
|
|
186
|
+
if (!freed) {
|
|
187
|
+
log('reticle_restart_aborted', {
|
|
188
|
+
port,
|
|
189
|
+
reason: 'the port was not freed, so nothing was started — the old holder is still there',
|
|
190
|
+
});
|
|
191
|
+
process.exit(1);
|
|
192
|
+
return;
|
|
193
|
+
}
|
|
194
|
+
// Reuses `serve`'s path, which waits for the daemon to ANSWER before claiming it started and exits
|
|
195
|
+
// non-zero when it does not. A restart that reports success for a daemon that never bound is the
|
|
196
|
+
// same lie `serve` already stopped telling.
|
|
197
|
+
await serveWithHonestExit({ port, headless: true, http: false });
|
|
198
|
+
}
|
|
@@ -17,7 +17,7 @@ import { resolveMcpPort, daemonProjectAt } from '../daemon/daemon-resolve.js';
|
|
|
17
17
|
import { daemonSpawnArgs } from './daemon-start-options.js';
|
|
18
18
|
import { WakeAction, decideWake } from '../daemon/wake-decision.js';
|
|
19
19
|
import { pickDaemonPortToBind } from '../daemon/binding/free-port.js';
|
|
20
|
-
import { fetchStatus } from '
|
|
20
|
+
import { fetchStatus } from '../daemon/binding/daemon-status-probe.js';
|
|
21
21
|
import { migrateApprovals } from '../setup/approval-migration.js';
|
|
22
22
|
import { ReticleEnv } from '@reticlehq/core';
|
|
23
23
|
import { agentIo } from '../setup/agent-io.js';
|
|
@@ -80,7 +80,7 @@ export async function handleMcp(opts) {
|
|
|
80
80
|
// swallows stderr, so wiring these to `log` meant a crash was handled and then unrecorded — the
|
|
81
81
|
// failure a user reports as "it disconnected" left nothing behind to read.
|
|
82
82
|
setProxyLogPort(port);
|
|
83
|
-
installProxyResilience(process, proxyLog);
|
|
83
|
+
const resilience = installProxyResilience(process, proxyLog);
|
|
84
84
|
/**
|
|
85
85
|
* Make sure a daemon is on the port, spawning one if not.
|
|
86
86
|
*
|
|
@@ -163,7 +163,9 @@ export async function handleMcp(opts) {
|
|
|
163
163
|
// crash. The proxy itself is unaffected — it has already installed its stdin reader and goes
|
|
164
164
|
// on serving from cache, waking a daemon on the next request — which is exactly why this
|
|
165
165
|
// must be logged as the expected condition it is rather than reported as a defect.
|
|
166
|
-
void startMcpProxy(port, ensure
|
|
166
|
+
void startMcpProxy(port, ensure, undefined, () => {
|
|
167
|
+
resilience.noteUsableSession();
|
|
168
|
+
}).catch((err) => {
|
|
167
169
|
log('reticle_mcp_proxy_first_connect_failed', {
|
|
168
170
|
port,
|
|
169
171
|
error: err instanceof Error ? err.message : String(err),
|