@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.
Files changed (69) hide show
  1. package/README.md +5 -2
  2. package/README.zh-CN.md +5 -2
  3. package/cli-manifest.json +77 -1
  4. package/clis/bilibili/video.js +61 -0
  5. package/clis/bilibili/video.test.js +81 -0
  6. package/clis/deepseek/ask.js +21 -1
  7. package/clis/deepseek/ask.test.js +73 -0
  8. package/clis/deepseek/utils.js +84 -1
  9. package/clis/deepseek/utils.test.js +37 -0
  10. package/clis/jianyu/search.js +139 -3
  11. package/clis/jianyu/search.test.js +25 -0
  12. package/clis/jianyu/shared/procurement-detail.js +15 -0
  13. package/clis/jianyu/shared/procurement-detail.test.js +12 -0
  14. package/clis/twitter/shared.js +7 -2
  15. package/clis/twitter/tweets.js +218 -0
  16. package/clis/twitter/tweets.test.js +125 -0
  17. package/clis/youtube/channel.js +35 -0
  18. package/dist/src/browser/base-page.d.ts +13 -3
  19. package/dist/src/browser/base-page.js +35 -25
  20. package/dist/src/browser/cdp.d.ts +1 -0
  21. package/dist/src/browser/cdp.js +12 -3
  22. package/dist/src/browser/compound.d.ts +59 -0
  23. package/dist/src/browser/compound.js +112 -0
  24. package/dist/src/browser/compound.test.d.ts +1 -0
  25. package/dist/src/browser/compound.test.js +175 -0
  26. package/dist/src/browser/dom-snapshot.d.ts +7 -0
  27. package/dist/src/browser/dom-snapshot.js +76 -3
  28. package/dist/src/browser/dom-snapshot.test.js +65 -0
  29. package/dist/src/browser/extract.d.ts +69 -0
  30. package/dist/src/browser/extract.js +132 -0
  31. package/dist/src/browser/extract.test.d.ts +1 -0
  32. package/dist/src/browser/extract.test.js +129 -0
  33. package/dist/src/browser/find.d.ts +76 -0
  34. package/dist/src/browser/find.js +179 -0
  35. package/dist/src/browser/find.test.d.ts +1 -0
  36. package/dist/src/browser/find.test.js +120 -0
  37. package/dist/src/browser/html-tree.d.ts +75 -0
  38. package/dist/src/browser/html-tree.js +112 -0
  39. package/dist/src/browser/html-tree.test.d.ts +1 -0
  40. package/dist/src/browser/html-tree.test.js +181 -0
  41. package/dist/src/browser/network-cache.d.ts +48 -0
  42. package/dist/src/browser/network-cache.js +66 -0
  43. package/dist/src/browser/network-cache.test.d.ts +1 -0
  44. package/dist/src/browser/network-cache.test.js +58 -0
  45. package/dist/src/browser/network-key.d.ts +22 -0
  46. package/dist/src/browser/network-key.js +66 -0
  47. package/dist/src/browser/network-key.test.d.ts +1 -0
  48. package/dist/src/browser/network-key.test.js +49 -0
  49. package/dist/src/browser/shape-filter.d.ts +52 -0
  50. package/dist/src/browser/shape-filter.js +101 -0
  51. package/dist/src/browser/shape-filter.test.d.ts +1 -0
  52. package/dist/src/browser/shape-filter.test.js +101 -0
  53. package/dist/src/browser/shape.d.ts +23 -0
  54. package/dist/src/browser/shape.js +95 -0
  55. package/dist/src/browser/shape.test.d.ts +1 -0
  56. package/dist/src/browser/shape.test.js +82 -0
  57. package/dist/src/browser/target-errors.d.ts +14 -1
  58. package/dist/src/browser/target-errors.js +13 -0
  59. package/dist/src/browser/target-errors.test.js +39 -6
  60. package/dist/src/browser/target-resolver.d.ts +57 -10
  61. package/dist/src/browser/target-resolver.js +195 -75
  62. package/dist/src/browser/target-resolver.test.js +80 -5
  63. package/dist/src/cli.js +630 -125
  64. package/dist/src/cli.test.js +794 -0
  65. package/dist/src/execution.js +7 -2
  66. package/dist/src/execution.test.js +54 -0
  67. package/dist/src/main.js +16 -0
  68. package/dist/src/types.d.ts +18 -3
  69. 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' | 'ambiguous' | 'stale_ref';
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 ambiguous error with candidates', () => {
17
+ it('creates selector_ambiguous error with candidates + matches_n', () => {
18
18
  const err = new TargetError({
19
- code: 'ambiguous',
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('ambiguous');
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: 'ambiguous',
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: 'ambiguous',
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
- * Replaces the ad-hoc 4-strategy fallback in dom-helpers.ts with a
5
- * principled resolution pipeline:
4
+ * Resolution pipeline:
6
5
  *
7
- * 1. Input classification: numeric → ref path, CSS-like → CSS path
8
- * 2. Ref path: lookup by data-opencli-ref, then verify fingerprint
9
- * 3. CSS path: querySelectorAll + uniqueness check
10
- * 4. Structured errors: stale_ref / ambiguous / not_found
6
+ * 1. Input classification: all-digitnumeric 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, el: Element } — success (el is assigned to `__resolved`)
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 the caller to use.
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.