@jackwener/opencli 1.5.8 → 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.
Files changed (194) hide show
  1. package/CHANGELOG.md +21 -0
  2. package/README.md +17 -1
  3. package/README.zh-CN.md +17 -1
  4. package/dist/browser/base-page.d.ts +48 -0
  5. package/dist/browser/base-page.js +160 -0
  6. package/dist/browser/cdp.js +4 -106
  7. package/dist/browser/daemon-client.d.ts +1 -7
  8. package/dist/browser/daemon-client.js +2 -9
  9. package/dist/browser/discover.d.ts +1 -4
  10. package/dist/browser/discover.js +1 -4
  11. package/dist/browser/errors.d.ts +4 -0
  12. package/dist/browser/errors.js +20 -0
  13. package/dist/browser/index.d.ts +1 -1
  14. package/dist/browser/index.js +1 -1
  15. package/dist/browser/page.d.ts +6 -35
  16. package/dist/browser/page.js +10 -189
  17. package/dist/browser/tabs.js +5 -5
  18. package/dist/browser.test.js +15 -15
  19. package/dist/cli-manifest.json +294 -22
  20. package/dist/clis/amazon/bestsellers.d.ts +21 -0
  21. package/dist/clis/amazon/bestsellers.js +130 -0
  22. package/dist/clis/amazon/bestsellers.test.js +20 -0
  23. package/dist/clis/amazon/discussion.d.ts +20 -0
  24. package/dist/clis/amazon/discussion.js +91 -0
  25. package/dist/clis/amazon/discussion.test.js +36 -0
  26. package/dist/clis/amazon/offer.d.ts +23 -0
  27. package/dist/clis/amazon/offer.js +140 -0
  28. package/dist/clis/amazon/offer.test.d.ts +1 -0
  29. package/dist/clis/amazon/offer.test.js +29 -0
  30. package/dist/clis/amazon/product.d.ts +18 -0
  31. package/dist/clis/amazon/product.js +92 -0
  32. package/dist/clis/amazon/product.test.d.ts +1 -0
  33. package/dist/clis/amazon/product.test.js +24 -0
  34. package/dist/clis/amazon/search.d.ts +18 -0
  35. package/dist/clis/amazon/search.js +87 -0
  36. package/dist/clis/amazon/search.test.d.ts +1 -0
  37. package/dist/clis/amazon/search.test.js +22 -0
  38. package/dist/clis/amazon/shared.d.ts +64 -0
  39. package/dist/clis/amazon/shared.js +255 -0
  40. package/dist/clis/amazon/shared.test.d.ts +1 -0
  41. package/dist/clis/amazon/shared.test.js +33 -0
  42. package/dist/clis/gemini/ask.d.ts +1 -0
  43. package/dist/clis/gemini/ask.js +40 -0
  44. package/dist/clis/gemini/image.d.ts +1 -0
  45. package/dist/clis/gemini/image.js +105 -0
  46. package/dist/clis/gemini/new.d.ts +1 -0
  47. package/dist/clis/gemini/new.js +20 -0
  48. package/dist/clis/gemini/utils.d.ts +34 -0
  49. package/dist/clis/gemini/utils.js +463 -0
  50. package/dist/clis/gemini/utils.test.d.ts +1 -0
  51. package/dist/clis/gemini/utils.test.js +31 -0
  52. package/dist/clis/notebooklm/compat.test.d.ts +1 -1
  53. package/dist/clis/notebooklm/compat.test.js +3 -3
  54. package/dist/clis/notebooklm/current.js +2 -3
  55. package/dist/clis/notebooklm/get.js +2 -3
  56. package/dist/clis/notebooklm/history.js +2 -3
  57. package/dist/clis/notebooklm/note-list.js +2 -3
  58. package/dist/clis/notebooklm/notes-get.js +2 -3
  59. package/dist/clis/notebooklm/open.d.ts +1 -0
  60. package/dist/clis/notebooklm/open.js +41 -0
  61. package/dist/clis/notebooklm/open.test.d.ts +1 -0
  62. package/dist/clis/notebooklm/open.test.js +63 -0
  63. package/dist/clis/notebooklm/source-fulltext.js +2 -3
  64. package/dist/clis/notebooklm/source-get.js +2 -3
  65. package/dist/clis/notebooklm/source-guide.js +2 -3
  66. package/dist/clis/notebooklm/source-list.js +2 -3
  67. package/dist/clis/notebooklm/status.js +1 -2
  68. package/dist/clis/notebooklm/summary.js +2 -3
  69. package/dist/clis/notebooklm/utils.d.ts +2 -1
  70. package/dist/clis/notebooklm/utils.js +20 -21
  71. package/dist/clis/xiaohongshu/creator-note-detail.test.js +11 -11
  72. package/dist/clis/xiaohongshu/creator-notes-summary.test.js +6 -6
  73. package/dist/clis/xiaohongshu/creator-notes.test.js +22 -22
  74. package/dist/commanderAdapter.js +6 -3
  75. package/dist/commanderAdapter.test.js +33 -0
  76. package/dist/commands/daemon.js +1 -1
  77. package/dist/commands/daemon.test.js +1 -1
  78. package/dist/doctor.d.ts +1 -2
  79. package/dist/doctor.js +7 -8
  80. package/dist/explore.js +1 -1
  81. package/dist/output.js +28 -0
  82. package/dist/output.test.js +15 -0
  83. package/dist/pipeline/executor.js +2 -7
  84. package/dist/pipeline/steps/browser.js +1 -1
  85. package/dist/pipeline/template.js +25 -3
  86. package/dist/record.d.ts +50 -0
  87. package/dist/record.js +298 -57
  88. package/dist/record.test.d.ts +1 -0
  89. package/dist/record.test.js +293 -0
  90. package/dist/registry.d.ts +2 -0
  91. package/dist/registry.js +1 -0
  92. package/dist/registry.test.js +10 -0
  93. package/dist/runtime.js +3 -3
  94. package/dist/snapshotFormatter.d.ts +1 -1
  95. package/dist/snapshotFormatter.js +4 -4
  96. package/dist/snapshotFormatter.test.d.ts +1 -1
  97. package/dist/snapshotFormatter.test.js +2 -2
  98. package/dist/types.d.ts +3 -1
  99. package/dist/types.js +1 -1
  100. package/docs/.vitepress/config.mts +2 -0
  101. package/docs/adapters/browser/amazon.md +53 -0
  102. package/docs/adapters/browser/gemini.md +72 -0
  103. package/docs/adapters/browser/notebooklm.md +5 -5
  104. package/docs/adapters/index.md +3 -1
  105. package/extension/dist/background.js +5 -143
  106. package/extension/src/background.test.ts +7 -163
  107. package/extension/src/background.ts +7 -157
  108. package/extension/src/protocol.ts +1 -5
  109. package/package.json +1 -1
  110. package/skills/opencli-explorer/SKILL.md +847 -0
  111. package/skills/opencli-oneshot/SKILL.md +216 -0
  112. package/skills/opencli-usage/SKILL.md +71 -0
  113. package/skills/opencli-usage/browser.md +429 -0
  114. package/skills/opencli-usage/desktop.md +118 -0
  115. package/skills/opencli-usage/plugins.md +82 -0
  116. package/skills/opencli-usage/public-api.md +149 -0
  117. package/src/browser/base-page.ts +197 -0
  118. package/src/browser/cdp.ts +7 -131
  119. package/src/browser/daemon-client.ts +3 -14
  120. package/src/browser/discover.ts +1 -4
  121. package/src/browser/errors.ts +22 -0
  122. package/src/browser/index.ts +1 -1
  123. package/src/browser/page.ts +13 -212
  124. package/src/browser/tabs.ts +5 -5
  125. package/src/browser.test.ts +15 -15
  126. package/src/clis/amazon/bestsellers.test.ts +22 -0
  127. package/src/clis/amazon/bestsellers.ts +180 -0
  128. package/src/clis/amazon/discussion.test.ts +38 -0
  129. package/src/clis/amazon/discussion.ts +131 -0
  130. package/src/clis/amazon/offer.test.ts +35 -0
  131. package/src/clis/amazon/offer.ts +185 -0
  132. package/src/clis/amazon/product.test.ts +26 -0
  133. package/src/clis/amazon/product.ts +131 -0
  134. package/src/clis/amazon/search.test.ts +24 -0
  135. package/src/clis/amazon/search.ts +128 -0
  136. package/src/clis/amazon/shared.test.ts +37 -0
  137. package/src/clis/amazon/shared.ts +316 -0
  138. package/src/clis/gemini/ask.ts +46 -0
  139. package/src/clis/gemini/image.ts +115 -0
  140. package/src/clis/gemini/new.ts +22 -0
  141. package/src/clis/gemini/utils.test.ts +36 -0
  142. package/src/clis/gemini/utils.ts +523 -0
  143. package/src/clis/notebooklm/compat.test.ts +3 -3
  144. package/src/clis/notebooklm/current.ts +2 -3
  145. package/src/clis/notebooklm/get.ts +1 -3
  146. package/src/clis/notebooklm/history.ts +1 -3
  147. package/src/clis/notebooklm/note-list.ts +1 -3
  148. package/src/clis/notebooklm/notes-get.ts +1 -3
  149. package/src/clis/notebooklm/open.test.ts +78 -0
  150. package/src/clis/notebooklm/open.ts +61 -0
  151. package/src/clis/notebooklm/source-fulltext.ts +1 -3
  152. package/src/clis/notebooklm/source-get.ts +1 -3
  153. package/src/clis/notebooklm/source-guide.ts +1 -3
  154. package/src/clis/notebooklm/source-list.ts +1 -3
  155. package/src/clis/notebooklm/status.ts +1 -2
  156. package/src/clis/notebooklm/summary.ts +1 -3
  157. package/src/clis/notebooklm/utils.ts +29 -20
  158. package/src/clis/xiaohongshu/creator-note-detail.test.ts +11 -11
  159. package/src/clis/xiaohongshu/creator-notes-summary.test.ts +6 -6
  160. package/src/clis/xiaohongshu/creator-notes.test.ts +22 -22
  161. package/src/commanderAdapter.test.ts +47 -0
  162. package/src/commanderAdapter.ts +7 -3
  163. package/src/commands/daemon.test.ts +1 -1
  164. package/src/commands/daemon.ts +1 -1
  165. package/src/doctor.ts +7 -8
  166. package/src/explore.ts +1 -1
  167. package/src/output.test.ts +17 -0
  168. package/src/output.ts +27 -0
  169. package/src/pipeline/executor.ts +2 -7
  170. package/src/pipeline/steps/browser.ts +1 -1
  171. package/src/pipeline/template.ts +27 -4
  172. package/src/record.test.ts +362 -0
  173. package/src/record.ts +341 -62
  174. package/src/registry.test.ts +12 -0
  175. package/src/registry.ts +3 -0
  176. package/src/runtime.ts +3 -3
  177. package/src/snapshotFormatter.test.ts +2 -2
  178. package/src/snapshotFormatter.ts +4 -4
  179. package/src/types.ts +3 -1
  180. package/.agents/skills/cross-project-adapter-migration/SKILL.md +0 -249
  181. package/.agents/workflows/cross-project-adapter-migration.md +0 -54
  182. package/SKILL.md +0 -879
  183. package/dist/clis/notebooklm/bind-current.js +0 -29
  184. package/dist/clis/notebooklm/bind-current.test.d.ts +0 -1
  185. package/dist/clis/notebooklm/bind-current.test.js +0 -35
  186. package/dist/clis/notebooklm/binding.test.js +0 -44
  187. package/src/clis/notebooklm/bind-current.test.ts +0 -43
  188. package/src/clis/notebooklm/bind-current.ts +0 -36
  189. package/src/clis/notebooklm/binding.test.ts +0 -53
  190. /package/dist/browser/{mcp.d.ts → bridge.d.ts} +0 -0
  191. /package/dist/browser/{mcp.js → bridge.js} +0 -0
  192. /package/dist/clis/{notebooklm/bind-current.d.ts → amazon/bestsellers.test.d.ts} +0 -0
  193. /package/dist/clis/{notebooklm/binding.test.d.ts → amazon/discussion.test.d.ts} +0 -0
  194. /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
