@jackwener/opencli 1.0.1 → 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 (253) hide show
  1. package/.github/workflows/build-extension.yml +80 -0
  2. package/.github/workflows/ci.yml +6 -6
  3. package/.github/workflows/docs.yml +52 -0
  4. package/.github/workflows/e2e-headed.yml +2 -2
  5. package/.github/workflows/pkg-pr-new.yml +2 -2
  6. package/.github/workflows/release.yml +2 -5
  7. package/.github/workflows/security.yml +2 -2
  8. package/CDP.md +1 -1
  9. package/CDP.zh-CN.md +1 -1
  10. package/README.md +42 -34
  11. package/README.zh-CN.md +42 -34
  12. package/SKILL.md +3 -5
  13. package/dist/browser/cdp.d.ts +42 -0
  14. package/dist/browser/cdp.js +339 -0
  15. package/dist/browser/daemon-client.d.ts +3 -1
  16. package/dist/browser/daemon-client.js +4 -0
  17. package/dist/browser/dom-helpers.d.ts +20 -0
  18. package/dist/browser/dom-helpers.js +109 -0
  19. package/dist/browser/index.d.ts +3 -0
  20. package/dist/browser/index.js +4 -0
  21. package/dist/browser/mcp.d.ts +1 -0
  22. package/dist/browser/mcp.js +10 -5
  23. package/dist/browser/page.d.ts +7 -0
  24. package/dist/browser/page.js +39 -123
  25. package/dist/browser/utils.d.ts +10 -0
  26. package/dist/browser/utils.js +27 -0
  27. package/dist/browser.test.js +49 -1
  28. package/dist/build-manifest.js +3 -1
  29. package/dist/build-manifest.test.js +34 -0
  30. package/dist/capabilityRouting.d.ts +2 -0
  31. package/dist/capabilityRouting.js +30 -0
  32. package/dist/capabilityRouting.test.d.ts +1 -0
  33. package/dist/capabilityRouting.test.js +42 -0
  34. package/dist/chaoxing.d.ts +58 -0
  35. package/dist/chaoxing.js +225 -0
  36. package/dist/chaoxing.test.d.ts +1 -0
  37. package/dist/chaoxing.test.js +45 -0
  38. package/dist/cli-manifest.json +885 -48
  39. package/dist/cli.d.ts +1 -0
  40. package/dist/cli.js +234 -0
  41. package/dist/clis/antigravity/serve.d.ts +14 -0
  42. package/dist/clis/antigravity/serve.js +263 -0
  43. package/dist/clis/bilibili/download.js +4 -14
  44. package/dist/clis/boss/chatlist.d.ts +1 -0
  45. package/dist/clis/boss/chatlist.js +50 -0
  46. package/dist/clis/boss/chatmsg.d.ts +1 -0
  47. package/dist/clis/boss/chatmsg.js +73 -0
  48. package/dist/clis/boss/resume.d.ts +1 -0
  49. package/dist/clis/boss/resume.js +249 -0
  50. package/dist/clis/boss/send.d.ts +1 -0
  51. package/dist/clis/boss/send.js +176 -0
  52. package/dist/clis/chaoxing/assignments.d.ts +1 -0
  53. package/dist/clis/chaoxing/assignments.js +74 -0
  54. package/dist/clis/chaoxing/exams.d.ts +1 -0
  55. package/dist/clis/chaoxing/exams.js +74 -0
  56. package/dist/clis/chatgpt/ask.js +15 -14
  57. package/dist/clis/chatgpt/ax.d.ts +1 -0
  58. package/dist/clis/chatgpt/ax.js +78 -0
  59. package/dist/clis/chatgpt/read.js +5 -6
  60. package/dist/clis/hf/top.d.ts +1 -0
  61. package/dist/clis/hf/top.js +119 -0
  62. package/dist/clis/jike/comment.d.ts +1 -0
  63. package/dist/clis/jike/comment.js +107 -0
  64. package/dist/clis/jike/create.d.ts +1 -0
  65. package/dist/clis/jike/create.js +106 -0
  66. package/dist/clis/jike/feed.d.ts +1 -0
  67. package/dist/clis/jike/feed.js +67 -0
  68. package/dist/clis/jike/like.d.ts +1 -0
  69. package/dist/clis/jike/like.js +61 -0
  70. package/dist/clis/jike/notifications.d.ts +1 -0
  71. package/dist/clis/jike/notifications.js +169 -0
  72. package/dist/clis/jike/post.yaml +58 -0
  73. package/dist/clis/jike/repost.d.ts +1 -0
  74. package/dist/clis/jike/repost.js +103 -0
  75. package/dist/clis/jike/search.d.ts +1 -0
  76. package/dist/clis/jike/search.js +67 -0
  77. package/dist/clis/jike/shared.d.ts +19 -0
  78. package/dist/clis/jike/shared.js +25 -0
  79. package/dist/clis/jike/topic.yaml +52 -0
  80. package/dist/clis/jike/user.yaml +51 -0
  81. package/dist/clis/smzdm/search.js +28 -39
  82. package/dist/clis/stackoverflow/bounties.yaml +29 -0
  83. package/dist/clis/stackoverflow/hot.yaml +28 -0
  84. package/dist/clis/stackoverflow/search.yaml +32 -0
  85. package/dist/clis/stackoverflow/unanswered.yaml +28 -0
  86. package/dist/clis/twitter/download.js +6 -16
  87. package/dist/clis/twitter/post.js +9 -2
  88. package/dist/clis/twitter/search.js +14 -33
  89. package/dist/clis/xiaohongshu/download.d.ts +1 -1
  90. package/dist/clis/xiaohongshu/download.js +4 -4
  91. package/dist/clis/zhihu/download.js +3 -3
  92. package/dist/doctor.d.ts +7 -0
  93. package/dist/doctor.js +16 -0
  94. package/dist/download/index.d.ts +12 -8
  95. package/dist/download/index.js +11 -3
  96. package/dist/download/index.test.d.ts +1 -0
  97. package/dist/download/index.test.js +14 -0
  98. package/dist/engine.js +25 -14
  99. package/dist/explore.d.ts +1 -0
  100. package/dist/explore.js +48 -103
  101. package/dist/generate.js +1 -0
  102. package/dist/interceptor.js +3 -2
  103. package/dist/main.js +4 -193
  104. package/dist/output.d.ts +2 -1
  105. package/dist/output.js +3 -1
  106. package/dist/pipeline/executor.test.js +1 -0
  107. package/dist/pipeline/steps/download.js +14 -18
  108. package/dist/registry.d.ts +4 -3
  109. package/dist/registry.js +5 -2
  110. package/dist/runtime.d.ts +4 -1
  111. package/dist/runtime.js +2 -2
  112. package/dist/scripts/framework.d.ts +4 -0
  113. package/dist/scripts/framework.js +21 -0
  114. package/dist/scripts/interact.d.ts +4 -0
  115. package/dist/scripts/interact.js +20 -0
  116. package/dist/scripts/store.d.ts +9 -0
  117. package/dist/scripts/store.js +44 -0
  118. package/dist/synthesize.js +1 -1
  119. package/dist/types.d.ts +12 -0
  120. package/dist/verify.d.ts +6 -1
  121. package/dist/verify.js +54 -2
  122. package/docs/.vitepress/config.mts +193 -0
  123. package/docs/adapters/browser/apple-podcasts.md +28 -0
  124. package/docs/adapters/browser/bbc.md +26 -0
  125. package/docs/adapters/browser/bilibili.md +38 -0
  126. package/docs/adapters/browser/boss.md +28 -0
  127. package/docs/adapters/browser/coupang.md +28 -0
  128. package/docs/adapters/browser/ctrip.md +27 -0
  129. package/docs/adapters/browser/github.md +26 -0
  130. package/docs/adapters/browser/hackernews.md +26 -0
  131. package/docs/adapters/browser/linkedin.md +27 -0
  132. package/docs/adapters/browser/reddit.md +41 -0
  133. package/docs/adapters/browser/reuters.md +27 -0
  134. package/docs/adapters/browser/smzdm.md +27 -0
  135. package/docs/adapters/browser/twitter.md +47 -0
  136. package/docs/adapters/browser/v2ex.md +32 -0
  137. package/docs/adapters/browser/weibo.md +27 -0
  138. package/docs/adapters/browser/xiaohongshu.md +32 -0
  139. package/docs/adapters/browser/xiaoyuzhou.md +28 -0
  140. package/docs/adapters/browser/xueqiu.md +32 -0
  141. package/docs/adapters/browser/yahoo-finance.md +26 -0
  142. package/docs/adapters/browser/youtube.md +29 -0
  143. package/docs/adapters/browser/zhihu.md +30 -0
  144. package/docs/adapters/desktop/antigravity.md +46 -0
  145. package/docs/adapters/desktop/chatgpt.md +43 -0
  146. package/docs/adapters/desktop/chatwise.md +38 -0
  147. package/docs/adapters/desktop/codex.md +32 -0
  148. package/docs/adapters/desktop/cursor.md +33 -0
  149. package/docs/adapters/desktop/discord.md +28 -0
  150. package/docs/adapters/desktop/feishu.md +20 -0
  151. package/docs/adapters/desktop/neteasemusic.md +31 -0
  152. package/docs/adapters/desktop/notion.md +29 -0
  153. package/docs/adapters/desktop/wechat.md +28 -0
  154. package/docs/adapters/index.md +49 -0
  155. package/docs/advanced/cdp.md +103 -0
  156. package/docs/advanced/download.md +63 -0
  157. package/docs/advanced/electron.md +125 -0
  158. package/docs/advanced/remote-chrome.md +72 -0
  159. package/docs/developer/ai-workflow.md +66 -0
  160. package/docs/developer/architecture.md +90 -0
  161. package/docs/developer/contributing.md +136 -0
  162. package/docs/developer/testing.md +237 -0
  163. package/docs/developer/ts-adapter.md +87 -0
  164. package/docs/developer/yaml-adapter.md +108 -0
  165. package/docs/guide/browser-bridge.md +38 -0
  166. package/docs/guide/getting-started.md +56 -0
  167. package/docs/guide/installation.md +37 -0
  168. package/docs/guide/troubleshooting.md +56 -0
  169. package/docs/index.md +35 -0
  170. package/docs/zh/adapters/index.md +5 -0
  171. package/docs/zh/advanced/cdp.md +3 -0
  172. package/docs/zh/developer/contributing.md +24 -0
  173. package/docs/zh/guide/browser-bridge.md +25 -0
  174. package/docs/zh/guide/getting-started.md +40 -0
  175. package/docs/zh/guide/installation.md +37 -0
  176. package/docs/zh/index.md +29 -0
  177. package/extension/dist/background.js +386 -438
  178. package/extension/manifest.json +2 -2
  179. package/extension/package-lock.json +1156 -0
  180. package/extension/src/background.test.ts +151 -0
  181. package/extension/src/background.ts +124 -53
  182. package/extension/src/protocol.ts +3 -1
  183. package/package.json +7 -3
  184. package/src/browser/cdp.ts +367 -0
  185. package/src/browser/daemon-client.ts +7 -1
  186. package/src/browser/dom-helpers.ts +116 -0
  187. package/src/browser/index.ts +4 -0
  188. package/src/browser/mcp.ts +14 -6
  189. package/src/browser/page.ts +47 -124
  190. package/src/browser/utils.ts +27 -0
  191. package/src/browser.test.ts +56 -0
  192. package/src/build-manifest.test.ts +36 -0
  193. package/src/build-manifest.ts +2 -1
  194. package/src/capabilityRouting.test.ts +47 -0
  195. package/src/capabilityRouting.ts +28 -0
  196. package/src/chaoxing.test.ts +53 -0
  197. package/src/chaoxing.ts +268 -0
  198. package/src/cli.ts +205 -0
  199. package/src/clis/antigravity/SKILL.md +5 -0
  200. package/src/clis/antigravity/serve.ts +329 -0
  201. package/src/clis/bilibili/download.ts +4 -15
  202. package/src/clis/boss/chatlist.ts +50 -0
  203. package/src/clis/boss/chatmsg.ts +70 -0
  204. package/src/clis/boss/resume.ts +262 -0
  205. package/src/clis/boss/send.ts +193 -0
  206. package/src/clis/chaoxing/README.md +36 -0
  207. package/src/clis/chaoxing/README.zh-CN.md +35 -0
  208. package/src/clis/chaoxing/assignments.ts +88 -0
  209. package/src/clis/chaoxing/exams.ts +88 -0
  210. package/src/clis/chatgpt/ask.ts +14 -15
  211. package/src/clis/chatgpt/ax.ts +81 -0
  212. package/src/clis/chatgpt/read.ts +5 -7
  213. package/src/clis/hf/top.ts +141 -0
  214. package/src/clis/jike/comment.ts +113 -0
  215. package/src/clis/jike/create.ts +113 -0
  216. package/src/clis/jike/feed.ts +74 -0
  217. package/src/clis/jike/like.ts +65 -0
  218. package/src/clis/jike/notifications.ts +185 -0
  219. package/src/clis/jike/post.yaml +58 -0
  220. package/src/clis/jike/repost.ts +114 -0
  221. package/src/clis/jike/search.ts +74 -0
  222. package/src/clis/jike/shared.ts +36 -0
  223. package/src/clis/jike/topic.yaml +52 -0
  224. package/src/clis/jike/user.yaml +51 -0
  225. package/src/clis/smzdm/search.ts +30 -39
  226. package/src/clis/stackoverflow/bounties.yaml +29 -0
  227. package/src/clis/stackoverflow/hot.yaml +28 -0
  228. package/src/clis/stackoverflow/search.yaml +32 -0
  229. package/src/clis/stackoverflow/unanswered.yaml +28 -0
  230. package/src/clis/twitter/download.ts +6 -17
  231. package/src/clis/twitter/post.ts +9 -2
  232. package/src/clis/twitter/search.ts +15 -33
  233. package/src/clis/xiaohongshu/download.ts +4 -4
  234. package/src/clis/zhihu/download.ts +3 -3
  235. package/src/doctor.ts +18 -2
  236. package/src/download/index.test.ts +16 -0
  237. package/src/download/index.ts +22 -4
  238. package/src/engine.ts +20 -13
  239. package/src/explore.ts +54 -103
  240. package/src/generate.ts +1 -0
  241. package/src/interceptor.ts +3 -2
  242. package/src/main.ts +4 -180
  243. package/src/output.ts +15 -13
  244. package/src/pipeline/executor.test.ts +1 -0
  245. package/src/pipeline/steps/download.ts +14 -17
  246. package/src/registry.ts +9 -5
  247. package/src/runtime.ts +3 -2
  248. package/src/scripts/framework.ts +20 -0
  249. package/src/scripts/interact.ts +22 -0
  250. package/src/scripts/store.ts +40 -0
  251. package/src/synthesize.ts +1 -1
  252. package/src/types.ts +9 -0
  253. package/src/verify.ts +64 -3
