@jobshimo/browser-link 0.22.0 → 0.23.1
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 +5 -0
- package/dist/agent-instructions/content.js +11 -0
- package/dist/agent-instructions/content.js.map +1 -1
- package/dist/bridge/ws-bridge.js +13 -0
- package/dist/bridge/ws-bridge.js.map +1 -1
- package/dist/cdp/client.d.ts +35 -0
- package/dist/cdp/client.js +149 -0
- package/dist/cdp/client.js.map +1 -0
- package/dist/cdp/flow.d.ts +178 -0
- package/dist/cdp/flow.js +217 -0
- package/dist/cdp/flow.js.map +1 -0
- package/dist/cdp/gate.d.ts +31 -0
- package/dist/cdp/gate.js +35 -0
- package/dist/cdp/gate.js.map +1 -0
- package/dist/cdp/grant.d.ts +45 -0
- package/dist/cdp/grant.js +81 -0
- package/dist/cdp/grant.js.map +1 -0
- package/dist/cdp/inpage/builders.d.ts +165 -0
- package/dist/cdp/inpage/builders.js +501 -0
- package/dist/cdp/inpage/builders.js.map +1 -0
- package/dist/cdp/inpage/deep-query.d.ts +57 -0
- package/dist/cdp/inpage/deep-query.js +325 -0
- package/dist/cdp/inpage/deep-query.js.map +1 -0
- package/dist/cdp/inpage/dom-helpers.d.ts +27 -0
- package/dist/cdp/inpage/dom-helpers.js +147 -0
- package/dist/cdp/inpage/dom-helpers.js.map +1 -0
- package/dist/cdp/keymap.d.ts +83 -0
- package/dist/cdp/keymap.js +205 -0
- package/dist/cdp/keymap.js.map +1 -0
- package/dist/cdp/settle.d.ts +38 -0
- package/dist/cdp/settle.js +76 -0
- package/dist/cdp/settle.js.map +1 -0
- package/dist/cdp/support.d.ts +26 -0
- package/dist/cdp/support.js +58 -0
- package/dist/cdp/support.js.map +1 -0
- package/dist/cdp/targets.d.ts +54 -0
- package/dist/cdp/targets.js +176 -0
- package/dist/cdp/targets.js.map +1 -0
- package/dist/cdp/transport.d.ts +31 -0
- package/dist/cdp/transport.js +531 -0
- package/dist/cdp/transport.js.map +1 -0
- package/dist/cli.js +50 -0
- package/dist/cli.js.map +1 -1
- package/dist/commands/about.js +13 -0
- package/dist/commands/about.js.map +1 -1
- package/dist/commands/cdp.d.ts +28 -0
- package/dist/commands/cdp.js +172 -0
- package/dist/commands/cdp.js.map +1 -0
- package/dist/commands/config.d.ts +92 -0
- package/dist/commands/config.js +173 -12
- package/dist/commands/config.js.map +1 -1
- package/dist/config.d.ts +70 -0
- package/dist/config.js +94 -1
- package/dist/config.js.map +1 -1
- package/dist/extension/background.js +58 -5
- package/dist/extension/background.js.map +1 -1
- package/dist/extension/inpage/recorder.js +25 -0
- package/dist/extension/inpage/recorder.js.map +1 -1
- package/dist/extension/manifest.json +1 -1
- package/dist/extension/recording.d.ts +39 -3
- package/dist/extension/recording.js +57 -3
- package/dist/extension/recording.js.map +1 -1
- package/dist/server.js +9 -0
- package/dist/server.js.map +1 -1
- package/dist/tools/browser-definitions.js +4 -3
- package/dist/tools/browser-definitions.js.map +1 -1
- package/dist/tools/browser-dispatch.d.ts +31 -0
- package/dist/tools/browser-dispatch.js +88 -40
- package/dist/tools/browser-dispatch.js.map +1 -1
- package/dist/tools/server-instructions.js +19 -0
- package/dist/tools/server-instructions.js.map +1 -1
- package/package.json +1 -1
package/dist/cdp/flow.js
ADDED
|
@@ -0,0 +1,217 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Independent copy of `packages/extension/src/flow.ts` — see
|
|
3
|
+
* `inpage/deep-query.ts`'s header comment for why this package duplicates
|
|
4
|
+
* rather than imports extension-side modules. Byte-identical logic, so
|
|
5
|
+
* `browser.flow` behaves identically whether the target tab is reached
|
|
6
|
+
* through the extension or through cdp-direct. Kept transport-agnostic ON
|
|
7
|
+
* PURPOSE — it does NOT re-check the cdp-direct gate per step; a mid-flow
|
|
8
|
+
* `cdp revoke` is grandfathered until the flow finishes (bounded ≤60s by
|
|
9
|
+
* the flow budget), documented in the README's cdp-direct section.
|
|
10
|
+
* `cdp/drift.test.ts` asserts `MAX_FLOW_STEPS` stays in lockstep with the
|
|
11
|
+
* sibling at `packages/extension/src/flow.ts`.
|
|
12
|
+
*
|
|
13
|
+
* browser.flow step sequencing — the logic behind the composite action
|
|
14
|
+
* tool. Runs a short, declarative list of find/click/type/press/wait_for
|
|
15
|
+
* steps strictly in order, fails fast on the first step that does not
|
|
16
|
+
* succeed, and threads an "implicit target" from a `find` step into the
|
|
17
|
+
* very next click/type/press step that omits its own `selector`.
|
|
18
|
+
*
|
|
19
|
+
* Deliberately decoupled from any transport: every side effect goes
|
|
20
|
+
* through the `FlowDeps` the caller injects. `cdp/transport.ts` wires the
|
|
21
|
+
* real perform* functions (already closed over the CDP client); tests wire
|
|
22
|
+
* fakes. This is what makes the sequencing logic — step-shape validation,
|
|
23
|
+
* implicit-target threading, fail-fast, result shaping — unit-testable
|
|
24
|
+
* without a real Chrome target.
|
|
25
|
+
*/
|
|
26
|
+
/** Hard cap on steps per flow — mirrors the `maxItems: 20` on the MCP
|
|
27
|
+
* schema (`browser-definitions.ts`) and the server-side validation
|
|
28
|
+
* (`browser-dispatch.ts`). Enforced again here so cdp-direct never runs an
|
|
29
|
+
* oversized flow even if a future caller bypasses the server's own check. */
|
|
30
|
+
export const MAX_FLOW_STEPS = 20;
|
|
31
|
+
const STEP_KINDS = ['find', 'click', 'type', 'press', 'wait_for'];
|
|
32
|
+
function isStepKind(key) {
|
|
33
|
+
return STEP_KINDS.includes(key);
|
|
34
|
+
}
|
|
35
|
+
/** Identify which of the 5 step kinds a raw (untrusted) step object is.
|
|
36
|
+
* Returns `null` unless the object has EXACTLY ONE key and that key is a
|
|
37
|
+
* recognized kind — extra keys (`{find: {...}, mystery: 1}`) reject, in
|
|
38
|
+
* line with the MCP schema's `additionalProperties: false`. The caller
|
|
39
|
+
* treats `null` as an "unknown step shape" flow failure. */
|
|
40
|
+
function stepKind(step) {
|
|
41
|
+
if (typeof step !== 'object' || step === null)
|
|
42
|
+
return null;
|
|
43
|
+
const keys = Object.keys(step);
|
|
44
|
+
if (keys.length !== 1)
|
|
45
|
+
return null;
|
|
46
|
+
return isStepKind(keys[0]) ? keys[0] : null;
|
|
47
|
+
}
|
|
48
|
+
/** Drop the noisy fields of a click/type/press result down to `{ ok, settle? }`
|
|
49
|
+
* for the flow's per-step result — the agent already knows what it asked
|
|
50
|
+
* for (the selector, the text length, the key); echoing that back N times
|
|
51
|
+
* in a row is not worth the tokens. `settle` is kept because it carries
|
|
52
|
+
* NEW information (did the page go quiet, did focus/URL drift). */
|
|
53
|
+
function compactActionResult(result) {
|
|
54
|
+
return result.settle ? { ok: true, settle: result.settle } : { ok: true };
|
|
55
|
+
}
|
|
56
|
+
async function withRecoverySnapshot(deps, index, kind, error, stepsCompleted) {
|
|
57
|
+
let recovery_snapshot = null;
|
|
58
|
+
try {
|
|
59
|
+
recovery_snapshot = await deps.buildRecoverySnapshot();
|
|
60
|
+
}
|
|
61
|
+
catch {
|
|
62
|
+
// Best-effort: if the tab is gone or CDP throws on the recovery
|
|
63
|
+
// snapshot too, still report the original step failure rather than
|
|
64
|
+
// masking it behind a secondary snapshot error.
|
|
65
|
+
}
|
|
66
|
+
return {
|
|
67
|
+
ok: false,
|
|
68
|
+
failed_step: index,
|
|
69
|
+
step_kind: kind,
|
|
70
|
+
error,
|
|
71
|
+
steps_completed: stepsCompleted,
|
|
72
|
+
recovery_snapshot,
|
|
73
|
+
};
|
|
74
|
+
}
|
|
75
|
+
/** Resolve a click/type/press step's target: an explicit `selector` always
|
|
76
|
+
* wins; otherwise fall back to the implicit target left by a preceding
|
|
77
|
+
* `find` step. `consumed` is true only when the implicit target was the
|
|
78
|
+
* one actually used — a step providing its own selector leaves the
|
|
79
|
+
* implicit target untouched for a LATER step to pick up. */
|
|
80
|
+
function resolveTarget(explicit, implicit) {
|
|
81
|
+
if (explicit !== undefined)
|
|
82
|
+
return { resolved: explicit, consumed: false };
|
|
83
|
+
return { resolved: implicit, consumed: implicit !== undefined };
|
|
84
|
+
}
|
|
85
|
+
/** Appended to a step failure when the PREVIOUS step's settle wait reported
|
|
86
|
+
* `context-destroyed` — that reason means the page navigated during or right
|
|
87
|
+
* after that step, so the failing step almost certainly raced the loading
|
|
88
|
+
* document rather than genuinely missing its target. */
|
|
89
|
+
const NAVIGATION_RACE_HINT = " (previous step's settle reported context-destroyed — the page likely navigated;" +
|
|
90
|
+
' insert a wait_for step after the navigating action so this step does not race the page load)';
|
|
91
|
+
/**
|
|
92
|
+
* Run a declarative sequence of steps strictly in order, stopping at the
|
|
93
|
+
* first failure. See the module doc comment for the implicit-target and
|
|
94
|
+
* result-shaping rules.
|
|
95
|
+
*
|
|
96
|
+
* `steps` is typed as `FlowStep[]` for ergonomic test-writing, but every
|
|
97
|
+
* element is re-validated at runtime via `stepKind()` before use — the
|
|
98
|
+
* array may originate from an untrusted JSON payload cast at the wire
|
|
99
|
+
* boundary in `cdp/transport.ts`, and a malformed entry must fail the flow
|
|
100
|
+
* cleanly rather than throw.
|
|
101
|
+
*/
|
|
102
|
+
export async function runFlow(steps, deps) {
|
|
103
|
+
if (!Array.isArray(steps) || steps.length === 0) {
|
|
104
|
+
return withRecoverySnapshot(deps, 0, 'none', 'flow: steps must be a non-empty array', 0);
|
|
105
|
+
}
|
|
106
|
+
if (steps.length > MAX_FLOW_STEPS) {
|
|
107
|
+
return withRecoverySnapshot(deps, 0, 'none', `flow: at most ${MAX_FLOW_STEPS} steps allowed, got ${steps.length}`, 0);
|
|
108
|
+
}
|
|
109
|
+
const results = [];
|
|
110
|
+
let implicitTarget;
|
|
111
|
+
/** `settle.reason` from the most recent SUCCESSFUL action step. When it
|
|
112
|
+
* is 'context-destroyed', the next step failure gets the page-load-race
|
|
113
|
+
* hint appended. Reset by any successful find/wait_for step — those
|
|
114
|
+
* prove the page is reachable again. */
|
|
115
|
+
let lastSettleReason;
|
|
116
|
+
const raceHint = () => lastSettleReason === 'context-destroyed' ? NAVIGATION_RACE_HINT : '';
|
|
117
|
+
for (let i = 0; i < steps.length; i++) {
|
|
118
|
+
const raw = steps[i];
|
|
119
|
+
const kind = stepKind(raw);
|
|
120
|
+
if (!kind) {
|
|
121
|
+
return withRecoverySnapshot(deps, i, 'unknown', 'flow: each step must be exactly one of find | click | type | press | wait_for', results.length);
|
|
122
|
+
}
|
|
123
|
+
const stepRecord = raw;
|
|
124
|
+
if (kind === 'find') {
|
|
125
|
+
const params = stepRecord.find;
|
|
126
|
+
const outcome = await deps.performFind(params);
|
|
127
|
+
if (!outcome.ok) {
|
|
128
|
+
return withRecoverySnapshot(deps, i, kind, outcome.error + raceHint(), results.length);
|
|
129
|
+
}
|
|
130
|
+
const r = outcome.result;
|
|
131
|
+
if (!r.matched) {
|
|
132
|
+
const candidateNote = r.reason === 'multiple-matches' && r.candidates
|
|
133
|
+
? ` — candidates: ${JSON.stringify(r.candidates)}`
|
|
134
|
+
: '';
|
|
135
|
+
return withRecoverySnapshot(deps, i, kind, `find: ${r.reason ?? 'not-found'}${candidateNote}${raceHint()}`, results.length);
|
|
136
|
+
}
|
|
137
|
+
if (r.ambiguous) {
|
|
138
|
+
return withRecoverySnapshot(deps, i, kind, `find: selector "${r.selector ?? ''}" is ambiguous across shadow roots/iframes — refine text/role for a unique match`, results.length);
|
|
139
|
+
}
|
|
140
|
+
implicitTarget = r.selector;
|
|
141
|
+
lastSettleReason = undefined;
|
|
142
|
+
results.push({ selector: r.selector });
|
|
143
|
+
continue;
|
|
144
|
+
}
|
|
145
|
+
if (kind === 'wait_for') {
|
|
146
|
+
const params = stepRecord.wait_for;
|
|
147
|
+
const outcome = await deps.performWaitFor(params);
|
|
148
|
+
if (!outcome.ok) {
|
|
149
|
+
return withRecoverySnapshot(deps, i, kind, outcome.error + raceHint(), results.length);
|
|
150
|
+
}
|
|
151
|
+
if (!outcome.result.matched) {
|
|
152
|
+
// No race hint here: a wait_for step IS the recommended remedy for
|
|
153
|
+
// a navigation race — a plain timeout is a genuine non-match.
|
|
154
|
+
return withRecoverySnapshot(deps, i, kind, `wait_for: condition not met within timeout (${outcome.result.elapsed_ms}ms, ${outcome.result.checks} checks)`, results.length);
|
|
155
|
+
}
|
|
156
|
+
lastSettleReason = undefined;
|
|
157
|
+
results.push({ ok: true });
|
|
158
|
+
continue;
|
|
159
|
+
}
|
|
160
|
+
if (kind === 'click') {
|
|
161
|
+
const params = stepRecord.click;
|
|
162
|
+
const { resolved, consumed } = resolveTarget(params.selector, implicitTarget);
|
|
163
|
+
if (consumed)
|
|
164
|
+
implicitTarget = undefined;
|
|
165
|
+
if (resolved === undefined) {
|
|
166
|
+
return withRecoverySnapshot(deps, i, kind, 'click: no selector provided and no implicit target from a preceding find', results.length);
|
|
167
|
+
}
|
|
168
|
+
const outcome = await deps.performClick({
|
|
169
|
+
force: params.force,
|
|
170
|
+
settle_ms: params.settle_ms,
|
|
171
|
+
selector: resolved,
|
|
172
|
+
});
|
|
173
|
+
if (!outcome.ok) {
|
|
174
|
+
return withRecoverySnapshot(deps, i, kind, outcome.error + raceHint(), results.length);
|
|
175
|
+
}
|
|
176
|
+
lastSettleReason = outcome.result.settle?.reason;
|
|
177
|
+
results.push(compactActionResult(outcome.result));
|
|
178
|
+
continue;
|
|
179
|
+
}
|
|
180
|
+
if (kind === 'type') {
|
|
181
|
+
const params = stepRecord.type;
|
|
182
|
+
const { resolved, consumed } = resolveTarget(params.selector, implicitTarget);
|
|
183
|
+
if (consumed)
|
|
184
|
+
implicitTarget = undefined;
|
|
185
|
+
const outcome = await deps.performType({
|
|
186
|
+
text: params.text,
|
|
187
|
+
clear: params.clear,
|
|
188
|
+
settle_ms: params.settle_ms,
|
|
189
|
+
selector: resolved,
|
|
190
|
+
});
|
|
191
|
+
if (!outcome.ok) {
|
|
192
|
+
return withRecoverySnapshot(deps, i, kind, outcome.error + raceHint(), results.length);
|
|
193
|
+
}
|
|
194
|
+
lastSettleReason = outcome.result.settle?.reason;
|
|
195
|
+
results.push(compactActionResult(outcome.result));
|
|
196
|
+
continue;
|
|
197
|
+
}
|
|
198
|
+
// press — the only kind left after the checks above.
|
|
199
|
+
const params = stepRecord.press;
|
|
200
|
+
const { resolved, consumed } = resolveTarget(params.selector, implicitTarget);
|
|
201
|
+
if (consumed)
|
|
202
|
+
implicitTarget = undefined;
|
|
203
|
+
const outcome = await deps.performPress({
|
|
204
|
+
key: params.key,
|
|
205
|
+
modifiers: params.modifiers,
|
|
206
|
+
settle_ms: params.settle_ms,
|
|
207
|
+
selector: resolved,
|
|
208
|
+
});
|
|
209
|
+
if (!outcome.ok) {
|
|
210
|
+
return withRecoverySnapshot(deps, i, kind, outcome.error + raceHint(), results.length);
|
|
211
|
+
}
|
|
212
|
+
lastSettleReason = outcome.result.settle?.reason;
|
|
213
|
+
results.push(compactActionResult(outcome.result));
|
|
214
|
+
}
|
|
215
|
+
return { ok: true, steps_completed: results.length, results };
|
|
216
|
+
}
|
|
217
|
+
//# sourceMappingURL=flow.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"flow.js","sourceRoot":"","sources":["../../src/cdp/flow.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;;;;;;;;;;;;GAwBG;AASH;;;6EAG6E;AAC7E,MAAM,CAAC,MAAM,cAAc,GAAG,EAAE,CAAC;AA2FjC,MAAM,UAAU,GAAwB,CAAC,MAAM,EAAE,OAAO,EAAE,MAAM,EAAE,OAAO,EAAE,UAAU,CAAC,CAAC;AAEvF,SAAS,UAAU,CAAC,GAAW;IAC7B,OAAQ,UAAgC,CAAC,QAAQ,CAAC,GAAG,CAAC,CAAC;AACzD,CAAC;AAED;;;;4DAI4D;AAC5D,SAAS,QAAQ,CAAC,IAAa;IAC7B,IAAI,OAAO,IAAI,KAAK,QAAQ,IAAI,IAAI,KAAK,IAAI;QAAE,OAAO,IAAI,CAAC;IAC3D,MAAM,IAAI,GAAG,MAAM,CAAC,IAAI,CAAC,IAAI,CAAC,CAAC;IAC/B,IAAI,IAAI,CAAC,MAAM,KAAK,CAAC;QAAE,OAAO,IAAI,CAAC;IACnC,OAAO,UAAU,CAAC,IAAI,CAAC,CAAC,CAAC,CAAC,CAAC,CAAC,CAAC,IAAI,CAAC,CAAC,CAAC,CAAC,CAAC,CAAC,IAAI,CAAC;AAC9C,CAAC;AAqCD;;;;mEAImE;AACnE,SAAS,mBAAmB,CAAC,MAA4C;IAIvE,OAAO,MAAM,CAAC,MAAM,CAAC,CAAC,CAAC,EAAE,EAAE,EAAE,IAAI,EAAE,MAAM,EAAE,MAAM,CAAC,MAAM,EAAE,CAAC,CAAC,CAAC,EAAE,EAAE,EAAE,IAAI,EAAE,CAAC;AAC5E,CAAC;AAED,KAAK,UAAU,oBAAoB,CACjC,IAAc,EACd,KAAa,EACb,IAAY,EACZ,KAAa,EACb,cAAsB;IAEtB,IAAI,iBAAiB,GAAY,IAAI,CAAC;IACtC,IAAI,CAAC;QACH,iBAAiB,GAAG,MAAM,IAAI,CAAC,qBAAqB,EAAE,CAAC;IACzD,CAAC;IAAC,MAAM,CAAC;QACP,gEAAgE;QAChE,mEAAmE;QACnE,gDAAgD;IAClD,CAAC;IACD,OAAO;QACL,EAAE,EAAE,KAAK;QACT,WAAW,EAAE,KAAK;QAClB,SAAS,EAAE,IAAI;QACf,KAAK;QACL,eAAe,EAAE,cAAc;QAC/B,iBAAiB;KAClB,CAAC;AACJ,CAAC;AAED;;;;4DAI4D;AAC5D,SAAS,aAAa,CACpB,QAA4B,EAC5B,QAA4B;IAE5B,IAAI,QAAQ,KAAK,SAAS;QAAE,OAAO,EAAE,QAAQ,EAAE,QAAQ,EAAE,QAAQ,EAAE,KAAK,EAAE,CAAC;IAC3E,OAAO,EAAE,QAAQ,EAAE,QAAQ,EAAE,QAAQ,EAAE,QAAQ,KAAK,SAAS,EAAE,CAAC;AAClE,CAAC;AAED;;;wDAGwD;AACxD,MAAM,oBAAoB,GACxB,kFAAkF;IAClF,+FAA+F,CAAC;AAElG;;;;;;;;;;GAUG;AACH,MAAM,CAAC,KAAK,UAAU,OAAO,CAAC,KAA0B,EAAE,IAAc;IACtE,IAAI,CAAC,KAAK,CAAC,OAAO,CAAC,KAAK,CAAC,IAAI,KAAK,CAAC,MAAM,KAAK,CAAC,EAAE,CAAC;QAChD,OAAO,oBAAoB,CAAC,IAAI,EAAE,CAAC,EAAE,MAAM,EAAE,uCAAuC,EAAE,CAAC,CAAC,CAAC;IAC3F,CAAC;IACD,IAAI,KAAK,CAAC,MAAM,GAAG,cAAc,EAAE,CAAC;QAClC,OAAO,oBAAoB,CACzB,IAAI,EACJ,CAAC,EACD,MAAM,EACN,iBAAiB,cAAc,uBAAuB,KAAK,CAAC,MAAM,EAAE,EACpE,CAAC,CACF,CAAC;IACJ,CAAC;IAED,MAAM,OAAO,GAAc,EAAE,CAAC;IAC9B,IAAI,cAAkC,CAAC;IACvC;;;4CAGwC;IACxC,IAAI,gBAAyB,CAAC;IAC9B,MAAM,QAAQ,GAAG,GAAW,EAAE,CAC5B,gBAAgB,KAAK,mBAAmB,CAAC,CAAC,CAAC,oBAAoB,CAAC,CAAC,CAAC,EAAE,CAAC;IAEvE,KAAK,IAAI,CAAC,GAAG,CAAC,EAAE,CAAC,GAAG,KAAK,CAAC,MAAM,EAAE,CAAC,EAAE,EAAE,CAAC;QACtC,MAAM,GAAG,GAAY,KAAK,CAAC,CAAC,CAAC,CAAC;QAC9B,MAAM,IAAI,GAAG,QAAQ,CAAC,GAAG,CAAC,CAAC;QAC3B,IAAI,CAAC,IAAI,EAAE,CAAC;YACV,OAAO,oBAAoB,CACzB,IAAI,EACJ,CAAC,EACD,SAAS,EACT,+EAA+E,EAC/E,OAAO,CAAC,MAAM,CACf,CAAC;QACJ,CAAC;QACD,MAAM,UAAU,GAAG,GAAgC,CAAC;QAEpD,IAAI,IAAI,KAAK,MAAM,EAAE,CAAC;YACpB,MAAM,MAAM,GAAG,UAAU,CAAC,IAAsB,CAAC;YACjD,MAAM,OAAO,GAAG,MAAM,IAAI,CAAC,WAAW,CAAC,MAAM,CAAC,CAAC;YAC/C,IAAI,CAAC,OAAO,CAAC,EAAE,EAAE,CAAC;gBAChB,OAAO,oBAAoB,CAAC,IAAI,EAAE,CAAC,EAAE,IAAI,EAAE,OAAO,CAAC,KAAK,GAAG,QAAQ,EAAE,EAAE,OAAO,CAAC,MAAM,CAAC,CAAC;YACzF,CAAC;YACD,MAAM,CAAC,GAAG,OAAO,CAAC,MAAM,CAAC;YACzB,IAAI,CAAC,CAAC,CAAC,OAAO,EAAE,CAAC;gBACf,MAAM,aAAa,GACjB,CAAC,CAAC,MAAM,KAAK,kBAAkB,IAAI,CAAC,CAAC,UAAU;oBAC7C,CAAC,CAAC,kBAAkB,IAAI,CAAC,SAAS,CAAC,CAAC,CAAC,UAAU,CAAC,EAAE;oBAClD,CAAC,CAAC,EAAE,CAAC;gBACT,OAAO,oBAAoB,CACzB,IAAI,EACJ,CAAC,EACD,IAAI,EACJ,SAAS,CAAC,CAAC,MAAM,IAAI,WAAW,GAAG,aAAa,GAAG,QAAQ,EAAE,EAAE,EAC/D,OAAO,CAAC,MAAM,CACf,CAAC;YACJ,CAAC;YACD,IAAI,CAAC,CAAC,SAAS,EAAE,CAAC;gBAChB,OAAO,oBAAoB,CACzB,IAAI,EACJ,CAAC,EACD,IAAI,EACJ,mBAAmB,CAAC,CAAC,QAAQ,IAAI,EAAE,kFAAkF,EACrH,OAAO,CAAC,MAAM,CACf,CAAC;YACJ,CAAC;YACD,cAAc,GAAG,CAAC,CAAC,QAAQ,CAAC;YAC5B,gBAAgB,GAAG,SAAS,CAAC;YAC7B,OAAO,CAAC,IAAI,CAAC,EAAE,QAAQ,EAAE,CAAC,CAAC,QAAQ,EAAE,CAAC,CAAC;YACvC,SAAS;QACX,CAAC;QAED,IAAI,IAAI,KAAK,UAAU,EAAE,CAAC;YACxB,MAAM,MAAM,GAAG,UAAU,CAAC,QAA6B,CAAC;YACxD,MAAM,OAAO,GAAG,MAAM,IAAI,CAAC,cAAc,CAAC,MAAM,CAAC,CAAC;YAClD,IAAI,CAAC,OAAO,CAAC,EAAE,EAAE,CAAC;gBAChB,OAAO,oBAAoB,CAAC,IAAI,EAAE,CAAC,EAAE,IAAI,EAAE,OAAO,CAAC,KAAK,GAAG,QAAQ,EAAE,EAAE,OAAO,CAAC,MAAM,CAAC,CAAC;YACzF,CAAC;YACD,IAAI,CAAC,OAAO,CAAC,MAAM,CAAC,OAAO,EAAE,CAAC;gBAC5B,mEAAmE;gBACnE,8DAA8D;gBAC9D,OAAO,oBAAoB,CACzB,IAAI,EACJ,CAAC,EACD,IAAI,EACJ,+CAA+C,OAAO,CAAC,MAAM,CAAC,UAAU,OAAO,OAAO,CAAC,MAAM,CAAC,MAAM,UAAU,EAC9G,OAAO,CAAC,MAAM,CACf,CAAC;YACJ,CAAC;YACD,gBAAgB,GAAG,SAAS,CAAC;YAC7B,OAAO,CAAC,IAAI,CAAC,EAAE,EAAE,EAAE,IAAI,EAAE,CAAC,CAAC;YAC3B,SAAS;QACX,CAAC;QAED,IAAI,IAAI,KAAK,OAAO,EAAE,CAAC;YACrB,MAAM,MAAM,GAAG,UAAU,CAAC,KAAwB,CAAC;YACnD,MAAM,EAAE,QAAQ,EAAE,QAAQ,EAAE,GAAG,aAAa,CAAC,MAAM,CAAC,QAAQ,EAAE,cAAc,CAAC,CAAC;YAC9E,IAAI,QAAQ;gBAAE,cAAc,GAAG,SAAS,CAAC;YACzC,IAAI,QAAQ,KAAK,SAAS,EAAE,CAAC;gBAC3B,OAAO,oBAAoB,CACzB,IAAI,EACJ,CAAC,EACD,IAAI,EACJ,0EAA0E,EAC1E,OAAO,CAAC,MAAM,CACf,CAAC;YACJ,CAAC;YACD,MAAM,OAAO,GAAG,MAAM,IAAI,CAAC,YAAY,CAAC;gBACtC,KAAK,EAAE,MAAM,CAAC,KAAK;gBACnB,SAAS,EAAE,MAAM,CAAC,SAAS;gBAC3B,QAAQ,EAAE,QAAQ;aACnB,CAAC,CAAC;YACH,IAAI,CAAC,OAAO,CAAC,EAAE,EAAE,CAAC;gBAChB,OAAO,oBAAoB,CAAC,IAAI,EAAE,CAAC,EAAE,IAAI,EAAE,OAAO,CAAC,KAAK,GAAG,QAAQ,EAAE,EAAE,OAAO,CAAC,MAAM,CAAC,CAAC;YACzF,CAAC;YACD,gBAAgB,GAAG,OAAO,CAAC,MAAM,CAAC,MAAM,EAAE,MAAM,CAAC;YACjD,OAAO,CAAC,IAAI,CAAC,mBAAmB,CAAC,OAAO,CAAC,MAAM,CAAC,CAAC,CAAC;YAClD,SAAS;QACX,CAAC;QAED,IAAI,IAAI,KAAK,MAAM,EAAE,CAAC;YACpB,MAAM,MAAM,GAAG,UAAU,CAAC,IAAsB,CAAC;YACjD,MAAM,EAAE,QAAQ,EAAE,QAAQ,EAAE,GAAG,aAAa,CAAC,MAAM,CAAC,QAAQ,EAAE,cAAc,CAAC,CAAC;YAC9E,IAAI,QAAQ;gBAAE,cAAc,GAAG,SAAS,CAAC;YACzC,MAAM,OAAO,GAAG,MAAM,IAAI,CAAC,WAAW,CAAC;gBACrC,IAAI,EAAE,MAAM,CAAC,IAAI;gBACjB,KAAK,EAAE,MAAM,CAAC,KAAK;gBACnB,SAAS,EAAE,MAAM,CAAC,SAAS;gBAC3B,QAAQ,EAAE,QAAQ;aACnB,CAAC,CAAC;YACH,IAAI,CAAC,OAAO,CAAC,EAAE,EAAE,CAAC;gBAChB,OAAO,oBAAoB,CAAC,IAAI,EAAE,CAAC,EAAE,IAAI,EAAE,OAAO,CAAC,KAAK,GAAG,QAAQ,EAAE,EAAE,OAAO,CAAC,MAAM,CAAC,CAAC;YACzF,CAAC;YACD,gBAAgB,GAAG,OAAO,CAAC,MAAM,CAAC,MAAM,EAAE,MAAM,CAAC;YACjD,OAAO,CAAC,IAAI,CAAC,mBAAmB,CAAC,OAAO,CAAC,MAAM,CAAC,CAAC,CAAC;YAClD,SAAS;QACX,CAAC;QAED,qDAAqD;QACrD,MAAM,MAAM,GAAG,UAAU,CAAC,KAAwB,CAAC;QACnD,MAAM,EAAE,QAAQ,EAAE,QAAQ,EAAE,GAAG,aAAa,CAAC,MAAM,CAAC,QAAQ,EAAE,cAAc,CAAC,CAAC;QAC9E,IAAI,QAAQ;YAAE,cAAc,GAAG,SAAS,CAAC;QACzC,MAAM,OAAO,GAAG,MAAM,IAAI,CAAC,YAAY,CAAC;YACtC,GAAG,EAAE,MAAM,CAAC,GAAG;YACf,SAAS,EAAE,MAAM,CAAC,SAAS;YAC3B,SAAS,EAAE,MAAM,CAAC,SAAS;YAC3B,QAAQ,EAAE,QAAQ;SACnB,CAAC,CAAC;QACH,IAAI,CAAC,OAAO,CAAC,EAAE,EAAE,CAAC;YAChB,OAAO,oBAAoB,CAAC,IAAI,EAAE,CAAC,EAAE,IAAI,EAAE,OAAO,CAAC,KAAK,GAAG,QAAQ,EAAE,EAAE,OAAO,CAAC,MAAM,CAAC,CAAC;QACzF,CAAC;QACD,gBAAgB,GAAG,OAAO,CAAC,MAAM,CAAC,MAAM,EAAE,MAAM,CAAC;QACjD,OAAO,CAAC,IAAI,CAAC,mBAAmB,CAAC,OAAO,CAAC,MAAM,CAAC,CAAC,CAAC;IACpD,CAAC;IAED,OAAO,EAAE,EAAE,EAAE,IAAI,EAAE,eAAe,EAAE,OAAO,CAAC,MAAM,EAAE,OAAO,EAAE,CAAC;AAChE,CAAC"}
|
|
@@ -0,0 +1,31 @@
|
|
|
1
|
+
export type CdpDirectGate = {
|
|
2
|
+
ok: true;
|
|
3
|
+
} | {
|
|
4
|
+
ok: false;
|
|
5
|
+
error: string;
|
|
6
|
+
};
|
|
7
|
+
/** Exact wording agents receive when cdp-direct is off entirely. Agents get
|
|
8
|
+
* this string and NOTHING else — there is no bypass, no partial access, no
|
|
9
|
+
* alternate path to a `cdp:` tab while this check fails. */
|
|
10
|
+
export declare const CDP_DIRECT_DISABLED_ERROR = "cdp-direct is disabled. The user can enable it with: browser-link config set cdp-direct.enabled true";
|
|
11
|
+
/** Exact wording agents receive when cdp-direct is enabled but no live
|
|
12
|
+
* grant exists (never granted, revoked, or expired). */
|
|
13
|
+
export declare const CDP_DIRECT_NO_GRANT_ERROR = "cdp-direct requires an active grant. Ask the user to run: browser-link cdp allow";
|
|
14
|
+
/**
|
|
15
|
+
* The ONE gate every cdp-direct code path runs through, in the exact order
|
|
16
|
+
* the feature's permission model requires: (1) is cdp-direct enabled at
|
|
17
|
+
* all, (2) is there a live, unexpired grant. Re-evaluated on every call —
|
|
18
|
+
* never cached — because either condition can flip mid-session (the user
|
|
19
|
+
* disables the setting, or a time-boxed grant simply expires) and the very
|
|
20
|
+
* next tool call must see that.
|
|
21
|
+
*
|
|
22
|
+
* Called from two places by design: `tools/browser-dispatch.ts`'s routing
|
|
23
|
+
* layer (so an agent gets the exact error text above before any cdp-direct
|
|
24
|
+
* code runs at all) AND `cdp/transport.ts` itself (defense in depth, so the
|
|
25
|
+
* transport module is never safe to call without this check even if a
|
|
26
|
+
* future caller reaches it directly). `cdp/targets.ts`'s `browser.list_tabs`
|
|
27
|
+
* discovery also gates on this, but treats a failing gate as "show nothing"
|
|
28
|
+
* rather than an error — an agent that has never heard of cdp-direct should
|
|
29
|
+
* not see it mentioned in a routine list_tabs call.
|
|
30
|
+
*/
|
|
31
|
+
export declare function checkCdpDirectGate(now?: number): CdpDirectGate;
|
package/dist/cdp/gate.js
ADDED
|
@@ -0,0 +1,35 @@
|
|
|
1
|
+
import { loadConfig } from '../config.js';
|
|
2
|
+
import { isGrantLive, loadGrant } from './grant.js';
|
|
3
|
+
/** Exact wording agents receive when cdp-direct is off entirely. Agents get
|
|
4
|
+
* this string and NOTHING else — there is no bypass, no partial access, no
|
|
5
|
+
* alternate path to a `cdp:` tab while this check fails. */
|
|
6
|
+
export const CDP_DIRECT_DISABLED_ERROR = 'cdp-direct is disabled. The user can enable it with: browser-link config set cdp-direct.enabled true';
|
|
7
|
+
/** Exact wording agents receive when cdp-direct is enabled but no live
|
|
8
|
+
* grant exists (never granted, revoked, or expired). */
|
|
9
|
+
export const CDP_DIRECT_NO_GRANT_ERROR = 'cdp-direct requires an active grant. Ask the user to run: browser-link cdp allow';
|
|
10
|
+
/**
|
|
11
|
+
* The ONE gate every cdp-direct code path runs through, in the exact order
|
|
12
|
+
* the feature's permission model requires: (1) is cdp-direct enabled at
|
|
13
|
+
* all, (2) is there a live, unexpired grant. Re-evaluated on every call —
|
|
14
|
+
* never cached — because either condition can flip mid-session (the user
|
|
15
|
+
* disables the setting, or a time-boxed grant simply expires) and the very
|
|
16
|
+
* next tool call must see that.
|
|
17
|
+
*
|
|
18
|
+
* Called from two places by design: `tools/browser-dispatch.ts`'s routing
|
|
19
|
+
* layer (so an agent gets the exact error text above before any cdp-direct
|
|
20
|
+
* code runs at all) AND `cdp/transport.ts` itself (defense in depth, so the
|
|
21
|
+
* transport module is never safe to call without this check even if a
|
|
22
|
+
* future caller reaches it directly). `cdp/targets.ts`'s `browser.list_tabs`
|
|
23
|
+
* discovery also gates on this, but treats a failing gate as "show nothing"
|
|
24
|
+
* rather than an error — an agent that has never heard of cdp-direct should
|
|
25
|
+
* not see it mentioned in a routine list_tabs call.
|
|
26
|
+
*/
|
|
27
|
+
export function checkCdpDirectGate(now = Date.now()) {
|
|
28
|
+
const cfg = loadConfig();
|
|
29
|
+
if (cfg.cdpDirectEnabled !== true)
|
|
30
|
+
return { ok: false, error: CDP_DIRECT_DISABLED_ERROR };
|
|
31
|
+
if (!isGrantLive(loadGrant(), now))
|
|
32
|
+
return { ok: false, error: CDP_DIRECT_NO_GRANT_ERROR };
|
|
33
|
+
return { ok: true };
|
|
34
|
+
}
|
|
35
|
+
//# sourceMappingURL=gate.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"gate.js","sourceRoot":"","sources":["../../src/cdp/gate.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,UAAU,EAAE,MAAM,cAAc,CAAC;AAC1C,OAAO,EAAE,WAAW,EAAE,SAAS,EAAE,MAAM,YAAY,CAAC;AAIpD;;4DAE4D;AAC5D,MAAM,CAAC,MAAM,yBAAyB,GACpC,sGAAsG,CAAC;AAEzG;wDACwD;AACxD,MAAM,CAAC,MAAM,yBAAyB,GACpC,kFAAkF,CAAC;AAErF;;;;;;;;;;;;;;;;GAgBG;AACH,MAAM,UAAU,kBAAkB,CAAC,MAAc,IAAI,CAAC,GAAG,EAAE;IACzD,MAAM,GAAG,GAAG,UAAU,EAAE,CAAC;IACzB,IAAI,GAAG,CAAC,gBAAgB,KAAK,IAAI;QAAE,OAAO,EAAE,EAAE,EAAE,KAAK,EAAE,KAAK,EAAE,yBAAyB,EAAE,CAAC;IAC1F,IAAI,CAAC,WAAW,CAAC,SAAS,EAAE,EAAE,GAAG,CAAC;QAAE,OAAO,EAAE,EAAE,EAAE,KAAK,EAAE,KAAK,EAAE,yBAAyB,EAAE,CAAC;IAC3F,OAAO,EAAE,EAAE,EAAE,IAAI,EAAE,CAAC;AACtB,CAAC"}
|
|
@@ -0,0 +1,45 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* On-disk record of a `browser-link cdp allow` grant — the time-boxed human
|
|
3
|
+
* consent that, together with `cdp-direct.enabled`, is required before any
|
|
4
|
+
* tool may address a `cdp:` tab (see `gate.ts`). Lives in its own small JSON
|
|
5
|
+
* file in the data dir, never in the repo, never in `config.json` (a grant
|
|
6
|
+
* is a momentary permission, not a persistent preference).
|
|
7
|
+
*/
|
|
8
|
+
export interface CdpGrant {
|
|
9
|
+
grantedAt: number;
|
|
10
|
+
/** `null` means the grant never expires ("never" / `--minutes 0`) —
|
|
11
|
+
* documented in the CLI help and README as reducing the security
|
|
12
|
+
* posture, not a default anyone should reach for casually. */
|
|
13
|
+
expiresAt: number | null;
|
|
14
|
+
}
|
|
15
|
+
/** Absolute path of the grant file — exported so the CLI can name it in an
|
|
16
|
+
* honest "could not remove" error (see `commands/cdp.ts`) rather than
|
|
17
|
+
* leaving the user guessing which file to delete by hand. */
|
|
18
|
+
export declare function grantFilePath(): string;
|
|
19
|
+
/** Read the current grant, or `null` when none exists or the file is
|
|
20
|
+
* corrupt/unreadable — corruption degrades to "no grant" (the safer
|
|
21
|
+
* failure mode for a permission file) rather than throwing. */
|
|
22
|
+
export declare function loadGrant(): CdpGrant | null;
|
|
23
|
+
/** Record a fresh grant. `ttlMinutes: 0` means "never expires"
|
|
24
|
+
* (`expiresAt: null`); any other value is the grant's lifetime in minutes
|
|
25
|
+
* from `now`. `now` is a parameter so tests do not need to mock the clock. */
|
|
26
|
+
export declare function saveGrant(ttlMinutes: number, now?: number): CdpGrant;
|
|
27
|
+
/** Revoke the current grant, if any. Idempotent — revoking with no grant
|
|
28
|
+
* present is not an error and is a no-op.
|
|
29
|
+
*
|
|
30
|
+
* Deliberately does NOT swallow an unlink failure: this is a SECURITY
|
|
31
|
+
* operation, and a grant file that survives a "revoke" (a transient
|
|
32
|
+
* Windows lock, an EPERM, a read-only dir) is still honoured by the gate
|
|
33
|
+
* on the very next tool call — so reporting success while the door stays
|
|
34
|
+
* open would be a lie. The error propagates; the CLI (`revokeCdpDirect` in
|
|
35
|
+
* `commands/cdp.ts`) catches it and tells the user the grant may still be
|
|
36
|
+
* active and to remove the file by hand. */
|
|
37
|
+
export declare function clearGrant(): void;
|
|
38
|
+
/** Whether `grant` is currently live: present, and either non-expiring or
|
|
39
|
+
* not yet past its `expiresAt`. */
|
|
40
|
+
export declare function isGrantLive(grant: CdpGrant | null, now?: number): boolean;
|
|
41
|
+
/** Milliseconds remaining on `grant`, or `null` when it never expires or
|
|
42
|
+
* does not exist (callers distinguish "no grant" from "never expires" by
|
|
43
|
+
* checking `grant` itself first). Never negative — an expired grant reads
|
|
44
|
+
* as `0` remaining, not a negative number. */
|
|
45
|
+
export declare function remainingMs(grant: CdpGrant | null, now?: number): number | null;
|
|
@@ -0,0 +1,81 @@
|
|
|
1
|
+
import { existsSync, mkdirSync, readFileSync, unlinkSync, writeFileSync } from 'node:fs';
|
|
2
|
+
import { dirname, join } from 'node:path';
|
|
3
|
+
import { getDataDir } from '../map/paths.js';
|
|
4
|
+
function grantFile() {
|
|
5
|
+
return join(getDataDir(), 'cdp-grant.json');
|
|
6
|
+
}
|
|
7
|
+
/** Absolute path of the grant file — exported so the CLI can name it in an
|
|
8
|
+
* honest "could not remove" error (see `commands/cdp.ts`) rather than
|
|
9
|
+
* leaving the user guessing which file to delete by hand. */
|
|
10
|
+
export function grantFilePath() {
|
|
11
|
+
return grantFile();
|
|
12
|
+
}
|
|
13
|
+
function isCdpGrant(value) {
|
|
14
|
+
if (typeof value !== 'object' || value === null)
|
|
15
|
+
return false;
|
|
16
|
+
const v = value;
|
|
17
|
+
return (typeof v.grantedAt === 'number' && (v.expiresAt === null || typeof v.expiresAt === 'number'));
|
|
18
|
+
}
|
|
19
|
+
/** Read the current grant, or `null` when none exists or the file is
|
|
20
|
+
* corrupt/unreadable — corruption degrades to "no grant" (the safer
|
|
21
|
+
* failure mode for a permission file) rather than throwing. */
|
|
22
|
+
export function loadGrant() {
|
|
23
|
+
const path = grantFile();
|
|
24
|
+
if (!existsSync(path))
|
|
25
|
+
return null;
|
|
26
|
+
try {
|
|
27
|
+
const parsed = JSON.parse(readFileSync(path, 'utf8'));
|
|
28
|
+
return isCdpGrant(parsed) ? parsed : null;
|
|
29
|
+
}
|
|
30
|
+
catch {
|
|
31
|
+
return null;
|
|
32
|
+
}
|
|
33
|
+
}
|
|
34
|
+
/** Record a fresh grant. `ttlMinutes: 0` means "never expires"
|
|
35
|
+
* (`expiresAt: null`); any other value is the grant's lifetime in minutes
|
|
36
|
+
* from `now`. `now` is a parameter so tests do not need to mock the clock. */
|
|
37
|
+
export function saveGrant(ttlMinutes, now = Date.now()) {
|
|
38
|
+
const path = grantFile();
|
|
39
|
+
mkdirSync(dirname(path), { recursive: true });
|
|
40
|
+
const grant = {
|
|
41
|
+
grantedAt: now,
|
|
42
|
+
expiresAt: ttlMinutes === 0 ? null : now + ttlMinutes * 60_000,
|
|
43
|
+
};
|
|
44
|
+
writeFileSync(path, JSON.stringify(grant, null, 2) + '\n', 'utf8');
|
|
45
|
+
return grant;
|
|
46
|
+
}
|
|
47
|
+
/** Revoke the current grant, if any. Idempotent — revoking with no grant
|
|
48
|
+
* present is not an error and is a no-op.
|
|
49
|
+
*
|
|
50
|
+
* Deliberately does NOT swallow an unlink failure: this is a SECURITY
|
|
51
|
+
* operation, and a grant file that survives a "revoke" (a transient
|
|
52
|
+
* Windows lock, an EPERM, a read-only dir) is still honoured by the gate
|
|
53
|
+
* on the very next tool call — so reporting success while the door stays
|
|
54
|
+
* open would be a lie. The error propagates; the CLI (`revokeCdpDirect` in
|
|
55
|
+
* `commands/cdp.ts`) catches it and tells the user the grant may still be
|
|
56
|
+
* active and to remove the file by hand. */
|
|
57
|
+
export function clearGrant() {
|
|
58
|
+
const path = grantFile();
|
|
59
|
+
if (!existsSync(path))
|
|
60
|
+
return;
|
|
61
|
+
unlinkSync(path);
|
|
62
|
+
}
|
|
63
|
+
/** Whether `grant` is currently live: present, and either non-expiring or
|
|
64
|
+
* not yet past its `expiresAt`. */
|
|
65
|
+
export function isGrantLive(grant, now = Date.now()) {
|
|
66
|
+
if (!grant)
|
|
67
|
+
return false;
|
|
68
|
+
if (grant.expiresAt === null)
|
|
69
|
+
return true;
|
|
70
|
+
return grant.expiresAt > now;
|
|
71
|
+
}
|
|
72
|
+
/** Milliseconds remaining on `grant`, or `null` when it never expires or
|
|
73
|
+
* does not exist (callers distinguish "no grant" from "never expires" by
|
|
74
|
+
* checking `grant` itself first). Never negative — an expired grant reads
|
|
75
|
+
* as `0` remaining, not a negative number. */
|
|
76
|
+
export function remainingMs(grant, now = Date.now()) {
|
|
77
|
+
if (!grant || grant.expiresAt === null)
|
|
78
|
+
return null;
|
|
79
|
+
return Math.max(0, grant.expiresAt - now);
|
|
80
|
+
}
|
|
81
|
+
//# sourceMappingURL=grant.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"grant.js","sourceRoot":"","sources":["../../src/cdp/grant.ts"],"names":[],"mappings":"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"}
|
|
@@ -0,0 +1,165 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Build the snapshot expression. Filters are applied INSIDE the page so the
|
|
3
|
+
* server never receives the dropped material at all — that's where the token
|
|
4
|
+
* win for `within_selector` / `only_interactive` / `exclude` actually lives.
|
|
5
|
+
* The serializer omits empty-string fields per entry (token win that applies
|
|
6
|
+
* unconditionally on every snapshot).
|
|
7
|
+
*
|
|
8
|
+
* The scan pierces open Shadow DOM roots and same-origin iframes via
|
|
9
|
+
* `deepQueryAll` (see `deep-query.ts`). `within_selector` itself is resolved
|
|
10
|
+
* with `deepQueryFirst`, so a subtree inside a shadow root or iframe can be
|
|
11
|
+
* targeted too. Matched entries that live inside an iframe carry an
|
|
12
|
+
* optional `frame` field with the innermost hosting iframe's selector, and
|
|
13
|
+
* entries whose generated selector could not be made unique across the deep
|
|
14
|
+
* search scope carry `ambiguous: true` (see `genSelectorInfo`).
|
|
15
|
+
*/
|
|
16
|
+
export interface SnapshotOpts {
|
|
17
|
+
within_selector?: string;
|
|
18
|
+
only_interactive?: boolean;
|
|
19
|
+
exclude?: string[];
|
|
20
|
+
max_interactive?: number;
|
|
21
|
+
}
|
|
22
|
+
export declare function buildSnapshotJs(opts?: SnapshotOpts): string;
|
|
23
|
+
/** Build the find-by-text expression. Returns one of:
|
|
24
|
+
* { matched: true, selector, coords:{x,y}, tag, text, frame?, ambiguous? }
|
|
25
|
+
* { matched: false, reason: 'not-found', error?, near_misses?: [{text,selector,role?}] }
|
|
26
|
+
* { matched: false, reason: 'multiple-matches', candidates: [{selector,text,tag}] }
|
|
27
|
+
*
|
|
28
|
+
* Role-aware: when `role` is provided, only elements whose explicit ARIA
|
|
29
|
+
* role or implicit role match are considered. When omitted, the search
|
|
30
|
+
* scans a broad set of interactive + clickable elements.
|
|
31
|
+
*
|
|
32
|
+
* The scan pierces open Shadow DOM roots and same-origin iframes via
|
|
33
|
+
* `deepQueryAll`. `coords` are mapped to TOP-LEVEL viewport coordinates
|
|
34
|
+
* (accounting for ancestor iframe offsets) so they are directly usable by
|
|
35
|
+
* `browser.drag` and other coordinate-based tools.
|
|
36
|
+
*
|
|
37
|
+
* `not-found` carries up to 3 `near_misses` — VISIBLE interactive-ish
|
|
38
|
+
* elements ranked by (1) case-insensitive substring containment of the
|
|
39
|
+
* query in the candidate's accessible text/name, then (2) token overlap —
|
|
40
|
+
* so the agent gets a "did you mean" instead of a bare miss. Omitted
|
|
41
|
+
* entirely when nothing scores (no junk suggestions). When `role` narrowed
|
|
42
|
+
* the scan and the text DOES exist outside that role, `error` names the
|
|
43
|
+
* exclusion explicitly (e.g. `text matched 2 elements but none with role
|
|
44
|
+
* "button" — closest: <div onclick> "GIF picker"`) and `near_misses` is
|
|
45
|
+
* populated from that broader, role-agnostic match set instead.
|
|
46
|
+
*
|
|
47
|
+
* Near-miss entries use `genSelector` (not `genSelectorInfo`), so the
|
|
48
|
+
* `ambiguous` flag is intentionally NOT carried — a near-miss selector
|
|
49
|
+
* that collides with structurally-identical twins in other roots resolves
|
|
50
|
+
* first-match-wins silently. Documented on the tool as: hints for the
|
|
51
|
+
* next `find`, never click targets.
|
|
52
|
+
*/
|
|
53
|
+
export interface FindOpts {
|
|
54
|
+
text: string;
|
|
55
|
+
role?: string;
|
|
56
|
+
exact?: boolean;
|
|
57
|
+
}
|
|
58
|
+
export declare function buildFindJs(opts: FindOpts): string;
|
|
59
|
+
/**
|
|
60
|
+
* Build the click-resolution expression: resolves the selector across the
|
|
61
|
+
* deep search scope, scrolls the element (and every ancestor iframe) into
|
|
62
|
+
* view, computes the TOP-LEVEL viewport click point, and — unless `force`
|
|
63
|
+
* is set — hit-tests that point before returning it, so a covering overlay
|
|
64
|
+
* is caught before dispatching CDP mouse events. Returns:
|
|
65
|
+
* { ok: true, x, y, tag }
|
|
66
|
+
* { ok: false, reason: 'invalid-selector', error }
|
|
67
|
+
* { ok: false, reason: 'not-found' }
|
|
68
|
+
* { ok: false, reason: 'occluded', blocker }
|
|
69
|
+
*
|
|
70
|
+
* The `invalid-selector` check runs BEFORE `deepQueryFirst` — a malformed
|
|
71
|
+
* CSS selector throws the identical `SyntaxError` from every root's own
|
|
72
|
+
* `querySelector`, which `deepQueryFirst`'s per-root try/catch otherwise
|
|
73
|
+
* swallows into an indistinguishable "not-found". Checking once against
|
|
74
|
+
* `document` up front separates "the selector text is broken" from "the
|
|
75
|
+
* selector is valid but nothing on the page matches it".
|
|
76
|
+
*/
|
|
77
|
+
export interface ClickResolveOpts {
|
|
78
|
+
selector: string;
|
|
79
|
+
force: boolean;
|
|
80
|
+
}
|
|
81
|
+
export declare function buildClickResolveJs(opts: ClickResolveOpts): string;
|
|
82
|
+
/**
|
|
83
|
+
* Build the type-resolution expression: resolves the selector across the
|
|
84
|
+
* deep search scope, focuses it, optionally clears its value, and reports
|
|
85
|
+
* success so the caller can follow up with CDP `Input.insertText` (which
|
|
86
|
+
* types into whatever currently has focus at the browser level — works the
|
|
87
|
+
* same whether that focus target lives in the top document, a shadow root,
|
|
88
|
+
* or a same-origin iframe). Returns:
|
|
89
|
+
* { ok: true }
|
|
90
|
+
* { ok: false, reason: 'invalid-selector', error }
|
|
91
|
+
* { ok: false, reason: 'not-found' }
|
|
92
|
+
*
|
|
93
|
+
* Same invalid-selector pre-check as `buildClickResolveJs` — see its doc
|
|
94
|
+
* comment for why the check has to run before `deepQueryFirst`.
|
|
95
|
+
*/
|
|
96
|
+
export interface TypeResolveOpts {
|
|
97
|
+
selector: string;
|
|
98
|
+
clear: boolean;
|
|
99
|
+
}
|
|
100
|
+
export declare function buildTypeResolveJs(opts: TypeResolveOpts): string;
|
|
101
|
+
/**
|
|
102
|
+
* Build the focus-resolution expression for `browser.press`'s optional
|
|
103
|
+
* `selector`: resolves the selector across the deep search scope and
|
|
104
|
+
* focuses it, reporting success so the caller can follow up with the CDP
|
|
105
|
+
* key event sequence (which — like `Input.insertText` — targets whatever
|
|
106
|
+
* currently has focus at the browser level). Intentionally separate from
|
|
107
|
+
* `buildTypeResolveJs`: press never clears a value, and naming it
|
|
108
|
+
* distinctly keeps the press code path readable on its own.
|
|
109
|
+
*/
|
|
110
|
+
export interface FocusResolveOpts {
|
|
111
|
+
selector: string;
|
|
112
|
+
}
|
|
113
|
+
export declare function buildFocusJs(opts: FocusResolveOpts): string;
|
|
114
|
+
/**
|
|
115
|
+
* Build the settle-await expression shared by `browser.click`, `.type` and
|
|
116
|
+
* `.press`. Called AFTER the action's CDP input events have been
|
|
117
|
+
* dispatched: installs one `MutationObserver` on `document` (subtree,
|
|
118
|
+
* childList, attributes, characterData) and resolves once no mutation has
|
|
119
|
+
* landed for `settle_ms` consecutive milliseconds, or once `settle_timeout_ms`
|
|
120
|
+
* total has elapsed — whichever comes first. Returned as a Promise so the
|
|
121
|
+
* caller's `evaluateInTab` (CDP `Runtime.evaluate` with `awaitPromise: true`)
|
|
122
|
+
* waits for it naturally.
|
|
123
|
+
*
|
|
124
|
+
* Baseline `url`/`activeElement` are captured at the TOP of this
|
|
125
|
+
* expression — i.e. right after the action's own focus/navigation side
|
|
126
|
+
* effects have already landed (dispatch already happened by the time this
|
|
127
|
+
* runs), so `focus_moved`/`url_changed` report drift that happened DURING
|
|
128
|
+
* the settle window, not the action's own expected effect.
|
|
129
|
+
*
|
|
130
|
+
* The result omits `url_changed` / `focus_moved` when they did not change
|
|
131
|
+
* — token-lean by construction, no post-filtering needed.
|
|
132
|
+
*/
|
|
133
|
+
export interface SettleOpts {
|
|
134
|
+
/** Quiet-period length in ms. Caller is expected to have already
|
|
135
|
+
* clamped this to (0, 2000] — 0 means "don't call this builder at all". */
|
|
136
|
+
settle_ms: number;
|
|
137
|
+
/** Overall cap in ms. Caller is expected to have already clamped this
|
|
138
|
+
* to [0, 10000]. */
|
|
139
|
+
settle_timeout_ms: number;
|
|
140
|
+
}
|
|
141
|
+
export declare function buildSettleJs(opts: SettleOpts): string;
|
|
142
|
+
/**
|
|
143
|
+
* Build the `browser.state` expression: a compact orientation snapshot —
|
|
144
|
+
* current url/title, the deep-resolved focused element, visible dialog-role
|
|
145
|
+
* elements, scroll position, and viewport size. Cheaper than a full
|
|
146
|
+
* `browser.snapshot` when the agent only needs "where am I right now".
|
|
147
|
+
*
|
|
148
|
+
* `focused` descends through `document.activeElement` past shadow-root and
|
|
149
|
+
* same-origin-iframe boundaries to the real innermost focused element (a
|
|
150
|
+
* shadow host or an <iframe> being "active" at one level is not the actual
|
|
151
|
+
* focus target the agent cares about). Its `selector` goes through the same
|
|
152
|
+
* `genSelectorInfo` uniqueness check `snapshot`/`find` use, so it carries
|
|
153
|
+
* `ambiguous: true` under the same structurally-identical-twins condition.
|
|
154
|
+
*
|
|
155
|
+
* `dialogs` matches visible `[role=dialog]`, `[role=alertdialog]` and open
|
|
156
|
+
* `<dialog>` elements found via the same deep walk, with a best-effort
|
|
157
|
+
* `label` resolved from aria-label / aria-labelledby / the first heading
|
|
158
|
+
* inside the dialog.
|
|
159
|
+
*
|
|
160
|
+
* Every optional field is omitted when there is nothing to report: no
|
|
161
|
+
* `focused` beyond `<body>`, no `dialogs` when none are open, and no
|
|
162
|
+
* `scroll` when the page is at its default (0,0) position — token-lean by
|
|
163
|
+
* construction, matching the omit-falsy convention the other builders use.
|
|
164
|
+
*/
|
|
165
|
+
export declare function buildStateJs(): string;
|