@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
@@ -0,0 +1,109 @@
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
+ /** Generate JS to click an element by ref */
8
+ export function clickJs(ref) {
9
+ const safeRef = JSON.stringify(ref);
10
+ return `
11
+ (() => {
12
+ const ref = ${safeRef};
13
+ const el = document.querySelector('[data-ref="' + ref + '"]')
14
+ || document.querySelectorAll('a, button, input, [role="button"], [tabindex]')[parseInt(ref, 10) || 0];
15
+ if (!el) throw new Error('Element not found: ' + ref);
16
+ el.scrollIntoView({ behavior: 'instant', block: 'center' });
17
+ el.click();
18
+ return 'clicked';
19
+ })()
20
+ `;
21
+ }
22
+ /** Generate JS to type text into an element by ref */
23
+ export function typeTextJs(ref, text) {
24
+ const safeRef = JSON.stringify(ref);
25
+ const safeText = JSON.stringify(text);
26
+ return `
27
+ (() => {
28
+ const ref = ${safeRef};
29
+ const el = document.querySelector('[data-ref="' + ref + '"]')
30
+ || document.querySelectorAll('input, textarea, [contenteditable]')[parseInt(ref, 10) || 0];
31
+ if (!el) throw new Error('Element not found: ' + ref);
32
+ el.focus();
33
+ el.value = ${safeText};
34
+ el.dispatchEvent(new Event('input', { bubbles: true }));
35
+ el.dispatchEvent(new Event('change', { bubbles: true }));
36
+ return 'typed';
37
+ })()
38
+ `;
39
+ }
40
+ /** Generate JS to press a keyboard key */
41
+ export function pressKeyJs(key) {
42
+ return `
43
+ (() => {
44
+ const el = document.activeElement || document.body;
45
+ el.dispatchEvent(new KeyboardEvent('keydown', { key: ${JSON.stringify(key)}, bubbles: true }));
46
+ el.dispatchEvent(new KeyboardEvent('keyup', { key: ${JSON.stringify(key)}, bubbles: true }));
47
+ return 'pressed';
48
+ })()
49
+ `;
50
+ }
51
+ /** Generate JS to wait for text to appear in the page */
52
+ export function waitForTextJs(text, timeoutMs) {
53
+ return `
54
+ new Promise((resolve, reject) => {
55
+ const deadline = Date.now() + ${timeoutMs};
56
+ const check = () => {
57
+ if (document.body.innerText.includes(${JSON.stringify(text)})) return resolve('found');
58
+ if (Date.now() > deadline) return reject(new Error('Text not found: ' + ${JSON.stringify(text)}));
59
+ setTimeout(check, 200);
60
+ };
61
+ check();
62
+ })
63
+ `;
64
+ }
65
+ /** Generate JS for scroll */
66
+ export function scrollJs(direction, amount) {
67
+ const dx = direction === 'left' ? -amount : direction === 'right' ? amount : 0;
68
+ const dy = direction === 'up' ? -amount : direction === 'down' ? amount : 0;
69
+ return `window.scrollBy(${dx}, ${dy})`;
70
+ }
71
+ /** Generate JS for auto-scroll with lazy-load detection */
72
+ export function autoScrollJs(times, delayMs) {
73
+ return `
74
+ (async () => {
75
+ for (let i = 0; i < ${times}; i++) {
76
+ const lastHeight = document.body.scrollHeight;
77
+ window.scrollTo(0, lastHeight);
78
+ await new Promise(resolve => {
79
+ let timeoutId;
80
+ const observer = new MutationObserver(() => {
81
+ if (document.body.scrollHeight > lastHeight) {
82
+ clearTimeout(timeoutId);
83
+ observer.disconnect();
84
+ setTimeout(resolve, 100);
85
+ }
86
+ });
87
+ observer.observe(document.body, { childList: true, subtree: true });
88
+ timeoutId = setTimeout(() => { observer.disconnect(); resolve(null); }, ${delayMs});
89
+ });
90
+ }
91
+ })()
92
+ `;
93
+ }
94
+ /** Generate JS to read performance resource entries as network requests */
95
+ export function networkRequestsJs(includeStatic) {
96
+ return `
97
+ (() => {
98
+ const entries = performance.getEntriesByType('resource');
99
+ return entries
100
+ ${includeStatic ? '' : '.filter(e => !["img", "font", "css", "script"].some(t => e.initiatorType === t))'}
101
+ .map(e => ({
102
+ url: e.name,
103
+ type: e.initiatorType,
104
+ duration: Math.round(e.duration),
105
+ size: e.transferSize || 0,
106
+ }));
107
+ })()
108
+ `;
109
+ }
@@ -13,6 +13,7 @@ export declare class BrowserBridge {
13
13
  get state(): BrowserBridgeState;
14
14
  connect(opts?: {
15
15
  timeout?: number;
16
+ workspace?: string;
16
17
  }): Promise<IPage>;
17
18
  close(): Promise<void>;
18
19
  private _ensureDaemon;
@@ -29,8 +29,8 @@ export class BrowserBridge {
29
29
  throw new Error('Session is closed');
30
30
  this._state = 'connecting';
31
31
  try {
32
- await this._ensureDaemon();
33
- this._page = new Page();
32
+ await this._ensureDaemon(opts.timeout);
33
+ this._page = new Page(opts.workspace);
34
34
  this._state = 'connected';
35
35
  return this._page;
36
36
  }
@@ -48,9 +48,14 @@ export class BrowserBridge {
48
48
  this._page = null;
49
49
  this._state = 'closed';
50
50
  }
51
- async _ensureDaemon() {
52
- if (await isDaemonRunning())
51
+ async _ensureDaemon(timeoutSeconds) {
52
+ const timeoutMs = Math.max(1, timeoutSeconds ?? Math.ceil(DAEMON_SPAWN_TIMEOUT / 1000)) * 1000;
53
+ if (await isExtensionConnected())
53
54
  return;
55
+ if (await isDaemonRunning()) {
56
+ throw new Error('Daemon is running but the Browser Extension is not connected.\n' +
57
+ 'Please install and enable the opencli Browser Bridge extension in Chrome.');
58
+ }
54
59
  // Find daemon relative to this file — works for both:
55
60
  // npx tsx src/main.ts → src/browser/mcp.ts → src/daemon.ts
56
61
  // node dist/main.js → dist/browser/mcp.js → dist/daemon.js
@@ -75,7 +80,7 @@ export class BrowserBridge {
75
80
  });
76
81
  this._daemonProc.unref();
77
82
  // Wait for daemon to be ready AND extension to connect
78
- const deadline = Date.now() + DAEMON_SPAWN_TIMEOUT;
83
+ const deadline = Date.now() + timeoutMs;
79
84
  while (Date.now() < deadline) {
80
85
  await new Promise(resolve => setTimeout(resolve, 300));
81
86
  if (await isExtensionConnected())
@@ -14,14 +14,21 @@ import type { IPage } from '../types.js';
14
14
  * Page — implements IPage by talking to the daemon via HTTP.
15
15
  */
16
16
  export declare class Page implements IPage {
17
+ private readonly workspace;
18
+ constructor(workspace?: string);
17
19
  /** Active tab ID, set after navigate and used in all subsequent commands */
18
20
  private _tabId;
19
21
  /** Helper: spread tabId into command params if we have one */
20
22
  private _tabOpt;
23
+ private _workspaceOpt;
21
24
  goto(url: string): Promise<void>;
22
25
  /** Close the automation window in the extension */
23
26
  closeWindow(): Promise<void>;
24
27
  evaluate(js: string): Promise<any>;
28
+ getCookies(opts?: {
29
+ domain?: string;
30
+ url?: string;
31
+ }): Promise<any[]>;
25
32
  snapshot(opts?: {
26
33
  interactive?: boolean;
27
34
  compact?: boolean;
@@ -12,19 +12,28 @@
12
12
  import { formatSnapshot } from '../snapshotFormatter.js';
13
13
  import { sendCommand } from './daemon-client.js';
14
14
  import { wrapForEval } from './utils.js';
15
+ import { clickJs, typeTextJs, pressKeyJs, waitForTextJs, scrollJs, autoScrollJs, networkRequestsJs, } from './dom-helpers.js';
15
16
  /**
16
17
  * Page — implements IPage by talking to the daemon via HTTP.
17
18
  */
18
19
  export class Page {
20
+ workspace;
21
+ constructor(workspace = 'default') {
22
+ this.workspace = workspace;
23
+ }
19
24
  /** Active tab ID, set after navigate and used in all subsequent commands */
20
25
  _tabId;
21
26
  /** Helper: spread tabId into command params if we have one */
22
27
  _tabOpt() {
23
28
  return this._tabId !== undefined ? { tabId: this._tabId } : {};
24
29
  }
30
+ _workspaceOpt() {
31
+ return { workspace: this.workspace };
32
+ }
25
33
  async goto(url) {
26
34
  const result = await sendCommand('navigate', {
27
35
  url,
36
+ ...this._workspaceOpt(),
28
37
  ...this._tabOpt(),
29
38
  });
30
39
  // Remember the tabId for subsequent exec calls
@@ -35,7 +44,7 @@ export class Page {
35
44
  /** Close the automation window in the extension */
36
45
  async closeWindow() {
37
46
  try {
38
- await sendCommand('close-window', {});
47
+ await sendCommand('close-window', { ...this._workspaceOpt() });
39
48
  }
40
49
  catch {
41
50
  // Window may already be closed or daemon may be down
@@ -43,7 +52,11 @@ export class Page {
43
52
  }
44
53
  async evaluate(js) {
45
54
  const code = wrapForEval(js);
46
- return sendCommand('exec', { code, ...this._tabOpt() });
55
+ return sendCommand('exec', { code, ...this._workspaceOpt(), ...this._tabOpt() });
56
+ }
57
+ async getCookies(opts = {}) {
58
+ const result = await sendCommand('cookies', { ...this._workspaceOpt(), ...opts });
59
+ return Array.isArray(result) ? result : [];
47
60
  }
48
61
  async snapshot(opts = {}) {
49
62
  const maxDepth = Math.max(1, Math.min(Number(opts.maxDepth) || 50, 200));
@@ -74,7 +87,7 @@ export class Page {
74
87
  return buildTree(document.body, 0);
75
88
  })()
76
89
  `;
77
- const raw = await sendCommand('exec', { code, ...this._tabOpt() });
90
+ const raw = await sendCommand('exec', { code, ...this._workspaceOpt(), ...this._tabOpt() });
78
91
  if (opts.raw)
79
92
  return raw;
80
93
  if (typeof raw === 'string')
@@ -82,48 +95,16 @@ export class Page {
82
95
  return raw;
83
96
  }
84
97
  async click(ref) {
85
- const safeRef = JSON.stringify(ref);
86
- const code = `
87
- (() => {
88
- const ref = ${safeRef};
89
- const el = document.querySelector('[data-ref="' + ref + '"]')
90
- || document.querySelectorAll('a, button, input, [role="button"], [tabindex]')[parseInt(ref, 10) || 0];
91
- if (!el) throw new Error('Element not found: ' + ref);
92
- el.scrollIntoView({ behavior: 'instant', block: 'center' });
93
- el.click();
94
- return 'clicked';
95
- })()
96
- `;
97
- await sendCommand('exec', { code, ...this._tabOpt() });
98
+ const code = clickJs(ref);
99
+ await sendCommand('exec', { code, ...this._workspaceOpt(), ...this._tabOpt() });
98
100
  }
99
101
  async typeText(ref, text) {
100
- const safeRef = JSON.stringify(ref);
101
- const safeText = JSON.stringify(text);
102
- const code = `
103
- (() => {
104
- const ref = ${safeRef};
105
- const el = document.querySelector('[data-ref="' + ref + '"]')
106
- || document.querySelectorAll('input, textarea, [contenteditable]')[parseInt(ref, 10) || 0];
107
- if (!el) throw new Error('Element not found: ' + ref);
108
- el.focus();
109
- el.value = ${safeText};
110
- el.dispatchEvent(new Event('input', { bubbles: true }));
111
- el.dispatchEvent(new Event('change', { bubbles: true }));
112
- return 'typed';
113
- })()
114
- `;
115
- await sendCommand('exec', { code, ...this._tabOpt() });
102
+ const code = typeTextJs(ref, text);
103
+ await sendCommand('exec', { code, ...this._workspaceOpt(), ...this._tabOpt() });
116
104
  }
117
105
  async pressKey(key) {
118
- const code = `
119
- (() => {
120
- const el = document.activeElement || document.body;
121
- el.dispatchEvent(new KeyboardEvent('keydown', { key: ${JSON.stringify(key)}, bubbles: true }));
122
- el.dispatchEvent(new KeyboardEvent('keyup', { key: ${JSON.stringify(key)}, bubbles: true }));
123
- return 'pressed';
124
- })()
125
- `;
126
- await sendCommand('exec', { code, ...this._tabOpt() });
106
+ const code = pressKeyJs(key);
107
+ await sendCommand('exec', { code, ...this._workspaceOpt(), ...this._tabOpt() });
127
108
  }
128
109
  async wait(options) {
129
110
  if (typeof options === 'number') {
@@ -136,47 +117,25 @@ export class Page {
136
117
  }
137
118
  if (options.text) {
138
119
  const timeout = (options.timeout ?? 30) * 1000;
139
- const code = `
140
- new Promise((resolve, reject) => {
141
- const deadline = Date.now() + ${timeout};
142
- const check = () => {
143
- if (document.body.innerText.includes(${JSON.stringify(options.text)})) return resolve('found');
144
- if (Date.now() > deadline) return reject(new Error('Text not found: ' + ${JSON.stringify(options.text)}));
145
- setTimeout(check, 200);
146
- };
147
- check();
148
- })
149
- `;
150
- await sendCommand('exec', { code, ...this._tabOpt() });
120
+ const code = waitForTextJs(options.text, timeout);
121
+ await sendCommand('exec', { code, ...this._workspaceOpt(), ...this._tabOpt() });
151
122
  }
152
123
  }
153
124
  async tabs() {
154
- return sendCommand('tabs', { op: 'list' });
125
+ return sendCommand('tabs', { op: 'list', ...this._workspaceOpt() });
155
126
  }
156
127
  async closeTab(index) {
157
- await sendCommand('tabs', { op: 'close', ...(index !== undefined ? { index } : {}) });
128
+ await sendCommand('tabs', { op: 'close', ...this._workspaceOpt(), ...(index !== undefined ? { index } : {}) });
158
129
  }
159
130
  async newTab() {
160
- await sendCommand('tabs', { op: 'new' });
131
+ await sendCommand('tabs', { op: 'new', ...this._workspaceOpt() });
161
132
  }
162
133
  async selectTab(index) {
163
- await sendCommand('tabs', { op: 'select', index });
134
+ await sendCommand('tabs', { op: 'select', index, ...this._workspaceOpt() });
164
135
  }
165
136
  async networkRequests(includeStatic = false) {
166
- const code = `
167
- (() => {
168
- const entries = performance.getEntriesByType('resource');
169
- return entries
170
- ${includeStatic ? '' : '.filter(e => !["img", "font", "css", "script"].some(t => e.initiatorType === t))'}
171
- .map(e => ({
172
- url: e.name,
173
- type: e.initiatorType,
174
- duration: Math.round(e.duration),
175
- size: e.transferSize || 0,
176
- }));
177
- })()
178
- `;
179
- return sendCommand('exec', { code, ...this._tabOpt() });
137
+ const code = networkRequestsJs(includeStatic);
138
+ return sendCommand('exec', { code, ...this._workspaceOpt(), ...this._tabOpt() });
180
139
  }
181
140
  /**
182
141
  * Console messages are not available in lightweight daemon mode.
@@ -195,6 +154,7 @@ export class Page {
195
154
  */
196
155
  async screenshot(options = {}) {
197
156
  const base64 = await sendCommand('screenshot', {
157
+ ...this._workspaceOpt(),
198
158
  format: options.format,
199
159
  quality: options.quality,
200
160
  fullPage: options.fullPage,
@@ -204,43 +164,20 @@ export class Page {
204
164
  const fs = await import('node:fs');
205
165
  const path = await import('node:path');
206
166
  const dir = path.dirname(options.path);
207
- fs.mkdirSync(dir, { recursive: true });
208
- fs.writeFileSync(options.path, Buffer.from(base64, 'base64'));
167
+ await fs.promises.mkdir(dir, { recursive: true });
168
+ await fs.promises.writeFile(options.path, Buffer.from(base64, 'base64'));
209
169
  }
210
170
  return base64;
211
171
  }
212
172
  async scroll(direction = 'down', amount = 500) {
213
- const dx = direction === 'left' ? -amount : direction === 'right' ? amount : 0;
214
- const dy = direction === 'up' ? -amount : direction === 'down' ? amount : 0;
215
- await sendCommand('exec', {
216
- code: `window.scrollBy(${dx}, ${dy})`,
217
- ...this._tabOpt(),
218
- });
173
+ const code = scrollJs(direction, amount);
174
+ await sendCommand('exec', { code, ...this._workspaceOpt(), ...this._tabOpt() });
219
175
  }
220
176
  async autoScroll(options = {}) {
221
177
  const times = options.times ?? 3;
222
178
  const delayMs = options.delayMs ?? 2000;
223
- const code = `
224
- (async () => {
225
- for (let i = 0; i < ${times}; i++) {
226
- const lastHeight = document.body.scrollHeight;
227
- window.scrollTo(0, lastHeight);
228
- await new Promise(resolve => {
229
- let timeoutId;
230
- const observer = new MutationObserver(() => {
231
- if (document.body.scrollHeight > lastHeight) {
232
- clearTimeout(timeoutId);
233
- observer.disconnect();
234
- setTimeout(resolve, 100);
235
- }
236
- });
237
- observer.observe(document.body, { childList: true, subtree: true });
238
- timeoutId = setTimeout(() => { observer.disconnect(); resolve(null); }, ${delayMs});
239
- });
240
- }
241
- })()
242
- `;
243
- await sendCommand('exec', { code, ...this._tabOpt() });
179
+ const code = autoScrollJs(times, delayMs);
180
+ await sendCommand('exec', { code, ...this._workspaceOpt(), ...this._tabOpt() });
244
181
  }
245
182
  async installInterceptor(pattern) {
246
183
  const { generateInterceptorJs } = await import('../interceptor.js');
@@ -1,5 +1,6 @@
1
1
  import { describe, it, expect, vi } from 'vitest';
2
2
  import { BrowserBridge, __test__ } from './browser/index.js';
3
+ import * as daemonClient from './browser/daemon-client.js';
3
4
  describe('browser helpers', () => {
4
5
  it('extracts tab entries from string snapshots', () => {
5
6
  const entries = __test__.extractTabEntries('Tab 0 https://example.com\nTab 1 Chrome Extension');
@@ -94,4 +95,10 @@ describe('BrowserBridge state', () => {
94
95
  mcp._state = 'closing';
95
96
  await expect(mcp.connect()).rejects.toThrow('Session is closing');
96
97
  });
98
+ it('fails fast when daemon is running but extension is disconnected', async () => {
99
+ vi.spyOn(daemonClient, 'isExtensionConnected').mockResolvedValue(false);
100
+ vi.spyOn(daemonClient, 'isDaemonRunning').mockResolvedValue(true);
101
+ const mcp = new BrowserBridge();
102
+ await expect(mcp.connect()).rejects.toThrow('Browser Extension is not connected');
103
+ });
97
104
  });
@@ -73,7 +73,7 @@ export function parseTsArgsBlock(argsBlock) {
73
73
  const args = [];
74
74
  let cursor = 0;
75
75
  while (cursor < argsBlock.length) {
76
- const nameMatch = argsBlock.slice(cursor).match(/\{\s*name\s*:\s*['"`](\w+)['"`]/);
76
+ const nameMatch = argsBlock.slice(cursor).match(/\{\s*name\s*:\s*['"`]([^'"`]+)['"`]/);
77
77
  if (!nameMatch || nameMatch.index === undefined)
78
78
  break;
79
79
  const objectStart = cursor + nameMatch.index;
@@ -186,6 +186,8 @@ function scanTs(filePath, site) {
186
186
  const browserMatch = src.match(/browser\s*:\s*(true|false)/);
187
187
  if (browserMatch)
188
188
  entry.browser = browserMatch[1] === 'true';
189
+ else
190
+ entry.browser = entry.strategy !== 'public';
189
191
  // Extract columns
190
192
  const colMatch = src.match(/columns\s*:\s*\[([^\]]*)\]/);
191
193
  if (colMatch) {
@@ -23,4 +23,38 @@ describe('parseTsArgsBlock', () => {
23
23
  },
24
24
  ]);
25
25
  });
26
+ it('keeps hyphenated arg names from TS adapters', () => {
27
+ const args = parseTsArgsBlock(`
28
+ {
29
+ name: 'tweet-url',
30
+ help: 'Single tweet URL to download',
31
+ },
32
+ {
33
+ name: 'download-images',
34
+ type: 'boolean',
35
+ default: false,
36
+ help: 'Download images locally',
37
+ },
38
+ `);
39
+ expect(args).toEqual([
40
+ {
41
+ name: 'tweet-url',
42
+ type: 'str',
43
+ default: undefined,
44
+ required: false,
45
+ positional: undefined,
46
+ help: 'Single tweet URL to download',
47
+ choices: undefined,
48
+ },
49
+ {
50
+ name: 'download-images',
51
+ type: 'boolean',
52
+ default: false,
53
+ required: false,
54
+ positional: undefined,
55
+ help: 'Download images locally',
56
+ choices: undefined,
57
+ },
58
+ ]);
59
+ });
26
60
  });
@@ -0,0 +1,2 @@
1
+ import { type CliCommand } from './registry.js';
2
+ export declare function shouldUseBrowserSession(cmd: CliCommand): boolean;
@@ -0,0 +1,30 @@
1
+ import { Strategy } from './registry.js';
2
+ const BROWSER_ONLY_STEPS = new Set([
3
+ 'navigate',
4
+ 'click',
5
+ 'type',
6
+ 'wait',
7
+ 'press',
8
+ 'snapshot',
9
+ 'evaluate',
10
+ 'intercept',
11
+ 'tap',
12
+ ]);
13
+ function pipelineNeedsBrowserSession(pipeline) {
14
+ return pipeline.some((step) => {
15
+ if (!step || typeof step !== 'object')
16
+ return false;
17
+ return Object.keys(step).some((op) => BROWSER_ONLY_STEPS.has(op));
18
+ });
19
+ }
20
+ export function shouldUseBrowserSession(cmd) {
21
+ if (!cmd.browser)
22
+ return false;
23
+ if (cmd.func)
24
+ return true;
25
+ if (!cmd.pipeline || cmd.pipeline.length === 0)
26
+ return true;
27
+ if (cmd.strategy !== Strategy.PUBLIC)
28
+ return true;
29
+ return pipelineNeedsBrowserSession(cmd.pipeline);
30
+ }
@@ -0,0 +1 @@
1
+ export {};
@@ -0,0 +1,42 @@
1
+ import { describe, expect, it } from 'vitest';
2
+ import { Strategy } from './registry.js';
3
+ import { shouldUseBrowserSession } from './capabilityRouting.js';
4
+ function makeCmd(partial) {
5
+ return {
6
+ site: 'test',
7
+ name: 'command',
8
+ description: '',
9
+ args: [],
10
+ ...partial,
11
+ };
12
+ }
13
+ describe('shouldUseBrowserSession', () => {
14
+ it('skips browser session for public fetch-only pipelines', () => {
15
+ expect(shouldUseBrowserSession(makeCmd({
16
+ browser: true,
17
+ strategy: Strategy.PUBLIC,
18
+ pipeline: [{ fetch: 'https://example.com/api' }, { select: 'items' }],
19
+ }))).toBe(false);
20
+ });
21
+ it('keeps browser session for public pipelines with browser-only steps', () => {
22
+ expect(shouldUseBrowserSession(makeCmd({
23
+ browser: true,
24
+ strategy: Strategy.PUBLIC,
25
+ pipeline: [{ navigate: 'https://example.com' }, { evaluate: '() => []' }],
26
+ }))).toBe(true);
27
+ });
28
+ it('keeps browser session for non-public strategies', () => {
29
+ expect(shouldUseBrowserSession(makeCmd({
30
+ browser: true,
31
+ strategy: Strategy.COOKIE,
32
+ pipeline: [{ fetch: 'https://example.com/api' }],
33
+ }))).toBe(true);
34
+ });
35
+ it('keeps browser session for function adapters', () => {
36
+ expect(shouldUseBrowserSession(makeCmd({
37
+ browser: true,
38
+ strategy: Strategy.PUBLIC,
39
+ func: async () => [],
40
+ }))).toBe(true);
41
+ });
42
+ });
@@ -1,16 +1,23 @@
1
1
  import { describe, expect, it } from 'vitest';
2
2
  import { formatTimestamp, workStatusLabel } from './chaoxing.js';
3
+ function localDatePrefixFromMillis(ts) {
4
+ const d = new Date(ts);
5
+ const yyyy = d.getFullYear();
6
+ const mm = String(d.getMonth() + 1).padStart(2, '0');
7
+ const dd = String(d.getDate()).padStart(2, '0');
8
+ return `${yyyy}-${mm}-${dd}`;
9
+ }
3
10
  describe('formatTimestamp', () => {
4
11
  it('formats millisecond timestamp', () => {
5
- // 2026-01-15 08:30 UTC+8
6
12
  const ts = new Date('2026-01-15T00:30:00Z').getTime();
7
13
  const result = formatTimestamp(ts);
8
- expect(result).toMatch(/2026-01-15/);
14
+ expect(result).toMatch(new RegExp(`^${localDatePrefixFromMillis(ts)}\\s`));
9
15
  });
10
16
  it('formats second timestamp', () => {
11
- const ts = Math.floor(new Date('2026-06-01T12:00:00Z').getTime() / 1000);
17
+ const millis = new Date('2026-06-01T12:00:00Z').getTime();
18
+ const ts = Math.floor(millis / 1000);
12
19
  const result = formatTimestamp(ts);
13
- expect(result).toMatch(/2026-06-01/);
20
+ expect(result).toMatch(new RegExp(`^${localDatePrefixFromMillis(millis)}\\s`));
14
21
  });
15
22
  it('returns empty for null/undefined/0', () => {
16
23
  expect(formatTimestamp(null)).toBe('');