@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
@@ -0,0 +1,28 @@
1
+ site: stackoverflow
2
+ name: hot
3
+ description: Hot Stack Overflow questions
4
+ domain: stackoverflow.com
5
+ strategy: public
6
+ browser: false
7
+
8
+ args:
9
+ limit:
10
+ type: int
11
+ default: 10
12
+ description: Max number of results
13
+
14
+ pipeline:
15
+ - fetch:
16
+ url: https://api.stackexchange.com/2.3/questions?order=desc&sort=hot&site=stackoverflow
17
+
18
+ - select: items
19
+
20
+ - map:
21
+ title: "${{ item.title }}"
22
+ score: "${{ item.score }}"
23
+ answers: "${{ item.answer_count }}"
24
+ url: "${{ item.link }}"
25
+
26
+ - limit: ${{ args.limit }}
27
+
28
+ columns: [title, score, answers, url]
@@ -0,0 +1,32 @@
1
+ site: stackoverflow
2
+ name: search
3
+ description: Search Stack Overflow questions
4
+ domain: stackoverflow.com
5
+ strategy: public
6
+ browser: false
7
+
8
+ args:
9
+ query:
10
+ type: string
11
+ required: true
12
+ description: Search query
13
+ limit:
14
+ type: int
15
+ default: 10
16
+ description: Max number of results
17
+
18
+ pipeline:
19
+ - fetch:
20
+ url: https://api.stackexchange.com/2.3/search/advanced?order=desc&sort=relevance&q=${{ args.query }}&site=stackoverflow
21
+
22
+ - select: items
23
+
24
+ - map:
25
+ title: "${{ item.title }}"
26
+ score: "${{ item.score }}"
27
+ answers: "${{ item.answer_count }}"
28
+ url: "${{ item.link }}"
29
+
30
+ - limit: ${{ args.limit }}
31
+
32
+ columns: [title, score, answers, url]
@@ -0,0 +1,28 @@
1
+ site: stackoverflow
2
+ name: unanswered
3
+ description: Top voted unanswered questions on Stack Overflow
4
+ domain: stackoverflow.com
5
+ strategy: public
6
+ browser: false
7
+
8
+ args:
9
+ limit:
10
+ type: int
11
+ default: 10
12
+ description: Max number of results
13
+
14
+ pipeline:
15
+ - fetch:
16
+ url: https://api.stackexchange.com/2.3/questions/unanswered?order=desc&sort=votes&site=stackoverflow
17
+
18
+ - select: items
19
+
20
+ - map:
21
+ title: "${{ item.title }}"
22
+ score: "${{ item.score }}"
23
+ answers: "${{ item.answer_count }}"
24
+ url: "${{ item.link }}"
25
+
26
+ - limit: ${{ args.limit }}
27
+
28
+ columns: [title, score, answers, url]
@@ -8,7 +8,7 @@
8
8
  import * as fs from 'node:fs';
9
9
  import * as path from 'node:path';
10
10
  import { cli, Strategy } from '../../registry.js';
11
- import { httpDownload, ytdlpDownload, checkYtdlp, getTempDir, exportCookiesToNetscape, } from '../../download/index.js';
11
+ import { httpDownload, ytdlpDownload, checkYtdlp, getTempDir, exportCookiesToNetscape, formatCookieHeader, } from '../../download/index.js';
12
12
  import { DownloadProgressTracker, formatBytes } from '../../download/progress.js';
