@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
|
@@ -7,9 +7,11 @@
|
|
|
7
7
|
import { DEFAULT_DAEMON_PORT } from '../constants.js';
|
|
8
8
|
import type { BrowserSessionInfo } from '../types.js';
|
|
9
9
|
import { sleep } from '../utils.js';
|
|
10
|
+
import { isTransientBrowserError } from './errors.js';
|
|
10
11
|
|
|
11
12
|
const DAEMON_PORT = parseInt(process.env.OPENCLI_DAEMON_PORT ?? String(DEFAULT_DAEMON_PORT), 10);
|
|
12
13
|
const DAEMON_URL = `http://127.0.0.1:${DAEMON_PORT}`;
|
|
14
|
+
const OPENCLI_HEADERS = { 'X-OpenCLI': '1' };
|
|
13
15
|
|
|
14
16
|
let _idCounter = 0;
|
|
15
17
|
|
|
@@ -19,7 +21,7 @@ function generateId(): string {
|
|
|
19
21
|
|
|
20
22
|
export interface DaemonCommand {
|
|
21
23
|
id: string;
|
|
22
|
-
action: 'exec' | 'navigate' | 'tabs' | 'cookies' | 'screenshot' | 'close-window' | 'sessions' | 'set-file-input' | '
|
|
24
|
+
action: 'exec' | 'navigate' | 'tabs' | 'cookies' | 'screenshot' | 'close-window' | 'sessions' | 'set-file-input' | 'cdp';
|
|
23
25
|
tabId?: number;
|
|
24
26
|
code?: string;
|
|
25
27
|
workspace?: string;
|
|
@@ -27,15 +29,16 @@ export interface DaemonCommand {
|
|
|
27
29
|
op?: string;
|
|
28
30
|
index?: number;
|
|
29
31
|
domain?: string;
|
|
30
|
-
matchDomain?: string;
|
|
31
|
-
matchPathPrefix?: string;
|
|
32
32
|
format?: 'png' | 'jpeg';
|
|
33
33
|
quality?: number;
|
|
34
34
|
fullPage?: boolean;
|
|
35
|
+
|
|
35
36
|
/** Local file paths for set-file-input action */
|
|
36
37
|
files?: string[];
|
|
37
38
|
/** CSS selector for file input element (set-file-input action) */
|
|
38
39
|
selector?: string;
|
|
40
|
+
cdpMethod?: string;
|
|
41
|
+
cdpParams?: Record<string, unknown>;
|
|
39
42
|
}
|
|
40
43
|
|
|
41
44
|
export interface DaemonResult {
|
|
@@ -45,42 +48,65 @@ export interface DaemonResult {
|
|
|
45
48
|
error?: string;
|
|
46
49
|
}
|
|
47
50
|
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
|
|
51
|
+
export interface DaemonStatus {
|
|
52
|
+
ok: boolean;
|
|
53
|
+
pid: number;
|
|
54
|
+
uptime: number;
|
|
55
|
+
extensionConnected: boolean;
|
|
56
|
+
extensionVersion?: string;
|
|
57
|
+
pending: number;
|
|
58
|
+
lastCliRequestTime: number;
|
|
59
|
+
memoryMB: number;
|
|
60
|
+
port: number;
|
|
61
|
+
}
|
|
62
|
+
|
|
63
|
+
async function requestDaemon(pathname: string, init?: RequestInit & { timeout?: number }): Promise<Response> {
|
|
64
|
+
const { timeout = 2000, headers, ...rest } = init ?? {};
|
|
65
|
+
const controller = new AbortController();
|
|
66
|
+
const timer = setTimeout(() => controller.abort(), timeout);
|
|
52
67
|
try {
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
headers: { 'X-OpenCLI': '1' },
|
|
68
|
+
return await fetch(`${DAEMON_URL}${pathname}`, {
|
|
69
|
+
...rest,
|
|
70
|
+
headers: { ...OPENCLI_HEADERS, ...headers },
|
|
57
71
|
signal: controller.signal,
|
|
58
72
|
});
|
|
73
|
+
} finally {
|
|
59
74
|
clearTimeout(timer);
|
|
75
|
+
}
|
|
76
|
+
}
|
|
77
|
+
|
|
78
|
+
export async function fetchDaemonStatus(opts?: { timeout?: number }): Promise<DaemonStatus | null> {
|
|
79
|
+
try {
|
|
80
|
+
const res = await requestDaemon('/status', { timeout: opts?.timeout ?? 2000 });
|
|
81
|
+
if (!res.ok) return null;
|
|
82
|
+
return await res.json() as DaemonStatus;
|
|
83
|
+
} catch {
|
|
84
|
+
return null;
|
|
85
|
+
}
|
|
86
|
+
}
|
|
87
|
+
|
|
88
|
+
export async function requestDaemonShutdown(opts?: { timeout?: number }): Promise<boolean> {
|
|
89
|
+
try {
|
|
90
|
+
const res = await requestDaemon('/shutdown', { method: 'POST', timeout: opts?.timeout ?? 5000 });
|
|
60
91
|
return res.ok;
|
|
61
92
|
} catch {
|
|
62
93
|
return false;
|
|
63
94
|
}
|
|
64
95
|
}
|
|
65
96
|
|
|
97
|
+
/**
|
|
98
|
+
* Check if daemon is running.
|
|
99
|
+
*/
|
|
100
|
+
export async function isDaemonRunning(): Promise<boolean> {
|
|
101
|
+
return (await fetchDaemonStatus()) !== null;
|
|
102
|
+
}
|
|
103
|
+
|
|
66
104
|
/**
|
|
67
105
|
* Check if daemon is running AND the extension is connected.
|
|
68
106
|
*/
|
|
69
107
|
export async function isExtensionConnected(): Promise<boolean> {
|
|
70
|
-
|
|
71
|
-
|
|
72
|
-
const timer = setTimeout(() => controller.abort(), 2000);
|
|
73
|
-
const res = await fetch(`${DAEMON_URL}/status`, {
|
|
74
|
-
headers: { 'X-OpenCLI': '1' },
|
|
75
|
-
signal: controller.signal,
|
|
76
|
-
});
|
|
77
|
-
clearTimeout(timer);
|
|
78
|
-
if (!res.ok) return false;
|
|
79
|
-
const data = await res.json() as { extensionConnected?: boolean };
|
|
80
|
-
return !!data.extensionConnected;
|
|
81
|
-
} catch {
|
|
82
|
-
return false;
|
|
83
|
-
}
|
|
108
|
+
const status = await fetchDaemonStatus();
|
|
109
|
+
return !!status?.extensionConnected;
|
|
84
110
|
}
|
|
85
111
|
|
|
86
112
|
/**
|
|
@@ -99,27 +125,18 @@ export async function sendCommand(
|
|
|
99
125
|
const id = generateId();
|
|
100
126
|
const command: DaemonCommand = { id, action, ...params };
|
|
101
127
|
try {
|
|
102
|
-
const
|
|
103
|
-
const timer = setTimeout(() => controller.abort(), 30000);
|
|
104
|
-
|
|
105
|
-
const res = await fetch(`${DAEMON_URL}/command`, {
|
|
128
|
+
const res = await requestDaemon('/command', {
|
|
106
129
|
method: 'POST',
|
|
107
|
-
headers: { 'Content-Type': 'application/json'
|
|
130
|
+
headers: { 'Content-Type': 'application/json' },
|
|
108
131
|
body: JSON.stringify(command),
|
|
109
|
-
|
|
132
|
+
timeout: 30000,
|
|
110
133
|
});
|
|
111
|
-
clearTimeout(timer);
|
|
112
134
|
|
|
113
135
|
const result = (await res.json()) as DaemonResult;
|
|
114
136
|
|
|
115
137
|
if (!result.ok) {
|
|
116
138
|
// Check if error is a transient extension issue worth retrying
|
|
117
|
-
|
|
118
|
-
const isTransient = errMsg.includes('Extension disconnected')
|
|
119
|
-
|| errMsg.includes('Extension not connected')
|
|
120
|
-
|| errMsg.includes('attach failed')
|
|
121
|
-
|| errMsg.includes('no longer exists');
|
|
122
|
-
if (isTransient && attempt < maxRetries) {
|
|
139
|
+
if (isTransientBrowserError(new Error(result.error ?? '')) && attempt < maxRetries) {
|
|
123
140
|
// Longer delay for extension recovery (service worker restart)
|
|
124
141
|
await sleep(1500);
|
|
125
142
|
continue;
|
|
@@ -146,8 +163,3 @@ export async function listSessions(): Promise<BrowserSessionInfo[]> {
|
|
|
146
163
|
const result = await sendCommand('sessions');
|
|
147
164
|
return Array.isArray(result) ? result : [];
|
|
148
165
|
}
|
|
149
|
-
|
|
150
|
-
export async function bindCurrentTab(workspace: string, opts: { matchDomain?: string; matchPathPrefix?: string } = {}): Promise<unknown> {
|
|
151
|
-
return sendCommand('bind-current', { workspace, ...opts });
|
|
152
|
-
}
|
|
153
|
-
|
package/src/browser/discover.ts
CHANGED
|
@@ -1,12 +1,8 @@
|
|
|
1
1
|
/**
|
|
2
|
-
* Daemon discovery —
|
|
3
|
-
*
|
|
4
|
-
* Only needs to check if the daemon is running. No more file system
|
|
5
|
-
* scanning for @playwright/mcp locations.
|
|
2
|
+
* Daemon discovery — checks if the daemon is running.
|
|
6
3
|
*/
|
|
7
4
|
|
|
8
|
-
import {
|
|
9
|
-
import { isDaemonRunning } from './daemon-client.js';
|
|
5
|
+
import { fetchDaemonStatus, isDaemonRunning } from './daemon-client.js';
|
|
10
6
|
|
|
11
7
|
export { isDaemonRunning };
|
|
12
8
|
|
|
@@ -18,21 +14,13 @@ export async function checkDaemonStatus(opts?: { timeout?: number }): Promise<{
|
|
|
18
14
|
extensionConnected: boolean;
|
|
19
15
|
extensionVersion?: string;
|
|
20
16
|
}> {
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
const controller = new AbortController();
|
|
24
|
-
const timer = setTimeout(() => controller.abort(), opts?.timeout ?? 2000);
|
|
25
|
-
try {
|
|
26
|
-
const res = await fetch(`http://127.0.0.1:${port}/status`, {
|
|
27
|
-
headers: { 'X-OpenCLI': '1' },
|
|
28
|
-
signal: controller.signal,
|
|
29
|
-
});
|
|
30
|
-
const data = await res.json() as { ok: boolean; extensionConnected: boolean; extensionVersion?: string };
|
|
31
|
-
return { running: true, extensionConnected: data.extensionConnected, extensionVersion: data.extensionVersion };
|
|
32
|
-
} finally {
|
|
33
|
-
clearTimeout(timer);
|
|
34
|
-
}
|
|
35
|
-
} catch {
|
|
17
|
+
const status = await fetchDaemonStatus({ timeout: opts?.timeout ?? 2000 });
|
|
18
|
+
if (!status) {
|
|
36
19
|
return { running: false, extensionConnected: false };
|
|
37
20
|
}
|
|
21
|
+
return {
|
|
22
|
+
running: true,
|
|
23
|
+
extensionConnected: status.extensionConnected,
|
|
24
|
+
extensionVersion: status.extensionVersion,
|
|
25
|
+
};
|
|
38
26
|
}
|
package/src/browser/errors.ts
CHANGED
|
@@ -8,6 +8,28 @@
|
|
|
8
8
|
import { BrowserConnectError, type BrowserConnectKind } from '../errors.js';
|
|
9
9
|
import { DEFAULT_DAEMON_PORT } from '../constants.js';
|
|
10
10
|
|
|
11
|
+
/**
|
|
12
|
+
* Transient browser error patterns — shared across daemon-client, pipeline executor,
|
|
13
|
+
* and page retry logic. These errors indicate temporary conditions (extension restart,
|
|
14
|
+
* service worker cycle, tab navigation) that are worth retrying.
|
|
15
|
+
*/
|
|
16
|
+
const TRANSIENT_ERROR_PATTERNS = [
|
|
17
|
+
'Extension disconnected',
|
|
18
|
+
'Extension not connected',
|
|
19
|
+
'attach failed',
|
|
20
|
+
'no longer exists',
|
|
21
|
+
'CDP connection',
|
|
22
|
+
'Daemon command failed',
|
|
23
|
+
] as const;
|
|
24
|
+
|
|
25
|
+
/**
|
|
26
|
+
* Check if an error message indicates a transient browser error worth retrying.
|
|
27
|
+
*/
|
|
28
|
+
export function isTransientBrowserError(err: unknown): boolean {
|
|
29
|
+
const msg = err instanceof Error ? err.message : String(err);
|
|
30
|
+
return TRANSIENT_ERROR_PATTERNS.some(pattern => msg.includes(pattern));
|
|
31
|
+
}
|
|
32
|
+
|
|
11
33
|
// Re-export so callers don't need to import from two places
|
|
12
34
|
export type ConnectFailureKind = BrowserConnectKind;
|
|
13
35
|
|
package/src/browser/index.ts
CHANGED
|
@@ -6,7 +6,7 @@
|
|
|
6
6
|
*/
|
|
7
7
|
|
|
8
8
|
export { Page } from './page.js';
|
|
9
|
-
export { BrowserBridge } from './
|
|
9
|
+
export { BrowserBridge } from './bridge.js';
|
|
10
10
|
export { CDPBridge } from './cdp.js';
|
|
11
11
|
export { isDaemonRunning } from './daemon-client.js';
|
|
12
12
|
export { generateSnapshotJs, scrollToRefJs, getFormStateJs } from './dom-snapshot.js';
|
package/src/browser/page.ts
CHANGED
|
@@ -10,25 +10,13 @@
|
|
|
10
10
|
* chrome-extension:// tab that can't be debugged.
|
|
11
11
|
*/
|
|
12
12
|
|
|
13
|
-
import {
|
|
14
|
-
import type { BrowserCookie, IPage, ScreenshotOptions, SnapshotOptions, WaitOptions } from '../types.js';
|
|
13
|
+
import type { BrowserCookie, ScreenshotOptions } from '../types.js';
|
|
15
14
|
import { sendCommand } from './daemon-client.js';
|
|
16
15
|
import { wrapForEval } from './utils.js';
|
|
17
16
|
import { saveBase64ToFile } from '../utils.js';
|
|
18
|
-
import { generateSnapshotJs, scrollToRefJs, getFormStateJs } from './dom-snapshot.js';
|
|
19
17
|
import { generateStealthJs } from './stealth.js';
|
|
20
|
-
import {
|
|
21
|
-
|
|
22
|
-
typeTextJs,
|
|
23
|
-
pressKeyJs,
|
|
24
|
-
waitForTextJs,
|
|
25
|
-
waitForCaptureJs,
|
|
26
|
-
waitForSelectorJs,
|
|
27
|
-
scrollJs,
|
|
28
|
-
autoScrollJs,
|
|
29
|
-
networkRequestsJs,
|
|
30
|
-
waitForDomStableJs,
|
|
31
|
-
} from './dom-helpers.js';
|
|
18
|
+
import { waitForDomStableJs } from './dom-helpers.js';
|
|
19
|
+
import { BasePage } from './base-page.js';
|
|
32
20
|
|
|
33
21
|
export function isRetryableSettleError(err: unknown): boolean {
|
|
34
22
|
const message = err instanceof Error ? err.message : String(err);
|
|
@@ -39,13 +27,13 @@ export function isRetryableSettleError(err: unknown): boolean {
|
|
|
39
27
|
/**
|
|
40
28
|
* Page — implements IPage by talking to the daemon via HTTP.
|
|
41
29
|
*/
|
|
42
|
-
export class Page
|
|
43
|
-
constructor(private readonly workspace: string = 'default') {
|
|
30
|
+
export class Page extends BasePage {
|
|
31
|
+
constructor(private readonly workspace: string = 'default') {
|
|
32
|
+
super();
|
|
33
|
+
}
|
|
44
34
|
|
|
45
35
|
/** Active tab ID, set after navigate and used in all subsequent commands */
|
|
46
36
|
private _tabId: number | undefined;
|
|
47
|
-
/** Last navigated URL, tracked in-memory to avoid extra round-trips */
|
|
48
|
-
private _lastUrl: string | null = null;
|
|
49
37
|
|
|
50
38
|
/** Helper: spread workspace into command params */
|
|
51
39
|
private _wsOpt(): { workspace: string } {
|
|
@@ -107,27 +95,8 @@ export class Page implements IPage {
|
|
|
107
95
|
}
|
|
108
96
|
}
|
|
109
97
|
|
|
110
|
-
|
|
111
|
-
|
|
112
|
-
try {
|
|
113
|
-
const current = await this.evaluate('window.location.href');
|
|
114
|
-
if (typeof current === 'string' && current) {
|
|
115
|
-
this._lastUrl = current;
|
|
116
|
-
return current;
|
|
117
|
-
}
|
|
118
|
-
} catch {
|
|
119
|
-
// Best-effort: some commands may run before a debuggable tab is ready.
|
|
120
|
-
}
|
|
121
|
-
return null;
|
|
122
|
-
}
|
|
123
|
-
|
|
124
|
-
/** Close the automation window in the extension */
|
|
125
|
-
async closeWindow(): Promise<void> {
|
|
126
|
-
try {
|
|
127
|
-
await sendCommand('close-window', { ...this._wsOpt() });
|
|
128
|
-
} catch {
|
|
129
|
-
// Window may already be closed or daemon may be down
|
|
130
|
-
}
|
|
98
|
+
getActiveTabId(): number | undefined {
|
|
99
|
+
return this._tabId;
|
|
131
100
|
}
|
|
132
101
|
|
|
133
102
|
async evaluate(js: string): Promise<unknown> {
|
|
@@ -146,124 +115,12 @@ export class Page implements IPage {
|
|
|
146
115
|
return Array.isArray(result) ? result : [];
|
|
147
116
|
}
|
|
148
117
|
|
|
149
|
-
|
|
150
|
-
|
|
151
|
-
const snapshotJs = generateSnapshotJs({
|
|
152
|
-
viewportExpand: opts.viewportExpand ?? 800,
|
|
153
|
-
maxDepth: Math.max(1, Math.min(Number(opts.maxDepth) || 50, 200)),
|
|
154
|
-
interactiveOnly: opts.interactive ?? false,
|
|
155
|
-
maxTextLength: opts.maxTextLength ?? 120,
|
|
156
|
-
includeScrollInfo: true,
|
|
157
|
-
bboxDedup: true,
|
|
158
|
-
});
|
|
159
|
-
|
|
118
|
+
/** Close the automation window in the extension */
|
|
119
|
+
async closeWindow(): Promise<void> {
|
|
160
120
|
try {
|
|
161
|
-
|
|
162
|
-
// The advanced engine already produces a clean, pruned, LLM-friendly output.
|
|
163
|
-
// Do NOT pass through formatSnapshot — its format is incompatible.
|
|
164
|
-
return result;
|
|
121
|
+
await sendCommand('close-window', { ...this._wsOpt() });
|
|
165
122
|
} catch {
|
|
166
|
-
//
|
|
167
|
-
return this._basicSnapshot(opts);
|
|
168
|
-
}
|
|
169
|
-
}
|
|
170
|
-
|
|
171
|
-
/** Fallback basic snapshot — original buildTree approach */
|
|
172
|
-
private async _basicSnapshot(opts: Pick<SnapshotOptions, 'interactive' | 'compact' | 'maxDepth' | 'raw'> = {}): Promise<unknown> {
|
|
173
|
-
const maxDepth = Math.max(1, Math.min(Number(opts.maxDepth) || 50, 200));
|
|
174
|
-
const code = `
|
|
175
|
-
(async () => {
|
|
176
|
-
function buildTree(node, depth) {
|
|
177
|
-
if (depth > ${maxDepth}) return '';
|
|
178
|
-
const role = node.getAttribute?.('role') || node.tagName?.toLowerCase() || 'generic';
|
|
179
|
-
const name = node.getAttribute?.('aria-label') || node.getAttribute?.('alt') || node.textContent?.trim().slice(0, 80) || '';
|
|
180
|
-
const isInteractive = ['a', 'button', 'input', 'select', 'textarea'].includes(node.tagName?.toLowerCase()) || node.getAttribute?.('tabindex') != null;
|
|
181
|
-
|
|
182
|
-
${opts.interactive ? 'if (!isInteractive && !node.children?.length) return "";' : ''}
|
|
183
|
-
|
|
184
|
-
let indent = ' '.repeat(depth);
|
|
185
|
-
let line = indent + role;
|
|
186
|
-
if (name) line += ' "' + name.replace(/"/g, '\\\\\\"') + '"';
|
|
187
|
-
if (node.tagName?.toLowerCase() === 'a' && node.href) line += ' [' + node.href + ']';
|
|
188
|
-
if (node.tagName?.toLowerCase() === 'input') line += ' [' + (node.type || 'text') + ']';
|
|
189
|
-
|
|
190
|
-
let result = line + '\\n';
|
|
191
|
-
if (node.children) {
|
|
192
|
-
for (const child of node.children) {
|
|
193
|
-
result += buildTree(child, depth + 1);
|
|
194
|
-
}
|
|
195
|
-
}
|
|
196
|
-
return result;
|
|
197
|
-
}
|
|
198
|
-
return buildTree(document.body, 0);
|
|
199
|
-
})()
|
|
200
|
-
`;
|
|
201
|
-
const raw = await sendCommand('exec', { code, ...this._cmdOpts() });
|
|
202
|
-
if (opts.raw) return raw;
|
|
203
|
-
if (typeof raw === 'string') return formatSnapshot(raw, opts);
|
|
204
|
-
return raw;
|
|
205
|
-
}
|
|
206
|
-
|
|
207
|
-
async click(ref: string): Promise<void> {
|
|
208
|
-
const code = clickJs(ref);
|
|
209
|
-
await sendCommand('exec', { code, ...this._cmdOpts() });
|
|
210
|
-
}
|
|
211
|
-
|
|
212
|
-
async typeText(ref: string, text: string): Promise<void> {
|
|
213
|
-
const code = typeTextJs(ref, text);
|
|
214
|
-
await sendCommand('exec', { code, ...this._cmdOpts() });
|
|
215
|
-
}
|
|
216
|
-
|
|
217
|
-
async pressKey(key: string): Promise<void> {
|
|
218
|
-
const code = pressKeyJs(key);
|
|
219
|
-
await sendCommand('exec', { code, ...this._cmdOpts() });
|
|
220
|
-
}
|
|
221
|
-
|
|
222
|
-
async scrollTo(ref: string): Promise<unknown> {
|
|
223
|
-
const code = scrollToRefJs(ref);
|
|
224
|
-
return sendCommand('exec', { code, ...this._cmdOpts() });
|
|
225
|
-
}
|
|
226
|
-
|
|
227
|
-
async getFormState(): Promise<Record<string, unknown>> {
|
|
228
|
-
const code = getFormStateJs();
|
|
229
|
-
return (await sendCommand('exec', { code, ...this._cmdOpts() })) as Record<string, unknown>;
|
|
230
|
-
}
|
|
231
|
-
|
|
232
|
-
async wait(options: number | WaitOptions): Promise<void> {
|
|
233
|
-
if (typeof options === 'number') {
|
|
234
|
-
if (options >= 1) {
|
|
235
|
-
// For waits >= 1s, use DOM-stable check: return early when the page
|
|
236
|
-
// stops mutating, with the original wait time as the hard cap.
|
|
237
|
-
// This turns e.g. `page.wait(5)` from a fixed 5s sleep into
|
|
238
|
-
// "wait until DOM is stable, max 5s" — often completing in <1s.
|
|
239
|
-
try {
|
|
240
|
-
const maxMs = options * 1000;
|
|
241
|
-
await sendCommand('exec', {
|
|
242
|
-
code: waitForDomStableJs(maxMs, Math.min(500, maxMs)),
|
|
243
|
-
...this._cmdOpts(),
|
|
244
|
-
});
|
|
245
|
-
return;
|
|
246
|
-
} catch {
|
|
247
|
-
// Fallback: fixed sleep (e.g. if page has no DOM yet)
|
|
248
|
-
}
|
|
249
|
-
}
|
|
250
|
-
await new Promise(resolve => setTimeout(resolve, options * 1000));
|
|
251
|
-
return;
|
|
252
|
-
}
|
|
253
|
-
if (typeof options.time === 'number') {
|
|
254
|
-
await new Promise(resolve => setTimeout(resolve, options.time! * 1000));
|
|
255
|
-
return;
|
|
256
|
-
}
|
|
257
|
-
if (options.selector) {
|
|
258
|
-
const timeout = (options.timeout ?? 10) * 1000;
|
|
259
|
-
const code = waitForSelectorJs(options.selector, timeout);
|
|
260
|
-
await sendCommand('exec', { code, ...this._cmdOpts() });
|
|
261
|
-
return;
|
|
262
|
-
}
|
|
263
|
-
if (options.text) {
|
|
264
|
-
const timeout = (options.timeout ?? 30) * 1000;
|
|
265
|
-
const code = waitForTextJs(options.text, timeout);
|
|
266
|
-
await sendCommand('exec', { code, ...this._cmdOpts() });
|
|
123
|
+
// Window may already be closed or daemon may be down
|
|
267
124
|
}
|
|
268
125
|
}
|
|
269
126
|
|
|
@@ -289,27 +146,8 @@ export class Page implements IPage {
|
|
|
289
146
|
if (result?.selected) this._tabId = result.selected;
|
|
290
147
|
}
|
|
291
148
|
|
|
292
|
-
async networkRequests(includeStatic: boolean = false): Promise<unknown[]> {
|
|
293
|
-
const code = networkRequestsJs(includeStatic);
|
|
294
|
-
const result = await sendCommand('exec', { code, ...this._cmdOpts() });
|
|
295
|
-
return Array.isArray(result) ? result : [];
|
|
296
|
-
}
|
|
297
|
-
|
|
298
|
-
/**
|
|
299
|
-
* Console messages are not available in lightweight daemon mode.
|
|
300
|
-
* Would require CDP Runtime.consoleAPICalled event listener.
|
|
301
|
-
* @returns Always returns empty array.
|
|
302
|
-
*/
|
|
303
|
-
async consoleMessages(_level: string = 'info'): Promise<unknown[]> {
|
|
304
|
-
return [];
|
|
305
|
-
}
|
|
306
|
-
|
|
307
149
|
/**
|
|
308
150
|
* Capture a screenshot via CDP Page.captureScreenshot.
|
|
309
|
-
* @param options.format - 'png' (default) or 'jpeg'
|
|
310
|
-
* @param options.quality - JPEG quality 0-100
|
|
311
|
-
* @param options.fullPage - capture full scrollable page
|
|
312
|
-
* @param options.path - save to file path (returns base64 if omitted)
|
|
313
151
|
*/
|
|
314
152
|
async screenshot(options: ScreenshotOptions = {}): Promise<string> {
|
|
315
153
|
const base64 = await sendCommand('screenshot', {
|
|
@@ -326,35 +164,6 @@ export class Page implements IPage {
|
|
|
326
164
|
return base64;
|
|
327
165
|
}
|
|
328
166
|
|
|
329
|
-
async scroll(direction: string = 'down', amount: number = 500): Promise<void> {
|
|
330
|
-
const code = scrollJs(direction, amount);
|
|
331
|
-
await sendCommand('exec', { code, ...this._cmdOpts() });
|
|
332
|
-
}
|
|
333
|
-
|
|
334
|
-
async autoScroll(options: { times?: number; delayMs?: number } = {}): Promise<void> {
|
|
335
|
-
const times = options.times ?? 3;
|
|
336
|
-
const delayMs = options.delayMs ?? 2000;
|
|
337
|
-
const code = autoScrollJs(times, delayMs);
|
|
338
|
-
await sendCommand('exec', { code, ...this._cmdOpts() });
|
|
339
|
-
}
|
|
340
|
-
|
|
341
|
-
async installInterceptor(pattern: string): Promise<void> {
|
|
342
|
-
const { generateInterceptorJs } = await import('../interceptor.js');
|
|
343
|
-
// Must use evaluate() so wrapForEval() converts the arrow function into an IIFE;
|
|
344
|
-
// sendCommand('exec') sends the code as-is, and CDP never executes a bare arrow.
|
|
345
|
-
await this.evaluate(generateInterceptorJs(JSON.stringify(pattern), {
|
|
346
|
-
arrayName: '__opencli_xhr',
|
|
347
|
-
patchGuard: '__opencli_interceptor_patched',
|
|
348
|
-
}));
|
|
349
|
-
}
|
|
350
|
-
|
|
351
|
-
async getInterceptedRequests(): Promise<unknown[]> {
|
|
352
|
-
const { generateReadInterceptedJs } = await import('../interceptor.js');
|
|
353
|
-
// Same as installInterceptor: must go through evaluate() for IIFE wrapping
|
|
354
|
-
const result = await this.evaluate(generateReadInterceptedJs('__opencli_xhr'));
|
|
355
|
-
return Array.isArray(result) ? result : [];
|
|
356
|
-
}
|
|
357
|
-
|
|
358
167
|
/**
|
|
359
168
|
* Set local file paths on a file input element via CDP DOM.setFileInputFiles.
|
|
360
169
|
* Chrome reads the files directly from the local filesystem, avoiding the
|
|
@@ -371,13 +180,52 @@ export class Page implements IPage {
|
|
|
371
180
|
}
|
|
372
181
|
}
|
|
373
182
|
|
|
374
|
-
async
|
|
375
|
-
|
|
376
|
-
|
|
377
|
-
|
|
183
|
+
async cdp(method: string, params: Record<string, unknown> = {}): Promise<unknown> {
|
|
184
|
+
return sendCommand('cdp', {
|
|
185
|
+
cdpMethod: method,
|
|
186
|
+
cdpParams: params,
|
|
378
187
|
...this._cmdOpts(),
|
|
379
188
|
});
|
|
380
189
|
}
|
|
190
|
+
|
|
191
|
+
async nativeClick(x: number, y: number): Promise<void> {
|
|
192
|
+
await this.cdp('Input.dispatchMouseEvent', {
|
|
193
|
+
type: 'mousePressed',
|
|
194
|
+
x, y,
|
|
195
|
+
button: 'left',
|
|
196
|
+
clickCount: 1,
|
|
197
|
+
});
|
|
198
|
+
await this.cdp('Input.dispatchMouseEvent', {
|
|
199
|
+
type: 'mouseReleased',
|
|
200
|
+
x, y,
|
|
201
|
+
button: 'left',
|
|
202
|
+
clickCount: 1,
|
|
203
|
+
});
|
|
204
|
+
}
|
|
205
|
+
|
|
206
|
+
async nativeType(text: string): Promise<void> {
|
|
207
|
+
// Use Input.insertText for reliable Unicode/CJK text insertion
|
|
208
|
+
await this.cdp('Input.insertText', { text });
|
|
209
|
+
}
|
|
210
|
+
|
|
211
|
+
async nativeKeyPress(key: string, modifiers: string[] = []): Promise<void> {
|
|
212
|
+
let modifierFlags = 0;
|
|
213
|
+
for (const mod of modifiers) {
|
|
214
|
+
if (mod === 'Alt') modifierFlags |= 1;
|
|
215
|
+
if (mod === 'Ctrl') modifierFlags |= 2;
|
|
216
|
+
if (mod === 'Meta') modifierFlags |= 4;
|
|
217
|
+
if (mod === 'Shift') modifierFlags |= 8;
|
|
218
|
+
}
|
|
219
|
+
await this.cdp('Input.dispatchKeyEvent', {
|
|
220
|
+
type: 'keyDown',
|
|
221
|
+
key,
|
|
222
|
+
modifiers: modifierFlags,
|
|
223
|
+
});
|
|
224
|
+
await this.cdp('Input.dispatchKeyEvent', {
|
|
225
|
+
type: 'keyUp',
|
|
226
|
+
key,
|
|
227
|
+
modifiers: modifierFlags,
|
|
228
|
+
});
|
|
229
|
+
}
|
|
381
230
|
}
|
|
382
231
|
|
|
383
|
-
// (End of file)
|
package/src/browser/tabs.ts
CHANGED
|
@@ -21,12 +21,12 @@ export function extractTabEntries(raw: unknown): Array<{ index: number; identity
|
|
|
21
21
|
.map(line => line.trim())
|
|
22
22
|
.filter(Boolean)
|
|
23
23
|
.map(line => {
|
|
24
|
-
// Match
|
|
25
|
-
const
|
|
26
|
-
if (
|
|
24
|
+
// Match tab list format: "- 0: (current) [title](url)" or "- 1: [title](url)"
|
|
25
|
+
const tabMatch = line.match(/^-\s+(\d+):\s*(.*)$/);
|
|
26
|
+
if (tabMatch) {
|
|
27
27
|
return {
|
|
28
|
-
index: parseInt(
|
|
29
|
-
identity:
|
|
28
|
+
index: parseInt(tabMatch[1], 10),
|
|
29
|
+
identity: tabMatch[2].trim() || `tab-${tabMatch[1]}`,
|
|
30
30
|
};
|
|
31
31
|
}
|
|
32
32
|
// Legacy format: "Tab 0 ..."
|
package/src/browser.test.ts
CHANGED
|
@@ -104,43 +104,43 @@ describe('browser helpers', () => {
|
|
|
104
104
|
|
|
105
105
|
describe('BrowserBridge state', () => {
|
|
106
106
|
it('transitions to closed after close()', async () => {
|
|
107
|
-
const
|
|
107
|
+
const bridge = new BrowserBridge();
|
|
108
108
|
|
|
109
|
-
expect(
|
|
109
|
+
expect(bridge.state).toBe('idle');
|
|
110
110
|
|
|
111
|
-
await
|
|
111
|
+
await bridge.close();
|
|
112
112
|
|
|
113
|
-
expect(
|
|
113
|
+
expect(bridge.state).toBe('closed');
|
|
114
114
|
});
|
|
115
115
|
|
|
116
116
|
it('rejects connect() after the session has been closed', async () => {
|
|
117
|
-
const
|
|
118
|
-
await
|
|
117
|
+
const bridge = new BrowserBridge();
|
|
118
|
+
await bridge.close();
|
|
119
119
|
|
|
120
|
-
await expect(
|
|
120
|
+
await expect(bridge.connect()).rejects.toThrow('Session is closed');
|
|
121
121
|
});
|
|
122
122
|
|
|
123
123
|
it('rejects connect() while already connecting', async () => {
|
|
124
|
-
const
|
|
125
|
-
(
|
|
124
|
+
const bridge = new BrowserBridge();
|
|
125
|
+
(bridge as any)._state = 'connecting';
|
|
126
126
|
|
|
127
|
-
await expect(
|
|
127
|
+
await expect(bridge.connect()).rejects.toThrow('Already connecting');
|
|
128
128
|
});
|
|
129
129
|
|
|
130
130
|
it('rejects connect() while closing', async () => {
|
|
131
|
-
const
|
|
132
|
-
(
|
|
131
|
+
const bridge = new BrowserBridge();
|
|
132
|
+
(bridge as any)._state = 'closing';
|
|
133
133
|
|
|
134
|
-
await expect(
|
|
134
|
+
await expect(bridge.connect()).rejects.toThrow('Session is closing');
|
|
135
135
|
});
|
|
136
136
|
|
|
137
137
|
it('fails fast when daemon is running but extension is disconnected', async () => {
|
|
138
138
|
vi.spyOn(daemonClient, 'isExtensionConnected').mockResolvedValue(false);
|
|
139
139
|
vi.spyOn(daemonClient, 'isDaemonRunning').mockResolvedValue(true);
|
|
140
140
|
|
|
141
|
-
const
|
|
141
|
+
const bridge = new BrowserBridge();
|
|
142
142
|
|
|
143
|
-
await expect(
|
|
143
|
+
await expect(bridge.connect({ timeout: 0.1 })).rejects.toThrow('Browser Extension is not connected');
|
|
144
144
|
});
|
|
145
145
|
});
|
|
146
146
|
|