@reticlehq/server 3.4.0 → 3.6.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +1 -1
- package/SKILL.md +16 -8
- package/dist/chromium-launch-options.d.ts +3 -1
- package/dist/chromium-launch-options.js +2 -1
- package/dist/command/cli/adhoc-verdict.d.ts +1 -0
- package/dist/command/cli/adhoc-verdict.js +6 -3
- package/dist/command/cli/answers/open-note.d.ts +2 -0
- package/dist/command/cli/answers/open-note.js +22 -0
- package/dist/command/cli/auth/cloud-session.d.ts +4 -4
- package/dist/command/cli/auth/device-flow.d.ts +2 -2
- package/dist/command/cli/cli-flow-commands.js +4 -8
- package/dist/command/cli/cli-parse.d.ts +1 -1
- package/dist/command/cli/cli-parse.js +13 -11
- package/dist/command/cli/cli-verify.d.ts +1 -1
- package/dist/command/cli/cli-verify.js +35 -25
- package/dist/command/cli/cloud-cli.js +110 -12
- package/dist/command/cli/cloud-login.d.ts +9 -1
- package/dist/command/cli/cloud-login.js +43 -8
- package/dist/command/cli/demo/demo-page.js +11 -3
- package/dist/command/cli/doctor/browser/chromium-hint.d.ts +46 -0
- package/dist/command/cli/doctor/browser/chromium-hint.js +99 -1
- package/dist/command/cli/drive/drive-attach.d.ts +27 -2
- package/dist/command/cli/drive/drive-attach.js +71 -4
- package/dist/command/cli/drive/drive-command.d.ts +2 -1
- package/dist/command/cli/drive/drive-command.js +13 -7
- package/dist/command/cli/lifecycle/daemon-lifecycle.js +4 -1
- package/dist/command/cli/ports/port-holder.d.ts +10 -0
- package/dist/command/cli/ports/port-holder.js +24 -0
- package/dist/command/cli/setup-mcp-cli.js +15 -5
- package/dist/command/cli/tutorial.d.ts +2 -2
- package/dist/command/cli/tutorial.js +6 -9
- package/dist/command/cli.d.ts +6 -0
- package/dist/command/cli.js +45 -23
- package/dist/command/daemon/binding/daemon-status-probe.d.ts +2 -0
- package/dist/command/daemon/binding/daemon-status-probe.js +1 -0
- package/dist/command/daemon/daemon-resolve.d.ts +14 -0
- package/dist/command/daemon/daemon-resolve.js +33 -2
- package/dist/command/setup/agent-writer.d.ts +1 -1
- package/dist/command/setup/agent-writer.js +3 -2
- package/dist/command/setup/bringup/connect-lease.d.ts +1 -0
- package/dist/command/setup/bringup/connect-lease.js +1 -0
- package/dist/command/setup/bringup/dev-server-wait.d.ts +10 -0
- package/dist/command/setup/bringup/dev-server-wait.js +20 -1
- package/dist/command/setup/init/init-port.d.ts +4 -0
- package/dist/command/setup/init/init-port.js +10 -0
- package/dist/command/setup/init/init-runtime.d.ts +10 -0
- package/dist/command/setup/init/init-runtime.js +71 -9
- package/dist/command/setup/node-effects.js +8 -2
- package/dist/command/setup/remaining-steps.js +2 -1
- package/dist/command/setup/run-setup.d.ts +7 -0
- package/dist/command/setup/run-setup.js +38 -25
- package/dist/command/setup/setup-command.d.ts +2 -1
- package/dist/command/setup/setup-command.js +2 -0
- package/dist/command/setup/setup-mcp.d.ts +2 -0
- package/dist/command/setup/setup-mcp.js +14 -5
- 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/command/version/version-skew.js +11 -2
- package/dist/features/crawl/crawl-tools.js +4 -0
- package/dist/features/crawl/crawl.d.ts +6 -0
- package/dist/features/crawl/crawl.js +9 -1
- package/dist/features/exhaust/ledger.d.ts +7 -0
- package/dist/features/exhaust/ledger.js +35 -9
- package/dist/features/harness/app-findings.d.ts +2 -0
- package/dist/features/harness/app-findings.js +52 -0
- package/dist/features/harness/drive-report.d.ts +27 -1
- package/dist/features/harness/drive-report.js +162 -11
- package/dist/features/harness/drivers.d.ts +10 -14
- package/dist/features/harness/drivers.js +10 -14
- package/dist/features/harness/fill-values.d.ts +3 -69
- package/dist/features/harness/fill-values.js +3 -133
- package/dist/features/harness/goals.d.ts +21 -0
- package/dist/features/harness/goals.js +77 -0
- package/dist/features/harness/harness.d.ts +21 -0
- package/dist/features/harness/harness.js +69 -2
- package/dist/features/harness/platform/drive-target.d.ts +30 -0
- package/dist/features/harness/platform/drive-target.js +81 -0
- package/dist/features/harness/platform/local-apps.d.ts +94 -0
- package/dist/features/harness/platform/local-apps.js +204 -0
- package/dist/features/harness/platform/remote-drive.d.ts +180 -0
- package/dist/features/harness/platform/remote-drive.js +365 -0
- package/dist/features/harness/platform/script.d.ts +48 -0
- package/dist/features/harness/platform/script.js +128 -0
- package/dist/features/harness/platform/server-driver.d.ts +41 -0
- package/dist/features/harness/platform/server-driver.js +179 -0
- package/dist/features/harness/platform-config.d.ts +7 -0
- package/dist/features/harness/platform-config.js +14 -2
- package/dist/features/harness/script/app-controls.d.ts +5 -0
- package/dist/features/harness/script/app-controls.js +40 -0
- package/dist/features/harness/script/lane-ids.d.ts +14 -0
- package/dist/features/harness/script/lane-ids.js +16 -0
- package/dist/features/harness/script/script-run.d.ts +41 -0
- package/dist/features/harness/script/script-run.js +299 -0
- package/dist/features/license/license.d.ts +2 -2
- package/dist/features/visual/visual-tools.d.ts +10 -1
- package/dist/features/visual/visual-tools.js +40 -3
- package/dist/hooks/driven-by.d.ts +4 -0
- package/dist/hooks/driven-by.js +16 -0
- package/dist/hooks/hook-commands.js +15 -4
- package/dist/hooks/tool-log.d.ts +9 -0
- package/dist/hooks/tool-log.js +29 -0
- package/dist/index.d.ts +1 -0
- package/dist/index.js +91 -89
- package/dist/judgement/capsule/capsule-store.d.ts +4 -4
- package/dist/judgement/runs/artifact/build-verification-run.js +6 -0
- package/dist/judgement/runs/artifact/replay-mapping.d.ts +1 -1
- package/dist/judgement/runs/artifact/replay-mapping.js +16 -6
- package/dist/judgement/runs/artifact/run-store.d.ts +1 -1
- package/dist/judgement/runs/artifact/run-store.js +26 -1
- package/dist/judgement/runs/drive-run.d.ts +22 -0
- package/dist/judgement/runs/drive-run.js +99 -1
- package/dist/judgement/runs/repair-prompt.js +1 -1
- package/dist/judgement/runs/reticle-runner.js +8 -1
- package/dist/judgement/runs/verification-sync.d.ts +3 -2
- package/dist/judgement/runs/verification-sync.js +4 -3
- package/dist/language/flows/change/gate.d.ts +10 -0
- package/dist/language/flows/change/gate.js +22 -0
- package/dist/language/flows/decision.js +34 -0
- package/dist/language/flows/flow-anchor.d.ts +54 -8
- package/dist/language/flows/flow-anchor.js +88 -3
- package/dist/language/flows/flow-cross-step.d.ts +15 -0
- package/dist/language/flows/flow-cross-step.js +21 -0
- package/dist/language/flows/flow-learning.js +8 -11
- package/dist/language/flows/flow-mutate-tools.js +5 -3
- package/dist/language/flows/flow-replay-run.d.ts +23 -9
- package/dist/language/flows/flow-replay-run.js +101 -94
- package/dist/language/flows/flow-replay.d.ts +23 -1
- package/dist/language/flows/flow-replay.js +92 -19
- package/dist/language/flows/flow-scope.js +5 -1
- package/dist/language/flows/flow-step-runners.d.ts +20 -2
- package/dist/language/flows/flow-step-runners.js +236 -42
- package/dist/language/flows/flow-store-for-session.d.ts +2 -2
- package/dist/language/flows/flow-store-for-session.js +2 -2
- package/dist/language/flows/flow-tools.js +58 -34
- package/dist/language/flows/flows.js +5 -2
- package/dist/language/flows/heal-run.js +6 -4
- package/dist/language/flows/recording/flake.d.ts +2 -2
- package/dist/language/flows/recording/tape/recordings.d.ts +30 -4
- package/dist/language/flows/recording/tape/recordings.js +40 -8
- package/dist/language/flows/replay-from-hud.d.ts +20 -0
- package/dist/language/flows/replay-from-hud.js +51 -0
- package/dist/language/flows/replay.d.ts +9 -5
- package/dist/language/flows/replay.js +27 -9
- package/dist/language/flows/suite/suite-flakes.js +2 -0
- package/dist/language/flows/suite-selection.d.ts +2 -15
- package/dist/language/flows/suite-selection.js +4 -3
- package/dist/language/flows/verify-change-tools.js +8 -4
- package/dist/launch-chromium.d.ts +27 -5
- package/dist/launch-chromium.js +38 -8
- package/dist/memory/cloud/cloud-config.d.ts +12 -0
- package/dist/memory/cloud/cloud-config.js +15 -0
- package/dist/memory/cloud/cloud-sync.d.ts +17 -14
- package/dist/memory/cloud/cloud-sync.js +12 -1
- package/dist/memory/cloud/harness-config.d.ts +26 -1
- package/dist/memory/cloud/harness-config.js +99 -4
- package/dist/memory/cloud/hud-notices-source.d.ts +21 -0
- package/dist/memory/cloud/hud-notices-source.js +128 -0
- package/dist/memory/cloud/memory-scope.d.ts +144 -0
- package/dist/memory/cloud/memory-scope.js +205 -0
- package/dist/memory/cloud/sync-cycle.d.ts +21 -3
- package/dist/memory/cloud/sync-cycle.js +45 -5
- package/dist/memory/cloud/sync-daemon.js +7 -5
- package/dist/memory/cloud/sync-disk.d.ts +1 -1
- package/dist/memory/cloud/sync-disk.js +5 -8
- package/dist/memory/impact/account-watch.d.ts +19 -0
- package/dist/memory/impact/account-watch.js +34 -0
- package/dist/memory/impact/impact-recorder.d.ts +8 -0
- package/dist/memory/impact/impact-recorder.js +12 -2
- package/dist/memory/impact/impact-store.d.ts +16 -0
- package/dist/memory/impact/impact-store.js +142 -26
- package/dist/memory/intent/intent-shard.d.ts +2 -2
- package/dist/memory/intent/intent-tools.js +88 -5
- package/dist/memory/journal/envelope.d.ts +16 -16
- package/dist/memory/journal/journal-query.d.ts +14 -0
- package/dist/memory/journal/journal-query.js +15 -0
- package/dist/memory/journal/journal-recorder.js +7 -1
- package/dist/memory/journal/on-disk/retention.d.ts +3 -0
- package/dist/memory/journal/on-disk/retention.js +5 -0
- package/dist/memory/journal/on-disk/startup-maintenance.js +3 -1
- package/dist/memory/journal/on-disk/workspace-tiers.js +7 -4
- package/dist/memory/journal/session-end.js +6 -4
- package/dist/memory/project/artifact-root-resolver.d.ts +36 -1
- package/dist/memory/project/artifact-root-resolver.js +134 -26
- package/dist/memory/project/artifact-root.d.ts +42 -5
- package/dist/memory/project/artifact-root.js +87 -8
- package/dist/memory/project/dir/reticle-dir.d.ts +4 -2
- package/dist/memory/project/dir/reticle-dir.js +3 -1
- package/dist/memory/project/project-tools.js +22 -3
- package/dist/memory/project/session-root.d.ts +24 -1
- package/dist/memory/project/session-root.js +41 -5
- package/dist/memory/project/sync-status.d.ts +39 -0
- package/dist/memory/project/sync-status.js +90 -0
- package/dist/memory/recall/memory-tools.js +2 -0
- package/dist/memory/recall/project-memory.d.ts +19 -0
- package/dist/memory/recall/project-memory.js +45 -6
- package/dist/portal/bridge/bridge.d.ts +7 -4
- package/dist/portal/bridge/bridge.js +25 -3
- package/dist/portal/bridge/pairing-token.d.ts +9 -0
- package/dist/portal/bridge/pairing-token.js +17 -0
- package/dist/portal/input/real-input.d.ts +81 -10
- package/dist/portal/input/real-input.js +207 -22
- package/dist/portal/input/viewport-tools.js +19 -9
- package/dist/portal/pool/browser-pool.d.ts +22 -9
- package/dist/portal/pool/browser-pool.js +70 -35
- package/dist/portal/pool/playwright-launcher.d.ts +13 -0
- package/dist/portal/pool/playwright-launcher.js +17 -1
- package/dist/portal/pool/pool-contract.d.ts +7 -4
- package/dist/portal/session/command-payload.d.ts +8 -0
- package/dist/portal/session/command-payload.js +19 -0
- package/dist/portal/session/dev-server/dev-command.d.ts +12 -0
- package/dist/portal/session/dev-server/dev-command.js +61 -12
- package/dist/portal/session/facts/departure-note.d.ts +22 -0
- package/dist/portal/session/facts/departure-note.js +47 -0
- package/dist/portal/session/facts/session-replaced.d.ts +5 -0
- package/dist/portal/session/facts/session-replaced.js +5 -0
- package/dist/portal/session/first-run-wiring.d.ts +42 -0
- package/dist/portal/session/first-run-wiring.js +132 -0
- package/dist/portal/session/human/notes-ledger.d.ts +20 -0
- package/dist/portal/session/human/notes-ledger.js +55 -0
- package/dist/portal/session/human/review-store.d.ts +12 -0
- package/dist/portal/session/human/review-store.js +18 -10
- package/dist/portal/session/live-control-tools.d.ts +0 -13
- package/dist/portal/session/live-control-tools.js +28 -3
- package/dist/portal/session/no-session-diagnosis.d.ts +7 -0
- package/dist/portal/session/no-session-diagnosis.js +15 -0
- package/dist/portal/session/no-session-next-action.d.ts +11 -0
- package/dist/portal/session/no-session-next-action.js +31 -5
- package/dist/portal/session/no-session-watch.d.ts +14 -1
- package/dist/portal/session/no-session-watch.js +63 -10
- package/dist/portal/session/presence/session-mirror.js +3 -0
- package/dist/portal/session/serving-directory.d.ts +27 -0
- package/dist/portal/session/serving-directory.js +45 -0
- package/dist/portal/session/session-health.d.ts +7 -0
- package/dist/portal/session/session-health.js +6 -0
- package/dist/portal/session/session-info.d.ts +8 -1
- package/dist/portal/session/session-info.js +6 -0
- package/dist/portal/session/session-manager.d.ts +21 -0
- package/dist/portal/session/session-manager.js +44 -8
- package/dist/portal/session/session-reaper.js +4 -1
- package/dist/portal/session/session-successor.d.ts +5 -0
- package/dist/portal/session/session-tools.js +26 -3
- package/dist/portal/session/session.d.ts +12 -10
- package/dist/portal/session/session.js +30 -31
- package/dist/start-options.d.ts +6 -0
- package/dist/status-payload.d.ts +10 -0
- package/dist/status-payload.js +23 -0
- package/dist/surface/http-server.d.ts +1 -1
- package/dist/surface/http-server.js +15 -4
- package/dist/surface/mcp/mcp-proxy.d.ts +5 -0
- package/dist/surface/mcp/mcp-proxy.js +36 -5
- package/dist/surface/mcp/mcp.d.ts +11 -6
- package/dist/surface/mcp/mcp.js +28 -14
- package/dist/surface/mcp/proxy/proxy-log.js +3 -5
- package/dist/surface/mcp/server-instructions.d.ts +4 -2
- package/dist/surface/mcp/server-instructions.js +14 -4
- package/dist/surface/mcp/shared-params.js +2 -2
- package/dist/surface/tools/act/navigation/navigate-arrival.d.ts +8 -0
- package/dist/surface/tools/act/navigation/navigate-arrival.js +56 -2
- package/dist/surface/tools/act/settle-in-flight.js +25 -3
- package/dist/surface/tools/act-sequence-tool.js +2 -2
- package/dist/surface/tools/act-tools.js +25 -9
- package/dist/surface/tools/assert/assert-verdict.d.ts +1 -1
- package/dist/surface/tools/assert/assert-verdict.js +17 -2
- package/dist/surface/tools/browser-tools.js +8 -1
- package/dist/surface/tools/chat-drives.d.ts +26 -0
- package/dist/surface/tools/chat-drives.js +165 -0
- package/dist/surface/tools/dynamic-tools.js +29 -2
- package/dist/surface/tools/empty-session-list.d.ts +41 -0
- package/dist/surface/tools/empty-session-list.js +80 -0
- package/dist/surface/tools/error-recovery.d.ts +6 -0
- package/dist/surface/tools/error-recovery.js +38 -6
- package/dist/surface/tools/explore-tools.d.ts +10 -1
- package/dist/surface/tools/explore-tools.js +82 -13
- package/dist/surface/tools/feedback-tools.js +14 -0
- package/dist/surface/tools/harness-explore.d.ts +96 -14
- package/dist/surface/tools/harness-explore.js +227 -202
- package/dist/surface/tools/harness-plan.d.ts +12 -0
- package/dist/surface/tools/harness-plan.js +31 -0
- package/dist/surface/tools/harness-script.d.ts +8 -0
- package/dist/surface/tools/harness-script.js +342 -0
- package/dist/surface/tools/harness-toolset.d.ts +14 -1
- package/dist/surface/tools/harness-toolset.js +77 -6
- package/dist/surface/tools/invoke-tool.js +64 -9
- package/dist/surface/tools/lease-seed.d.ts +29 -0
- package/dist/surface/tools/lease-seed.js +123 -0
- package/dist/surface/tools/lease-tools.d.ts +3 -23
- package/dist/surface/tools/lease-tools.js +30 -131
- package/dist/surface/tools/live-call-text.js +2 -2
- package/dist/surface/tools/next-step.d.ts +19 -0
- package/dist/surface/tools/next-step.js +159 -0
- package/dist/surface/tools/observe-tools.js +2 -4
- package/dist/surface/tools/platform-moment.d.ts +12 -0
- package/dist/surface/tools/platform-moment.js +168 -0
- package/dist/surface/tools/query-hint-schema.d.ts +7 -0
- package/dist/surface/tools/query-hint-schema.js +10 -0
- package/dist/surface/tools/read-tools.js +8 -2
- package/dist/surface/tools/real-input-attempt.js +96 -13
- package/dist/surface/tools/tool-kit.d.ts +25 -1
- package/dist/surface/tools/tool-kit.js +4 -6
- package/dist/surface/tools/tools.js +20 -48
- package/dist/telemetry/argument-shape.js +1 -1
- package/dist/telemetry/hud-metrics.d.ts +2 -0
- package/dist/telemetry/hud-metrics.js +17 -0
- package/dist/telemetry/onboarding-firsts.d.ts +2 -2
- package/dist/telemetry/onboarding-firsts.js +2 -2
- package/dist/telemetry/telemetry.d.ts +12 -0
- package/dist/telemetry/telemetry.js +43 -14
- package/dist/telemetry/verification-of.js +1 -0
- package/dist/wire-journal.js +4 -1
- package/docs/README.md +1 -1
- package/docs/actions.mdx +1 -2
- package/docs/agent-cheatsheet.md +1 -1
- package/docs/autodrive.md +29 -25
- package/docs/cli/cloud.mdx +6 -4
- package/docs/cli/doctor.mdx +6 -0
- package/docs/cli/drive.mdx +8 -2
- package/docs/cli/init.mdx +11 -13
- package/docs/cli/status.mdx +5 -1
- package/docs/cli/version.mdx +3 -3
- package/docs/cli.mdx +14 -8
- package/docs/desktop.mdx +5 -1
- package/docs/environment.mdx +3 -7
- package/docs/getting-started.md +14 -2
- package/docs/human-control.md +1 -2
- package/docs/index.mdx +4 -3
- package/docs/install-agentic.mdx +1 -1
- package/docs/install-manual.mdx +2 -2
- package/docs/packages/next.mdx +3 -0
- package/docs/packages/tauri.mdx +2 -2
- package/docs/packages/vite-plugin.mdx +24 -1
- package/docs/packages.mdx +1 -1
- package/docs/quickstart.mdx +23 -11
- package/docs/telemetry.md +1 -1
- package/docs/tools/act.mdx +1 -1
- package/docs/tools/session-and-feedback.mdx +9 -9
- package/docs/usage.md +5 -3
- package/docs/what-is-recorded.md +13 -3
- package/docs/what-is-reticle.mdx +3 -1
- package/package.json +8 -12
- package/dist/features/harness/driver.d.ts +0 -50
- package/dist/features/harness/driver.js +0 -236
- package/dist/features/harness/jev-driver.d.ts +0 -188
- package/dist/features/harness/jev-driver.js +0 -678
- package/dist/features/harness/openai-driver.d.ts +0 -46
- package/dist/features/harness/openai-driver.js +0 -194
- package/dist/memory/project/dir/fill-value-store.d.ts +0 -32
- package/dist/memory/project/dir/fill-value-store.js +0 -88
- package/docs/local-registry.md +0 -143
|
@@ -0,0 +1,39 @@
|
|
|
1
|
+
import type { CloudSyncState } from '../cloud/sync-cycle.js';
|
|
2
|
+
export declare const SyncStatus: {
|
|
3
|
+
readonly ON_PLATFORM: "on-platform";
|
|
4
|
+
readonly PENDING: "pending";
|
|
5
|
+
readonly REFUSED: "refused";
|
|
6
|
+
readonly LOCAL_ONLY: "local-only";
|
|
7
|
+
};
|
|
8
|
+
export type SyncStatus = (typeof SyncStatus)[keyof typeof SyncStatus];
|
|
9
|
+
export interface SyncSummary {
|
|
10
|
+
linked: boolean;
|
|
11
|
+
/** Runs on this machine. */
|
|
12
|
+
runs: number;
|
|
13
|
+
onPlatform: number;
|
|
14
|
+
pending: number;
|
|
15
|
+
/** The runs the platform refused in their current form, with its reason. */
|
|
16
|
+
refused: {
|
|
17
|
+
runId: string;
|
|
18
|
+
reason: string;
|
|
19
|
+
}[];
|
|
20
|
+
/** Refusals beyond the ones named. */
|
|
21
|
+
refusedMore: number;
|
|
22
|
+
lastPushAt?: number;
|
|
23
|
+
/** Why the last cycle failed, when it did. */
|
|
24
|
+
lastError?: string;
|
|
25
|
+
}
|
|
26
|
+
export declare function summarizeSync(input: {
|
|
27
|
+
linked: boolean;
|
|
28
|
+
runs: readonly {
|
|
29
|
+
runId: string;
|
|
30
|
+
payload: unknown;
|
|
31
|
+
}[];
|
|
32
|
+
state: CloudSyncState;
|
|
33
|
+
}): SyncSummary;
|
|
34
|
+
/** This project's summary, read from its `.reticle`. Never throws: an unreadable record is empty. */
|
|
35
|
+
export declare function readSyncSummary(reticleRoot: string, linked: boolean): SyncSummary;
|
|
36
|
+
/** One status for the whole project: the worst thing true of it. */
|
|
37
|
+
export declare function overallStatus(summary: SyncSummary): SyncStatus;
|
|
38
|
+
/** The summary in one sentence an agent can repeat to a person. */
|
|
39
|
+
export declare function describeSync(summary: SyncSummary, now: number): string;
|
|
@@ -0,0 +1,90 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Where this project's work stands with the Reticle platform, run by run.
|
|
3
|
+
*
|
|
4
|
+
* Read from the sync cycle's own record (`.reticle/cloud-state.json`): the content hash of every run
|
|
5
|
+
* the platform ACCEPTED, and the reason for every run it REFUSED. A run whose current content matches
|
|
6
|
+
* an accepted hash is on the platform; one whose current content was refused is refused, with the
|
|
7
|
+
* platform's own sentence; anything else is waiting for the next cycle. On a machine that is not
|
|
8
|
+
* linked, everything is local only.
|
|
9
|
+
*
|
|
10
|
+
* Nothing here talks to the network. It answers "did my work go up?" from what the machine already
|
|
11
|
+
* knows, so it can ride on every verdict without costing a request.
|
|
12
|
+
*/
|
|
13
|
+
import { diskSource, readCloudState } from '../cloud/sync-disk.js';
|
|
14
|
+
import { hashPayload } from '../cloud/sync-hash.js';
|
|
15
|
+
export const SyncStatus = {
|
|
16
|
+
ON_PLATFORM: 'on-platform',
|
|
17
|
+
PENDING: 'pending',
|
|
18
|
+
REFUSED: 'refused',
|
|
19
|
+
LOCAL_ONLY: 'local-only',
|
|
20
|
+
};
|
|
21
|
+
/** How many refusals a summary names before it only counts them. */
|
|
22
|
+
const MAX_NAMED_REFUSALS = 3;
|
|
23
|
+
export function summarizeSync(input) {
|
|
24
|
+
const { linked, runs, state } = input;
|
|
25
|
+
let onPlatform = 0;
|
|
26
|
+
let pending = 0;
|
|
27
|
+
const refused = [];
|
|
28
|
+
if (linked) {
|
|
29
|
+
for (const run of runs) {
|
|
30
|
+
const hash = hashPayload(run.payload);
|
|
31
|
+
const refusal = state.refusedRuns?.[run.runId];
|
|
32
|
+
if (state.sentRunHashes?.[run.runId] === hash)
|
|
33
|
+
onPlatform += 1;
|
|
34
|
+
else if (refusal !== undefined && refusal.payloadHash === hash)
|
|
35
|
+
refused.push({ runId: run.runId, reason: refusal.reason });
|
|
36
|
+
else
|
|
37
|
+
pending += 1;
|
|
38
|
+
}
|
|
39
|
+
}
|
|
40
|
+
return {
|
|
41
|
+
linked,
|
|
42
|
+
runs: runs.length,
|
|
43
|
+
onPlatform,
|
|
44
|
+
pending,
|
|
45
|
+
refused: refused.slice(0, MAX_NAMED_REFUSALS),
|
|
46
|
+
refusedMore: Math.max(0, refused.length - MAX_NAMED_REFUSALS),
|
|
47
|
+
...(state.lastPushAt === undefined ? {} : { lastPushAt: state.lastPushAt }),
|
|
48
|
+
...(state.lastError === undefined || 0 === state.lastError.length
|
|
49
|
+
? {}
|
|
50
|
+
: { lastError: state.lastError }),
|
|
51
|
+
};
|
|
52
|
+
}
|
|
53
|
+
/** This project's summary, read from its `.reticle`. Never throws: an unreadable record is empty. */
|
|
54
|
+
export function readSyncSummary(reticleRoot, linked) {
|
|
55
|
+
let runs = [];
|
|
56
|
+
try {
|
|
57
|
+
runs = diskSource(reticleRoot).runs();
|
|
58
|
+
}
|
|
59
|
+
catch {
|
|
60
|
+
runs = [];
|
|
61
|
+
}
|
|
62
|
+
return summarizeSync({ linked, runs, state: readCloudState(reticleRoot) });
|
|
63
|
+
}
|
|
64
|
+
/** One status for the whole project: the worst thing true of it. */
|
|
65
|
+
export function overallStatus(summary) {
|
|
66
|
+
if (!summary.linked)
|
|
67
|
+
return SyncStatus.LOCAL_ONLY;
|
|
68
|
+
if (0 < summary.refused.length)
|
|
69
|
+
return SyncStatus.REFUSED;
|
|
70
|
+
if (0 < summary.pending || summary.lastError !== undefined)
|
|
71
|
+
return SyncStatus.PENDING;
|
|
72
|
+
return SyncStatus.ON_PLATFORM;
|
|
73
|
+
}
|
|
74
|
+
/** The summary in one sentence an agent can repeat to a person. */
|
|
75
|
+
export function describeSync(summary, now) {
|
|
76
|
+
if (!summary.linked)
|
|
77
|
+
return `${String(summary.runs)} run(s) exist only on this machine; nothing is synced until the project is linked (npx @reticlehq/server connect).`;
|
|
78
|
+
const ago = summary.lastPushAt === undefined
|
|
79
|
+
? 'nothing pushed yet'
|
|
80
|
+
: `last push ${String(Math.max(0, Math.round((now - summary.lastPushAt) / 1000)))}s ago`;
|
|
81
|
+
const parts = [`${String(summary.onPlatform)} of ${String(summary.runs)} run(s) on the platform`];
|
|
82
|
+
if (0 < summary.pending)
|
|
83
|
+
parts.push(`${String(summary.pending)} waiting for the next push`);
|
|
84
|
+
const named = summary.refused.map((r) => `${r.runId}: ${r.reason}`);
|
|
85
|
+
if (0 < named.length)
|
|
86
|
+
parts.push(`refused by the platform: ${named.join('; ')}${0 < summary.refusedMore ? ` (+${String(summary.refusedMore)} more)` : ''}`);
|
|
87
|
+
if (summary.lastError !== undefined)
|
|
88
|
+
parts.push(`the last push failed: ${summary.lastError}`);
|
|
89
|
+
return `${parts.join('; ')} (${ago}).`;
|
|
90
|
+
}
|
|
@@ -30,6 +30,8 @@ const ADVICE = {
|
|
|
30
30
|
[MemoryUnavailable.NOT_LINKED]: 'this project is not linked to a Reticle workspace, so there is no shared memory to read. Run `npx @reticlehq/server login` in the project to link it.',
|
|
31
31
|
[MemoryUnavailable.DISABLED]: 'this project is linked but memory sync is turned off in .reticle/cloud.json, so nothing is being shared. Enable `sync.memory` to use it.',
|
|
32
32
|
[MemoryUnavailable.UNREACHABLE]: 'the workspace could not be reached, or refused the key. Nothing is wrong with this project — try again, or check the link with `reticle status`.',
|
|
33
|
+
[MemoryUnavailable.UNVERIFIED]: 'the server answered, but the answer could not be shown to be about THIS project, so none of it is being reported. This is not an empty corpus: do NOT conclude this project knows nothing. Check the link with `reticle status`, and retry once the workspace is reachable.',
|
|
34
|
+
[MemoryUnavailable.UNREADABLE]: 'the workspace answered about THIS project, but the body was a shape this build cannot read entries out of. Retrying will not change it — this is not an empty corpus and not a link problem. Check the workspace version, and that `reticle memory` from the same terminal returns a `{ projectId, entries }` body.',
|
|
33
35
|
};
|
|
34
36
|
export const MEMORY_TOOLS = [
|
|
35
37
|
{
|
|
@@ -7,6 +7,25 @@ export declare const MemoryUnavailable: {
|
|
|
7
7
|
readonly DISABLED: "memory-sync-disabled";
|
|
8
8
|
/** The server was reached and said no, or could not be reached at all. */
|
|
9
9
|
readonly UNREACHABLE: "unreachable";
|
|
10
|
+
/**
|
|
11
|
+
* The server answered, and the answer could not be shown to be about THIS project: its envelope
|
|
12
|
+
* named another one, or named none at all.
|
|
13
|
+
*
|
|
14
|
+
* Its own reason rather than an empty list, because the two are opposite facts about the corpus.
|
|
15
|
+
* "This project knows nothing" is something an agent can act on; "we cannot tell whose knowledge
|
|
16
|
+
* this is" is something it must not, and an empty list reports the first while meaning the second.
|
|
17
|
+
*/
|
|
18
|
+
readonly UNVERIFIED: "unverified";
|
|
19
|
+
/**
|
|
20
|
+
* The server answered about THIS project, and the body is a shape this build cannot read entries
|
|
21
|
+
* out of — no `entries` field, or one that is not a list.
|
|
22
|
+
*
|
|
23
|
+
* Apart from `UNREACHABLE` on purpose. That reason's advice is "try again, or check the link", and
|
|
24
|
+
* neither applies: the workspace WAS reached, its answer IS this project's, and the shape will be
|
|
25
|
+
* the same on the second attempt. Sending an agent to retry it costs a round trip and teaches it
|
|
26
|
+
* to distrust a link that is fine, which is what a reason is for.
|
|
27
|
+
*/
|
|
28
|
+
readonly UNREADABLE: "unreadable";
|
|
10
29
|
};
|
|
11
30
|
export type MemoryUnavailable = (typeof MemoryUnavailable)[keyof typeof MemoryUnavailable];
|
|
12
31
|
export interface KnownThing {
|
|
@@ -13,6 +13,7 @@
|
|
|
13
13
|
*/
|
|
14
14
|
import { cloudFetch } from '../cloud/cloud-sync.js';
|
|
15
15
|
import { resolveProjectCloud } from '../cloud/cloud-config.js';
|
|
16
|
+
import { isReadableMemoryScope, memoryReadUrl, scopedMemoryEntries, scopeOfMemoryResponse, } from '../cloud/memory-scope.js';
|
|
16
17
|
/** Named so a read from the tool surface is distinguishable from the CLI's and from replay's. */
|
|
17
18
|
const MCP_AGENT_ID = 'reticle-mcp';
|
|
18
19
|
/** Why a lookup returned nothing. Each one is a different thing for the agent to do next. */
|
|
@@ -23,6 +24,25 @@ export const MemoryUnavailable = {
|
|
|
23
24
|
DISABLED: 'memory-sync-disabled',
|
|
24
25
|
/** The server was reached and said no, or could not be reached at all. */
|
|
25
26
|
UNREACHABLE: 'unreachable',
|
|
27
|
+
/**
|
|
28
|
+
* The server answered, and the answer could not be shown to be about THIS project: its envelope
|
|
29
|
+
* named another one, or named none at all.
|
|
30
|
+
*
|
|
31
|
+
* Its own reason rather than an empty list, because the two are opposite facts about the corpus.
|
|
32
|
+
* "This project knows nothing" is something an agent can act on; "we cannot tell whose knowledge
|
|
33
|
+
* this is" is something it must not, and an empty list reports the first while meaning the second.
|
|
34
|
+
*/
|
|
35
|
+
UNVERIFIED: 'unverified',
|
|
36
|
+
/**
|
|
37
|
+
* The server answered about THIS project, and the body is a shape this build cannot read entries
|
|
38
|
+
* out of — no `entries` field, or one that is not a list.
|
|
39
|
+
*
|
|
40
|
+
* Apart from `UNREACHABLE` on purpose. That reason's advice is "try again, or check the link", and
|
|
41
|
+
* neither applies: the workspace WAS reached, its answer IS this project's, and the shape will be
|
|
42
|
+
* the same on the second attempt. Sending an agent to retry it costs a round trip and teaches it
|
|
43
|
+
* to distrust a link that is fine, which is what a reason is for.
|
|
44
|
+
*/
|
|
45
|
+
UNREADABLE: 'unreadable',
|
|
26
46
|
};
|
|
27
47
|
/** Defensive: the wire is somebody else's server, so every field is checked before it is trusted. */
|
|
28
48
|
const asKnown = (raw) => {
|
|
@@ -58,9 +78,15 @@ export async function readProjectMemory(fs, root, home, env, opts) {
|
|
|
58
78
|
return { ok: false, reason: MemoryUnavailable.NOT_LINKED };
|
|
59
79
|
if (!cloud.policy.memory)
|
|
60
80
|
return { ok: false, reason: MemoryUnavailable.DISABLED };
|
|
61
|
-
|
|
81
|
+
// Scoped to the LINKED project, not to whatever the key happens to cover. A workspace can hold
|
|
82
|
+
// more than one repo, and a read that named no project could be answered with another one's
|
|
83
|
+
// established knowledge — indistinguishable from this project's, and acted on. See memory-scope.
|
|
84
|
+
const url = memoryReadUrl(cloud.config.url, {
|
|
85
|
+
projectId: cloud.projectId,
|
|
86
|
+
subject: opts.subject,
|
|
87
|
+
});
|
|
62
88
|
try {
|
|
63
|
-
const res = await cloudFetch(
|
|
89
|
+
const res = await cloudFetch(url, {
|
|
64
90
|
method: 'GET',
|
|
65
91
|
headers: {
|
|
66
92
|
authorization: `Bearer ${cloud.config.apiKey}`,
|
|
@@ -75,10 +101,23 @@ export async function readProjectMemory(fs, root, home, env, opts) {
|
|
|
75
101
|
// property yields the function, and every downstream field is undefined — a silent "the project
|
|
76
102
|
// knows nothing" that is indistinguishable from the honest empty case. It cost a live drive to
|
|
77
103
|
// find once already.
|
|
78
|
-
const body =
|
|
79
|
-
|
|
80
|
-
|
|
81
|
-
|
|
104
|
+
const body = await res.json();
|
|
105
|
+
// The envelope first, and it decides everything. The platform names the project on the response
|
|
106
|
+
// and NOT on each entry, so an entry-level filter alone keeps a whole sibling workspace while
|
|
107
|
+
// looking like it did something — see `memory-scope.ts`. A response that names another project,
|
|
108
|
+
// or names none, is UNVERIFIED: not empty, and not this project's.
|
|
109
|
+
const entries = scopedMemoryEntries(body, cloud.projectId);
|
|
110
|
+
if (entries === undefined) {
|
|
111
|
+
return {
|
|
112
|
+
ok: false,
|
|
113
|
+
reason: isReadableMemoryScope(scopeOfMemoryResponse(body, cloud.projectId))
|
|
114
|
+
? MemoryUnavailable.UNREADABLE // scoped fine, but no readable `entries` — a shape we do not know
|
|
115
|
+
: MemoryUnavailable.UNVERIFIED,
|
|
116
|
+
};
|
|
117
|
+
}
|
|
118
|
+
// The entry-level pass is redundant against the envelope check above and kept for a server that
|
|
119
|
+
// labels entries instead. `total` is counted AFTER it, so a truncated list still tells the truth
|
|
120
|
+
// about how many of THIS project's statements there were.
|
|
82
121
|
const known = entries.map(asKnown).filter((k) => k !== null);
|
|
83
122
|
return {
|
|
84
123
|
ok: true,
|
|
@@ -66,10 +66,11 @@ interface BridgeOptions {
|
|
|
66
66
|
*/
|
|
67
67
|
server?: http.Server;
|
|
68
68
|
/**
|
|
69
|
-
* The SDK-upgrade sentence to attach when a HELLO is skewed
|
|
70
|
-
*
|
|
69
|
+
* The SDK-upgrade sentence to attach when a HELLO is skewed, for the project that HELLO named.
|
|
70
|
+
* Injected so the daemon can name that project's packages and package manager; tests that omit
|
|
71
|
+
* it get the no-project fallback.
|
|
71
72
|
*/
|
|
72
|
-
sdkFix?: () => string;
|
|
73
|
+
sdkFix?: (projectId?: string) => string;
|
|
73
74
|
}
|
|
74
75
|
/**
|
|
75
76
|
* The browser-facing half of the relay: a localhost WebSocket server. Each connection
|
|
@@ -109,7 +110,9 @@ export declare class Bridge {
|
|
|
109
110
|
* Register a handler for the panel's harness switch. Optional for the same reason as the above: a
|
|
110
111
|
* bridge built without one ignores the request rather than refusing it.
|
|
111
112
|
*/
|
|
112
|
-
attachHarnessRequest(handler: (enabled: boolean) => void): void;
|
|
113
|
+
attachHarnessRequest(handler: (enabled: boolean, session: Session) => void): void;
|
|
114
|
+
/** Register a handler for the panel's Sign in. Optional, like the above: without one it is ignored. */
|
|
115
|
+
attachSigninRequest(handler: () => void): void;
|
|
113
116
|
/** Register a handler to run when a session connects. Additive — every handler runs. */
|
|
114
117
|
attachSessionCreate(handler: (session: Session) => void): void;
|
|
115
118
|
close(): Promise<void>;
|
|
@@ -11,7 +11,7 @@ import { tokensMatch } from './token-auth.js';
|
|
|
11
11
|
import { pairingTokenSource } from './pairing-token.js';
|
|
12
12
|
import { log } from '../../log.js';
|
|
13
13
|
import { getSessionMetrics } from '../../telemetry/session-metrics.js';
|
|
14
|
-
import { sessionReplacedReason } from '../session/facts/session-replaced.js';
|
|
14
|
+
import { SESSION_PARKED_REASON, sessionReplacedReason, } from '../session/facts/session-replaced.js';
|
|
15
15
|
import { describeSkew, sdkFix, SkewPair } from '../../command/version/version-skew.js';
|
|
16
16
|
import { noteVersionSkew } from '../../command/version/version-nudge.js';
|
|
17
17
|
import { protocolSkewReason } from './protocol-skew.js';
|
|
@@ -147,6 +147,10 @@ function harnessRequest(event) {
|
|
|
147
147
|
return false;
|
|
148
148
|
return undefined;
|
|
149
149
|
}
|
|
150
|
+
/** True when this event is the panel's Sign in. Pure boundary narrowing, like the above. */
|
|
151
|
+
function isSigninRequest(event) {
|
|
152
|
+
return event.type === EventType.HUMAN_CONTROL && event.data['kind'] === HumanControlKind.SIGNIN;
|
|
153
|
+
}
|
|
150
154
|
/** True when this event is the panel's "sync now" button. Pure boundary narrowing, like the above. */
|
|
151
155
|
function isSyncRequest(event) {
|
|
152
156
|
return event.type === EventType.HUMAN_CONTROL && event.data['kind'] === HumanControlKind.SYNC;
|
|
@@ -273,6 +277,7 @@ export class Bridge {
|
|
|
273
277
|
/** Wired by the daemon: push to the dashboard now, because somebody asked in the panel. */
|
|
274
278
|
#onSyncRequest;
|
|
275
279
|
#onHarnessRequest;
|
|
280
|
+
#onSigninRequest;
|
|
276
281
|
constructor(options) {
|
|
277
282
|
const host = options.host ?? LOOPBACK_HOST;
|
|
278
283
|
if ((options.token?.length ?? 0) > TRANSPORT_LIMITS.MAX_TOKEN_LENGTH) {
|
|
@@ -543,6 +548,11 @@ export class Bridge {
|
|
|
543
548
|
socket.close(WS_CLOSE.AUTH_FAILED[0], reason);
|
|
544
549
|
return;
|
|
545
550
|
}
|
|
551
|
+
// A window the agent detached stays detached across its own reloads. See SessionManager.park.
|
|
552
|
+
if (this.sessions.isParked(parsed.sessionId)) {
|
|
553
|
+
socket.close(1008, SESSION_PARKED_REASON);
|
|
554
|
+
return;
|
|
555
|
+
}
|
|
546
556
|
const existing = this.sessions.get(parsed.sessionId);
|
|
547
557
|
if (existing === undefined && this.sessions.count() >= this.#maxSessions) {
|
|
548
558
|
socket.close(...WS_CLOSE.SESSION_LIMIT);
|
|
@@ -608,6 +618,9 @@ export class Bridge {
|
|
|
608
618
|
// The daemon is the single judge of skew, and HELLO is where the page announces itself.
|
|
609
619
|
// Reported on the session (reticle_sessions) AND queued for the next tool result, because an
|
|
610
620
|
// agent driving a flow never calls reticle_sessions and would never learn.
|
|
621
|
+
// Read only if the page IS skewed: the fix reads the project's package.json and lockfile,
|
|
622
|
+
// and a compatible page (nearly every HELLO) should not pay for that on connect.
|
|
623
|
+
const fixForThisProject = () => this.#sdkFix(parsed.projectId);
|
|
611
624
|
const skew = describeSkew({
|
|
612
625
|
what: 'the page',
|
|
613
626
|
version: parsed.sdkVersion,
|
|
@@ -618,7 +631,9 @@ export class Bridge {
|
|
|
618
631
|
...(parsed.contractParts === undefined
|
|
619
632
|
? {}
|
|
620
633
|
: { contractParts: parsed.contractParts }),
|
|
621
|
-
fix
|
|
634
|
+
get fix() {
|
|
635
|
+
return fixForThisProject();
|
|
636
|
+
},
|
|
622
637
|
}, {
|
|
623
638
|
version: SERVER_VERSION,
|
|
624
639
|
contract: CONTRACT_FINGERPRINT,
|
|
@@ -690,7 +705,10 @@ export class Bridge {
|
|
|
690
705
|
// The switch is written through to the platform by the daemon, for the same reason: the
|
|
691
706
|
// Session has no cloud credential and should not grow one.
|
|
692
707
|
else if (harness !== undefined)
|
|
693
|
-
this.#onHarnessRequest?.(harness);
|
|
708
|
+
this.#onHarnessRequest?.(harness, session);
|
|
709
|
+
// Signing in writes `~/.reticle`, which only the daemon may do; the panel only asks.
|
|
710
|
+
else if (isSigninRequest(parsed.event))
|
|
711
|
+
this.#onSigninRequest?.();
|
|
694
712
|
// Pass the raw frame's byte length so the buffer doesn't re-serialize every event for accounting.
|
|
695
713
|
else
|
|
696
714
|
session.pushEvent(parsed.event, Buffer.byteLength(text, 'utf8'));
|
|
@@ -833,6 +851,10 @@ export class Bridge {
|
|
|
833
851
|
attachHarnessRequest(handler) {
|
|
834
852
|
this.#onHarnessRequest = handler;
|
|
835
853
|
}
|
|
854
|
+
/** Register a handler for the panel's Sign in. Optional, like the above: without one it is ignored. */
|
|
855
|
+
attachSigninRequest(handler) {
|
|
856
|
+
this.#onSigninRequest = handler;
|
|
857
|
+
}
|
|
836
858
|
/** Register a handler to run when a session connects. Additive — every handler runs. */
|
|
837
859
|
attachSessionCreate(handler) {
|
|
838
860
|
this.#onSessionCreate.push(handler);
|
|
@@ -33,6 +33,15 @@ export declare function pairingTokenSource(input: {
|
|
|
33
33
|
explicit: boolean;
|
|
34
34
|
env: NodeJS.ProcessEnv;
|
|
35
35
|
}): 'explicit' | 'env:RETICLE_TOKEN' | 'env:RETICLE_PAIRING_TOKEN_DIR' | 'default';
|
|
36
|
+
/**
|
|
37
|
+
* The pairing token this machine was told to use, or undefined when it was told nothing.
|
|
38
|
+
*
|
|
39
|
+
* `RETICLE_TOKEN` first, then the token file in `RETICLE_PAIRING_TOKEN_DIR`, the precedence
|
|
40
|
+
* `pairingTokenSource` names. A command that mints its own one-shot token for a hosted preview asks
|
|
41
|
+
* this first: in a container the app's plugin bakes the directory token in, and a fresh token made
|
|
42
|
+
* the daemon refuse the only app it was started to verify (#1251).
|
|
43
|
+
*/
|
|
44
|
+
export declare function configuredPairingToken(env: NodeJS.ProcessEnv, deps: PairingTokenDeps): Promise<string | undefined>;
|
|
36
45
|
export declare function pairingTokenPath(dir: string): string;
|
|
37
46
|
/** Production IO adapter: 0600 file + 0700 dir so only the owner can read the secret. */
|
|
38
47
|
export declare function nodePairingTokenDeps(): PairingTokenDeps;
|
|
@@ -41,6 +41,23 @@ export function pairingTokenSource(input) {
|
|
|
41
41
|
return `env:${PAIRING_TOKEN_DIR_ENV}`;
|
|
42
42
|
return 'default';
|
|
43
43
|
}
|
|
44
|
+
/**
|
|
45
|
+
* The pairing token this machine was told to use, or undefined when it was told nothing.
|
|
46
|
+
*
|
|
47
|
+
* `RETICLE_TOKEN` first, then the token file in `RETICLE_PAIRING_TOKEN_DIR`, the precedence
|
|
48
|
+
* `pairingTokenSource` names. A command that mints its own one-shot token for a hosted preview asks
|
|
49
|
+
* this first: in a container the app's plugin bakes the directory token in, and a fresh token made
|
|
50
|
+
* the daemon refuse the only app it was started to verify (#1251).
|
|
51
|
+
*/
|
|
52
|
+
export async function configuredPairingToken(env, deps) {
|
|
53
|
+
const token = env[ReticleEnv.TOKEN]?.trim();
|
|
54
|
+
if (token !== undefined && 0 < token.length)
|
|
55
|
+
return token;
|
|
56
|
+
const dir = env[PAIRING_TOKEN_DIR_ENV];
|
|
57
|
+
if (dir === undefined || 0 === dir.length)
|
|
58
|
+
return undefined;
|
|
59
|
+
return readOrCreatePairingToken(dir, deps);
|
|
60
|
+
}
|
|
44
61
|
export function pairingTokenPath(dir) {
|
|
45
62
|
return join(dir, ReticleDir.PAIRING_TOKEN_FILE);
|
|
46
63
|
}
|
|
@@ -11,7 +11,7 @@
|
|
|
11
11
|
*/
|
|
12
12
|
import { type ScriptCoverage } from './js-coverage.js';
|
|
13
13
|
import type { Browser, Page } from 'playwright';
|
|
14
|
-
import { ActionType, DriveErrorCode } from '@reticlehq/core';
|
|
14
|
+
import { ActionType, DriveErrorCode, InputMode, InputModeReason } from '@reticlehq/core';
|
|
15
15
|
import { type MockRule } from './network-mock.js';
|
|
16
16
|
import { type NetworkDetail } from './network-detail.js';
|
|
17
17
|
/** Viewport CSS-px box as returned by the INSPECT command (getBoundingClientRect). */
|
|
@@ -21,28 +21,72 @@ export interface ElementBox {
|
|
|
21
21
|
width: number;
|
|
22
22
|
height: number;
|
|
23
23
|
}
|
|
24
|
-
/**
|
|
25
|
-
|
|
24
|
+
/**
|
|
25
|
+
* Args forwarded from reticle_act (fill value, type text, press key, drag drop-target box).
|
|
26
|
+
*
|
|
27
|
+
* A `type`, not an `interface`: only the alias form gets an implicit index signature, so the core
|
|
28
|
+
* arg readers take it as a plain record without a cast at every call site.
|
|
29
|
+
*/
|
|
30
|
+
export type RealInputArgs = {
|
|
26
31
|
value?: string;
|
|
27
32
|
text?: string;
|
|
33
|
+
/** For press: the key name, e.g. Escape or Tab. `text` also carries it; `key` is the fallback. */
|
|
34
|
+
key?: string;
|
|
35
|
+
/** For press: modifier names (Meta/Control/Shift/Alt; aliases like `cmd` accepted). */
|
|
36
|
+
modifiers?: string[];
|
|
37
|
+
/**
|
|
38
|
+
* For press: several keys held down TOGETHER, pressed in order and released in reverse. Read but
|
|
39
|
+
* never forwarded — a chord with a non-modifier key has no `page.keyboard.press` spelling, so the
|
|
40
|
+
* caller routes such a press to the synthetic dispatcher before it reaches here.
|
|
41
|
+
*/
|
|
42
|
+
keys?: string[];
|
|
43
|
+
/**
|
|
44
|
+
* For press: how long to keep the key down between keydown and keyup. The same argument the
|
|
45
|
+
* synthetic path has always honoured, so a hold-to-confirm key driven through the driver holds
|
|
46
|
+
* for exactly as long as one driven in the page.
|
|
47
|
+
*/
|
|
48
|
+
holdMs?: number;
|
|
28
49
|
/** For drag: the resolved box of the drop-target ref (toRef). */
|
|
29
50
|
toBox?: ElementBox;
|
|
30
51
|
steps?: number;
|
|
31
|
-
}
|
|
32
|
-
|
|
52
|
+
};
|
|
53
|
+
/** What a provider reports for one driven gesture. Exported so a fake can build it by name. */
|
|
54
|
+
export interface RealInputResult {
|
|
33
55
|
/** True if a native gesture was actually driven. */
|
|
34
56
|
performed: boolean;
|
|
35
|
-
/** Center used, for diagnostics/tests. */
|
|
36
|
-
center
|
|
57
|
+
/** Center used, for diagnostics/tests. ABSENT for a key press: see `undriven`. */
|
|
58
|
+
center?: {
|
|
37
59
|
cx: number;
|
|
38
60
|
cy: number;
|
|
39
61
|
};
|
|
62
|
+
/**
|
|
63
|
+
* Which path this result came from. `real` whenever a gesture was actually driven, so a caller
|
|
64
|
+
* reading it on a `performed: true` result learns nothing it did not already know — it is here
|
|
65
|
+
* for the `performed: false` case, where the caller is about to take the synthetic path and
|
|
66
|
+
* saying `real` would describe a gesture that never ran.
|
|
67
|
+
*/
|
|
68
|
+
inputMode: InputMode;
|
|
69
|
+
/**
|
|
70
|
+
* How long the key was ACTUALLY held, for a real press that was asked to hold. Absent for every
|
|
71
|
+
* other gesture, and for a press with no `holdMs` — the same rule the synthetic path uses, where
|
|
72
|
+
* an absent `heldMs` means "this action does not hold" rather than "it held for no time".
|
|
73
|
+
*
|
|
74
|
+
* Measured rather than echoed back: `holdMs: 1200` against a 1200ms animation is a race by
|
|
75
|
+
* construction, and a throttled tab stretches the wait. A caller needs to tell "held 1200" from
|
|
76
|
+
* "held 1204", and can only do that with the achieved number.
|
|
77
|
+
*/
|
|
78
|
+
heldMs?: number;
|
|
40
79
|
}
|
|
41
80
|
/** Options for a page screenshot — full-page scroll capture and/or a clip box. */
|
|
42
81
|
export interface ScreenshotOpts {
|
|
43
82
|
fullPage?: boolean;
|
|
44
83
|
/** Restrict the capture to one element/region (viewport CSS px). */
|
|
45
84
|
clip?: ElementBox;
|
|
85
|
+
/**
|
|
86
|
+
* A live picture, as a JPEG at this quality: what a person is watching, taken as it is. The
|
|
87
|
+
* baseline tweaks (hiding the HUD, stopping animations) would make their window flicker each second.
|
|
88
|
+
*/
|
|
89
|
+
jpegQuality?: number;
|
|
46
90
|
}
|
|
47
91
|
/** The capability surface reticle_act depends on. A FAKE implementing this is injected in tests. */
|
|
48
92
|
export interface RealInputProvider {
|
|
@@ -117,17 +161,40 @@ export declare function boxCenter(box: ElementBox): {
|
|
|
117
161
|
cy: number;
|
|
118
162
|
};
|
|
119
163
|
/**
|
|
120
|
-
* Which actions are driven by native pointer
|
|
121
|
-
*
|
|
164
|
+
* Which actions are driven by a native pointer. `press` is excluded on purpose — a key has no
|
|
165
|
+
* pointer, and widening this would change what every existing caller of it means.
|
|
122
166
|
*/
|
|
123
167
|
export declare function isPointerAction(action: ActionType): boolean;
|
|
168
|
+
/**
|
|
169
|
+
* The routing question `tryRealInput` asks: does this action have a real-input path at all? Wider
|
|
170
|
+
* than `isPointerAction` by `press`, which needs no coordinates.
|
|
171
|
+
*/
|
|
172
|
+
export declare function isRealInputAction(action: ActionType): boolean;
|
|
124
173
|
type SleepFn = (ms: number) => Promise<void>;
|
|
125
174
|
type ConnectFn = (url: string) => Promise<Browser>;
|
|
175
|
+
/**
|
|
176
|
+
* The `press`es this module will NOT drive: a chord that only the in-page dispatcher can express.
|
|
177
|
+
*
|
|
178
|
+
* `args.keys` asks for several keys held down TOGETHER, pressed in order and released in reverse.
|
|
179
|
+
* That is not a `page.keyboard.press` chord — a non-modifier key cannot appear before the last `+`
|
|
180
|
+
* — and a real keyboard cannot aim a key at a named element. `args.code` is the third: the driver
|
|
181
|
+
* presses by KEY name and offers no way to send a physical `code` that disagrees with it, so
|
|
182
|
+
* `{ text: 'z', code: 'KeyY' }` would strike a different key than the one asked for. All three are
|
|
183
|
+
* routed to the synthetic path by the caller, which reports the reason rather than pretending a
|
|
184
|
+
* different gesture happened.
|
|
185
|
+
*
|
|
186
|
+
* Ordered by how specifically the caller asked: `keys` then `code` name the KEY, `ref` names only
|
|
187
|
+
* the target. Two can apply at once, and the earlier one is the more unusual request — the reason
|
|
188
|
+
* should name that, not the generic "you passed a ref".
|
|
189
|
+
*/
|
|
190
|
+
export declare function unspellablePressReason(ref: string, args: Record<string, unknown>): InputModeReason | undefined;
|
|
126
191
|
/**
|
|
127
192
|
* Shared gesture executor: drive a native gesture on an already-resolved Page. Used by both the
|
|
128
193
|
* CDP-attached and the launched (drive) providers so the pointer logic lives in one place.
|
|
129
194
|
*/
|
|
130
|
-
export declare function performGesture(page: Page, action: ActionType, box: ElementBox, args: RealInputArgs, sleep: SleepFn
|
|
195
|
+
export declare function performGesture(page: Page, action: ActionType, box: ElementBox, args: RealInputArgs, sleep: SleepFn,
|
|
196
|
+
/** Injected so a test can assert the achieved hold deterministically; defaults to the real clock. */
|
|
197
|
+
now?: () => number): Promise<RealInputResult>;
|
|
131
198
|
/**
|
|
132
199
|
* Capture a PNG from a Playwright page. Shared by the CDP + launched providers so the
|
|
133
200
|
* screenshot path lives in one place (mirrors performGesture). Returns the raw PNG bytes.
|
|
@@ -137,6 +204,8 @@ interface CdpProviderOptions {
|
|
|
137
204
|
cdpUrl: string;
|
|
138
205
|
/** Injected so the settle delay is deterministic in tests; defaults to a real Node timer. */
|
|
139
206
|
sleep?: SleepFn;
|
|
207
|
+
/** Injected so a measured hold is deterministic in tests; defaults to the real clock. */
|
|
208
|
+
now?: () => number;
|
|
140
209
|
/** Injected connector so unit tests can stub Playwright without import. */
|
|
141
210
|
connect?: ConnectFn;
|
|
142
211
|
/**
|
|
@@ -193,6 +262,8 @@ export interface LaunchedProviderOptions {
|
|
|
193
262
|
headless: boolean;
|
|
194
263
|
/** Injected so the settle delay is deterministic in tests; defaults to a real Node timer. */
|
|
195
264
|
sleep?: SleepFn;
|
|
265
|
+
/** Injected so a measured hold is deterministic in tests; defaults to the real clock. */
|
|
266
|
+
now?: () => number;
|
|
196
267
|
/** Injected launcher so unit tests can stub Playwright; defaults to dynamic import('playwright'). */
|
|
197
268
|
launch?: LaunchFn;
|
|
198
269
|
/** When set, re-invoke the page's reticle.connect with these after load (drive-a-hosted-preview). */
|