@jackwener/opencli 1.7.5 → 1.7.6
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 -2
- package/README.zh-CN.md +5 -2
- package/cli-manifest.json +77 -1
- package/clis/bilibili/video.js +61 -0
- package/clis/bilibili/video.test.js +81 -0
- package/clis/deepseek/ask.js +21 -1
- package/clis/deepseek/ask.test.js +73 -0
- package/clis/deepseek/utils.js +84 -1
- package/clis/deepseek/utils.test.js +37 -0
- package/clis/jianyu/search.js +139 -3
- package/clis/jianyu/search.test.js +25 -0
- package/clis/jianyu/shared/procurement-detail.js +15 -0
- package/clis/jianyu/shared/procurement-detail.test.js +12 -0
- package/clis/twitter/shared.js +7 -2
- package/clis/twitter/tweets.js +218 -0
- package/clis/twitter/tweets.test.js +125 -0
- package/clis/youtube/channel.js +35 -0
- package/dist/src/browser/base-page.d.ts +13 -3
- package/dist/src/browser/base-page.js +35 -25
- package/dist/src/browser/cdp.d.ts +1 -0
- package/dist/src/browser/cdp.js +12 -3
- package/dist/src/browser/compound.d.ts +59 -0
- package/dist/src/browser/compound.js +112 -0
- package/dist/src/browser/compound.test.d.ts +1 -0
- package/dist/src/browser/compound.test.js +175 -0
- package/dist/src/browser/dom-snapshot.d.ts +7 -0
- package/dist/src/browser/dom-snapshot.js +76 -3
- package/dist/src/browser/dom-snapshot.test.js +65 -0
- package/dist/src/browser/extract.d.ts +69 -0
- package/dist/src/browser/extract.js +132 -0
- package/dist/src/browser/extract.test.d.ts +1 -0
- package/dist/src/browser/extract.test.js +129 -0
- package/dist/src/browser/find.d.ts +76 -0
- package/dist/src/browser/find.js +179 -0
- package/dist/src/browser/find.test.d.ts +1 -0
- package/dist/src/browser/find.test.js +120 -0
- package/dist/src/browser/html-tree.d.ts +75 -0
- package/dist/src/browser/html-tree.js +112 -0
- package/dist/src/browser/html-tree.test.d.ts +1 -0
- package/dist/src/browser/html-tree.test.js +181 -0
- package/dist/src/browser/network-cache.d.ts +48 -0
- package/dist/src/browser/network-cache.js +66 -0
- package/dist/src/browser/network-cache.test.d.ts +1 -0
- package/dist/src/browser/network-cache.test.js +58 -0
- package/dist/src/browser/network-key.d.ts +22 -0
- package/dist/src/browser/network-key.js +66 -0
- package/dist/src/browser/network-key.test.d.ts +1 -0
- package/dist/src/browser/network-key.test.js +49 -0
- package/dist/src/browser/shape-filter.d.ts +52 -0
- package/dist/src/browser/shape-filter.js +101 -0
- package/dist/src/browser/shape-filter.test.d.ts +1 -0
- package/dist/src/browser/shape-filter.test.js +101 -0
- package/dist/src/browser/shape.d.ts +23 -0
- package/dist/src/browser/shape.js +95 -0
- package/dist/src/browser/shape.test.d.ts +1 -0
- package/dist/src/browser/shape.test.js +82 -0
- package/dist/src/browser/target-errors.d.ts +14 -1
- package/dist/src/browser/target-errors.js +13 -0
- package/dist/src/browser/target-errors.test.js +39 -6
- package/dist/src/browser/target-resolver.d.ts +57 -10
- package/dist/src/browser/target-resolver.js +195 -75
- package/dist/src/browser/target-resolver.test.js +80 -5
- package/dist/src/cli.js +630 -125
- package/dist/src/cli.test.js +794 -0
- package/dist/src/execution.js +7 -2
- package/dist/src/execution.test.js +54 -0
- package/dist/src/main.js +16 -0
- package/dist/src/types.d.ts +18 -3
- package/package.json +1 -1
|
@@ -0,0 +1,95 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* JSON shape inference for browser network response previews.
|
|
3
|
+
*
|
|
4
|
+
* Produces a flat path → type descriptor map so agents can understand
|
|
5
|
+
* response structure without paying the token cost of the full body.
|
|
6
|
+
*
|
|
7
|
+
* Descriptors:
|
|
8
|
+
* string | number | boolean | null primitives
|
|
9
|
+
* string(len=N) strings longer than sampleStringLen
|
|
10
|
+
* array(0) | array(N) array at depth cap or summarized
|
|
11
|
+
* object | object(empty) objects at depth cap or summarized
|
|
12
|
+
* (truncated) output size budget exceeded
|
|
13
|
+
*/
|
|
14
|
+
const ROOT = '$';
|
|
15
|
+
export function inferShape(value, opts = {}) {
|
|
16
|
+
const maxDepth = opts.maxDepth ?? 6;
|
|
17
|
+
const maxBytes = opts.maxBytes ?? 2048;
|
|
18
|
+
const sampleStringLen = opts.sampleStringLen ?? 80;
|
|
19
|
+
const out = {};
|
|
20
|
+
let bytes = 2; // account for `{}` braces when serialized
|
|
21
|
+
let truncated = false;
|
|
22
|
+
const add = (path, desc) => {
|
|
23
|
+
if (truncated)
|
|
24
|
+
return false;
|
|
25
|
+
const entryBytes = JSON.stringify(path).length + JSON.stringify(desc).length + 2; // ":" + ","
|
|
26
|
+
if (bytes + entryBytes > maxBytes) {
|
|
27
|
+
out['(truncated)'] = `reached ${maxBytes}B budget`;
|
|
28
|
+
truncated = true;
|
|
29
|
+
return false;
|
|
30
|
+
}
|
|
31
|
+
out[path] = desc;
|
|
32
|
+
bytes += entryBytes;
|
|
33
|
+
return true;
|
|
34
|
+
};
|
|
35
|
+
const walk = (node, path, depth) => {
|
|
36
|
+
if (truncated)
|
|
37
|
+
return;
|
|
38
|
+
if (node === null) {
|
|
39
|
+
add(path, 'null');
|
|
40
|
+
return;
|
|
41
|
+
}
|
|
42
|
+
const t = typeof node;
|
|
43
|
+
if (t === 'string') {
|
|
44
|
+
const s = node;
|
|
45
|
+
add(path, s.length > sampleStringLen ? `string(len=${s.length})` : 'string');
|
|
46
|
+
return;
|
|
47
|
+
}
|
|
48
|
+
if (t === 'number' || t === 'boolean') {
|
|
49
|
+
add(path, t);
|
|
50
|
+
return;
|
|
51
|
+
}
|
|
52
|
+
if (t === 'undefined' || t === 'function' || t === 'symbol' || t === 'bigint') {
|
|
53
|
+
add(path, t);
|
|
54
|
+
return;
|
|
55
|
+
}
|
|
56
|
+
if (Array.isArray(node)) {
|
|
57
|
+
if (node.length === 0) {
|
|
58
|
+
add(path, 'array(0)');
|
|
59
|
+
return;
|
|
60
|
+
}
|
|
61
|
+
if (depth >= maxDepth) {
|
|
62
|
+
add(path, `array(${node.length})`);
|
|
63
|
+
return;
|
|
64
|
+
}
|
|
65
|
+
if (!add(path, `array(${node.length})`))
|
|
66
|
+
return;
|
|
67
|
+
walk(node[0], `${path}[0]`, depth + 1);
|
|
68
|
+
return;
|
|
69
|
+
}
|
|
70
|
+
// plain object
|
|
71
|
+
const obj = node;
|
|
72
|
+
const keys = Object.keys(obj);
|
|
73
|
+
if (keys.length === 0) {
|
|
74
|
+
add(path, 'object(empty)');
|
|
75
|
+
return;
|
|
76
|
+
}
|
|
77
|
+
if (depth >= maxDepth) {
|
|
78
|
+
add(path, `object(keys=${keys.length})`);
|
|
79
|
+
return;
|
|
80
|
+
}
|
|
81
|
+
if (!add(path, 'object'))
|
|
82
|
+
return;
|
|
83
|
+
for (const k of keys) {
|
|
84
|
+
if (truncated)
|
|
85
|
+
return;
|
|
86
|
+
const childPath = isSafeIdent(k) ? `${path}.${k}` : `${path}[${JSON.stringify(k)}]`;
|
|
87
|
+
walk(obj[k], childPath, depth + 1);
|
|
88
|
+
}
|
|
89
|
+
};
|
|
90
|
+
walk(value, ROOT, 0);
|
|
91
|
+
return out;
|
|
92
|
+
}
|
|
93
|
+
function isSafeIdent(key) {
|
|
94
|
+
return /^[A-Za-z_$][\w$]*$/.test(key);
|
|
95
|
+
}
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export {};
|
|
@@ -0,0 +1,82 @@
|
|
|
1
|
+
import { describe, expect, it } from 'vitest';
|
|
2
|
+
import { inferShape } from './shape.js';
|
|
3
|
+
describe('inferShape', () => {
|
|
4
|
+
it('describes primitives at root', () => {
|
|
5
|
+
expect(inferShape('hello')).toEqual({ $: 'string' });
|
|
6
|
+
expect(inferShape(42)).toEqual({ $: 'number' });
|
|
7
|
+
expect(inferShape(true)).toEqual({ $: 'boolean' });
|
|
8
|
+
expect(inferShape(null)).toEqual({ $: 'null' });
|
|
9
|
+
});
|
|
10
|
+
it('summarizes long strings with their length', () => {
|
|
11
|
+
const long = 'x'.repeat(200);
|
|
12
|
+
expect(inferShape(long, { sampleStringLen: 80 })).toEqual({ $: 'string(len=200)' });
|
|
13
|
+
});
|
|
14
|
+
it('walks nested objects and emits dotted paths', () => {
|
|
15
|
+
const shape = inferShape({ user: { id: 1, name: 'bob' } });
|
|
16
|
+
expect(shape).toEqual({
|
|
17
|
+
$: 'object',
|
|
18
|
+
'$.user': 'object',
|
|
19
|
+
'$.user.id': 'number',
|
|
20
|
+
'$.user.name': 'string',
|
|
21
|
+
});
|
|
22
|
+
});
|
|
23
|
+
it('quotes unsafe keys using bracket notation', () => {
|
|
24
|
+
const shape = inferShape({ 'weird key': 1, '123bad': 2 });
|
|
25
|
+
expect(shape['$["weird key"]']).toBe('number');
|
|
26
|
+
expect(shape['$["123bad"]']).toBe('number');
|
|
27
|
+
});
|
|
28
|
+
it('samples the first array element and reports length', () => {
|
|
29
|
+
const shape = inferShape({ items: [{ a: 1 }, { a: 2 }, { a: 3 }] });
|
|
30
|
+
expect(shape['$.items']).toBe('array(3)');
|
|
31
|
+
expect(shape['$.items[0]']).toBe('object');
|
|
32
|
+
expect(shape['$.items[0].a']).toBe('number');
|
|
33
|
+
});
|
|
34
|
+
it('marks empty containers explicitly', () => {
|
|
35
|
+
const shape = inferShape({ arr: [], obj: {} });
|
|
36
|
+
expect(shape['$.arr']).toBe('array(0)');
|
|
37
|
+
expect(shape['$.obj']).toBe('object(empty)');
|
|
38
|
+
});
|
|
39
|
+
it('collapses subtrees past maxDepth', () => {
|
|
40
|
+
const deep = { a: { b: { c: { d: { e: { f: 'too deep' } } } } } };
|
|
41
|
+
const shape = inferShape(deep, { maxDepth: 2 });
|
|
42
|
+
expect(shape['$.a.b']).toMatch(/^object/);
|
|
43
|
+
expect(shape['$.a.b.c']).toBeUndefined();
|
|
44
|
+
});
|
|
45
|
+
it('truncates when the byte budget is exhausted', () => {
|
|
46
|
+
const wide = {};
|
|
47
|
+
for (let i = 0; i < 500; i++)
|
|
48
|
+
wide[`field_${i}`] = i;
|
|
49
|
+
const shape = inferShape(wide, { maxBytes: 256 });
|
|
50
|
+
expect(shape['(truncated)']).toMatch(/256B/);
|
|
51
|
+
expect(Object.keys(shape).length).toBeLessThan(500);
|
|
52
|
+
});
|
|
53
|
+
it('stops descending into an array once the budget is hit by its own descriptor', () => {
|
|
54
|
+
// Budget just large enough for `$` + one deep array descriptor, not its element.
|
|
55
|
+
const shape = inferShape({ items: [{ deep: 1 }] }, { maxBytes: 40 });
|
|
56
|
+
expect(shape['$.items[0]']).toBeUndefined();
|
|
57
|
+
expect(shape['(truncated)']).toBeDefined();
|
|
58
|
+
});
|
|
59
|
+
it('handles the Twitter UserTweets payload envelope', () => {
|
|
60
|
+
const payload = {
|
|
61
|
+
data: {
|
|
62
|
+
user: {
|
|
63
|
+
result: {
|
|
64
|
+
rest_id: '42',
|
|
65
|
+
timeline_v2: {
|
|
66
|
+
timeline: {
|
|
67
|
+
instructions: [
|
|
68
|
+
{ type: 'TimelinePinEntry', entries: [] },
|
|
69
|
+
{ entries: [{ entryId: 'tweet-1', content: { entryType: 'TimelineTimelineItem' } }] },
|
|
70
|
+
],
|
|
71
|
+
},
|
|
72
|
+
},
|
|
73
|
+
},
|
|
74
|
+
},
|
|
75
|
+
},
|
|
76
|
+
};
|
|
77
|
+
const shape = inferShape(payload, { maxDepth: 10 });
|
|
78
|
+
expect(shape['$.data.user.result.rest_id']).toBe('string');
|
|
79
|
+
expect(shape['$.data.user.result.timeline_v2.timeline.instructions']).toBe('array(2)');
|
|
80
|
+
expect(shape['$.data.user.result.timeline_v2.timeline.instructions[0]']).toBe('object');
|
|
81
|
+
});
|
|
82
|
+
});
|
|
@@ -5,18 +5,31 @@
|
|
|
5
5
|
* goes through the unified resolver. When resolution fails, one of these
|
|
6
6
|
* structured errors is thrown so that AI agents and adapter authors get
|
|
7
7
|
* actionable diagnostics instead of a generic "Element not found".
|
|
8
|
+
*
|
|
9
|
+
* Numeric-ref codes (from snapshot indices):
|
|
10
|
+
* - not_found: the ref no longer exists in the DOM
|
|
11
|
+
* - stale_ref: the ref still exists but points to a different element
|
|
12
|
+
*
|
|
13
|
+
* CSS-selector codes (from `--selector <css>` entrypoints):
|
|
14
|
+
* - invalid_selector: selector syntax rejected by querySelectorAll
|
|
15
|
+
* - selector_not_found: 0 matches
|
|
16
|
+
* - selector_ambiguous: >1 matches for a write op without --nth
|
|
17
|
+
* - selector_nth_out_of_range: --nth beyond matches_n
|
|
8
18
|
*/
|
|
9
|
-
export type TargetErrorCode = 'not_found' | '
|
|
19
|
+
export type TargetErrorCode = 'not_found' | 'stale_ref' | 'invalid_selector' | 'selector_not_found' | 'selector_ambiguous' | 'selector_nth_out_of_range';
|
|
10
20
|
export interface TargetErrorInfo {
|
|
11
21
|
code: TargetErrorCode;
|
|
12
22
|
message: string;
|
|
13
23
|
hint: string;
|
|
14
24
|
candidates?: string[];
|
|
25
|
+
/** CSS-path match count, when the error was raised mid-resolution */
|
|
26
|
+
matches_n?: number;
|
|
15
27
|
}
|
|
16
28
|
export declare class TargetError extends Error {
|
|
17
29
|
readonly code: TargetErrorCode;
|
|
18
30
|
readonly hint: string;
|
|
19
31
|
readonly candidates?: string[];
|
|
32
|
+
readonly matches_n?: number;
|
|
20
33
|
constructor(info: TargetErrorInfo);
|
|
21
34
|
/** Serialize for structured output to AI agents */
|
|
22
35
|
toJSON(): TargetErrorInfo;
|
|
@@ -5,17 +5,29 @@
|
|
|
5
5
|
* goes through the unified resolver. When resolution fails, one of these
|
|
6
6
|
* structured errors is thrown so that AI agents and adapter authors get
|
|
7
7
|
* actionable diagnostics instead of a generic "Element not found".
|
|
8
|
+
*
|
|
9
|
+
* Numeric-ref codes (from snapshot indices):
|
|
10
|
+
* - not_found: the ref no longer exists in the DOM
|
|
11
|
+
* - stale_ref: the ref still exists but points to a different element
|
|
12
|
+
*
|
|
13
|
+
* CSS-selector codes (from `--selector <css>` entrypoints):
|
|
14
|
+
* - invalid_selector: selector syntax rejected by querySelectorAll
|
|
15
|
+
* - selector_not_found: 0 matches
|
|
16
|
+
* - selector_ambiguous: >1 matches for a write op without --nth
|
|
17
|
+
* - selector_nth_out_of_range: --nth beyond matches_n
|
|
8
18
|
*/
|
|
9
19
|
export class TargetError extends Error {
|
|
10
20
|
code;
|
|
11
21
|
hint;
|
|
12
22
|
candidates;
|
|
23
|
+
matches_n;
|
|
13
24
|
constructor(info) {
|
|
14
25
|
super(info.message);
|
|
15
26
|
this.name = 'TargetError';
|
|
16
27
|
this.code = info.code;
|
|
17
28
|
this.hint = info.hint;
|
|
18
29
|
this.candidates = info.candidates;
|
|
30
|
+
this.matches_n = info.matches_n;
|
|
19
31
|
}
|
|
20
32
|
/** Serialize for structured output to AI agents */
|
|
21
33
|
toJSON() {
|
|
@@ -24,6 +36,7 @@ export class TargetError extends Error {
|
|
|
24
36
|
message: this.message,
|
|
25
37
|
hint: this.hint,
|
|
26
38
|
...(this.candidates && { candidates: this.candidates }),
|
|
39
|
+
...(this.matches_n !== undefined && { matches_n: this.matches_n }),
|
|
27
40
|
};
|
|
28
41
|
}
|
|
29
42
|
}
|
|
@@ -14,16 +14,47 @@ describe('TargetError', () => {
|
|
|
14
14
|
expect(err.hint).toContain('fresh snapshot');
|
|
15
15
|
expect(err.candidates).toBeUndefined();
|
|
16
16
|
});
|
|
17
|
-
it('creates
|
|
17
|
+
it('creates selector_ambiguous error with candidates + matches_n', () => {
|
|
18
18
|
const err = new TargetError({
|
|
19
|
-
code: '
|
|
19
|
+
code: 'selector_ambiguous',
|
|
20
20
|
message: 'CSS selector ".btn" matched 3 elements',
|
|
21
|
-
hint: 'Use a more specific selector.',
|
|
21
|
+
hint: 'Use a more specific selector, or pass --nth.',
|
|
22
22
|
candidates: ['<button> "Login"', '<button> "Sign Up"', '<button> "Cancel"'],
|
|
23
|
+
matches_n: 3,
|
|
23
24
|
});
|
|
24
|
-
expect(err.code).toBe('
|
|
25
|
+
expect(err.code).toBe('selector_ambiguous');
|
|
25
26
|
expect(err.candidates).toHaveLength(3);
|
|
26
27
|
expect(err.candidates[0]).toContain('Login');
|
|
28
|
+
expect(err.matches_n).toBe(3);
|
|
29
|
+
});
|
|
30
|
+
it('creates invalid_selector error', () => {
|
|
31
|
+
const err = new TargetError({
|
|
32
|
+
code: 'invalid_selector',
|
|
33
|
+
message: 'Invalid CSS selector: >>> (unexpected token)',
|
|
34
|
+
hint: 'Check the selector syntax.',
|
|
35
|
+
});
|
|
36
|
+
expect(err.code).toBe('invalid_selector');
|
|
37
|
+
expect(err.message).toContain('Invalid CSS selector');
|
|
38
|
+
});
|
|
39
|
+
it('creates selector_not_found error with matches_n=0', () => {
|
|
40
|
+
const err = new TargetError({
|
|
41
|
+
code: 'selector_not_found',
|
|
42
|
+
message: 'CSS selector ".missing" matched 0 elements',
|
|
43
|
+
hint: 'Check the page or use browser find.',
|
|
44
|
+
matches_n: 0,
|
|
45
|
+
});
|
|
46
|
+
expect(err.code).toBe('selector_not_found');
|
|
47
|
+
expect(err.matches_n).toBe(0);
|
|
48
|
+
});
|
|
49
|
+
it('creates selector_nth_out_of_range error', () => {
|
|
50
|
+
const err = new TargetError({
|
|
51
|
+
code: 'selector_nth_out_of_range',
|
|
52
|
+
message: 'matched 3 elements, but --nth=5 is out of range',
|
|
53
|
+
hint: 'Use --nth between 0 and 2.',
|
|
54
|
+
matches_n: 3,
|
|
55
|
+
});
|
|
56
|
+
expect(err.code).toBe('selector_nth_out_of_range');
|
|
57
|
+
expect(err.matches_n).toBe(3);
|
|
27
58
|
});
|
|
28
59
|
it('creates stale_ref error', () => {
|
|
29
60
|
const err = new TargetError({
|
|
@@ -36,17 +67,19 @@ describe('TargetError', () => {
|
|
|
36
67
|
});
|
|
37
68
|
it('serializes to JSON for structured output', () => {
|
|
38
69
|
const err = new TargetError({
|
|
39
|
-
code: '
|
|
70
|
+
code: 'selector_ambiguous',
|
|
40
71
|
message: 'matched 3',
|
|
41
72
|
hint: 'be specific',
|
|
42
73
|
candidates: ['a', 'b'],
|
|
74
|
+
matches_n: 3,
|
|
43
75
|
});
|
|
44
76
|
const json = err.toJSON();
|
|
45
77
|
expect(json).toEqual({
|
|
46
|
-
code: '
|
|
78
|
+
code: 'selector_ambiguous',
|
|
47
79
|
message: 'matched 3',
|
|
48
80
|
hint: 'be specific',
|
|
49
81
|
candidates: ['a', 'b'],
|
|
82
|
+
matches_n: 3,
|
|
50
83
|
});
|
|
51
84
|
});
|
|
52
85
|
it('omits candidates from JSON when not present', () => {
|
|
@@ -1,26 +1,73 @@
|
|
|
1
1
|
/**
|
|
2
2
|
* Unified target resolver for browser actions.
|
|
3
3
|
*
|
|
4
|
-
*
|
|
5
|
-
* principled resolution pipeline:
|
|
4
|
+
* Resolution pipeline:
|
|
6
5
|
*
|
|
7
|
-
* 1. Input classification:
|
|
8
|
-
*
|
|
9
|
-
*
|
|
10
|
-
*
|
|
6
|
+
* 1. Input classification: all-digit → numeric ref path, otherwise → CSS path.
|
|
7
|
+
* The CSS path passes the raw string to `querySelectorAll` and lets the
|
|
8
|
+
* browser parser decide what's valid. No frontend regex whitelist — the
|
|
9
|
+
* goal is that any selector accepted by `browser find --css` is accepted
|
|
10
|
+
* by the same selector on `get/click/type/select`.
|
|
11
|
+
* 2. Ref path: cascading match levels (see below), using data-opencli-ref
|
|
12
|
+
* plus the fingerprint map populated by snapshot + find.
|
|
13
|
+
* 3. CSS path: querySelectorAll + match-count policy (see ResolveOptions)
|
|
14
|
+
* 4. Structured errors:
|
|
15
|
+
* - numeric: not_found / stale_ref
|
|
16
|
+
* - CSS: invalid_selector / selector_not_found / selector_ambiguous
|
|
17
|
+
* / selector_nth_out_of_range
|
|
11
18
|
*
|
|
12
19
|
* All JS is generated as strings for page.evaluate() — runs in the browser.
|
|
20
|
+
*
|
|
21
|
+
* ── Cascading stale-ref (browser-use style) ──────────────────────────
|
|
22
|
+
* Strict equality on the fingerprint rejected too many live pages — SPA
|
|
23
|
+
* re-renders swap text / role while keeping id + testId. The resolver
|
|
24
|
+
* now walks three tiers before giving up:
|
|
25
|
+
*
|
|
26
|
+
* 1. EXACT — tag + strong id (id or testId) agree, ≤1 soft mismatch
|
|
27
|
+
* 2. STABLE — tag + strong id agree, soft signals drifted (aria-label,
|
|
28
|
+
* role, text) — agent gets a warning but the action
|
|
29
|
+
* proceeds so dynamic pages don't stall
|
|
30
|
+
* 3. REIDENTIFIED — original ref either missing from the DOM or fully
|
|
31
|
+
* mismatched, but the fingerprint uniquely identifies
|
|
32
|
+
* a single other live element via id / testId /
|
|
33
|
+
* aria-label. Re-tag that element with the old ref and
|
|
34
|
+
* surface match_level so the caller knows we swapped.
|
|
35
|
+
*
|
|
36
|
+
* Only when all three fail do we emit `stale_ref`. Every success envelope
|
|
37
|
+
* carries `match_level` so downstream CLIs can surface the weakest tier
|
|
38
|
+
* a caller actually traversed.
|
|
13
39
|
*/
|
|
40
|
+
export interface ResolveOptions {
|
|
41
|
+
/**
|
|
42
|
+
* When CSS matches multiple elements, pick the element at this 0-based
|
|
43
|
+
* index instead of raising `selector_ambiguous`. Raises
|
|
44
|
+
* `selector_nth_out_of_range` if `nth >= matches.length`.
|
|
45
|
+
*/
|
|
46
|
+
nth?: number;
|
|
47
|
+
/**
|
|
48
|
+
* When CSS matches multiple elements, pick the first match instead of
|
|
49
|
+
* raising `selector_ambiguous`. Used by read commands (get text / value /
|
|
50
|
+
* attributes) to deliver a best-effort answer + matches_n in the envelope.
|
|
51
|
+
* Ignored when `nth` is also set (nth wins).
|
|
52
|
+
*/
|
|
53
|
+
firstOnMulti?: boolean;
|
|
54
|
+
}
|
|
55
|
+
/** Tier the resolver traversed to land the target. Callers may surface this to agents. */
|
|
56
|
+
export type TargetMatchLevel = 'exact' | 'stable' | 'reidentified';
|
|
14
57
|
/**
|
|
15
58
|
* Generate JS that resolves a target to a single DOM element.
|
|
16
59
|
*
|
|
17
60
|
* Returns a JS expression that evaluates to:
|
|
18
|
-
* { ok: true,
|
|
19
|
-
* { ok: false, code, message, hint, candidates } — structured error
|
|
61
|
+
* { ok: true, matches_n, match_level } — success (el stored in `__resolved`)
|
|
62
|
+
* { ok: false, code, message, hint, candidates, matches_n? } — structured error
|
|
63
|
+
*
|
|
64
|
+
* `match_level` is always set on success:
|
|
65
|
+
* - CSS path → 'exact'
|
|
66
|
+
* - numeric ref path → whichever tier matched ('exact' / 'stable' / 'reidentified')
|
|
20
67
|
*
|
|
21
|
-
* The resolved element is stored in `__resolved` for
|
|
68
|
+
* The resolved element is stored in `window.__resolved` for downstream helpers.
|
|
22
69
|
*/
|
|
23
|
-
export declare function resolveTargetJs(ref: string): string;
|
|
70
|
+
export declare function resolveTargetJs(ref: string, opts?: ResolveOptions): string;
|
|
24
71
|
/**
|
|
25
72
|
* Generate JS for click that uses the unified resolver.
|
|
26
73
|
* Assumes resolveTargetJs has been called and __resolved is set.
|