@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,51 @@
1
+ site: jike
2
+ name: user
3
+ description: 即刻用户动态
4
+ domain: m.okjike.com
5
+ browser: true
6
+
7
+ args:
8
+ username:
9
+ type: string
10
+ required: true
11
+ description: Username from profile URL (e.g. wenhao1996)
12
+ limit:
13
+ type: int
14
+ default: 20
15
+ description: Number of posts
16
+
17
+ pipeline:
18
+ - navigate: https://m.okjike.com/users/${{ args.username }}
19
+
20
+ # 从 Next.js SSR 内嵌 JSON 中提取用户动态
21
+ - evaluate: |
22
+ (() => {
23
+ try {
24
+ const el = document.querySelector('script[type="application/json"]');
25
+ if (!el) return [];
26
+ const data = JSON.parse(el.textContent);
27
+ const posts = data?.props?.pageProps?.posts || [];
28
+ return posts.map(p => ({
29
+ content: (p.content || '').replace(/\n/g, ' ').slice(0, 80),
30
+ type: p.type === 'ORIGINAL_POST' ? 'post' : p.type === 'REPOST' ? 'repost' : p.type || '',
31
+ likes: p.likeCount || 0,
32
+ comments: p.commentCount || 0,
33
+ time: p.actionTime || p.createdAt || '',
34
+ id: p.id || '',
35
+ }));
36
+ } catch (e) {
37
+ return [];
38
+ }
39
+ })()
40
+
41
+ - map:
42
+ content: ${{ item.content }}
43
+ type: ${{ item.type }}
44
+ likes: ${{ item.likes }}
45
+ comments: ${{ item.comments }}
46
+ time: ${{ item.time }}
47
+ url: https://web.okjike.com/originalPost/${{ item.id }}
48
+
49
+ - limit: ${{ args.limit }}
50
+
51
+ columns: [content, type, likes, comments, time, url]
@@ -1,6 +1,9 @@
1
1
  /**
2
- * 什么值得买搜索好价 — browser cookie, HTML parse.
3
- * Source: bb-sites/smzdm/search.js
2
+ * 什么值得买搜索好价 — browser cookie, DOM scraping.
3
+ *
4
+ * Fix: The old adapter used `search.smzdm.com/ajax/` which returns 404.
5
+ * New approach: navigate to `search.smzdm.com/?c=home&s=<keyword>&v=b`
6
+ * and scrape the rendered DOM directly.
4
7
  */
5
8
  import { cli, Strategy } from '../../registry.js';
6
9
 
