@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.
Files changed (199) hide show
  1. package/CHANGELOG.md +29 -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/extension-manifest-regression.test.js +1 -0
  82. package/dist/output.js +28 -0
  83. package/dist/output.test.js +15 -0
  84. package/dist/pipeline/executor.js +2 -7
  85. package/dist/pipeline/steps/browser.js +1 -1
  86. package/dist/pipeline/template.js +25 -3
  87. package/dist/record.d.ts +50 -0
  88. package/dist/record.js +298 -57
  89. package/dist/record.test.d.ts +1 -0
  90. package/dist/record.test.js +293 -0
  91. package/dist/registry.d.ts +2 -0
  92. package/dist/registry.js +1 -0
  93. package/dist/registry.test.js +10 -0
  94. package/dist/runtime.js +3 -3
  95. package/dist/snapshotFormatter.d.ts +1 -1
  96. package/dist/snapshotFormatter.js +4 -4
  97. package/dist/snapshotFormatter.test.d.ts +1 -1
  98. package/dist/snapshotFormatter.test.js +2 -2
  99. package/dist/types.d.ts +3 -1
  100. package/dist/types.js +1 -1
  101. package/docs/.vitepress/config.mts +2 -0
  102. package/docs/adapters/browser/amazon.md +53 -0
  103. package/docs/adapters/browser/gemini.md +72 -0
  104. package/docs/adapters/browser/notebooklm.md +5 -5
  105. package/docs/adapters/index.md +3 -1
  106. package/extension/dist/background.js +614 -794
  107. package/extension/manifest.json +2 -1
  108. package/extension/src/background.test.ts +7 -163
  109. package/extension/src/background.ts +7 -156
  110. package/extension/src/cdp.test.ts +75 -0
  111. package/extension/src/cdp.ts +77 -3
  112. package/extension/src/protocol.ts +1 -5
  113. package/package.json +1 -1
  114. package/skills/opencli-explorer/SKILL.md +847 -0
  115. package/skills/opencli-oneshot/SKILL.md +216 -0
  116. package/skills/opencli-usage/SKILL.md +71 -0
  117. package/skills/opencli-usage/browser.md +429 -0
  118. package/skills/opencli-usage/desktop.md +118 -0
  119. package/skills/opencli-usage/plugins.md +82 -0
  120. package/skills/opencli-usage/public-api.md +149 -0
  121. package/src/browser/base-page.ts +197 -0
  122. package/src/browser/cdp.ts +7 -131
  123. package/src/browser/daemon-client.ts +3 -14
  124. package/src/browser/discover.ts +1 -4
  125. package/src/browser/errors.ts +22 -0
  126. package/src/browser/index.ts +1 -1
  127. package/src/browser/page.ts +13 -212
  128. package/src/browser/tabs.ts +5 -5
  129. package/src/browser.test.ts +15 -15
  130. package/src/clis/amazon/bestsellers.test.ts +22 -0
  131. package/src/clis/amazon/bestsellers.ts +180 -0
  132. package/src/clis/amazon/discussion.test.ts +38 -0
  133. package/src/clis/amazon/discussion.ts +131 -0
  134. package/src/clis/amazon/offer.test.ts +35 -0
  135. package/src/clis/amazon/offer.ts +185 -0
  136. package/src/clis/amazon/product.test.ts +26 -0
  137. package/src/clis/amazon/product.ts +131 -0
  138. package/src/clis/amazon/search.test.ts +24 -0
  139. package/src/clis/amazon/search.ts +128 -0
  140. package/src/clis/amazon/shared.test.ts +37 -0
  141. package/src/clis/amazon/shared.ts +316 -0
  142. package/src/clis/gemini/ask.ts +46 -0
  143. package/src/clis/gemini/image.ts +115 -0
  144. package/src/clis/gemini/new.ts +22 -0
  145. package/src/clis/gemini/utils.test.ts +36 -0
  146. package/src/clis/gemini/utils.ts +523 -0
  147. package/src/clis/notebooklm/compat.test.ts +3 -3
  148. package/src/clis/notebooklm/current.ts +2 -3
  149. package/src/clis/notebooklm/get.ts +1 -3
  150. package/src/clis/notebooklm/history.ts +1 -3
  151. package/src/clis/notebooklm/note-list.ts +1 -3
  152. package/src/clis/notebooklm/notes-get.ts +1 -3
  153. package/src/clis/notebooklm/open.test.ts +78 -0
  154. package/src/clis/notebooklm/open.ts +61 -0
  155. package/src/clis/notebooklm/source-fulltext.ts +1 -3
  156. package/src/clis/notebooklm/source-get.ts +1 -3
  157. package/src/clis/notebooklm/source-guide.ts +1 -3
  158. package/src/clis/notebooklm/source-list.ts +1 -3
  159. package/src/clis/notebooklm/status.ts +1 -2
  160. package/src/clis/notebooklm/summary.ts +1 -3
  161. package/src/clis/notebooklm/utils.ts +29 -20
  162. package/src/clis/xiaohongshu/creator-note-detail.test.ts +11 -11
  163. package/src/clis/xiaohongshu/creator-notes-summary.test.ts +6 -6
  164. package/src/clis/xiaohongshu/creator-notes.test.ts +22 -22
  165. package/src/commanderAdapter.test.ts +47 -0
  166. package/src/commanderAdapter.ts +7 -3
  167. package/src/commands/daemon.test.ts +1 -1
  168. package/src/commands/daemon.ts +1 -1
  169. package/src/doctor.ts +7 -8
  170. package/src/explore.ts +1 -1
  171. package/src/extension-manifest-regression.test.ts +1 -0
  172. package/src/output.test.ts +17 -0
  173. package/src/output.ts +27 -0
  174. package/src/pipeline/executor.ts +2 -7
  175. package/src/pipeline/steps/browser.ts +1 -1
  176. package/src/pipeline/template.ts +27 -4
  177. package/src/record.test.ts +362 -0
  178. package/src/record.ts +341 -62
  179. package/src/registry.test.ts +12 -0
  180. package/src/registry.ts +3 -0
  181. package/src/runtime.ts +3 -3
  182. package/src/snapshotFormatter.test.ts +2 -2
  183. package/src/snapshotFormatter.ts +4 -4
  184. package/src/types.ts +3 -1
  185. package/.agents/skills/cross-project-adapter-migration/SKILL.md +0 -249
  186. package/.agents/workflows/cross-project-adapter-migration.md +0 -54
  187. package/SKILL.md +0 -879
  188. package/dist/clis/notebooklm/bind-current.js +0 -29
  189. package/dist/clis/notebooklm/bind-current.test.d.ts +0 -1
  190. package/dist/clis/notebooklm/bind-current.test.js +0 -35
  191. package/dist/clis/notebooklm/binding.test.js +0 -44
  192. package/src/clis/notebooklm/bind-current.test.ts +0 -43
  193. package/src/clis/notebooklm/bind-current.ts +0 -36
  194. package/src/clis/notebooklm/binding.test.ts +0 -53
  195. /package/dist/browser/{mcp.d.ts → bridge.d.ts} +0 -0
  196. /package/dist/browser/{mcp.js → bridge.js} +0 -0
  197. /package/dist/clis/{notebooklm/bind-current.d.ts → amazon/bestsellers.test.d.ts} +0 -0
  198. /package/dist/clis/{notebooklm/binding.test.d.ts → amazon/discussion.test.d.ts} +0 -0
  199. /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
- | **notebooklm** | `status` `list` `current` `get` `metadata` `bind-current` `use` `source-list` `source-get` `source-fulltext` `source-guide` `history` `note-list` `notes-list` `notes-get` `summary` |
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` `current` `get` `metadata` `bind-current` `use` `source-list` `source-get` `source-fulltext` `source-guide` `history` `note-list` `notes-list` `notes-get` `summary` | 浏览器 |
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
+ }
@@ -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 { clickJs, typeTextJs, pressKeyJs, waitForTextJs, scrollJs, autoScrollJs, networkRequestsJs, waitForDomStableJs, waitForCaptureJs, waitForSelectorJs, } from './dom-helpers.js';
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' | 'bind-current';
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
- const errMsg = result.error ?? '';
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 — 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
  import { isDaemonRunning } from './daemon-client.js';
8
5
  export { isDaemonRunning };
@@ -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
  import { DEFAULT_DAEMON_PORT } from '../constants.js';
8
5
  import { isDaemonRunning } from './daemon-client.js';
@@ -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;
@@ -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':
@@ -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 './mcp.js';
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';
@@ -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 './mcp.js';
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';