@@ -11,19 +11,29 @@
11
11
  */
12
12
  import { formatSnapshot } from '../snapshotFormatter.js';
13
13
  import { sendCommand } from './daemon-client.js';
14
+ import { wrapForEval } from './utils.js';
15
+ import { clickJs, typeTextJs, pressKeyJs, waitForTextJs, scrollJs, autoScrollJs, networkRequestsJs, } from './dom-helpers.js';
14
16
  /**
15
17
  * Page — implements IPage by talking to the daemon via HTTP.
16
18
  */
17
19
  export class Page {
20
+ workspace;
21
+ constructor(workspace = 'default') {
22
+ this.workspace = workspace;
23
+ }
18
24
  /** Active tab ID, set after navigate and used in all subsequent commands */
19
25
  _tabId;
20
26
  /** Helper: spread tabId into command params if we have one */
21
27
  _tabOpt() {
22
28
  return this._tabId !== undefined ? { tabId: this._tabId } : {};
23
29
  }
30
+ _workspaceOpt() {
31
+ return { workspace: this.workspace };
32
+ }
24
33
  async goto(url) {
25
34
  const result = await sendCommand('navigate', {
26
35
  url,
36
+ ...this._workspaceOpt(),
27
37
  ...this._tabOpt(),
28
38
  });
29
39
  // Remember the tabId for subsequent exec calls
@@ -34,7 +44,7 @@ export class Page {
34
44
  /** Close the automation window in the extension */
35
45
  async closeWindow() {
36
46
  try {
37
- await sendCommand('close-window', {});
47
+ await sendCommand('close-window', { ...this._workspaceOpt() });
38
48
  }
39
49
  catch {
40
50
  // Window may already be closed or daemon may be down
@@ -42,7 +52,11 @@ export class Page {
42
52
  }
43
53
  async evaluate(js) {
44
54
  const code = wrapForEval(js);
45
- 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 : [];
46
60
  }
47
61
  async snapshot(opts = {}) {
48
62
  const maxDepth = Math.max(1, Math.min(Number(opts.maxDepth) || 50, 200));
@@ -73,7 +87,7 @@ export class Page {
73
87
  return buildTree(document.body, 0);
74
88
  })()
75
89
  `;
76
- const raw = await sendCommand('exec', { code, ...this._tabOpt() });
90
+ const raw = await sendCommand('exec', { code, ...this._workspaceOpt(), ...this._tabOpt() });
77
91
  if (opts.raw)
78
92
  return raw;
79
93
  if (typeof raw === 'string')
@@ -81,48 +95,16 @@ export class Page {
81
95
  return raw;
82
96
  }
83
97
  async click(ref) {
84
- const safeRef = JSON.stringify(ref);
85
- const code = `
86
- (() => {
87
- const ref = ${safeRef};
88
- const el = document.querySelector('[data-ref="' + ref + '"]')
89
- || document.querySelectorAll('a, button, input, [role="button"], [tabindex]')[parseInt(ref, 10) || 0];
90
- if (!el) throw new Error('Element not found: ' + ref);
91
- el.scrollIntoView({ behavior: 'instant', block: 'center' });
92
- el.click();
93
- return 'clicked';
94
- })()
95
- `;
96
- await sendCommand('exec', { code, ...this._tabOpt() });
98
+ const code = clickJs(ref);
99
+ await sendCommand('exec', { code, ...this._workspaceOpt(), ...this._tabOpt() });
97
100
  }
98
101
  async typeText(ref, text) {
99
- const safeRef = JSON.stringify(ref);
100
- const safeText = JSON.stringify(text);
101
- const code = `
102
- (() => {
103
- const ref = ${safeRef};
104
- const el = document.querySelector('[data-ref="' + ref + '"]')
105
- || document.querySelectorAll('input, textarea, [contenteditable]')[parseInt(ref, 10) || 0];
106
- if (!el) throw new Error('Element not found: ' + ref);
107
- el.focus();
108
- el.value = ${safeText};
109
- el.dispatchEvent(new Event('input', { bubbles: true }));
110
- el.dispatchEvent(new Event('change', { bubbles: true }));
111
- return 'typed';
112
- })()
113
- `;
114
- await sendCommand('exec', { code, ...this._tabOpt() });
102
+ const code = typeTextJs(ref, text);
103
+ await sendCommand('exec', { code, ...this._workspaceOpt(), ...this._tabOpt() });
115
104
  }
116
105
  async pressKey(key) {
117
- const code = `
118
- (() => {
119
- const el = document.activeElement || document.body;
120
- el.dispatchEvent(new KeyboardEvent('keydown', { key: ${JSON.stringify(key)}, bubbles: true }));
121
- el.dispatchEvent(new KeyboardEvent('keyup', { key: ${JSON.stringify(key)}, bubbles: true }));
122
- return 'pressed';
123
- })()
124
- `;
125
- await sendCommand('exec', { code, ...this._tabOpt() });
106
+ const code = pressKeyJs(key);
107
+ await sendCommand('exec', { code, ...this._workspaceOpt(), ...this._tabOpt() });
126
108
  }
127
109
  async wait(options) {
128
110
  if (typeof options === 'number') {
@@ -135,47 +117,25 @@ export class Page {
135
117
  }
136
118
  if (options.text) {
137
119
  const timeout = (options.timeout ?? 30) * 1000;
138
- const code = `
139
- new Promise((resolve, reject) => {
140
- const deadline = Date.now() + ${timeout};
141
- const check = () => {
142
- if (document.body.innerText.includes(${JSON.stringify(options.text)})) return resolve('found');
143
- if (Date.now() > deadline) return reject(new Error('Text not found: ' + ${JSON.stringify(options.text)}));
144
- setTimeout(check, 200);
145
- };
146
- check();
147
- })
148
- `;
149
- await sendCommand('exec', { code, ...this._tabOpt() });
120
+ const code = waitForTextJs(options.text, timeout);
121
+ await sendCommand('exec', { code, ...this._workspaceOpt(), ...this._tabOpt() });
150
122
  }
151
123
  }
152
124
  async tabs() {
153
- return sendCommand('tabs', { op: 'list' });
125
+ return sendCommand('tabs', { op: 'list', ...this._workspaceOpt() });
154
126
  }
155
127
  async closeTab(index) {
156
- await sendCommand('tabs', { op: 'close', ...(index !== undefined ? { index } : {}) });
128
+ await sendCommand('tabs', { op: 'close', ...this._workspaceOpt(), ...(index !== undefined ? { index } : {}) });
157
129
  }
158
130
  async newTab() {
159
- await sendCommand('tabs', { op: 'new' });
131
+ await sendCommand('tabs', { op: 'new', ...this._workspaceOpt() });
160
132
  }
161
133
  async selectTab(index) {
162
- await sendCommand('tabs', { op: 'select', index });
134
+ await sendCommand('tabs', { op: 'select', index, ...this._workspaceOpt() });
163
135
  }
164
136
  async networkRequests(includeStatic = false) {
165
- const code = `
166
- (() => {
167
- const entries = performance.getEntriesByType('resource');
168
- return entries
169
- ${includeStatic ? '' : '.filter(e => !["img", "font", "css", "script"].some(t => e.initiatorType === t))'}
170
- .map(e => ({
171
- url: e.name,
172
- type: e.initiatorType,
173
- duration: Math.round(e.duration),
174
- size: e.transferSize || 0,
175
- }));
176
- })()
177
- `;
178
- return sendCommand('exec', { code, ...this._tabOpt() });
137
+ const code = networkRequestsJs(includeStatic);
138
+ return sendCommand('exec', { code, ...this._workspaceOpt(), ...this._tabOpt() });
179
139
  }
180
140
  /**
181
141
  * Console messages are not available in lightweight daemon mode.
@@ -194,6 +154,7 @@ export class Page {
194
154
  */
195
155
  async screenshot(options = {}) {
196
156
  const base64 = await sendCommand('screenshot', {
157
+ ...this._workspaceOpt(),
197
158
  format: options.format,
198
159
  quality: options.quality,
199
160
  fullPage: options.fullPage,
@@ -203,43 +164,20 @@ export class Page {
203
164
  const fs = await import('node:fs');
204
165
  const path = await import('node:path');
205
166
  const dir = path.dirname(options.path);
206
- fs.mkdirSync(dir, { recursive: true });
207
- 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'));
208
169
  }
209
170
  return base64;
210
171
  }
211
172
  async scroll(direction = 'down', amount = 500) {
212
- const dx = direction === 'left' ? -amount : direction === 'right' ? amount : 0;
213
- const dy = direction === 'up' ? -amount : direction === 'down' ? amount : 0;
214
- await sendCommand('exec', {
215
- code: `window.scrollBy(${dx}, ${dy})`,
216
- ...this._tabOpt(),
217
- });
173
+ const code = scrollJs(direction, amount);
174
+ await sendCommand('exec', { code, ...this._workspaceOpt(), ...this._tabOpt() });
218
175
  }
219
176
  async autoScroll(options = {}) {
220
177
  const times = options.times ?? 3;
221
178
  const delayMs = options.delayMs ?? 2000;
222
- const code = `
223
- (async () => {
224
- for (let i = 0; i < ${times}; i++) {
225
- const lastHeight = document.body.scrollHeight;
226
- window.scrollTo(0, lastHeight);
227
- await new Promise(resolve => {
228
- let timeoutId;
229
- const observer = new MutationObserver(() => {
230
- if (document.body.scrollHeight > lastHeight) {
231
- clearTimeout(timeoutId);
232
- observer.disconnect();
233
- setTimeout(resolve, 100);
234
- }
235
- });
236
- observer.observe(document.body, { childList: true, subtree: true });
237
- timeoutId = setTimeout(() => { observer.disconnect(); resolve(null); }, ${delayMs});
238
- });
239
- }
240
- })()
241
- `;
242
- await sendCommand('exec', { code, ...this._tabOpt() });
179
+ const code = autoScrollJs(times, delayMs);
180
+ await sendCommand('exec', { code, ...this._workspaceOpt(), ...this._tabOpt() });
243
181
  }
244
182
  async installInterceptor(pattern) {
245
183
  const { generateInterceptorJs } = await import('../interceptor.js');
@@ -257,26 +195,4 @@ export class Page {
257
195
  return result || [];
258
196
  }
259
197
  }
260
- // ─── Helpers ─────────────────────────────────────────────────────────
261
- /**
262
- * Wrap JS code for CDP Runtime.evaluate:
263
- * - Already an IIFE `(...)()` → send as-is
264
- * - Arrow/function literal → wrap as IIFE `(code)()`
265
- * - `new Promise(...)` or raw expression → send as-is (expression)
266
- */
267
- function wrapForEval(js) {
268
- const code = js.trim();
269
- if (!code)
270
- return 'undefined';
271
- // Already an IIFE: `(async () => { ... })()` or `(function() {...})()`
272
- if (/^\([\s\S]*\)\s*\(.*\)\s*$/.test(code))
273
- return code;
274
- // Arrow function: `() => ...` or `async () => ...`
275
- if (/^(async\s+)?(\([^)]*\)|[A-Za-z_]\w*)\s*=>/.test(code))
276
- return `(${code})()`;
277
- // Function declaration: `function ...` or `async function ...`
278
- if (/^(async\s+)?function[\s(]/.test(code))
279
- return `(${code})()`;
280
- // Everything else: bare expression, `new Promise(...)`, etc. → evaluate directly
281
- return code;
282
- }
198
+ // (End of file)
@@ -0,0 +1,10 @@
1
+ /**
2
+ * Utility functions for browser operations
3
+ */
4
+ /**
5
+ * Wrap JS code for CDP Runtime.evaluate:
6
+ * - Already an IIFE `(...)()` → send as-is
7
+ * - Arrow/function literal → wrap as IIFE `(code)()`
8
+ * - `new Promise(...)` or raw expression → send as-is (expression)
9
+ */
10
+ export declare function wrapForEval(js: string): string;
@@ -0,0 +1,27 @@
1
+ /**
2
+ * Utility functions for browser operations
3
+ */
4
+ /**
5
+ * Wrap JS code for CDP Runtime.evaluate:
6
+ * - Already an IIFE `(...)()` → send as-is
7
+ * - Arrow/function literal → wrap as IIFE `(code)()`
8
+ * - `new Promise(...)` or raw expression → send as-is (expression)
9
+ */
10
+ export function wrapForEval(js) {
11
+ if (typeof js !== 'string')
12
+ return 'undefined';
13
+ const code = js.trim();
14
+ if (!code)
15
+ return 'undefined';
16
+ // Already an IIFE: `(async () => { ... })()` or `(function() {...})()`
17
+ if (/^\([\s\S]*\)\s*\(.*\)\s*$/.test(code))
18
+ return code;
19
+ // Arrow function: `() => ...` or `async () => ...`
20
+ if (/^(async\s+)?(\([^)]*\)|[A-Za-z_]\w*)\s*=>/.test(code))
21
+ return `(${code})()`;
22
+ // Function declaration: `function ...` or `async function ...`
23
+ if (/^(async\s+)?function[\s(]/.test(code))
24
+ return `(${code})()`;
25
+ // Everything else: bare expression, `new Promise(...)`, etc. → evaluate directly
26
+ return code;
27
+ }
@@ -1,5 +1,6 @@
1
- import { describe, it, expect } from 'vitest';
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');
@@ -30,6 +31,47 @@ describe('browser helpers', () => {
30
31
  it('times out slow promises', async () => {
31
32
  await expect(__test__.withTimeoutMs(new Promise(() => { }), 10, 'timeout')).rejects.toThrow('timeout');
32
33
  });
34
+ it('prefers the real Electron app target over DevTools and blank pages', () => {
35
+ const target = __test__.selectCDPTarget([
36
+ {
37
+ type: 'page',
38
+ title: 'DevTools - localhost:9224',
39
+ url: 'devtools://devtools/bundled/inspector.html',
40
+ webSocketDebuggerUrl: 'ws://127.0.0.1:9224/devtools',
41
+ },
42
+ {
43
+ type: 'page',
44
+ title: '',
45
+ url: 'about:blank',
46
+ webSocketDebuggerUrl: 'ws://127.0.0.1:9224/blank',
47
+ },
48
+ {
49
+ type: 'app',
50
+ title: 'Antigravity',
51
+ url: 'http://localhost:3000/',
52
+ webSocketDebuggerUrl: 'ws://127.0.0.1:9224/app',
53
+ },
54
+ ]);
55
+ expect(target?.webSocketDebuggerUrl).toBe('ws://127.0.0.1:9224/app');
56
+ });
57
+ it('honors OPENCLI_CDP_TARGET when multiple inspectable targets exist', () => {
58
+ vi.stubEnv('OPENCLI_CDP_TARGET', 'codex');
59
+ const target = __test__.selectCDPTarget([
60
+ {
61
+ type: 'app',
62
+ title: 'Cursor',
63
+ url: 'http://localhost:3000/cursor',
64
+ webSocketDebuggerUrl: 'ws://127.0.0.1:9226/cursor',
65
+ },
66
+ {
67
+ type: 'app',
68
+ title: 'OpenAI Codex',
69
+ url: 'http://localhost:3000/codex',
70
+ webSocketDebuggerUrl: 'ws://127.0.0.1:9226/codex',
71
+ },
72
+ ]);
73
+ expect(target?.webSocketDebuggerUrl).toBe('ws://127.0.0.1:9226/codex');
74
+ });
33
75
  });
34
76
  describe('BrowserBridge state', () => {
35
77
  it('transitions to closed after close()', async () => {
@@ -53,4 +95,10 @@ describe('BrowserBridge state', () => {
53
95
  mcp._state = 'closing';
54
96
  await expect(mcp.connect()).rejects.toThrow('Session is closing');
55
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
+ });
56
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
+ });
@@ -0,0 +1,58 @@
1
+ /**
2
+ * Chaoxing (学习通) shared helpers.
3
+ *
4
+ * Flow: initSession → getCourses → enterCourse → getTabIframeUrl → navigate → parse DOM
5
+ * Chaoxing has no flat "list all assignments" API; data is behind session-gated
6
+ * course pages loaded as iframes.
7
+ */
8
+ import type { IPage } from './types.js';
9
+ /** Sleep for given milliseconds (anti-scraping delay). */
10
+ export declare function sleep(ms: number): Promise<void>;
11
+ /** Execute a credentialed fetch in the browser context, returning JSON or text. */
12
+ export declare function fetchChaoxing(page: IPage, url: string): Promise<any>;
13
+ /** Format a timestamp (seconds or milliseconds or date string) to YYYY-MM-DD HH:mm. */
14
+ export declare function formatTimestamp(ts: unknown): string;
15
+ /** Map numeric work status to Chinese label. */
16
+ export declare function workStatusLabel(status: unknown): string;
17
+ export interface ChaoxingCourse {
18
+ courseId: string;
19
+ classId: string;
20
+ cpi: string;
21
+ title: string;
22
+ }
23
+ /** Fetch enrolled course list via backclazzdata JSON API. */
24
+ export declare function getCourses(page: IPage): Promise<ChaoxingCourse[]>;
25
+ /** Navigate to the interaction page to establish a Chaoxing session. */
26
+ export declare function initSession(page: IPage): Promise<void>;
27
+ /**
28
+ * Enter a course via stucoursemiddle redirect (establishes course session + enc).
29
+ * After this call the browser is on the course page.
30
+ */
31
+ export declare function enterCourse(page: IPage, course: ChaoxingCourse): Promise<void>;
32
+ /**
33
+ * On the course page, click a tab (作业 / 考试) and return the iframe src
34
+ * that gets loaded. Returns empty string if the tab is not found.
35
+ */
36
+ export declare function getTabIframeUrl(page: IPage, tabName: string): Promise<string>;
37
+ export interface AssignmentRow {
38
+ course: string;
39
+ title: string;
40
+ deadline: string;
41
+ status: string;
42
+ score: string;
43
+ }
44
+ /**
45
+ * Parse assignments from the current page DOM (the 作业列表 page).
46
+ * The page uses `.ulDiv li` items with status/deadline/score info.
47
+ */
48
+ export declare function parseAssignmentsFromDom(page: IPage, courseName: string): Promise<AssignmentRow[]>;
49
+ export interface ExamRow {
50
+ course: string;
51
+ title: string;
52
+ start: string;
53
+ end: string;
54
+ status: string;
55
+ score: string;
56
+ }
57
+ /** Parse exams from the current page DOM (the 考试列表 page). */
58
+ export declare function parseExamsFromDom(page: IPage, courseName: string): Promise<ExamRow[]>;