@jackwener/opencli 1.0.0 → 1.0.3

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 (171) hide show
  1. package/.github/workflows/build-extension.yml +62 -0
  2. package/.github/workflows/ci.yml +6 -6
  3. package/.github/workflows/e2e-headed.yml +2 -2
  4. package/.github/workflows/pkg-pr-new.yml +2 -2
  5. package/.github/workflows/release.yml +2 -5
  6. package/.github/workflows/security.yml +2 -2
  7. package/CDP.md +1 -1
  8. package/CDP.zh-CN.md +1 -1
  9. package/README.md +35 -8
  10. package/README.zh-CN.md +35 -8
  11. package/SKILL.md +3 -5
  12. package/dist/browser/cdp.d.ts +27 -0
  13. package/dist/browser/cdp.js +295 -0
  14. package/dist/browser/daemon-client.d.ts +1 -1
  15. package/dist/browser/index.d.ts +4 -2
  16. package/dist/browser/index.js +5 -5
  17. package/dist/browser/mcp.d.ts +5 -8
  18. package/dist/browser/mcp.js +9 -10
  19. package/dist/browser/page.d.ts +8 -1
  20. package/dist/browser/page.js +25 -40
  21. package/dist/browser/utils.d.ts +10 -0
  22. package/dist/browser/utils.js +27 -0
  23. package/dist/browser.test.js +48 -7
  24. package/dist/chaoxing.d.ts +58 -0
  25. package/dist/chaoxing.js +225 -0
  26. package/dist/chaoxing.test.d.ts +1 -0
  27. package/dist/chaoxing.test.js +38 -0
  28. package/dist/cli-manifest.json +597 -14
  29. package/dist/cli.d.ts +1 -0
  30. package/dist/cli.js +197 -0
  31. package/dist/clis/apple-podcasts/episodes.d.ts +1 -0
  32. package/dist/clis/apple-podcasts/episodes.js +28 -0
  33. package/dist/clis/apple-podcasts/search.d.ts +1 -0
  34. package/dist/clis/apple-podcasts/search.js +29 -0
  35. package/dist/clis/apple-podcasts/top.d.ts +1 -0
  36. package/dist/clis/apple-podcasts/top.js +34 -0
  37. package/dist/clis/apple-podcasts/utils.d.ts +11 -0
  38. package/dist/clis/apple-podcasts/utils.js +30 -0
  39. package/dist/clis/apple-podcasts/utils.test.d.ts +1 -0
  40. package/dist/clis/apple-podcasts/utils.test.js +57 -0
  41. package/dist/clis/boss/chatlist.d.ts +1 -0
  42. package/dist/clis/boss/chatlist.js +50 -0
  43. package/dist/clis/boss/chatmsg.d.ts +1 -0
  44. package/dist/clis/boss/chatmsg.js +73 -0
  45. package/dist/clis/boss/send.d.ts +1 -0
  46. package/dist/clis/boss/send.js +176 -0
  47. package/dist/clis/chaoxing/assignments.d.ts +1 -0
  48. package/dist/clis/chaoxing/assignments.js +74 -0
  49. package/dist/clis/chaoxing/exams.d.ts +1 -0
  50. package/dist/clis/chaoxing/exams.js +74 -0
  51. package/dist/clis/chatgpt/ask.js +15 -14
  52. package/dist/clis/chatgpt/ax.d.ts +1 -0
  53. package/dist/clis/chatgpt/ax.js +78 -0
  54. package/dist/clis/chatgpt/read.js +5 -6
  55. package/dist/clis/chatwise/history.js +18 -1
  56. package/dist/clis/discord-app/channels.js +33 -21
  57. package/dist/clis/twitter/accept.d.ts +1 -0
  58. package/dist/clis/twitter/accept.js +202 -0
  59. package/dist/clis/twitter/followers.js +30 -22
  60. package/dist/clis/twitter/following.js +19 -14
  61. package/dist/clis/twitter/notifications.js +29 -22
  62. package/dist/clis/twitter/post.js +9 -2
  63. package/dist/clis/twitter/reply-dm.d.ts +1 -0
  64. package/dist/clis/twitter/reply-dm.js +181 -0
  65. package/dist/clis/twitter/search.js +30 -11
  66. package/dist/clis/weread/book.d.ts +1 -0
  67. package/dist/clis/weread/book.js +26 -0
  68. package/dist/clis/weread/highlights.d.ts +1 -0
  69. package/dist/clis/weread/highlights.js +23 -0
  70. package/dist/clis/weread/notebooks.d.ts +1 -0
  71. package/dist/clis/weread/notebooks.js +21 -0
  72. package/dist/clis/weread/notes.d.ts +1 -0
  73. package/dist/clis/weread/notes.js +29 -0
  74. package/dist/clis/weread/ranking.d.ts +1 -0
  75. package/dist/clis/weread/ranking.js +28 -0
  76. package/dist/clis/weread/search.d.ts +1 -0
  77. package/dist/clis/weread/search.js +25 -0
  78. package/dist/clis/weread/shelf.d.ts +1 -0
  79. package/dist/clis/weread/shelf.js +24 -0
  80. package/dist/clis/weread/utils.d.ts +20 -0
  81. package/dist/clis/weread/utils.js +72 -0
  82. package/dist/clis/weread/utils.test.d.ts +1 -0
  83. package/dist/clis/weread/utils.test.js +85 -0
  84. package/dist/clis/xiaohongshu/download.d.ts +1 -1
  85. package/dist/clis/xiaohongshu/download.js +1 -1
  86. package/dist/daemon.js +2 -2
  87. package/dist/doctor.d.ts +0 -21
  88. package/dist/doctor.js +2 -24
  89. package/dist/engine.js +24 -13
  90. package/dist/explore.js +46 -101
  91. package/dist/main.js +4 -203
  92. package/dist/output.d.ts +1 -1
  93. package/dist/registry.d.ts +3 -3
  94. package/dist/runtime.d.ts +1 -4
  95. package/dist/runtime.js +1 -4
  96. package/dist/scripts/framework.d.ts +4 -0
  97. package/dist/scripts/framework.js +21 -0
  98. package/dist/scripts/interact.d.ts +4 -0
  99. package/dist/scripts/interact.js +20 -0
  100. package/dist/scripts/store.d.ts +9 -0
  101. package/dist/scripts/store.js +44 -0
  102. package/dist/setup.js +2 -2
  103. package/dist/synthesize.js +1 -1
  104. package/extension/dist/background.js +392 -0
  105. package/extension/manifest.json +3 -3
  106. package/extension/package.json +1 -1
  107. package/extension/src/background.ts +101 -24
  108. package/extension/src/protocol.ts +1 -1
  109. package/package.json +1 -1
  110. package/src/browser/cdp.ts +295 -0
  111. package/src/browser/daemon-client.ts +1 -1
  112. package/src/browser/index.ts +5 -6
  113. package/src/browser/mcp.ts +14 -15
  114. package/src/browser/page.ts +25 -41
  115. package/src/browser/utils.ts +27 -0
  116. package/src/browser.test.ts +52 -6
  117. package/src/chaoxing.test.ts +45 -0
  118. package/src/chaoxing.ts +268 -0
  119. package/src/cli.ts +185 -0
  120. package/src/clis/antigravity/SKILL.md +5 -0
  121. package/src/clis/apple-podcasts/episodes.ts +28 -0
  122. package/src/clis/apple-podcasts/search.ts +29 -0
  123. package/src/clis/apple-podcasts/top.ts +34 -0
  124. package/src/clis/apple-podcasts/utils.test.ts +72 -0
  125. package/src/clis/apple-podcasts/utils.ts +37 -0
  126. package/src/clis/boss/chatlist.ts +50 -0
  127. package/src/clis/boss/chatmsg.ts +70 -0
  128. package/src/clis/boss/send.ts +193 -0
  129. package/src/clis/chaoxing/README.md +36 -0
  130. package/src/clis/chaoxing/README.zh-CN.md +35 -0
  131. package/src/clis/chaoxing/assignments.ts +88 -0
  132. package/src/clis/chaoxing/exams.ts +88 -0
  133. package/src/clis/chatgpt/ask.ts +14 -15
  134. package/src/clis/chatgpt/ax.ts +81 -0
  135. package/src/clis/chatgpt/read.ts +5 -7
  136. package/src/clis/chatwise/history.ts +15 -1
  137. package/src/clis/discord-app/channels.ts +33 -21
  138. package/src/clis/twitter/accept.ts +213 -0
  139. package/src/clis/twitter/followers.ts +36 -29
  140. package/src/clis/twitter/following.ts +25 -20
  141. package/src/clis/twitter/notifications.ts +34 -27
  142. package/src/clis/twitter/post.ts +9 -2
  143. package/src/clis/twitter/reply-dm.ts +193 -0
  144. package/src/clis/twitter/search.ts +34 -12
  145. package/src/clis/weread/book.ts +28 -0
  146. package/src/clis/weread/highlights.ts +25 -0
  147. package/src/clis/weread/notebooks.ts +23 -0
  148. package/src/clis/weread/notes.ts +31 -0
  149. package/src/clis/weread/ranking.ts +29 -0
  150. package/src/clis/weread/search.ts +26 -0
  151. package/src/clis/weread/shelf.ts +26 -0
  152. package/src/clis/weread/utils.test.ts +104 -0
  153. package/src/clis/weread/utils.ts +74 -0
  154. package/src/clis/xiaohongshu/download.ts +1 -1
  155. package/src/daemon.ts +2 -2
  156. package/src/doctor.ts +2 -19
  157. package/src/engine.ts +20 -13
  158. package/src/explore.ts +51 -100
  159. package/src/main.ts +4 -186
  160. package/src/output.ts +12 -12
  161. package/src/registry.ts +3 -3
  162. package/src/runtime.ts +2 -6
  163. package/src/scripts/framework.ts +20 -0
  164. package/src/scripts/interact.ts +22 -0
  165. package/src/scripts/store.ts +40 -0
  166. package/src/setup.ts +2 -2
  167. package/src/synthesize.ts +1 -1
  168. package/tests/e2e/public-commands.test.ts +68 -1
  169. package/dist/clis/grok/debug.d.ts +0 -1
  170. package/dist/clis/grok/debug.js +0 -45
  171. package/src/clis/grok/debug.ts +0 -49
