@jackwener/opencli 1.5.8 → 1.6.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 +42 -0
- package/README.md +35 -1
- package/README.zh-CN.md +17 -1
- package/SKILL.md +31 -851
- package/autoresearch/baseline-browse.txt +1 -0
- package/autoresearch/baseline-skill.txt +1 -0
- package/autoresearch/browse-tasks.json +688 -0
- package/autoresearch/eval-browse.ts +185 -0
- package/autoresearch/eval-skill.ts +248 -0
- package/autoresearch/run-browse.sh +9 -0
- package/autoresearch/run-skill.sh +9 -0
- package/dist/browser/base-page.d.ts +48 -0
- package/dist/browser/base-page.js +160 -0
- package/dist/browser/cdp.js +4 -106
- package/dist/browser/daemon-client.d.ts +20 -7
- package/dist/browser/daemon-client.js +39 -39
- package/dist/browser/daemon-client.test.js +77 -0
- package/dist/browser/discover.d.ts +1 -4
- package/dist/browser/discover.js +9 -23
- package/dist/browser/errors.d.ts +4 -0
- package/dist/browser/errors.js +20 -0
- package/dist/browser/index.d.ts +1 -1
- package/dist/browser/index.js +1 -1
- package/dist/browser/page.d.ts +10 -35
- package/dist/browser/page.js +55 -187
- package/dist/browser/tabs.js +5 -5
- package/dist/browser.test.js +15 -15
- package/dist/cli-manifest.json +294 -22
- package/dist/cli.js +392 -0
- package/dist/clis/amazon/bestsellers.d.ts +21 -0
- package/dist/clis/amazon/bestsellers.js +130 -0
- package/dist/clis/amazon/bestsellers.test.js +20 -0
- package/dist/clis/amazon/discussion.d.ts +20 -0
- package/dist/clis/amazon/discussion.js +91 -0
- package/dist/clis/amazon/discussion.test.d.ts +1 -0
- package/dist/clis/amazon/discussion.test.js +36 -0
- package/dist/clis/amazon/offer.d.ts +23 -0
- package/dist/clis/amazon/offer.js +140 -0
- package/dist/clis/amazon/offer.test.d.ts +1 -0
- package/dist/clis/amazon/offer.test.js +29 -0
- package/dist/clis/amazon/product.d.ts +18 -0
- package/dist/clis/amazon/product.js +92 -0
- package/dist/clis/amazon/product.test.d.ts +1 -0
- package/dist/clis/amazon/product.test.js +24 -0
- package/dist/clis/amazon/search.d.ts +18 -0
- package/dist/clis/amazon/search.js +87 -0
- package/dist/clis/amazon/search.test.d.ts +1 -0
- package/dist/clis/amazon/search.test.js +22 -0
- package/dist/clis/amazon/shared.d.ts +64 -0
- package/dist/clis/amazon/shared.js +255 -0
- package/dist/clis/amazon/shared.test.d.ts +1 -0
- package/dist/clis/amazon/shared.test.js +33 -0
- package/dist/clis/gemini/ask.d.ts +1 -0
- package/dist/clis/gemini/ask.js +40 -0
- package/dist/clis/gemini/image.d.ts +1 -0
- package/dist/clis/gemini/image.js +105 -0
- package/dist/clis/gemini/new.d.ts +1 -0
- package/dist/clis/gemini/new.js +20 -0
- package/dist/clis/gemini/utils.d.ts +34 -0
- package/dist/clis/gemini/utils.js +463 -0
- package/dist/clis/gemini/utils.test.d.ts +1 -0
- package/dist/clis/gemini/utils.test.js +31 -0
- package/dist/clis/notebooklm/compat.test.d.ts +1 -1
- package/dist/clis/notebooklm/compat.test.js +3 -3
- package/dist/clis/notebooklm/current.js +2 -3
- package/dist/clis/notebooklm/get.js +2 -3
- package/dist/clis/notebooklm/history.js +2 -3
- package/dist/clis/notebooklm/note-list.js +2 -3
- package/dist/clis/notebooklm/notes-get.js +2 -3
- package/dist/clis/notebooklm/open.d.ts +1 -0
- package/dist/clis/notebooklm/open.js +41 -0
- package/dist/clis/notebooklm/open.test.d.ts +1 -0
- package/dist/clis/notebooklm/open.test.js +63 -0
- package/dist/clis/notebooklm/source-fulltext.js +2 -3
- package/dist/clis/notebooklm/source-get.js +2 -3
- package/dist/clis/notebooklm/source-guide.js +2 -3
- package/dist/clis/notebooklm/source-list.js +2 -3
- package/dist/clis/notebooklm/status.js +1 -2
- package/dist/clis/notebooklm/summary.js +2 -3
- package/dist/clis/notebooklm/utils.d.ts +2 -1
- package/dist/clis/notebooklm/utils.js +20 -21
- package/dist/clis/twitter/article.js +28 -1
- package/dist/clis/xiaohongshu/creator-note-detail.test.js +11 -11
- package/dist/clis/xiaohongshu/creator-notes-summary.test.js +6 -6
- package/dist/clis/xiaohongshu/creator-notes.test.js +22 -22
- package/dist/clis/xiaohongshu/note.js +11 -0
- package/dist/clis/xiaohongshu/note.test.js +49 -0
- package/dist/commanderAdapter.js +7 -4
- package/dist/commanderAdapter.test.js +76 -0
- package/dist/commands/daemon.js +8 -47
- package/dist/commands/daemon.test.js +45 -70
- package/dist/discovery.js +27 -0
- package/dist/doctor.d.ts +1 -2
- package/dist/doctor.js +7 -8
- package/dist/explore.js +1 -1
- package/dist/output.js +28 -0
- package/dist/output.test.js +15 -0
- package/dist/pipeline/executor.js +2 -7
- package/dist/pipeline/steps/browser.js +1 -1
- package/dist/pipeline/template.js +25 -3
- package/dist/record.d.ts +50 -0
- package/dist/record.js +298 -57
- package/dist/record.test.d.ts +1 -0
- package/dist/record.test.js +293 -0
- package/dist/registry.d.ts +2 -0
- package/dist/registry.js +1 -0
- package/dist/registry.test.js +10 -0
- package/dist/runtime.js +3 -3
- package/dist/snapshotFormatter.d.ts +1 -1
- package/dist/snapshotFormatter.js +4 -4
- package/dist/snapshotFormatter.test.d.ts +1 -1
- package/dist/snapshotFormatter.test.js +2 -2
- package/dist/types.d.ts +11 -1
- package/dist/types.js +1 -1
- package/docs/.vitepress/config.mts +2 -0
- package/docs/adapters/browser/amazon.md +53 -0
- package/docs/adapters/browser/gemini.md +72 -0
- package/docs/adapters/browser/notebooklm.md +5 -5
- package/docs/adapters/index.md +3 -1
- package/docs/guide/getting-started.md +21 -0
- package/docs/superpowers/specs/2026-04-02-browse-skill-testing-design.md +144 -0
- package/docs/zh/guide/getting-started.md +21 -0
- package/extension/package-lock.json +2 -2
- package/extension/src/background.test.ts +7 -163
- package/extension/src/background.ts +58 -161
- package/extension/src/cdp.ts +77 -124
- package/extension/src/protocol.ts +5 -5
- package/package.json +1 -1
- package/skills/opencli-explorer/SKILL.md +853 -0
- package/skills/opencli-oneshot/SKILL.md +222 -0
- package/skills/opencli-operate/SKILL.md +213 -0
- package/skills/opencli-usage/SKILL.md +152 -0
- package/skills/opencli-usage/browser.md +429 -0
- package/skills/opencli-usage/desktop.md +118 -0
- package/skills/opencli-usage/plugins.md +82 -0
- package/skills/opencli-usage/public-api.md +149 -0
- package/src/browser/base-page.ts +197 -0
- package/src/browser/cdp.ts +7 -131
- package/src/browser/daemon-client.test.ts +103 -0
- package/src/browser/daemon-client.ts +55 -43
- package/src/browser/discover.ts +9 -21
- package/src/browser/errors.ts +22 -0
- package/src/browser/index.ts +1 -1
- package/src/browser/page.ts +57 -209
- package/src/browser/tabs.ts +5 -5
- package/src/browser.test.ts +15 -15
- package/src/cli.ts +392 -0
- package/src/clis/amazon/bestsellers.test.ts +22 -0
- package/src/clis/amazon/bestsellers.ts +180 -0
- package/src/clis/amazon/discussion.test.ts +38 -0
- package/src/clis/amazon/discussion.ts +131 -0
- package/src/clis/amazon/offer.test.ts +35 -0
- package/src/clis/amazon/offer.ts +185 -0
- package/src/clis/amazon/product.test.ts +26 -0
- package/src/clis/amazon/product.ts +131 -0
- package/src/clis/amazon/search.test.ts +24 -0
- package/src/clis/amazon/search.ts +128 -0
- package/src/clis/amazon/shared.test.ts +37 -0
- package/src/clis/amazon/shared.ts +316 -0
- package/src/clis/gemini/ask.ts +46 -0
- package/src/clis/gemini/image.ts +115 -0
- package/src/clis/gemini/new.ts +22 -0
- package/src/clis/gemini/utils.test.ts +36 -0
- package/src/clis/gemini/utils.ts +523 -0
- package/src/clis/notebooklm/compat.test.ts +3 -3
- package/src/clis/notebooklm/current.ts +2 -3
- package/src/clis/notebooklm/get.ts +1 -3
- package/src/clis/notebooklm/history.ts +1 -3
- package/src/clis/notebooklm/note-list.ts +1 -3
- package/src/clis/notebooklm/notes-get.ts +1 -3
- package/src/clis/notebooklm/open.test.ts +78 -0
- package/src/clis/notebooklm/open.ts +61 -0
- package/src/clis/notebooklm/source-fulltext.ts +1 -3
- package/src/clis/notebooklm/source-get.ts +1 -3
- package/src/clis/notebooklm/source-guide.ts +1 -3
- package/src/clis/notebooklm/source-list.ts +1 -3
- package/src/clis/notebooklm/status.ts +1 -2
- package/src/clis/notebooklm/summary.ts +1 -3
- package/src/clis/notebooklm/utils.ts +29 -20
- package/src/clis/twitter/article.ts +31 -1
- package/src/clis/xiaohongshu/creator-note-detail.test.ts +11 -11
- package/src/clis/xiaohongshu/creator-notes-summary.test.ts +6 -6
- package/src/clis/xiaohongshu/creator-notes.test.ts +22 -22
- package/src/clis/xiaohongshu/note.test.ts +51 -0
- package/src/clis/xiaohongshu/note.ts +18 -0
- package/src/commanderAdapter.test.ts +109 -0
- package/src/commanderAdapter.ts +8 -4
- package/src/commands/daemon.test.ts +50 -84
- package/src/commands/daemon.ts +8 -56
- package/src/discovery.ts +22 -0
- package/src/doctor.ts +8 -9
- package/src/explore.ts +1 -1
- package/src/output.test.ts +17 -0
- package/src/output.ts +27 -0
- package/src/pipeline/executor.ts +2 -7
- package/src/pipeline/steps/browser.ts +1 -1
- package/src/pipeline/template.ts +27 -4
- package/src/record.test.ts +362 -0
- package/src/record.ts +341 -62
- package/src/registry.test.ts +12 -0
- package/src/registry.ts +3 -0
- package/src/runtime.ts +3 -3
- package/src/snapshotFormatter.test.ts +2 -2
- package/src/snapshotFormatter.ts +4 -4
- package/src/types.ts +11 -1
- package/.agents/skills/cross-project-adapter-migration/SKILL.md +0 -249
- package/.agents/workflows/cross-project-adapter-migration.md +0 -54
- package/dist/clis/notebooklm/bind-current.js +0 -29
- package/dist/clis/notebooklm/bind-current.test.d.ts +0 -1
- package/dist/clis/notebooklm/bind-current.test.js +0 -35
- package/dist/clis/notebooklm/binding.test.js +0 -44
- package/extension/dist/background.js +0 -819
- package/src/clis/notebooklm/bind-current.test.ts +0 -43
- package/src/clis/notebooklm/bind-current.ts +0 -36
- package/src/clis/notebooklm/binding.test.ts +0 -53
- /package/dist/browser/{mcp.d.ts → bridge.d.ts} +0 -0
- /package/dist/browser/{mcp.js → bridge.js} +0 -0
- /package/dist/{clis/notebooklm/bind-current.d.ts → browser/daemon-client.test.d.ts} +0 -0
- /package/dist/clis/{notebooklm/binding.test.d.ts → amazon/bestsellers.test.d.ts} +0 -0
- /package/src/browser/{mcp.ts → bridge.ts} +0 -0
|
@@ -0,0 +1,185 @@
|
|
|
1
|
+
#!/usr/bin/env npx tsx
|
|
2
|
+
/**
|
|
3
|
+
* Layer 1: Deterministic Browse Command Testing
|
|
4
|
+
*
|
|
5
|
+
* Runs predefined opencli operate command sequences against real websites.
|
|
6
|
+
* No LLM involved — tests command reliability only.
|
|
7
|
+
*
|
|
8
|
+
* Usage:
|
|
9
|
+
* npx tsx autoresearch/eval-browse.ts # Run all tasks
|
|
10
|
+
* npx tsx autoresearch/eval-browse.ts --task hn-top5 # Run single task
|
|
11
|
+
*/
|
|
12
|
+
|
|
13
|
+
import { execSync } from 'node:child_process';
|
|
14
|
+
import { readFileSync, writeFileSync, mkdirSync, readdirSync } from 'node:fs';
|
|
15
|
+
import { join, dirname } from 'node:path';
|
|
16
|
+
import { fileURLToPath } from 'node:url';
|
|
17
|
+
|
|
18
|
+
const __dirname = dirname(fileURLToPath(import.meta.url));
|
|
19
|
+
const TASKS_FILE = join(__dirname, 'browse-tasks.json');
|
|
20
|
+
const RESULTS_DIR = join(__dirname, 'results');
|
|
21
|
+
const BASELINE_FILE = join(__dirname, 'baseline-browse.txt');
|
|
22
|
+
|
|
23
|
+
interface BrowseTask {
|
|
24
|
+
name: string;
|
|
25
|
+
steps: string[];
|
|
26
|
+
judge: JudgeCriteria;
|
|
27
|
+
set?: 'test';
|
|
28
|
+
note?: string;
|
|
29
|
+
}
|
|
30
|
+
|
|
31
|
+
type JudgeCriteria =
|
|
32
|
+
| { type: 'contains'; value: string }
|
|
33
|
+
| { type: 'arrayMinLength'; minLength: number }
|
|
34
|
+
| { type: 'nonEmpty' }
|
|
35
|
+
| { type: 'matchesPattern'; pattern: string };
|
|
36
|
+
|
|
37
|
+
interface TaskResult {
|
|
38
|
+
name: string;
|
|
39
|
+
passed: boolean;
|
|
40
|
+
duration: number;
|
|
41
|
+
error?: string;
|
|
42
|
+
set: 'train' | 'test';
|
|
43
|
+
}
|
|
44
|
+
|
|
45
|
+
function judge(criteria: JudgeCriteria, output: string): boolean {
|
|
46
|
+
try {
|
|
47
|
+
switch (criteria.type) {
|
|
48
|
+
case 'contains':
|
|
49
|
+
return output.toLowerCase().includes(criteria.value.toLowerCase());
|
|
50
|
+
case 'arrayMinLength': {
|
|
51
|
+
try {
|
|
52
|
+
const arr = JSON.parse(output);
|
|
53
|
+
if (Array.isArray(arr)) return arr.length >= criteria.minLength;
|
|
54
|
+
} catch { /* not JSON array */ }
|
|
55
|
+
return false;
|
|
56
|
+
}
|
|
57
|
+
case 'nonEmpty':
|
|
58
|
+
return output.trim().length > 0 && output.trim() !== 'null' && output.trim() !== 'undefined';
|
|
59
|
+
case 'matchesPattern':
|
|
60
|
+
return new RegExp(criteria.pattern).test(output);
|
|
61
|
+
default:
|
|
62
|
+
return false;
|
|
63
|
+
}
|
|
64
|
+
} catch {
|
|
65
|
+
return false;
|
|
66
|
+
}
|
|
67
|
+
}
|
|
68
|
+
|
|
69
|
+
function runCommand(cmd: string): string {
|
|
70
|
+
try {
|
|
71
|
+
return execSync(cmd, {
|
|
72
|
+
cwd: join(__dirname, '..'),
|
|
73
|
+
timeout: 30000,
|
|
74
|
+
encoding: 'utf-8',
|
|
75
|
+
env: process.env,
|
|
76
|
+
stdio: ['pipe', 'pipe', 'pipe'],
|
|
77
|
+
}).trim();
|
|
78
|
+
} catch (err: any) {
|
|
79
|
+
return err.stdout?.trim() ?? '';
|
|
80
|
+
}
|
|
81
|
+
}
|
|
82
|
+
|
|
83
|
+
function runTask(task: BrowseTask): TaskResult {
|
|
84
|
+
const start = Date.now();
|
|
85
|
+
let lastOutput = '';
|
|
86
|
+
|
|
87
|
+
try {
|
|
88
|
+
for (const step of task.steps) {
|
|
89
|
+
lastOutput = runCommand(step);
|
|
90
|
+
}
|
|
91
|
+
|
|
92
|
+
const passed = judge(task.judge, lastOutput);
|
|
93
|
+
|
|
94
|
+
return {
|
|
95
|
+
name: task.name,
|
|
96
|
+
passed,
|
|
97
|
+
duration: Date.now() - start,
|
|
98
|
+
error: passed ? undefined : `Output: ${lastOutput.slice(0, 100)}`,
|
|
99
|
+
set: task.set === 'test' ? 'test' : 'train',
|
|
100
|
+
};
|
|
101
|
+
} catch (err: any) {
|
|
102
|
+
return {
|
|
103
|
+
name: task.name,
|
|
104
|
+
passed: false,
|
|
105
|
+
duration: Date.now() - start,
|
|
106
|
+
error: err.message?.slice(0, 100),
|
|
107
|
+
set: task.set === 'test' ? 'test' : 'train',
|
|
108
|
+
};
|
|
109
|
+
}
|
|
110
|
+
}
|
|
111
|
+
|
|
112
|
+
function main() {
|
|
113
|
+
const args = process.argv.slice(2);
|
|
114
|
+
const singleTask = args.includes('--task') ? args[args.indexOf('--task') + 1] : null;
|
|
115
|
+
|
|
116
|
+
const allTasks: BrowseTask[] = JSON.parse(readFileSync(TASKS_FILE, 'utf-8'));
|
|
117
|
+
const tasks = singleTask ? allTasks.filter(t => t.name === singleTask) : allTasks;
|
|
118
|
+
|
|
119
|
+
if (tasks.length === 0) {
|
|
120
|
+
console.error(`Task "${singleTask}" not found.`);
|
|
121
|
+
process.exit(1);
|
|
122
|
+
}
|
|
123
|
+
|
|
124
|
+
console.log(`\n🔬 Layer 1: Browse Commands — ${tasks.length} tasks\n`);
|
|
125
|
+
|
|
126
|
+
const results: TaskResult[] = [];
|
|
127
|
+
|
|
128
|
+
for (let i = 0; i < tasks.length; i++) {
|
|
129
|
+
const task = tasks[i];
|
|
130
|
+
process.stdout.write(` [${i + 1}/${tasks.length}] ${task.name}...`);
|
|
131
|
+
|
|
132
|
+
const result = runTask(task);
|
|
133
|
+
results.push(result);
|
|
134
|
+
|
|
135
|
+
const icon = result.passed ? '✓' : '✗';
|
|
136
|
+
console.log(` ${icon} (${(result.duration / 1000).toFixed(1)}s)`);
|
|
137
|
+
|
|
138
|
+
// Close browser between tasks for clean state
|
|
139
|
+
if (i < tasks.length - 1) {
|
|
140
|
+
try { runCommand('opencli operate close'); } catch { /* ignore */ }
|
|
141
|
+
}
|
|
142
|
+
}
|
|
143
|
+
|
|
144
|
+
// Final close
|
|
145
|
+
try { runCommand('opencli operate close'); } catch { /* ignore */ }
|
|
146
|
+
|
|
147
|
+
// Summary
|
|
148
|
+
const trainResults = results.filter(r => r.set === 'train');
|
|
149
|
+
const testResults = results.filter(r => r.set === 'test');
|
|
150
|
+
const totalPassed = results.filter(r => r.passed).length;
|
|
151
|
+
const trainPassed = trainResults.filter(r => r.passed).length;
|
|
152
|
+
const testPassed = testResults.filter(r => r.passed).length;
|
|
153
|
+
const totalDuration = results.reduce((s, r) => s + r.duration, 0);
|
|
154
|
+
|
|
155
|
+
console.log(`\n${'─'.repeat(50)}`);
|
|
156
|
+
console.log(` Score: ${totalPassed}/${results.length} (train: ${trainPassed}/${trainResults.length}, test: ${testPassed}/${testResults.length})`);
|
|
157
|
+
console.log(` Time: ${Math.round(totalDuration / 60000)}min`);
|
|
158
|
+
|
|
159
|
+
const failures = results.filter(r => !r.passed);
|
|
160
|
+
if (failures.length > 0) {
|
|
161
|
+
console.log(`\n Failures:`);
|
|
162
|
+
for (const f of failures) {
|
|
163
|
+
console.log(` ✗ ${f.name}: ${f.error ?? 'unknown'}`);
|
|
164
|
+
}
|
|
165
|
+
}
|
|
166
|
+
console.log('');
|
|
167
|
+
|
|
168
|
+
// Save result
|
|
169
|
+
mkdirSync(RESULTS_DIR, { recursive: true });
|
|
170
|
+
const existing = readdirSync(RESULTS_DIR).filter(f => f.startsWith('browse-')).length;
|
|
171
|
+
const roundNum = String(existing + 1).padStart(3, '0');
|
|
172
|
+
const resultPath = join(RESULTS_DIR, `browse-${roundNum}.json`);
|
|
173
|
+
writeFileSync(resultPath, JSON.stringify({
|
|
174
|
+
timestamp: new Date().toISOString(),
|
|
175
|
+
score: `${totalPassed}/${results.length}`,
|
|
176
|
+
trainScore: `${trainPassed}/${trainResults.length}`,
|
|
177
|
+
testScore: `${testPassed}/${testResults.length}`,
|
|
178
|
+
duration: `${Math.round(totalDuration / 60000)}min`,
|
|
179
|
+
tasks: results,
|
|
180
|
+
}, null, 2), 'utf-8');
|
|
181
|
+
console.log(` Results saved to: ${resultPath}`);
|
|
182
|
+
console.log(`\nSCORE=${totalPassed}/${results.length}`);
|
|
183
|
+
}
|
|
184
|
+
|
|
185
|
+
main();
|
|
@@ -0,0 +1,248 @@
|
|
|
1
|
+
#!/usr/bin/env npx tsx
|
|
2
|
+
/**
|
|
3
|
+
* Layer 2: Claude Code Skill E2E Testing (LLM Judge)
|
|
4
|
+
*
|
|
5
|
+
* Spawns Claude Code with the opencli-operate skill. Claude Code
|
|
6
|
+
* completes the task using browse commands AND judges its own result.
|
|
7
|
+
*
|
|
8
|
+
* Task format: YAML with judge_context (multi-criteria, like Browser Use)
|
|
9
|
+
*
|
|
10
|
+
* Usage:
|
|
11
|
+
* npx tsx autoresearch/eval-skill.ts # Run all
|
|
12
|
+
* npx tsx autoresearch/eval-skill.ts --task hn-top5 # Run single
|
|
13
|
+
*/
|
|
14
|
+
|
|
15
|
+
import { execSync } from 'node:child_process';
|
|
16
|
+
import { readFileSync, writeFileSync, mkdirSync, readdirSync } from 'node:fs';
|
|
17
|
+
import { join, dirname } from 'node:path';
|
|
18
|
+
import { fileURLToPath } from 'node:url';
|
|
19
|
+
|
|
20
|
+
const __dirname = dirname(fileURLToPath(import.meta.url));
|
|
21
|
+
const TASKS_FILE = join(__dirname, 'skill-tasks.yaml');
|
|
22
|
+
const RESULTS_DIR = join(__dirname, 'results');
|
|
23
|
+
const SKILL_PATH = join(__dirname, '..', 'skills', 'opencli-operate', 'SKILL.md');
|
|
24
|
+
|
|
25
|
+
// ── Types ──────────────────────────────────────────────────────────
|
|
26
|
+
|
|
27
|
+
interface SkillTask {
|
|
28
|
+
name: string;
|
|
29
|
+
task: string;
|
|
30
|
+
url?: string;
|
|
31
|
+
judge_context: string[];
|
|
32
|
+
max_steps?: number;
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
interface TaskResult {
|
|
36
|
+
name: string;
|
|
37
|
+
passed: boolean;
|
|
38
|
+
duration: number;
|
|
39
|
+
cost: number;
|
|
40
|
+
explanation: string;
|
|
41
|
+
}
|
|
42
|
+
|
|
43
|
+
// ── Task Definitions (inline, to avoid YAML dependency) ────────────
|
|
44
|
+
|
|
45
|
+
const TASKS: SkillTask[] = [
|
|
46
|
+
// Extract
|
|
47
|
+
{ name: "extract-title-example", task: "Extract the main heading text from this page", url: "https://example.com", judge_context: ["Output must contain 'Example Domain'"] },
|
|
48
|
+
{ name: "extract-paragraph-wiki", task: "Extract the first paragraph of the JavaScript article", url: "https://en.wikipedia.org/wiki/JavaScript", judge_context: ["Output must mention 'programming language'", "Output must contain actual paragraph text, not just the title"] },
|
|
49
|
+
{ name: "extract-github-stars", task: "Find the number of stars on this repository", url: "https://github.com/browser-use/browser-use", judge_context: ["Output must contain a number (the star count)"] },
|
|
50
|
+
{ name: "extract-npm-downloads", task: "Find the weekly download count for this package", url: "https://www.npmjs.com/package/zod", judge_context: ["Output must contain a number (weekly downloads)"] },
|
|
51
|
+
|
|
52
|
+
// List extraction
|
|
53
|
+
{ name: "list-hn-top5", task: "Extract the top 5 stories with their titles", url: "https://news.ycombinator.com", judge_context: ["Output must contain 5 story titles", "Each title must be an actual HN story, not made up"] },
|
|
54
|
+
{ name: "list-books-5", task: "Extract the first 5 books with their title and price", url: "https://books.toscrape.com", judge_context: ["Output must contain 5 books", "Each book must have a title and a price"] },
|
|
55
|
+
{ name: "list-quotes-3", task: "Extract the first 3 quotes with their text and author", url: "https://quotes.toscrape.com", judge_context: ["Output must contain 3 quotes", "Each quote must have text and an author name"] },
|
|
56
|
+
{ name: "list-github-trending", task: "Extract the top 3 trending repositories with name and description", url: "https://github.com/trending", judge_context: ["Output must contain 3 repositories", "Each must have a repo name"] },
|
|
57
|
+
{ name: "list-jsonplaceholder", task: "Extract the first 5 posts with their title", url: "https://jsonplaceholder.typicode.com/posts", judge_context: ["Output must contain 5 posts", "Each post must have a title"] },
|
|
58
|
+
|
|
59
|
+
// Search
|
|
60
|
+
{ name: "search-ddg", task: "Search for 'TypeScript tutorial' and extract the first 3 result titles", url: "https://duckduckgo.com", judge_context: ["The agent must type a search query", "Output must contain at least 3 search result titles"] },
|
|
61
|
+
{ name: "search-npm", task: "Search for 'react' and extract the top 3 package names", url: "https://www.npmjs.com", judge_context: ["The agent must search for 'react'", "Output must contain at least 3 package names"] },
|
|
62
|
+
{ name: "search-wiki", task: "Search for 'Rust programming language' and extract the first sentence of the article", url: "https://en.wikipedia.org", judge_context: ["The agent must search and navigate to the article", "Output must mention 'programming language'"] },
|
|
63
|
+
|
|
64
|
+
// Navigation
|
|
65
|
+
{ name: "nav-click-link", task: "Click the 'More information...' link and extract the heading of the new page", url: "https://example.com", judge_context: ["The agent must click a link", "Output must contain 'IANA' or reference the new page"] },
|
|
66
|
+
{ name: "nav-click-hn", task: "Click on the first story link and tell me the title of the page you land on", url: "https://news.ycombinator.com", judge_context: ["The agent must click a story link", "Output must contain the title of the destination page"] },
|
|
67
|
+
{ name: "nav-go-back", task: "Click the 'More information...' link, then go back, and tell me the heading of the original page", url: "https://example.com", judge_context: ["The agent must click a link then go back", "Output must contain 'Example Domain'"] },
|
|
68
|
+
{ name: "nav-multi-step", task: "Click the Next page link at the bottom, then extract the first quote from page 2", url: "https://quotes.toscrape.com", judge_context: ["The agent must navigate to page 2", "Output must contain a quote from page 2"] },
|
|
69
|
+
|
|
70
|
+
// Scroll
|
|
71
|
+
{ name: "scroll-footer", task: "Scroll to the bottom and extract the footer text", url: "https://quotes.toscrape.com", judge_context: ["The agent must scroll down", "Output must contain footer or bottom-of-page content"] },
|
|
72
|
+
{ name: "scroll-pagination", task: "Find the pagination info at the bottom of the page", url: "https://books.toscrape.com", judge_context: ["Output must contain page number or pagination info"] },
|
|
73
|
+
|
|
74
|
+
// Form
|
|
75
|
+
{ name: "form-fill-basic", task: "Fill the Customer Name with 'OpenCLI' and Telephone with '555-0100'. Do not submit.", url: "https://httpbin.org/forms/post", judge_context: ["The agent must type 'OpenCLI' into a name field", "The agent must type '555-0100' into a phone field", "The form must NOT be submitted"] },
|
|
76
|
+
{ name: "form-radio", task: "Select the 'Medium' pizza size option. Do not submit.", url: "https://httpbin.org/forms/post", judge_context: ["The agent must select a radio button for Medium size"] },
|
|
77
|
+
{ name: "form-login", task: "Fill the username with 'testuser' and password with 'testpass'. Do not submit.", url: "https://the-internet.herokuapp.com/login", judge_context: ["The agent must fill the username field", "The agent must fill the password field", "The form must NOT be submitted"] },
|
|
78
|
+
|
|
79
|
+
// Complex
|
|
80
|
+
{ name: "complex-wiki-toc", task: "Extract the table of contents headings", url: "https://en.wikipedia.org/wiki/JavaScript", judge_context: ["Output must contain at least 5 section headings from the table of contents"] },
|
|
81
|
+
{ name: "complex-books-detail", task: "Click on the first book and extract its title and price from the detail page", url: "https://books.toscrape.com", judge_context: ["The agent must click on a book", "Output must contain the book title", "Output must contain a price"] },
|
|
82
|
+
{ name: "complex-quotes-page2", task: "Navigate to page 2 and extract the first 3 quotes with their authors", url: "https://quotes.toscrape.com", judge_context: ["The agent must navigate to page 2", "Output must contain 3 quotes with authors"] },
|
|
83
|
+
{ name: "complex-multi-extract", task: "Extract both the page title and the first paragraph text", url: "https://en.wikipedia.org/wiki/TypeScript", judge_context: ["Output must contain 'TypeScript'", "Output must contain actual paragraph text"] },
|
|
84
|
+
|
|
85
|
+
// Bench (harder, real-world)
|
|
86
|
+
{ name: "bench-reddit", task: "Extract the titles of the top 5 posts", url: "https://old.reddit.com", judge_context: ["Output must contain 5 post titles", "Titles must be actual Reddit posts"] },
|
|
87
|
+
{ name: "bench-imdb", task: "Find the year and rating of The Matrix", url: "https://www.imdb.com/title/tt0133093/", judge_context: ["Output must contain '1999'", "Output must contain a rating number"] },
|
|
88
|
+
{ name: "bench-github-profile", task: "Extract the bio and number of public repositories", url: "https://github.com/torvalds", judge_context: ["Output must contain bio text or 'Linux'", "Output must contain a number for repos"] },
|
|
89
|
+
{ name: "bench-httpbin", task: "Extract the User-Agent header shown on this page", url: "https://httpbin.org/headers", judge_context: ["Output must contain a User-Agent string"] },
|
|
90
|
+
{ name: "bench-jsonapi-todo", task: "Extract the first 5 todo items with their title and completion status", url: "https://jsonplaceholder.typicode.com/todos", judge_context: ["Output must contain 5 todo items", "Each must have a title and completed status"] },
|
|
91
|
+
|
|
92
|
+
// Codex form (the real test)
|
|
93
|
+
{ name: "codex-form-fill", task: "Fill the basic information using 'opencli' as the identity (first name=open, last name=cli, email=opencli@example.com, GitHub username=opencli). Do NOT submit the form.", url: "https://openai.com/form/codex-for-oss/", judge_context: ["The agent must fill the first name field", "The agent must fill the last name field", "The agent must fill the email field", "The form must NOT be submitted"], max_steps: 15 },
|
|
94
|
+
];
|
|
95
|
+
|
|
96
|
+
// ── Run Task ───────────────────────────────────────────────────────
|
|
97
|
+
|
|
98
|
+
function runSkillTask(task: SkillTask): TaskResult {
|
|
99
|
+
const start = Date.now();
|
|
100
|
+
const skillContent = readFileSync(SKILL_PATH, 'utf-8');
|
|
101
|
+
const urlPart = task.url ? ` Start URL: ${task.url}` : '';
|
|
102
|
+
const criteria = task.judge_context.map((c, i) => `${i + 1}. ${c}`).join('\n');
|
|
103
|
+
|
|
104
|
+
const prompt = `Complete this browser task using opencli operate commands:
|
|
105
|
+
|
|
106
|
+
TASK: ${task.task}${urlPart}
|
|
107
|
+
|
|
108
|
+
After completing the task, evaluate your own result against these criteria:
|
|
109
|
+
${criteria}
|
|
110
|
+
|
|
111
|
+
At the very end of your response, output a JSON verdict on its own line:
|
|
112
|
+
{"success": true/false, "explanation": "brief explanation"}
|
|
113
|
+
|
|
114
|
+
Always close the browser with 'opencli operate close' when done.`;
|
|
115
|
+
|
|
116
|
+
try {
|
|
117
|
+
const output = execSync(
|
|
118
|
+
`claude -p --dangerously-skip-permissions --allowedTools "Bash(opencli:*)" --system-prompt ${JSON.stringify(skillContent)} --output-format json --no-session-persistence ${JSON.stringify(prompt)}`,
|
|
119
|
+
{
|
|
120
|
+
cwd: join(__dirname, '..'),
|
|
121
|
+
timeout: (task.max_steps ?? 10) * 15_000,
|
|
122
|
+
encoding: 'utf-8',
|
|
123
|
+
env: process.env,
|
|
124
|
+
stdio: ['pipe', 'pipe', 'pipe'],
|
|
125
|
+
}
|
|
126
|
+
);
|
|
127
|
+
|
|
128
|
+
const duration = Date.now() - start;
|
|
129
|
+
|
|
130
|
+
// Parse Claude Code output
|
|
131
|
+
let resultText = '';
|
|
132
|
+
let cost = 0;
|
|
133
|
+
try {
|
|
134
|
+
const parsed = JSON.parse(output);
|
|
135
|
+
resultText = parsed.result ?? output;
|
|
136
|
+
cost = parsed.total_cost_usd ?? 0;
|
|
137
|
+
} catch {
|
|
138
|
+
resultText = output;
|
|
139
|
+
}
|
|
140
|
+
|
|
141
|
+
// Extract verdict JSON from the result
|
|
142
|
+
const verdict = extractVerdict(resultText);
|
|
143
|
+
|
|
144
|
+
return {
|
|
145
|
+
name: task.name,
|
|
146
|
+
passed: verdict.success,
|
|
147
|
+
duration,
|
|
148
|
+
cost,
|
|
149
|
+
explanation: verdict.explanation,
|
|
150
|
+
};
|
|
151
|
+
} catch (err: any) {
|
|
152
|
+
return {
|
|
153
|
+
name: task.name,
|
|
154
|
+
passed: false,
|
|
155
|
+
duration: Date.now() - start,
|
|
156
|
+
cost: 0,
|
|
157
|
+
explanation: (err.stdout ?? err.message ?? 'timeout or crash').slice(0, 200),
|
|
158
|
+
};
|
|
159
|
+
}
|
|
160
|
+
}
|
|
161
|
+
|
|
162
|
+
function extractVerdict(text: string): { success: boolean; explanation: string } {
|
|
163
|
+
// Try to find {"success": ...} JSON in the text
|
|
164
|
+
const jsonMatches = text.match(/\{"success"\s*:\s*(true|false)\s*,\s*"explanation"\s*:\s*"([^"]*)"\s*\}/g);
|
|
165
|
+
if (jsonMatches) {
|
|
166
|
+
const last = jsonMatches[jsonMatches.length - 1];
|
|
167
|
+
try {
|
|
168
|
+
return JSON.parse(last);
|
|
169
|
+
} catch { /* fall through */ }
|
|
170
|
+
}
|
|
171
|
+
|
|
172
|
+
// Fallback: check for success indicators in text
|
|
173
|
+
const lower = text.toLowerCase();
|
|
174
|
+
if (lower.includes('"success": true') || lower.includes('"success":true')) {
|
|
175
|
+
return { success: true, explanation: 'Parsed success from output' };
|
|
176
|
+
}
|
|
177
|
+
if (lower.includes('"success": false') || lower.includes('"success":false')) {
|
|
178
|
+
return { success: false, explanation: 'Parsed failure from output' };
|
|
179
|
+
}
|
|
180
|
+
|
|
181
|
+
// Final fallback: assume failure if we can't parse
|
|
182
|
+
return { success: false, explanation: 'Could not parse verdict from output' };
|
|
183
|
+
}
|
|
184
|
+
|
|
185
|
+
// ── Main ───────────────────────────────────────────────────────────
|
|
186
|
+
|
|
187
|
+
function main() {
|
|
188
|
+
const args = process.argv.slice(2);
|
|
189
|
+
const singleTask = args.includes('--task') ? args[args.indexOf('--task') + 1] : null;
|
|
190
|
+
const tasks = singleTask ? TASKS.filter(t => t.name === singleTask) : TASKS;
|
|
191
|
+
|
|
192
|
+
if (tasks.length === 0) {
|
|
193
|
+
console.error(`Task "${singleTask}" not found. Available: ${TASKS.map(t => t.name).join(', ')}`);
|
|
194
|
+
process.exit(1);
|
|
195
|
+
}
|
|
196
|
+
|
|
197
|
+
console.log(`\n🔬 Layer 2: Skill E2E (LLM Judge) — ${tasks.length} tasks\n`);
|
|
198
|
+
|
|
199
|
+
const results: TaskResult[] = [];
|
|
200
|
+
|
|
201
|
+
for (let i = 0; i < tasks.length; i++) {
|
|
202
|
+
const task = tasks[i];
|
|
203
|
+
process.stdout.write(` [${i + 1}/${tasks.length}] ${task.name}...`);
|
|
204
|
+
|
|
205
|
+
const result = runSkillTask(task);
|
|
206
|
+
results.push(result);
|
|
207
|
+
|
|
208
|
+
const icon = result.passed ? '✓' : '✗';
|
|
209
|
+
const costStr = result.cost > 0 ? `, $${result.cost.toFixed(2)}` : '';
|
|
210
|
+
console.log(` ${icon} (${Math.round(result.duration / 1000)}s${costStr})`);
|
|
211
|
+
}
|
|
212
|
+
|
|
213
|
+
// Summary
|
|
214
|
+
const totalPassed = results.filter(r => r.passed).length;
|
|
215
|
+
const totalCost = results.reduce((s, r) => s + r.cost, 0);
|
|
216
|
+
const totalDuration = results.reduce((s, r) => s + r.duration, 0);
|
|
217
|
+
|
|
218
|
+
console.log(`\n${'─'.repeat(50)}`);
|
|
219
|
+
console.log(` Score: ${totalPassed}/${results.length} (${Math.round(totalPassed / results.length * 100)}%)`);
|
|
220
|
+
console.log(` Cost: $${totalCost.toFixed(2)}`);
|
|
221
|
+
console.log(` Time: ${Math.round(totalDuration / 60000)}min`);
|
|
222
|
+
|
|
223
|
+
const failures = results.filter(r => !r.passed);
|
|
224
|
+
if (failures.length > 0) {
|
|
225
|
+
console.log(`\n Failures:`);
|
|
226
|
+
for (const f of failures) {
|
|
227
|
+
console.log(` ✗ ${f.name}: ${f.explanation}`);
|
|
228
|
+
}
|
|
229
|
+
}
|
|
230
|
+
console.log('');
|
|
231
|
+
|
|
232
|
+
// Save
|
|
233
|
+
mkdirSync(RESULTS_DIR, { recursive: true });
|
|
234
|
+
const existing = readdirSync(RESULTS_DIR).filter(f => f.startsWith('skill-')).length;
|
|
235
|
+
const roundNum = String(existing + 1).padStart(3, '0');
|
|
236
|
+
const resultPath = join(RESULTS_DIR, `skill-${roundNum}.json`);
|
|
237
|
+
writeFileSync(resultPath, JSON.stringify({
|
|
238
|
+
timestamp: new Date().toISOString(),
|
|
239
|
+
score: `${totalPassed}/${results.length}`,
|
|
240
|
+
totalCost,
|
|
241
|
+
duration: `${Math.round(totalDuration / 60000)}min`,
|
|
242
|
+
tasks: results,
|
|
243
|
+
}, null, 2), 'utf-8');
|
|
244
|
+
console.log(` Results saved to: ${resultPath}`);
|
|
245
|
+
console.log(`\nSCORE=${totalPassed}/${results.length}`);
|
|
246
|
+
}
|
|
247
|
+
|
|
248
|
+
main();
|
|
@@ -0,0 +1,48 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* BasePage — shared IPage method implementations for DOM helpers.
|
|
3
|
+
*
|
|
4
|
+
* Both Page (daemon-backed) and CDPPage (direct CDP) execute JS the same way
|
|
5
|
+
* for DOM operations. This base class deduplicates ~200 lines of identical
|
|
6
|
+
* click/type/scroll/wait/snapshot/interceptor methods.
|
|
7
|
+
*
|
|
8
|
+
* Subclasses implement the transport-specific methods: goto, evaluate,
|
|
9
|
+
* getCookies, screenshot, tabs, etc.
|
|
10
|
+
*/
|
|
11
|
+
import type { BrowserCookie, IPage, ScreenshotOptions, SnapshotOptions, WaitOptions } from '../types.js';
|
|
12
|
+
export declare abstract class BasePage implements IPage {
|
|
13
|
+
protected _lastUrl: string | null;
|
|
14
|
+
abstract goto(url: string, options?: {
|
|
15
|
+
waitUntil?: 'load' | 'none';
|
|
16
|
+
settleMs?: number;
|
|
17
|
+
}): Promise<void>;
|
|
18
|
+
abstract evaluate(js: string): Promise<unknown>;
|
|
19
|
+
abstract getCookies(opts?: {
|
|
20
|
+
domain?: string;
|
|
21
|
+
url?: string;
|
|
22
|
+
}): Promise<BrowserCookie[]>;
|
|
23
|
+
abstract screenshot(options?: ScreenshotOptions): Promise<string>;
|
|
24
|
+
abstract tabs(): Promise<unknown[]>;
|
|
25
|
+
abstract closeTab(index?: number): Promise<void>;
|
|
26
|
+
abstract newTab(): Promise<void>;
|
|
27
|
+
abstract selectTab(index: number): Promise<void>;
|
|
28
|
+
click(ref: string): Promise<void>;
|
|
29
|
+
typeText(ref: string, text: string): Promise<void>;
|
|
30
|
+
pressKey(key: string): Promise<void>;
|
|
31
|
+
scrollTo(ref: string): Promise<unknown>;
|
|
32
|
+
getFormState(): Promise<Record<string, unknown>>;
|
|
33
|
+
scroll(direction?: string, amount?: number): Promise<void>;
|
|
34
|
+
autoScroll(options?: {
|
|
35
|
+
times?: number;
|
|
36
|
+
delayMs?: number;
|
|
37
|
+
}): Promise<void>;
|
|
38
|
+
networkRequests(includeStatic?: boolean): Promise<unknown[]>;
|
|
39
|
+
consoleMessages(_level?: string): Promise<unknown[]>;
|
|
40
|
+
wait(options: number | WaitOptions): Promise<void>;
|
|
41
|
+
snapshot(opts?: SnapshotOptions): Promise<unknown>;
|
|
42
|
+
getCurrentUrl(): Promise<string | null>;
|
|
43
|
+
installInterceptor(pattern: string): Promise<void>;
|
|
44
|
+
getInterceptedRequests(): Promise<unknown[]>;
|
|
45
|
+
waitForCapture(timeout?: number): Promise<void>;
|
|
46
|
+
/** Fallback basic snapshot */
|
|
47
|
+
protected _basicSnapshot(opts?: Pick<SnapshotOptions, 'interactive' | 'compact' | 'maxDepth' | 'raw'>): Promise<unknown>;
|
|
48
|
+
}
|
|
@@ -0,0 +1,160 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* BasePage — shared IPage method implementations for DOM helpers.
|
|
3
|
+
*
|
|
4
|
+
* Both Page (daemon-backed) and CDPPage (direct CDP) execute JS the same way
|
|
5
|
+
* for DOM operations. This base class deduplicates ~200 lines of identical
|
|
6
|
+
* click/type/scroll/wait/snapshot/interceptor methods.
|
|
7
|
+
*
|
|
8
|
+
* Subclasses implement the transport-specific methods: goto, evaluate,
|
|
9
|
+
* getCookies, screenshot, tabs, etc.
|
|
10
|
+
*/
|
|
11
|
+
import { generateSnapshotJs, scrollToRefJs, getFormStateJs } from './dom-snapshot.js';
|
|
12
|
+
import { clickJs, typeTextJs, pressKeyJs, waitForTextJs, waitForCaptureJs, waitForSelectorJs, scrollJs, autoScrollJs, networkRequestsJs, waitForDomStableJs, } from './dom-helpers.js';
|
|
13
|
+
import { formatSnapshot } from '../snapshotFormatter.js';
|
|
14
|
+
export class BasePage {
|
|
15
|
+
_lastUrl = null;
|
|
16
|
+
// ── Shared DOM helper implementations ──
|
|
17
|
+
async click(ref) {
|
|
18
|
+
await this.evaluate(clickJs(ref));
|
|
19
|
+
}
|
|
20
|
+
async typeText(ref, text) {
|
|
21
|
+
await this.evaluate(typeTextJs(ref, text));
|
|
22
|
+
}
|
|
23
|
+
async pressKey(key) {
|
|
24
|
+
await this.evaluate(pressKeyJs(key));
|
|
25
|
+
}
|
|
26
|
+
async scrollTo(ref) {
|
|
27
|
+
return this.evaluate(scrollToRefJs(ref));
|
|
28
|
+
}
|
|
29
|
+
async getFormState() {
|
|
30
|
+
return (await this.evaluate(getFormStateJs()));
|
|
31
|
+
}
|
|
32
|
+
async scroll(direction = 'down', amount = 500) {
|
|
33
|
+
await this.evaluate(scrollJs(direction, amount));
|
|
34
|
+
}
|
|
35
|
+
async autoScroll(options) {
|
|
36
|
+
const times = options?.times ?? 3;
|
|
37
|
+
const delayMs = options?.delayMs ?? 2000;
|
|
38
|
+
await this.evaluate(autoScrollJs(times, delayMs));
|
|
39
|
+
}
|
|
40
|
+
async networkRequests(includeStatic = false) {
|
|
41
|
+
const result = await this.evaluate(networkRequestsJs(includeStatic));
|
|
42
|
+
return Array.isArray(result) ? result : [];
|
|
43
|
+
}
|
|
44
|
+
async consoleMessages(_level = 'info') {
|
|
45
|
+
return [];
|
|
46
|
+
}
|
|
47
|
+
async wait(options) {
|
|
48
|
+
if (typeof options === 'number') {
|
|
49
|
+
if (options >= 1) {
|
|
50
|
+
try {
|
|
51
|
+
const maxMs = options * 1000;
|
|
52
|
+
await this.evaluate(waitForDomStableJs(maxMs, Math.min(500, maxMs)));
|
|
53
|
+
return;
|
|
54
|
+
}
|
|
55
|
+
catch {
|
|
56
|
+
// Fallback: fixed sleep
|
|
57
|
+
}
|
|
58
|
+
}
|
|
59
|
+
await new Promise(resolve => setTimeout(resolve, options * 1000));
|
|
60
|
+
return;
|
|
61
|
+
}
|
|
62
|
+
if (typeof options.time === 'number') {
|
|
63
|
+
await new Promise(resolve => setTimeout(resolve, options.time * 1000));
|
|
64
|
+
return;
|
|
65
|
+
}
|
|
66
|
+
if (options.selector) {
|
|
67
|
+
const timeout = (options.timeout ?? 10) * 1000;
|
|
68
|
+
await this.evaluate(waitForSelectorJs(options.selector, timeout));
|
|
69
|
+
return;
|
|
70
|
+
}
|
|
71
|
+
if (options.text) {
|
|
72
|
+
const timeout = (options.timeout ?? 30) * 1000;
|
|
73
|
+
await this.evaluate(waitForTextJs(options.text, timeout));
|
|
74
|
+
}
|
|
75
|
+
}
|
|
76
|
+
async snapshot(opts = {}) {
|
|
77
|
+
const snapshotJs = generateSnapshotJs({
|
|
78
|
+
viewportExpand: opts.viewportExpand ?? 800,
|
|
79
|
+
maxDepth: Math.max(1, Math.min(Number(opts.maxDepth) || 50, 200)),
|
|
80
|
+
interactiveOnly: opts.interactive ?? false,
|
|
81
|
+
maxTextLength: opts.maxTextLength ?? 120,
|
|
82
|
+
includeScrollInfo: true,
|
|
83
|
+
bboxDedup: true,
|
|
84
|
+
});
|
|
85
|
+
try {
|
|
86
|
+
return await this.evaluate(snapshotJs);
|
|
87
|
+
}
|
|
88
|
+
catch {
|
|
89
|
+
return this._basicSnapshot(opts);
|
|
90
|
+
}
|
|
91
|
+
}
|
|
92
|
+
async getCurrentUrl() {
|
|
93
|
+
if (this._lastUrl)
|
|
94
|
+
return this._lastUrl;
|
|
95
|
+
try {
|
|
96
|
+
const current = await this.evaluate('window.location.href');
|
|
97
|
+
if (typeof current === 'string' && current) {
|
|
98
|
+
this._lastUrl = current;
|
|
99
|
+
return current;
|
|
100
|
+
}
|
|
101
|
+
}
|
|
102
|
+
catch {
|
|
103
|
+
// Best-effort
|
|
104
|
+
}
|
|
105
|
+
return null;
|
|
106
|
+
}
|
|
107
|
+
async installInterceptor(pattern) {
|
|
108
|
+
const { generateInterceptorJs } = await import('../interceptor.js');
|
|
109
|
+
await this.evaluate(generateInterceptorJs(JSON.stringify(pattern), {
|
|
110
|
+
arrayName: '__opencli_xhr',
|
|
111
|
+
patchGuard: '__opencli_interceptor_patched',
|
|
112
|
+
}));
|
|
113
|
+
}
|
|
114
|
+
async getInterceptedRequests() {
|
|
115
|
+
const { generateReadInterceptedJs } = await import('../interceptor.js');
|
|
116
|
+
const result = await this.evaluate(generateReadInterceptedJs('__opencli_xhr'));
|
|
117
|
+
return Array.isArray(result) ? result : [];
|
|
118
|
+
}
|
|
119
|
+
async waitForCapture(timeout = 10) {
|
|
120
|
+
const maxMs = timeout * 1000;
|
|
121
|
+
await this.evaluate(waitForCaptureJs(maxMs));
|
|
122
|
+
}
|
|
123
|
+
/** Fallback basic snapshot */
|
|
124
|
+
async _basicSnapshot(opts = {}) {
|
|
125
|
+
const maxDepth = Math.max(1, Math.min(Number(opts.maxDepth) || 50, 200));
|
|
126
|
+
const code = `
|
|
127
|
+
(async () => {
|
|
128
|
+
function buildTree(node, depth) {
|
|
129
|
+
if (depth > ${maxDepth}) return '';
|
|
130
|
+
const role = node.getAttribute?.('role') || node.tagName?.toLowerCase() || 'generic';
|
|
131
|
+
const name = node.getAttribute?.('aria-label') || node.getAttribute?.('alt') || node.textContent?.trim().slice(0, 80) || '';
|
|
132
|
+
const isInteractive = ['a', 'button', 'input', 'select', 'textarea'].includes(node.tagName?.toLowerCase()) || node.getAttribute?.('tabindex') != null;
|
|
133
|
+
|
|
134
|
+
${opts.interactive ? 'if (!isInteractive && !node.children?.length) return "";' : ''}
|
|
135
|
+
|
|
136
|
+
let indent = ' '.repeat(depth);
|
|
137
|
+
let line = indent + role;
|
|
138
|
+
if (name) line += ' "' + name.replace(/"/g, '\\\\\\"') + '"';
|
|
139
|
+
if (node.tagName?.toLowerCase() === 'a' && node.href) line += ' [' + node.href + ']';
|
|
140
|
+
if (node.tagName?.toLowerCase() === 'input') line += ' [' + (node.type || 'text') + ']';
|
|
141
|
+
|
|
142
|
+
let result = line + '\\n';
|
|
143
|
+
if (node.children) {
|
|
144
|
+
for (const child of node.children) {
|
|
145
|
+
result += buildTree(child, depth + 1);
|
|
146
|
+
}
|
|
147
|
+
}
|
|
148
|
+
return result;
|
|
149
|
+
}
|
|
150
|
+
return buildTree(document.body, 0);
|
|
151
|
+
})()
|
|
152
|
+
`;
|
|
153
|
+
const raw = await this.evaluate(code);
|
|
154
|
+
if (opts.raw)
|
|
155
|
+
return raw;
|
|
156
|
+
if (typeof raw === 'string')
|
|
157
|
+
return formatSnapshot(raw, opts);
|
|
158
|
+
return raw;
|
|
159
|
+
}
|
|
160
|
+
}
|