@jackwener/opencli 1.0.3 → 1.0.5

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 +48 -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 +30 -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
@@ -14,11 +14,22 @@ import { formatSnapshot } from '../snapshotFormatter.js';
14
14
  import type { IPage } from '../types.js';
15
15
  import { sendCommand } from './daemon-client.js';
16
16
  import { wrapForEval } from './utils.js';
17
+ import {
18
+ clickJs,
19
+ typeTextJs,
20
+ pressKeyJs,
21
+ waitForTextJs,
22
+ scrollJs,
23
+ autoScrollJs,
24
+ networkRequestsJs,
25
+ } from './dom-helpers.js';
17
26
 
18
27
  /**
19
28
  * Page — implements IPage by talking to the daemon via HTTP.
20
29
  */
21
30
  export class Page implements IPage {
31
+ constructor(private readonly workspace: string = 'default') {}
32
+
22
33
  /** Active tab ID, set after navigate and used in all subsequent commands */
23
34
  private _tabId: number | undefined;
24
35
 
@@ -27,9 +38,14 @@ export class Page implements IPage {
27
38
  return this._tabId !== undefined ? { tabId: this._tabId } : {};
28
39
  }
29
40
 
41
+ private _workspaceOpt(): { workspace: string } {
42
+ return { workspace: this.workspace };
43
+ }
44
+
30
45
  async goto(url: string): Promise<void> {
31
46
  const result = await sendCommand('navigate', {
32
47
  url,
48
+ ...this._workspaceOpt(),
33
49
  ...this._tabOpt(),
34
50
  }) as { tabId?: number };
35
51
  // Remember the tabId for subsequent exec calls
@@ -41,7 +57,7 @@ export class Page implements IPage {
41
57
  /** Close the automation window in the extension */
42
58
  async closeWindow(): Promise<void> {
43
59
  try {
44
- await sendCommand('close-window', {});
60
+ await sendCommand('close-window', { ...this._workspaceOpt() });
45
61
  } catch {
46
62
  // Window may already be closed or daemon may be down
47
63
  }
@@ -49,7 +65,12 @@ export class Page implements IPage {
49
65
 
50
66
  async evaluate(js: string): Promise<any> {
51
67
  const code = wrapForEval(js);
52
- return sendCommand('exec', { code, ...this._tabOpt() });
68
+ return sendCommand('exec', { code, ...this._workspaceOpt(), ...this._tabOpt() });
69
+ }
70
+
71
+ async getCookies(opts: { domain?: string; url?: string } = {}): Promise<any[]> {
72
+ const result = await sendCommand('cookies', { ...this._workspaceOpt(), ...opts });
73
+ return Array.isArray(result) ? result : [];
53
74
  }
54
75
 
55
76
  async snapshot(opts: { interactive?: boolean; compact?: boolean; maxDepth?: number; raw?: boolean } = {}): Promise<any> {
@@ -81,57 +102,25 @@ export class Page implements IPage {
81
102
  return buildTree(document.body, 0);
82
103
  })()
83
104
  `;
84
- const raw = await sendCommand('exec', { code, ...this._tabOpt() });
105
+ const raw = await sendCommand('exec', { code, ...this._workspaceOpt(), ...this._tabOpt() });
85
106
  if (opts.raw) return raw;
86
107
  if (typeof raw === 'string') return formatSnapshot(raw, opts);
87
108
  return raw;
88
109
  }
89
110
 
90
111
  async click(ref: string): Promise<void> {
91
- const safeRef = JSON.stringify(ref);
92
- const code = `
93
- (() => {
94
- const ref = ${safeRef};
95
- const el = document.querySelector('[data-ref="' + ref + '"]')
96
- || document.querySelectorAll('a, button, input, [role="button"], [tabindex]')[parseInt(ref, 10) || 0];
97
- if (!el) throw new Error('Element not found: ' + ref);
98
- el.scrollIntoView({ behavior: 'instant', block: 'center' });
99
- el.click();
100
- return 'clicked';
101
- })()
102
- `;
103
- await sendCommand('exec', { code, ...this._tabOpt() });
112
+ const code = clickJs(ref);
113
+ await sendCommand('exec', { code, ...this._workspaceOpt(), ...this._tabOpt() });
104
114
  }
105
115
 
106
116
  async typeText(ref: string, text: string): Promise<void> {
107
- const safeRef = JSON.stringify(ref);
108
- const safeText = JSON.stringify(text);
109
- const code = `
110
- (() => {
111
- const ref = ${safeRef};
112
- const el = document.querySelector('[data-ref="' + ref + '"]')
113
- || document.querySelectorAll('input, textarea, [contenteditable]')[parseInt(ref, 10) || 0];
114
- if (!el) throw new Error('Element not found: ' + ref);
115
- el.focus();
116
- el.value = ${safeText};
117
- el.dispatchEvent(new Event('input', { bubbles: true }));
118
- el.dispatchEvent(new Event('change', { bubbles: true }));
119
- return 'typed';
120
- })()
121
- `;
122
- await sendCommand('exec', { code, ...this._tabOpt() });
117
+ const code = typeTextJs(ref, text);
118
+ await sendCommand('exec', { code, ...this._workspaceOpt(), ...this._tabOpt() });
123
119
  }
124
120
 
125
121
  async pressKey(key: string): Promise<void> {
126
- const code = `
127
- (() => {
128
- const el = document.activeElement || document.body;
129
- el.dispatchEvent(new KeyboardEvent('keydown', { key: ${JSON.stringify(key)}, bubbles: true }));
130
- el.dispatchEvent(new KeyboardEvent('keyup', { key: ${JSON.stringify(key)}, bubbles: true }));
131
- return 'pressed';
132
- })()
133
- `;
134
- await sendCommand('exec', { code, ...this._tabOpt() });
122
+ const code = pressKeyJs(key);
123
+ await sendCommand('exec', { code, ...this._workspaceOpt(), ...this._tabOpt() });
135
124
  }
136
125
 
137
126
  async wait(options: number | { text?: string; time?: number; timeout?: number }): Promise<void> {
@@ -145,52 +134,30 @@ export class Page implements IPage {
145
134
  }
146
135
  if (options.text) {
147
136
  const timeout = (options.timeout ?? 30) * 1000;
148
- const code = `
149
- new Promise((resolve, reject) => {
150
- const deadline = Date.now() + ${timeout};
151
- const check = () => {
152
- if (document.body.innerText.includes(${JSON.stringify(options.text)})) return resolve('found');
153
- if (Date.now() > deadline) return reject(new Error('Text not found: ' + ${JSON.stringify(options.text)}));
154
- setTimeout(check, 200);
155
- };
156
- check();
157
- })
158
- `;
159
- await sendCommand('exec', { code, ...this._tabOpt() });
137
+ const code = waitForTextJs(options.text, timeout);
138
+ await sendCommand('exec', { code, ...this._workspaceOpt(), ...this._tabOpt() });
160
139
  }
161
140
  }
162
141
 
163
142
  async tabs(): Promise<any> {
164
- return sendCommand('tabs', { op: 'list' });
143
+ return sendCommand('tabs', { op: 'list', ...this._workspaceOpt() });
165
144
  }
166
145
 
167
146
  async closeTab(index?: number): Promise<void> {
168
- await sendCommand('tabs', { op: 'close', ...(index !== undefined ? { index } : {}) });
147
+ await sendCommand('tabs', { op: 'close', ...this._workspaceOpt(), ...(index !== undefined ? { index } : {}) });
169
148
  }
170
149
 
171
150
  async newTab(): Promise<void> {
172
- await sendCommand('tabs', { op: 'new' });
151
+ await sendCommand('tabs', { op: 'new', ...this._workspaceOpt() });
173
152
  }
174
153
 
175
154
  async selectTab(index: number): Promise<void> {
176
- await sendCommand('tabs', { op: 'select', index });
155
+ await sendCommand('tabs', { op: 'select', index, ...this._workspaceOpt() });
177
156
  }
178
157
 
179
158
  async networkRequests(includeStatic: boolean = false): Promise<any> {
180
- const code = `
181
- (() => {
182
- const entries = performance.getEntriesByType('resource');
183
- return entries
184
- ${includeStatic ? '' : '.filter(e => !["img", "font", "css", "script"].some(t => e.initiatorType === t))'}
185
- .map(e => ({
186
- url: e.name,
187
- type: e.initiatorType,
188
- duration: Math.round(e.duration),
189
- size: e.transferSize || 0,
190
- }));
191
- })()
192
- `;
193
- return sendCommand('exec', { code, ...this._tabOpt() });
159
+ const code = networkRequestsJs(includeStatic);
160
+ return sendCommand('exec', { code, ...this._workspaceOpt(), ...this._tabOpt() });
194
161
  }
195
162
 
196
163
  /**
@@ -216,6 +183,7 @@ export class Page implements IPage {
216
183
  path?: string;
217
184
  } = {}): Promise<string> {
218
185
  const base64 = await sendCommand('screenshot', {
186
+ ...this._workspaceOpt(),
219
187
  format: options.format,
220
188
  quality: options.quality,
221
189
  fullPage: options.fullPage,
@@ -226,46 +194,23 @@ export class Page implements IPage {
226
194
  const fs = await import('node:fs');
227
195
  const path = await import('node:path');
228
196
  const dir = path.dirname(options.path);
229
- fs.mkdirSync(dir, { recursive: true });
230
- fs.writeFileSync(options.path, Buffer.from(base64, 'base64'));
197
+ await fs.promises.mkdir(dir, { recursive: true });
198
+ await fs.promises.writeFile(options.path, Buffer.from(base64, 'base64'));
231
199
  }
232
200
 
233
201
  return base64;
234
202
  }
235
203
 
236
204
  async scroll(direction: string = 'down', amount: number = 500): Promise<void> {
237
- const dx = direction === 'left' ? -amount : direction === 'right' ? amount : 0;
238
- const dy = direction === 'up' ? -amount : direction === 'down' ? amount : 0;
239
- await sendCommand('exec', {
240
- code: `window.scrollBy(${dx}, ${dy})`,
241
- ...this._tabOpt(),
242
- });
205
+ const code = scrollJs(direction, amount);
206
+ await sendCommand('exec', { code, ...this._workspaceOpt(), ...this._tabOpt() });
243
207
  }
244
208
 
245
209
  async autoScroll(options: { times?: number; delayMs?: number } = {}): Promise<void> {
246
210
  const times = options.times ?? 3;
247
211
  const delayMs = options.delayMs ?? 2000;
248
- const code = `
249
- (async () => {
250
- for (let i = 0; i < ${times}; i++) {
251
- const lastHeight = document.body.scrollHeight;
252
- window.scrollTo(0, lastHeight);
253
- await new Promise(resolve => {
254
- let timeoutId;
255
- const observer = new MutationObserver(() => {
256
- if (document.body.scrollHeight > lastHeight) {
257
- clearTimeout(timeoutId);
258
- observer.disconnect();
259
- setTimeout(resolve, 100);
260
- }
261
- });
262
- observer.observe(document.body, { childList: true, subtree: true });
263
- timeoutId = setTimeout(() => { observer.disconnect(); resolve(null); }, ${delayMs});
264
- });
265
- }
266
- })()
267
- `;
268
- await sendCommand('exec', { code, ...this._tabOpt() });
212
+ const code = autoScrollJs(times, delayMs);
213
+ await sendCommand('exec', { code, ...this._workspaceOpt(), ...this._tabOpt() });
269
214
  }
270
215
 
271
216
  async installInterceptor(pattern: string): Promise<void> {
@@ -1,5 +1,6 @@
1
1
  import { afterEach, 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
 
4
5
  describe('browser helpers', () => {
5
6
  it('extracts tab entries from string snapshots', () => {
@@ -122,4 +123,13 @@ describe('BrowserBridge state', () => {
122
123
 
123
124
  await expect(mcp.connect()).rejects.toThrow('Session is closing');
124
125
  });
126
+
127
+ it('fails fast when daemon is running but extension is disconnected', async () => {
128
+ vi.spyOn(daemonClient, 'isExtensionConnected').mockResolvedValue(false);
129
+ vi.spyOn(daemonClient, 'isDaemonRunning').mockResolvedValue(true);
130
+
131
+ const mcp = new BrowserBridge();
132
+
133
+ await expect(mcp.connect()).rejects.toThrow('Browser Extension is not connected');
134
+ });
125
135
  });
@@ -25,4 +25,40 @@ describe('parseTsArgsBlock', () => {
25
25
  },
26
26
  ]);
27
27
  });
28
+
29
+ it('keeps hyphenated arg names from TS adapters', () => {
30
+ const args = parseTsArgsBlock(`
31
+ {
32
+ name: 'tweet-url',
33
+ help: 'Single tweet URL to download',
34
+ },
35
+ {
36
+ name: 'download-images',
37
+ type: 'boolean',
38
+ default: false,
39
+ help: 'Download images locally',
40
+ },
41
+ `);
42
+
43
+ expect(args).toEqual([
44
+ {
45
+ name: 'tweet-url',
46
+ type: 'str',
47
+ default: undefined,
48
+ required: false,
49
+ positional: undefined,
50
+ help: 'Single tweet URL to download',
51
+ choices: undefined,
52
+ },
53
+ {
54
+ name: 'download-images',
55
+ type: 'boolean',
56
+ default: false,
57
+ required: false,
58
+ positional: undefined,
59
+ help: 'Download images locally',
60
+ choices: undefined,
61
+ },
62
+ ]);
63
+ });
28
64
  });
@@ -114,7 +114,7 @@ export function parseTsArgsBlock(argsBlock: string): ManifestEntry['args'] {
114
114
  let cursor = 0;
115
115
 
116
116
  while (cursor < argsBlock.length) {
117
- const nameMatch = argsBlock.slice(cursor).match(/\{\s*name\s*:\s*['"`](\w+)['"`]/);
117
+ const nameMatch = argsBlock.slice(cursor).match(/\{\s*name\s*:\s*['"`]([^'"`]+)['"`]/);
118
118
  if (!nameMatch || nameMatch.index === undefined) break;
119
119
 
120
120
  const objectStart = cursor + nameMatch.index;
@@ -231,6 +231,7 @@ function scanTs(filePath: string, site: string): ManifestEntry {
231
231
  // Extract browser: false (some adapters bypass browser entirely)
232
232
  const browserMatch = src.match(/browser\s*:\s*(true|false)/);
233
233
  if (browserMatch) entry.browser = browserMatch[1] === 'true';
234
+ else entry.browser = entry.strategy !== 'public';
234
235
 
235
236
  // Extract columns
236
237
  const colMatch = src.match(/columns\s*:\s*\[([^\]]*)\]/);
@@ -0,0 +1,47 @@
1
+ import { describe, expect, it } from 'vitest';
2
+ import { Strategy, type CliCommand } from './registry.js';
3
+ import { shouldUseBrowserSession } from './capabilityRouting.js';
4
+
5
+ function makeCmd(partial: Partial<CliCommand>): CliCommand {
6
+ return {
7
+ site: 'test',
8
+ name: 'command',
9
+ description: '',
10
+ args: [],
11
+ ...partial,
12
+ };
13
+ }
14
+
15
+ describe('shouldUseBrowserSession', () => {
16
+ it('skips browser session for public fetch-only pipelines', () => {
17
+ expect(shouldUseBrowserSession(makeCmd({
18
+ browser: true,
19
+ strategy: Strategy.PUBLIC,
20
+ pipeline: [{ fetch: 'https://example.com/api' }, { select: 'items' }],
21
+ }))).toBe(false);
22
+ });
23
+
24
+ it('keeps browser session for public pipelines with browser-only steps', () => {
25
+ expect(shouldUseBrowserSession(makeCmd({
26
+ browser: true,
27
+ strategy: Strategy.PUBLIC,
28
+ pipeline: [{ navigate: 'https://example.com' }, { evaluate: '() => []' }],
29
+ }))).toBe(true);
30
+ });
31
+
32
+ it('keeps browser session for non-public strategies', () => {
33
+ expect(shouldUseBrowserSession(makeCmd({
34
+ browser: true,
35
+ strategy: Strategy.COOKIE,
36
+ pipeline: [{ fetch: 'https://example.com/api' }],
37
+ }))).toBe(true);
38
+ });
39
+
40
+ it('keeps browser session for function adapters', () => {
41
+ expect(shouldUseBrowserSession(makeCmd({
42
+ browser: true,
43
+ strategy: Strategy.PUBLIC,
44
+ func: async () => [],
45
+ }))).toBe(true);
46
+ });
47
+ });
@@ -0,0 +1,28 @@
1
+ import { Strategy, type CliCommand } from './registry.js';
2
+
3
+ const BROWSER_ONLY_STEPS = new Set([
4
+ 'navigate',
5
+ 'click',
6
+ 'type',
7
+ 'wait',
8
+ 'press',
9
+ 'snapshot',
10
+ 'evaluate',
11
+ 'intercept',
12
+ 'tap',
13
+ ]);
14
+
15
+ function pipelineNeedsBrowserSession(pipeline: Record<string, unknown>[]): boolean {
16
+ return pipeline.some((step) => {
17
+ if (!step || typeof step !== 'object') return false;
18
+ return Object.keys(step).some((op) => BROWSER_ONLY_STEPS.has(op));
19
+ });
20
+ }
21
+
22
+ export function shouldUseBrowserSession(cmd: CliCommand): boolean {
23
+ if (!cmd.browser) return false;
24
+ if (cmd.func) return true;
25
+ if (!cmd.pipeline || cmd.pipeline.length === 0) return true;
26
+ if (cmd.strategy !== Strategy.PUBLIC) return true;
27
+ return pipelineNeedsBrowserSession(cmd.pipeline as Record<string, unknown>[]);
28
+ }
@@ -1,18 +1,26 @@
1
1
  import { describe, expect, it } from 'vitest';
2
2
  import { formatTimestamp, workStatusLabel } from './chaoxing.js';
3
3
 
4
+ function localDatePrefixFromMillis(ts: number): string {
5
+ const d = new Date(ts);
6
+ const yyyy = d.getFullYear();
7
+ const mm = String(d.getMonth() + 1).padStart(2, '0');
8
+ const dd = String(d.getDate()).padStart(2, '0');
9
+ return `${yyyy}-${mm}-${dd}`;
10
+ }
11
+
4
12
  describe('formatTimestamp', () => {
5
13
  it('formats millisecond timestamp', () => {
6
- // 2026-01-15 08:30 UTC+8
7
14
  const ts = new Date('2026-01-15T00:30:00Z').getTime();
8
15
  const result = formatTimestamp(ts);
9
- expect(result).toMatch(/2026-01-15/);
16
+ expect(result).toMatch(new RegExp(`^${localDatePrefixFromMillis(ts)}\\s`));
10
17
  });
11
18
 
12
19
  it('formats second timestamp', () => {
13
- const ts = Math.floor(new Date('2026-06-01T12:00:00Z').getTime() / 1000);
20
+ const millis = new Date('2026-06-01T12:00:00Z').getTime();
21
+ const ts = Math.floor(millis / 1000);
14
22
  const result = formatTimestamp(ts);
15
- expect(result).toMatch(/2026-06-01/);
23
+ expect(result).toMatch(new RegExp(`^${localDatePrefixFromMillis(millis)}\\s`));
16
24
  });
17
25
 
18
26
  it('returns empty for null/undefined/0', () => {
package/src/cli.ts CHANGED
@@ -1,13 +1,14 @@
1
1
  import { Command } from 'commander';
2
2
  import chalk from 'chalk';
3
3
  import { executeCommand } from './engine.js';
4
- import { type CliCommand, fullName, getRegistry, strategyLabel } from './registry.js';
4
+ import { Strategy, type CliCommand, fullName, getRegistry, strategyLabel } from './registry.js';
5
5
  import { render as renderOutput } from './output.js';
6
6
  import { BrowserBridge, CDPBridge } from './browser/index.js';
7
7
  import { browserSession, DEFAULT_BROWSER_COMMAND_TIMEOUT, runWithTimeout } from './runtime.js';
8
8
  import { PKG_VERSION } from './version.js';
9
9
  import { printCompletionScript } from './completion.js';
10
10
  import { CliError } from './errors.js';
11
+ import { shouldUseBrowserSession } from './capabilityRouting.js';
11
12
 
12
13
  export function runCli(BUILTIN_CLIS: string, USER_CLIS: string): void {
13
14
  const program = new Command();
@@ -64,13 +65,13 @@ export function runCli(BUILTIN_CLIS: string, USER_CLIS: string): void {
64
65
  });
65
66
 
66
67
  program.command('explore').alias('probe').description('Explore a website: discover APIs, stores, and recommend strategies').argument('<url>').option('--site <name>').option('--goal <text>').option('--wait <s>', '', '3').option('--auto', 'Enable interactive fuzzing (simulate clicks to trigger lazy APIs)').option('--click <labels>', 'Comma-separated labels to click before fuzzing (e.g. "字幕,CC,评论")')
67
- .action(async (url, opts) => { const { exploreUrl, renderExploreSummary } = await import('./explore.js'); const clickLabels = opts.click ? opts.click.split(',').map((s: string) => s.trim()) : undefined; const BrowserFactory = process.env.OPENCLI_CDP_ENDPOINT ? CDPBridge : BrowserBridge; console.log(renderExploreSummary(await exploreUrl(url, { BrowserFactory: BrowserFactory as any, site: opts.site, goal: opts.goal, waitSeconds: parseFloat(opts.wait), auto: opts.auto, clickLabels }))); });
68
+ .action(async (url, opts) => { const { exploreUrl, renderExploreSummary } = await import('./explore.js'); const clickLabels = opts.click ? opts.click.split(',').map((s: string) => s.trim()) : undefined; const BrowserFactory = process.env.OPENCLI_CDP_ENDPOINT ? CDPBridge : BrowserBridge; const workspace = `explore:${opts.site ?? (() => { try { return new URL(url).host; } catch { return 'default'; } })()}`; console.log(renderExploreSummary(await exploreUrl(url, { BrowserFactory: BrowserFactory as any, site: opts.site, goal: opts.goal, waitSeconds: parseFloat(opts.wait), auto: opts.auto, clickLabels, workspace }))); });
68
69
 
69
70
  program.command('synthesize').description('Synthesize CLIs from explore').argument('<target>').option('--top <n>', '', '3')
70
71
  .action(async (target, opts) => { const { synthesizeFromExplore, renderSynthesizeSummary } = await import('./synthesize.js'); console.log(renderSynthesizeSummary(synthesizeFromExplore(target, { top: parseInt(opts.top) }))); });
71
72
 
72
73
  program.command('generate').description('One-shot: explore → synthesize → register').argument('<url>').option('--goal <text>').option('--site <name>')
73
- .action(async (url, opts) => { const { generateCliFromUrl, renderGenerateSummary } = await import('./generate.js'); const BrowserFactory = process.env.OPENCLI_CDP_ENDPOINT ? CDPBridge : BrowserBridge; const r = await generateCliFromUrl({ url, BrowserFactory: BrowserFactory as any, builtinClis: BUILTIN_CLIS, userClis: USER_CLIS, goal: opts.goal, site: opts.site }); console.log(renderGenerateSummary(r)); process.exitCode = r.ok ? 0 : 1; });
74
+ .action(async (url, opts) => { const { generateCliFromUrl, renderGenerateSummary } = await import('./generate.js'); const BrowserFactory = process.env.OPENCLI_CDP_ENDPOINT ? CDPBridge : BrowserBridge; const workspace = `generate:${opts.site ?? (() => { try { return new URL(url).host; } catch { return 'default'; } })()}`; const r = await generateCliFromUrl({ url, BrowserFactory: BrowserFactory as any, builtinClis: BUILTIN_CLIS, userClis: USER_CLIS, goal: opts.goal, site: opts.site, workspace }); console.log(renderGenerateSummary(r)); process.exitCode = r.ok ? 0 : 1; });
74
75
 
75
76
  program.command('cascade').description('Strategy cascade: find simplest working strategy').argument('<url>').option('--site <name>')
76
77
  .action(async (url, opts) => {
@@ -80,16 +81,17 @@ export function runCli(BUILTIN_CLIS: string, USER_CLIS: string): void {
80
81
  // Navigate to the site first for cookie context
81
82
  try { const siteUrl = new URL(url); await page.goto(`${siteUrl.protocol}//${siteUrl.host}`); await page.wait(2); } catch {}
82
83
  return cascadeProbe(page, url);
83
- });
84
+ }, { workspace: `cascade:${opts.site ?? (() => { try { return new URL(url).host; } catch { return 'default'; } })()}` });
84
85
  console.log(renderCascadeResult(result));
85
86
  });
86
87
 
87
88
  program.command('doctor')
88
89
  .description('Diagnose opencli browser bridge connectivity')
89
90
  .option('--live', 'Test browser connectivity (requires Chrome running)', false)
91
+ .option('--sessions', 'Show active automation sessions', false)
90
92
  .action(async (opts) => {
91
93
  const { runBrowserDoctor, renderBrowserDoctorReport } = await import('./doctor.js');
92
- const report = await runBrowserDoctor({ live: opts.live, cliVersion: PKG_VERSION });
94
+ const report = await runBrowserDoctor({ live: opts.live, sessions: opts.sessions, cliVersion: PKG_VERSION });
93
95
  console.log(renderBrowserDoctorReport(report));
94
96
  });
95
97
 
@@ -107,14 +109,29 @@ export function runCli(BUILTIN_CLIS: string, USER_CLIS: string): void {
107
109
  printCompletionScript(shell);
108
110
  });
109
111
 
112
+ // ── Antigravity serve (built-in, long-running) ──────────────────────────────
113
+
114
+ const antigravityCmd = program.command('antigravity').description('antigravity commands');
115
+ antigravityCmd.command('serve')
116
+ .description('Start Anthropic-compatible API proxy for Antigravity')
117
+ .option('--port <port>', 'Server port (default: 8082)', '8082')
118
+ .action(async (opts) => {
119
+ const { startServe } = await import('./clis/antigravity/serve.js');
120
+ await startServe({ port: parseInt(opts.port) });
121
+ });
122
+
110
123
  // ── Dynamic site commands ──────────────────────────────────────────────────
111
124
 
112
125
  const registry = getRegistry();
113
126
  const siteGroups = new Map<string, Command>();
127
+ // Pre-seed with the antigravity command registered above to avoid duplicates
128
+ siteGroups.set('antigravity', antigravityCmd);
114
129
 
115
130
  for (const [, cmd] of registry) {
116
131
  let siteCmd = siteGroups.get(cmd.site);
117
132
  if (!siteCmd) { siteCmd = program.command(cmd.site).description(`${cmd.site} commands`); siteGroups.set(cmd.site, siteCmd); }
133
+ // Skip if this subcommand was already hardcoded (e.g. antigravity serve)
134
+ if (siteCmd.commands.some((c: Command) => c.name() === cmd.name)) continue;
118
135
  const subCmd = siteCmd.command(cmd.name).description(cmd.description);
119
136
 
120
137
  // Register positional args first, then named options
@@ -157,16 +174,21 @@ export function runCli(BUILTIN_CLIS: string, USER_CLIS: string): void {
157
174
  try {
158
175
  if (actionOpts.verbose) process.env.OPENCLI_VERBOSE = '1';
159
176
  let result: any;
160
- if (cmd.browser) {
177
+ if (shouldUseBrowserSession(cmd)) {
161
178
  const BrowserFactory = process.env.OPENCLI_CDP_ENDPOINT ? CDPBridge : BrowserBridge;
162
179
  result = await browserSession(BrowserFactory as any, async (page) => {
180
+ // Cookie/header strategies require same-origin context for credentialed fetch.
181
+ if ((cmd.strategy === Strategy.COOKIE || cmd.strategy === Strategy.HEADER) && cmd.domain) {
182
+ try { await page.goto(`https://${cmd.domain}`); await page.wait(2); } catch {}
183
+ }
163
184
  return runWithTimeout(executeCommand(cmd, page, kwargs, actionOpts.verbose), { timeout: cmd.timeoutSeconds ?? DEFAULT_BROWSER_COMMAND_TIMEOUT, label: fullName(cmd) });
164
- });
185
+ }, { workspace: `site:${cmd.site}` });
165
186
  } else { result = await executeCommand(cmd, null, kwargs, actionOpts.verbose); }
166
187
  if (actionOpts.verbose && (!result || (Array.isArray(result) && result.length === 0))) {
167
188
  console.error(chalk.yellow(`[Verbose] Warning: Command returned an empty result. If the website structural API changed or requires authentication, check the network or update the adapter.`));
168
189
  }
169
- renderOutput(result, { fmt: actionOpts.format, columns: cmd.columns, title: `${cmd.site}/${cmd.name}`, elapsed: (Date.now() - startTime) / 1000, source: fullName(cmd) });
190
+ const resolved = getRegistry().get(fullName(cmd)) ?? cmd;
191
+ renderOutput(result, { fmt: actionOpts.format, columns: resolved.columns, title: `${resolved.site}/${resolved.name}`, elapsed: (Date.now() - startTime) / 1000, source: fullName(resolved), footerExtra: resolved.footerExtra?.(kwargs) });
170
192
  } catch (err: any) {
171
193
  if (err instanceof CliError) {
172
194
  console.error(chalk.red(`Error [${err.code}]: ${err.message}`));