@jackwener/opencli 1.5.7 → 1.5.9
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 +29 -0
- package/README.md +17 -1
- package/README.zh-CN.md +17 -1
- 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 +1 -7
- package/dist/browser/daemon-client.js +2 -9
- package/dist/browser/discover.d.ts +1 -4
- package/dist/browser/discover.js +1 -4
- 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 +6 -35
- package/dist/browser/page.js +10 -189
- package/dist/browser/tabs.js +5 -5
- package/dist/browser.test.js +15 -15
- package/dist/cli-manifest.json +294 -22
- 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.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/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/commanderAdapter.js +6 -3
- package/dist/commanderAdapter.test.js +33 -0
- package/dist/commands/daemon.js +1 -1
- package/dist/commands/daemon.test.js +1 -1
- package/dist/doctor.d.ts +1 -2
- package/dist/doctor.js +7 -8
- package/dist/explore.js +1 -1
- package/dist/extension-manifest-regression.test.js +1 -0
- 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 +3 -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/extension/dist/background.js +614 -794
- package/extension/manifest.json +2 -1
- package/extension/src/background.test.ts +7 -163
- package/extension/src/background.ts +7 -156
- package/extension/src/cdp.test.ts +75 -0
- package/extension/src/cdp.ts +77 -3
- package/extension/src/protocol.ts +1 -5
- package/package.json +1 -1
- package/skills/opencli-explorer/SKILL.md +847 -0
- package/skills/opencli-oneshot/SKILL.md +216 -0
- package/skills/opencli-usage/SKILL.md +71 -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.ts +3 -14
- package/src/browser/discover.ts +1 -4
- package/src/browser/errors.ts +22 -0
- package/src/browser/index.ts +1 -1
- package/src/browser/page.ts +13 -212
- package/src/browser/tabs.ts +5 -5
- package/src/browser.test.ts +15 -15
- 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/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/commanderAdapter.test.ts +47 -0
- package/src/commanderAdapter.ts +7 -3
- package/src/commands/daemon.test.ts +1 -1
- package/src/commands/daemon.ts +1 -1
- package/src/doctor.ts +7 -8
- package/src/explore.ts +1 -1
- package/src/extension-manifest-regression.test.ts +1 -0
- 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 +3 -1
- package/.agents/skills/cross-project-adapter-migration/SKILL.md +0 -249
- package/.agents/workflows/cross-project-adapter-migration.md +0 -54
- package/SKILL.md +0 -879
- 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/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 → amazon/bestsellers.test.d.ts} +0 -0
- /package/dist/clis/{notebooklm/binding.test.d.ts → amazon/discussion.test.d.ts} +0 -0
- /package/src/browser/{mcp.ts → bridge.ts} +0 -0
package/CHANGELOG.md
CHANGED
|
@@ -1,5 +1,34 @@
|
|
|
1
1
|
# Changelog
|
|
2
2
|
|
|
3
|
+
## [1.5.9](https://github.com/jackwener/opencli/compare/v1.5.8...v1.5.9) (2026-04-02)
|
|
4
|
+
|
|
5
|
+
|
|
6
|
+
### Features
|
|
7
|
+
|
|
8
|
+
* **amazon:** add browser adapter — bestsellers, search, product, offer, discussion ([#659](https://github.com/jackwener/opencli/issues/659))
|
|
9
|
+
* **skills:** create skills/ directory structure with opencli-usage, opencli-explorer, opencli-oneshot ([#670](https://github.com/jackwener/opencli/issues/670))
|
|
10
|
+
* **record:** add minimal record write candidates ([#665](https://github.com/jackwener/opencli/issues/665))
|
|
11
|
+
|
|
12
|
+
|
|
13
|
+
### Refactoring
|
|
14
|
+
|
|
15
|
+
* src cleanup — deduplicate errors, cache VM, extract BasePage, remove Playwright MCP legacy ([#667](https://github.com/jackwener/opencli/issues/667))
|
|
16
|
+
* remove bind-current, restore owned-only browser automation model ([#664](https://github.com/jackwener/opencli/issues/664))
|
|
17
|
+
|
|
18
|
+
|
|
19
|
+
### Chores
|
|
20
|
+
|
|
21
|
+
* remove .agents directory ([#668](https://github.com/jackwener/opencli/issues/668))
|
|
22
|
+
|
|
23
|
+
|
|
24
|
+
## [1.5.8](https://github.com/jackwener/opencli/compare/v1.5.7...v1.5.8) (2026-04-01)
|
|
25
|
+
|
|
26
|
+
|
|
27
|
+
### Bug Fixes
|
|
28
|
+
|
|
29
|
+
* **extension:** avoid mutating healthy tabs before debugger attach and add regression coverage ([#662](https://github.com/jackwener/opencli/issues/662))
|
|
30
|
+
|
|
31
|
+
|
|
3
32
|
## [1.5.7](https://github.com/jackwener/opencli/compare/v1.5.6...v1.5.7) (2026-04-01)
|
|
4
33
|
|
|
5
34
|
|
package/README.md
CHANGED
|
@@ -90,6 +90,20 @@ opencli bilibili hot --limit 5 # Browser command (requires Extension)
|
|
|
90
90
|
npm install -g @jackwener/opencli@latest
|
|
91
91
|
```
|
|
92
92
|
|
|
93
|
+
### Install AI Skills
|
|
94
|
+
|
|
95
|
+
OpenCLI provides [skills](./skills/) for AI agents (Claude Code, etc.):
|
|
96
|
+
|
|
97
|
+
```bash
|
|
98
|
+
# Install all OpenCLI skills
|
|
99
|
+
npx skills add jackwener/opencli
|
|
100
|
+
|
|
101
|
+
# Or install specific skills
|
|
102
|
+
npx skills add jackwener/opencli --skill opencli-usage # Command reference
|
|
103
|
+
npx skills add jackwener/opencli --skill opencli-explorer # Adapter development guide
|
|
104
|
+
npx skills add jackwener/opencli --skill opencli-oneshot # Quick command reference
|
|
105
|
+
```
|
|
106
|
+
|
|
93
107
|
---
|
|
94
108
|
|
|
95
109
|
### For Developers
|
|
@@ -123,7 +137,9 @@ git clone git@github.com:jackwener/opencli.git && cd opencli && npm install && n
|
|
|
123
137
|
| **tieba** | `hot` `posts` `search` `read` |
|
|
124
138
|
| **twitter** | `trending` `search` `timeline` `bookmarks` `post` `download` `profile` `article` `like` `likes` `notifications` `reply` `reply-dm` `thread` `follow` `unfollow` `followers` `following` `block` `unblock` `bookmark` `unbookmark` `delete` `hide-reply` `accept` |
|
|
125
139
|
| **reddit** | `hot` `frontpage` `popular` `search` `subreddit` `user` `user-posts` `user-comments` `read` `save` `saved` `subscribe` `upvote` `upvoted` `comment` |
|
|
126
|
-
| **
|
|
140
|
+
| **amazon** | `bestsellers` `search` `product` `offer` `discussion` |
|
|
141
|
+
| **gemini** | `new` `ask` `image` |
|
|
142
|
+
| **notebooklm** | `status` `list` `open` `select` `current` `get` `metadata` `source-list` `source-get` `source-fulltext` `source-guide` `history` `note-list` `notes-list` `notes-get` `summary` |
|
|
127
143
|
| **spotify** | `auth` `status` `play` `pause` `next` `prev` `volume` `search` `queue` `shuffle` `repeat` |
|
|
128
144
|
|
|
129
145
|
66+ adapters in total — **[→ see all supported sites & commands](./docs/adapters/index.md)**
|
package/README.zh-CN.md
CHANGED
|
@@ -116,6 +116,20 @@ opencli list # 可以在任何地方使用了!
|
|
|
116
116
|
npm install -g @jackwener/opencli@latest
|
|
117
117
|
```
|
|
118
118
|
|
|
119
|
+
### 安装 AI Skills
|
|
120
|
+
|
|
121
|
+
OpenCLI 提供 [skills](./skills/) 供 AI Agent(Claude Code 等)使用:
|
|
122
|
+
|
|
123
|
+
```bash
|
|
124
|
+
# 安装所有 OpenCLI skills
|
|
125
|
+
npx skills add jackwener/opencli
|
|
126
|
+
|
|
127
|
+
# 或安装特定 skill
|
|
128
|
+
npx skills add jackwener/opencli --skill opencli-usage # 命令参考
|
|
129
|
+
npx skills add jackwener/opencli --skill opencli-explorer # 适配器开发指南
|
|
130
|
+
npx skills add jackwener/opencli --skill opencli-oneshot # 快速命令参考
|
|
131
|
+
```
|
|
132
|
+
|
|
119
133
|
## 内置命令
|
|
120
134
|
|
|
121
135
|
运行 `opencli list` 查看完整注册表。
|
|
@@ -176,8 +190,10 @@ npm install -g @jackwener/opencli@latest
|
|
|
176
190
|
| **douban** | `search` `top250` `subject` `photos` `download` `marks` `reviews` `movie-hot` `book-hot` | 浏览器 |
|
|
177
191
|
| **facebook** | `feed` `profile` `search` `friends` `groups` `events` `notifications` `memories` `add-friend` `join-group` | 浏览器 |
|
|
178
192
|
| **google** | `news` `search` `suggest` `trends` | 公开 |
|
|
193
|
+
| **amazon** | `bestsellers` `search` `product` `offer` `discussion` | 浏览器 |
|
|
194
|
+
| **gemini** | `new` `ask` `image` | 浏览器 |
|
|
179
195
|
| **spotify** | `auth` `status` `play` `pause` `next` `prev` `volume` `search` `queue` `shuffle` `repeat` | OAuth API |
|
|
180
|
-
| **notebooklm** | `status` `list` `
|
|
196
|
+
| **notebooklm** | `status` `list` `open` `select` `current` `get` `metadata` `source-list` `source-get` `source-fulltext` `source-guide` `history` `note-list` `notes-list` `notes-get` `summary` | 浏览器 |
|
|
181
197
|
| **36kr** | `news` `hot` `search` `article` | 公开 / 浏览器 |
|
|
182
198
|
| **imdb** | `search` `title` `top` `trending` `person` `reviews` | 公开 |
|
|
183
199
|
| **producthunt** | `posts` `today` `hot` `browse` | 公开 / 浏览器 |
|
|
@@ -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
|
+
}
|
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';
|
|
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;
|
|
@@ -45,7 +43,3 @@ export declare function isExtensionConnected(): Promise<boolean>;
|
|
|
45
43
|
*/
|
|
46
44
|
export declare function sendCommand(action: DaemonCommand['action'], params?: Omit<DaemonCommand, 'id' | 'action'>): Promise<unknown>;
|
|
47
45
|
export declare function listSessions(): Promise<BrowserSessionInfo[]>;
|
|
48
|
-
export declare function bindCurrentTab(workspace: string, opts?: {
|
|
49
|
-
matchDomain?: string;
|
|
50
|
-
matchPathPrefix?: string;
|
|
51
|
-
}): Promise<unknown>;
|
|
@@ -5,6 +5,7 @@
|
|
|
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}`;
|
|
10
11
|
let _idCounter = 0;
|
|
@@ -74,12 +75,7 @@ export async function sendCommand(action, params = {}) {
|
|
|
74
75
|
const result = (await res.json());
|
|
75
76
|
if (!result.ok) {
|
|
76
77
|
// 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) {
|
|
78
|
+
if (isTransientBrowserError(new Error(result.error ?? '')) && attempt < maxRetries) {
|
|
83
79
|
// Longer delay for extension recovery (service worker restart)
|
|
84
80
|
await sleep(1500);
|
|
85
81
|
continue;
|
|
@@ -105,6 +101,3 @@ export async function listSessions() {
|
|
|
105
101
|
const result = await sendCommand('sessions');
|
|
106
102
|
return Array.isArray(result) ? result : [];
|
|
107
103
|
}
|
|
108
|
-
export async function bindCurrentTab(workspace, opts = {}) {
|
|
109
|
-
return sendCommand('bind-current', { workspace, ...opts });
|
|
110
|
-
}
|
|
@@ -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,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 { DEFAULT_DAEMON_PORT } from '../constants.js';
|
|
8
5
|
import { isDaemonRunning } from './daemon-client.js';
|
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';
|