13
13
  cli({
14
14
  site: 'twitter',
@@ -95,7 +95,8 @@ cli({
95
95
  }];
96
96
  }
97
97
  // Extract cookies
98
- const cookieString = await page.evaluate(`(() => document.cookie)()`);
98
+ const cookies = await page.getCookies({ domain: 'x.com' });
99
+ const cookieString = formatCookieHeader(cookies);
99
100
  // Create output directory
100
101
  const outputDir = tweetUrl
101
102
  ? path.join(output, 'tweets')
@@ -103,21 +104,10 @@ cli({
103
104
  fs.mkdirSync(outputDir, { recursive: true });
104
105
  // Export cookies for yt-dlp
105
106
  let cookiesFile;
106
- if (typeof cookieString === 'string' && cookieString) {
107
+ if (cookies.length > 0) {
107
108
  const tempDir = getTempDir();
108
109
  fs.mkdirSync(tempDir, { recursive: true });
109
110
  cookiesFile = path.join(tempDir, `twitter_cookies_${Date.now()}.txt`);
110
- const cookies = cookieString.split(';').map((c) => {
111
- const [name, ...rest] = c.trim().split('=');
112
- return {
113
- name: name || '',
114
- value: rest.join('=') || '',
115
- domain: '.x.com',
116
- path: '/',
117
- secure: true,
118
- httpOnly: false,
119
- };
120
- }).filter((c) => c.name);
121
111
  exportCookiesToNetscape(cookies, cookiesFile);
122
112
  }
123
113
  // Deduplicate media
@@ -152,7 +142,7 @@ cli({
152
142
  else if (media.type === 'image') {
153
143
  // Direct HTTP download for images
154
144
  result = await httpDownload(media.url, destPath, {
155
- cookies: typeof cookieString === 'string' ? cookieString : '',
145
+ cookies: cookieString,
156
146
  timeout: 30000,
157
147
  onProgress: (received, total) => {
158
148
  if (progressBar)
@@ -163,7 +153,7 @@ cli({
163
153
  else {
164
154
  // Direct HTTP download for direct video URLs
165
155
  result = await httpDownload(media.url, destPath, {
166
- cookies: typeof cookieString === 'string' ? cookieString : '',
156
+ cookies: cookieString,
167
157
  timeout: 60000,
168
158
  onProgress: (received, total) => {
169
159
  if (progressBar)
@@ -23,8 +23,15 @@ cli({
23
23
  const box = document.querySelector('[data-testid="tweetTextarea_0"]');
24
24
  if (box) {
25
25
  box.focus();
26
- // insertText is the most reliable way to trigger React's onChange events
27
- document.execCommand('insertText', false, ${JSON.stringify(kwargs.text)});
26
+ // Simulate a paste event to properly handle newlines in Draft.js/React
27
+ const textToInsert = ${JSON.stringify(kwargs.text)};
28
+ const dataTransfer = new DataTransfer();
29
+ dataTransfer.setData('text/plain', textToInsert);
30
+ box.dispatchEvent(new ClipboardEvent('paste', {
31
+ clipboardData: dataTransfer,
32
+ bubbles: true,
33
+ cancelable: true
34
+ }));
28
35
  } else {
29
36
  return { ok: false, message: 'Could not find the tweet composer text area.' };
30
37
  }
@@ -20,45 +20,26 @@ cli({
20
20
  // SPA navigation preserves the JS context, so the monkey-patched
21
21
  // fetch will capture the SearchTimeline API call.
22
22
  await page.installInterceptor('SearchTimeline');
23
- // 3. Use the search input to submit the query (SPA, no full reload).
24
- // Find the search input, type the query, and submit.
23
+ // 3. Trigger SPA navigation to search results via history API.
24
+ // pushState + popstate triggers React Router's listener without
25
+ // a full page reload, so the interceptor stays alive.
26
+ // Note: the previous approach (nativeSetter + Enter keydown on the
27
+ // search input) does not reliably trigger Twitter's form submission.
28
+ const searchUrl = JSON.stringify(`/search?q=${encodeURIComponent(query)}&f=top`);
25
29
  await page.evaluate(`
26
30
  (() => {
27
- const input = document.querySelector('input[data-testid="SearchBox_Search_Input"]');
28
- if (!input) throw new Error('Search input not found');
29
- input.focus();
30
- const nativeSetter = Object.getOwnPropertyDescriptor(
31
- HTMLInputElement.prototype, 'value'
32
- ).set;
33
- nativeSetter.call(input, ${JSON.stringify(query)});
34
- input.dispatchEvent(new Event('input', { bubbles: true }));
35
- })()
36
- `);
37
- await page.wait(0.5);
38
- // Press Enter to submit
39
- await page.evaluate(`
40
- (() => {
41
- const input = document.querySelector('input[data-testid="SearchBox_Search_Input"]');
42
- if (!input) throw new Error('Search input not found');
43
- input.dispatchEvent(new KeyboardEvent('keydown', { key: 'Enter', code: 'Enter', keyCode: 13, bubbles: true }));
31
+ window.history.pushState({}, '', ${searchUrl});
32
+ window.dispatchEvent(new PopStateEvent('popstate', { state: {} }));
44
33
  })()
45
34
  `);
46
35
  await page.wait(5);
47
- // 4. Click "Top" tab if available (ensures we get top results)
48
- try {
49
- await page.evaluate(`
50
- (() => {
51
- const tabs = document.querySelectorAll('[role="tab"]');
52
- for (const tab of tabs) {
53
- if (tab.textContent.trim() === 'Top') { tab.click(); break; }
54
- }
55
- })()
56
- `);
57
- await page.wait(2);
36
+ // Verify SPA navigation succeeded
37
+ const currentPath = await page.evaluate('() => window.location.pathname');
38
+ if (!currentPath?.startsWith('/search')) {
39
+ throw new Error('SPA navigation to /search failed. Twitter may have changed its routing.');
58
40
  }
59
- catch { /* ignore if tab not found */ }
60
- // 5. Scroll to trigger additional pagination
61
- await page.autoScroll({ times: 2, delayMs: 2000 });
41
+ // 4. Scroll to trigger additional pagination
42
+ await page.autoScroll({ times: 3, delayMs: 2000 });
62
43
  // 6. Retrieve captured data
63
44
  const requests = await page.getInterceptedRequests();
64
45
  if (!requests || requests.length === 0)
@@ -2,6 +2,6 @@
2
2
  * Xiaohongshu download — download images and videos from a note.
3
3
  *
4
4
  * Usage:
5
- * opencli xiaohongshu download --note-id abc123 --output ./xhs
5
+ * opencli xiaohongshu download --note_id abc123 --output ./xhs
6
6
  */
7
7
  export {};
@@ -2,12 +2,12 @@
2
2
  * Xiaohongshu download — download images and videos from a note.
3
3
  *
4
4
  * Usage:
5
- * opencli xiaohongshu download --note-id abc123 --output ./xhs
5
+ * opencli xiaohongshu download --note_id abc123 --output ./xhs
6
6
  */
7
7
  import * as fs from 'node:fs';
8
8
  import * as path from 'node:path';
9
9
  import { cli, Strategy } from '../../registry.js';
10
- import { httpDownload, } from '../../download/index.js';
10
+ import { httpDownload, formatCookieHeader, } from '../../download/index.js';
11
11
  import { DownloadProgressTracker, formatBytes } from '../../download/progress.js';
12
12
  cli({
13
13
  site: 'xiaohongshu',
@@ -104,7 +104,7 @@ cli({
104
104
  return [{ index: 0, type: '-', status: 'failed', size: 'No media found' }];
105
105
  }
106
106
  // Extract cookies for authenticated downloads
107
- const cookies = await page.evaluate(`(() => document.cookie)()`);
107
+ const cookies = formatCookieHeader(await page.getCookies({ domain: 'xiaohongshu.com' }));
108
108
  // Create output directory
109
109
  const outputDir = path.join(output, noteId);
110
110
  fs.mkdirSync(outputDir, { recursive: true });
@@ -119,7 +119,7 @@ cli({
119
119
  const progressBar = tracker.onFileStart(filename, i);
120
120
  try {
121
121
  const result = await httpDownload(media.url, destPath, {
122
- cookies: typeof cookies === 'string' ? cookies : '',
122
+ cookies,
123
123
  timeout: 60000,
124
124
  onProgress: (received, total) => {
125
125
  if (progressBar)
@@ -7,7 +7,7 @@
7
7
  import * as fs from 'node:fs';
8
8
  import * as path from 'node:path';
9
9
  import { cli, Strategy } from '../../registry.js';
10
- import { sanitizeFilename, httpDownload } from '../../download/index.js';
10
+ import { sanitizeFilename, httpDownload, formatCookieHeader } from '../../download/index.js';
11
11
  import { formatBytes } from '../../download/progress.js';
12
12
  /**
13
13
  * Convert HTML content to Markdown.
@@ -150,7 +150,7 @@ cli({
150
150
  if (downloadImages && data.images && data.images.length > 0) {
151
151
  const imagesDir = path.join(output, 'images');
152
152
  fs.mkdirSync(imagesDir, { recursive: true });
153
- const cookies = await page.evaluate(`(() => document.cookie)()`);
153
+ const cookies = formatCookieHeader(await page.getCookies({ domain: 'zhihu.com' }));
154
154
  for (let i = 0; i < data.images.length; i++) {
155
155
  const imgUrl = data.images[i];
156
156
  const ext = imgUrl.match(/\.(jpg|jpeg|png|gif|webp)/i)?.[1] || 'jpg';
@@ -158,7 +158,7 @@ cli({
158
158
  const imgPath = path.join(imagesDir, imgFilename);
159
159
  try {
160
160
  await httpDownload(imgUrl, imgPath, {
161
- cookies: typeof cookies === 'string' ? cookies : '',
161
+ cookies,
162
162
  timeout: 30000,
163
163
  });
164
164
  // Replace image URL in markdown with local path
package/dist/doctor.d.ts CHANGED
@@ -8,6 +8,7 @@ export type DoctorOptions = {
8
8
  fix?: boolean;
9
9
  yes?: boolean;
10
10
  live?: boolean;
11
+ sessions?: boolean;
11
12
  cliVersion?: string;
12
13
  };
13
14
  export type ConnectivityResult = {
@@ -20,6 +21,12 @@ export type DoctorReport = {
20
21
  daemonRunning: boolean;
21
22
  extensionConnected: boolean;
22
23
  connectivity?: ConnectivityResult;
24
+ sessions?: Array<{
25
+ workspace: string;
26
+ windowId: number;
27
+ tabCount: number;
28
+ idleMsRemaining: number;
29
+ }>;
23
30
  issues: string[];
24
31
  };
25
32
  /**
package/dist/doctor.js CHANGED
@@ -7,6 +7,7 @@
7
7
  import chalk from 'chalk';
8
8
  import { checkDaemonStatus } from './browser/discover.js';
9
9
  import { BrowserBridge } from './browser/index.js';
10
+ import { listSessions } from './browser/daemon-client.js';
10
11
  /**
11
12
  * Test connectivity by attempting a real browser command.
12
13
  */
@@ -30,6 +31,9 @@ export async function runBrowserDoctor(opts = {}) {
30
31
  if (opts.live) {
31
32
  connectivity = await checkConnectivity();
32
33
  }
34
+ const sessions = opts.sessions && status.running && status.extensionConnected
35
+ ? await listSessions()
36
+ : undefined;
33
37
  const issues = [];
34
38
  if (!status.running) {
35
39
  issues.push('Daemon is not running. It should start automatically when you run an opencli browser command.');
@@ -49,6 +53,7 @@ export async function runBrowserDoctor(opts = {}) {
49
53
  daemonRunning: status.running,
50
54
  extensionConnected: status.extensionConnected,
51
55
  connectivity,
56
+ sessions,
52
57
  issues,
53
58
  };
54
59
  }
@@ -71,6 +76,17 @@ export function renderBrowserDoctorReport(report) {
71
76
  else {
72
77
  lines.push(`${chalk.dim('[SKIP]')} Connectivity: not tested (use --live)`);
73
78
  }
79
+ if (report.sessions) {
80
+ lines.push('', chalk.bold('Sessions:'));
81
+ if (report.sessions.length === 0) {
82
+ lines.push(chalk.dim(' • no active automation sessions'));
83
+ }
84
+ else {
85
+ for (const session of report.sessions) {
86
+ lines.push(chalk.dim(` • ${session.workspace} → window ${session.windowId}, tabs=${session.tabCount}, idle=${Math.ceil(session.idleMsRemaining / 1000)}s`));
87
+ }
88
+ }
89
+ }
74
90
  if (report.issues.length) {
75
91
  lines.push('', chalk.yellow('Issues:'));
76
92
  for (const issue of report.issues) {
@@ -14,6 +14,15 @@ export interface YtdlpOptions {
14
14
  extraArgs?: string[];
15
15
  onProgress?: (percent: number) => void;
16
16
  }
17
+ export interface BrowserCookie {
18
+ name: string;
19
+ value: string;
20
+ domain: string;
21
+ path?: string;
22
+ secure?: boolean;
23
+ httpOnly?: boolean;
24
+ expirationDate?: number;
25
+ }
17
26
  /**
18
27
  * Check if yt-dlp is available in PATH.
19
28
  */
@@ -38,17 +47,12 @@ export declare function httpDownload(url: string, destPath: string, options?: Do
38
47
  size: number;
39
48
  error?: string;
40
49
  }>;
50
+ export declare function resolveRedirectUrl(currentUrl: string, location: string): string;
41
51
  /**
42
52
  * Export cookies to Netscape format for yt-dlp.
43
53
  */
44
- export declare function exportCookiesToNetscape(cookies: Array<{
45
- name: string;
46
- value: string;
47
- domain: string;
48
- path?: string;
49
- secure?: boolean;
50
- httpOnly?: boolean;
51
- }>, filePath: string): void;
54
+ export declare function exportCookiesToNetscape(cookies: BrowserCookie[], filePath: string): void;
55
+ export declare function formatCookieHeader(cookies: BrowserCookie[]): string;
52
56
  /**
53
57
  * Download video using yt-dlp.
54
58
  */
@@ -107,13 +107,15 @@ export async function httpDownload(url, destPath, options = {}) {
107
107
  // Handle redirects
108
108
  if (response.statusCode && response.statusCode >= 300 && response.statusCode < 400 && response.headers.location) {
109
109
  file.close();
110
- fs.unlinkSync(tempPath);
111
- httpDownload(response.headers.location, destPath, options).then(resolve);
110
+ if (fs.existsSync(tempPath))
111
+ fs.unlinkSync(tempPath);
112
+ httpDownload(resolveRedirectUrl(url, response.headers.location), destPath, options).then(resolve);
112
113
  return;
113
114
  }
114
115
  if (response.statusCode !== 200) {
115
116
  file.close();
116
- fs.unlinkSync(tempPath);
117
+ if (fs.existsSync(tempPath))
118
+ fs.unlinkSync(tempPath);
117
119
  resolve({ success: false, size: 0, error: `HTTP ${response.statusCode}` });
118
120
  return;
119
121
  }
@@ -147,6 +149,9 @@ export async function httpDownload(url, destPath, options = {}) {
147
149
  });
148
150
  });
149
151
  }
152
+ export function resolveRedirectUrl(currentUrl, location) {
153
+ return new URL(location, currentUrl).toString();
154
+ }
150
155
  /**
151
156
  * Export cookies to Netscape format for yt-dlp.
152
157
  */
@@ -168,6 +173,9 @@ export function exportCookiesToNetscape(cookies, filePath) {
168
173
  fs.mkdirSync(path.dirname(filePath), { recursive: true });
169
174
  fs.writeFileSync(filePath, lines.join('\n'));
170
175
  }
176
+ export function formatCookieHeader(cookies) {
177
+ return cookies.map((cookie) => `${cookie.name}=${cookie.value}`).join('; ');
178
+ }
171
179
  /**
172
180
  * Download video using yt-dlp.
173
181
  */
@@ -0,0 +1 @@
1
+ export {};
@@ -0,0 +1,14 @@
1
+ import { describe, expect, it } from 'vitest';
2
+ import { formatCookieHeader, resolveRedirectUrl } from './index.js';
3
+ describe('download helpers', () => {
4
+ it('resolves relative redirects against the original URL', () => {
5
+ expect(resolveRedirectUrl('https://example.com/a/file', '/cdn/file.bin')).toBe('https://example.com/cdn/file.bin');
6
+ expect(resolveRedirectUrl('https://example.com/a/file', '../next')).toBe('https://example.com/next');
7
+ });
8
+ it('formats browser cookies into a Cookie header', () => {
9
+ expect(formatCookieHeader([
10
+ { name: 'sid', value: 'abc', domain: 'example.com' },
11
+ { name: 'ct0', value: 'def', domain: 'example.com' },
12
+ ])).toBe('sid=abc; ct0=def');
13
+ });
14
+ });
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)) {
97
- const siteDir = path.join(dir, site);
98
- if (!fs.statSync(siteDir).isDirectory())
104
+ const entries = await fs.promises.readdir(dir, { withFileTypes: true });
105
+ for (const entry of entries) {
106
+ if (!entry.isDirectory())
99
107
  continue;
100
- for (const file of fs.readdirSync(siteDir)) {
108
+ const site = entry.name;
109
+ const siteDir = path.join(dir, site);
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.d.ts CHANGED
@@ -23,5 +23,6 @@ export declare function exploreUrl(url: string, opts: {
23
23
  query?: string;
24
24
  clickLabels?: string[];
25
25
  auto?: boolean;
26
+ workspace?: string;
26
27
  }): Promise<Record<string, any>>;
27
28
  export declare function renderExploreSummary(result: Record<string, any>): string;