+ }
@@ -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, SnapshotOptions, WaitOptions } from '../types.js';
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 implements IPage {
166
+ class CDPPage extends BasePage {
178
167
  private _pageEnabled = false;
179
- private _lastUrl: string | null = null;
180
- constructor(private bridge: CDPBridge) {}
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 {
@@ -7,6 +7,7 @@
7
7
  import { DEFAULT_DAEMON_PORT } from '../constants.js';
8
8
  import type { BrowserSessionInfo } from '../types.js';
9
9
  import { sleep } from '../utils.js';
10
+ import { isTransientBrowserError } from './errors.js';
10
11
 
11
12
  const DAEMON_PORT = parseInt(process.env.OPENCLI_DAEMON_PORT ?? String(DEFAULT_DAEMON_PORT), 10);
12
13
  const DAEMON_URL = `http://127.0.0.1:${DAEMON_PORT}`;
@@ -19,7 +20,7 @@ function generateId(): string {
19
20
 
20
21
  export interface DaemonCommand {
21
22
  id: string;
22
- action: 'exec' | 'navigate' | 'tabs' | 'cookies' | 'screenshot' | 'close-window' | 'sessions' | 'set-file-input' | 'bind-current';
23
+ action: 'exec' | 'navigate' | 'tabs' | 'cookies' | 'screenshot' | 'close-window' | 'sessions' | 'set-file-input';
23
24
  tabId?: number;
24
25
  code?: string;
25
26
  workspace?: string;
@@ -27,8 +28,6 @@ export interface DaemonCommand {
27
28
  op?: string;
28
29
  index?: number;
29
30
  domain?: string;
30
- matchDomain?: string;
31
- matchPathPrefix?: string;
32
31
  format?: 'png' | 'jpeg';
33
32
  quality?: number;
34
33
  fullPage?: boolean;
@@ -114,12 +113,7 @@ export async function sendCommand(
114
113
 
115
114
  if (!result.ok) {
116
115
  // Check if error is a transient extension issue worth retrying
117
- const errMsg = result.error ?? '';
118
- const isTransient = errMsg.includes('Extension disconnected')
119
- || errMsg.includes('Extension not connected')
120
- || errMsg.includes('attach failed')
121
- || errMsg.includes('no longer exists');
122
- if (isTransient && attempt < maxRetries) {
116
+ if (isTransientBrowserError(new Error(result.error ?? '')) && attempt < maxRetries) {
123
117
  // Longer delay for extension recovery (service worker restart)
124
118
  await sleep(1500);
125
119
  continue;
@@ -146,8 +140,3 @@ export async function listSessions(): Promise<BrowserSessionInfo[]> {
146
140
  const result = await sendCommand('sessions');
147
141
  return Array.isArray(result) ? result : [];
148
142
  }
149
-
150
- export async function bindCurrentTab(workspace: string, opts: { matchDomain?: string; matchPathPrefix?: string } = {}): Promise<unknown> {
151
- return sendCommand('bind-current', { workspace, ...opts });
152
- }
153
-
@@ -1,8 +1,5 @@
1
1
  /**
2
- * Daemon discovery — simplified from MCP server path discovery.
3
- *
4
- * Only needs to check if the daemon is running. No more file system
5
- * scanning for @playwright/mcp locations.
2
+ * Daemon discovery — checks if the daemon is running.
6
3
  */
7
4
 
8
5
  import { DEFAULT_DAEMON_PORT } from '../constants.js';
@@ -8,6 +8,28 @@
8
8
  import { BrowserConnectError, type BrowserConnectKind } from '../errors.js';
9
9
  import { DEFAULT_DAEMON_PORT } from '../constants.js';
10
10
 
11
+ /**
12
+ * Transient browser error patterns — shared across daemon-client, pipeline executor,
13
+ * and page retry logic. These errors indicate temporary conditions (extension restart,
14
+ * service worker cycle, tab navigation) that are worth retrying.
15
+ */
16
+ const TRANSIENT_ERROR_PATTERNS = [
17
+ 'Extension disconnected',
18
+ 'Extension not connected',
19
+ 'attach failed',
20
+ 'no longer exists',
21
+ 'CDP connection',
22
+ 'Daemon command failed',
23
+ ] as const;
24
+
25
+ /**
26
+ * Check if an error message indicates a transient browser error worth retrying.
27
+ */
28
+ export function isTransientBrowserError(err: unknown): boolean {
29
+ const msg = err instanceof Error ? err.message : String(err);
30
+ return TRANSIENT_ERROR_PATTERNS.some(pattern => msg.includes(pattern));
31
+ }
32
+
11
33
  // Re-export so callers don't need to import from two places
12
34
  export type ConnectFailureKind = BrowserConnectKind;
13
35
 
@@ -6,7 +6,7 @@
6
6
  */
7
7
 
8
8
  export { Page } from './page.js';
9
- export { BrowserBridge } from './mcp.js';
9
+ export { BrowserBridge } from './bridge.js';
10
10
  export { CDPBridge } from './cdp.js';
11
11
  export { isDaemonRunning } from './daemon-client.js';
12
12
  export { generateSnapshotJs, scrollToRefJs, getFormStateJs } from './dom-snapshot.js';