@@ -18,46 +21,34 @@ cli({
18
21
  func: async (page, kwargs) => {
19
22
  const q = encodeURIComponent(kwargs.keyword);
20
23
  const limit = kwargs.limit || 20;
21
- await page.goto('https://www.smzdm.com');
24
+
25
+ // Navigate directly to search results page
26
+ await page.goto(`https://search.smzdm.com/?c=home&s=${q}&v=b`);
22
27
  await page.wait(2);
28
+
23
29
  const data = await page.evaluate(`
24
- (async () => {
25
- const q = '${q}';
30
+ (() => {
26
31
  const limit = ${limit};
27
- // Try youhui channel first, then home
28
- for (const channel of ['youhui', 'home']) {
29
- try {
30
- const resp = await fetch('https://search.smzdm.com/ajax/?c=' + channel + '&s=' + q + '&p=1&v=b', {
31
- credentials: 'include',
32
- headers: {'X-Requested-With': 'XMLHttpRequest'}
33
- });
34
- if (!resp.ok) continue;
35
- const html = await resp.text();
36
- if (html.indexOf('feed-row-wide') === -1) continue;
37
- const parser = new DOMParser();
38
- const doc = parser.parseFromString(html, 'text/html');
39
- const items = doc.querySelectorAll('li.feed-row-wide');
40
- const results = [];
41
- items.forEach((li, i) => {
42
- if (results.length >= limit) return;
43
- const titleEl = li.querySelector('h5.feed-block-title > a')
44
- || li.querySelector('h5 > a');
45
- if (!titleEl) return;
46
- const title = (titleEl.getAttribute('title') || titleEl.textContent || '').trim();
47
- const url = titleEl.getAttribute('href') || '';
48
- const priceEl = li.querySelector('.z-highlight');
49
- const price = priceEl ? priceEl.textContent.trim() : '';
50
- let mall = '';
51
- const extrasSpan = li.querySelector('.z-feed-foot-r .feed-block-extras span');
52
- if (extrasSpan) mall = extrasSpan.textContent.trim();
53
- const commentEl = li.querySelector('.feed-btn-comment');
54
- const comments = commentEl ? parseInt(commentEl.textContent.trim()) || 0 : 0;
55
- results.push({rank: results.length + 1, title, price, mall, comments, url});
56
- });
57
- if (results.length > 0) return results;
58
- } catch(e) { continue; }
59
- }
60
- return {error: 'No results'};
32
+ const items = document.querySelectorAll('li.feed-row-wide');
33
+ const results = [];
34
+ items.forEach((li) => {
35
+ if (results.length >= limit) return;
36
+ const titleEl = li.querySelector('h5.feed-block-title > a')
37
+ || li.querySelector('h5 > a');
38
+ if (!titleEl) return;
39
+ const title = (titleEl.getAttribute('title') || titleEl.textContent || '').trim();
40
+ const url = titleEl.getAttribute('href') || titleEl.href || '';
41
+ const priceEl = li.querySelector('.z-highlight');
42
+ const price = priceEl ? priceEl.textContent.trim() : '';
43
+ let mall = '';
44
+ const mallEl = li.querySelector('.z-feed-foot-r .feed-block-extras span')
45
+ || li.querySelector('.z-feed-foot-r span');
46
+ if (mallEl) mall = mallEl.textContent.trim();
47
+ const commentEl = li.querySelector('.feed-btn-comment');
48
+ const comments = commentEl ? parseInt(commentEl.textContent.trim()) || 0 : 0;
49
+ results.push({ rank: results.length + 1, title, price, mall, comments, url });
50
+ });
51
+ return results;
61
52
  })()
62
53
  `);
63
54
  if (!Array.isArray(data)) return [];
@@ -0,0 +1,29 @@
1
+ site: stackoverflow
2
+ name: bounties
3
+ description: Active bounties 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/featured?order=desc&sort=activity&site=stackoverflow
17
+
18
+ - select: items
19
+
20
+ - map:
21
+ title: "${{ item.title }}"
22
+ bounty: "${{ item.bounty_amount }}"
23
+ score: "${{ item.score }}"
24
+ answers: "${{ item.answer_count }}"
25
+ url: "${{ item.link }}"
26
+
27
+ - limit: ${{ args.limit }}
28
+
29
+ columns: [bounty, title, score, answers, url]
@@ -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]
@@ -16,6 +16,7 @@ import {
16
16
  sanitizeFilename,
17
17
  getTempDir,
18
18
  exportCookiesToNetscape,
19
+ formatCookieHeader,
19
20
  } from '../../download/index.js';
20
21
  import { DownloadProgressTracker, formatBytes } from '../../download/progress.js';
21
22
 
@@ -109,7 +110,8 @@ cli({
109
110
  }
110
111
 
111
112
  // Extract cookies
112
- const cookieString = await page.evaluate(`(() => document.cookie)()`);
113
+ const cookies = await page.getCookies({ domain: 'x.com' });
114
+ const cookieString = formatCookieHeader(cookies);
113
115
 
114
116
  // Create output directory
115
117
  const outputDir = tweetUrl
@@ -119,23 +121,10 @@ cli({
119
121
 
120
122
  // Export cookies for yt-dlp
121
123
  let cookiesFile: string | undefined;
122
- if (typeof cookieString === 'string' && cookieString) {
124
+ if (cookies.length > 0) {
123
125
  const tempDir = getTempDir();
124
126
  fs.mkdirSync(tempDir, { recursive: true });
125
127
  cookiesFile = path.join(tempDir, `twitter_cookies_${Date.now()}.txt`);
126
-
127
- const cookies = cookieString.split(';').map((c) => {
128
- const [name, ...rest] = c.trim().split('=');
129
- return {
130
- name: name || '',
131
- value: rest.join('=') || '',
132
- domain: '.x.com',
133
- path: '/',
134
- secure: true,
135
- httpOnly: false,
136
- };
137
- }).filter((c) => c.name);
138
-
139
128
  exportCookiesToNetscape(cookies, cookiesFile);
140
129
  }
141
130
 
@@ -173,7 +162,7 @@ cli({
173
162
  } else if (media.type === 'image') {
174
163
  // Direct HTTP download for images
175
164
  result = await httpDownload(media.url, destPath, {
176
- cookies: typeof cookieString === 'string' ? cookieString : '',
165
+ cookies: cookieString,
177
166
  timeout: 30000,
178
167
  onProgress: (received, total) => {
179
168
  if (progressBar) progressBar.update(received, total);
@@ -182,7 +171,7 @@ cli({
182
171
  } else {
183
172
  // Direct HTTP download for direct video URLs
184
173
  result = await httpDownload(media.url, destPath, {
185
- cookies: typeof cookieString === 'string' ? cookieString : '',
174
+ cookies: cookieString,
186
175
  timeout: 60000,
187
176
  onProgress: (received, total) => {
188
177
  if (progressBar) progressBar.update(received, total);
@@ -26,8 +26,15 @@ cli({
26
26
  const box = document.querySelector('[data-testid="tweetTextarea_0"]');
27
27
  if (box) {
28
28
  box.focus();
29
- // insertText is the most reliable way to trigger React's onChange events
30
- document.execCommand('insertText', false, ${JSON.stringify(kwargs.text)});
29
+ // Simulate a paste event to properly handle newlines in Draft.js/React
30
+ const textToInsert = ${JSON.stringify(kwargs.text)};
31
+ const dataTransfer = new DataTransfer();
32
+ dataTransfer.setData('text/plain', textToInsert);
33
+ box.dispatchEvent(new ClipboardEvent('paste', {
34
+ clipboardData: dataTransfer,
35
+ bubbles: true,
36
+ cancelable: true
37
+ }));
31
38
  } else {
32
39
  return { ok: false, message: 'Could not find the tweet composer text area.' };
33
40
  }
@@ -24,46 +24,28 @@ cli({
24
24
  // fetch will capture the SearchTimeline API call.
25
25
  await page.installInterceptor('SearchTimeline');
26
26
 
27
- // 3. Use the search input to submit the query (SPA, no full reload).
28
- // Find the search input, type the query, and submit.
27
+ // 3. Trigger SPA navigation to search results via history API.
28
+ // pushState + popstate triggers React Router's listener without
29
+ // a full page reload, so the interceptor stays alive.
30
+ // Note: the previous approach (nativeSetter + Enter keydown on the
31
+ // search input) does not reliably trigger Twitter's form submission.
32
+ const searchUrl = JSON.stringify(`/search?q=${encodeURIComponent(query)}&f=top`);
29
33
  await page.evaluate(`
30
34
  (() => {
31
- const input = document.querySelector('input[data-testid="SearchBox_Search_Input"]');
32
- if (!input) throw new Error('Search input not found');
33
- input.focus();
34
- const nativeSetter = Object.getOwnPropertyDescriptor(
35
- HTMLInputElement.prototype, 'value'
36
- ).set;
37
- nativeSetter.call(input, ${JSON.stringify(query)});
38
- input.dispatchEvent(new Event('input', { bubbles: true }));
39
- })()
40
- `);
41
- await page.wait(0.5);
42
- // Press Enter to submit
43
- await page.evaluate(`
44
- (() => {
45
- const input = document.querySelector('input[data-testid="SearchBox_Search_Input"]');
46
- if (!input) throw new Error('Search input not found');
47
- input.dispatchEvent(new KeyboardEvent('keydown', { key: 'Enter', code: 'Enter', keyCode: 13, bubbles: true }));
35
+ window.history.pushState({}, '', ${searchUrl});
36
+ window.dispatchEvent(new PopStateEvent('popstate', { state: {} }));
48
37
  })()
49
38
  `);
50
39
  await page.wait(5);
51
40
 
52
- // 4. Click "Top" tab if available (ensures we get top results)
53
- try {
54
- await page.evaluate(`
55
- (() => {
56
- const tabs = document.querySelectorAll('[role="tab"]');
57
- for (const tab of tabs) {
58
- if (tab.textContent.trim() === 'Top') { tab.click(); break; }
59
- }
60
- })()
61
- `);
62
- await page.wait(2);
63
- } catch { /* ignore if tab not found */ }
41
+ // Verify SPA navigation succeeded
42
+ const currentPath = await page.evaluate('() => window.location.pathname');
43
+ if (!currentPath?.startsWith('/search')) {
44
+ throw new Error('SPA navigation to /search failed. Twitter may have changed its routing.');
45
+ }
64
46
 
65
- // 5. Scroll to trigger additional pagination
66
- await page.autoScroll({ times: 2, delayMs: 2000 });
47
+ // 4. Scroll to trigger additional pagination
48
+ await page.autoScroll({ times: 3, delayMs: 2000 });
67
49
 
68
50
  // 6. Retrieve captured data
69
51
  const requests = await page.getInterceptedRequests();
@@ -2,7 +2,7 @@
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
 
8
8
  import * as fs from 'node:fs';
@@ -11,7 +11,7 @@ import { cli, Strategy } from '../../registry.js';
11
11
  import {
12
12
  httpDownload,
13
13
  sanitizeFilename,
14
- detectContentType,
14
+ formatCookieHeader,
15
15
  } from '../../download/index.js';
16
16
  import { DownloadProgressTracker, formatBytes } from '../../download/progress.js';
17
17
 
@@ -114,7 +114,7 @@ cli({
114
114
  }
115
115
 
116
116
  // Extract cookies for authenticated downloads
117
- const cookies = await page.evaluate(`(() => document.cookie)()`);
117
+ const cookies = formatCookieHeader(await page.getCookies({ domain: 'xiaohongshu.com' }));
118
118
 
119
119
  // Create output directory
120
120
  const outputDir = path.join(output, noteId);
@@ -134,7 +134,7 @@ cli({
134
134
 
135
135
  try {
136
136
  const result = await httpDownload(media.url, destPath, {
137
- cookies: typeof cookies === 'string' ? cookies : '',
137
+ cookies,
138
138
  timeout: 60000,
139
139
  onProgress: (received, total) => {
140
140
  if (progressBar) progressBar.update(received, total);
@@ -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 { sanitizeFilename, httpDownload } from '../../download/index.js';
11
+ import { sanitizeFilename, httpDownload, formatCookieHeader } from '../../download/index.js';
12
12
  import { formatBytes } from '../../download/progress.js';
13
13
 
14
14
  /**
@@ -178,7 +178,7 @@ cli({
178
178
  const imagesDir = path.join(output, 'images');
179
179
  fs.mkdirSync(imagesDir, { recursive: true });
180
180
 
181
- const cookies = await page.evaluate(`(() => document.cookie)()`);
181
+ const cookies = formatCookieHeader(await page.getCookies({ domain: 'zhihu.com' }));
182
182
 
183
183
  for (let i = 0; i < data.images.length; i++) {
184
184
  const imgUrl = data.images[i];
@@ -188,7 +188,7 @@ cli({
188
188
 
189
189
  try {
190
190
  await httpDownload(imgUrl, imgPath, {
191
- cookies: typeof cookies === 'string' ? cookies : '',
191
+ cookies,
192
192
  timeout: 30000,
193
193
  });
194
194
 
package/src/doctor.ts CHANGED
@@ -8,12 +8,13 @@
8
8
  import chalk from 'chalk';
9
9
  import { checkDaemonStatus } from './browser/discover.js';
10
10
  import { BrowserBridge } from './browser/index.js';
11
- import { browserSession } from './runtime.js';
11
+ import { listSessions } from './browser/daemon-client.js';
12
12
 
13
13
  export type DoctorOptions = {
14
14
  fix?: boolean;
15
15
  yes?: boolean;
16
16
  live?: boolean;
17
+ sessions?: boolean;
17
18
  cliVersion?: string;
18
19
  };
19
20
 
@@ -28,6 +29,7 @@ export type DoctorReport = {
28
29
  daemonRunning: boolean;
29
30
  extensionConnected: boolean;
30
31
  connectivity?: ConnectivityResult;
32
+ sessions?: Array<{ workspace: string; windowId: number; tabCount: number; idleMsRemaining: number }>;
31
33
  issues: string[];
32
34
  };
33
35
 
@@ -55,6 +57,9 @@ export async function runBrowserDoctor(opts: DoctorOptions = {}): Promise<Doctor
55
57
  if (opts.live) {
56
58
  connectivity = await checkConnectivity();
57
59
  }
60
+ const sessions = opts.sessions && status.running && status.extensionConnected
61
+ ? await listSessions() as Array<{ workspace: string; windowId: number; tabCount: number; idleMsRemaining: number }>
62
+ : undefined;
58
63
 
59
64
  const issues: string[] = [];
60
65
  if (!status.running) {
@@ -78,6 +83,7 @@ export async function runBrowserDoctor(opts: DoctorOptions = {}): Promise<Doctor
78
83
  daemonRunning: status.running,
79
84
  extensionConnected: status.extensionConnected,
80
85
  connectivity,
86
+ sessions,
81
87
  issues,
82
88
  };
83
89
  }
@@ -104,6 +110,17 @@ export function renderBrowserDoctorReport(report: DoctorReport): string {
104
110
  lines.push(`${chalk.dim('[SKIP]')} Connectivity: not tested (use --live)`);
105
111
  }
106
112
 
113
+ if (report.sessions) {
114
+ lines.push('', chalk.bold('Sessions:'));
115
+ if (report.sessions.length === 0) {
116
+ lines.push(chalk.dim(' • no active automation sessions'));
117
+ } else {
118
+ for (const session of report.sessions) {
119
+ lines.push(chalk.dim(` • ${session.workspace} → window ${session.windowId}, tabs=${session.tabCount}, idle=${Math.ceil(session.idleMsRemaining / 1000)}s`));
120
+ }
121
+ }
122
+ }
123
+
107
124
  if (report.issues.length) {
108
125
  lines.push('', chalk.yellow('Issues:'));
109
126
  for (const issue of report.issues) {
@@ -115,4 +132,3 @@ export function renderBrowserDoctorReport(report: DoctorReport): string {
115
132
 
116
133
  return lines.join('\n');
117
134
  }
118
-
@@ -0,0 +1,16 @@
1
+ import { describe, expect, it } from 'vitest';
2
+ import { formatCookieHeader, resolveRedirectUrl } from './index.js';
3
+
4
+ describe('download helpers', () => {
5
+ it('resolves relative redirects against the original URL', () => {
6
+ expect(resolveRedirectUrl('https://example.com/a/file', '/cdn/file.bin')).toBe('https://example.com/cdn/file.bin');
7
+ expect(resolveRedirectUrl('https://example.com/a/file', '../next')).toBe('https://example.com/next');
8
+ });
9
+
10
+ it('formats browser cookies into a Cookie header', () => {
11
+ expect(formatCookieHeader([
12
+ { name: 'sid', value: 'abc', domain: 'example.com' },
13
+ { name: 'ct0', value: 'def', domain: 'example.com' },
14
+ ])).toBe('sid=abc; ct0=def');
15
+ });
16
+ });
@@ -26,6 +26,16 @@ export interface YtdlpOptions {
26
26
  onProgress?: (percent: number) => void;
27
27
  }
28
28
 
29
+ export interface BrowserCookie {
30
+ name: string;
31
+ value: string;
32
+ domain: string;
33
+ path?: string;
34
+ secure?: boolean;
35
+ httpOnly?: boolean;
36
+ expirationDate?: number;
37
+ }
38
+
29
39
  /**
30
40
  * Check if yt-dlp is available in PATH.
31
41
  */
@@ -142,14 +152,14 @@ export async function httpDownload(
142
152
  // Handle redirects
143
153
  if (response.statusCode && response.statusCode >= 300 && response.statusCode < 400 && response.headers.location) {
144
154
  file.close();
145
- fs.unlinkSync(tempPath);
146
- httpDownload(response.headers.location, destPath, options).then(resolve);
155
+ if (fs.existsSync(tempPath)) fs.unlinkSync(tempPath);
156
+ httpDownload(resolveRedirectUrl(url, response.headers.location), destPath, options).then(resolve);
147
157
  return;
148
158
  }
149
159
 
150
160
  if (response.statusCode !== 200) {
151
161
  file.close();
152
- fs.unlinkSync(tempPath);
162
+ if (fs.existsSync(tempPath)) fs.unlinkSync(tempPath);
153
163
  resolve({ success: false, size: 0, error: `HTTP ${response.statusCode}` });
154
164
  return;
155
165
  }
@@ -187,11 +197,15 @@ export async function httpDownload(
187
197
  });
188
198
  }
189
199
 
200
+ export function resolveRedirectUrl(currentUrl: string, location: string): string {
201
+ return new URL(location, currentUrl).toString();
202
+ }
203
+
190
204
  /**
191
205
  * Export cookies to Netscape format for yt-dlp.
192
206
  */
193
207
  export function exportCookiesToNetscape(
194
- cookies: Array<{ name: string; value: string; domain: string; path?: string; secure?: boolean; httpOnly?: boolean }>,
208
+ cookies: BrowserCookie[],
195
209
  filePath: string,
196
210
  ): void {
197
211
  const lines = [
@@ -214,6 +228,10 @@ export function exportCookiesToNetscape(
214
228
  fs.writeFileSync(filePath, lines.join('\n'));
215
229
  }
216
230
 
231
+ export function formatCookieHeader(cookies: BrowserCookie[]): string {
232
+ return cookies.map((cookie) => `${cookie.name}=${cookie.value}`).join('; ');
233
+ }
234
+
217
235
  /**
218
236
  * Download video using yt-dlp.
219
237
  */