@jackwener/opencli 1.5.8 → 1.6.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (220) hide show
  1. package/CHANGELOG.md +42 -0
  2. package/README.md +35 -1
  3. package/README.zh-CN.md +17 -1
  4. package/SKILL.md +31 -851
  5. package/autoresearch/baseline-browse.txt +1 -0
  6. package/autoresearch/baseline-skill.txt +1 -0
  7. package/autoresearch/browse-tasks.json +688 -0
  8. package/autoresearch/eval-browse.ts +185 -0
  9. package/autoresearch/eval-skill.ts +248 -0
  10. package/autoresearch/run-browse.sh +9 -0
  11. package/autoresearch/run-skill.sh +9 -0
  12. package/dist/browser/base-page.d.ts +48 -0
  13. package/dist/browser/base-page.js +160 -0
  14. package/dist/browser/cdp.js +4 -106
  15. package/dist/browser/daemon-client.d.ts +20 -7
  16. package/dist/browser/daemon-client.js +39 -39
  17. package/dist/browser/daemon-client.test.js +77 -0
  18. package/dist/browser/discover.d.ts +1 -4
  19. package/dist/browser/discover.js +9 -23
  20. package/dist/browser/errors.d.ts +4 -0
  21. package/dist/browser/errors.js +20 -0
  22. package/dist/browser/index.d.ts +1 -1
  23. package/dist/browser/index.js +1 -1
  24. package/dist/browser/page.d.ts +10 -35
  25. package/dist/browser/page.js +55 -187
  26. package/dist/browser/tabs.js +5 -5
  27. package/dist/browser.test.js +15 -15
  28. package/dist/cli-manifest.json +294 -22
  29. package/dist/cli.js +392 -0
  30. package/dist/clis/amazon/bestsellers.d.ts +21 -0
  31. package/dist/clis/amazon/bestsellers.js +130 -0
  32. package/dist/clis/amazon/bestsellers.test.js +20 -0
  33. package/dist/clis/amazon/discussion.d.ts +20 -0
  34. package/dist/clis/amazon/discussion.js +91 -0
  35. package/dist/clis/amazon/discussion.test.d.ts +1 -0
  36. package/dist/clis/amazon/discussion.test.js +36 -0
  37. package/dist/clis/amazon/offer.d.ts +23 -0
  38. package/dist/clis/amazon/offer.js +140 -0
  39. package/dist/clis/amazon/offer.test.d.ts +1 -0
  40. package/dist/clis/amazon/offer.test.js +29 -0
  41. package/dist/clis/amazon/product.d.ts +18 -0
  42. package/dist/clis/amazon/product.js +92 -0
  43. package/dist/clis/amazon/product.test.d.ts +1 -0
  44. package/dist/clis/amazon/product.test.js +24 -0
  45. package/dist/clis/amazon/search.d.ts +18 -0
  46. package/dist/clis/amazon/search.js +87 -0
  47. package/dist/clis/amazon/search.test.d.ts +1 -0
  48. package/dist/clis/amazon/search.test.js +22 -0
  49. package/dist/clis/amazon/shared.d.ts +64 -0
  50. package/dist/clis/amazon/shared.js +255 -0
  51. package/dist/clis/amazon/shared.test.d.ts +1 -0
  52. package/dist/clis/amazon/shared.test.js +33 -0
  53. package/dist/clis/gemini/ask.d.ts +1 -0
  54. package/dist/clis/gemini/ask.js +40 -0
  55. package/dist/clis/gemini/image.d.ts +1 -0
  56. package/dist/clis/gemini/image.js +105 -0
  57. package/dist/clis/gemini/new.d.ts +1 -0
  58. package/dist/clis/gemini/new.js +20 -0
  59. package/dist/clis/gemini/utils.d.ts +34 -0
  60. package/dist/clis/gemini/utils.js +463 -0
  61. package/dist/clis/gemini/utils.test.d.ts +1 -0
  62. package/dist/clis/gemini/utils.test.js +31 -0
  63. package/dist/clis/notebooklm/compat.test.d.ts +1 -1
  64. package/dist/clis/notebooklm/compat.test.js +3 -3
  65. package/dist/clis/notebooklm/current.js +2 -3
  66. package/dist/clis/notebooklm/get.js +2 -3
  67. package/dist/clis/notebooklm/history.js +2 -3
  68. package/dist/clis/notebooklm/note-list.js +2 -3
  69. package/dist/clis/notebooklm/notes-get.js +2 -3
  70. package/dist/clis/notebooklm/open.d.ts +1 -0
  71. package/dist/clis/notebooklm/open.js +41 -0
  72. package/dist/clis/notebooklm/open.test.d.ts +1 -0
  73. package/dist/clis/notebooklm/open.test.js +63 -0
  74. package/dist/clis/notebooklm/source-fulltext.js +2 -3
  75. package/dist/clis/notebooklm/source-get.js +2 -3
  76. package/dist/clis/notebooklm/source-guide.js +2 -3
  77. package/dist/clis/notebooklm/source-list.js +2 -3
  78. package/dist/clis/notebooklm/status.js +1 -2
  79. package/dist/clis/notebooklm/summary.js +2 -3
  80. package/dist/clis/notebooklm/utils.d.ts +2 -1
  81. package/dist/clis/notebooklm/utils.js +20 -21
  82. package/dist/clis/twitter/article.js +28 -1
  83. package/dist/clis/xiaohongshu/creator-note-detail.test.js +11 -11
  84. package/dist/clis/xiaohongshu/creator-notes-summary.test.js +6 -6
  85. package/dist/clis/xiaohongshu/creator-notes.test.js +22 -22
  86. package/dist/clis/xiaohongshu/note.js +11 -0
  87. package/dist/clis/xiaohongshu/note.test.js +49 -0
  88. package/dist/commanderAdapter.js +7 -4
  89. package/dist/commanderAdapter.test.js +76 -0
  90. package/dist/commands/daemon.js +8 -47
  91. package/dist/commands/daemon.test.js +45 -70
  92. package/dist/discovery.js +27 -0
  93. package/dist/doctor.d.ts +1 -2
  94. package/dist/doctor.js +7 -8
  95. package/dist/explore.js +1 -1
  96. package/dist/output.js +28 -0
  97. package/dist/output.test.js +15 -0
  98. package/dist/pipeline/executor.js +2 -7
  99. package/dist/pipeline/steps/browser.js +1 -1
  100. package/dist/pipeline/template.js +25 -3
  101. package/dist/record.d.ts +50 -0
  102. package/dist/record.js +298 -57
  103. package/dist/record.test.d.ts +1 -0
  104. package/dist/record.test.js +293 -0
  105. package/dist/registry.d.ts +2 -0
  106. package/dist/registry.js +1 -0
  107. package/dist/registry.test.js +10 -0
  108. package/dist/runtime.js +3 -3
  109. package/dist/snapshotFormatter.d.ts +1 -1
  110. package/dist/snapshotFormatter.js +4 -4
  111. package/dist/snapshotFormatter.test.d.ts +1 -1
  112. package/dist/snapshotFormatter.test.js +2 -2
  113. package/dist/types.d.ts +11 -1
  114. package/dist/types.js +1 -1
  115. package/docs/.vitepress/config.mts +2 -0
  116. package/docs/adapters/browser/amazon.md +53 -0
  117. package/docs/adapters/browser/gemini.md +72 -0
  118. package/docs/adapters/browser/notebooklm.md +5 -5
  119. package/docs/adapters/index.md +3 -1
  120. package/docs/guide/getting-started.md +21 -0
  121. package/docs/superpowers/specs/2026-04-02-browse-skill-testing-design.md +144 -0
  122. package/docs/zh/guide/getting-started.md +21 -0
  123. package/extension/package-lock.json +2 -2
  124. package/extension/src/background.test.ts +7 -163
  125. package/extension/src/background.ts +58 -161
  126. package/extension/src/cdp.ts +77 -124
  127. package/extension/src/protocol.ts +5 -5
  128. package/package.json +1 -1
  129. package/skills/opencli-explorer/SKILL.md +853 -0
  130. package/skills/opencli-oneshot/SKILL.md +222 -0
  131. package/skills/opencli-operate/SKILL.md +213 -0
  132. package/skills/opencli-usage/SKILL.md +152 -0
  133. package/skills/opencli-usage/browser.md +429 -0
  134. package/skills/opencli-usage/desktop.md +118 -0
  135. package/skills/opencli-usage/plugins.md +82 -0
  136. package/skills/opencli-usage/public-api.md +149 -0
  137. package/src/browser/base-page.ts +197 -0
  138. package/src/browser/cdp.ts +7 -131
  139. package/src/browser/daemon-client.test.ts +103 -0
  140. package/src/browser/daemon-client.ts +55 -43
  141. package/src/browser/discover.ts +9 -21
  142. package/src/browser/errors.ts +22 -0
  143. package/src/browser/index.ts +1 -1
  144. package/src/browser/page.ts +57 -209
  145. package/src/browser/tabs.ts +5 -5
  146. package/src/browser.test.ts +15 -15
  147. package/src/cli.ts +392 -0
  148. package/src/clis/amazon/bestsellers.test.ts +22 -0
  149. package/src/clis/amazon/bestsellers.ts +180 -0
  150. package/src/clis/amazon/discussion.test.ts +38 -0
  151. package/src/clis/amazon/discussion.ts +131 -0
  152. package/src/clis/amazon/offer.test.ts +35 -0
  153. package/src/clis/amazon/offer.ts +185 -0
  154. package/src/clis/amazon/product.test.ts +26 -0
  155. package/src/clis/amazon/product.ts +131 -0
  156. package/src/clis/amazon/search.test.ts +24 -0
  157. package/src/clis/amazon/search.ts +128 -0
  158. package/src/clis/amazon/shared.test.ts +37 -0
  159. package/src/clis/amazon/shared.ts +316 -0
  160. package/src/clis/gemini/ask.ts +46 -0
  161. package/src/clis/gemini/image.ts +115 -0
  162. package/src/clis/gemini/new.ts +22 -0
  163. package/src/clis/gemini/utils.test.ts +36 -0
  164. package/src/clis/gemini/utils.ts +523 -0
  165. package/src/clis/notebooklm/compat.test.ts +3 -3
  166. package/src/clis/notebooklm/current.ts +2 -3
  167. package/src/clis/notebooklm/get.ts +1 -3
  168. package/src/clis/notebooklm/history.ts +1 -3
  169. package/src/clis/notebooklm/note-list.ts +1 -3
  170. package/src/clis/notebooklm/notes-get.ts +1 -3
  171. package/src/clis/notebooklm/open.test.ts +78 -0
  172. package/src/clis/notebooklm/open.ts +61 -0
  173. package/src/clis/notebooklm/source-fulltext.ts +1 -3
  174. package/src/clis/notebooklm/source-get.ts +1 -3
  175. package/src/clis/notebooklm/source-guide.ts +1 -3
  176. package/src/clis/notebooklm/source-list.ts +1 -3
  177. package/src/clis/notebooklm/status.ts +1 -2
  178. package/src/clis/notebooklm/summary.ts +1 -3
  179. package/src/clis/notebooklm/utils.ts +29 -20
  180. package/src/clis/twitter/article.ts +31 -1
  181. package/src/clis/xiaohongshu/creator-note-detail.test.ts +11 -11
  182. package/src/clis/xiaohongshu/creator-notes-summary.test.ts +6 -6
  183. package/src/clis/xiaohongshu/creator-notes.test.ts +22 -22
  184. package/src/clis/xiaohongshu/note.test.ts +51 -0
  185. package/src/clis/xiaohongshu/note.ts +18 -0
  186. package/src/commanderAdapter.test.ts +109 -0
  187. package/src/commanderAdapter.ts +8 -4
  188. package/src/commands/daemon.test.ts +50 -84
  189. package/src/commands/daemon.ts +8 -56
  190. package/src/discovery.ts +22 -0
  191. package/src/doctor.ts +8 -9
  192. package/src/explore.ts +1 -1
  193. package/src/output.test.ts +17 -0
  194. package/src/output.ts +27 -0
  195. package/src/pipeline/executor.ts +2 -7
  196. package/src/pipeline/steps/browser.ts +1 -1
  197. package/src/pipeline/template.ts +27 -4
  198. package/src/record.test.ts +362 -0
  199. package/src/record.ts +341 -62
  200. package/src/registry.test.ts +12 -0
  201. package/src/registry.ts +3 -0
  202. package/src/runtime.ts +3 -3
  203. package/src/snapshotFormatter.test.ts +2 -2
  204. package/src/snapshotFormatter.ts +4 -4
  205. package/src/types.ts +11 -1
  206. package/.agents/skills/cross-project-adapter-migration/SKILL.md +0 -249
  207. package/.agents/workflows/cross-project-adapter-migration.md +0 -54
  208. package/dist/clis/notebooklm/bind-current.js +0 -29
  209. package/dist/clis/notebooklm/bind-current.test.d.ts +0 -1
  210. package/dist/clis/notebooklm/bind-current.test.js +0 -35
  211. package/dist/clis/notebooklm/binding.test.js +0 -44
  212. package/extension/dist/background.js +0 -819
  213. package/src/clis/notebooklm/bind-current.test.ts +0 -43
  214. package/src/clis/notebooklm/bind-current.ts +0 -36
  215. package/src/clis/notebooklm/binding.test.ts +0 -53
  216. /package/dist/browser/{mcp.d.ts → bridge.d.ts} +0 -0
  217. /package/dist/browser/{mcp.js → bridge.js} +0 -0
  218. /package/dist/{clis/notebooklm/bind-current.d.ts → browser/daemon-client.test.d.ts} +0 -0
  219. /package/dist/clis/{notebooklm/binding.test.d.ts → amazon/bestsellers.test.d.ts} +0 -0
  220. /package/src/browser/{mcp.ts → bridge.ts} +0 -0
