@jackwener/opencli 1.5.8 → 1.6.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/CHANGELOG.md +42 -0
- package/README.md +35 -1
- package/README.zh-CN.md +17 -1
- package/SKILL.md +31 -851
- package/autoresearch/baseline-browse.txt +1 -0
- package/autoresearch/baseline-skill.txt +1 -0
- package/autoresearch/browse-tasks.json +688 -0
- package/autoresearch/eval-browse.ts +185 -0
- package/autoresearch/eval-skill.ts +248 -0
- package/autoresearch/run-browse.sh +9 -0
- package/autoresearch/run-skill.sh +9 -0
- package/dist/browser/base-page.d.ts +48 -0
- package/dist/browser/base-page.js +160 -0
- package/dist/browser/cdp.js +4 -106
- package/dist/browser/daemon-client.d.ts +20 -7
- package/dist/browser/daemon-client.js +39 -39
- package/dist/browser/daemon-client.test.js +77 -0
- package/dist/browser/discover.d.ts +1 -4
- package/dist/browser/discover.js +9 -23
- package/dist/browser/errors.d.ts +4 -0
- package/dist/browser/errors.js +20 -0
- package/dist/browser/index.d.ts +1 -1
- package/dist/browser/index.js +1 -1
- package/dist/browser/page.d.ts +10 -35
- package/dist/browser/page.js +55 -187
- package/dist/browser/tabs.js +5 -5
- package/dist/browser.test.js +15 -15
- package/dist/cli-manifest.json +294 -22
- package/dist/cli.js +392 -0
- package/dist/clis/amazon/bestsellers.d.ts +21 -0
- package/dist/clis/amazon/bestsellers.js +130 -0
- package/dist/clis/amazon/bestsellers.test.js +20 -0
- package/dist/clis/amazon/discussion.d.ts +20 -0
- package/dist/clis/amazon/discussion.js +91 -0
- package/dist/clis/amazon/discussion.test.d.ts +1 -0
- package/dist/clis/amazon/discussion.test.js +36 -0
- package/dist/clis/amazon/offer.d.ts +23 -0
- package/dist/clis/amazon/offer.js +140 -0
- package/dist/clis/amazon/offer.test.d.ts +1 -0
- package/dist/clis/amazon/offer.test.js +29 -0
- package/dist/clis/amazon/product.d.ts +18 -0
- package/dist/clis/amazon/product.js +92 -0
- package/dist/clis/amazon/product.test.d.ts +1 -0
- package/dist/clis/amazon/product.test.js +24 -0
- package/dist/clis/amazon/search.d.ts +18 -0
- package/dist/clis/amazon/search.js +87 -0
- package/dist/clis/amazon/search.test.d.ts +1 -0
- package/dist/clis/amazon/search.test.js +22 -0
- package/dist/clis/amazon/shared.d.ts +64 -0
- package/dist/clis/amazon/shared.js +255 -0
- package/dist/clis/amazon/shared.test.d.ts +1 -0
- package/dist/clis/amazon/shared.test.js +33 -0
- package/dist/clis/gemini/ask.d.ts +1 -0
- package/dist/clis/gemini/ask.js +40 -0
- package/dist/clis/gemini/image.d.ts +1 -0
- package/dist/clis/gemini/image.js +105 -0
- package/dist/clis/gemini/new.d.ts +1 -0
- package/dist/clis/gemini/new.js +20 -0
- package/dist/clis/gemini/utils.d.ts +34 -0
- package/dist/clis/gemini/utils.js +463 -0
- package/dist/clis/gemini/utils.test.d.ts +1 -0
- package/dist/clis/gemini/utils.test.js +31 -0
- package/dist/clis/notebooklm/compat.test.d.ts +1 -1
- package/dist/clis/notebooklm/compat.test.js +3 -3
- package/dist/clis/notebooklm/current.js +2 -3
- package/dist/clis/notebooklm/get.js +2 -3
- package/dist/clis/notebooklm/history.js +2 -3
- package/dist/clis/notebooklm/note-list.js +2 -3
- package/dist/clis/notebooklm/notes-get.js +2 -3
- package/dist/clis/notebooklm/open.d.ts +1 -0
- package/dist/clis/notebooklm/open.js +41 -0
- package/dist/clis/notebooklm/open.test.d.ts +1 -0
- package/dist/clis/notebooklm/open.test.js +63 -0
- package/dist/clis/notebooklm/source-fulltext.js +2 -3
- package/dist/clis/notebooklm/source-get.js +2 -3
- package/dist/clis/notebooklm/source-guide.js +2 -3
- package/dist/clis/notebooklm/source-list.js +2 -3
- package/dist/clis/notebooklm/status.js +1 -2
- package/dist/clis/notebooklm/summary.js +2 -3
- package/dist/clis/notebooklm/utils.d.ts +2 -1
- package/dist/clis/notebooklm/utils.js +20 -21
- package/dist/clis/twitter/article.js +28 -1
- package/dist/clis/xiaohongshu/creator-note-detail.test.js +11 -11
- package/dist/clis/xiaohongshu/creator-notes-summary.test.js +6 -6
- package/dist/clis/xiaohongshu/creator-notes.test.js +22 -22
- package/dist/clis/xiaohongshu/note.js +11 -0
- package/dist/clis/xiaohongshu/note.test.js +49 -0
- package/dist/commanderAdapter.js +7 -4
- package/dist/commanderAdapter.test.js +76 -0
- package/dist/commands/daemon.js +8 -47
- package/dist/commands/daemon.test.js +45 -70
- package/dist/discovery.js +27 -0
- package/dist/doctor.d.ts +1 -2
- package/dist/doctor.js +7 -8
- package/dist/explore.js +1 -1
- package/dist/output.js +28 -0
- package/dist/output.test.js +15 -0
- package/dist/pipeline/executor.js +2 -7
- package/dist/pipeline/steps/browser.js +1 -1
- package/dist/pipeline/template.js +25 -3
- package/dist/record.d.ts +50 -0
- package/dist/record.js +298 -57
- package/dist/record.test.d.ts +1 -0
- package/dist/record.test.js +293 -0
- package/dist/registry.d.ts +2 -0
- package/dist/registry.js +1 -0
- package/dist/registry.test.js +10 -0
- package/dist/runtime.js +3 -3
- package/dist/snapshotFormatter.d.ts +1 -1
- package/dist/snapshotFormatter.js +4 -4
- package/dist/snapshotFormatter.test.d.ts +1 -1
- package/dist/snapshotFormatter.test.js +2 -2
- package/dist/types.d.ts +11 -1
- package/dist/types.js +1 -1
- package/docs/.vitepress/config.mts +2 -0
- package/docs/adapters/browser/amazon.md +53 -0
- package/docs/adapters/browser/gemini.md +72 -0
- package/docs/adapters/browser/notebooklm.md +5 -5
- package/docs/adapters/index.md +3 -1
- package/docs/guide/getting-started.md +21 -0
- package/docs/superpowers/specs/2026-04-02-browse-skill-testing-design.md +144 -0
- package/docs/zh/guide/getting-started.md +21 -0
- package/extension/package-lock.json +2 -2
- package/extension/src/background.test.ts +7 -163
- package/extension/src/background.ts +58 -161
- package/extension/src/cdp.ts +77 -124
- package/extension/src/protocol.ts +5 -5
- package/package.json +1 -1
- package/skills/opencli-explorer/SKILL.md +853 -0
- package/skills/opencli-oneshot/SKILL.md +222 -0
- package/skills/opencli-operate/SKILL.md +213 -0
- package/skills/opencli-usage/SKILL.md +152 -0
- package/skills/opencli-usage/browser.md +429 -0
- package/skills/opencli-usage/desktop.md +118 -0
- package/skills/opencli-usage/plugins.md +82 -0
- package/skills/opencli-usage/public-api.md +149 -0
- package/src/browser/base-page.ts +197 -0
- package/src/browser/cdp.ts +7 -131
- package/src/browser/daemon-client.test.ts +103 -0
- package/src/browser/daemon-client.ts +55 -43
- package/src/browser/discover.ts +9 -21
- package/src/browser/errors.ts +22 -0
- package/src/browser/index.ts +1 -1
- package/src/browser/page.ts +57 -209
- package/src/browser/tabs.ts +5 -5
- package/src/browser.test.ts +15 -15
- package/src/cli.ts +392 -0
- package/src/clis/amazon/bestsellers.test.ts +22 -0
- package/src/clis/amazon/bestsellers.ts +180 -0
- package/src/clis/amazon/discussion.test.ts +38 -0
- package/src/clis/amazon/discussion.ts +131 -0
- package/src/clis/amazon/offer.test.ts +35 -0
- package/src/clis/amazon/offer.ts +185 -0
- package/src/clis/amazon/product.test.ts +26 -0
- package/src/clis/amazon/product.ts +131 -0
- package/src/clis/amazon/search.test.ts +24 -0
- package/src/clis/amazon/search.ts +128 -0
- package/src/clis/amazon/shared.test.ts +37 -0
- package/src/clis/amazon/shared.ts +316 -0
- package/src/clis/gemini/ask.ts +46 -0
- package/src/clis/gemini/image.ts +115 -0
- package/src/clis/gemini/new.ts +22 -0
- package/src/clis/gemini/utils.test.ts +36 -0
- package/src/clis/gemini/utils.ts +523 -0
- package/src/clis/notebooklm/compat.test.ts +3 -3
- package/src/clis/notebooklm/current.ts +2 -3
- package/src/clis/notebooklm/get.ts +1 -3
- package/src/clis/notebooklm/history.ts +1 -3
- package/src/clis/notebooklm/note-list.ts +1 -3
- package/src/clis/notebooklm/notes-get.ts +1 -3
- package/src/clis/notebooklm/open.test.ts +78 -0
- package/src/clis/notebooklm/open.ts +61 -0
- package/src/clis/notebooklm/source-fulltext.ts +1 -3
- package/src/clis/notebooklm/source-get.ts +1 -3
- package/src/clis/notebooklm/source-guide.ts +1 -3
- package/src/clis/notebooklm/source-list.ts +1 -3
- package/src/clis/notebooklm/status.ts +1 -2
- package/src/clis/notebooklm/summary.ts +1 -3
- package/src/clis/notebooklm/utils.ts +29 -20
- package/src/clis/twitter/article.ts +31 -1
- package/src/clis/xiaohongshu/creator-note-detail.test.ts +11 -11
- package/src/clis/xiaohongshu/creator-notes-summary.test.ts +6 -6
- package/src/clis/xiaohongshu/creator-notes.test.ts +22 -22
- package/src/clis/xiaohongshu/note.test.ts +51 -0
- package/src/clis/xiaohongshu/note.ts +18 -0
- package/src/commanderAdapter.test.ts +109 -0
- package/src/commanderAdapter.ts +8 -4
- package/src/commands/daemon.test.ts +50 -84
- package/src/commands/daemon.ts +8 -56
- package/src/discovery.ts +22 -0
- package/src/doctor.ts +8 -9
- package/src/explore.ts +1 -1
- package/src/output.test.ts +17 -0
- package/src/output.ts +27 -0
- package/src/pipeline/executor.ts +2 -7
- package/src/pipeline/steps/browser.ts +1 -1
- package/src/pipeline/template.ts +27 -4
- package/src/record.test.ts +362 -0
- package/src/record.ts +341 -62
- package/src/registry.test.ts +12 -0
- package/src/registry.ts +3 -0
- package/src/runtime.ts +3 -3
- package/src/snapshotFormatter.test.ts +2 -2
- package/src/snapshotFormatter.ts +4 -4
- package/src/types.ts +11 -1
- package/.agents/skills/cross-project-adapter-migration/SKILL.md +0 -249
- package/.agents/workflows/cross-project-adapter-migration.md +0 -54
- package/dist/clis/notebooklm/bind-current.js +0 -29
- package/dist/clis/notebooklm/bind-current.test.d.ts +0 -1
- package/dist/clis/notebooklm/bind-current.test.js +0 -35
- package/dist/clis/notebooklm/binding.test.js +0 -44
- package/extension/dist/background.js +0 -819
- package/src/clis/notebooklm/bind-current.test.ts +0 -43
- package/src/clis/notebooklm/bind-current.ts +0 -36
- package/src/clis/notebooklm/binding.test.ts +0 -53
- /package/dist/browser/{mcp.d.ts → bridge.d.ts} +0 -0
- /package/dist/browser/{mcp.js → bridge.js} +0 -0
- /package/dist/{clis/notebooklm/bind-current.d.ts → browser/daemon-client.test.d.ts} +0 -0
- /package/dist/clis/{notebooklm/binding.test.d.ts → amazon/bestsellers.test.d.ts} +0 -0
- /package/src/browser/{mcp.ts → bridge.ts} +0 -0
|
@@ -0,0 +1,149 @@
|
|
|
1
|
+
# Public API Commands
|
|
2
|
+
|
|
3
|
+
Commands that work without browser or authentication.
|
|
4
|
+
|
|
5
|
+
## Hacker News
|
|
6
|
+
|
|
7
|
+
```bash
|
|
8
|
+
opencli hackernews top --limit 10 # Top stories
|
|
9
|
+
opencli hackernews new --limit 10 # Newest stories
|
|
10
|
+
opencli hackernews best --limit 10 # Best stories
|
|
11
|
+
opencli hackernews ask --limit 10 # Ask HN posts
|
|
12
|
+
opencli hackernews show --limit 10 # Show HN posts
|
|
13
|
+
opencli hackernews jobs --limit 10 # Job postings
|
|
14
|
+
opencli hackernews search "rust" # 搜索 (query positional)
|
|
15
|
+
opencli hackernews user dang # 用户资料 (username positional)
|
|
16
|
+
```
|
|
17
|
+
|
|
18
|
+
## V2EX (Public Features)
|
|
19
|
+
|
|
20
|
+
```bash
|
|
21
|
+
opencli v2ex hot --limit 10 # 热门话题
|
|
22
|
+
opencli v2ex latest --limit 10 # 最新话题
|
|
23
|
+
opencli v2ex topic 1024 # 主题详情 (id positional)
|
|
24
|
+
opencli v2ex node python # 节点话题列表 (name positional)
|
|
25
|
+
opencli v2ex nodes --limit 30 # 所有节点列表
|
|
26
|
+
opencli v2ex member username # 用户资料 (username positional)
|
|
27
|
+
opencli v2ex user username # 用户发帖列表 (username positional)
|
|
28
|
+
opencli v2ex replies 1024 # 主题回复列表 (id positional)
|
|
29
|
+
```
|
|
30
|
+
|
|
31
|
+
## BBC News
|
|
32
|
+
|
|
33
|
+
```bash
|
|
34
|
+
opencli bbc news --limit 10 # BBC News RSS headlines
|
|
35
|
+
```
|
|
36
|
+
|
|
37
|
+
## Sina Finance
|
|
38
|
+
|
|
39
|
+
```bash
|
|
40
|
+
opencli sinafinance news --limit 10 --type 1 # 7x24实时快讯
|
|
41
|
+
# Types: 0=全部 1=A股 2=宏观 3=公司 4=数据 5=市场 6=国际 7=观点 8=央行 9=其它
|
|
42
|
+
```
|
|
43
|
+
|
|
44
|
+
## Lobsters
|
|
45
|
+
|
|
46
|
+
```bash
|
|
47
|
+
opencli lobsters hot --limit 10 # 热门
|
|
48
|
+
opencli lobsters newest --limit 10 # 最新
|
|
49
|
+
opencli lobsters active --limit 10 # 活跃
|
|
50
|
+
opencli lobsters tag rust # 按标签筛选 (tag positional)
|
|
51
|
+
```
|
|
52
|
+
|
|
53
|
+
## Google
|
|
54
|
+
|
|
55
|
+
```bash
|
|
56
|
+
opencli google news --limit 10 # 新闻
|
|
57
|
+
opencli google search "AI" # 搜索 (query positional)
|
|
58
|
+
opencli google suggest "AI" # 搜索建议 (query positional)
|
|
59
|
+
opencli google trends # 趋势
|
|
60
|
+
```
|
|
61
|
+
|
|
62
|
+
## DEV.to
|
|
63
|
+
|
|
64
|
+
```bash
|
|
65
|
+
opencli devto top --limit 10 # 热门文章
|
|
66
|
+
opencli devto tag javascript --limit 10 # 按标签 (tag positional)
|
|
67
|
+
opencli devto user username # 用户文章 (username positional)
|
|
68
|
+
```
|
|
69
|
+
|
|
70
|
+
## Steam
|
|
71
|
+
|
|
72
|
+
```bash
|
|
73
|
+
opencli steam top-sellers --limit 10 # 热销游戏
|
|
74
|
+
```
|
|
75
|
+
|
|
76
|
+
## Apple Podcasts
|
|
77
|
+
|
|
78
|
+
```bash
|
|
79
|
+
opencli apple-podcasts top --limit 10 # 热门播客排行榜 (支持 --country us/cn/gb/jp)
|
|
80
|
+
opencli apple-podcasts search "科技" # 搜索播客 (query positional)
|
|
81
|
+
opencli apple-podcasts episodes 12345 # 播客剧集列表 (id positional)
|
|
82
|
+
```
|
|
83
|
+
|
|
84
|
+
## arXiv
|
|
85
|
+
|
|
86
|
+
```bash
|
|
87
|
+
opencli arxiv search "attention" # 搜索论文 (query positional)
|
|
88
|
+
opencli arxiv paper 1706.03762 # 论文详情 (id positional)
|
|
89
|
+
```
|
|
90
|
+
|
|
91
|
+
## StackOverflow
|
|
92
|
+
|
|
93
|
+
```bash
|
|
94
|
+
opencli stackoverflow hot --limit 10 # 热门问题
|
|
95
|
+
opencli stackoverflow search "typescript" # 搜索 (query positional)
|
|
96
|
+
opencli stackoverflow bounties --limit 10 # 悬赏问题
|
|
97
|
+
```
|
|
98
|
+
|
|
99
|
+
## Xiaoyuzhou (小宇宙)
|
|
100
|
+
|
|
101
|
+
```bash
|
|
102
|
+
opencli xiaoyuzhou podcast 12345 # 播客资料 (id positional)
|
|
103
|
+
opencli xiaoyuzhou podcast-episodes 12345 # 播客剧集列表 (id positional)
|
|
104
|
+
opencli xiaoyuzhou episode 12345 # 单集详情 (id positional)
|
|
105
|
+
```
|
|
106
|
+
|
|
107
|
+
## Wikipedia
|
|
108
|
+
|
|
109
|
+
```bash
|
|
110
|
+
opencli wikipedia search "AI" # 搜索 (query positional)
|
|
111
|
+
opencli wikipedia summary "Python" # 摘要 (title positional)
|
|
112
|
+
```
|
|
113
|
+
|
|
114
|
+
## Bloomberg (RSS)
|
|
115
|
+
|
|
116
|
+
```bash
|
|
117
|
+
opencli bloomberg main --limit 10 # Bloomberg 首页头条
|
|
118
|
+
opencli bloomberg markets --limit 10 # 市场新闻
|
|
119
|
+
opencli bloomberg tech --limit 10 # 科技新闻
|
|
120
|
+
opencli bloomberg politics --limit 10 # 政治新闻
|
|
121
|
+
opencli bloomberg economics --limit 10 # 经济新闻
|
|
122
|
+
opencli bloomberg opinions --limit 10 # 观点
|
|
123
|
+
opencli bloomberg industries --limit 10 # 行业新闻
|
|
124
|
+
opencli bloomberg businessweek --limit 10 # Businessweek
|
|
125
|
+
opencli bloomberg feeds # 列出所有 RSS feed 别名
|
|
126
|
+
```
|
|
127
|
+
|
|
128
|
+
## Dictionary
|
|
129
|
+
|
|
130
|
+
```bash
|
|
131
|
+
opencli dictionary search "serendipity" # 单词释义 (word positional)
|
|
132
|
+
opencli dictionary synonyms "happy" # 近义词 (word positional)
|
|
133
|
+
opencli dictionary examples "ubiquitous" # 例句 (word positional)
|
|
134
|
+
```
|
|
135
|
+
|
|
136
|
+
## HuggingFace
|
|
137
|
+
|
|
138
|
+
```bash
|
|
139
|
+
opencli hf top --limit 10 # 热门模型
|
|
140
|
+
```
|
|
141
|
+
|
|
142
|
+
## Product Hunt
|
|
143
|
+
|
|
144
|
+
```bash
|
|
145
|
+
opencli producthunt today --limit 10 # 今日产品
|
|
146
|
+
opencli producthunt week --limit 10 # 本周产品
|
|
147
|
+
opencli producthunt month --limit 10 # 本月产品
|
|
148
|
+
opencli producthunt search "AI" # 搜索产品 (query positional)
|
|
149
|
+
```
|
|
@@ -0,0 +1,197 @@
|
|
|
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
|
+
|
|
12
|
+
import type { BrowserCookie, IPage, ScreenshotOptions, SnapshotOptions, WaitOptions } from '../types.js';
|
|
13
|
+
import { generateSnapshotJs, scrollToRefJs, getFormStateJs } from './dom-snapshot.js';
|
|
14
|
+
import {
|
|
15
|
+
clickJs,
|
|
16
|
+
typeTextJs,
|
|
17
|
+
pressKeyJs,
|
|
18
|
+
waitForTextJs,
|
|
19
|
+
waitForCaptureJs,
|
|
20
|
+
waitForSelectorJs,
|
|
21
|
+
scrollJs,
|
|
22
|
+
autoScrollJs,
|
|
23
|
+
networkRequestsJs,
|
|
24
|
+
waitForDomStableJs,
|
|
25
|
+
} from './dom-helpers.js';
|
|
26
|
+
import { formatSnapshot } from '../snapshotFormatter.js';
|
|
27
|
+
|
|
28
|
+
export abstract class BasePage implements IPage {
|
|
29
|
+
protected _lastUrl: string | null = null;
|
|
30
|
+
|
|
31
|
+
// ── Transport-specific methods (must be implemented by subclasses) ──
|
|
32
|
+
|
|
33
|
+
abstract goto(url: string, options?: { waitUntil?: 'load' | 'none'; settleMs?: number }): Promise<void>;
|
|
34
|
+
abstract evaluate(js: string): Promise<unknown>;
|
|
35
|
+
abstract getCookies(opts?: { domain?: string; url?: string }): Promise<BrowserCookie[]>;
|
|
36
|
+
abstract screenshot(options?: ScreenshotOptions): Promise<string>;
|
|
37
|
+
abstract tabs(): Promise<unknown[]>;
|
|
38
|
+
abstract closeTab(index?: number): Promise<void>;
|
|
39
|
+
abstract newTab(): Promise<void>;
|
|
40
|
+
abstract selectTab(index: number): Promise<void>;
|
|
41
|
+
|
|
42
|
+
// ── Shared DOM helper implementations ──
|
|
43
|
+
|
|
44
|
+
async click(ref: string): Promise<void> {
|
|
45
|
+
await this.evaluate(clickJs(ref));
|
|
46
|
+
}
|
|
47
|
+
|
|
48
|
+
async typeText(ref: string, text: string): Promise<void> {
|
|
49
|
+
await this.evaluate(typeTextJs(ref, text));
|
|
50
|
+
}
|
|
51
|
+
|
|
52
|
+
async pressKey(key: string): Promise<void> {
|
|
53
|
+
await this.evaluate(pressKeyJs(key));
|
|
54
|
+
}
|
|
55
|
+
|
|
56
|
+
async scrollTo(ref: string): Promise<unknown> {
|
|
57
|
+
return this.evaluate(scrollToRefJs(ref));
|
|
58
|
+
}
|
|
59
|
+
|
|
60
|
+
async getFormState(): Promise<Record<string, unknown>> {
|
|
61
|
+
return (await this.evaluate(getFormStateJs())) as Record<string, unknown>;
|
|
62
|
+
}
|
|
63
|
+
|
|
64
|
+
async scroll(direction: string = 'down', amount: number = 500): Promise<void> {
|
|
65
|
+
await this.evaluate(scrollJs(direction, amount));
|
|
66
|
+
}
|
|
67
|
+
|
|
68
|
+
async autoScroll(options?: { times?: number; delayMs?: number }): Promise<void> {
|
|
69
|
+
const times = options?.times ?? 3;
|
|
70
|
+
const delayMs = options?.delayMs ?? 2000;
|
|
71
|
+
await this.evaluate(autoScrollJs(times, delayMs));
|
|
72
|
+
}
|
|
73
|
+
|
|
74
|
+
async networkRequests(includeStatic: boolean = false): Promise<unknown[]> {
|
|
75
|
+
const result = await this.evaluate(networkRequestsJs(includeStatic));
|
|
76
|
+
return Array.isArray(result) ? result : [];
|
|
77
|
+
}
|
|
78
|
+
|
|
79
|
+
async consoleMessages(_level: string = 'info'): Promise<unknown[]> {
|
|
80
|
+
return [];
|
|
81
|
+
}
|
|
82
|
+
|
|
83
|
+
async wait(options: number | WaitOptions): Promise<void> {
|
|
84
|
+
if (typeof options === 'number') {
|
|
85
|
+
if (options >= 1) {
|
|
86
|
+
try {
|
|
87
|
+
const maxMs = options * 1000;
|
|
88
|
+
await this.evaluate(waitForDomStableJs(maxMs, Math.min(500, maxMs)));
|
|
89
|
+
return;
|
|
90
|
+
} catch {
|
|
91
|
+
// Fallback: fixed sleep
|
|
92
|
+
}
|
|
93
|
+
}
|
|
94
|
+
await new Promise(resolve => setTimeout(resolve, options * 1000));
|
|
95
|
+
return;
|
|
96
|
+
}
|
|
97
|
+
if (typeof options.time === 'number') {
|
|
98
|
+
await new Promise(resolve => setTimeout(resolve, options.time! * 1000));
|
|
99
|
+
return;
|
|
100
|
+
}
|
|
101
|
+
if (options.selector) {
|
|
102
|
+
const timeout = (options.timeout ?? 10) * 1000;
|
|
103
|
+
await this.evaluate(waitForSelectorJs(options.selector, timeout));
|
|
104
|
+
return;
|
|
105
|
+
}
|
|
106
|
+
if (options.text) {
|
|
107
|
+
const timeout = (options.timeout ?? 30) * 1000;
|
|
108
|
+
await this.evaluate(waitForTextJs(options.text, timeout));
|
|
109
|
+
}
|
|
110
|
+
}
|
|
111
|
+
|
|
112
|
+
async snapshot(opts: SnapshotOptions = {}): Promise<unknown> {
|
|
113
|
+
const snapshotJs = generateSnapshotJs({
|
|
114
|
+
viewportExpand: opts.viewportExpand ?? 800,
|
|
115
|
+
maxDepth: Math.max(1, Math.min(Number(opts.maxDepth) || 50, 200)),
|
|
116
|
+
interactiveOnly: opts.interactive ?? false,
|
|
117
|
+
maxTextLength: opts.maxTextLength ?? 120,
|
|
118
|
+
includeScrollInfo: true,
|
|
119
|
+
bboxDedup: true,
|
|
120
|
+
});
|
|
121
|
+
|
|
122
|
+
try {
|
|
123
|
+
return await this.evaluate(snapshotJs);
|
|
124
|
+
} catch {
|
|
125
|
+
return this._basicSnapshot(opts);
|
|
126
|
+
}
|
|
127
|
+
}
|
|
128
|
+
|
|
129
|
+
async getCurrentUrl(): Promise<string | null> {
|
|
130
|
+
if (this._lastUrl) return this._lastUrl;
|
|
131
|
+
try {
|
|
132
|
+
const current = await this.evaluate('window.location.href');
|
|
133
|
+
if (typeof current === 'string' && current) {
|
|
134
|
+
this._lastUrl = current;
|
|
135
|
+
return current;
|
|
136
|
+
}
|
|
137
|
+
} catch {
|
|
138
|
+
// Best-effort
|
|
139
|
+
}
|
|
140
|
+
return null;
|
|
141
|
+
}
|
|
142
|
+
|
|
143
|
+
async installInterceptor(pattern: string): Promise<void> {
|
|
144
|
+
const { generateInterceptorJs } = await import('../interceptor.js');
|
|
145
|
+
await this.evaluate(generateInterceptorJs(JSON.stringify(pattern), {
|
|
146
|
+
arrayName: '__opencli_xhr',
|
|
147
|
+
patchGuard: '__opencli_interceptor_patched',
|
|
148
|
+
}));
|
|
149
|
+
}
|
|
150
|
+
|
|
151
|
+
async getInterceptedRequests(): Promise<unknown[]> {
|
|
152
|
+
const { generateReadInterceptedJs } = await import('../interceptor.js');
|
|
153
|
+
const result = await this.evaluate(generateReadInterceptedJs('__opencli_xhr'));
|
|
154
|
+
return Array.isArray(result) ? result : [];
|
|
155
|
+
}
|
|
156
|
+
|
|
157
|
+
async waitForCapture(timeout: number = 10): Promise<void> {
|
|
158
|
+
const maxMs = timeout * 1000;
|
|
159
|
+
await this.evaluate(waitForCaptureJs(maxMs));
|
|
160
|
+
}
|
|
161
|
+
|
|
162
|
+
/** Fallback basic snapshot */
|
|
163
|
+
protected async _basicSnapshot(opts: Pick<SnapshotOptions, 'interactive' | 'compact' | 'maxDepth' | 'raw'> = {}): Promise<unknown> {
|
|
164
|
+
const maxDepth = Math.max(1, Math.min(Number(opts.maxDepth) || 50, 200));
|
|
165
|
+
const code = `
|
|
166
|
+
(async () => {
|
|
167
|
+
function buildTree(node, depth) {
|
|
168
|
+
if (depth > ${maxDepth}) return '';
|
|
169
|
+
const role = node.getAttribute?.('role') || node.tagName?.toLowerCase() || 'generic';
|
|
170
|
+
const name = node.getAttribute?.('aria-label') || node.getAttribute?.('alt') || node.textContent?.trim().slice(0, 80) || '';
|
|
171
|
+
const isInteractive = ['a', 'button', 'input', 'select', 'textarea'].includes(node.tagName?.toLowerCase()) || node.getAttribute?.('tabindex') != null;
|
|
172
|
+
|
|
173
|
+
${opts.interactive ? 'if (!isInteractive && !node.children?.length) return "";' : ''}
|
|
174
|
+
|
|
175
|
+
let indent = ' '.repeat(depth);
|
|
176
|
+
let line = indent + role;
|
|
177
|
+
if (name) line += ' "' + name.replace(/"/g, '\\\\\\"') + '"';
|
|
178
|
+
if (node.tagName?.toLowerCase() === 'a' && node.href) line += ' [' + node.href + ']';
|
|
179
|
+
if (node.tagName?.toLowerCase() === 'input') line += ' [' + (node.type || 'text') + ']';
|
|
180
|
+
|
|
181
|
+
let result = line + '\\n';
|
|
182
|
+
if (node.children) {
|
|
183
|
+
for (const child of node.children) {
|
|
184
|
+
result += buildTree(child, depth + 1);
|
|
185
|
+
}
|
|
186
|
+
}
|
|
187
|
+
return result;
|
|
188
|
+
}
|
|
189
|
+
return buildTree(document.body, 0);
|
|
190
|
+
})()
|
|
191
|
+
`;
|
|
192
|
+
const raw = await this.evaluate(code);
|
|
193
|
+
if (opts.raw) return raw;
|
|
194
|
+
if (typeof raw === 'string') return formatSnapshot(raw, opts);
|
|
195
|
+
return raw;
|
|
196
|
+
}
|
|
197
|
+
}
|
package/src/browser/cdp.ts
CHANGED
|
@@ -11,25 +11,14 @@
|
|
|
11
11
|
import { WebSocket, type RawData } from 'ws';
|
|
12
12
|
import { request as httpRequest } from 'node:http';
|
|
13
13
|
import { request as httpsRequest } from 'node:https';
|
|
14
|
-
import type { BrowserCookie, IPage, ScreenshotOptions
|
|
14
|
+
import type { BrowserCookie, IPage, ScreenshotOptions } from '../types.js';
|
|
15
15
|
import type { IBrowserFactory } from '../runtime.js';
|
|
16
16
|
import { wrapForEval } from './utils.js';
|
|
17
|
-
import { generateSnapshotJs, scrollToRefJs, getFormStateJs } from './dom-snapshot.js';
|
|
18
17
|
import { generateStealthJs } from './stealth.js';
|
|
19
|
-
import {
|
|
20
|
-
clickJs,
|
|
21
|
-
typeTextJs,
|
|
22
|
-
pressKeyJs,
|
|
23
|
-
waitForTextJs,
|
|
24
|
-
scrollJs,
|
|
25
|
-
autoScrollJs,
|
|
26
|
-
networkRequestsJs,
|
|
27
|
-
waitForDomStableJs,
|
|
28
|
-
waitForCaptureJs,
|
|
29
|
-
waitForSelectorJs,
|
|
30
|
-
} from './dom-helpers.js';
|
|
18
|
+
import { waitForDomStableJs } from './dom-helpers.js';
|
|
31
19
|
import { isRecord, saveBase64ToFile } from '../utils.js';
|
|
32
20
|
import { getAllElectronApps } from '../electron-apps.js';
|
|
21
|
+
import { BasePage } from './base-page.js';
|
|
33
22
|
|
|
34
23
|
export interface CDPTarget {
|
|
35
24
|
type?: string;
|
|
@@ -174,10 +163,11 @@ export class CDPBridge implements IBrowserFactory {
|
|
|
174
163
|
}
|
|
175
164
|
}
|
|
176
165
|
|
|
177
|
-
class CDPPage
|
|
166
|
+
class CDPPage extends BasePage {
|
|
178
167
|
private _pageEnabled = false;
|
|
179
|
-
private
|
|
180
|
-
|
|
168
|
+
constructor(private bridge: CDPBridge) {
|
|
169
|
+
super();
|
|
170
|
+
}
|
|
181
171
|
|
|
182
172
|
async goto(url: string, options?: { waitUntil?: 'load' | 'none'; settleMs?: number }): Promise<void> {
|
|
183
173
|
if (!this._pageEnabled) {
|
|
@@ -216,78 +206,6 @@ class CDPPage implements IPage {
|
|
|
216
206
|
: cookies;
|
|
217
207
|
}
|
|
218
208
|
|
|
219
|
-
async snapshot(opts: SnapshotOptions = {}): Promise<unknown> {
|
|
220
|
-
const snapshotJs = generateSnapshotJs({
|
|
221
|
-
viewportExpand: opts.viewportExpand ?? 800,
|
|
222
|
-
maxDepth: Math.max(1, Math.min(Number(opts.maxDepth) || 50, 200)),
|
|
223
|
-
interactiveOnly: opts.interactive ?? false,
|
|
224
|
-
maxTextLength: opts.maxTextLength ?? 120,
|
|
225
|
-
includeScrollInfo: true,
|
|
226
|
-
bboxDedup: true,
|
|
227
|
-
});
|
|
228
|
-
return this.evaluate(snapshotJs);
|
|
229
|
-
}
|
|
230
|
-
|
|
231
|
-
async click(ref: string): Promise<void> {
|
|
232
|
-
await this.evaluate(clickJs(ref));
|
|
233
|
-
}
|
|
234
|
-
|
|
235
|
-
async typeText(ref: string, text: string): Promise<void> {
|
|
236
|
-
await this.evaluate(typeTextJs(ref, text));
|
|
237
|
-
}
|
|
238
|
-
|
|
239
|
-
async pressKey(key: string): Promise<void> {
|
|
240
|
-
await this.evaluate(pressKeyJs(key));
|
|
241
|
-
}
|
|
242
|
-
|
|
243
|
-
async scrollTo(ref: string): Promise<unknown> {
|
|
244
|
-
return this.evaluate(scrollToRefJs(ref));
|
|
245
|
-
}
|
|
246
|
-
|
|
247
|
-
async getFormState(): Promise<Record<string, unknown>> {
|
|
248
|
-
return (await this.evaluate(getFormStateJs())) as Record<string, unknown>;
|
|
249
|
-
}
|
|
250
|
-
|
|
251
|
-
async wait(options: number | WaitOptions): Promise<void> {
|
|
252
|
-
if (typeof options === 'number') {
|
|
253
|
-
if (options >= 1) {
|
|
254
|
-
try {
|
|
255
|
-
const maxMs = options * 1000;
|
|
256
|
-
await this.evaluate(waitForDomStableJs(maxMs, Math.min(500, maxMs)));
|
|
257
|
-
return;
|
|
258
|
-
} catch {
|
|
259
|
-
// Fallback: fixed sleep
|
|
260
|
-
}
|
|
261
|
-
}
|
|
262
|
-
await new Promise((resolve) => setTimeout(resolve, options * 1000));
|
|
263
|
-
return;
|
|
264
|
-
}
|
|
265
|
-
if (typeof options.time === 'number') {
|
|
266
|
-
const waitTime = options.time;
|
|
267
|
-
await new Promise((resolve) => setTimeout(resolve, waitTime * 1000));
|
|
268
|
-
return;
|
|
269
|
-
}
|
|
270
|
-
if (options.selector) {
|
|
271
|
-
const timeout = (options.timeout ?? 10) * 1000;
|
|
272
|
-
await this.evaluate(waitForSelectorJs(options.selector, timeout));
|
|
273
|
-
return;
|
|
274
|
-
}
|
|
275
|
-
if (options.text) {
|
|
276
|
-
const timeout = (options.timeout ?? 30) * 1000;
|
|
277
|
-
await this.evaluate(waitForTextJs(options.text, timeout));
|
|
278
|
-
}
|
|
279
|
-
}
|
|
280
|
-
|
|
281
|
-
async scroll(direction: string = 'down', amount: number = 500): Promise<void> {
|
|
282
|
-
await this.evaluate(scrollJs(direction, amount));
|
|
283
|
-
}
|
|
284
|
-
|
|
285
|
-
async autoScroll(options?: { times?: number; delayMs?: number }): Promise<void> {
|
|
286
|
-
const times = options?.times ?? 3;
|
|
287
|
-
const delayMs = options?.delayMs ?? 2000;
|
|
288
|
-
await this.evaluate(autoScrollJs(times, delayMs));
|
|
289
|
-
}
|
|
290
|
-
|
|
291
209
|
async screenshot(options: ScreenshotOptions = {}): Promise<string> {
|
|
292
210
|
const result = await this.bridge.send('Page.captureScreenshot', {
|
|
293
211
|
format: options.format ?? 'png',
|
|
@@ -301,11 +219,6 @@ class CDPPage implements IPage {
|
|
|
301
219
|
return base64;
|
|
302
220
|
}
|
|
303
221
|
|
|
304
|
-
async networkRequests(includeStatic: boolean = false): Promise<unknown[]> {
|
|
305
|
-
const result = await this.evaluate(networkRequestsJs(includeStatic));
|
|
306
|
-
return Array.isArray(result) ? result : [];
|
|
307
|
-
}
|
|
308
|
-
|
|
309
222
|
async tabs(): Promise<unknown[]> {
|
|
310
223
|
return [];
|
|
311
224
|
}
|
|
@@ -321,43 +234,6 @@ class CDPPage implements IPage {
|
|
|
321
234
|
async selectTab(_index: number): Promise<void> {
|
|
322
235
|
// Not supported in direct CDP mode
|
|
323
236
|
}
|
|
324
|
-
|
|
325
|
-
async consoleMessages(_level?: string): Promise<unknown[]> {
|
|
326
|
-
return [];
|
|
327
|
-
}
|
|
328
|
-
|
|
329
|
-
async getCurrentUrl(): Promise<string | null> {
|
|
330
|
-
if (this._lastUrl) return this._lastUrl;
|
|
331
|
-
try {
|
|
332
|
-
const current = await this.evaluate('window.location.href');
|
|
333
|
-
if (typeof current === 'string' && current) {
|
|
334
|
-
this._lastUrl = current;
|
|
335
|
-
return current;
|
|
336
|
-
}
|
|
337
|
-
} catch {
|
|
338
|
-
// Best-effort: direct CDP sessions may not have a ready page yet.
|
|
339
|
-
}
|
|
340
|
-
return null;
|
|
341
|
-
}
|
|
342
|
-
|
|
343
|
-
async installInterceptor(pattern: string): Promise<void> {
|
|
344
|
-
const { generateInterceptorJs } = await import('../interceptor.js');
|
|
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
|
-
const result = await this.evaluate(generateReadInterceptedJs('__opencli_xhr'));
|
|
354
|
-
return Array.isArray(result) ? result : [];
|
|
355
|
-
}
|
|
356
|
-
|
|
357
|
-
async waitForCapture(timeout: number = 10): Promise<void> {
|
|
358
|
-
const maxMs = timeout * 1000;
|
|
359
|
-
await this.evaluate(waitForCaptureJs(maxMs));
|
|
360
|
-
}
|
|
361
237
|
}
|
|
362
238
|
|
|
363
239
|
function isCookie(value: unknown): value is BrowserCookie {
|
|
@@ -0,0 +1,103 @@
|
|
|
1
|
+
import { afterEach, beforeEach, describe, expect, it, vi } from 'vitest';
|
|
2
|
+
|
|
3
|
+
import {
|
|
4
|
+
fetchDaemonStatus,
|
|
5
|
+
isDaemonRunning,
|
|
6
|
+
isExtensionConnected,
|
|
7
|
+
requestDaemonShutdown,
|
|
8
|
+
} from './daemon-client.js';
|
|
9
|
+
|
|
10
|
+
describe('daemon-client', () => {
|
|
11
|
+
beforeEach(() => {
|
|
12
|
+
vi.stubGlobal('fetch', vi.fn());
|
|
13
|
+
});
|
|
14
|
+
|
|
15
|
+
afterEach(() => {
|
|
16
|
+
vi.restoreAllMocks();
|
|
17
|
+
});
|
|
18
|
+
|
|
19
|
+
it('fetchDaemonStatus sends the shared status request and returns parsed data', async () => {
|
|
20
|
+
const status = {
|
|
21
|
+
ok: true,
|
|
22
|
+
pid: 123,
|
|
23
|
+
uptime: 10,
|
|
24
|
+
extensionConnected: true,
|
|
25
|
+
extensionVersion: '1.2.3',
|
|
26
|
+
pending: 0,
|
|
27
|
+
lastCliRequestTime: Date.now(),
|
|
28
|
+
memoryMB: 32,
|
|
29
|
+
port: 19825,
|
|
30
|
+
};
|
|
31
|
+
const fetchMock = vi.mocked(fetch);
|
|
32
|
+
fetchMock.mockResolvedValue({
|
|
33
|
+
ok: true,
|
|
34
|
+
json: () => Promise.resolve(status),
|
|
35
|
+
} as Response);
|
|
36
|
+
|
|
37
|
+
await expect(fetchDaemonStatus()).resolves.toEqual(status);
|
|
38
|
+
expect(fetchMock).toHaveBeenCalledWith(
|
|
39
|
+
expect.stringMatching(/\/status$/),
|
|
40
|
+
expect.objectContaining({
|
|
41
|
+
headers: expect.objectContaining({ 'X-OpenCLI': '1' }),
|
|
42
|
+
}),
|
|
43
|
+
);
|
|
44
|
+
});
|
|
45
|
+
|
|
46
|
+
it('fetchDaemonStatus returns null on network failure', async () => {
|
|
47
|
+
vi.mocked(fetch).mockRejectedValue(new Error('ECONNREFUSED'));
|
|
48
|
+
|
|
49
|
+
await expect(fetchDaemonStatus()).resolves.toBeNull();
|
|
50
|
+
});
|
|
51
|
+
|
|
52
|
+
it('requestDaemonShutdown POSTs to the shared shutdown endpoint', async () => {
|
|
53
|
+
const fetchMock = vi.mocked(fetch);
|
|
54
|
+
fetchMock.mockResolvedValue({ ok: true } as Response);
|
|
55
|
+
|
|
56
|
+
await expect(requestDaemonShutdown()).resolves.toBe(true);
|
|
57
|
+
expect(fetchMock).toHaveBeenCalledWith(
|
|
58
|
+
expect.stringMatching(/\/shutdown$/),
|
|
59
|
+
expect.objectContaining({
|
|
60
|
+
method: 'POST',
|
|
61
|
+
headers: expect.objectContaining({ 'X-OpenCLI': '1' }),
|
|
62
|
+
}),
|
|
63
|
+
);
|
|
64
|
+
});
|
|
65
|
+
|
|
66
|
+
it('isDaemonRunning reflects shared status availability', async () => {
|
|
67
|
+
vi.mocked(fetch).mockResolvedValue({
|
|
68
|
+
ok: true,
|
|
69
|
+
json: () =>
|
|
70
|
+
Promise.resolve({
|
|
71
|
+
ok: true,
|
|
72
|
+
pid: 123,
|
|
73
|
+
uptime: 10,
|
|
74
|
+
extensionConnected: false,
|
|
75
|
+
pending: 0,
|
|
76
|
+
lastCliRequestTime: Date.now(),
|
|
77
|
+
memoryMB: 16,
|
|
78
|
+
port: 19825,
|
|
79
|
+
}),
|
|
80
|
+
} as Response);
|
|
81
|
+
|
|
82
|
+
await expect(isDaemonRunning()).resolves.toBe(true);
|
|
83
|
+
});
|
|
84
|
+
|
|
85
|
+
it('isExtensionConnected reflects shared status payload', async () => {
|
|
86
|
+
vi.mocked(fetch).mockResolvedValue({
|
|
87
|
+
ok: true,
|
|
88
|
+
json: () =>
|
|
89
|
+
Promise.resolve({
|
|
90
|
+
ok: true,
|
|
91
|
+
pid: 123,
|
|
92
|
+
uptime: 10,
|
|
93
|
+
extensionConnected: false,
|
|
94
|
+
pending: 0,
|
|
95
|
+
lastCliRequestTime: Date.now(),
|
|
96
|
+
memoryMB: 16,
|
|
97
|
+
port: 19825,
|
|
98
|
+
}),
|
|
99
|
+
} as Response);
|
|
100
|
+
|
|
101
|
+
await expect(isExtensionConnected()).resolves.toBe(false);
|
|
102
|
+
});
|
|
103
|
+
});
|