@jackwener/opencli 1.0.3 → 1.0.4

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 (190) hide show
  1. package/.github/workflows/build-extension.yml +21 -3
  2. package/.github/workflows/docs.yml +52 -0
  3. package/README.md +28 -28
  4. package/README.zh-CN.md +28 -28
  5. package/dist/browser/cdp.d.ts +16 -1
  6. package/dist/browser/cdp.js +124 -80
  7. package/dist/browser/daemon-client.d.ts +3 -1
  8. package/dist/browser/daemon-client.js +4 -0
  9. package/dist/browser/dom-helpers.d.ts +20 -0
  10. package/dist/browser/dom-helpers.js +109 -0
  11. package/dist/browser/mcp.d.ts +1 -0
  12. package/dist/browser/mcp.js +10 -5
  13. package/dist/browser/page.d.ts +7 -0
  14. package/dist/browser/page.js +37 -100
  15. package/dist/browser.test.js +7 -0
  16. package/dist/build-manifest.js +3 -1
  17. package/dist/build-manifest.test.js +34 -0
  18. package/dist/capabilityRouting.d.ts +2 -0
  19. package/dist/capabilityRouting.js +30 -0
  20. package/dist/capabilityRouting.test.d.ts +1 -0
  21. package/dist/capabilityRouting.test.js +42 -0
  22. package/dist/chaoxing.test.js +11 -4
  23. package/dist/cli-manifest.json +635 -1
  24. package/dist/cli.js +45 -8
  25. package/dist/clis/antigravity/serve.d.ts +14 -0
  26. package/dist/clis/antigravity/serve.js +263 -0
  27. package/dist/clis/bilibili/download.js +4 -14
  28. package/dist/clis/boss/resume.d.ts +1 -0
  29. package/dist/clis/boss/resume.js +249 -0
  30. package/dist/clis/hf/top.d.ts +1 -0
  31. package/dist/clis/hf/top.js +119 -0
  32. package/dist/clis/jike/comment.d.ts +1 -0
  33. package/dist/clis/jike/comment.js +107 -0
  34. package/dist/clis/jike/create.d.ts +1 -0
  35. package/dist/clis/jike/create.js +106 -0
  36. package/dist/clis/jike/feed.d.ts +1 -0
  37. package/dist/clis/jike/feed.js +67 -0
  38. package/dist/clis/jike/like.d.ts +1 -0
  39. package/dist/clis/jike/like.js +61 -0
  40. package/dist/clis/jike/notifications.d.ts +1 -0
  41. package/dist/clis/jike/notifications.js +169 -0
  42. package/dist/clis/jike/post.yaml +58 -0
  43. package/dist/clis/jike/repost.d.ts +1 -0
  44. package/dist/clis/jike/repost.js +103 -0
  45. package/dist/clis/jike/search.d.ts +1 -0
  46. package/dist/clis/jike/search.js +67 -0
  47. package/dist/clis/jike/shared.d.ts +19 -0
  48. package/dist/clis/jike/shared.js +25 -0
  49. package/dist/clis/jike/topic.yaml +52 -0
  50. package/dist/clis/jike/user.yaml +51 -0
  51. package/dist/clis/smzdm/search.js +28 -39
  52. package/dist/clis/stackoverflow/bounties.yaml +29 -0
  53. package/dist/clis/stackoverflow/hot.yaml +28 -0
  54. package/dist/clis/stackoverflow/search.yaml +32 -0
  55. package/dist/clis/stackoverflow/unanswered.yaml +28 -0
  56. package/dist/clis/twitter/download.js +6 -16
  57. package/dist/clis/xiaohongshu/download.js +3 -3
  58. package/dist/clis/zhihu/download.js +3 -3
  59. package/dist/doctor.d.ts +7 -0
  60. package/dist/doctor.js +16 -0
  61. package/dist/download/index.d.ts +12 -8
  62. package/dist/download/index.js +11 -3
  63. package/dist/download/index.test.d.ts +1 -0
  64. package/dist/download/index.test.js +14 -0
  65. package/dist/engine.js +5 -5
  66. package/dist/explore.d.ts +1 -0
  67. package/dist/explore.js +3 -3
  68. package/dist/generate.js +1 -0
  69. package/dist/interceptor.js +3 -2
  70. package/dist/output.d.ts +1 -0
  71. package/dist/output.js +3 -1
  72. package/dist/pipeline/executor.test.js +1 -0
  73. package/dist/pipeline/steps/download.js +14 -18
  74. package/dist/registry.d.ts +1 -0
  75. package/dist/registry.js +5 -2
  76. package/dist/runtime.d.ts +4 -1
  77. package/dist/runtime.js +2 -2
  78. package/dist/types.d.ts +12 -0
  79. package/dist/verify.d.ts +6 -1
  80. package/dist/verify.js +54 -2
  81. package/docs/.vitepress/config.mts +193 -0
  82. package/docs/adapters/browser/apple-podcasts.md +28 -0
  83. package/docs/adapters/browser/bbc.md +26 -0
  84. package/docs/adapters/browser/bilibili.md +38 -0
  85. package/docs/adapters/browser/boss.md +28 -0
  86. package/docs/adapters/browser/coupang.md +28 -0
  87. package/docs/adapters/browser/ctrip.md +27 -0
  88. package/docs/adapters/browser/github.md +26 -0
  89. package/docs/adapters/browser/hackernews.md +26 -0
  90. package/docs/adapters/browser/linkedin.md +27 -0
  91. package/docs/adapters/browser/reddit.md +41 -0
  92. package/docs/adapters/browser/reuters.md +27 -0
  93. package/docs/adapters/browser/smzdm.md +27 -0
  94. package/docs/adapters/browser/twitter.md +47 -0
  95. package/docs/adapters/browser/v2ex.md +32 -0
  96. package/docs/adapters/browser/weibo.md +27 -0
  97. package/docs/adapters/browser/xiaohongshu.md +32 -0
  98. package/docs/adapters/browser/xiaoyuzhou.md +28 -0
  99. package/docs/adapters/browser/xueqiu.md +32 -0
  100. package/docs/adapters/browser/yahoo-finance.md +26 -0
  101. package/docs/adapters/browser/youtube.md +29 -0
  102. package/docs/adapters/browser/zhihu.md +30 -0
  103. package/docs/adapters/desktop/antigravity.md +46 -0
  104. package/docs/adapters/desktop/chatgpt.md +43 -0
  105. package/docs/adapters/desktop/chatwise.md +38 -0
  106. package/docs/adapters/desktop/codex.md +32 -0
  107. package/docs/adapters/desktop/cursor.md +33 -0
  108. package/docs/adapters/desktop/discord.md +28 -0
  109. package/docs/adapters/desktop/feishu.md +20 -0
  110. package/docs/adapters/desktop/neteasemusic.md +31 -0
  111. package/docs/adapters/desktop/notion.md +29 -0
  112. package/docs/adapters/desktop/wechat.md +28 -0
  113. package/docs/adapters/index.md +49 -0
  114. package/docs/advanced/cdp.md +103 -0
  115. package/docs/advanced/download.md +63 -0
  116. package/docs/advanced/electron.md +125 -0
  117. package/docs/advanced/remote-chrome.md +72 -0
  118. package/docs/developer/ai-workflow.md +66 -0
  119. package/docs/developer/architecture.md +90 -0
  120. package/docs/developer/contributing.md +136 -0
  121. package/docs/developer/testing.md +237 -0
  122. package/docs/developer/ts-adapter.md +87 -0
  123. package/docs/developer/yaml-adapter.md +108 -0
  124. package/docs/guide/browser-bridge.md +38 -0
  125. package/docs/guide/getting-started.md +56 -0
  126. package/docs/guide/installation.md +37 -0
  127. package/docs/guide/troubleshooting.md +56 -0
  128. package/docs/index.md +35 -0
  129. package/docs/zh/adapters/index.md +5 -0
  130. package/docs/zh/advanced/cdp.md +3 -0
  131. package/docs/zh/developer/contributing.md +24 -0
  132. package/docs/zh/guide/browser-bridge.md +25 -0
  133. package/docs/zh/guide/getting-started.md +40 -0
  134. package/docs/zh/guide/installation.md +37 -0
  135. package/docs/zh/index.md +29 -0
  136. package/extension/dist/background.js +92 -52
  137. package/extension/package-lock.json +1156 -0
  138. package/extension/src/background.test.ts +151 -0
  139. package/extension/src/background.ts +122 -51
  140. package/extension/src/protocol.ts +3 -1
  141. package/package.json +7 -3
  142. package/src/browser/cdp.ts +154 -82
  143. package/src/browser/daemon-client.ts +7 -1
  144. package/src/browser/dom-helpers.ts +116 -0
  145. package/src/browser/mcp.ts +14 -6
  146. package/src/browser/page.ts +45 -100
  147. package/src/browser.test.ts +10 -0
  148. package/src/build-manifest.test.ts +36 -0
  149. package/src/build-manifest.ts +2 -1
  150. package/src/capabilityRouting.test.ts +47 -0
  151. package/src/capabilityRouting.ts +28 -0
  152. package/src/chaoxing.test.ts +12 -4
  153. package/src/cli.ts +28 -8
  154. package/src/clis/antigravity/serve.ts +329 -0
  155. package/src/clis/bilibili/download.ts +4 -15
  156. package/src/clis/boss/resume.ts +262 -0
  157. package/src/clis/hf/top.ts +141 -0
  158. package/src/clis/jike/comment.ts +113 -0
  159. package/src/clis/jike/create.ts +113 -0
  160. package/src/clis/jike/feed.ts +74 -0
  161. package/src/clis/jike/like.ts +65 -0
  162. package/src/clis/jike/notifications.ts +185 -0
  163. package/src/clis/jike/post.yaml +58 -0
  164. package/src/clis/jike/repost.ts +114 -0
  165. package/src/clis/jike/search.ts +74 -0
  166. package/src/clis/jike/shared.ts +36 -0
  167. package/src/clis/jike/topic.yaml +52 -0
  168. package/src/clis/jike/user.yaml +51 -0
  169. package/src/clis/smzdm/search.ts +30 -39
  170. package/src/clis/stackoverflow/bounties.yaml +29 -0
  171. package/src/clis/stackoverflow/hot.yaml +28 -0
  172. package/src/clis/stackoverflow/search.yaml +32 -0
  173. package/src/clis/stackoverflow/unanswered.yaml +28 -0
  174. package/src/clis/twitter/download.ts +6 -17
  175. package/src/clis/xiaohongshu/download.ts +3 -3
  176. package/src/clis/zhihu/download.ts +3 -3
  177. package/src/doctor.ts +18 -2
  178. package/src/download/index.test.ts +16 -0
  179. package/src/download/index.ts +22 -4
  180. package/src/engine.ts +4 -4
  181. package/src/explore.ts +4 -4
  182. package/src/generate.ts +1 -0
  183. package/src/interceptor.ts +3 -2
  184. package/src/output.ts +3 -1
  185. package/src/pipeline/executor.test.ts +1 -0
  186. package/src/pipeline/steps/download.ts +14 -17
  187. package/src/registry.ts +6 -2
  188. package/src/runtime.ts +3 -2
  189. package/src/types.ts +9 -0
  190. package/src/verify.ts +64 -3
