@reticlehq/server 3.8.0 → 3.8.2
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 +2 -2
- package/dist/command/cli/answers/gate-hook-message.d.ts +2 -0
- package/dist/command/cli/answers/gate-hook-message.js +5 -1
- package/dist/command/cli/auth/cloud-session.d.ts +1 -19
- package/dist/command/cli/auth/device-flow.d.ts +3 -37
- package/dist/command/cli/cli-doctor.js +30 -23
- package/dist/command/cli/cli-flow-commands.js +7 -1
- package/dist/command/cli/cli-parse-serve.d.ts +24 -0
- package/dist/command/cli/cli-parse-serve.js +124 -0
- package/dist/command/cli/cli-parse.d.ts +1 -10
- package/dist/command/cli/cli-parse.js +62 -121
- package/dist/command/cli/cli-usage.js +5 -2
- package/dist/command/cli/cloud-link.d.ts +1 -20
- package/dist/command/cli/doctor/doctor-agent-link.d.ts +23 -0
- package/dist/command/cli/doctor/doctor-agent-link.js +28 -0
- package/dist/command/cli/ports/port-holder.js +6 -1
- package/dist/command/cli/project-name.d.ts +1 -7
- package/dist/command/cli/project-name.js +1 -0
- package/dist/command/daemon/daemon.d.ts +6 -0
- package/dist/command/daemon/daemon.js +52 -2
- package/dist/command/daemon/win32-escape.d.ts +32 -0
- package/dist/command/daemon/win32-escape.js +119 -0
- package/dist/command/setup/bringup/dev-server-files.d.ts +1 -9
- package/dist/command/setup/bringup/dev-server-wait.js +5 -1
- package/dist/command/version/version-skew.d.ts +2 -0
- package/dist/command/version/version-skew.js +7 -3
- package/dist/features/crawl/crawl-tools.js +2 -2
- package/dist/features/exhaust/ledger.d.ts +15 -96
- package/dist/features/exhaust/ledger.js +3 -3
- package/dist/features/license/license.d.ts +2 -14
- package/dist/hooks/hook-commands.js +19 -9
- package/dist/judgement/capsule/capsule-store.d.ts +2 -39
- package/dist/language/flows/annotate-tools.js +1 -1
- package/dist/language/flows/fields/flow-secret-field.js +1 -1
- package/dist/language/flows/flow-expect-grammar.js +14 -6
- package/dist/language/flows/flow-lease.d.ts +27 -0
- package/dist/language/flows/flow-lease.js +59 -0
- package/dist/language/flows/flow-replay-run.d.ts +1 -87
- package/dist/language/flows/flow-replay-run.js +88 -283
- package/dist/language/flows/flow-replayable.d.ts +1 -1
- package/dist/language/flows/flow-start-path.d.ts +108 -0
- package/dist/language/flows/flow-start-path.js +313 -0
- package/dist/language/flows/flow-tools.d.ts +2 -23
- package/dist/language/flows/flow-tools.js +34 -66
- package/dist/language/flows/recording/flake.d.ts +3 -27
- package/dist/language/flows/recording/flake.js +1 -1
- package/dist/language/flows/stores/assertion-tiers-store.js +1 -1
- package/dist/machine/git-tracks.js +1 -0
- package/dist/memory/cloud/cloud-sync.d.ts +2 -24
- package/dist/memory/intent/intent-shard.d.ts +41 -266
- package/dist/memory/journal/envelope-store.js +1 -1
- package/dist/memory/journal/envelope.d.ts +6 -154
- package/dist/memory/journal/on-disk/ambient-file.d.ts +1 -7
- package/dist/memory/journal/on-disk/ambient-file.js +1 -1
- package/dist/memory/journal/self-observation.js +3 -1
- package/dist/memory/journal/session-journal.js +39 -20
- package/dist/portal/input/drive-error.d.ts +6 -0
- package/dist/portal/input/drive-error.js +9 -0
- package/dist/portal/input/real-input-press.d.ts +34 -0
- package/dist/portal/input/real-input-press.js +150 -0
- package/dist/portal/input/real-input.d.ts +6 -23
- package/dist/portal/input/real-input.js +95 -192
- package/dist/portal/input/viewport-tools.d.ts +19 -15
- package/dist/portal/pool/browser-pool.d.ts +5 -55
- package/dist/portal/pool/browser-pool.js +121 -100
- package/dist/portal/pool/pool-lease.d.ts +128 -0
- package/dist/portal/pool/pool-lease.js +49 -0
- package/dist/portal/production/production-activation.d.ts +5 -27
- package/dist/portal/session/control-envelope.d.ts +1 -1
- package/dist/portal/session/zero-install-first-call.d.ts +3 -0
- package/dist/portal/session/zero-install-first-call.js +33 -12
- package/dist/surface/mcp/mcp.d.ts +3 -3
- package/dist/surface/mcp/mcp.js +4 -6
- package/dist/surface/tools/act/act-danger.js +32 -17
- package/dist/surface/tools/act/durable.js +1 -1
- package/dist/surface/tools/act-sequence-tool.js +3 -3
- package/dist/surface/tools/act-tools.js +15 -16
- package/dist/surface/tools/agent/compact-envelope.d.ts +2 -2
- package/dist/surface/tools/coverage-tools.js +1 -1
- package/dist/surface/tools/dynamic-tools.d.ts +8 -0
- package/dist/surface/tools/dynamic-tools.js +36 -16
- package/dist/surface/tools/empty-session-list.d.ts +5 -29
- package/dist/surface/tools/feedback-tools.js +1 -1
- package/dist/surface/tools/gap-novelty.d.ts +1 -1
- package/dist/surface/tools/gaps/capability-absences.js +4 -3
- package/dist/surface/tools/harness-gate.d.ts +1 -1
- package/dist/surface/tools/harness-toolset.js +3 -4
- package/dist/surface/tools/lease-permissions.d.ts +1 -1
- package/dist/surface/tools/lease-readiness.d.ts +23 -1
- package/dist/surface/tools/lease-readiness.js +75 -9
- package/dist/surface/tools/lease-tools.d.ts +1 -52
- package/dist/surface/tools/lease-tools.js +79 -142
- package/dist/surface/tools/observe-tools.js +3 -1
- package/dist/surface/tools/read-tools.js +5 -5
- package/dist/surface/tools/real-input-attempt.js +70 -31
- package/dist/surface/tools/session-ports.d.ts +57 -0
- package/dist/surface/tools/session-ports.js +102 -0
- package/dist/surface/tools/tool-kit.d.ts +5 -3
- package/dist/surface/tools/tools.js +1 -1
- package/dist/telemetry/daemon-telemetry.d.ts +3 -1
- package/dist/telemetry/daemon-telemetry.js +9 -2
- package/dist/telemetry/telemetry.d.ts +3 -1
- package/dist/telemetry/telemetry.js +2 -0
- package/docs/agent-cheatsheet.md +12 -0
- package/docs/cli/gate.mdx +1 -1
- package/docs/cli/verify.mdx +8 -1
- package/docs/first-drive.md +7 -0
- package/docs/multi-agent-testing.md +2 -0
- package/docs/packages/react.mdx +14 -2
- package/docs/packages/vite-plugin.mdx +1 -0
- package/docs/predicates.mdx +6 -0
- package/docs/skill.md +3 -3
- package/docs/troubleshooting.mdx +10 -0
- package/docs/usage.md +1 -1
- package/package.json +8 -9
|
@@ -10,25 +10,20 @@
|
|
|
10
10
|
* pay for it; the type-only import is elided by `tsc`, so the build stays green without it.
|
|
11
11
|
*/
|
|
12
12
|
import { takeJsCoverage } from './js-coverage.js';
|
|
13
|
+
import { DriveError } from './drive-error.js';
|
|
14
|
+
import { pressViaKeyboard } from './real-input-press.js';
|
|
13
15
|
import { stampedDriveUrl } from './drive-url-stamp.js';
|
|
14
16
|
import { launchChromium } from '../../launch-chromium.js';
|
|
15
17
|
import { chromiumLaunchHint, gotoOptions, SCREENSHOT_DETERMINISM, captureWithChromeHidden, } from '../pool/playwright-launcher.js';
|
|
16
18
|
import { BrowserLaunchKind } from '@reticlehq/core/telemetry';
|
|
17
19
|
import { getSessionMetrics } from '../../telemetry/session-metrics.js';
|
|
18
20
|
import { classifyConnectFailure } from '../../telemetry/connect-failure.js';
|
|
19
|
-
import { ActionType,
|
|
21
|
+
import { ActionType, DriveErrorCode, DRIVE_PLAYWRIGHT_MISSING_MSG, ClickButton, clickButtonFromArgs, InputMode, pressModifiersFromArgs, } from '@reticlehq/core';
|
|
20
22
|
import { installNetworkMocks } from './network-mock.js';
|
|
21
23
|
import { attachNetworkDetail } from './network-detail.js';
|
|
22
24
|
import { injectedConnectArgs } from '../pool/zero-install.js';
|
|
23
|
-
|
|
24
|
-
export
|
|
25
|
-
code;
|
|
26
|
-
constructor(code, message) {
|
|
27
|
-
super(message);
|
|
28
|
-
this.code = code;
|
|
29
|
-
this.name = 'DriveError';
|
|
30
|
-
}
|
|
31
|
-
}
|
|
25
|
+
export { DriveError };
|
|
26
|
+
export { unspellablePressReason } from './real-input-press.js';
|
|
32
27
|
/** Center of a viewport box in CSS px. Pure — unit-tested directly. */
|
|
33
28
|
export function boxCenter(box) {
|
|
34
29
|
return { cx: box.x + box.width / 2, cy: box.y + box.height / 2 };
|
|
@@ -63,149 +58,12 @@ function undriven(action, box) {
|
|
|
63
58
|
const REAL_INPUT_SETTLE_MS = 16;
|
|
64
59
|
/** Default number of interpolation steps for a native drag. */
|
|
65
60
|
const DEFAULT_DRAG_STEPS = 8;
|
|
66
|
-
/**
|
|
67
|
-
|
|
68
|
-
|
|
69
|
-
|
|
70
|
-
|
|
71
|
-
* A `holdMs` splits the chord into down / wait / up, the same shape the page uses for a held key.
|
|
72
|
-
* `keyboard.press` would send it instantly instead, reporting a hold-to-confirm that never held.
|
|
73
|
-
* The one thing this cannot reproduce is the auto-repeat a held key emits: the driver sends a single
|
|
74
|
-
* keydown, where the in-page path sends the repeats a browser would. Named rather than papered over
|
|
75
|
-
* — an app that counts repeats needs the synthetic path, and a caller that sees a held key do
|
|
76
|
-
* nothing now has a reason to try it.
|
|
77
|
-
*/
|
|
78
|
-
async function pressViaKeyboard(page, args, sleep, now) {
|
|
79
|
-
const key = pressKeyFromArgs(args);
|
|
80
|
-
const modifiers = pressModifiersFromArgs(args);
|
|
81
|
-
const chord = 0 < modifiers.length ? `${modifiers.join('+')}+${key}` : key;
|
|
82
|
-
const holdMs = clampHoldMs(args.holdMs);
|
|
83
|
-
if (0 === holdMs) {
|
|
84
|
-
// `press` DOES take a chord string ("Control+k"), so the tap path needs no splitting.
|
|
85
|
-
await page.keyboard.press(chord);
|
|
86
|
-
return 0;
|
|
87
|
-
}
|
|
88
|
-
/*
|
|
89
|
-
* `down`/`up` take ONE key each, unlike `press`: `down('Control+k')` throws
|
|
90
|
-
* `Unknown key: "Control+k"` — measured on a real Chromium, not assumed. The chord string the
|
|
91
|
-
* tap path uses therefore cannot express a hold, so the chord is split here: modifiers down in
|
|
92
|
-
* chord order, the key last, and released in reverse (key first, modifiers reversed), which is
|
|
93
|
-
* what a hand does and what an app's keyup bookkeeping expects.
|
|
94
|
-
*
|
|
95
|
-
* Without the split a held shortcut threw before a single key moved, the caller fell back to
|
|
96
|
-
* the synthetic path, and a real hold was impossible for any press with modifiers — the exact
|
|
97
|
-
* gesture `holdMs` exists for.
|
|
98
|
-
*
|
|
99
|
-
* ## Why every key that went down is tracked, and every exit path cleans up
|
|
100
|
-
*
|
|
101
|
-
* The down calls are the ones that MOVE the keyboard, and they can fail partway: `Control` goes
|
|
102
|
-
* down, the key after it rejects, and the driver's copy of `Control` is still held. That path
|
|
103
|
-
* used to fall straight to the caller's synthetic replay — which presses the same chord again on
|
|
104
|
-
* a keyboard that is already holding a modifier. Tracking what actually went down is what makes
|
|
105
|
-
* the cleanup below able to release the right keys, and only those: releasing a key that never
|
|
106
|
-
* went down is its own lie, and it would turn a clean failure into an unnecessary refusal.
|
|
107
|
-
*/
|
|
108
|
-
const pressed = [];
|
|
109
|
-
const pressDown = async (k) => {
|
|
110
|
-
await page.keyboard.down(k);
|
|
111
|
-
pressed.push(k);
|
|
112
|
-
};
|
|
113
|
-
try {
|
|
114
|
-
for (const modifier of modifiers)
|
|
115
|
-
await pressDown(modifier);
|
|
116
|
-
await pressDown(key);
|
|
117
|
-
}
|
|
118
|
-
catch (error) {
|
|
119
|
-
// The chord is PARTIALLY down. Release what is held before reporting the failure; if that
|
|
120
|
-
// cannot be proved, `releaseKeys` throws RELEASE_FAILED and the caller refuses the replay —
|
|
121
|
-
// the same rule as a failed release on the ordinary path, for the same reason. Only when the
|
|
122
|
-
// cleanup DOES succeed is the original error safe to hand back.
|
|
123
|
-
await releaseKeys(page, pressed);
|
|
124
|
-
throw error;
|
|
125
|
-
}
|
|
126
|
-
const startedAt = now();
|
|
127
|
-
try {
|
|
128
|
-
await sleep(holdMs);
|
|
129
|
-
}
|
|
130
|
-
catch (error) {
|
|
131
|
-
// The wait failed, and the caller is about to run the synthetic path — which presses this SAME
|
|
132
|
-
// key a second time. If the driver's copy stays down, a held modifier then colours every later
|
|
133
|
-
// action for the rest of the run. Best-effort on purpose: whatever went wrong with the wait is
|
|
134
|
-
// the error worth reporting. A cleanup that cannot be proved is the exception — that one is a
|
|
135
|
-
// stuck key, and it must reach the caller as a refusal rather than be swallowed.
|
|
136
|
-
try {
|
|
137
|
-
await releaseKeys(page, pressed);
|
|
138
|
-
}
|
|
139
|
-
catch {
|
|
140
|
-
throw new DriveError(DriveErrorCode.RELEASE_FAILED, 'the wait failed and the keys it pressed could not be released, so they may still be held');
|
|
141
|
-
}
|
|
142
|
-
throw error;
|
|
143
|
-
}
|
|
144
|
-
// NOT best-effort: on the ordinary path a failed release is a real failure the caller must hear
|
|
145
|
-
// about, and swallowing it would report a press whose key is still down.
|
|
146
|
-
await releaseKeys(page, pressed);
|
|
147
|
-
return now() - startedAt;
|
|
148
|
-
}
|
|
149
|
-
/**
|
|
150
|
-
* Release the given keys, in reverse press order, and report a failure as one that leaves the
|
|
151
|
-
* keyboard UNKNOWN rather than as an ordinary provider error.
|
|
152
|
-
*
|
|
153
|
-
* Every release is attempted even after one fails: a stuck `Control` is worse than a stuck letter,
|
|
154
|
-
* so a failure on one key must not skip the rest. The first failure is what is thrown, since it is
|
|
155
|
-
* the one that happened first, and it is wrapped in `DriveError` so the caller can tell "this
|
|
156
|
-
* gesture failed" from "this gesture may have left a key down" — the difference between a safe
|
|
157
|
-
* synthetic replay and an unsafe one.
|
|
158
|
-
*/
|
|
159
|
-
async function releaseKeys(page, pressed) {
|
|
160
|
-
let failure;
|
|
161
|
-
for (const k of [...pressed].reverse()) {
|
|
162
|
-
try {
|
|
163
|
-
await page.keyboard.up(k);
|
|
164
|
-
}
|
|
165
|
-
catch (error) {
|
|
166
|
-
failure ??= error;
|
|
167
|
-
}
|
|
168
|
-
}
|
|
169
|
-
if (failure !== undefined) {
|
|
170
|
-
throw new DriveError(DriveErrorCode.RELEASE_FAILED, `the key was pressed but could not be released, so it may still be held: ${describeFailure(failure)}`);
|
|
171
|
-
}
|
|
172
|
-
}
|
|
173
|
-
/**
|
|
174
|
-
* A thrown value as a line a reader can act on.
|
|
175
|
-
*
|
|
176
|
-
* Not `String(error)`: a non-Error thrown value (a rejected plain object, an SDK that throws a
|
|
177
|
-
* record) stringifies to `[object Object]`, which names nothing and hides the one fact the message
|
|
178
|
-
* exists to carry. Anything that is neither an Error nor a string says so rather than pretending.
|
|
179
|
-
*/
|
|
180
|
-
function describeFailure(error) {
|
|
181
|
-
if (error instanceof Error)
|
|
182
|
-
return error.message;
|
|
183
|
-
return 'string' === typeof error ? error : 'an unknown error';
|
|
184
|
-
}
|
|
185
|
-
/**
|
|
186
|
-
* The `press`es this module will NOT drive: a chord that only the in-page dispatcher can express.
|
|
187
|
-
*
|
|
188
|
-
* `args.keys` asks for several keys held down TOGETHER, pressed in order and released in reverse.
|
|
189
|
-
* That is not a `page.keyboard.press` chord — a non-modifier key cannot appear before the last `+`
|
|
190
|
-
* — and a real keyboard cannot aim a key at a named element. `args.code` is the third: the driver
|
|
191
|
-
* presses by KEY name and offers no way to send a physical `code` that disagrees with it, so
|
|
192
|
-
* `{ text: 'z', code: 'KeyY' }` would strike a different key than the one asked for. All three are
|
|
193
|
-
* routed to the synthetic path by the caller, which reports the reason rather than pretending a
|
|
194
|
-
* different gesture happened.
|
|
195
|
-
*
|
|
196
|
-
* Ordered by how specifically the caller asked: `keys` then `code` name the KEY, `ref` names only
|
|
197
|
-
* the target. Two can apply at once, and the earlier one is the more unusual request — the reason
|
|
198
|
-
* should name that, not the generic "you passed a ref".
|
|
199
|
-
*/
|
|
200
|
-
export function unspellablePressReason(ref, args) {
|
|
201
|
-
if (0 < pressKeysFromArgs(args).length) {
|
|
202
|
-
return InputModeReason.SYNTHETIC_MULTI_KEY_PRESS_PREFERRED;
|
|
61
|
+
/** The mouse options a click needs: the button, only when it is not the default left one. */
|
|
62
|
+
function clickOptionsOf(args) {
|
|
63
|
+
const button = clickButtonFromArgs({ button: args.button });
|
|
64
|
+
if (button === ClickButton.RIGHT || button === ClickButton.MIDDLE) {
|
|
65
|
+
return { button };
|
|
203
66
|
}
|
|
204
|
-
if (explicitCodeFromArgs(args) !== undefined) {
|
|
205
|
-
return InputModeReason.SYNTHETIC_KEY_CODE_PRESS_PREFERRED;
|
|
206
|
-
}
|
|
207
|
-
if (0 < ref.length)
|
|
208
|
-
return InputModeReason.SYNTHETIC_ELEMENT_PRESS_PREFERRED;
|
|
209
67
|
return undefined;
|
|
210
68
|
}
|
|
211
69
|
/**
|
|
@@ -241,19 +99,32 @@ now = Date.now) {
|
|
|
241
99
|
await sleep(REAL_INPUT_SETTLE_MS);
|
|
242
100
|
return real(true);
|
|
243
101
|
}
|
|
244
|
-
if (action === ActionType.CLICK) {
|
|
245
|
-
await page.mouse.move(cx, cy);
|
|
246
|
-
await page.mouse.click(cx, cy);
|
|
247
|
-
return real(true);
|
|
248
|
-
}
|
|
249
|
-
if (action === ActionType.DBLCLICK) {
|
|
102
|
+
if (action === ActionType.CLICK || action === ActionType.DBLCLICK) {
|
|
250
103
|
await page.mouse.move(cx, cy);
|
|
251
|
-
|
|
104
|
+
const options = clickOptionsOf(args);
|
|
105
|
+
const modifiers = pressModifiersFromArgs(args);
|
|
106
|
+
for (const modifier of modifiers) {
|
|
107
|
+
await page.keyboard.down(modifier);
|
|
108
|
+
}
|
|
109
|
+
try {
|
|
110
|
+
if (action === ActionType.CLICK) {
|
|
111
|
+
await page.mouse.click(cx, cy, options);
|
|
112
|
+
}
|
|
113
|
+
else {
|
|
114
|
+
await page.mouse.dblclick(cx, cy, options);
|
|
115
|
+
}
|
|
116
|
+
}
|
|
117
|
+
finally {
|
|
118
|
+
for (const modifier of [...modifiers].reverse()) {
|
|
119
|
+
await page.keyboard.up(modifier);
|
|
120
|
+
}
|
|
121
|
+
}
|
|
252
122
|
return real(true);
|
|
253
123
|
}
|
|
254
124
|
if (action === ActionType.DRAG) {
|
|
255
|
-
if (args.toBox === undefined)
|
|
125
|
+
if (args.toBox === undefined) {
|
|
256
126
|
return real(false);
|
|
127
|
+
}
|
|
257
128
|
const dst = boxCenter(args.toBox);
|
|
258
129
|
const steps = args.steps ?? DEFAULT_DRAG_STEPS;
|
|
259
130
|
await page.mouse.move(cx, cy);
|
|
@@ -341,8 +212,9 @@ export class CdpRealInputProvider {
|
|
|
341
212
|
/** Attach the response listener the first time we see a page; a no-op afterwards. */
|
|
342
213
|
#listen(page) {
|
|
343
214
|
const sink = this.#onNetworkDetail;
|
|
344
|
-
if (sink === undefined || this.#listening.has(page))
|
|
215
|
+
if (sink === undefined || this.#listening.has(page)) {
|
|
345
216
|
return;
|
|
217
|
+
}
|
|
346
218
|
this.#listening.add(page);
|
|
347
219
|
attachNetworkDetail(page, sink);
|
|
348
220
|
}
|
|
@@ -354,8 +226,9 @@ export class CdpRealInputProvider {
|
|
|
354
226
|
// Playwright's Browser exposes isConnected(); a test fake may not, so only an EXPLICIT false
|
|
355
227
|
// drops the cache — a fake without the method is assumed live.
|
|
356
228
|
const alive = this.#browser.isConnected?.() ?? true;
|
|
357
|
-
if (alive)
|
|
229
|
+
if (alive) {
|
|
358
230
|
return this.#browser;
|
|
231
|
+
}
|
|
359
232
|
this.#browser = undefined; // fall through and reconnect
|
|
360
233
|
}
|
|
361
234
|
try {
|
|
@@ -369,11 +242,13 @@ export class CdpRealInputProvider {
|
|
|
369
242
|
}
|
|
370
243
|
async #pageFor(sessionUrl) {
|
|
371
244
|
const browser = await this.#ensureBrowser();
|
|
372
|
-
if (browser === undefined)
|
|
245
|
+
if (browser === undefined) {
|
|
373
246
|
return undefined;
|
|
247
|
+
}
|
|
374
248
|
const page = selectPage(browser.contexts().flatMap((c) => c.pages()), sessionUrl);
|
|
375
|
-
if (page !== undefined)
|
|
249
|
+
if (page !== undefined) {
|
|
376
250
|
this.#listen(page);
|
|
251
|
+
}
|
|
377
252
|
return page;
|
|
378
253
|
}
|
|
379
254
|
async takeCodeCoverage(sessionUrl) {
|
|
@@ -390,22 +265,25 @@ export class CdpRealInputProvider {
|
|
|
390
265
|
}
|
|
391
266
|
async perform(sessionUrl, action, box, args) {
|
|
392
267
|
const page = await this.#pageFor(sessionUrl);
|
|
393
|
-
if (page === undefined)
|
|
268
|
+
if (page === undefined) {
|
|
394
269
|
return undriven(action, box);
|
|
270
|
+
}
|
|
395
271
|
return performGesture(page, action, box, args, this.#sleep, this.#now);
|
|
396
272
|
}
|
|
397
273
|
/** PNG of the correlated page, or undefined if none matches. */
|
|
398
274
|
async screenshot(sessionUrl, opts) {
|
|
399
275
|
const page = await this.#pageFor(sessionUrl);
|
|
400
|
-
if (page === undefined)
|
|
276
|
+
if (page === undefined) {
|
|
401
277
|
return undefined;
|
|
278
|
+
}
|
|
402
279
|
return capturePage(page, opts);
|
|
403
280
|
}
|
|
404
281
|
/** The context's cookies and per-origin storage; undefined when no driven page matches. */
|
|
405
282
|
async storageState(sessionUrl) {
|
|
406
283
|
const page = await this.#pageFor(sessionUrl);
|
|
407
|
-
if (page === undefined)
|
|
284
|
+
if (page === undefined) {
|
|
408
285
|
return undefined;
|
|
286
|
+
}
|
|
409
287
|
return page.context().storageState();
|
|
410
288
|
}
|
|
411
289
|
/**
|
|
@@ -416,27 +294,32 @@ export class CdpRealInputProvider {
|
|
|
416
294
|
*/
|
|
417
295
|
async applyStorageState(sessionUrl, state) {
|
|
418
296
|
const page = await this.#pageFor(sessionUrl);
|
|
419
|
-
if (page === undefined)
|
|
297
|
+
if (page === undefined) {
|
|
420
298
|
return false;
|
|
299
|
+
}
|
|
421
300
|
const saved = state;
|
|
422
|
-
if (saved.cookies !== undefined)
|
|
301
|
+
if (saved.cookies !== undefined) {
|
|
423
302
|
await page.context().addCookies(saved.cookies);
|
|
303
|
+
}
|
|
424
304
|
for (const origin of saved.origins ?? []) {
|
|
425
305
|
// Only the document for an origin can write that origin's localStorage, so this is applied in
|
|
426
306
|
// the page rather than through the context. A page sitting on a different origin cannot be
|
|
427
307
|
// given another origin's storage, and silently succeeding there would be the quiet half-restore
|
|
428
308
|
// this whole path exists to avoid.
|
|
429
|
-
if (!page.url().startsWith(origin.origin))
|
|
309
|
+
if (!page.url().startsWith(origin.origin)) {
|
|
430
310
|
continue;
|
|
311
|
+
}
|
|
431
312
|
await page.evaluate((entries) => {
|
|
432
313
|
// Reached through `globalThis` rather than `window`: this body is serialised and runs in the
|
|
433
314
|
// PAGE, but it is typechecked here, and this package never pulls in the DOM lib — a server
|
|
434
315
|
// that can name `window` is one step from a server that uses it.
|
|
435
316
|
const store = globalThis.localStorage;
|
|
436
|
-
if (store === undefined)
|
|
317
|
+
if (store === undefined) {
|
|
437
318
|
return;
|
|
438
|
-
|
|
319
|
+
}
|
|
320
|
+
for (const entry of entries) {
|
|
439
321
|
store.setItem(entry.name, entry.value);
|
|
322
|
+
}
|
|
440
323
|
}, origin.localStorage ?? []);
|
|
441
324
|
}
|
|
442
325
|
return true;
|
|
@@ -444,16 +327,18 @@ export class CdpRealInputProvider {
|
|
|
444
327
|
/** Apply network-mock rules to the correlated page; false when no driven page matches. */
|
|
445
328
|
async setMocks(sessionUrl, rules) {
|
|
446
329
|
const page = await this.#pageFor(sessionUrl);
|
|
447
|
-
if (page === undefined)
|
|
330
|
+
if (page === undefined) {
|
|
448
331
|
return false;
|
|
332
|
+
}
|
|
449
333
|
await installNetworkMocks(page, rules);
|
|
450
334
|
return true;
|
|
451
335
|
}
|
|
452
336
|
/** Pin the correlated page's viewport to fixed dimensions; false when no driven page matches. */
|
|
453
337
|
async setViewport(sessionUrl, size) {
|
|
454
338
|
const page = await this.#pageFor(sessionUrl);
|
|
455
|
-
if (page === undefined)
|
|
339
|
+
if (page === undefined) {
|
|
456
340
|
return false;
|
|
341
|
+
}
|
|
457
342
|
await page.setViewportSize({ width: size.width, height: size.height });
|
|
458
343
|
return true;
|
|
459
344
|
}
|
|
@@ -461,8 +346,9 @@ export class CdpRealInputProvider {
|
|
|
461
346
|
async dispose() {
|
|
462
347
|
const browser = this.#browser;
|
|
463
348
|
this.#browser = undefined;
|
|
464
|
-
if (browser !== undefined)
|
|
349
|
+
if (browser !== undefined) {
|
|
465
350
|
await browser.close();
|
|
351
|
+
}
|
|
466
352
|
}
|
|
467
353
|
}
|
|
468
354
|
const INJECT_CONNECT_WAIT_MS = 8_000;
|
|
@@ -519,8 +405,9 @@ export class LaunchedRealInputProvider {
|
|
|
519
405
|
const page = await this.#browser.newPage(this.#storageState !== undefined ? { storageState: this.#storageState } : undefined);
|
|
520
406
|
this.#page = page;
|
|
521
407
|
// Capture CDP-authoritative response detail into the driven session's journal (best-effort).
|
|
522
|
-
if (this.#onNetworkDetail !== undefined)
|
|
408
|
+
if (this.#onNetworkDetail !== undefined) {
|
|
523
409
|
attachNetworkDetail(page, this.#onNetworkDetail);
|
|
410
|
+
}
|
|
524
411
|
try {
|
|
525
412
|
// Same navigation rule as the pool, and for the same measured reason: Playwright's default
|
|
526
413
|
// waits for `load`, which an app with one never-finishing subresource never fires — 30s of
|
|
@@ -542,8 +429,9 @@ export class LaunchedRealInputProvider {
|
|
|
542
429
|
*/
|
|
543
430
|
async #tryInjectConnect(page) {
|
|
544
431
|
const opts = this.#injectConnect;
|
|
545
|
-
if (opts === undefined)
|
|
432
|
+
if (opts === undefined) {
|
|
546
433
|
return;
|
|
434
|
+
}
|
|
547
435
|
try {
|
|
548
436
|
await page.waitForFunction('!!globalThis.__reticleInstance', {
|
|
549
437
|
timeout: INJECT_CONNECT_WAIT_MS,
|
|
@@ -567,8 +455,9 @@ export class LaunchedRealInputProvider {
|
|
|
567
455
|
*/
|
|
568
456
|
#livePage() {
|
|
569
457
|
const page = this.#page;
|
|
570
|
-
if (page === undefined)
|
|
458
|
+
if (page === undefined) {
|
|
571
459
|
return undefined;
|
|
460
|
+
}
|
|
572
461
|
if (true === page.isClosed?.()) {
|
|
573
462
|
this.#page = undefined; // never ask a corpse twice
|
|
574
463
|
return undefined;
|
|
@@ -577,10 +466,12 @@ export class LaunchedRealInputProvider {
|
|
|
577
466
|
}
|
|
578
467
|
isAvailableFor(sessionUrl) {
|
|
579
468
|
const page = this.#livePage();
|
|
580
|
-
if (page === undefined)
|
|
469
|
+
if (page === undefined) {
|
|
581
470
|
return Promise.resolve(false);
|
|
582
|
-
|
|
471
|
+
}
|
|
472
|
+
if (page.url() === sessionUrl) {
|
|
583
473
|
return Promise.resolve(true);
|
|
474
|
+
}
|
|
584
475
|
return Promise.resolve(stripVolatile(page.url()) === stripVolatile(sessionUrl));
|
|
585
476
|
}
|
|
586
477
|
/**
|
|
@@ -593,8 +484,9 @@ export class LaunchedRealInputProvider {
|
|
|
593
484
|
*/
|
|
594
485
|
async storageState(_sessionUrl) {
|
|
595
486
|
const page = this.#livePage();
|
|
596
|
-
if (page === undefined)
|
|
487
|
+
if (page === undefined) {
|
|
597
488
|
return undefined;
|
|
489
|
+
}
|
|
598
490
|
return page.context().storageState();
|
|
599
491
|
}
|
|
600
492
|
/**
|
|
@@ -603,37 +495,44 @@ export class LaunchedRealInputProvider {
|
|
|
603
495
|
*/
|
|
604
496
|
async applyStorageState(_sessionUrl, state) {
|
|
605
497
|
const page = this.#livePage();
|
|
606
|
-
if (page === undefined)
|
|
498
|
+
if (page === undefined) {
|
|
607
499
|
return false;
|
|
500
|
+
}
|
|
608
501
|
const saved = state;
|
|
609
|
-
if (saved.cookies !== undefined)
|
|
502
|
+
if (saved.cookies !== undefined) {
|
|
610
503
|
await page.context().addCookies(saved.cookies);
|
|
504
|
+
}
|
|
611
505
|
for (const origin of saved.origins ?? []) {
|
|
612
506
|
// A page sitting on a different origin cannot be given another origin's storage, and silently
|
|
613
507
|
// succeeding there is the quiet half-restore this whole path exists to avoid.
|
|
614
|
-
if (!page.url().startsWith(origin.origin))
|
|
508
|
+
if (!page.url().startsWith(origin.origin)) {
|
|
615
509
|
continue;
|
|
510
|
+
}
|
|
616
511
|
await page.evaluate((entries) => {
|
|
617
512
|
const store = globalThis.localStorage;
|
|
618
|
-
if (store === undefined)
|
|
513
|
+
if (store === undefined) {
|
|
619
514
|
return;
|
|
620
|
-
|
|
515
|
+
}
|
|
516
|
+
for (const entry of entries) {
|
|
621
517
|
store.setItem(entry.name, entry.value);
|
|
518
|
+
}
|
|
622
519
|
}, origin.localStorage ?? []);
|
|
623
520
|
}
|
|
624
521
|
return true;
|
|
625
522
|
}
|
|
626
523
|
perform(_sessionUrl, action, box, args) {
|
|
627
524
|
const page = this.#livePage();
|
|
628
|
-
if (page === undefined)
|
|
525
|
+
if (page === undefined) {
|
|
629
526
|
return Promise.resolve(undriven(action, box));
|
|
527
|
+
}
|
|
630
528
|
return performGesture(page, action, box, args, this.#sleep, this.#now);
|
|
631
529
|
}
|
|
632
530
|
/** PNG of the owned page, or undefined before navigate / after dispose. */
|
|
633
531
|
screenshot(_sessionUrl, opts) {
|
|
634
532
|
const page = this.#livePage();
|
|
635
|
-
if (page === undefined)
|
|
533
|
+
if (page === undefined) {
|
|
636
534
|
return Promise.resolve(undefined);
|
|
535
|
+
}
|
|
637
536
|
return capturePage(page, opts);
|
|
638
537
|
}
|
|
639
538
|
/**
|
|
@@ -646,8 +545,9 @@ export class LaunchedRealInputProvider {
|
|
|
646
545
|
*/
|
|
647
546
|
async setViewport(_sessionUrl, size) {
|
|
648
547
|
const page = this.#livePage();
|
|
649
|
-
if (page === undefined)
|
|
548
|
+
if (page === undefined) {
|
|
650
549
|
return false;
|
|
550
|
+
}
|
|
651
551
|
await page.setViewportSize({ width: size.width, height: size.height });
|
|
652
552
|
return true;
|
|
653
553
|
}
|
|
@@ -658,8 +558,9 @@ export class LaunchedRealInputProvider {
|
|
|
658
558
|
/** Apply network-mock rules to the owned page; false before navigate / after dispose. */
|
|
659
559
|
async setMocks(_sessionUrl, rules) {
|
|
660
560
|
const page = this.#livePage();
|
|
661
|
-
if (page === undefined)
|
|
561
|
+
if (page === undefined) {
|
|
662
562
|
return false;
|
|
563
|
+
}
|
|
663
564
|
await installNetworkMocks(page, rules);
|
|
664
565
|
return true;
|
|
665
566
|
}
|
|
@@ -668,8 +569,9 @@ export class LaunchedRealInputProvider {
|
|
|
668
569
|
const browser = this.#browser;
|
|
669
570
|
this.#browser = undefined;
|
|
670
571
|
this.#page = undefined;
|
|
671
|
-
if (browser !== undefined)
|
|
572
|
+
if (browser !== undefined) {
|
|
672
573
|
await browser.close();
|
|
574
|
+
}
|
|
673
575
|
}
|
|
674
576
|
}
|
|
675
577
|
/** Drop hash/query so a page whose URL drifted by fragment still correlates to the session. */
|
|
@@ -688,8 +590,9 @@ export class LaunchedRealInputProvider {
|
|
|
688
590
|
*/
|
|
689
591
|
export function selectPage(pages, sessionUrl) {
|
|
690
592
|
const exact = pages.find((p) => p.url() === sessionUrl);
|
|
691
|
-
if (exact !== undefined)
|
|
593
|
+
if (exact !== undefined) {
|
|
692
594
|
return exact;
|
|
595
|
+
}
|
|
693
596
|
const target = stripVolatile(sessionUrl);
|
|
694
597
|
const loose = pages.filter((p) => stripVolatile(p.url()) === target);
|
|
695
598
|
return 1 === loose.length ? loose[0] : undefined;
|
|
@@ -1,24 +1,28 @@
|
|
|
1
1
|
import { z } from 'zod';
|
|
2
2
|
import type { ToolDef } from '../../surface/tools/tool-kit.js';
|
|
3
3
|
import { type LeaseEmulation, type LeaseMedia } from '../pool/lease-emulation.js';
|
|
4
|
-
export declare const COLOR_SCHEME_ARG: z.ZodOptional<z.ZodEnum<
|
|
5
|
-
|
|
4
|
+
export declare const COLOR_SCHEME_ARG: z.ZodOptional<z.ZodEnum<{
|
|
5
|
+
light: "light";
|
|
6
|
+
dark: "dark";
|
|
7
|
+
"no-preference": "no-preference";
|
|
8
|
+
}>>;
|
|
9
|
+
export declare const REDUCED_MOTION_ARG: z.ZodOptional<z.ZodEnum<{
|
|
10
|
+
reduce: "reduce";
|
|
11
|
+
"no-preference": "no-preference";
|
|
12
|
+
}>>;
|
|
6
13
|
export declare const LEASE_EMULATE_ARG: z.ZodOptional<z.ZodObject<{
|
|
7
|
-
colorScheme: z.ZodOptional<z.ZodEnum<
|
|
8
|
-
|
|
14
|
+
colorScheme: z.ZodOptional<z.ZodEnum<{
|
|
15
|
+
light: "light";
|
|
16
|
+
dark: "dark";
|
|
17
|
+
"no-preference": "no-preference";
|
|
18
|
+
}>>;
|
|
19
|
+
reducedMotion: z.ZodOptional<z.ZodEnum<{
|
|
20
|
+
reduce: "reduce";
|
|
21
|
+
"no-preference": "no-preference";
|
|
22
|
+
}>>;
|
|
9
23
|
width: z.ZodOptional<z.ZodNumber>;
|
|
10
24
|
height: z.ZodOptional<z.ZodNumber>;
|
|
11
|
-
},
|
|
12
|
-
width?: number | undefined;
|
|
13
|
-
height?: number | undefined;
|
|
14
|
-
colorScheme?: "light" | "dark" | "no-preference" | undefined;
|
|
15
|
-
reducedMotion?: "reduce" | "no-preference" | undefined;
|
|
16
|
-
}, {
|
|
17
|
-
width?: number | undefined;
|
|
18
|
-
height?: number | undefined;
|
|
19
|
-
colorScheme?: "light" | "dark" | "no-preference" | undefined;
|
|
20
|
-
reducedMotion?: "reduce" | "no-preference" | undefined;
|
|
21
|
-
}>>;
|
|
25
|
+
}, z.core.$strict>>;
|
|
22
26
|
/** The media part of what a caller asked for, or undefined when it asked for none. */
|
|
23
27
|
export declare function mediaOf(raw: {
|
|
24
28
|
colorScheme?: unknown;
|
|
@@ -14,61 +14,9 @@ import type { Launcher, PooledMockRule, ScreenshotOptions } from './pool-contrac
|
|
|
14
14
|
import type { RealInputArgs, RealInputResult } from '../input/real-input.js';
|
|
15
15
|
import { type LeaseEmulation, type LeaseMedia } from './lease-emulation.js';
|
|
16
16
|
export type { InitScriptHandle, Launcher, PooledBrowser, PooledContext, PooledCookie, PooledDialog, PooledMockRule, PooledPage, } from './pool-contract.js';
|
|
17
|
-
|
|
18
|
-
export
|
|
19
|
-
|
|
20
|
-
readonly url: string;
|
|
21
|
-
readonly navStatus?: number;
|
|
22
|
-
release(): Promise<void>;
|
|
23
|
-
/**
|
|
24
|
-
* Hand the page the zero-install reader: evaluated in the live document, and added ahead of every
|
|
25
|
-
* document this lease loads next. False when there is no reader, or the page cannot run one.
|
|
26
|
-
* Rejects with `NotHtmlDocumentError` when the page is not an HTML document.
|
|
27
|
-
*/
|
|
28
|
-
injectReader?(): Promise<boolean>;
|
|
29
|
-
/** Whether the page's document has not reached `complete` yet. False when it cannot be read. */
|
|
30
|
-
isLoading?(): Promise<boolean>;
|
|
31
|
-
}
|
|
32
|
-
/** Default lease time-to-live: a lease untouched for this long is presumed orphaned and reclaimed. */
|
|
33
|
-
export declare const DEFAULT_LEASE_TTL_MS: number;
|
|
34
|
-
/**
|
|
35
|
-
* How long a lease outlives the MCP connection that took it. Long enough for an agent whose
|
|
36
|
-
* connection only blipped to touch it again; short enough that a gone agent's window closes soon.
|
|
37
|
-
*/
|
|
38
|
-
export declare const ORPHANED_LEASE_GRACE_MS = 30000;
|
|
39
|
-
interface BrowserPoolOptions {
|
|
40
|
-
/** Max simultaneous leased contexts. Over-cap acquires queue. */
|
|
41
|
-
maxContexts: number;
|
|
42
|
-
/** Stable id generator for lease sessionIds (injected to stay clock-/random-free in logic). */
|
|
43
|
-
genSessionId: () => string;
|
|
44
|
-
/** Injected clock (ms). Defaults to Date.now; tests pass a controllable one. */
|
|
45
|
-
now?: () => number;
|
|
46
|
-
/** A lease untouched for longer than this is reclaimed by sweepExpired. */
|
|
47
|
-
leaseTtlMs?: number;
|
|
48
|
-
/** Per-lease navigation timeout — a page that won't load fails its own lease, never blocks a slot. */
|
|
49
|
-
navTimeoutMs?: number;
|
|
50
|
-
/**
|
|
51
|
-
* The zero-install reader, built where the daemon knows its own port and pairing token. A thunk so
|
|
52
|
-
* the ~450KB build is only read if some page ever needs it; undefined when there is none to give.
|
|
53
|
-
*/
|
|
54
|
-
zeroInstallScript?: () => string | undefined;
|
|
55
|
-
/**
|
|
56
|
-
* Launches a browser with a window, for a lease somebody wants to watch. Launched only when the
|
|
57
|
-
* first headed lease is asked for. Absent: a headed lease is refused.
|
|
58
|
-
*/
|
|
59
|
-
launchHeaded?: Launcher;
|
|
60
|
-
}
|
|
61
|
-
/** What a headed lease is refused with where no window can be opened. */
|
|
62
|
-
export declare const NO_HEADED_BROWSER = "no headed browser can be opened here";
|
|
63
|
-
/**
|
|
64
|
-
* An acquire that asked to be refused rather than queued found every slot taken (#1535). Queued,
|
|
65
|
-
* it waited behind the cap with no deadline and no status, and `reticle drive` reported the 60 s
|
|
66
|
-
* timeout as a daemon shutting down. The message names the cap and the one override for it.
|
|
67
|
-
*/
|
|
68
|
-
export declare class PoolAtCapacityError extends Error {
|
|
69
|
-
readonly max: number;
|
|
70
|
-
constructor(max: number);
|
|
71
|
-
}
|
|
17
|
+
import { type BrowserPoolOptions, type Lease } from './pool-lease.js';
|
|
18
|
+
export { DEFAULT_LEASE_TTL_MS, NO_HEADED_BROWSER, ORPHANED_LEASE_GRACE_MS, PoolAtCapacityError, } from './pool-lease.js';
|
|
19
|
+
export type { Lease } from './pool-lease.js';
|
|
72
20
|
/**
|
|
73
21
|
* Owns a headless browser (and, when asked, a headed one) and leases isolated contexts out of them,
|
|
74
22
|
* capped at `maxContexts` across both.
|
|
@@ -171,6 +119,8 @@ export declare class BrowserPool {
|
|
|
171
119
|
hoverLease(sessionId: string, x: number, y: number): Promise<boolean>;
|
|
172
120
|
/** Set a live lease's media features, or false when this is not a lease or its page cannot. */
|
|
173
121
|
emulateLease(sessionId: string, media: LeaseMedia): Promise<boolean>;
|
|
122
|
+
/** Whether this lease's page is still loading. False for a non-lease, or a page that cannot say. */
|
|
123
|
+
leaseLoading(sessionId: string): Promise<boolean>;
|
|
174
124
|
/** Whether this session is a lease whose page has a real keyboard. */
|
|
175
125
|
hasKeyboard(sessionId: string): boolean;
|
|
176
126
|
/**
|