package/dist/daemon.js CHANGED
@@ -95,8 +95,8 @@ async function handleRequest(req, res) {
95
95
  const result = await new Promise((resolve, reject) => {
96
96
  const timer = setTimeout(() => {
97
97
  pending.delete(body.id);
98
- reject(new Error('Command timeout (30s)'));
99
- }, 30000);
98
+ reject(new Error('Command timeout (120s)'));
99
+ }, 120000);
100
100
  pending.set(body.id, { resolve, reject, timer });
101
101
  extensionWs.send(JSON.stringify(body));
102
102
  });
package/dist/doctor.d.ts CHANGED
@@ -30,24 +30,3 @@ export declare function checkConnectivity(opts?: {
30
30
  }): Promise<ConnectivityResult>;
31
31
  export declare function runBrowserDoctor(opts?: DoctorOptions): Promise<DoctorReport>;
32
32
  export declare function renderBrowserDoctorReport(report: DoctorReport): string;
33
- export declare const PLAYWRIGHT_TOKEN_ENV = "PLAYWRIGHT_MCP_EXTENSION_TOKEN";
34
- export declare function discoverExtensionToken(): string | null;
35
- export declare function checkExtensionInstalled(): {
36
- installed: boolean;
37
- browsers: string[];
38
- };
39
- export declare function applyBrowserDoctorFix(): Promise<string[]>;
40
- export declare function getDefaultShellRcPath(): string;
41
- export declare function getDefaultMcpConfigPaths(): string[];
42
- export declare function readTokenFromShellContent(_content: string): string | null;
43
- export declare function upsertShellToken(content: string): string;
44
- export declare function upsertJsonConfigToken(content: string): string;
45
- export declare function readTomlConfigToken(_content: string): string | null;
46
- export declare function upsertTomlConfigToken(content: string): string;
47
- export declare function shortenPath(p: string): string;
48
- export declare function toolName(_p: string): string;
49
- export declare function fileExists(filePath: string): boolean;
50
- export declare function writeFileWithMkdir(_p: string, _c: string): void;
51
- export declare function checkTokenConnectivity(opts?: {
52
- timeout?: number;
53
- }): Promise<ConnectivityResult>;
package/dist/doctor.js CHANGED
@@ -6,14 +6,14 @@
6
6
  */