@@ -1,10 +1,25 @@
1
1
  /**
2
2
  * CDP client — implements IPage by connecting directly to a Chrome/Electron CDP WebSocket.
3
+ *
4
+ * Fixes applied:
5
+ * - send() now has a 30s timeout guard (P0 #4)
6
+ * - goto() waits for Page.loadEventFired instead of hardcoded 1s sleep (P1 #3)
7
+ * - Implemented scroll, autoScroll, screenshot, networkRequests (P1 #2)
8
+ * - Shared DOM helper methods extracted to reduce duplication with Page (P1 #5)
3
9
  */
4
10
 
5
11
  import { WebSocket } from 'ws';
6
12
  import type { IPage } from '../types.js';
7
13
  import { wrapForEval } from './utils.js';
14
+ import {
15
+ clickJs,
16
+ typeTextJs,
17
+ pressKeyJs,
18
+ waitForTextJs,
19
+ scrollJs,
20
+ autoScrollJs,
21
+ networkRequestsJs,
22
+ } from './dom-helpers.js';
8
23
 
9
24
  export interface CDPTarget {
10
25
  type?: string;
@@ -13,12 +28,15 @@ export interface CDPTarget {
13
28
  webSocketDebuggerUrl?: string;
14
29
  }
15
30
 
31
+ const CDP_SEND_TIMEOUT = 30_000; // 30s per command
32
+
16
33
  export class CDPBridge {
17
34
  private _ws: WebSocket | null = null;
18
35
  private _idCounter = 0;
19
- private _pending = new Map<number, { resolve: (val: any) => void; reject: (err: Error) => void }>();
36
+ private _pending = new Map<number, { resolve: (val: any) => void; reject: (err: Error) => void; timer: ReturnType<typeof setTimeout> }>();
37
+ private _eventListeners = new Map<string, Set<(params: any) => void>>();
20
38
 
21
- async connect(opts?: { timeout?: number }): Promise<IPage> {
39
+ async connect(opts?: { timeout?: number; workspace?: string }): Promise<IPage> {
22
40
  const endpoint = process.env.OPENCLI_CDP_ENDPOINT;
23
41
  if (!endpoint) throw new Error('OPENCLI_CDP_ENDPOINT is not set');
24
42
 
@@ -53,16 +71,25 @@ export class CDPBridge {
53
71
  ws.on('message', (data) => {
54
72
  try {
55
73
  const msg = JSON.parse(data.toString());
74
+ // Handle command responses
56
75
  if (msg.id && this._pending.has(msg.id)) {
57
- const { resolve, reject } = this._pending.get(msg.id)!;
76
+ const entry = this._pending.get(msg.id)!;
77
+ clearTimeout(entry.timer);
58
78
  this._pending.delete(msg.id);
59
79
  if (msg.error) {
60
- reject(new Error(msg.error.message));
80
+ entry.reject(new Error(msg.error.message));
61
81
  } else {
62
- resolve(msg.result);
82
+ entry.resolve(msg.result);
83
+ }
84
+ }
85
+ // Handle CDP events
86
+ if (msg.method) {
87
+ const listeners = this._eventListeners.get(msg.method);
88
+ if (listeners) {
89
+ for (const fn of listeners) fn(msg.params);
63
90
  }
64
91
  }
65
- } catch (e) {
92
+ } catch {
66
93
  // ignore parsing errors
67
94
  }
68
95
  });
@@ -75,29 +102,68 @@ export class CDPBridge {
75
102
  this._ws = null;
76
103
  }
77
104
  for (const p of this._pending.values()) {
105
+ clearTimeout(p.timer);
78
106
  p.reject(new Error('CDP connection closed'));
79
107
  }
80
108
  this._pending.clear();
109
+ this._eventListeners.clear();
81
110
  }
82
111
 
83
- async send(method: string, params: any = {}): Promise<any> {
112
+ /** Send a CDP command with timeout guard (P0 fix #4) */
113
+ async send(method: string, params: any = {}, timeoutMs: number = CDP_SEND_TIMEOUT): Promise<any> {
84
114
  if (!this._ws || this._ws.readyState !== WebSocket.OPEN) {
85
115
  throw new Error('CDP connection is not open');
86
116
  }
87
117
  const id = ++this._idCounter;
88
118
  return new Promise((resolve, reject) => {
89
- this._pending.set(id, { resolve, reject });
119
+ const timer = setTimeout(() => {
120
+ this._pending.delete(id);
121
+ reject(new Error(`CDP command '${method}' timed out after ${timeoutMs / 1000}s`));
122
+ }, timeoutMs);
123
+ this._pending.set(id, { resolve, reject, timer });
90
124
  this._ws!.send(JSON.stringify({ id, method, params }));
91
125
  });
92
126
  }
127
+
128
+ /** Listen for a CDP event */
129
+ on(event: string, handler: (params: any) => void): void {
130
+ let set = this._eventListeners.get(event);
131
+ if (!set) { set = new Set(); this._eventListeners.set(event, set); }
132
+ set.add(handler);
133
+ }
134
+
135
+ /** Remove a CDP event listener */
136
+ off(event: string, handler: (params: any) => void): void {
137
+ this._eventListeners.get(event)?.delete(handler);
138
+ }
139
+
140
+ /** Wait for a CDP event to fire (one-shot) */
141
+ waitForEvent(event: string, timeoutMs: number = 15_000): Promise<any> {
142
+ return new Promise((resolve, reject) => {
143
+ const timer = setTimeout(() => {
144
+ this.off(event, handler);
145
+ reject(new Error(`Timed out waiting for CDP event '${event}'`));
146
+ }, timeoutMs);
147
+ const handler = (params: any) => {
148
+ clearTimeout(timer);
149
+ this.off(event, handler);
150
+ resolve(params);
151
+ };
152
+ this.on(event, handler);
153
+ });
154
+ }
93
155
  }
94
156
 
95
157
  class CDPPage implements IPage {
96
158
  constructor(private bridge: CDPBridge) {}
97
159
 
160
+ /** Navigate with proper load event waiting (P1 fix #3) */
98
161
  async goto(url: string): Promise<void> {
162
+ await this.bridge.send('Page.enable');
163
+ const loadPromise = this.bridge.waitForEvent('Page.loadEventFired', 30_000)
164
+ .catch(() => {}); // Don't fail if event times out
99
165
  await this.bridge.send('Page.navigate', { url });
100
- await new Promise(r => setTimeout(r, 1000));
166
+ await loadPromise;
101
167
  }
102
168
 
103
169
  async evaluate(js: string): Promise<any> {
@@ -113,53 +179,33 @@ class CDPPage implements IPage {
113
179
  return result.result?.value;
114
180
  }
115
181
 
116
- async snapshot(opts?: any): Promise<any> {
117
- throw new Error('Method not implemented.');
182
+ async getCookies(opts: { domain?: string; url?: string } = {}): Promise<any[]> {
183
+ const result = await this.bridge.send('Network.getCookies', opts.url ? { urls: [opts.url] } : {});
184
+ const cookies = Array.isArray(result?.cookies) ? result.cookies : [];
185
+ return opts.domain
186
+ ? cookies.filter((cookie: any) => typeof cookie.domain === 'string' && cookie.domain.includes(opts.domain!))
187
+ : cookies;
188
+ }
189
+
190
+ async snapshot(_opts?: any): Promise<any> {
191
+ // CDP doesn't have a built-in accessibility tree equivalent without additional setup
192
+ return '(snapshot not available in CDP mode)';
118
193
  }
194
+
195
+ // ── Shared DOM operations (P1 fix #5 — using dom-helpers.ts) ──
196
+
119
197
  async click(ref: string): Promise<void> {
120
- const safeRef = JSON.stringify(ref);
121
- const code = `
122
- (() => {
123
- const ref = ${safeRef};
124
- const el = document.querySelector('[data-ref="' + ref + '"]')
125
- || document.querySelectorAll('a, button, input, [role="button"], [tabindex]')[parseInt(ref, 10) || 0];
126
- if (!el) throw new Error('Element not found: ' + ref);
127
- el.scrollIntoView({ behavior: 'instant', block: 'center' });
128
- el.click();
129
- return 'clicked';
130
- })()
131
- `;
132
- await this.evaluate(code);
198
+ await this.evaluate(clickJs(ref));
133
199
  }
200
+
134
201
  async typeText(ref: string, text: string): Promise<void> {
135
- const safeRef = JSON.stringify(ref);
136
- const safeText = JSON.stringify(text);
137
- const code = `
138
- (() => {
139
- const ref = ${safeRef};
140
- const el = document.querySelector('[data-ref="' + ref + '"]')
141
- || document.querySelectorAll('input, textarea, [contenteditable]')[parseInt(ref, 10) || 0];
142
- if (!el) throw new Error('Element not found: ' + ref);
143
- el.focus();
144
- el.value = ${safeText};
145
- el.dispatchEvent(new Event('input', { bubbles: true }));
146
- el.dispatchEvent(new Event('change', { bubbles: true }));
147
- return 'typed';
148
- })()
149
- `;
150
- await this.evaluate(code);
202
+ await this.evaluate(typeTextJs(ref, text));
151
203
  }
204
+
152
205
  async pressKey(key: string): Promise<void> {
153
- const code = `
154
- (() => {
155
- const el = document.activeElement || document.body;
156
- el.dispatchEvent(new KeyboardEvent('keydown', { key: ${JSON.stringify(key)}, bubbles: true }));
157
- el.dispatchEvent(new KeyboardEvent('keyup', { key: ${JSON.stringify(key)}, bubbles: true }));
158
- return 'pressed';
159
- })()
160
- `;
161
- await this.evaluate(code);
206
+ await this.evaluate(pressKeyJs(key));
162
207
  }
208
+
163
209
  async wait(options: any): Promise<void> {
164
210
  if (typeof options === 'number') {
165
211
  await new Promise(resolve => setTimeout(resolve, options * 1000));
@@ -171,54 +217,80 @@ class CDPPage implements IPage {
171
217
  }
172
218
  if (options.text) {
173
219
  const timeout = (options.timeout ?? 30) * 1000;
174
- const code = `
175
- new Promise((resolve, reject) => {
176
- const deadline = Date.now() + ${timeout};
177
- const check = () => {
178
- if (document.body.innerText.includes(${JSON.stringify(options.text)})) return resolve('found');
179
- if (Date.now() > deadline) return reject(new Error('Text not found: ' + ${JSON.stringify(options.text)}));
180
- setTimeout(check, 200);
181
- };
182
- check();
183
- })
184
- `;
185
- await this.evaluate(code);
220
+ await this.evaluate(waitForTextJs(options.text, timeout));
186
221
  }
187
222
  }
188
- async tabs(): Promise<any> {
189
- throw new Error('Method not implemented.');
223
+
224
+ // ── Implemented methods (P1 fix #2) ──
225
+
226
+ async scroll(direction: string = 'down', amount: number = 500): Promise<void> {
227
+ await this.evaluate(scrollJs(direction, amount));
190
228
  }
191
- async closeTab(index?: number): Promise<void> {
192
- throw new Error('Method not implemented.');
229
+
230
+ async autoScroll(options?: { times?: number; delayMs?: number }): Promise<void> {
231
+ const times = options?.times ?? 3;
232
+ const delayMs = options?.delayMs ?? 2000;
233
+ await this.evaluate(autoScrollJs(times, delayMs));
193
234
  }
194
- async newTab(): Promise<void> {
195
- throw new Error('Method not implemented.');
235
+
236
+ async screenshot(options: any = {}): Promise<string> {
237
+ const result = await this.bridge.send('Page.captureScreenshot', {
238
+ format: options.format ?? 'png',
239
+ quality: options.format === 'jpeg' ? (options.quality ?? 80) : undefined,
240
+ captureBeyondViewport: options.fullPage ?? false,
241
+ });
242
+ const base64 = result.data;
243
+ if (options.path) {
244
+ const fs = await import('node:fs');
245
+ const path = await import('node:path');
246
+ const dir = path.dirname(options.path);
247
+ await fs.promises.mkdir(dir, { recursive: true });
248
+ await fs.promises.writeFile(options.path, Buffer.from(base64, 'base64'));
249
+ }
250
+ return base64;
251
+ }
252
+
253
+ async networkRequests(includeStatic: boolean = false): Promise<any> {
254
+ return this.evaluate(networkRequestsJs(includeStatic));
196
255
  }
197
- async selectTab(index: number): Promise<void> {
198
- throw new Error('Method not implemented.');
256
+
257
+ async tabs(): Promise<any> {
258
+ return [];
199
259
  }
200
- async networkRequests(includeStatic?: boolean): Promise<any> {
201
- throw new Error('Method not implemented.');
260
+
261
+ async closeTab(_index?: number): Promise<void> {
262
+ // Not supported in direct CDP mode
202
263
  }
203
- async consoleMessages(level?: string): Promise<any> {
204
- throw new Error('Method not implemented.');
264
+
265
+ async newTab(): Promise<void> {
266
+ await this.bridge.send('Target.createTarget', { url: 'about:blank' });
205
267
  }
206
- async scroll(direction?: string, amount?: number): Promise<void> {
207
- throw new Error('Method not implemented.');
268
+
269
+ async selectTab(_index: number): Promise<void> {
270
+ // Not supported in direct CDP mode
208
271
  }
209
- async autoScroll(options?: any): Promise<void> {
210
- throw new Error('Method not implemented.');
272
+
273
+ async consoleMessages(_level?: string): Promise<any> {
274
+ return [];
211
275
  }
276
+
212
277
  async installInterceptor(pattern: string): Promise<void> {
213
- throw new Error('Method not implemented.');
278
+ const { generateInterceptorJs } = await import('../interceptor.js');
279
+ await this.evaluate(generateInterceptorJs(JSON.stringify(pattern), {
280
+ arrayName: '__opencli_xhr',
281
+ patchGuard: '__opencli_interceptor_patched',
282
+ }));
214
283
  }
284
+
215
285
  async getInterceptedRequests(): Promise<any[]> {
216
- throw new Error('Method not implemented.');
217
- }
218
- async screenshot(options?: any): Promise<string> {
219
- throw new Error('Method not implemented.');
286
+ const { generateReadInterceptedJs } = await import('../interceptor.js');
287
+ const result = await this.evaluate(generateReadInterceptedJs('__opencli_xhr'));
288
+ return (result as any[]) || [];
220
289
  }
221
290
  }
291
+
292
+ // ── CDP target selection (unchanged) ──
293
+
222
294
  function selectCDPTarget(targets: CDPTarget[]): CDPTarget | undefined {
223
295
  const preferredPattern = compilePreferredPattern(process.env.OPENCLI_CDP_TARGET);
224
296
 
@@ -15,9 +15,10 @@ function generateId(): string {
15
15
 
16
16
  export interface DaemonCommand {
17
17
  id: string;
18
- action: 'exec' | 'navigate' | 'tabs' | 'cookies' | 'screenshot' | 'close-window';
18
+ action: 'exec' | 'navigate' | 'tabs' | 'cookies' | 'screenshot' | 'close-window' | 'sessions';
19
19
  tabId?: number;
20
20
  code?: string;
21
+ workspace?: string;
21
22
  url?: string;
22
23
  op?: string;
23
24
  index?: number;
@@ -111,3 +112,8 @@ export async function sendCommand(
111
112
  // Unreachable — the loop always returns or throws
112
113
  throw new Error('sendCommand: max retries exhausted');
113
114
  }
115
+
116
+ export async function listSessions(): Promise<any[]> {
117
+ const result = await sendCommand('sessions');
118
+ return Array.isArray(result) ? result : [];
119
+ }
@@ -0,0 +1,116 @@
1
+ /**
2
+ * Shared DOM operation JS generators.
3
+ *
4
+ * Used by both Page (daemon mode) and CDPPage (direct CDP mode)
5
+ * to eliminate code duplication for click, type, press, wait, scroll, etc.
6
+ */
7
+
8
+ /** Generate JS to click an element by ref */
9
+ export function clickJs(ref: string): string {
10
+ const safeRef = JSON.stringify(ref);
11
+ return `
12
+ (() => {
13
+ const ref = ${safeRef};
14
+ const el = document.querySelector('[data-ref="' + ref + '"]')
15
+ || document.querySelectorAll('a, button, input, [role="button"], [tabindex]')[parseInt(ref, 10) || 0];
16
+ if (!el) throw new Error('Element not found: ' + ref);
17
+ el.scrollIntoView({ behavior: 'instant', block: 'center' });
18
+ el.click();
19
+ return 'clicked';
20
+ })()
21
+ `;
22
+ }
23
+
24
+ /** Generate JS to type text into an element by ref */
25
+ export function typeTextJs(ref: string, text: string): string {
26
+ const safeRef = JSON.stringify(ref);
27
+ const safeText = JSON.stringify(text);
28
+ return `
29
+ (() => {
30
+ const ref = ${safeRef};
31
+ const el = document.querySelector('[data-ref="' + ref + '"]')
32
+ || document.querySelectorAll('input, textarea, [contenteditable]')[parseInt(ref, 10) || 0];
33
+ if (!el) throw new Error('Element not found: ' + ref);
34
+ el.focus();
35
+ el.value = ${safeText};
36
+ el.dispatchEvent(new Event('input', { bubbles: true }));
37
+ el.dispatchEvent(new Event('change', { bubbles: true }));
38
+ return 'typed';
39
+ })()
40
+ `;
41
+ }
42
+
43
+ /** Generate JS to press a keyboard key */
44
+ export function pressKeyJs(key: string): string {
45
+ return `
46
+ (() => {
47
+ const el = document.activeElement || document.body;
48
+ el.dispatchEvent(new KeyboardEvent('keydown', { key: ${JSON.stringify(key)}, bubbles: true }));
49
+ el.dispatchEvent(new KeyboardEvent('keyup', { key: ${JSON.stringify(key)}, bubbles: true }));
50
+ return 'pressed';
51
+ })()
52
+ `;
53
+ }
54
+
55
+ /** Generate JS to wait for text to appear in the page */
56
+ export function waitForTextJs(text: string, timeoutMs: number): string {
57
+ return `
58
+ new Promise((resolve, reject) => {
59
+ const deadline = Date.now() + ${timeoutMs};
60
+ const check = () => {
61
+ if (document.body.innerText.includes(${JSON.stringify(text)})) return resolve('found');
62
+ if (Date.now() > deadline) return reject(new Error('Text not found: ' + ${JSON.stringify(text)}));
63
+ setTimeout(check, 200);
64
+ };
65
+ check();
66
+ })
67
+ `;
68
+ }
69
+
70
+ /** Generate JS for scroll */
71
+ export function scrollJs(direction: string, amount: number): string {
72
+ const dx = direction === 'left' ? -amount : direction === 'right' ? amount : 0;
73
+ const dy = direction === 'up' ? -amount : direction === 'down' ? amount : 0;
74
+ return `window.scrollBy(${dx}, ${dy})`;
75
+ }
76
+
77
+ /** Generate JS for auto-scroll with lazy-load detection */
78
+ export function autoScrollJs(times: number, delayMs: number): string {
79
+ return `
80
+ (async () => {
81
+ for (let i = 0; i < ${times}; i++) {
82
+ const lastHeight = document.body.scrollHeight;
83
+ window.scrollTo(0, lastHeight);
84
+ await new Promise(resolve => {
85
+ let timeoutId;
86
+ const observer = new MutationObserver(() => {
87
+ if (document.body.scrollHeight > lastHeight) {
88
+ clearTimeout(timeoutId);
89
+ observer.disconnect();
90
+ setTimeout(resolve, 100);
91
+ }
92
+ });
93
+ observer.observe(document.body, { childList: true, subtree: true });
94
+ timeoutId = setTimeout(() => { observer.disconnect(); resolve(null); }, ${delayMs});
95
+ });
96
+ }
97
+ })()
98
+ `;
99
+ }
100
+
101
+ /** Generate JS to read performance resource entries as network requests */
102
+ export function networkRequestsJs(includeStatic: boolean): string {
103
+ return `
104
+ (() => {
105
+ const entries = performance.getEntriesByType('resource');
106
+ return entries
107
+ ${includeStatic ? '' : '.filter(e => !["img", "font", "css", "script"].some(t => e.initiatorType === t))'}
108
+ .map(e => ({
109
+ url: e.name,
110
+ type: e.initiatorType,
111
+ duration: Math.round(e.duration),
112
+ size: e.transferSize || 0,
113
+ }));
114
+ })()
115
+ `;
116
+ }
@@ -26,7 +26,7 @@ export class BrowserBridge {
26
26
  return this._state;
27
27
  }
28
28
 
29
- async connect(opts: { timeout?: number } = {}): Promise<IPage> {
29
+ async connect(opts: { timeout?: number; workspace?: string } = {}): Promise<IPage> {
30
30
  if (this._state === 'connected' && this._page) return this._page;
31
31
  if (this._state === 'connecting') throw new Error('Already connecting');
32
32
  if (this._state === 'closing') throw new Error('Session is closing');
@@ -35,8 +35,8 @@ export class BrowserBridge {
35
35
  this._state = 'connecting';
36
36
 
37
37
  try {
38
- await this._ensureDaemon();
39
- this._page = new Page();
38
+ await this._ensureDaemon(opts.timeout);
39
+ this._page = new Page(opts.workspace);
40
40
  this._state = 'connected';
41
41
  return this._page;
42
42
  } catch (err) {
@@ -54,8 +54,16 @@ export class BrowserBridge {
54
54
  this._state = 'closed';
55
55
  }
56
56
 
57
- private async _ensureDaemon(): Promise<void> {
58
- if (await isDaemonRunning()) return;
57
+ private async _ensureDaemon(timeoutSeconds?: number): Promise<void> {
58
+ const timeoutMs = Math.max(1, timeoutSeconds ?? Math.ceil(DAEMON_SPAWN_TIMEOUT / 1000)) * 1000;
59
+
60
+ if (await isExtensionConnected()) return;
61
+ if (await isDaemonRunning()) {
62
+ throw new Error(
63
+ 'Daemon is running but the Browser Extension is not connected.\n' +
64
+ 'Please install and enable the opencli Browser Bridge extension in Chrome.',
65
+ );
66
+ }
59
67
 
60
68
  // Find daemon relative to this file — works for both:
61
69
  // npx tsx src/main.ts → src/browser/mcp.ts → src/daemon.ts
@@ -85,7 +93,7 @@ export class BrowserBridge {
85
93
  this._daemonProc.unref();
86
94
 
87
95
  // Wait for daemon to be ready AND extension to connect
88
- const deadline = Date.now() + DAEMON_SPAWN_TIMEOUT;
96
+ const deadline = Date.now() + timeoutMs;
89
97
  while (Date.now() < deadline) {
90
98
  await new Promise(resolve => setTimeout(resolve, 300));
91
99
  if (await isExtensionConnected()) return;