@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
package/dist/browser/cdp.js
CHANGED
|
@@ -11,11 +11,11 @@ import { WebSocket } from 'ws';
|
|
|
11
11
|
import { request as httpRequest } from 'node:http';
|
|
12
12
|
import { request as httpsRequest } from 'node:https';
|
|
13
13
|
import { wrapForEval } from './utils.js';
|
|
14
|
-
import { generateSnapshotJs, scrollToRefJs, getFormStateJs } from './dom-snapshot.js';
|
|
15
14
|
import { generateStealthJs } from './stealth.js';
|
|
16
|
-
import {
|
|
15
|
+
import { waitForDomStableJs } from './dom-helpers.js';
|
|
17
16
|
import { isRecord, saveBase64ToFile } from '../utils.js';
|
|
18
17
|
import { getAllElectronApps } from '../electron-apps.js';
|
|
18
|
+
import { BasePage } from './base-page.js';
|
|
19
19
|
const CDP_SEND_TIMEOUT = 30_000;
|
|
20
20
|
export class CDPBridge {
|
|
21
21
|
_ws = null;
|
|
@@ -133,11 +133,11 @@ export class CDPBridge {
|
|
|
133
133
|
});
|
|
134
134
|
}
|
|
135
135
|
}
|
|
136
|
-
class CDPPage {
|
|
136
|
+
class CDPPage extends BasePage {
|
|
137
137
|
bridge;
|
|
138
138
|
_pageEnabled = false;
|
|
139
|
-
_lastUrl = null;
|
|
140
139
|
constructor(bridge) {
|
|
140
|
+
super();
|
|
141
141
|
this.bridge = bridge;
|
|
142
142
|
}
|
|
143
143
|
async goto(url, options) {
|
|
@@ -174,70 +174,6 @@ class CDPPage {
|
|
|
174
174
|
? cookies.filter((cookie) => isCookie(cookie) && matchesCookieDomain(cookie.domain, domain))
|
|
175
175
|
: cookies;
|
|
176
176
|
}
|
|
177
|
-
async snapshot(opts = {}) {
|
|
178
|
-
const snapshotJs = generateSnapshotJs({
|
|
179
|
-
viewportExpand: opts.viewportExpand ?? 800,
|
|
180
|
-
maxDepth: Math.max(1, Math.min(Number(opts.maxDepth) || 50, 200)),
|
|
181
|
-
interactiveOnly: opts.interactive ?? false,
|
|
182
|
-
maxTextLength: opts.maxTextLength ?? 120,
|
|
183
|
-
includeScrollInfo: true,
|
|
184
|
-
bboxDedup: true,
|
|
185
|
-
});
|
|
186
|
-
return this.evaluate(snapshotJs);
|
|
187
|
-
}
|
|
188
|
-
async click(ref) {
|
|
189
|
-
await this.evaluate(clickJs(ref));
|
|
190
|
-
}
|
|
191
|
-
async typeText(ref, text) {
|
|
192
|
-
await this.evaluate(typeTextJs(ref, text));
|
|
193
|
-
}
|
|
194
|
-
async pressKey(key) {
|
|
195
|
-
await this.evaluate(pressKeyJs(key));
|
|
196
|
-
}
|
|
197
|
-
async scrollTo(ref) {
|
|
198
|
-
return this.evaluate(scrollToRefJs(ref));
|
|
199
|
-
}
|
|
200
|
-
async getFormState() {
|
|
201
|
-
return (await this.evaluate(getFormStateJs()));
|
|
202
|
-
}
|
|
203
|
-
async wait(options) {
|
|
204
|
-
if (typeof options === 'number') {
|
|
205
|
-
if (options >= 1) {
|
|
206
|
-
try {
|
|
207
|
-
const maxMs = options * 1000;
|
|
208
|
-
await this.evaluate(waitForDomStableJs(maxMs, Math.min(500, maxMs)));
|
|
209
|
-
return;
|
|
210
|
-
}
|
|
211
|
-
catch {
|
|
212
|
-
// Fallback: fixed sleep
|
|
213
|
-
}
|
|
214
|
-
}
|
|
215
|
-
await new Promise((resolve) => setTimeout(resolve, options * 1000));
|
|
216
|
-
return;
|
|
217
|
-
}
|
|
218
|
-
if (typeof options.time === 'number') {
|
|
219
|
-
const waitTime = options.time;
|
|
220
|
-
await new Promise((resolve) => setTimeout(resolve, waitTime * 1000));
|
|
221
|
-
return;
|
|
222
|
-
}
|
|
223
|
-
if (options.selector) {
|
|
224
|
-
const timeout = (options.timeout ?? 10) * 1000;
|
|
225
|
-
await this.evaluate(waitForSelectorJs(options.selector, timeout));
|
|
226
|
-
return;
|
|
227
|
-
}
|
|
228
|
-
if (options.text) {
|
|
229
|
-
const timeout = (options.timeout ?? 30) * 1000;
|
|
230
|
-
await this.evaluate(waitForTextJs(options.text, timeout));
|
|
231
|
-
}
|
|
232
|
-
}
|
|
233
|
-
async scroll(direction = 'down', amount = 500) {
|
|
234
|
-
await this.evaluate(scrollJs(direction, amount));
|
|
235
|
-
}
|
|
236
|
-
async autoScroll(options) {
|
|
237
|
-
const times = options?.times ?? 3;
|
|
238
|
-
const delayMs = options?.delayMs ?? 2000;
|
|
239
|
-
await this.evaluate(autoScrollJs(times, delayMs));
|
|
240
|
-
}
|
|
241
177
|
async screenshot(options = {}) {
|
|
242
178
|
const result = await this.bridge.send('Page.captureScreenshot', {
|
|
243
179
|
format: options.format ?? 'png',
|
|
@@ -250,10 +186,6 @@ class CDPPage {
|
|
|
250
186
|
}
|
|
251
187
|
return base64;
|
|
252
188
|
}
|
|
253
|
-
async networkRequests(includeStatic = false) {
|
|
254
|
-
const result = await this.evaluate(networkRequestsJs(includeStatic));
|
|
255
|
-
return Array.isArray(result) ? result : [];
|
|
256
|
-
}
|
|
257
189
|
async tabs() {
|
|
258
190
|
return [];
|
|
259
191
|
}
|
|
@@ -266,40 +198,6 @@ class CDPPage {
|
|
|
266
198
|
async selectTab(_index) {
|
|
267
199
|
// Not supported in direct CDP mode
|
|
268
200
|
}
|
|
269
|
-
async consoleMessages(_level) {
|
|
270
|
-
return [];
|
|
271
|
-
}
|
|
272
|
-
async getCurrentUrl() {
|
|
273
|
-
if (this._lastUrl)
|
|
274
|
-
return this._lastUrl;
|
|
275
|
-
try {
|
|
276
|
-
const current = await this.evaluate('window.location.href');
|
|
277
|
-
if (typeof current === 'string' && current) {
|
|
278
|
-
this._lastUrl = current;
|
|
279
|
-
return current;
|
|
280
|
-
}
|
|
281
|
-
}
|
|
282
|
-
catch {
|
|
283
|
-
// Best-effort: direct CDP sessions may not have a ready page yet.
|
|
284
|
-
}
|
|
285
|
-
return null;
|
|
286
|
-
}
|
|
287
|
-
async installInterceptor(pattern) {
|
|
288
|
-
const { generateInterceptorJs } = await import('../interceptor.js');
|
|
289
|
-
await this.evaluate(generateInterceptorJs(JSON.stringify(pattern), {
|
|
290
|
-
arrayName: '__opencli_xhr',
|
|
291
|
-
patchGuard: '__opencli_interceptor_patched',
|
|
292
|
-
}));
|
|
293
|
-
}
|
|
294
|
-
async getInterceptedRequests() {
|
|
295
|
-
const { generateReadInterceptedJs } = await import('../interceptor.js');
|
|
296
|
-
const result = await this.evaluate(generateReadInterceptedJs('__opencli_xhr'));
|
|
297
|
-
return Array.isArray(result) ? result : [];
|
|
298
|
-
}
|
|
299
|
-
async waitForCapture(timeout = 10) {
|
|
300
|
-
const maxMs = timeout * 1000;
|
|
301
|
-
await this.evaluate(waitForCaptureJs(maxMs));
|
|
302
|
-
}
|
|
303
201
|
}
|
|
304
202
|
function isCookie(value) {
|
|
305
203
|
return isRecord(value)
|
|
@@ -6,7 +6,7 @@
|
|
|
6
6
|
import type { BrowserSessionInfo } from '../types.js';
|
|
7
7
|
export interface DaemonCommand {
|
|
8
8
|
id: string;
|
|
9
|
-
action: 'exec' | 'navigate' | 'tabs' | 'cookies' | 'screenshot' | 'close-window' | 'sessions' | 'set-file-input' | '
|
|
9
|
+
action: 'exec' | 'navigate' | 'tabs' | 'cookies' | 'screenshot' | 'close-window' | 'sessions' | 'set-file-input' | 'cdp';
|
|
10
10
|
tabId?: number;
|
|
11
11
|
code?: string;
|
|
12
12
|
workspace?: string;
|
|
@@ -14,8 +14,6 @@ export interface DaemonCommand {
|
|
|
14
14
|
op?: string;
|
|
15
15
|
index?: number;
|
|
16
16
|
domain?: string;
|
|
17
|
-
matchDomain?: string;
|
|
18
|
-
matchPathPrefix?: string;
|
|
19
17
|
format?: 'png' | 'jpeg';
|
|
20
18
|
quality?: number;
|
|
21
19
|
fullPage?: boolean;
|
|
@@ -23,6 +21,8 @@ export interface DaemonCommand {
|
|
|
23
21
|
files?: string[];
|
|
24
22
|
/** CSS selector for file input element (set-file-input action) */
|
|
25
23
|
selector?: string;
|
|
24
|
+
cdpMethod?: string;
|
|
25
|
+
cdpParams?: Record<string, unknown>;
|
|
26
26
|
}
|
|
27
27
|
export interface DaemonResult {
|
|
28
28
|
id: string;
|
|
@@ -30,6 +30,23 @@ export interface DaemonResult {
|
|
|
30
30
|
data?: unknown;
|
|
31
31
|
error?: string;
|
|
32
32
|
}
|
|
33
|
+
export interface DaemonStatus {
|
|
34
|
+
ok: boolean;
|
|
35
|
+
pid: number;
|
|
36
|
+
uptime: number;
|
|
37
|
+
extensionConnected: boolean;
|
|
38
|
+
extensionVersion?: string;
|
|
39
|
+
pending: number;
|
|
40
|
+
lastCliRequestTime: number;
|
|
41
|
+
memoryMB: number;
|
|
42
|
+
port: number;
|
|
43
|
+
}
|
|
44
|
+
export declare function fetchDaemonStatus(opts?: {
|
|
45
|
+
timeout?: number;
|
|
46
|
+
}): Promise<DaemonStatus | null>;
|
|
47
|
+
export declare function requestDaemonShutdown(opts?: {
|
|
48
|
+
timeout?: number;
|
|
49
|
+
}): Promise<boolean>;
|
|
33
50
|
/**
|
|
34
51
|
* Check if daemon is running.
|
|
35
52
|
*/
|
|
@@ -45,7 +62,3 @@ export declare function isExtensionConnected(): Promise<boolean>;
|
|
|
45
62
|
*/
|
|
46
63
|
export declare function sendCommand(action: DaemonCommand['action'], params?: Omit<DaemonCommand, 'id' | 'action'>): Promise<unknown>;
|
|
47
64
|
export declare function listSessions(): Promise<BrowserSessionInfo[]>;
|
|
48
|
-
export declare function bindCurrentTab(workspace: string, opts?: {
|
|
49
|
-
matchDomain?: string;
|
|
50
|
-
matchPathPrefix?: string;
|
|
51
|
-
}): Promise<unknown>;
|
|
@@ -5,50 +5,61 @@
|
|
|
5
5
|
*/
|
|
6
6
|
import { DEFAULT_DAEMON_PORT } from '../constants.js';
|
|
7
7
|
import { sleep } from '../utils.js';
|
|
8
|
+
import { isTransientBrowserError } from './errors.js';
|
|
8
9
|
const DAEMON_PORT = parseInt(process.env.OPENCLI_DAEMON_PORT ?? String(DEFAULT_DAEMON_PORT), 10);
|
|
9
10
|
const DAEMON_URL = `http://127.0.0.1:${DAEMON_PORT}`;
|
|
11
|
+
const OPENCLI_HEADERS = { 'X-OpenCLI': '1' };
|
|
10
12
|
let _idCounter = 0;
|
|
11
13
|
function generateId() {
|
|
12
14
|
return `cmd_${Date.now()}_${++_idCounter}`;
|
|
13
15
|
}
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
|
|
16
|
+
async function requestDaemon(pathname, init) {
|
|
17
|
+
const { timeout = 2000, headers, ...rest } = init ?? {};
|
|
18
|
+
const controller = new AbortController();
|
|
19
|
+
const timer = setTimeout(() => controller.abort(), timeout);
|
|
18
20
|
try {
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
headers: { 'X-OpenCLI': '1' },
|
|
21
|
+
return await fetch(`${DAEMON_URL}${pathname}`, {
|
|
22
|
+
...rest,
|
|
23
|
+
headers: { ...OPENCLI_HEADERS, ...headers },
|
|
23
24
|
signal: controller.signal,
|
|
24
25
|
});
|
|
26
|
+
}
|
|
27
|
+
finally {
|
|
25
28
|
clearTimeout(timer);
|
|
29
|
+
}
|
|
30
|
+
}
|
|
31
|
+
export async function fetchDaemonStatus(opts) {
|
|
32
|
+
try {
|
|
33
|
+
const res = await requestDaemon('/status', { timeout: opts?.timeout ?? 2000 });
|
|
34
|
+
if (!res.ok)
|
|
35
|
+
return null;
|
|
36
|
+
return await res.json();
|
|
37
|
+
}
|
|
38
|
+
catch {
|
|
39
|
+
return null;
|
|
40
|
+
}
|
|
41
|
+
}
|
|
42
|
+
export async function requestDaemonShutdown(opts) {
|
|
43
|
+
try {
|
|
44
|
+
const res = await requestDaemon('/shutdown', { method: 'POST', timeout: opts?.timeout ?? 5000 });
|
|
26
45
|
return res.ok;
|
|
27
46
|
}
|
|
28
47
|
catch {
|
|
29
48
|
return false;
|
|
30
49
|
}
|
|
31
50
|
}
|
|
51
|
+
/**
|
|
52
|
+
* Check if daemon is running.
|
|
53
|
+
*/
|
|
54
|
+
export async function isDaemonRunning() {
|
|
55
|
+
return (await fetchDaemonStatus()) !== null;
|
|
56
|
+
}
|
|
32
57
|
/**
|
|
33
58
|
* Check if daemon is running AND the extension is connected.
|
|
34
59
|
*/
|
|
35
60
|
export async function isExtensionConnected() {
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
const timer = setTimeout(() => controller.abort(), 2000);
|
|
39
|
-
const res = await fetch(`${DAEMON_URL}/status`, {
|
|
40
|
-
headers: { 'X-OpenCLI': '1' },
|
|
41
|
-
signal: controller.signal,
|
|
42
|
-
});
|
|
43
|
-
clearTimeout(timer);
|
|
44
|
-
if (!res.ok)
|
|
45
|
-
return false;
|
|
46
|
-
const data = await res.json();
|
|
47
|
-
return !!data.extensionConnected;
|
|
48
|
-
}
|
|
49
|
-
catch {
|
|
50
|
-
return false;
|
|
51
|
-
}
|
|
61
|
+
const status = await fetchDaemonStatus();
|
|
62
|
+
return !!status?.extensionConnected;
|
|
52
63
|
}
|
|
53
64
|
/**
|
|
54
65
|
* Send a command to the daemon and wait for a result.
|
|
@@ -62,24 +73,16 @@ export async function sendCommand(action, params = {}) {
|
|
|
62
73
|
const id = generateId();
|
|
63
74
|
const command = { id, action, ...params };
|
|
64
75
|
try {
|
|
65
|
-
const
|
|
66
|
-
const timer = setTimeout(() => controller.abort(), 30000);
|
|
67
|
-
const res = await fetch(`${DAEMON_URL}/command`, {
|
|
76
|
+
const res = await requestDaemon('/command', {
|
|
68
77
|
method: 'POST',
|
|
69
|
-
headers: { 'Content-Type': 'application/json'
|
|
78
|
+
headers: { 'Content-Type': 'application/json' },
|
|
70
79
|
body: JSON.stringify(command),
|
|
71
|
-
|
|
80
|
+
timeout: 30000,
|
|
72
81
|
});
|
|
73
|
-
clearTimeout(timer);
|
|
74
82
|
const result = (await res.json());
|
|
75
83
|
if (!result.ok) {
|
|
76
84
|
// Check if error is a transient extension issue worth retrying
|
|
77
|
-
|
|
78
|
-
const isTransient = errMsg.includes('Extension disconnected')
|
|
79
|
-
|| errMsg.includes('Extension not connected')
|
|
80
|
-
|| errMsg.includes('attach failed')
|
|
81
|
-
|| errMsg.includes('no longer exists');
|
|
82
|
-
if (isTransient && attempt < maxRetries) {
|
|
85
|
+
if (isTransientBrowserError(new Error(result.error ?? '')) && attempt < maxRetries) {
|
|
83
86
|
// Longer delay for extension recovery (service worker restart)
|
|
84
87
|
await sleep(1500);
|
|
85
88
|
continue;
|
|
@@ -105,6 +108,3 @@ export async function listSessions() {
|
|
|
105
108
|
const result = await sendCommand('sessions');
|
|
106
109
|
return Array.isArray(result) ? result : [];
|
|
107
110
|
}
|
|
108
|
-
export async function bindCurrentTab(workspace, opts = {}) {
|
|
109
|
-
return sendCommand('bind-current', { workspace, ...opts });
|
|
110
|
-
}
|
|
@@ -0,0 +1,77 @@
|
|
|
1
|
+
import { afterEach, beforeEach, describe, expect, it, vi } from 'vitest';
|
|
2
|
+
import { fetchDaemonStatus, isDaemonRunning, isExtensionConnected, requestDaemonShutdown, } from './daemon-client.js';
|
|
3
|
+
describe('daemon-client', () => {
|
|
4
|
+
beforeEach(() => {
|
|
5
|
+
vi.stubGlobal('fetch', vi.fn());
|
|
6
|
+
});
|
|
7
|
+
afterEach(() => {
|
|
8
|
+
vi.restoreAllMocks();
|
|
9
|
+
});
|
|
10
|
+
it('fetchDaemonStatus sends the shared status request and returns parsed data', async () => {
|
|
11
|
+
const status = {
|
|
12
|
+
ok: true,
|
|
13
|
+
pid: 123,
|
|
14
|
+
uptime: 10,
|
|
15
|
+
extensionConnected: true,
|
|
16
|
+
extensionVersion: '1.2.3',
|
|
17
|
+
pending: 0,
|
|
18
|
+
lastCliRequestTime: Date.now(),
|
|
19
|
+
memoryMB: 32,
|
|
20
|
+
port: 19825,
|
|
21
|
+
};
|
|
22
|
+
const fetchMock = vi.mocked(fetch);
|
|
23
|
+
fetchMock.mockResolvedValue({
|
|
24
|
+
ok: true,
|
|
25
|
+
json: () => Promise.resolve(status),
|
|
26
|
+
});
|
|
27
|
+
await expect(fetchDaemonStatus()).resolves.toEqual(status);
|
|
28
|
+
expect(fetchMock).toHaveBeenCalledWith(expect.stringMatching(/\/status$/), expect.objectContaining({
|
|
29
|
+
headers: expect.objectContaining({ 'X-OpenCLI': '1' }),
|
|
30
|
+
}));
|
|
31
|
+
});
|
|
32
|
+
it('fetchDaemonStatus returns null on network failure', async () => {
|
|
33
|
+
vi.mocked(fetch).mockRejectedValue(new Error('ECONNREFUSED'));
|
|
34
|
+
await expect(fetchDaemonStatus()).resolves.toBeNull();
|
|
35
|
+
});
|
|
36
|
+
it('requestDaemonShutdown POSTs to the shared shutdown endpoint', async () => {
|
|
37
|
+
const fetchMock = vi.mocked(fetch);
|
|
38
|
+
fetchMock.mockResolvedValue({ ok: true });
|
|
39
|
+
await expect(requestDaemonShutdown()).resolves.toBe(true);
|
|
40
|
+
expect(fetchMock).toHaveBeenCalledWith(expect.stringMatching(/\/shutdown$/), expect.objectContaining({
|
|
41
|
+
method: 'POST',
|
|
42
|
+
headers: expect.objectContaining({ 'X-OpenCLI': '1' }),
|
|
43
|
+
}));
|
|
44
|
+
});
|
|
45
|
+
it('isDaemonRunning reflects shared status availability', async () => {
|
|
46
|
+
vi.mocked(fetch).mockResolvedValue({
|
|
47
|
+
ok: true,
|
|
48
|
+
json: () => Promise.resolve({
|
|
49
|
+
ok: true,
|
|
50
|
+
pid: 123,
|
|
51
|
+
uptime: 10,
|
|
52
|
+
extensionConnected: false,
|
|
53
|
+
pending: 0,
|
|
54
|
+
lastCliRequestTime: Date.now(),
|
|
55
|
+
memoryMB: 16,
|
|
56
|
+
port: 19825,
|
|
57
|
+
}),
|
|
58
|
+
});
|
|
59
|
+
await expect(isDaemonRunning()).resolves.toBe(true);
|
|
60
|
+
});
|
|
61
|
+
it('isExtensionConnected reflects shared status payload', async () => {
|
|
62
|
+
vi.mocked(fetch).mockResolvedValue({
|
|
63
|
+
ok: true,
|
|
64
|
+
json: () => Promise.resolve({
|
|
65
|
+
ok: true,
|
|
66
|
+
pid: 123,
|
|
67
|
+
uptime: 10,
|
|
68
|
+
extensionConnected: false,
|
|
69
|
+
pending: 0,
|
|
70
|
+
lastCliRequestTime: Date.now(),
|
|
71
|
+
memoryMB: 16,
|
|
72
|
+
port: 19825,
|
|
73
|
+
}),
|
|
74
|
+
});
|
|
75
|
+
await expect(isExtensionConnected()).resolves.toBe(false);
|
|
76
|
+
});
|
|
77
|
+
});
|
|
@@ -1,8 +1,5 @@
|
|
|
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
|
import { isDaemonRunning } from './daemon-client.js';
|
|
8
5
|
export { isDaemonRunning };
|
package/dist/browser/discover.js
CHANGED
|
@@ -1,33 +1,19 @@
|
|
|
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
|
-
import {
|
|
8
|
-
import { isDaemonRunning } from './daemon-client.js';
|
|
4
|
+
import { fetchDaemonStatus, isDaemonRunning } from './daemon-client.js';
|
|
9
5
|
export { isDaemonRunning };
|
|
10
6
|
/**
|
|
11
7
|
* Check daemon status and return connection info.
|
|
12
8
|
*/
|
|
13
9
|
export async function checkDaemonStatus(opts) {
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
const controller = new AbortController();
|
|
17
|
-
const timer = setTimeout(() => controller.abort(), opts?.timeout ?? 2000);
|
|
18
|
-
try {
|
|
19
|
-
const res = await fetch(`http://127.0.0.1:${port}/status`, {
|
|
20
|
-
headers: { 'X-OpenCLI': '1' },
|
|
21
|
-
signal: controller.signal,
|
|
22
|
-
});
|
|
23
|
-
const data = await res.json();
|
|
24
|
-
return { running: true, extensionConnected: data.extensionConnected, extensionVersion: data.extensionVersion };
|
|
25
|
-
}
|
|
26
|
-
finally {
|
|
27
|
-
clearTimeout(timer);
|
|
28
|
-
}
|
|
29
|
-
}
|
|
30
|
-
catch {
|
|
10
|
+
const status = await fetchDaemonStatus({ timeout: opts?.timeout ?? 2000 });
|
|
11
|
+
if (!status) {
|
|
31
12
|
return { running: false, extensionConnected: false };
|
|
32
13
|
}
|
|
14
|
+
return {
|
|
15
|
+
running: true,
|
|
16
|
+
extensionConnected: status.extensionConnected,
|
|
17
|
+
extensionVersion: status.extensionVersion,
|
|
18
|
+
};
|
|
33
19
|
}
|
package/dist/browser/errors.d.ts
CHANGED
|
@@ -5,5 +5,9 @@
|
|
|
5
5
|
* The daemon architecture has a single failure mode: daemon not reachable or extension not connected.
|
|
6
6
|
*/
|
|
7
7
|
import { BrowserConnectError, type BrowserConnectKind } from '../errors.js';
|
|
8
|
+
/**
|
|
9
|
+
* Check if an error message indicates a transient browser error worth retrying.
|
|
10
|
+
*/
|
|
11
|
+
export declare function isTransientBrowserError(err: unknown): boolean;
|
|
8
12
|
export type ConnectFailureKind = BrowserConnectKind;
|
|
9
13
|
export declare function formatBrowserConnectError(kind: ConnectFailureKind, detail?: string): BrowserConnectError;
|
package/dist/browser/errors.js
CHANGED
|
@@ -6,6 +6,26 @@
|
|
|
6
6
|
*/
|
|
7
7
|
import { BrowserConnectError } from '../errors.js';
|
|
8
8
|
import { DEFAULT_DAEMON_PORT } from '../constants.js';
|
|
9
|
+
/**
|
|
10
|
+
* Transient browser error patterns — shared across daemon-client, pipeline executor,
|
|
11
|
+
* and page retry logic. These errors indicate temporary conditions (extension restart,
|
|
12
|
+
* service worker cycle, tab navigation) that are worth retrying.
|
|
13
|
+
*/
|
|
14
|
+
const TRANSIENT_ERROR_PATTERNS = [
|
|
15
|
+
'Extension disconnected',
|
|
16
|
+
'Extension not connected',
|
|
17
|
+
'attach failed',
|
|
18
|
+
'no longer exists',
|
|
19
|
+
'CDP connection',
|
|
20
|
+
'Daemon command failed',
|
|
21
|
+
];
|
|
22
|
+
/**
|
|
23
|
+
* Check if an error message indicates a transient browser error worth retrying.
|
|
24
|
+
*/
|
|
25
|
+
export function isTransientBrowserError(err) {
|
|
26
|
+
const msg = err instanceof Error ? err.message : String(err);
|
|
27
|
+
return TRANSIENT_ERROR_PATTERNS.some(pattern => msg.includes(pattern));
|
|
28
|
+
}
|
|
9
29
|
export function formatBrowserConnectError(kind, detail) {
|
|
10
30
|
switch (kind) {
|
|
11
31
|
case 'daemon-not-running':
|
package/dist/browser/index.d.ts
CHANGED
|
@@ -5,7 +5,7 @@
|
|
|
5
5
|
* External code should import from './browser/index.js' (or './browser.js' via Node resolution).
|
|
6
6
|
*/
|
|
7
7
|
export { Page } from './page.js';
|
|
8
|
-
export { BrowserBridge } from './
|
|
8
|
+
export { BrowserBridge } from './bridge.js';
|
|
9
9
|
export { CDPBridge } from './cdp.js';
|
|
10
10
|
export { isDaemonRunning } from './daemon-client.js';
|
|
11
11
|
export { generateSnapshotJs, scrollToRefJs, getFormStateJs } from './dom-snapshot.js';
|
package/dist/browser/index.js
CHANGED
|
@@ -5,7 +5,7 @@
|
|
|
5
5
|
* External code should import from './browser/index.js' (or './browser.js' via Node resolution).
|
|
6
6
|
*/
|
|
7
7
|
export { Page } from './page.js';
|
|
8
|
-
export { BrowserBridge } from './
|
|
8
|
+
export { BrowserBridge } from './bridge.js';
|
|
9
9
|
export { CDPBridge } from './cdp.js';
|
|
10
10
|
export { isDaemonRunning } from './daemon-client.js';
|
|
11
11
|
export { generateSnapshotJs, scrollToRefJs, getFormStateJs } from './dom-snapshot.js';
|
package/dist/browser/page.d.ts
CHANGED
|
@@ -9,18 +9,17 @@
|
|
|
9
9
|
* where resolveTabId() in the extension picks a chrome:// or
|
|
10
10
|
* chrome-extension:// tab that can't be debugged.
|
|
11
11
|
*/
|
|
12
|
-
import type { BrowserCookie,
|
|
12
|
+
import type { BrowserCookie, ScreenshotOptions } from '../types.js';
|
|
13
|
+
import { BasePage } from './base-page.js';
|
|
13
14
|
export declare function isRetryableSettleError(err: unknown): boolean;
|
|
14
15
|
/**
|
|
15
16
|
* Page — implements IPage by talking to the daemon via HTTP.
|
|
16
17
|
*/
|
|
17
|
-
export declare class Page
|
|
18
|
+
export declare class Page extends BasePage {
|
|
18
19
|
private readonly workspace;
|
|
19
20
|
constructor(workspace?: string);
|
|
20
21
|
/** Active tab ID, set after navigate and used in all subsequent commands */
|
|
21
22
|
private _tabId;
|
|
22
|
-
/** Last navigated URL, tracked in-memory to avoid extra round-trips */
|
|
23
|
-
private _lastUrl;
|
|
24
23
|
/** Helper: spread workspace into command params */
|
|
25
24
|
private _wsOpt;
|
|
26
25
|
/** Helper: spread workspace + tabId into command params */
|
|
@@ -29,54 +28,30 @@ export declare class Page implements IPage {
|
|
|
29
28
|
waitUntil?: 'load' | 'none';
|
|
30
29
|
settleMs?: number;
|
|
31
30
|
}): Promise<void>;
|
|
32
|
-
|
|
33
|
-
/** Close the automation window in the extension */
|
|
34
|
-
closeWindow(): Promise<void>;
|
|
31
|
+
getActiveTabId(): number | undefined;
|
|
35
32
|
evaluate(js: string): Promise<unknown>;
|
|
36
33
|
getCookies(opts?: {
|
|
37
34
|
domain?: string;
|
|
38
35
|
url?: string;
|
|
39
36
|
}): Promise<BrowserCookie[]>;
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
private _basicSnapshot;
|
|
43
|
-
click(ref: string): Promise<void>;
|
|
44
|
-
typeText(ref: string, text: string): Promise<void>;
|
|
45
|
-
pressKey(key: string): Promise<void>;
|
|
46
|
-
scrollTo(ref: string): Promise<unknown>;
|
|
47
|
-
getFormState(): Promise<Record<string, unknown>>;
|
|
48
|
-
wait(options: number | WaitOptions): Promise<void>;
|
|
37
|
+
/** Close the automation window in the extension */
|
|
38
|
+
closeWindow(): Promise<void>;
|
|
49
39
|
tabs(): Promise<unknown[]>;
|
|
50
40
|
closeTab(index?: number): Promise<void>;
|
|
51
41
|
newTab(): Promise<void>;
|
|
52
42
|
selectTab(index: number): Promise<void>;
|
|
53
|
-
networkRequests(includeStatic?: boolean): Promise<unknown[]>;
|
|
54
|
-
/**
|
|
55
|
-
* Console messages are not available in lightweight daemon mode.
|
|
56
|
-
* Would require CDP Runtime.consoleAPICalled event listener.
|
|
57
|
-
* @returns Always returns empty array.
|
|
58
|
-
*/
|
|
59
|
-
consoleMessages(_level?: string): Promise<unknown[]>;
|
|
60
43
|
/**
|
|
61
44
|
* Capture a screenshot via CDP Page.captureScreenshot.
|
|
62
|
-
* @param options.format - 'png' (default) or 'jpeg'
|
|
63
|
-
* @param options.quality - JPEG quality 0-100
|
|
64
|
-
* @param options.fullPage - capture full scrollable page
|
|
65
|
-
* @param options.path - save to file path (returns base64 if omitted)
|
|
66
45
|
*/
|
|
67
46
|
screenshot(options?: ScreenshotOptions): Promise<string>;
|
|
68
|
-
scroll(direction?: string, amount?: number): Promise<void>;
|
|
69
|
-
autoScroll(options?: {
|
|
70
|
-
times?: number;
|
|
71
|
-
delayMs?: number;
|
|
72
|
-
}): Promise<void>;
|
|
73
|
-
installInterceptor(pattern: string): Promise<void>;
|
|
74
|
-
getInterceptedRequests(): Promise<unknown[]>;
|
|
75
47
|
/**
|
|
76
48
|
* Set local file paths on a file input element via CDP DOM.setFileInputFiles.
|
|
77
49
|
* Chrome reads the files directly from the local filesystem, avoiding the
|
|
78
50
|
* payload size limits of base64-in-evaluate.
|
|
79
51
|
*/
|
|
80
52
|
setFileInput(files: string[], selector?: string): Promise<void>;
|
|
81
|
-
|
|
53
|
+
cdp(method: string, params?: Record<string, unknown>): Promise<unknown>;
|
|
54
|
+
nativeClick(x: number, y: number): Promise<void>;
|
|
55
|
+
nativeType(text: string): Promise<void>;
|
|
56
|
+
nativeKeyPress(key: string, modifiers?: string[]): Promise<void>;
|
|
82
57
|
}
|