7
7
  import chalk from 'chalk';
8
8
  import { checkDaemonStatus } from './browser/discover.js';
9
- import { PlaywrightMCP } from './browser/index.js';
9
+ import { BrowserBridge } from './browser/index.js';
10
10
  /**
11
11
  * Test connectivity by attempting a real browser command.
12
12
  */
13
13
  export async function checkConnectivity(opts) {
14
14
  const start = Date.now();
15
15
  try {
16
- const mcp = new PlaywrightMCP();
16
+ const mcp = new BrowserBridge();
17
17
  const page = await mcp.connect({ timeout: opts?.timeout ?? 8 });
18
18
  // Try a simple eval to verify end-to-end connectivity
19
19
  await page.evaluate('1 + 1');
@@ -82,25 +82,3 @@ export function renderBrowserDoctorReport(report) {
82
82
  }
83
83
  return lines.join('\n');
84
84
  }
85
- // Backward compatibility exports (no-ops for things that no longer exist)
86
- export const PLAYWRIGHT_TOKEN_ENV = 'PLAYWRIGHT_MCP_EXTENSION_TOKEN';
87
- export function discoverExtensionToken() { return null; }
88
- export function checkExtensionInstalled() { return { installed: false, browsers: [] }; }
89
- export function applyBrowserDoctorFix() { return Promise.resolve([]); }
90
- export function getDefaultShellRcPath() { return ''; }
91
- export function getDefaultMcpConfigPaths() { return []; }
92
- export function readTokenFromShellContent(_content) { return null; }
93
- export function upsertShellToken(content) { return content; }
94
- export function upsertJsonConfigToken(content) { return content; }
95
- export function readTomlConfigToken(_content) { return null; }
96
- export function upsertTomlConfigToken(content) { return content; }
97
- export function shortenPath(p) { return p; }
98
- export function toolName(_p) { return ''; }
99
- export function fileExists(filePath) { try {
100
- return require('node:fs').existsSync(filePath);
101
- }
102
- catch {
103
- return false;
104
- } }
105
- export function writeFileWithMkdir(_p, _c) { }
106
- export async function checkTokenConnectivity(opts) { return checkConnectivity(opts); }
package/dist/engine.js CHANGED
@@ -24,12 +24,15 @@ export async function discoverClis(...dirs) {
24
24
  // Fast path: try manifest first (production / post-build)
25
25
  for (const dir of dirs) {
26
26
  const manifestPath = path.resolve(dir, '..', 'cli-manifest.json');
27
- if (fs.existsSync(manifestPath)) {
28
- loadFromManifest(manifestPath, dir);
27
+ try {
28
+ await fs.promises.access(manifestPath);
29
+ await loadFromManifest(manifestPath, dir);
29
30
  continue; // Skip filesystem scan for this directory
30
31
  }
31
- // Fallback: runtime filesystem scan (development)
32
- await discoverClisFromFs(dir);
32
+ catch {
33
+ // Fallback: runtime filesystem scan (development)
34
+ await discoverClisFromFs(dir);
35
+ }
33
36
  }
34
37
  }
35
38
  /**
@@ -37,9 +40,10 @@ export async function discoverClis(...dirs) {
37
40
  * YAML pipelines are inlined — zero YAML parsing at runtime.
38
41
  * TS modules are deferred — loaded lazily on first execution.
39
42
  */
40
- function loadFromManifest(manifestPath, clisDir) {
43
+ async function loadFromManifest(manifestPath, clisDir) {
41
44
  try {
42
- const manifest = JSON.parse(fs.readFileSync(manifestPath, 'utf-8'));
45
+ const raw = await fs.promises.readFile(manifestPath, 'utf-8');
46
+ const manifest = JSON.parse(raw);
43
47
  for (const entry of manifest) {
44
48
  if (entry.type === 'yaml') {
45
49
  // YAML pipelines fully inlined in manifest — register directly
@@ -90,17 +94,24 @@ function loadFromManifest(manifestPath, clisDir) {
90
94
  * Fallback: traditional filesystem scan (used during development with tsx).
91
95
  */
92
96
  async function discoverClisFromFs(dir) {
93
- if (!fs.existsSync(dir))
97
+ try {
98
+ await fs.promises.access(dir);
99
+ }
100
+ catch {
94
101
  return;
102
+ }
95
103
  const promises = [];
96
- for (const site of fs.readdirSync(dir)) {
104
+ const sites = await fs.promises.readdir(dir);
105
+ for (const site of sites) {
97
106
  const siteDir = path.join(dir, site);
98
- if (!fs.statSync(siteDir).isDirectory())
107
+ const stat = await fs.promises.stat(siteDir);
108
+ if (!stat.isDirectory())
99
109
  continue;
100
- for (const file of fs.readdirSync(siteDir)) {
110
+ const files = await fs.promises.readdir(siteDir);
111
+ for (const file of files) {
101
112
  const filePath = path.join(siteDir, file);
102
113
  if (file.endsWith('.yaml') || file.endsWith('.yml')) {
103
- registerYamlCli(filePath, site);
114
+ promises.push(registerYamlCli(filePath, site));
104
115
  }
105
116
  else if ((file.endsWith('.js') && !file.endsWith('.d.js')) ||
106
117
  (file.endsWith('.ts') && !file.endsWith('.d.ts') && !file.endsWith('.test.ts'))) {
@@ -112,9 +123,9 @@ async function discoverClisFromFs(dir) {
112
123
  }
113
124
  await Promise.all(promises);
114
125
  }
115
- function registerYamlCli(filePath, defaultSite) {
126
+ async function registerYamlCli(filePath, defaultSite) {
116
127
  try {
117
- const raw = fs.readFileSync(filePath, 'utf-8');
128
+ const raw = await fs.promises.readFile(filePath, 'utf-8');
118
129
  const def = yaml.load(raw);
119
130
  if (!def || typeof def !== 'object')
120
131
  return;
package/dist/explore.js CHANGED
@@ -9,6 +9,9 @@ import * as fs from 'node:fs';
9
9
  import * as path from 'node:path';
10
10
  import { DEFAULT_BROWSER_EXPLORE_TIMEOUT, browserSession, runWithTimeout } from './runtime.js';
11
11
  import { VOLATILE_PARAMS, SEARCH_PARAMS, PAGINATION_PARAMS, LIMIT_PARAMS, FIELD_ROLES } from './constants.js';
12
+ import { detectFramework } from './scripts/framework.js';
13
+ import { discoverStores } from './scripts/store.js';
14
+ import { interactFuzz } from './scripts/interact.js';
12
15
  // ── Site name detection ────────────────────────────────────────────────────
13
16
  const KNOWN_SITE_ALIASES = {
14
17
  'x.com': 'twitter', 'twitter.com': 'twitter',
@@ -134,11 +137,13 @@ function flattenFields(obj, prefix, maxDepth) {
134
137
  if (maxDepth <= 0 || !obj || typeof obj !== 'object')
135
138
  return [];
136
139
  const names = [];
137
- for (const key of Object.keys(obj)) {
140
+ const record = obj;
141
+ for (const key of Object.keys(record)) {
138
142
  const full = prefix ? `${prefix}.${key}` : key;
139
143
  names.push(full);
140
- if (obj[key] && typeof obj[key] === 'object' && !Array.isArray(obj[key]))
141
- names.push(...flattenFields(obj[key], full, maxDepth - 1));
144
+ const val = record[key];
145
+ if (val && typeof val === 'object' && !Array.isArray(val))
146
+ names.push(...flattenFields(val, full, maxDepth - 1));
142
147
  }
143
148
  return names;
144
149
  }
@@ -200,83 +205,11 @@ function inferStrategy(authIndicators) {
200
205
  return 'cookie';
201
206
  }
202
207
  // ── Framework detection ────────────────────────────────────────────────────
203
- const FRAMEWORK_DETECT_JS = `
204
- () => {
205
- const r = {};
206
- try {
207
- const app = document.querySelector('#app');
208
- r.vue3 = !!(app && app.__vue_app__);
209
- r.vue2 = !!(app && app.__vue__);
210
- r.react = !!window.__REACT_DEVTOOLS_GLOBAL_HOOK__ || !!document.querySelector('[data-reactroot]');
211
- r.nextjs = !!window.__NEXT_DATA__;
212
- r.nuxt = !!window.__NUXT__;
213
- if (r.vue3 && app.__vue_app__) { const gp = app.__vue_app__.config?.globalProperties; r.pinia = !!(gp && gp.$pinia); r.vuex = !!(gp && gp.$store); }
214
- } catch {}
215
- return r;
216
- }
217
- `;
208
+ const FRAMEWORK_DETECT_JS = detectFramework.toString();
218
209
  // ── Store discovery ────────────────────────────────────────────────────────
219
- const STORE_DISCOVER_JS = `
220
- () => {
221
- const stores = [];
222
- try {
223
- const app = document.querySelector('#app');
224
- if (!app?.__vue_app__) return stores;
225
- const gp = app.__vue_app__.config?.globalProperties;
226
-
227
- // Pinia stores
228
- const pinia = gp?.$pinia;
229
- if (pinia?._s) {
230
- pinia._s.forEach((store, id) => {
231
- const actions = [];
232
- const stateKeys = [];
233
- for (const k in store) {
234
- try {
235
- if (k.startsWith('$') || k.startsWith('_')) continue;
236
- if (typeof store[k] === 'function') actions.push(k);
237
- else stateKeys.push(k);
238
- } catch {}
239
- }
240
- stores.push({ type: 'pinia', id, actions: actions.slice(0, 20), stateKeys: stateKeys.slice(0, 15) });
241
- });
242
- }
243
-
244
- // Vuex store modules
245
- const vuex = gp?.$store;
246
- if (vuex?._modules?.root?._children) {
247
- const children = vuex._modules.root._children;
248
- for (const [modName, mod] of Object.entries(children)) {
249
- const actions = Object.keys(mod._rawModule?.actions ?? {}).slice(0, 20);
250
- const stateKeys = Object.keys(mod.state ?? {}).slice(0, 15);
251
- stores.push({ type: 'vuex', id: modName, actions, stateKeys });
252
- }
253
- }
254
- } catch {}
255
- return stores;
256
- }
257
- `;
210
+ const STORE_DISCOVER_JS = discoverStores.toString();
258
211
  // ── Auto-Interaction (Fuzzing) ─────────────────────────────────────────────
259
- const INTERACT_FUZZ_JS = `
260
- async () => {
261
- const sleep = ms => new Promise(r => setTimeout(r, ms));
262
- const clickables = Array.from(document.querySelectorAll(
263
- 'button, [role="button"], [role="tab"], .tab, .btn, a[href="javascript:void(0)"], a[href="#"]'
264
- )).slice(0, 15); // limit to 15 to avoid endless loops
265
-
266
- let clicked = 0;
267
- for (const el of clickables) {
268
- try {
269
- const rect = el.getBoundingClientRect();
270
- if (rect.width > 0 && rect.height > 0) {
271
- el.dispatchEvent(new MouseEvent('click', { bubbles: true, cancelable: true, view: window }));
272
- clicked++;
273
- await sleep(300); // give it time to trigger network
274
- }
275
- } catch {}
276
- }
277
- return clicked;
278
- }
279
- `;
212
+ const INTERACT_FUZZ_JS = interactFuzz.toString();
280
213
  // ── Main explore function ──────────────────────────────────────────────────
281
214
  export async function exploreUrl(url, opts) {
282
215
  const waitSeconds = opts.waitSeconds ?? 3.0;
@@ -286,25 +219,19 @@ export async function exploreUrl(url, opts) {
286
219
  // Step 1: Navigate
287
220
  await page.goto(url);
288
221
  await page.wait(waitSeconds);
289
- // Step 2: Auto-scroll to trigger lazy loading (use keyboard since page.scroll may not exist)
290
- for (let i = 0; i < 3; i++) {
291
- try {
292
- await page.pressKey('End');
293
- }
294
- catch { }
295
- await page.wait(1);
296
- }
222
+ // Step 2: Auto-scroll to trigger lazy loading intelligently
223
+ await page.autoScroll({ times: 3, delayMs: 1500 }).catch(() => { });
297
224
  // Step 2.5: Interactive Fuzzing (if requested)
298
225
  if (opts.auto) {
299
226
  try {
300
227
  // First: targeted clicks by label (e.g. "字幕", "CC", "评论")
301
228
  if (opts.clickLabels?.length) {
302
229
  for (const label of opts.clickLabels) {
303
- const safeLabel = label.replace(/'/g, "\\'");
230
+ const safeLabel = JSON.stringify(label);
304
231
  await page.evaluate(`
305
232
  (() => {
306
233
  const el = [...document.querySelectorAll('button, [role="button"], [role="tab"], a, span')]
307
- .find(e => e.textContent && e.textContent.trim().includes('${safeLabel}'));
234
+ .find(e => e.textContent && e.textContent.trim().includes(${safeLabel}));
308
235
  if (el) el.click();
309
236
  })()
310
237
  `);
@@ -324,11 +251,27 @@ export async function exploreUrl(url, opts) {
324
251
  // Step 4: Capture network traffic
325
252
  const rawNetwork = await page.networkRequests(false);
326
253
  const networkEntries = parseNetworkRequests(rawNetwork);
327
- // Step 5: For JSON endpoints, re-fetch response body in-browser
328
- const jsonEndpoints = networkEntries.filter(e => e.contentType.includes('json') && e.method === 'GET' && e.status === 200);
329
- for (const ep of jsonEndpoints.slice(0, 10)) {
254
+ // Step 5: For JSON endpoints missing a body, carefully re-fetch in-browser via a pristine iframe
255
+ const jsonEndpoints = networkEntries.filter(e => e.contentType.includes('json') && e.method === 'GET' && e.status === 200 && !e.responseBody);
256
+ for (const ep of jsonEndpoints.slice(0, 5)) {
330
257
  try {
331
- const body = await page.evaluate(`async () => { try { const r = await fetch(${JSON.stringify(ep.url)}, {credentials:'include'}); if (!r.ok) return null; const d = await r.json(); return JSON.stringify(d).slice(0,10000); } catch { return null; } }`);
258
+ const body = await page.evaluate(`async () => {
259
+ let iframe = null;
260
+ try {
261
+ iframe = document.createElement('iframe');
262
+ iframe.style.display = 'none';
263
+ document.body.appendChild(iframe);
264
+ const cleanFetch = iframe.contentWindow.fetch || window.fetch;
265
+ const r = await cleanFetch(${JSON.stringify(ep.url)}, { credentials: 'include' });
266
+ if (!r.ok) return null;
267
+ const d = await r.json();
268
+ return JSON.stringify(d).slice(0, 10000);
269
+ } catch {
270
+ return null;
271
+ } finally {
272
+ if (iframe && iframe.parentNode) iframe.parentNode.removeChild(iframe);
273
+ }
274
+ }`);
332
275
  if (body && typeof body === 'string') {
333
276
  try {
334
277
  ep.responseBody = JSON.parse(body);
@@ -443,7 +386,7 @@ export async function exploreUrl(url, opts) {
443
386
  const topStrategy = allAuth.has('signature') ? 'intercept' : allAuth.has('bearer') || allAuth.has('csrf') ? 'header' : allAuth.size === 0 ? 'public' : 'cookie';
444
387
  const siteName = opts.site ?? detectSiteName(metadata.url || url);
445
388
  const targetDir = opts.outDir ?? path.join('.opencli', 'explore', siteName);
446
- fs.mkdirSync(targetDir, { recursive: true });
389
+ await fs.promises.mkdir(targetDir, { recursive: true });
447
390
  const result = {
448
391
  site: siteName, target_url: url, final_url: metadata.url, title: metadata.title,
449
392
  framework, stores, top_strategy: topStrategy,
@@ -452,24 +395,26 @@ export async function exploreUrl(url, opts) {
452
395
  capabilities, auth_indicators: [...allAuth],
453
396
  };
454
397
  // Write artifacts
455
- fs.writeFileSync(path.join(targetDir, 'manifest.json'), JSON.stringify({
398
+ const writeTasks = [];
399
+ writeTasks.push(fs.promises.writeFile(path.join(targetDir, 'manifest.json'), JSON.stringify({
456
400
  site: siteName, target_url: url, final_url: metadata.url, title: metadata.title,
457
401
  framework, stores: stores.map(s => ({ type: s.type, id: s.id, actions: s.actions })),
458
402
  top_strategy: topStrategy, explored_at: new Date().toISOString(),
459
- }, null, 2));
460
- fs.writeFileSync(path.join(targetDir, 'endpoints.json'), JSON.stringify(analyzedEndpoints.map(ep => ({
403
+ }, null, 2)));
404
+ writeTasks.push(fs.promises.writeFile(path.join(targetDir, 'endpoints.json'), JSON.stringify(analyzedEndpoints.map(ep => ({
461
405
  pattern: ep.pattern, method: ep.method, url: ep.url, status: ep.status,
462
406
  contentType: ep.contentType, score: ep.score, queryParams: ep.queryParams,
463
407
  itemPath: ep.responseAnalysis?.itemPath ?? null, itemCount: ep.responseAnalysis?.itemCount ?? 0,
464
408
  detectedFields: ep.responseAnalysis?.detectedFields ?? {}, authIndicators: ep.authIndicators,
465
- })), null, 2));
466
- fs.writeFileSync(path.join(targetDir, 'capabilities.json'), JSON.stringify(capabilities, null, 2));
467
- fs.writeFileSync(path.join(targetDir, 'auth.json'), JSON.stringify({
409
+ })), null, 2)));
410
+ writeTasks.push(fs.promises.writeFile(path.join(targetDir, 'capabilities.json'), JSON.stringify(capabilities, null, 2)));
411
+ writeTasks.push(fs.promises.writeFile(path.join(targetDir, 'auth.json'), JSON.stringify({
468
412
  top_strategy: topStrategy, indicators: [...allAuth], framework,
469
- }, null, 2));
413
+ }, null, 2)));
470
414
  if (stores.length > 0) {
471
- fs.writeFileSync(path.join(targetDir, 'stores.json'), JSON.stringify(stores, null, 2));
415
+ writeTasks.push(fs.promises.writeFile(path.join(targetDir, 'stores.json'), JSON.stringify(stores, null, 2)));
472
416
  }
417
+ await Promise.all(writeTasks);
473
418
  return { ...result, out_dir: targetDir };
474
419
  })(), { timeout: exploreTimeout, label: `Explore ${url}` });
475
420
  });
package/dist/main.js CHANGED
@@ -5,16 +5,9 @@
5
5
  import * as os from 'node:os';
6
6
  import * as path from 'node:path';
7
7
  import { fileURLToPath } from 'node:url';
8
- import { Command } from 'commander';
9
- import chalk from 'chalk';
10
- import { discoverClis, executeCommand } from './engine.js';
11
- import { Strategy, fullName, getRegistry, strategyLabel } from './registry.js';
12
- import { render as renderOutput } from './output.js';
13
- import { PlaywrightMCP } from './browser/index.js';
14
- import { browserSession, DEFAULT_BROWSER_COMMAND_TIMEOUT, runWithTimeout } from './runtime.js';
15
- import { PKG_VERSION } from './version.js';
16
- import { getCompletions, printCompletionScript } from './completion.js';
17
- import { CliError } from './errors.js';
8
+ import { discoverClis } from './engine.js';
9
+ import { getCompletions } from './completion.js';
10
+ import { runCli } from './cli.js';
18
11
  const __filename = fileURLToPath(import.meta.url);
19
12
  const __dirname = path.dirname(__filename);
20
13
  const BUILTIN_CLIS = path.resolve(__dirname, 'clis');
@@ -42,196 +35,4 @@ if (getCompIdx !== -1) {
42
35
  process.stdout.write(candidates.join('\n') + '\n');
43
36
  process.exit(0);
44
37
  }
45
- const program = new Command();
46
- program.name('opencli').description('Make any website your CLI. Zero setup. AI-powered.').version(PKG_VERSION);
47
- // ── Built-in commands ──────────────────────────────────────────────────────
48
- program.command('list').description('List all available CLI commands').option('-f, --format <fmt>', 'Output format: table, json, yaml, md, csv', 'table').option('--json', 'JSON output (deprecated)')
49
- .action((opts) => {
50
- const registry = getRegistry();
51
- const commands = [...registry.values()].sort((a, b) => fullName(a).localeCompare(fullName(b)));
52
- const rows = commands.map(c => ({
53
- command: fullName(c),
54
- site: c.site,
55
- name: c.name,
56
- description: c.description,
57
- strategy: strategyLabel(c),
58
- browser: c.browser,
59
- args: c.args.map(a => a.name).join(', '),
60
- }));
61
- const fmt = opts.json && opts.format === 'table' ? 'json' : opts.format;
62
- if (fmt !== 'table') {
63
- renderOutput(rows, {
64
- fmt,
65
- columns: ['command', 'site', 'name', 'description', 'strategy', 'browser', 'args'],
66
- title: 'opencli/list',
67
- source: 'opencli list',
68
- });
69
- return;
70
- }
71
- const sites = new Map();
72
- for (const cmd of commands) {
73
- const g = sites.get(cmd.site) ?? [];
74
- g.push(cmd);
75
- sites.set(cmd.site, g);
76
- }
77
- console.log();
78
- console.log(chalk.bold(' opencli') + chalk.dim(' — available commands'));
79
- console.log();
80
- for (const [site, cmds] of sites) {
81
- console.log(chalk.bold.cyan(` ${site}`));
82
- for (const cmd of cmds) {
83
- const tag = strategyLabel(cmd) === 'public' ? chalk.green('[public]') : chalk.yellow(`[${strategyLabel(cmd)}]`);
84
- console.log(` ${cmd.name} ${tag}${cmd.description ? chalk.dim(` — ${cmd.description}`) : ''}`);
85
- }
86
- console.log();
87
- }
88
- console.log(chalk.dim(` ${commands.length} commands across ${sites.size} sites`));
89
- console.log();
90
- });
91
- program.command('validate').description('Validate CLI definitions').argument('[target]', 'site or site/name')
92
- .action(async (target) => {
93
- const { validateClisWithTarget, renderValidationReport } = await import('./validate.js');
94
- console.log(renderValidationReport(validateClisWithTarget([BUILTIN_CLIS, USER_CLIS], target)));
95
- });
96
- program.command('verify').description('Validate + smoke test').argument('[target]').option('--smoke', 'Run smoke tests', false)
97
- .action(async (target, opts) => {
98
- const { verifyClis, renderVerifyReport } = await import('./verify.js');
99
- const r = await verifyClis({ builtinClis: BUILTIN_CLIS, userClis: USER_CLIS, target, smoke: opts.smoke });
100
- console.log(renderVerifyReport(r));
101
- process.exitCode = r.ok ? 0 : 1;
102
- });
103
- 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,评论")')
104
- .action(async (url, opts) => { const { exploreUrl, renderExploreSummary } = await import('./explore.js'); const clickLabels = opts.click ? opts.click.split(',').map((s) => s.trim()) : undefined; console.log(renderExploreSummary(await exploreUrl(url, { BrowserFactory: PlaywrightMCP, site: opts.site, goal: opts.goal, waitSeconds: parseFloat(opts.wait), auto: opts.auto, clickLabels }))); });
105
- program.command('synthesize').description('Synthesize CLIs from explore').argument('<target>').option('--top <n>', '', '3')
106
- .action(async (target, opts) => { const { synthesizeFromExplore, renderSynthesizeSummary } = await import('./synthesize.js'); console.log(renderSynthesizeSummary(synthesizeFromExplore(target, { top: parseInt(opts.top) }))); });
107
- program.command('generate').description('One-shot: explore → synthesize → register').argument('<url>').option('--goal <text>').option('--site <name>')
108
- .action(async (url, opts) => { const { generateCliFromUrl, renderGenerateSummary } = await import('./generate.js'); const r = await generateCliFromUrl({ url, BrowserFactory: PlaywrightMCP, builtinClis: BUILTIN_CLIS, userClis: USER_CLIS, goal: opts.goal, site: opts.site }); console.log(renderGenerateSummary(r)); process.exitCode = r.ok ? 0 : 1; });
109
- program.command('cascade').description('Strategy cascade: find simplest working strategy').argument('<url>').option('--site <name>')
110
- .action(async (url, opts) => {
111
- const { cascadeProbe, renderCascadeResult } = await import('./cascade.js');
112
- const result = await browserSession(PlaywrightMCP, async (page) => {
113
- // Navigate to the site first for cookie context
114
- try {
115
- const siteUrl = new URL(url);
116
- await page.goto(`${siteUrl.protocol}//${siteUrl.host}`);
117
- await page.wait(2);
118
- }
119
- catch { }
120
- return cascadeProbe(page, url);
121
- });
122
- console.log(renderCascadeResult(result));
123
- });
124
- program.command('doctor')
125
- .description('Diagnose opencli browser bridge connectivity')
126
- .option('--live', 'Test browser connectivity (requires Chrome running)', false)
127
- .action(async (opts) => {
128
- const { runBrowserDoctor, renderBrowserDoctorReport } = await import('./doctor.js');
129
- const report = await runBrowserDoctor({ live: opts.live, cliVersion: PKG_VERSION });
130
- console.log(renderBrowserDoctorReport(report));
131
- });
132
- program.command('setup')
133
- .description('Interactive setup: verify browser bridge connectivity')
134
- .action(async () => {
135
- const { runSetup } = await import('./setup.js');
136
- await runSetup({ cliVersion: PKG_VERSION });
137
- });
138
- program.command('completion')
139
- .description('Output shell completion script')
140
- .argument('<shell>', 'Shell type: bash, zsh, or fish')
141
- .action((shell) => {
142
- printCompletionScript(shell);
143
- });
144
- // ── Dynamic site commands ──────────────────────────────────────────────────
145
- const registry = getRegistry();
146
- const siteGroups = new Map();
147
- for (const [, cmd] of registry) {
148
- let siteCmd = siteGroups.get(cmd.site);
149
- if (!siteCmd) {
150
- siteCmd = program.command(cmd.site).description(`${cmd.site} commands`);
151
- siteGroups.set(cmd.site, siteCmd);
152
- }
153
- const subCmd = siteCmd.command(cmd.name).description(cmd.description);
154
- // Register positional args first, then named options
155
- const positionalArgs = [];
156
- for (const arg of cmd.args) {
157
- if (arg.positional) {
158
- const bracket = arg.required ? `<${arg.name}>` : `[${arg.name}]`;
159
- subCmd.argument(bracket, arg.help ?? '');
160
- positionalArgs.push(arg);
161
- }
162
- else {
163
- const flag = arg.required ? `--${arg.name} <value>` : `--${arg.name} [value]`;
164
- if (arg.required)
165
- subCmd.requiredOption(flag, arg.help ?? '');
166
- else if (arg.default != null)
167
- subCmd.option(flag, arg.help ?? '', String(arg.default));
168
- else
169
- subCmd.option(flag, arg.help ?? '');
170
- }
171
- }
172
- subCmd.option('-f, --format <fmt>', 'Output format: table, json, yaml, md, csv', 'table').option('-v, --verbose', 'Debug output', false);
173
- subCmd.action(async (...actionArgs) => {
174
- // Commander passes positional args first, then options object, then the Command
175
- const actionOpts = actionArgs[positionalArgs.length] ?? {};
176
- const startTime = Date.now();
177
- const kwargs = {};
178
- // Collect positional args
179
- for (let i = 0; i < positionalArgs.length; i++) {
180
- const arg = positionalArgs[i];
181
- const v = actionArgs[i];
182
- if (v !== undefined)
183
- kwargs[arg.name] = v;
184
- }
185
- // Collect named options
186
- for (const arg of cmd.args) {
187
- if (arg.positional)
188
- continue;
189
- const camelName = arg.name.replace(/-([a-z])/g, (_m, ch) => ch.toUpperCase());
190
- const v = actionOpts[arg.name] ?? actionOpts[camelName];
191
- if (v !== undefined)
192
- kwargs[arg.name] = v;
193
- }
194
- try {
195
- if (actionOpts.verbose)
196
- process.env.OPENCLI_VERBOSE = '1';
197
- let result;
198
- if (cmd.browser) {
199
- result = await browserSession(PlaywrightMCP, async (page) => {
200
- // Cookie/header strategies require same-origin context for credentialed fetch.
201
- // In CDP mode the active tab may be on an unrelated domain, causing CORS failures.
202
- // Navigate to the command's domain first (mirrors cascade command behavior).
203
- if ((cmd.strategy === Strategy.COOKIE || cmd.strategy === Strategy.HEADER) && cmd.domain) {
204
- try {
205
- await page.goto(`https://${cmd.domain}`);
206
- await page.wait(2);
207
- }
208
- catch { }
209
- }
210
- return runWithTimeout(executeCommand(cmd, page, kwargs, actionOpts.verbose), { timeout: cmd.timeoutSeconds ?? DEFAULT_BROWSER_COMMAND_TIMEOUT, label: fullName(cmd) });
211
- });
212
- }
213
- else {
214
- result = await executeCommand(cmd, null, kwargs, actionOpts.verbose);
215
- }
216
- if (actionOpts.verbose && (!result || (Array.isArray(result) && result.length === 0))) {
217
- 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.`));
218
- }
219
- renderOutput(result, { fmt: actionOpts.format, columns: cmd.columns, title: `${cmd.site}/${cmd.name}`, elapsed: (Date.now() - startTime) / 1000, source: fullName(cmd) });
220
- }
221
- catch (err) {
222
- if (err instanceof CliError) {
223
- console.error(chalk.red(`Error [${err.code}]: ${err.message}`));
224
- if (err.hint)
225
- console.error(chalk.yellow(`Hint: ${err.hint}`));
226
- }
227
- else if (actionOpts.verbose && err.stack) {
228
- console.error(chalk.red(err.stack));
229
- }
230
- else {
231
- console.error(chalk.red(`Error: ${err.message ?? err}`));
232
- }
233
- process.exitCode = 1;
234
- }
235
- });
236
- }
237
- program.parse();
38
+ runCli(BUILTIN_CLIS, USER_CLIS);
package/dist/output.d.ts CHANGED
@@ -8,4 +8,4 @@ export interface RenderOptions {
8
8
  elapsed?: number;
9
9
  source?: string;
10
10
  }
11
- export declare function render(data: any, opts?: RenderOptions): void;
11
+ export declare function render(data: unknown, opts?: RenderOptions): void;
@@ -12,7 +12,7 @@ export declare enum Strategy {
12
12
  export interface Arg {
13
13
  name: string;
14
14
  type?: string;
15
- default?: any;
15
+ default?: unknown;
16
16
  required?: boolean;
17
17
  positional?: boolean;
18
18
  help?: string;
@@ -27,8 +27,8 @@ export interface CliCommand {
27
27
  browser?: boolean;
28
28
  args: Arg[];
29
29
  columns?: string[];
30
- func?: (page: IPage, kwargs: Record<string, any>, debug?: boolean) => Promise<any>;
31
- pipeline?: any[];
30
+ func?: (page: IPage, kwargs: Record<string, any>, debug?: boolean) => Promise<unknown>;
31
+ pipeline?: Record<string, unknown>[];
32
32
  timeoutSeconds?: number;
33
33
  source?: string;
34
34
  }
package/dist/runtime.d.ts CHANGED
@@ -1,6 +1,3 @@
1
- /**
2
- * Runtime utilities: timeouts and browser session management.
3
- */
4
1
  import type { IPage } from './types.js';
5
2
  export declare const DEFAULT_BROWSER_CONNECT_TIMEOUT: number;
6
3
  export declare const DEFAULT_BROWSER_COMMAND_TIMEOUT: number;
@@ -17,7 +14,7 @@ export declare function runWithTimeout<T>(promise: Promise<T>, opts: {
17
14
  * Timeout with milliseconds unit. Used for low-level internal timeouts.
18
15
  */
19
16
  export declare function withTimeoutMs<T>(promise: Promise<T>, timeoutMs: number, message: string): Promise<T>;
20
- /** Interface for browser factory (PlaywrightMCP or test mocks) */
17
+ /** Interface for browser factory (BrowserBridge or test mocks) */
21
18
  export interface IBrowserFactory {
22
19
  connect(opts?: {
23
20
  timeout?: number;
package/dist/runtime.js CHANGED
@@ -1,8 +1,5 @@
1
- /**
2
- * Runtime utilities: timeouts and browser session management.
3
- */
4
1
  export const DEFAULT_BROWSER_CONNECT_TIMEOUT = parseInt(process.env.OPENCLI_BROWSER_CONNECT_TIMEOUT ?? '30', 10);
5
- export const DEFAULT_BROWSER_COMMAND_TIMEOUT = parseInt(process.env.OPENCLI_BROWSER_COMMAND_TIMEOUT ?? '45', 10);
2
+ export const DEFAULT_BROWSER_COMMAND_TIMEOUT = parseInt(process.env.OPENCLI_BROWSER_COMMAND_TIMEOUT ?? '60', 10);
6
3
  export const DEFAULT_BROWSER_EXPLORE_TIMEOUT = parseInt(process.env.OPENCLI_BROWSER_EXPLORE_TIMEOUT ?? '120', 10);
7
4
  export const DEFAULT_BROWSER_SMOKE_TIMEOUT = parseInt(process.env.OPENCLI_BROWSER_SMOKE_TIMEOUT ?? '60', 10);
8
5
  /**
@@ -0,0 +1,4 @@
1
+ /**
2
+ * Injected script for detecting frontend frameworks (Vue, React, Next, Nuxt, etc.)
3
+ */
4
+ export declare function detectFramework(): Record<string, boolean>;