craftdriver 1.7.0 → 1.8.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/CHANGELOG.md +23 -11
- package/README.md +13 -4
- package/dist/cli/agentSession.d.ts +62 -0
- package/dist/cli/agentSession.d.ts.map +1 -0
- package/dist/cli/agentSession.js +100 -0
- package/dist/cli/agentSession.js.map +1 -0
- package/dist/cli/artifactPaths.d.ts +42 -0
- package/dist/cli/artifactPaths.d.ts.map +1 -0
- package/dist/cli/artifactPaths.js +144 -0
- package/dist/cli/artifactPaths.js.map +1 -0
- package/dist/cli/bounds.d.ts +88 -0
- package/dist/cli/bounds.d.ts.map +1 -0
- package/dist/cli/bounds.js +128 -0
- package/dist/cli/bounds.js.map +1 -0
- package/dist/cli/client.d.ts +3 -0
- package/dist/cli/client.d.ts.map +1 -1
- package/dist/cli/client.js +24 -8
- package/dist/cli/client.js.map +1 -1
- package/dist/cli/daemon.d.ts +3 -0
- package/dist/cli/daemon.d.ts.map +1 -1
- package/dist/cli/daemon.js +152 -39
- package/dist/cli/daemon.js.map +1 -1
- package/dist/cli/defaults.d.ts +23 -3
- package/dist/cli/defaults.d.ts.map +1 -1
- package/dist/cli/defaults.js +50 -5
- package/dist/cli/defaults.js.map +1 -1
- package/dist/cli/dispatcher.d.ts +54 -11
- package/dist/cli/dispatcher.d.ts.map +1 -1
- package/dist/cli/dispatcher.js +851 -54
- package/dist/cli/dispatcher.js.map +1 -1
- package/dist/cli/index.d.ts.map +1 -1
- package/dist/cli/index.js +305 -56
- package/dist/cli/index.js.map +1 -1
- package/dist/cli/init.d.ts +14 -14
- package/dist/cli/init.d.ts.map +1 -1
- package/dist/cli/init.js +328 -180
- package/dist/cli/init.js.map +1 -1
- package/dist/cli/journal.d.ts +153 -0
- package/dist/cli/journal.d.ts.map +1 -0
- package/dist/cli/journal.js +252 -0
- package/dist/cli/journal.js.map +1 -0
- package/dist/cli/lineReader.d.ts +33 -0
- package/dist/cli/lineReader.d.ts.map +1 -0
- package/dist/cli/lineReader.js +66 -0
- package/dist/cli/lineReader.js.map +1 -0
- package/dist/cli/locatorCandidates.d.ts +44 -0
- package/dist/cli/locatorCandidates.d.ts.map +1 -0
- package/dist/cli/locatorCandidates.js +190 -0
- package/dist/cli/locatorCandidates.js.map +1 -0
- package/dist/cli/mcp/artifacts.d.ts +39 -1
- package/dist/cli/mcp/artifacts.d.ts.map +1 -1
- package/dist/cli/mcp/artifacts.js +104 -9
- package/dist/cli/mcp/artifacts.js.map +1 -1
- package/dist/cli/mcp/bounds.d.ts +65 -0
- package/dist/cli/mcp/bounds.d.ts.map +1 -0
- package/dist/cli/mcp/bounds.js +124 -0
- package/dist/cli/mcp/bounds.js.map +1 -0
- package/dist/cli/mcp/params.d.ts +38 -0
- package/dist/cli/mcp/params.d.ts.map +1 -0
- package/dist/cli/mcp/params.js +174 -0
- package/dist/cli/mcp/params.js.map +1 -0
- package/dist/cli/mcp/server.d.ts +94 -16
- package/dist/cli/mcp/server.d.ts.map +1 -1
- package/dist/cli/mcp/server.js +304 -150
- package/dist/cli/mcp/server.js.map +1 -1
- package/dist/cli/mcp/tools.d.ts +62 -17
- package/dist/cli/mcp/tools.d.ts.map +1 -1
- package/dist/cli/mcp/tools.js +373 -158
- package/dist/cli/mcp/tools.js.map +1 -1
- package/dist/cli/pageSemantics.d.ts +42 -0
- package/dist/cli/pageSemantics.d.ts.map +1 -0
- package/dist/cli/pageSemantics.js +194 -0
- package/dist/cli/pageSemantics.js.map +1 -0
- package/dist/cli/parseArgs.d.ts +6 -18
- package/dist/cli/parseArgs.d.ts.map +1 -1
- package/dist/cli/parseArgs.js +689 -45
- package/dist/cli/parseArgs.js.map +1 -1
- package/dist/cli/protocol.d.ts +8 -0
- package/dist/cli/protocol.d.ts.map +1 -1
- package/dist/cli/sessionRegistry.d.ts +42 -0
- package/dist/cli/sessionRegistry.d.ts.map +1 -0
- package/dist/cli/sessionRegistry.js +99 -0
- package/dist/cli/sessionRegistry.js.map +1 -0
- package/dist/cli/snapshot.d.ts +100 -18
- package/dist/cli/snapshot.d.ts.map +1 -1
- package/dist/cli/snapshot.js +248 -102
- package/dist/cli/snapshot.js.map +1 -1
- package/dist/cli/stateStore.d.ts +50 -0
- package/dist/cli/stateStore.d.ts.map +1 -0
- package/dist/cli/stateStore.js +122 -0
- package/dist/cli/stateStore.js.map +1 -0
- package/dist/index.d.ts +1 -1
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js.map +1 -1
- package/dist/lib/bidi/index.d.ts +1 -1
- package/dist/lib/bidi/index.d.ts.map +1 -1
- package/dist/lib/bidi/index.js +2 -2
- package/dist/lib/bidi/index.js.map +1 -1
- package/dist/lib/bidi/logs.d.ts +3 -0
- package/dist/lib/bidi/logs.d.ts.map +1 -1
- package/dist/lib/bidi/logs.js +7 -0
- package/dist/lib/bidi/logs.js.map +1 -1
- package/dist/lib/bidi/network.d.ts +14 -1
- package/dist/lib/bidi/network.d.ts.map +1 -1
- package/dist/lib/bidi/network.js +45 -7
- package/dist/lib/bidi/network.js.map +1 -1
- package/dist/lib/bidi/storage.d.ts +30 -6
- package/dist/lib/bidi/storage.d.ts.map +1 -1
- package/dist/lib/bidi/storage.js +190 -45
- package/dist/lib/bidi/storage.js.map +1 -1
- package/dist/lib/bidi/types.d.ts +7 -2
- package/dist/lib/bidi/types.d.ts.map +1 -1
- package/dist/lib/browser.d.ts +6 -3
- package/dist/lib/browser.d.ts.map +1 -1
- package/dist/lib/browser.js +140 -30
- package/dist/lib/browser.js.map +1 -1
- package/dist/lib/browserContext.d.ts +54 -18
- package/dist/lib/browserContext.d.ts.map +1 -1
- package/dist/lib/browserContext.js +295 -92
- package/dist/lib/browserContext.js.map +1 -1
- package/dist/lib/by.d.ts +3 -3
- package/dist/lib/by.d.ts.map +1 -1
- package/dist/lib/by.js +7 -3
- package/dist/lib/by.js.map +1 -1
- package/dist/lib/driver.d.ts +7 -0
- package/dist/lib/driver.d.ts.map +1 -1
- package/dist/lib/driver.js +12 -0
- package/dist/lib/driver.js.map +1 -1
- package/dist/lib/errors.d.ts +8 -0
- package/dist/lib/errors.d.ts.map +1 -1
- package/dist/lib/errors.js +8 -0
- package/dist/lib/errors.js.map +1 -1
- package/dist/lib/initScript.d.ts +9 -0
- package/dist/lib/initScript.d.ts.map +1 -0
- package/dist/lib/initScript.js +14 -0
- package/dist/lib/initScript.js.map +1 -0
- package/dist/lib/page.d.ts +22 -0
- package/dist/lib/page.d.ts.map +1 -1
- package/dist/lib/page.js +48 -0
- package/dist/lib/page.js.map +1 -1
- package/dist/lib/secureFile.d.ts +8 -0
- package/dist/lib/secureFile.d.ts.map +1 -0
- package/dist/lib/secureFile.js +67 -0
- package/dist/lib/secureFile.js.map +1 -0
- package/dist/lib/sessionStateValidation.d.ts +35 -0
- package/dist/lib/sessionStateValidation.d.ts.map +1 -0
- package/dist/lib/sessionStateValidation.js +223 -0
- package/dist/lib/sessionStateValidation.js.map +1 -0
- package/dist/lib/tracing.d.ts +2 -1
- package/dist/lib/tracing.d.ts.map +1 -1
- package/dist/lib/tracing.js +12 -1
- package/dist/lib/tracing.js.map +1 -1
- package/docs/agents.md +56 -61
- package/docs/api-reference.md +2 -1
- package/docs/browser-api.md +24 -4
- package/docs/browser-context.md +18 -12
- package/docs/cli.md +346 -26
- package/docs/error-codes.md +5 -4
- package/docs/getting-started.md +6 -1
- package/docs/index.md +1 -1
- package/docs/mcp.md +129 -62
- package/docs/public/examples/agent-actions.html +48 -0
- package/docs/public/examples/agent-debug.html +86 -0
- package/docs/public/examples/agent-late-mutation.html +18 -0
- package/docs/public/examples/agent-ref-shift.html +33 -0
- package/docs/recipes/login-once-reuse-session.md +32 -4
- package/docs/session-management.md +64 -3
- package/docs/why-craftdriver.md +1 -1
- package/package.json +3 -1
- package/skills/craftdriver/SKILL.md +59 -75
- package/skills/craftdriver/agents/openai.yaml +4 -0
- package/skills/craftdriver/cheatsheet.md +18 -4
- package/skills/craftdriver/cli.md +194 -68
- package/skills/craftdriver/patterns.md +14 -8
- package/skills/craftdriver/workflow.md +149 -0
package/dist/cli/snapshot.js
CHANGED
|
@@ -1,23 +1,96 @@
|
|
|
1
|
+
import { PAGE_SEMANTICS_JS } from './pageSemantics.js';
|
|
1
2
|
const MAX_NODES = 80;
|
|
2
3
|
const MAX_NAME = 80;
|
|
4
|
+
/**
|
|
5
|
+
* Upper bound on refs issued per document. Reaching it resets the registry
|
|
6
|
+
* under a fresh `documentId`, which invalidates every outstanding ref
|
|
7
|
+
* rather than letting numbers wrap onto live elements.
|
|
8
|
+
*/
|
|
9
|
+
const MAX_REFS = 2000;
|
|
3
10
|
/**
|
|
4
11
|
* Build a fresh snapshot of the active page. Errors swallowed so a
|
|
5
12
|
* partial post-action payload never breaks the protocol.
|
|
6
13
|
*/
|
|
7
|
-
export async function takeSnapshot(browser) {
|
|
14
|
+
export async function takeSnapshot(browser, minRef = 0) {
|
|
8
15
|
try {
|
|
9
16
|
const page = await browser.activePage();
|
|
10
17
|
const [url, title] = await Promise.all([
|
|
11
18
|
page.url().catch(() => ''),
|
|
12
19
|
page.title().catch(() => ''),
|
|
13
20
|
]);
|
|
14
|
-
const
|
|
15
|
-
|
|
21
|
+
const raw = await page.evaluate(jsSnapshot(minRef));
|
|
22
|
+
if (!raw || typeof raw !== 'object')
|
|
23
|
+
return null;
|
|
24
|
+
const shaped = raw;
|
|
25
|
+
return {
|
|
26
|
+
url,
|
|
27
|
+
title,
|
|
28
|
+
lines: Array.isArray(shaped.lines) ? shaped.lines : [],
|
|
29
|
+
documentId: typeof shaped.documentId === 'string' ? shaped.documentId : '',
|
|
30
|
+
revision: 0,
|
|
31
|
+
refs: Array.isArray(shaped.refs) ? shaped.refs : [],
|
|
32
|
+
nextRef: typeof shaped.nextRef === 'number' ? shaped.nextRef : minRef + 1,
|
|
33
|
+
};
|
|
34
|
+
}
|
|
35
|
+
catch {
|
|
36
|
+
return null;
|
|
37
|
+
}
|
|
38
|
+
}
|
|
39
|
+
/**
|
|
40
|
+
* Return the text of an open JavaScript dialog, or null if none is open.
|
|
41
|
+
*
|
|
42
|
+
* Cheap (~2ms) and, unlike almost every other command, safe to call while
|
|
43
|
+
* a modal dialog is blocking the page. That matters: a modal blocks script
|
|
44
|
+
* execution, so an unguarded post-action snapshot sits there until the
|
|
45
|
+
* WebDriver script timeout — a measured 60 seconds — before failing.
|
|
46
|
+
*/
|
|
47
|
+
export async function peekDialog(browser) {
|
|
48
|
+
try {
|
|
49
|
+
return await browser.getDialogMessage();
|
|
16
50
|
}
|
|
17
51
|
catch {
|
|
52
|
+
// The driver reports "no such alert" by throwing; that is the common case.
|
|
18
53
|
return null;
|
|
19
54
|
}
|
|
20
55
|
}
|
|
56
|
+
/**
|
|
57
|
+
* Ask the page whether `ref` still identifies exactly one live element.
|
|
58
|
+
*
|
|
59
|
+
* `expectedDocumentId` is the document the ref was issued against; a
|
|
60
|
+
* mismatch means the page navigated and every outstanding ref is stale.
|
|
61
|
+
*/
|
|
62
|
+
export async function probeRef(browser, ref, expectedDocumentId) {
|
|
63
|
+
if (!/^e\d+$/.test(ref))
|
|
64
|
+
return { status: 'unknown-ref', documentId: null };
|
|
65
|
+
try {
|
|
66
|
+
const page = await browser.activePage();
|
|
67
|
+
const js = jsProbeRef(ref, expectedDocumentId);
|
|
68
|
+
const raw = await page.evaluate(js);
|
|
69
|
+
if (!raw || typeof raw !== 'object')
|
|
70
|
+
return { status: 'no-registry', documentId: null };
|
|
71
|
+
const shaped = raw;
|
|
72
|
+
return {
|
|
73
|
+
status: (typeof shaped.status === 'string' ? shaped.status : 'no-registry'),
|
|
74
|
+
documentId: typeof shaped.documentId === 'string' ? shaped.documentId : null,
|
|
75
|
+
...(typeof shaped.count === 'number' ? { count: shaped.count } : {}),
|
|
76
|
+
};
|
|
77
|
+
}
|
|
78
|
+
catch {
|
|
79
|
+
return { status: 'no-registry', documentId: null };
|
|
80
|
+
}
|
|
81
|
+
}
|
|
82
|
+
/** Human-readable reason attached to a STALE_REF failure. */
|
|
83
|
+
export function describeRefStatus(status) {
|
|
84
|
+
switch (status) {
|
|
85
|
+
case 'no-snapshot': return 'no snapshot has been taken in this session yet';
|
|
86
|
+
case 'no-registry': return 'the page has no ref registry (it navigated or was replaced)';
|
|
87
|
+
case 'document-changed': return 'the page navigated or reloaded after the ref was issued';
|
|
88
|
+
case 'unknown-ref': return 'the ref was never issued for the current document';
|
|
89
|
+
case 'detached': return 'the element was removed from the document';
|
|
90
|
+
case 'ambiguous': return 'the ref no longer identifies exactly one element';
|
|
91
|
+
default: return 'the ref is no longer valid';
|
|
92
|
+
}
|
|
93
|
+
}
|
|
21
94
|
/**
|
|
22
95
|
* Render a snapshot as a compact, agent-friendly string. Either the
|
|
23
96
|
* full snapshot (first call in a session) or a `+/-` diff vs. the
|
|
@@ -26,10 +99,8 @@ export async function takeSnapshot(browser) {
|
|
|
26
99
|
* Diff format intentionally mirrors `diff -u` lines so agents
|
|
27
100
|
* already familiar with unified diffs read it without instruction.
|
|
28
101
|
*
|
|
29
|
-
*
|
|
30
|
-
*
|
|
31
|
-
* the set-difference and re-attach them when rendering, so the diff
|
|
32
|
-
* stays semantically stable across renumberings.
|
|
102
|
+
* Refs are stripped before the set-difference and re-attached when
|
|
103
|
+
* rendering, so a diff reports semantic change rather than renumbering.
|
|
33
104
|
*/
|
|
34
105
|
export function renderDelta(prev, next) {
|
|
35
106
|
if (!next)
|
|
@@ -41,15 +112,41 @@ export function renderDelta(prev, next) {
|
|
|
41
112
|
return `${header}\n(no interactive elements detected)`;
|
|
42
113
|
return `${header}\n${next.lines.join('\n')}`;
|
|
43
114
|
}
|
|
44
|
-
|
|
45
|
-
|
|
115
|
+
// A new document means the old lines describe a page that no longer
|
|
116
|
+
// exists; a diff against them would be noise. Same-URL reloads count.
|
|
117
|
+
if (prev.documentId !== next.documentId || prev.url !== next.url) {
|
|
46
118
|
return `${header}\n${next.lines.join('\n')}`;
|
|
47
119
|
}
|
|
48
120
|
const strip = (l) => l.replace(/^e\d+:\s*/, '');
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
const
|
|
121
|
+
// Count occurrences rather than testing membership: a page with three
|
|
122
|
+
// identical "Remove" buttons must still report a change when one of them
|
|
123
|
+
// goes, and a set-difference silently calls that "no changes".
|
|
124
|
+
const tally = (lines) => {
|
|
125
|
+
const counts = new Map();
|
|
126
|
+
for (const line of lines) {
|
|
127
|
+
const body = strip(line);
|
|
128
|
+
counts.set(body, (counts.get(body) ?? 0) + 1);
|
|
129
|
+
}
|
|
130
|
+
return counts;
|
|
131
|
+
};
|
|
132
|
+
const prevCounts = tally(prev.lines);
|
|
133
|
+
const nextCounts = tally(next.lines);
|
|
134
|
+
const take = (lines, budget) => {
|
|
135
|
+
const out = [];
|
|
136
|
+
const seen = new Map();
|
|
137
|
+
for (const line of lines) {
|
|
138
|
+
const body = strip(line);
|
|
139
|
+
const used = seen.get(body) ?? 0;
|
|
140
|
+
// Report the surplus copies only — the first N matching lines are
|
|
141
|
+
// accounted for by the other side.
|
|
142
|
+
if (used >= (budget.get(body) ?? 0))
|
|
143
|
+
out.push(line);
|
|
144
|
+
seen.set(body, used + 1);
|
|
145
|
+
}
|
|
146
|
+
return out;
|
|
147
|
+
};
|
|
148
|
+
const removed = take(prev.lines, nextCounts).map((l) => `- ${l}`);
|
|
149
|
+
const added = take(next.lines, prevCounts).map((l) => `+ ${l}`);
|
|
53
150
|
if (removed.length === 0 && added.length === 0) {
|
|
54
151
|
return `${header}\n(no a11y changes)`;
|
|
55
152
|
}
|
|
@@ -69,20 +166,139 @@ export function renderFull(next) {
|
|
|
69
166
|
}
|
|
70
167
|
return `${header}\n${next.lines.join('\n')}`;
|
|
71
168
|
}
|
|
169
|
+
/**
|
|
170
|
+
* Owns the one snapshot baseline and ref-issuing document for a session.
|
|
171
|
+
*
|
|
172
|
+
* Both explicit `snapshot` and the automatic post-action snapshot record
|
|
173
|
+
* through here, so an explicit snapshot the agent has already been shown
|
|
174
|
+
* is never re-reported as a change caused by the next action.
|
|
175
|
+
*/
|
|
176
|
+
export class SnapshotTracker {
|
|
177
|
+
baseline = null;
|
|
178
|
+
revision = 0;
|
|
179
|
+
highWater = 0;
|
|
180
|
+
/** The document outstanding refs were issued against, if any. */
|
|
181
|
+
get documentId() {
|
|
182
|
+
return this.baseline ? this.baseline.documentId : null;
|
|
183
|
+
}
|
|
184
|
+
get current() {
|
|
185
|
+
return this.baseline;
|
|
186
|
+
}
|
|
187
|
+
/**
|
|
188
|
+
* Highest ref number issued anywhere in this session.
|
|
189
|
+
*
|
|
190
|
+
* Seeding each new document above this is what stops a reload from
|
|
191
|
+
* reissuing `e2` to a different element: a ref the agent still holds
|
|
192
|
+
* would otherwise silently resolve against the new page.
|
|
193
|
+
*/
|
|
194
|
+
get minRef() {
|
|
195
|
+
return this.highWater;
|
|
196
|
+
}
|
|
197
|
+
/** Stamp a session-monotonic revision and make this the new baseline. */
|
|
198
|
+
record(snap) {
|
|
199
|
+
if (!snap)
|
|
200
|
+
return null;
|
|
201
|
+
const stamped = { ...snap, revision: ++this.revision };
|
|
202
|
+
this.baseline = stamped;
|
|
203
|
+
this.highWater = Math.max(this.highWater, stamped.nextRef - 1);
|
|
204
|
+
return stamped;
|
|
205
|
+
}
|
|
206
|
+
/** Diff against the baseline and advance it in one step. */
|
|
207
|
+
advance(snap) {
|
|
208
|
+
if (!snap)
|
|
209
|
+
return '';
|
|
210
|
+
const delta = renderDelta(this.baseline, { ...snap, revision: this.revision + 1 });
|
|
211
|
+
this.record(snap);
|
|
212
|
+
return delta;
|
|
213
|
+
}
|
|
214
|
+
reset() {
|
|
215
|
+
this.baseline = null;
|
|
216
|
+
}
|
|
217
|
+
}
|
|
218
|
+
/**
|
|
219
|
+
* Ref-validation probe. `ref` is `^e\d+$`-validated before it gets here and
|
|
220
|
+
* both values are JSON-encoded into the source, so neither can break out.
|
|
221
|
+
*/
|
|
222
|
+
function jsProbeRef(ref, expectedDocumentId) {
|
|
223
|
+
return `
|
|
224
|
+
const reg = window.__craftdriverRefs;
|
|
225
|
+
const ref = ${JSON.stringify(ref)};
|
|
226
|
+
const expected = ${JSON.stringify(expectedDocumentId)};
|
|
227
|
+
if (!reg || !reg.map) return { status: 'no-registry', documentId: null };
|
|
228
|
+
if (expected !== null && reg.doc !== expected) {
|
|
229
|
+
return { status: 'document-changed', documentId: reg.doc };
|
|
230
|
+
}
|
|
231
|
+
const el = reg.map.get(ref);
|
|
232
|
+
if (!el) return { status: 'unknown-ref', documentId: reg.doc };
|
|
233
|
+
if (!document.contains(el)) return { status: 'detached', documentId: reg.doc };
|
|
234
|
+
const marked = document.querySelectorAll('[data-craftdriver-ref="' + ref + '"]');
|
|
235
|
+
if (marked.length !== 1 || marked[0] !== el) {
|
|
236
|
+
return { status: 'ambiguous', documentId: reg.doc, count: marked.length };
|
|
237
|
+
}
|
|
238
|
+
return { status: 'ok', documentId: reg.doc };
|
|
239
|
+
`;
|
|
240
|
+
}
|
|
72
241
|
/**
|
|
73
242
|
* JS executed in-page. Keep it self-contained — no closure capture.
|
|
74
243
|
*
|
|
75
244
|
* Strategy:
|
|
76
|
-
* 1.
|
|
77
|
-
* 2.
|
|
78
|
-
* 3.
|
|
79
|
-
*
|
|
80
|
-
*
|
|
245
|
+
* 1. Get or create the per-document ref registry.
|
|
246
|
+
* 2. Walk a curated set of selectors (interactive + structural).
|
|
247
|
+
* 3. Skip nodes outside the viewport-ish bounds and zero-sized nodes.
|
|
248
|
+
* 4. Reuse each node's existing ref, or issue the next unused one.
|
|
249
|
+
* 5. Cap output at MAX_NODES so the snapshot stays bounded regardless
|
|
81
250
|
* of page complexity.
|
|
82
251
|
*/
|
|
83
|
-
|
|
252
|
+
function jsSnapshot(minRef) {
|
|
253
|
+
const floor = Number.isSafeInteger(minRef) && minRef >= 0 ? minRef : 0;
|
|
254
|
+
return `
|
|
84
255
|
const MAX_NODES = ${MAX_NODES};
|
|
85
256
|
const MAX_NAME = ${MAX_NAME};
|
|
257
|
+
const MAX_REFS = ${MAX_REFS};
|
|
258
|
+
const MIN_REF = ${floor};
|
|
259
|
+
function newDocId(epoch) {
|
|
260
|
+
return 'd' + epoch + '-' + Math.random().toString(36).slice(2, 10);
|
|
261
|
+
}
|
|
262
|
+
function registry() {
|
|
263
|
+
let reg = window.__craftdriverRefs;
|
|
264
|
+
if (!reg || !reg.map) {
|
|
265
|
+
reg = { doc: newDocId(1), epoch: 1, next: 1, issued: 0, map: new Map() };
|
|
266
|
+
window.__craftdriverRefs = reg;
|
|
267
|
+
}
|
|
268
|
+
// Never reissue a number the session has already handed out. A fresh
|
|
269
|
+
// document starts its counter above the session high-water mark, so a
|
|
270
|
+
// ref captured before a navigation can never match a new element.
|
|
271
|
+
if (reg.next <= MIN_REF) reg.next = MIN_REF + 1;
|
|
272
|
+
// Bounded: at the cap, drop every marker and re-key the document so all
|
|
273
|
+
// outstanding refs fail as stale instead of wrapping onto live elements.
|
|
274
|
+
//
|
|
275
|
+
// The cap counts refs issued *by this registry*, not the counter value.
|
|
276
|
+
// Counting the counter would compare against the session-wide high-water
|
|
277
|
+
// mark, which passes the cap after enough navigations and would then
|
|
278
|
+
// re-key on every single snapshot — invalidating refs the instant they
|
|
279
|
+
// were handed out.
|
|
280
|
+
if (reg.issued >= MAX_REFS) {
|
|
281
|
+
const marked = document.querySelectorAll('[data-craftdriver-ref]');
|
|
282
|
+
for (let i = 0; i < marked.length; i++) marked[i].removeAttribute('data-craftdriver-ref');
|
|
283
|
+
reg.epoch += 1;
|
|
284
|
+
reg.doc = newDocId(reg.epoch);
|
|
285
|
+
reg.issued = 0;
|
|
286
|
+
reg.map = new Map();
|
|
287
|
+
}
|
|
288
|
+
return reg;
|
|
289
|
+
}
|
|
290
|
+
function refFor(reg, el) {
|
|
291
|
+
const existing = el.getAttribute('data-craftdriver-ref');
|
|
292
|
+
// Trust the attribute only when the registry agrees it names this node;
|
|
293
|
+
// that rejects authored markup and cloneNode duplicates.
|
|
294
|
+
if (existing && reg.map.get(existing) === el) return existing;
|
|
295
|
+
const ref = 'e' + reg.next;
|
|
296
|
+
reg.next += 1;
|
|
297
|
+
reg.issued += 1;
|
|
298
|
+
reg.map.set(ref, el);
|
|
299
|
+
el.setAttribute('data-craftdriver-ref', ref);
|
|
300
|
+
return ref;
|
|
301
|
+
}
|
|
86
302
|
function visible(el) {
|
|
87
303
|
if (!el || !el.getClientRects) return false;
|
|
88
304
|
if (el.hidden) return false;
|
|
@@ -94,81 +310,7 @@ function visible(el) {
|
|
|
94
310
|
if (cs.visibility === 'hidden' || cs.display === 'none' || cs.opacity === '0') return false;
|
|
95
311
|
return true;
|
|
96
312
|
}
|
|
97
|
-
|
|
98
|
-
const role = (el.getAttribute('role') || '').trim();
|
|
99
|
-
return role ? role.split(/\\s+/)[0] : null;
|
|
100
|
-
}
|
|
101
|
-
function hasSectioningAncestor(el) {
|
|
102
|
-
for (let p = el.parentElement; p; p = p.parentElement) {
|
|
103
|
-
const t = p.tagName.toLowerCase();
|
|
104
|
-
if (t === 'article' || t === 'aside' || t === 'main' || t === 'nav' || t === 'section') {
|
|
105
|
-
return true;
|
|
106
|
-
}
|
|
107
|
-
const role = firstRoleToken(p);
|
|
108
|
-
if (
|
|
109
|
-
role === 'article' ||
|
|
110
|
-
role === 'complementary' ||
|
|
111
|
-
role === 'main' ||
|
|
112
|
-
role === 'navigation' ||
|
|
113
|
-
role === 'region'
|
|
114
|
-
) {
|
|
115
|
-
return true;
|
|
116
|
-
}
|
|
117
|
-
}
|
|
118
|
-
return false;
|
|
119
|
-
}
|
|
120
|
-
function impliedRole(el) {
|
|
121
|
-
const t = el.tagName.toLowerCase();
|
|
122
|
-
const explicit = firstRoleToken(el);
|
|
123
|
-
if (explicit) return explicit;
|
|
124
|
-
if (t === 'a' && el.hasAttribute('href')) return 'link';
|
|
125
|
-
if (t === 'button') return 'button';
|
|
126
|
-
if (t === 'input') {
|
|
127
|
-
const it = (el.getAttribute('type') || 'text').toLowerCase();
|
|
128
|
-
if (it === 'submit' || it === 'button' || it === 'reset') return 'button';
|
|
129
|
-
if (it === 'checkbox') return 'checkbox';
|
|
130
|
-
if (it === 'radio') return 'radio';
|
|
131
|
-
return 'textbox';
|
|
132
|
-
}
|
|
133
|
-
if (t === 'textarea') return 'textbox';
|
|
134
|
-
if (t === 'select') {
|
|
135
|
-
const size = Number(el.getAttribute('size') || '0');
|
|
136
|
-
return el.multiple || size > 1 ? 'listbox' : 'combobox';
|
|
137
|
-
}
|
|
138
|
-
if (/^h[1-6]$/.test(t)) return 'heading';
|
|
139
|
-
if (t === 'nav') return 'navigation';
|
|
140
|
-
if (t === 'main') return 'main';
|
|
141
|
-
if (t === 'header') return hasSectioningAncestor(el) ? null : 'banner';
|
|
142
|
-
if (t === 'footer') return hasSectioningAncestor(el) ? null : 'contentinfo';
|
|
143
|
-
if (t === 'form') return 'form';
|
|
144
|
-
if (t === 'img') return 'img';
|
|
145
|
-
return t;
|
|
146
|
-
}
|
|
147
|
-
function accName(el) {
|
|
148
|
-
const aria = el.getAttribute('aria-label');
|
|
149
|
-
if (aria) return aria.trim();
|
|
150
|
-
const labelledby = el.getAttribute('aria-labelledby');
|
|
151
|
-
if (labelledby) {
|
|
152
|
-
const ref = document.getElementById(labelledby);
|
|
153
|
-
if (ref) return (ref.textContent || '').trim();
|
|
154
|
-
}
|
|
155
|
-
if (el.tagName === 'INPUT' || el.tagName === 'TEXTAREA' || el.tagName === 'SELECT') {
|
|
156
|
-
if (el.id) {
|
|
157
|
-
const lbl = document.querySelector('label[for=' + JSON.stringify(el.id) + ']');
|
|
158
|
-
if (lbl) return (lbl.textContent || '').trim();
|
|
159
|
-
}
|
|
160
|
-
const parentLabel = el.closest('label');
|
|
161
|
-
if (parentLabel) return (parentLabel.textContent || '').trim();
|
|
162
|
-
const placeholder = el.getAttribute('placeholder');
|
|
163
|
-
if (placeholder) return placeholder.trim();
|
|
164
|
-
}
|
|
165
|
-
if (el.tagName === 'IMG') {
|
|
166
|
-
const alt = el.getAttribute('alt');
|
|
167
|
-
if (alt) return alt.trim();
|
|
168
|
-
}
|
|
169
|
-
const text = (el.textContent || '').trim().replace(/\\s+/g, ' ');
|
|
170
|
-
return text;
|
|
171
|
-
}
|
|
313
|
+
${PAGE_SEMANTICS_JS}
|
|
172
314
|
function locatorHint(el) {
|
|
173
315
|
if (el.id) return '#' + el.id;
|
|
174
316
|
const testid = el.getAttribute('data-testid') || el.getAttribute('data-test-id');
|
|
@@ -177,22 +319,25 @@ function locatorHint(el) {
|
|
|
177
319
|
if (name) return el.tagName.toLowerCase() + '[name=' + JSON.stringify(name) + ']';
|
|
178
320
|
return null;
|
|
179
321
|
}
|
|
322
|
+
const reg = registry();
|
|
180
323
|
const sel = 'a,button,input,select,textarea,h1,h2,h3,h4,h5,h6,[role],nav,main,header,footer,form,img,label';
|
|
181
|
-
// Wipe stale refs from a previous snapshot so numbering is deterministic.
|
|
182
|
-
const stale = document.querySelectorAll('[data-craftdriver-ref]');
|
|
183
|
-
for (let i = 0; i < stale.length; i++) stale[i].removeAttribute('data-craftdriver-ref');
|
|
184
324
|
const out = [];
|
|
325
|
+
const refs = [];
|
|
185
326
|
const nodes = document.querySelectorAll(sel);
|
|
186
327
|
for (let i = 0; i < nodes.length && out.length < MAX_NODES; i++) {
|
|
187
328
|
const el = nodes[i];
|
|
188
329
|
if (!visible(el)) continue;
|
|
189
|
-
|
|
190
|
-
|
|
330
|
+
// A <header>/<footer> inside sectioning content is not a landmark, and
|
|
331
|
+
// listing it as a bare tag would be pure noise. Everything else keeps a
|
|
332
|
+
// line even without an ARIA role, so controls stay addressable by ref.
|
|
333
|
+
const tag = el.tagName.toLowerCase();
|
|
334
|
+
if (!ariaRole(el) && (tag === 'header' || tag === 'footer')) continue;
|
|
335
|
+
const role = displayRole(el);
|
|
191
336
|
let name = accName(el);
|
|
192
337
|
if (name.length > MAX_NAME) name = name.slice(0, MAX_NAME - 1) + '…';
|
|
193
338
|
const hint = locatorHint(el);
|
|
194
|
-
const ref =
|
|
195
|
-
|
|
339
|
+
const ref = refFor(reg, el);
|
|
340
|
+
refs.push(ref);
|
|
196
341
|
let line = ref + ': ' + role;
|
|
197
342
|
if (name) line += ' "' + name + '"';
|
|
198
343
|
if (hint) line += ' ' + hint;
|
|
@@ -201,6 +346,7 @@ for (let i = 0; i < nodes.length && out.length < MAX_NODES; i++) {
|
|
|
201
346
|
if (el.checked) line += ' (checked)';
|
|
202
347
|
out.push(line);
|
|
203
348
|
}
|
|
204
|
-
return out;
|
|
349
|
+
return { lines: out, refs: refs, documentId: reg.doc, nextRef: reg.next };
|
|
205
350
|
`;
|
|
351
|
+
}
|
|
206
352
|
//# sourceMappingURL=snapshot.js.map
|
package/dist/cli/snapshot.js.map
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"snapshot.js","sourceRoot":"","sources":["../../src/cli/snapshot.ts"],"names":[],"mappings":"
|
|
1
|
+
{"version":3,"file":"snapshot.js","sourceRoot":"","sources":["../../src/cli/snapshot.ts"],"names":[],"mappings":"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"}
|
|
@@ -0,0 +1,50 @@
|
|
|
1
|
+
import type { SessionState } from '../lib/bidi/types.js';
|
|
2
|
+
import { prepareTempPath, commitFile, discardTempFile } from './artifactPaths.js';
|
|
3
|
+
/** Owned root for state files, overridable for tests and CI sandboxes. */
|
|
4
|
+
export declare function stateRoot(env?: NodeJS.ProcessEnv): string;
|
|
5
|
+
/** Longest accepted state name. Kept exported for the error-message tests. */
|
|
6
|
+
export declare const MAX_STATE_NAME = 64;
|
|
7
|
+
/**
|
|
8
|
+
* Normalize and validate an untrusted state name.
|
|
9
|
+
*
|
|
10
|
+
* The rule is shared with every other CLI-owned artifact — see
|
|
11
|
+
* `artifactPaths.ts` — so a name that is safe as a path component is safe for
|
|
12
|
+
* all of them, and there is only one place to get it wrong.
|
|
13
|
+
*/
|
|
14
|
+
export declare function validateStateName(raw: unknown): string;
|
|
15
|
+
/**
|
|
16
|
+
* Full path for a state name, guaranteed to sit inside the owned root.
|
|
17
|
+
*/
|
|
18
|
+
export declare function resolveStatePath(name: string, env?: NodeJS.ProcessEnv): Promise<string>;
|
|
19
|
+
/** @see prepareTempPath */
|
|
20
|
+
export declare const prepareTempStatePath: typeof prepareTempPath;
|
|
21
|
+
/** @see commitFile */
|
|
22
|
+
export declare const commitStateFile: typeof commitFile;
|
|
23
|
+
/** @see discardTempFile */
|
|
24
|
+
export declare const discardTempStateFile: typeof discardTempFile;
|
|
25
|
+
/**
|
|
26
|
+
* Read and structurally validate a state file.
|
|
27
|
+
*
|
|
28
|
+
* The file is JSON that will be fed back into a browser, so a malformed or
|
|
29
|
+
* hand-edited one should fail here with a clear message rather than deep
|
|
30
|
+
* inside cookie restoration.
|
|
31
|
+
*/
|
|
32
|
+
export declare function readStateFile(target: string, name: string): Promise<SessionState>;
|
|
33
|
+
/** Origins whose storage a state file carries. */
|
|
34
|
+
export declare function stateOrigins(state: SessionState): string[];
|
|
35
|
+
export interface StateSummary {
|
|
36
|
+
cookies: number;
|
|
37
|
+
origins: string[];
|
|
38
|
+
storageKeys: number;
|
|
39
|
+
}
|
|
40
|
+
/**
|
|
41
|
+
* Reportable shape of a state file: counts and origins, never values.
|
|
42
|
+
*
|
|
43
|
+
* Cookie names and storage keys are omitted too. They are not secrets in the
|
|
44
|
+
* way values are, but they are unnecessary to the caller and land in agent
|
|
45
|
+
* transcripts and CI logs, so the useful-to-leaky trade is not worth it.
|
|
46
|
+
*/
|
|
47
|
+
export declare function summarizeState(state: SessionState): StateSummary;
|
|
48
|
+
/** Saved state names under the root, without the `.json` suffix. */
|
|
49
|
+
export declare function listStateNames(env?: NodeJS.ProcessEnv): Promise<string[]>;
|
|
50
|
+
//# sourceMappingURL=stateStore.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"stateStore.d.ts","sourceRoot":"","sources":["../../src/cli/stateStore.ts"],"names":[],"mappings":"AAwBA,OAAO,KAAK,EAAE,YAAY,EAAE,MAAM,sBAAsB,CAAC;AAEzD,OAAO,EAIL,eAAe,EACf,UAAU,EACV,eAAe,EAEhB,MAAM,oBAAoB,CAAC;AAE5B,0EAA0E;AAC1E,wBAAgB,SAAS,CAAC,GAAG,GAAE,MAAM,CAAC,UAAwB,GAAG,MAAM,CAEtE;AAED,8EAA8E;AAC9E,eAAO,MAAM,cAAc,KAAoB,CAAC;AAEhD;;;;;;GAMG;AACH,wBAAgB,iBAAiB,CAAC,GAAG,EAAE,OAAO,GAAG,MAAM,CAEtD;AAED;;GAEG;AACH,wBAAsB,gBAAgB,CAAC,IAAI,EAAE,MAAM,EAAE,GAAG,CAAC,EAAE,MAAM,CAAC,UAAU,GAAG,OAAO,CAAC,MAAM,CAAC,CAO7F;AAED,2BAA2B;AAC3B,eAAO,MAAM,oBAAoB,wBAAkB,CAAC;AACpD,sBAAsB;AACtB,eAAO,MAAM,eAAe,mBAAa,CAAC;AAC1C,2BAA2B;AAC3B,eAAO,MAAM,oBAAoB,wBAAkB,CAAC;AAEpD;;;;;;GAMG;AACH,wBAAsB,aAAa,CAAC,MAAM,EAAE,MAAM,EAAE,IAAI,EAAE,MAAM,GAAG,OAAO,CAAC,YAAY,CAAC,CAevF;AAED,kDAAkD;AAClD,wBAAgB,YAAY,CAAC,KAAK,EAAE,YAAY,GAAG,MAAM,EAAE,CAK1D;AAED,MAAM,WAAW,YAAY;IAC3B,OAAO,EAAE,MAAM,CAAC;IAChB,OAAO,EAAE,MAAM,EAAE,CAAC;IAClB,WAAW,EAAE,MAAM,CAAC;CACrB;AAED;;;;;;GAMG;AACH,wBAAgB,cAAc,CAAC,KAAK,EAAE,YAAY,GAAG,YAAY,CAYhE;AAED,oEAAoE;AACpE,wBAAsB,cAAc,CAAC,GAAG,CAAC,EAAE,MAAM,CAAC,UAAU,GAAG,OAAO,CAAC,MAAM,EAAE,CAAC,CAW/E"}
|
|
@@ -0,0 +1,122 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Safe on-disk storage for authentication/session state.
|
|
3
|
+
*
|
|
4
|
+
* A saved state file holds live session cookies — it is a credential, not a
|
|
5
|
+
* fixture. Three properties follow from that, and each is enforced here rather
|
|
6
|
+
* than at the call site:
|
|
7
|
+
*
|
|
8
|
+
* - **Files live under one owned root.** Callers name a file, not a path, so a
|
|
9
|
+
* command can never be talked into writing over `~/.ssh/config` or reading
|
|
10
|
+
* an arbitrary file back into a browser. The root is `CRAFTDRIVER_STATE_DIR`
|
|
11
|
+
* when set, else `.craftdriver/state` under the working directory.
|
|
12
|
+
* - **Symlinks cannot escape it.** Containment is checked against the resolved
|
|
13
|
+
* real path of the deepest existing ancestor, so a symlink planted inside
|
|
14
|
+
* the root does not widen it.
|
|
15
|
+
* - **Nothing is half-written and nothing is world-readable.** Writes land on
|
|
16
|
+
* a temp file created `0600` and are renamed into place, so a reader sees
|
|
17
|
+
* either the old file or the new one, never a truncated JSON document.
|
|
18
|
+
*
|
|
19
|
+
* Callers must also never print what these files contain — see
|
|
20
|
+
* `summarizeState`, which is deliberately the only reporting path.
|
|
21
|
+
*/
|
|
22
|
+
import fs from 'node:fs/promises';
|
|
23
|
+
import path from 'node:path';
|
|
24
|
+
import { CraftdriverError, ErrorCode } from '../lib/errors.js';
|
|
25
|
+
import { parseSessionState } from '../lib/sessionStateValidation.js';
|
|
26
|
+
import { artifactRoot, resolveArtifactPath, validateArtifactName, prepareTempPath, commitFile, discardTempFile, MAX_ARTIFACT_NAME, } from './artifactPaths.js';
|
|
27
|
+
/** Owned root for state files, overridable for tests and CI sandboxes. */
|
|
28
|
+
export function stateRoot(env = process.env) {
|
|
29
|
+
return artifactRoot('state', 'CRAFTDRIVER_STATE_DIR', env);
|
|
30
|
+
}
|
|
31
|
+
/** Longest accepted state name. Kept exported for the error-message tests. */
|
|
32
|
+
export const MAX_STATE_NAME = MAX_ARTIFACT_NAME;
|
|
33
|
+
/**
|
|
34
|
+
* Normalize and validate an untrusted state name.
|
|
35
|
+
*
|
|
36
|
+
* The rule is shared with every other CLI-owned artifact — see
|
|
37
|
+
* `artifactPaths.ts` — so a name that is safe as a path component is safe for
|
|
38
|
+
* all of them, and there is only one place to get it wrong.
|
|
39
|
+
*/
|
|
40
|
+
export function validateStateName(raw) {
|
|
41
|
+
return validateArtifactName(raw, 'state', stateRoot());
|
|
42
|
+
}
|
|
43
|
+
/**
|
|
44
|
+
* Full path for a state name, guaranteed to sit inside the owned root.
|
|
45
|
+
*/
|
|
46
|
+
export async function resolveStatePath(name, env) {
|
|
47
|
+
return resolveArtifactPath({
|
|
48
|
+
root: stateRoot(env),
|
|
49
|
+
name,
|
|
50
|
+
suffix: '.json',
|
|
51
|
+
kind: 'state',
|
|
52
|
+
});
|
|
53
|
+
}
|
|
54
|
+
/** @see prepareTempPath */
|
|
55
|
+
export const prepareTempStatePath = prepareTempPath;
|
|
56
|
+
/** @see commitFile */
|
|
57
|
+
export const commitStateFile = commitFile;
|
|
58
|
+
/** @see discardTempFile */
|
|
59
|
+
export const discardTempStateFile = discardTempFile;
|
|
60
|
+
/**
|
|
61
|
+
* Read and structurally validate a state file.
|
|
62
|
+
*
|
|
63
|
+
* The file is JSON that will be fed back into a browser, so a malformed or
|
|
64
|
+
* hand-edited one should fail here with a clear message rather than deep
|
|
65
|
+
* inside cookie restoration.
|
|
66
|
+
*/
|
|
67
|
+
export async function readStateFile(target, name) {
|
|
68
|
+
try {
|
|
69
|
+
await fs.access(target);
|
|
70
|
+
}
|
|
71
|
+
catch {
|
|
72
|
+
throw new CraftdriverError(ErrorCode.STATE_INVALID, `state: no saved state named ${JSON.stringify(name)}`, {
|
|
73
|
+
detail: { root: path.dirname(target) },
|
|
74
|
+
hint: 'save one first with `craftdriver state save <name>`, or `state list` to see what exists',
|
|
75
|
+
});
|
|
76
|
+
}
|
|
77
|
+
return parseSessionState(target, { operation: 'cli.state.load' });
|
|
78
|
+
}
|
|
79
|
+
/** Origins whose storage a state file carries. */
|
|
80
|
+
export function stateOrigins(state) {
|
|
81
|
+
const origins = new Set();
|
|
82
|
+
for (const key of Object.keys(state.localStorage ?? {}))
|
|
83
|
+
origins.add(key);
|
|
84
|
+
for (const key of Object.keys(state.sessionStorage ?? {}))
|
|
85
|
+
origins.add(key);
|
|
86
|
+
return [...origins];
|
|
87
|
+
}
|
|
88
|
+
/**
|
|
89
|
+
* Reportable shape of a state file: counts and origins, never values.
|
|
90
|
+
*
|
|
91
|
+
* Cookie names and storage keys are omitted too. They are not secrets in the
|
|
92
|
+
* way values are, but they are unnecessary to the caller and land in agent
|
|
93
|
+
* transcripts and CI logs, so the useful-to-leaky trade is not worth it.
|
|
94
|
+
*/
|
|
95
|
+
export function summarizeState(state) {
|
|
96
|
+
let storageKeys = 0;
|
|
97
|
+
for (const bucket of [state.localStorage, state.sessionStorage]) {
|
|
98
|
+
for (const entries of Object.values(bucket ?? {})) {
|
|
99
|
+
storageKeys += Object.keys(entries ?? {}).length;
|
|
100
|
+
}
|
|
101
|
+
}
|
|
102
|
+
return {
|
|
103
|
+
cookies: state.cookies?.length ?? 0,
|
|
104
|
+
origins: stateOrigins(state),
|
|
105
|
+
storageKeys,
|
|
106
|
+
};
|
|
107
|
+
}
|
|
108
|
+
/** Saved state names under the root, without the `.json` suffix. */
|
|
109
|
+
export async function listStateNames(env) {
|
|
110
|
+
const root = stateRoot(env);
|
|
111
|
+
try {
|
|
112
|
+
const entries = await fs.readdir(root);
|
|
113
|
+
return entries
|
|
114
|
+
.filter((entry) => entry.endsWith('.json'))
|
|
115
|
+
.map((entry) => entry.slice(0, -'.json'.length))
|
|
116
|
+
.sort();
|
|
117
|
+
}
|
|
118
|
+
catch {
|
|
119
|
+
return [];
|
|
120
|
+
}
|
|
121
|
+
}
|
|
122
|
+
//# sourceMappingURL=stateStore.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"stateStore.js","sourceRoot":"","sources":["../../src/cli/stateStore.ts"],"names":[],"mappings":"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"}
|
package/dist/index.d.ts
CHANGED
|
@@ -10,7 +10,7 @@ export { BrowserContext, type ClearCookiesFilter, type ContextStorageStateOption
|
|
|
10
10
|
export { Keyboard } from './lib/keyboard.js';
|
|
11
11
|
export { Mouse } from './lib/mouse.js';
|
|
12
12
|
export { type TraceStartOptions, type TraceStopOptions, type TraceScreenshotMode, type TraceEvent, } from './lib/tracing.js';
|
|
13
|
-
export { NetworkInterceptor, LogMonitor, SessionStateManager, type Cookie, type CookieInput, type SessionState, type StorageStateOptions, type MockResponse, type InterceptedRequest, type InterceptedResponse, type ConsoleMessage, type JavaScriptError, type LogMessage, } from './lib/bidi/index.js';
|
|
13
|
+
export { NetworkInterceptor, LogMonitor, SessionStateManager, type Cookie, type CookieInput, type SessionState, type SessionStateCookie, type StorageStateOptions, type MockResponse, type InterceptedRequest, type InterceptedResponse, type ConsoleMessage, type JavaScriptError, type LogMessage, } from './lib/bidi/index.js';
|
|
14
14
|
export { ChromeService, type ChromeServiceOptions } from './lib/chrome.js';
|
|
15
15
|
export { ElectronService, type ElectronServiceOptions } from './lib/electron.js';
|
|
16
16
|
export { FirefoxService, type FirefoxServiceOptions } from './lib/firefox.js';
|