@@ -9,13 +9,12 @@
9
9
  * where resolveTabId() in the extension picks a chrome:// or
10
10
  * chrome-extension:// tab that can't be debugged.
11
11
  */
12
- import { formatSnapshot } from '../snapshotFormatter.js';
13
12
  import { sendCommand } from './daemon-client.js';
14
13
  import { wrapForEval } from './utils.js';
15
14
  import { saveBase64ToFile } from '../utils.js';
16
- import { generateSnapshotJs, scrollToRefJs, getFormStateJs } from './dom-snapshot.js';
17
15
  import { generateStealthJs } from './stealth.js';
18
- import { clickJs, typeTextJs, pressKeyJs, waitForTextJs, waitForCaptureJs, waitForSelectorJs, scrollJs, autoScrollJs, networkRequestsJs, waitForDomStableJs, } from './dom-helpers.js';
16
+ import { waitForDomStableJs } from './dom-helpers.js';
17
+ import { BasePage } from './base-page.js';
19
18
  export function isRetryableSettleError(err) {
20
19
  const message = err instanceof Error ? err.message : String(err);
21
20
  return message.includes('Inspected target navigated or closed')
@@ -24,15 +23,14 @@ export function isRetryableSettleError(err) {
24
23
  /**
25
24
  * Page — implements IPage by talking to the daemon via HTTP.
26
25
  */
27
- export class Page {
26
+ export class Page extends BasePage {
28
27
  workspace;
29
28
  constructor(workspace = 'default') {
29
+ super();
30
30
  this.workspace = workspace;
31
31
  }
32
32
  /** Active tab ID, set after navigate and used in all subsequent commands */
33
33
  _tabId;
34
- /** Last navigated URL, tracked in-memory to avoid extra round-trips */
35
- _lastUrl = null;
36
34
  /** Helper: spread workspace into command params */
37
35
  _wsOpt() {
38
36
  return { workspace: this.workspace };
@@ -95,29 +93,8 @@ export class Page {
95
93
  }
96
94
  }
97
95
  }
98
- async getCurrentUrl() {
99
- if (this._lastUrl)
100
- return this._lastUrl;
101
- try {
102
- const current = await this.evaluate('window.location.href');
103
- if (typeof current === 'string' && current) {
104
- this._lastUrl = current;
105
- return current;
106
- }
107
- }
108
- catch {
109
- // Best-effort: some commands may run before a debuggable tab is ready.
110
- }
111
- return null;
112
- }
113
- /** Close the automation window in the extension */
114
- async closeWindow() {
115
- try {
116
- await sendCommand('close-window', { ...this._wsOpt() });
117
- }
118
- catch {
119
- // Window may already be closed or daemon may be down
120
- }
96
+ getActiveTabId() {
97
+ return this._tabId;
121
98
  }
122
99
  async evaluate(js) {
123
100
  const code = wrapForEval(js);
@@ -135,120 +112,13 @@ export class Page {
135
112
  const result = await sendCommand('cookies', { ...this._wsOpt(), ...opts });
136
113
  return Array.isArray(result) ? result : [];
137
114
  }
138
- async snapshot(opts = {}) {
139
- // Primary: use the advanced DOM snapshot engine with multi-layer pruning
140
- const snapshotJs = generateSnapshotJs({
141
- viewportExpand: opts.viewportExpand ?? 800,
142
- maxDepth: Math.max(1, Math.min(Number(opts.maxDepth) || 50, 200)),
143
- interactiveOnly: opts.interactive ?? false,
144
- maxTextLength: opts.maxTextLength ?? 120,
145
- includeScrollInfo: true,
146
- bboxDedup: true,
147
- });
115
+ /** Close the automation window in the extension */
116
+ async closeWindow() {
148
117
  try {
149
- const result = await sendCommand('exec', { code: snapshotJs, ...this._cmdOpts() });
150
- // The advanced engine already produces a clean, pruned, LLM-friendly output.
151
- // Do NOT pass through formatSnapshot — its format is incompatible.
152
- return result;
118
+ await sendCommand('close-window', { ...this._wsOpt() });
153
119
  }
154
120
  catch {
155
- // Fallback: basic DOM snapshot (original implementation)
156
- return this._basicSnapshot(opts);
157
- }
158
- }
159
- /** Fallback basic snapshot — original buildTree approach */
160
- async _basicSnapshot(opts = {}) {
161
- const maxDepth = Math.max(1, Math.min(Number(opts.maxDepth) || 50, 200));
162
- const code = `
163
- (async () => {
164
- function buildTree(node, depth) {
165
- if (depth > ${maxDepth}) return '';
166
- const role = node.getAttribute?.('role') || node.tagName?.toLowerCase() || 'generic';
167
- const name = node.getAttribute?.('aria-label') || node.getAttribute?.('alt') || node.textContent?.trim().slice(0, 80) || '';
168
- const isInteractive = ['a', 'button', 'input', 'select', 'textarea'].includes(node.tagName?.toLowerCase()) || node.getAttribute?.('tabindex') != null;
169
-
170
- ${opts.interactive ? 'if (!isInteractive && !node.children?.length) return "";' : ''}
171
-
172
- let indent = ' '.repeat(depth);
173
- let line = indent + role;
174
- if (name) line += ' "' + name.replace(/"/g, '\\\\\\"') + '"';
175
- if (node.tagName?.toLowerCase() === 'a' && node.href) line += ' [' + node.href + ']';
176
- if (node.tagName?.toLowerCase() === 'input') line += ' [' + (node.type || 'text') + ']';
177
-
178
- let result = line + '\\n';
179
- if (node.children) {
180
- for (const child of node.children) {
181
- result += buildTree(child, depth + 1);
182
- }
183
- }
184
- return result;
185
- }
186
- return buildTree(document.body, 0);
187
- })()
188
- `;
189
- const raw = await sendCommand('exec', { code, ...this._cmdOpts() });
190
- if (opts.raw)
191
- return raw;
192
- if (typeof raw === 'string')
193
- return formatSnapshot(raw, opts);
194
- return raw;
195
- }
196
- async click(ref) {
197
- const code = clickJs(ref);
198
- await sendCommand('exec', { code, ...this._cmdOpts() });
199
- }
200
- async typeText(ref, text) {
201
- const code = typeTextJs(ref, text);
202
- await sendCommand('exec', { code, ...this._cmdOpts() });
203
- }
204
- async pressKey(key) {
205
- const code = pressKeyJs(key);
206
- await sendCommand('exec', { code, ...this._cmdOpts() });
207
- }
208
- async scrollTo(ref) {
209
- const code = scrollToRefJs(ref);
210
- return sendCommand('exec', { code, ...this._cmdOpts() });
211
- }
212
- async getFormState() {
213
- const code = getFormStateJs();
214
- return (await sendCommand('exec', { code, ...this._cmdOpts() }));
215
- }
216
- async wait(options) {
217
- if (typeof options === 'number') {
218
- if (options >= 1) {
219
- // For waits >= 1s, use DOM-stable check: return early when the page
220
- // stops mutating, with the original wait time as the hard cap.
221
- // This turns e.g. `page.wait(5)` from a fixed 5s sleep into
222
- // "wait until DOM is stable, max 5s" — often completing in <1s.
223
- try {
224
- const maxMs = options * 1000;
225
- await sendCommand('exec', {
226
- code: waitForDomStableJs(maxMs, Math.min(500, maxMs)),
227
- ...this._cmdOpts(),
228
- });
229
- return;
230
- }
231
- catch {
232
- // Fallback: fixed sleep (e.g. if page has no DOM yet)
233
- }
234
- }
235
- await new Promise(resolve => setTimeout(resolve, options * 1000));
236
- return;
237
- }
238
- if (typeof options.time === 'number') {
239
- await new Promise(resolve => setTimeout(resolve, options.time * 1000));
240
- return;
241
- }
242
- if (options.selector) {
243
- const timeout = (options.timeout ?? 10) * 1000;
244
- const code = waitForSelectorJs(options.selector, timeout);
245
- await sendCommand('exec', { code, ...this._cmdOpts() });
246
- return;
247
- }
248
- if (options.text) {
249
- const timeout = (options.timeout ?? 30) * 1000;
250
- const code = waitForTextJs(options.text, timeout);
251
- await sendCommand('exec', { code, ...this._cmdOpts() });
121
+ // Window may already be closed or daemon may be down
252
122
  }
253
123
  }
254
124
  async tabs() {
@@ -271,25 +141,8 @@ export class Page {
271
141
  if (result?.selected)
272
142
  this._tabId = result.selected;
273
143
  }
274
- async networkRequests(includeStatic = false) {
275
- const code = networkRequestsJs(includeStatic);
276
- const result = await sendCommand('exec', { code, ...this._cmdOpts() });
277
- return Array.isArray(result) ? result : [];
278
- }
279
- /**
280
- * Console messages are not available in lightweight daemon mode.
281
- * Would require CDP Runtime.consoleAPICalled event listener.
282
- * @returns Always returns empty array.
283
- */
284
- async consoleMessages(_level = 'info') {
285
- return [];
286
- }
287
144
  /**
288
145
  * Capture a screenshot via CDP Page.captureScreenshot.
289
- * @param options.format - 'png' (default) or 'jpeg'
290
- * @param options.quality - JPEG quality 0-100
291
- * @param options.fullPage - capture full scrollable page
292
- * @param options.path - save to file path (returns base64 if omitted)
293
146
  */
294
147
  async screenshot(options = {}) {
295
148
  const base64 = await sendCommand('screenshot', {
@@ -303,31 +156,6 @@ export class Page {
303
156
  }
304
157
  return base64;
305
158
  }
306
- async scroll(direction = 'down', amount = 500) {
307
- const code = scrollJs(direction, amount);
308
- await sendCommand('exec', { code, ...this._cmdOpts() });
309
- }
310
- async autoScroll(options = {}) {
311
- const times = options.times ?? 3;
312
- const delayMs = options.delayMs ?? 2000;
313
- const code = autoScrollJs(times, delayMs);
314
- await sendCommand('exec', { code, ...this._cmdOpts() });
315
- }
316
- async installInterceptor(pattern) {
317
- const { generateInterceptorJs } = await import('../interceptor.js');
318
- // Must use evaluate() so wrapForEval() converts the arrow function into an IIFE;
319
- // sendCommand('exec') sends the code as-is, and CDP never executes a bare arrow.
320
- await this.evaluate(generateInterceptorJs(JSON.stringify(pattern), {
321
- arrayName: '__opencli_xhr',
322
- patchGuard: '__opencli_interceptor_patched',
323
- }));
324
- }
325
- async getInterceptedRequests() {
326
- const { generateReadInterceptedJs } = await import('../interceptor.js');
327
- // Same as installInterceptor: must go through evaluate() for IIFE wrapping
328
- const result = await this.evaluate(generateReadInterceptedJs('__opencli_xhr'));
329
- return Array.isArray(result) ? result : [];
330
- }
331
159
  /**
332
160
  * Set local file paths on a file input element via CDP DOM.setFileInputFiles.
333
161
  * Chrome reads the files directly from the local filesystem, avoiding the
@@ -343,12 +171,52 @@ export class Page {
343
171
  throw new Error('setFileInput returned no count — command may not be supported by the extension');
344
172
  }
345
173
  }
346
- async waitForCapture(timeout = 10) {
347
- const maxMs = timeout * 1000;
348
- await sendCommand('exec', {
349
- code: waitForCaptureJs(maxMs),
174
+ async cdp(method, params = {}) {
175
+ return sendCommand('cdp', {
176
+ cdpMethod: method,
177
+ cdpParams: params,
350
178
  ...this._cmdOpts(),
351
179
  });
352
180
  }
181
+ async nativeClick(x, y) {
182
+ await this.cdp('Input.dispatchMouseEvent', {
183
+ type: 'mousePressed',
184
+ x, y,
185
+ button: 'left',
186
+ clickCount: 1,
187
+ });
188
+ await this.cdp('Input.dispatchMouseEvent', {
189
+ type: 'mouseReleased',
190
+ x, y,
191
+ button: 'left',
192
+ clickCount: 1,
193
+ });
194
+ }
195
+ async nativeType(text) {
196
+ // Use Input.insertText for reliable Unicode/CJK text insertion
197
+ await this.cdp('Input.insertText', { text });
198
+ }
199
+ async nativeKeyPress(key, modifiers = []) {
200
+ let modifierFlags = 0;
201
+ for (const mod of modifiers) {
202
+ if (mod === 'Alt')
203
+ modifierFlags |= 1;
204
+ if (mod === 'Ctrl')
205
+ modifierFlags |= 2;
206
+ if (mod === 'Meta')
207
+ modifierFlags |= 4;
208
+ if (mod === 'Shift')
209
+ modifierFlags |= 8;
210
+ }
211
+ await this.cdp('Input.dispatchKeyEvent', {
212
+ type: 'keyDown',
213
+ key,
214
+ modifiers: modifierFlags,
215
+ });
216
+ await this.cdp('Input.dispatchKeyEvent', {
217
+ type: 'keyUp',
218
+ key,
219
+ modifiers: modifierFlags,
220
+ });
221
+ }
353
222
  }
354
- // (End of file)
@@ -19,12 +19,12 @@ export function extractTabEntries(raw) {
19
19
  .map(line => line.trim())
20
20
  .filter(Boolean)
21
21
  .map(line => {
22
- // Match actual Playwright MCP format: "- 0: (current) [title](url)" or "- 1: [title](url)"
23
- const mcpMatch = line.match(/^-\s+(\d+):\s*(.*)$/);
24
- if (mcpMatch) {
22
+ // Match tab list format: "- 0: (current) [title](url)" or "- 1: [title](url)"
23
+ const tabMatch = line.match(/^-\s+(\d+):\s*(.*)$/);
24
+ if (tabMatch) {
25
25
  return {
26
- index: parseInt(mcpMatch[1], 10),
27
- identity: mcpMatch[2].trim() || `tab-${mcpMatch[1]}`,
26
+ index: parseInt(tabMatch[1], 10),
27
+ identity: tabMatch[2].trim() || `tab-${tabMatch[1]}`,
28
28
  };
29
29
  }
30
30
  // Legacy format: "Tab 0 ..."
@@ -84,31 +84,31 @@ describe('browser helpers', () => {
84
84
  });
85
85
  describe('BrowserBridge state', () => {
86
86
  it('transitions to closed after close()', async () => {
87
- const mcp = new BrowserBridge();
88
- expect(mcp.state).toBe('idle');
89
- await mcp.close();
90
- expect(mcp.state).toBe('closed');
87
+ const bridge = new BrowserBridge();
88
+ expect(bridge.state).toBe('idle');
89
+ await bridge.close();
90
+ expect(bridge.state).toBe('closed');
91
91
  });
92
92
  it('rejects connect() after the session has been closed', async () => {
93
- const mcp = new BrowserBridge();
94
- await mcp.close();
95
- await expect(mcp.connect()).rejects.toThrow('Session is closed');
93
+ const bridge = new BrowserBridge();
94
+ await bridge.close();
95
+ await expect(bridge.connect()).rejects.toThrow('Session is closed');
96
96
  });
97
97
  it('rejects connect() while already connecting', async () => {
98
- const mcp = new BrowserBridge();
99
- mcp._state = 'connecting';
100
- await expect(mcp.connect()).rejects.toThrow('Already connecting');
98
+ const bridge = new BrowserBridge();
99
+ bridge._state = 'connecting';
100
+ await expect(bridge.connect()).rejects.toThrow('Already connecting');
101
101
  });
102
102
  it('rejects connect() while closing', async () => {
103
- const mcp = new BrowserBridge();
104
- mcp._state = 'closing';
105
- await expect(mcp.connect()).rejects.toThrow('Session is closing');
103
+ const bridge = new BrowserBridge();
104
+ bridge._state = 'closing';
105
+ await expect(bridge.connect()).rejects.toThrow('Session is closing');
106
106
  });
107
107
  it('fails fast when daemon is running but extension is disconnected', async () => {
108
108
  vi.spyOn(daemonClient, 'isExtensionConnected').mockResolvedValue(false);
109
109
  vi.spyOn(daemonClient, 'isDaemonRunning').mockResolvedValue(true);
110
- const mcp = new BrowserBridge();
111
- await expect(mcp.connect({ timeout: 0.1 })).rejects.toThrow('Browser Extension is not connected');
110
+ const bridge = new BrowserBridge();
111
+ await expect(bridge.connect({ timeout: 0.1 })).rejects.toThrow('Browser Extension is not connected');
112
112
  });
113
113
  });
114
114
  describe('stealth anti-detection', () => {