@jackwener/opencli 0.9.5 → 0.9.8

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 (270) hide show
  1. package/.github/ISSUE_TEMPLATE/bug_report.yml +83 -0
  2. package/.github/ISSUE_TEMPLATE/config.yml +8 -0
  3. package/.github/ISSUE_TEMPLATE/feature_request.yml +42 -0
  4. package/.github/ISSUE_TEMPLATE/new_site_adapter.yml +57 -0
  5. package/.github/dependabot.yml +27 -0
  6. package/.github/pull_request_template.md +24 -0
  7. package/.github/workflows/ci.yml +14 -8
  8. package/.github/workflows/e2e-headed.yml +6 -2
  9. package/.github/workflows/pkg-pr-new.yml +2 -2
  10. package/.github/workflows/release-please.yml +25 -0
  11. package/.github/workflows/release.yml +2 -2
  12. package/.github/workflows/security.yml +36 -0
  13. package/CLI-ELECTRON.md +89 -36
  14. package/CONTRIBUTING.md +167 -0
  15. package/README.md +98 -32
  16. package/README.zh-CN.md +99 -33
  17. package/dist/browser/discover.js +22 -7
  18. package/dist/browser.test.js +23 -0
  19. package/dist/build-manifest.d.ts +26 -0
  20. package/dist/build-manifest.js +132 -60
  21. package/dist/build-manifest.test.d.ts +1 -0
  22. package/dist/build-manifest.test.js +26 -0
  23. package/dist/cli-manifest.json +1875 -271
  24. package/dist/clis/antigravity/model.js +2 -2
  25. package/dist/clis/antigravity/send.js +2 -2
  26. package/dist/clis/bilibili/download.d.ts +10 -0
  27. package/dist/clis/bilibili/download.js +135 -0
  28. package/dist/clis/chatgpt/ask.d.ts +1 -0
  29. package/dist/clis/chatgpt/ask.js +68 -0
  30. package/dist/clis/chatgpt/send.js +11 -0
  31. package/dist/clis/chatwise/ask.d.ts +1 -0
  32. package/dist/clis/chatwise/ask.js +76 -0
  33. package/dist/clis/chatwise/export.d.ts +1 -0
  34. package/dist/clis/chatwise/export.js +46 -0
  35. package/dist/clis/chatwise/history.d.ts +1 -0
  36. package/dist/clis/chatwise/history.js +43 -0
  37. package/dist/clis/chatwise/model.d.ts +1 -0
  38. package/dist/clis/chatwise/model.js +81 -0
  39. package/dist/clis/chatwise/new.d.ts +1 -0
  40. package/dist/clis/chatwise/new.js +18 -0
  41. package/dist/clis/chatwise/read.d.ts +1 -0
  42. package/dist/clis/chatwise/read.js +39 -0
  43. package/dist/clis/chatwise/screenshot.d.ts +1 -0
  44. package/dist/clis/chatwise/screenshot.js +27 -0
  45. package/dist/clis/chatwise/send.d.ts +1 -0
  46. package/dist/clis/chatwise/send.js +45 -0
  47. package/dist/clis/chatwise/status.d.ts +1 -0
  48. package/dist/clis/chatwise/status.js +22 -0
  49. package/dist/clis/codex/ask.d.ts +1 -0
  50. package/dist/clis/codex/ask.js +67 -0
  51. package/dist/clis/codex/export.d.ts +1 -0
  52. package/dist/clis/codex/export.js +37 -0
  53. package/dist/clis/codex/history.d.ts +1 -0
  54. package/dist/clis/codex/history.js +43 -0
  55. package/dist/clis/codex/read.js +3 -5
  56. package/dist/clis/codex/screenshot.d.ts +1 -0
  57. package/dist/clis/codex/screenshot.js +27 -0
  58. package/dist/clis/codex/send.js +3 -6
  59. package/dist/clis/codex/status.js +2 -1
  60. package/dist/clis/cursor/ask.d.ts +1 -0
  61. package/dist/clis/cursor/ask.js +69 -0
  62. package/dist/clis/cursor/composer.js +9 -28
  63. package/dist/clis/cursor/export.d.ts +1 -0
  64. package/dist/clis/cursor/export.js +51 -0
  65. package/dist/clis/cursor/history.d.ts +1 -0
  66. package/dist/clis/cursor/history.js +43 -0
  67. package/dist/clis/cursor/new.js +4 -13
  68. package/dist/clis/cursor/screenshot.d.ts +1 -0
  69. package/dist/clis/cursor/screenshot.js +31 -0
  70. package/dist/clis/discord-app/channels.d.ts +1 -0
  71. package/dist/clis/discord-app/channels.js +45 -0
  72. package/dist/clis/discord-app/members.d.ts +1 -0
  73. package/dist/clis/discord-app/members.js +38 -0
  74. package/dist/clis/discord-app/read.d.ts +1 -0
  75. package/dist/clis/discord-app/read.js +45 -0
  76. package/dist/clis/discord-app/search.d.ts +1 -0
  77. package/dist/clis/discord-app/search.js +56 -0
  78. package/dist/clis/discord-app/send.d.ts +1 -0
  79. package/dist/clis/discord-app/send.js +27 -0
  80. package/dist/clis/discord-app/servers.d.ts +1 -0
  81. package/dist/clis/discord-app/servers.js +36 -0
  82. package/dist/clis/discord-app/status.d.ts +1 -0
  83. package/dist/clis/discord-app/status.js +16 -0
  84. package/dist/clis/feishu/new.d.ts +1 -0
  85. package/dist/clis/feishu/new.js +27 -0
  86. package/dist/clis/feishu/read.d.ts +1 -0
  87. package/dist/clis/feishu/read.js +40 -0
  88. package/dist/clis/feishu/search.d.ts +1 -0
  89. package/dist/clis/feishu/search.js +30 -0
  90. package/dist/clis/feishu/send.d.ts +1 -0
  91. package/dist/clis/feishu/send.js +39 -0
  92. package/dist/clis/feishu/status.d.ts +1 -0
  93. package/dist/clis/feishu/status.js +28 -0
  94. package/dist/clis/grok/ask.d.ts +1 -0
  95. package/dist/clis/grok/ask.js +82 -0
  96. package/dist/clis/grok/debug.d.ts +1 -0
  97. package/dist/clis/grok/debug.js +45 -0
  98. package/dist/clis/jimeng/generate.yaml +84 -0
  99. package/dist/clis/jimeng/history.yaml +47 -0
  100. package/dist/clis/linux-do/categories.yaml +41 -0
  101. package/dist/clis/linux-do/category.yaml +49 -0
  102. package/dist/clis/linux-do/hot.yaml +50 -0
  103. package/dist/clis/linux-do/latest.yaml +40 -0
  104. package/dist/clis/linux-do/search.yaml +45 -0
  105. package/dist/clis/linux-do/topic.yaml +38 -0
  106. package/dist/clis/notion/export.d.ts +1 -0
  107. package/dist/clis/notion/export.js +31 -0
  108. package/dist/clis/notion/favorites.d.ts +1 -0
  109. package/dist/clis/notion/favorites.js +84 -0
  110. package/dist/clis/notion/new.d.ts +1 -0
  111. package/dist/clis/notion/new.js +34 -0
  112. package/dist/clis/notion/read.d.ts +1 -0
  113. package/dist/clis/notion/read.js +30 -0
  114. package/dist/clis/notion/search.d.ts +1 -0
  115. package/dist/clis/notion/search.js +46 -0
  116. package/dist/clis/notion/sidebar.d.ts +1 -0
  117. package/dist/clis/notion/sidebar.js +41 -0
  118. package/dist/clis/notion/status.d.ts +1 -0
  119. package/dist/clis/notion/status.js +16 -0
  120. package/dist/clis/notion/write.d.ts +1 -0
  121. package/dist/clis/notion/write.js +40 -0
  122. package/dist/clis/twitter/download.d.ts +8 -0
  123. package/dist/clis/twitter/download.js +204 -0
  124. package/dist/clis/wechat/chats.d.ts +1 -0
  125. package/dist/clis/wechat/chats.js +28 -0
  126. package/dist/clis/wechat/contacts.d.ts +1 -0
  127. package/dist/clis/wechat/contacts.js +28 -0
  128. package/dist/clis/wechat/read.d.ts +1 -0
  129. package/dist/clis/wechat/read.js +58 -0
  130. package/dist/clis/wechat/search.d.ts +1 -0
  131. package/dist/clis/wechat/search.js +31 -0
  132. package/dist/clis/wechat/send.d.ts +1 -0
  133. package/dist/clis/wechat/send.js +42 -0
  134. package/dist/clis/wechat/status.d.ts +1 -0
  135. package/dist/clis/wechat/status.js +29 -0
  136. package/dist/clis/xiaohongshu/creator-note-detail.d.ts +10 -0
  137. package/dist/clis/xiaohongshu/creator-note-detail.js +88 -0
  138. package/dist/clis/xiaohongshu/creator-notes.d.ts +11 -0
  139. package/dist/clis/xiaohongshu/creator-notes.js +109 -0
  140. package/dist/clis/xiaohongshu/creator-profile.d.ts +10 -0
  141. package/dist/clis/xiaohongshu/creator-profile.js +54 -0
  142. package/dist/clis/xiaohongshu/creator-stats.d.ts +10 -0
  143. package/dist/clis/xiaohongshu/creator-stats.js +74 -0
  144. package/dist/clis/xiaohongshu/download.d.ts +7 -0
  145. package/dist/clis/xiaohongshu/download.js +155 -0
  146. package/dist/clis/xiaohongshu/search.js +1 -1
  147. package/dist/clis/xiaohongshu/user-helpers.d.ts +15 -0
  148. package/dist/clis/xiaohongshu/user-helpers.js +67 -0
  149. package/dist/clis/xiaohongshu/user-helpers.test.d.ts +1 -0
  150. package/dist/clis/xiaohongshu/user-helpers.test.js +81 -0
  151. package/dist/clis/xiaohongshu/user.js +46 -29
  152. package/dist/clis/zhihu/download.d.ts +11 -0
  153. package/dist/clis/zhihu/download.js +186 -0
  154. package/dist/clis/zhihu/download.test.d.ts +1 -0
  155. package/dist/clis/zhihu/download.test.js +10 -0
  156. package/dist/download/index.d.ts +79 -0
  157. package/dist/download/index.js +325 -0
  158. package/dist/download/progress.d.ts +36 -0
  159. package/dist/download/progress.js +111 -0
  160. package/dist/engine.test.js +15 -0
  161. package/dist/main.js +16 -3
  162. package/dist/pipeline/registry.js +2 -0
  163. package/dist/pipeline/steps/download.d.ts +34 -0
  164. package/dist/pipeline/steps/download.js +251 -0
  165. package/dist/pipeline/template.js +28 -0
  166. package/package.json +4 -3
  167. package/scripts/test-site.mjs +70 -0
  168. package/src/browser/discover.ts +23 -7
  169. package/src/browser.test.ts +23 -0
  170. package/src/build-manifest.test.ts +28 -0
  171. package/src/build-manifest.ts +147 -57
  172. package/src/clis/antigravity/README.md +2 -3
  173. package/src/clis/antigravity/README.zh-CN.md +2 -3
  174. package/src/clis/antigravity/SKILL.md +1 -1
  175. package/src/clis/antigravity/model.ts +2 -2
  176. package/src/clis/antigravity/send.ts +2 -2
  177. package/src/clis/bilibili/download.ts +161 -0
  178. package/src/clis/chatgpt/README.md +25 -16
  179. package/src/clis/chatgpt/README.zh-CN.md +27 -18
  180. package/src/clis/chatgpt/ask.ts +77 -0
  181. package/src/clis/chatgpt/send.ts +12 -0
  182. package/src/clis/chatwise/README.md +38 -0
  183. package/src/clis/chatwise/README.zh-CN.md +38 -0
  184. package/src/clis/chatwise/ask.ts +87 -0
  185. package/src/clis/chatwise/export.ts +51 -0
  186. package/src/clis/chatwise/history.ts +47 -0
  187. package/src/clis/chatwise/model.ts +87 -0
  188. package/src/clis/chatwise/new.ts +21 -0
  189. package/src/clis/chatwise/read.ts +42 -0
  190. package/src/clis/chatwise/screenshot.ts +33 -0
  191. package/src/clis/chatwise/send.ts +50 -0
  192. package/src/clis/chatwise/status.ts +25 -0
  193. package/src/clis/codex/ask.ts +77 -0
  194. package/src/clis/codex/export.ts +42 -0
  195. package/src/clis/codex/extract-diff.ts +1 -0
  196. package/src/clis/codex/history.ts +47 -0
  197. package/src/clis/codex/read.ts +5 -6
  198. package/src/clis/codex/screenshot.ts +33 -0
  199. package/src/clis/codex/send.ts +6 -7
  200. package/src/clis/codex/status.ts +4 -2
  201. package/src/clis/cursor/ask.ts +81 -0
  202. package/src/clis/cursor/composer.ts +9 -30
  203. package/src/clis/cursor/export.ts +57 -0
  204. package/src/clis/cursor/history.ts +47 -0
  205. package/src/clis/cursor/new.ts +4 -15
  206. package/src/clis/cursor/screenshot.ts +38 -0
  207. package/src/clis/discord-app/README.md +28 -0
  208. package/src/clis/discord-app/README.zh-CN.md +28 -0
  209. package/src/clis/discord-app/channels.ts +48 -0
  210. package/src/clis/discord-app/members.ts +41 -0
  211. package/src/clis/discord-app/read.ts +49 -0
  212. package/src/clis/discord-app/search.ts +64 -0
  213. package/src/clis/discord-app/send.ts +32 -0
  214. package/src/clis/discord-app/servers.ts +39 -0
  215. package/src/clis/discord-app/status.ts +18 -0
  216. package/src/clis/feishu/README.md +20 -0
  217. package/src/clis/feishu/README.zh-CN.md +20 -0
  218. package/src/clis/feishu/new.ts +32 -0
  219. package/src/clis/feishu/read.ts +48 -0
  220. package/src/clis/feishu/search.ts +35 -0
  221. package/src/clis/feishu/send.ts +46 -0
  222. package/src/clis/feishu/status.ts +34 -0
  223. package/src/clis/grok/ask.ts +90 -0
  224. package/src/clis/grok/debug.ts +49 -0
  225. package/src/clis/jimeng/generate.yaml +84 -0
  226. package/src/clis/jimeng/history.yaml +47 -0
  227. package/src/clis/linux-do/categories.yaml +41 -0
  228. package/src/clis/linux-do/category.yaml +49 -0
  229. package/src/clis/linux-do/hot.yaml +50 -0
  230. package/src/clis/linux-do/latest.yaml +40 -0
  231. package/src/clis/linux-do/search.yaml +45 -0
  232. package/src/clis/linux-do/topic.yaml +38 -0
  233. package/src/clis/notion/README.md +29 -0
  234. package/src/clis/notion/README.zh-CN.md +29 -0
  235. package/src/clis/notion/export.ts +36 -0
  236. package/src/clis/notion/favorites.ts +87 -0
  237. package/src/clis/notion/new.ts +39 -0
  238. package/src/clis/notion/read.ts +33 -0
  239. package/src/clis/notion/search.ts +54 -0
  240. package/src/clis/notion/sidebar.ts +44 -0
  241. package/src/clis/notion/status.ts +18 -0
  242. package/src/clis/notion/write.ts +45 -0
  243. package/src/clis/twitter/download.ts +227 -0
  244. package/src/clis/wechat/README.md +28 -0
  245. package/src/clis/wechat/README.zh-CN.md +28 -0
  246. package/src/clis/wechat/chats.ts +33 -0
  247. package/src/clis/wechat/contacts.ts +33 -0
  248. package/src/clis/wechat/read.ts +72 -0
  249. package/src/clis/wechat/search.ts +36 -0
  250. package/src/clis/wechat/send.ts +49 -0
  251. package/src/clis/wechat/status.ts +35 -0
  252. package/src/clis/xiaohongshu/creator-note-detail.ts +95 -0
  253. package/src/clis/xiaohongshu/creator-notes.ts +116 -0
  254. package/src/clis/xiaohongshu/creator-profile.ts +60 -0
  255. package/src/clis/xiaohongshu/creator-stats.ts +81 -0
  256. package/src/clis/xiaohongshu/download.ts +173 -0
  257. package/src/clis/xiaohongshu/search.ts +1 -1
  258. package/src/clis/xiaohongshu/user-helpers.test.ts +106 -0
  259. package/src/clis/xiaohongshu/user-helpers.ts +85 -0
  260. package/src/clis/xiaohongshu/user.ts +52 -32
  261. package/src/clis/zhihu/download.test.ts +12 -0
  262. package/src/clis/zhihu/download.ts +223 -0
  263. package/src/download/index.ts +395 -0
  264. package/src/download/progress.ts +125 -0
  265. package/src/engine.test.ts +17 -0
  266. package/src/main.ts +12 -3
  267. package/src/pipeline/registry.ts +2 -0
  268. package/src/pipeline/steps/download.ts +310 -0
  269. package/src/pipeline/template.ts +26 -0
  270. package/tests/e2e/browser-auth.test.ts +25 -0
@@ -0,0 +1,84 @@
1
+ site: jimeng
2
+ name: generate
3
+ description: 即梦AI 文生图 — 输入 prompt 生成图片
4
+ domain: jimeng.jianying.com
5
+ strategy: cookie
6
+ browser: true
7
+
8
+ args:
9
+ prompt:
10
+ type: string
11
+ required: true
12
+ description: "图片描述 prompt"
13
+ model:
14
+ type: string
15
+ default: "high_aes_general_v50"
16
+ description: "模型: high_aes_general_v50 (5.0 Lite), high_aes_general_v42 (4.6), high_aes_general_v40 (4.0)"
17
+ wait:
18
+ type: int
19
+ default: 40
20
+ description: "等待生成完成的秒数"
21
+
22
+ columns: [status, prompt, image_count, image_urls]
23
+
24
+ pipeline:
25
+ - navigate: https://jimeng.jianying.com/ai-tool/generate?type=image&workspace=0
26
+ - wait: 3
27
+ - evaluate: |
28
+ (async () => {
29
+ const prompt = ${{ args.prompt | json }};
30
+ const waitSec = ${{ args.wait }};
31
+
32
+ // Step 1: Count existing images before generation
33
+ const beforeImgs = document.querySelectorAll('img[src*="dreamina-sign"], img[src*="tb4s082cfz"]').length;
34
+
35
+ // Step 2: Clear and set prompt
36
+ const editors = document.querySelectorAll('[contenteditable="true"]');
37
+ const editor = editors[0];
38
+ if (!editor) return [{ status: 'failed', prompt: prompt, image_count: 0, image_urls: 'Editor not found' }];
39
+
40
+ editor.focus();
41
+ await new Promise(r => setTimeout(r, 200));
42
+ document.execCommand('selectAll');
43
+ await new Promise(r => setTimeout(r, 100));
44
+ document.execCommand('delete');
45
+ await new Promise(r => setTimeout(r, 200));
46
+ document.execCommand('insertText', false, prompt);
47
+ await new Promise(r => setTimeout(r, 500));
48
+
49
+ // Step 3: Click generate
50
+ const btn = document.querySelector('.lv-btn.lv-btn-primary[class*="circle"]');
51
+ if (!btn) return [{ status: 'failed', prompt: prompt, image_count: 0, image_urls: 'Generate button not found' }];
52
+ btn.click();
53
+
54
+ // Step 4: Wait for new images to appear
55
+ let newImgs = [];
56
+ for (let i = 0; i < waitSec; i++) {
57
+ await new Promise(r => setTimeout(r, 1000));
58
+ const allImgs = document.querySelectorAll('img[src*="dreamina-sign"], img[src*="tb4s082cfz"]');
59
+ if (allImgs.length > beforeImgs) {
60
+ // New images appeared — generation complete
61
+ newImgs = Array.from(allImgs).slice(0, allImgs.length - beforeImgs);
62
+ break;
63
+ }
64
+ }
65
+
66
+ if (newImgs.length === 0) {
67
+ return [{ status: 'timeout', prompt: prompt, image_count: 0, image_urls: 'Generation may still be in progress' }];
68
+ }
69
+
70
+ // Step 5: Extract image URLs (use thumbnail URLs which are accessible)
71
+ const urls = newImgs.map(img => img.src);
72
+
73
+ return [{
74
+ status: 'success',
75
+ prompt: prompt.substring(0, 80),
76
+ image_count: urls.length,
77
+ image_urls: urls.join('\n')
78
+ }];
79
+ })()
80
+ - map:
81
+ status: ${{ item.status }}
82
+ prompt: ${{ item.prompt }}
83
+ image_count: ${{ item.image_count }}
84
+ image_urls: ${{ item.image_urls }}
@@ -0,0 +1,47 @@
1
+ site: jimeng
2
+ name: history
3
+ description: 即梦AI 查看最近生成的作品
4
+ domain: jimeng.jianying.com
5
+ strategy: cookie
6
+ browser: true
7
+
8
+ args:
9
+ limit:
10
+ type: int
11
+ default: 5
12
+
13
+ columns: [prompt, model, status, image_url, created_at]
14
+
15
+ pipeline:
16
+ - navigate: https://jimeng.jianying.com/ai-tool/generate?type=image&workspace=0
17
+ - wait: 3
18
+ - evaluate: |
19
+ (async () => {
20
+ const limit = ${{ args.limit }};
21
+ const res = await fetch('/mweb/v1/get_history?aid=513695&device_platform=web&region=cn&da_version=3.3.11&web_version=7.5.0&aigc_features=app_lip_sync', {
22
+ method: 'POST',
23
+ credentials: 'include',
24
+ headers: { 'Content-Type': 'application/json' },
25
+ body: JSON.stringify({ cursor: '', count: limit, need_page_item: true, need_aigc_data: true, aigc_mode_list: ['workbench'] })
26
+ });
27
+ const data = await res.json();
28
+ const items = data?.data?.history_list || [];
29
+ return items.slice(0, limit).map(item => {
30
+ const params = item.aigc_image_params?.text2image_params || {};
31
+ const images = item.image?.large_images || [];
32
+ return {
33
+ prompt: params.prompt || item.common_attr?.title || 'N/A',
34
+ model: params.model_config?.model_name || 'unknown',
35
+ status: item.common_attr?.status === 102 ? 'completed' : 'pending',
36
+ image_url: images[0]?.image_url || '',
37
+ created_at: new Date((item.common_attr?.create_time || 0) * 1000).toLocaleString('zh-CN'),
38
+ };
39
+ });
40
+ })()
41
+ - map:
42
+ prompt: ${{ item.prompt }}
43
+ model: ${{ item.model }}
44
+ status: ${{ item.status }}
45
+ image_url: ${{ item.image_url }}
46
+ created_at: ${{ item.created_at }}
47
+ - limit: ${{ args.limit }}
@@ -0,0 +1,41 @@
1
+ site: linux-do
2
+ name: categories
3
+ description: linux.do 分类列表
4
+ domain: linux.do
5
+ browser: true
6
+
7
+ args:
8
+ limit:
9
+ type: int
10
+ default: 20
11
+ description: Number of categories
12
+
13
+ pipeline:
14
+ - navigate: https://linux.do
15
+
16
+ - evaluate: |
17
+ (async () => {
18
+ const res = await fetch('/categories.json', { credentials: 'include' });
19
+ if (!res.ok) throw new Error('HTTP ' + res.status + ' - 请先登录 linux.do');
20
+ let data;
21
+ try { data = await res.json(); } catch { throw new Error('响应不是有效 JSON - 请先登录 linux.do'); }
22
+ const cats = data?.category_list?.categories || [];
23
+ return cats.slice(0, ${{ args.limit }}).map(c => ({
24
+ name: c.name,
25
+ slug: c.slug,
26
+ id: c.id,
27
+ topics: c.topic_count,
28
+ description: (c.description_text || '').slice(0, 80),
29
+ }));
30
+ })()
31
+
32
+ - map:
33
+ name: ${{ item.name }}
34
+ slug: ${{ item.slug }}
35
+ id: ${{ item.id }}
36
+ topics: ${{ item.topics }}
37
+ description: ${{ item.description }}
38
+
39
+ - limit: ${{ args.limit }}
40
+
41
+ columns: [name, slug, id, topics, description]
@@ -0,0 +1,49 @@
1
+ site: linux-do
2
+ name: category
3
+ description: linux.do 分类内话题
4
+ domain: linux.do
5
+ browser: true
6
+
7
+ args:
8
+ slug:
9
+ type: str
10
+ required: true
11
+ description: Category slug (use 'categories' command to find)
12
+ id:
13
+ type: int
14
+ required: true
15
+ description: Category ID (use 'categories' command to find)
16
+ limit:
17
+ type: int
18
+ default: 20
19
+ description: Number of topics
20
+
21
+ pipeline:
22
+ - navigate: https://linux.do
23
+
24
+ - evaluate: |
25
+ (async () => {
26
+ const slug = ${{ args.slug | json }};
27
+ const res = await fetch('/c/' + encodeURIComponent(slug) + '/${{ args.id }}.json', { credentials: 'include' });
28
+ if (!res.ok) throw new Error('HTTP ' + res.status + ' - 请先登录 linux.do');
29
+ let data;
30
+ try { data = await res.json(); } catch { throw new Error('响应不是有效 JSON - 请先登录 linux.do'); }
31
+ const topics = data?.topic_list?.topics || [];
32
+ return topics.slice(0, ${{ args.limit }}).map(t => ({
33
+ title: t.title,
34
+ replies: (t.posts_count || 1) - 1,
35
+ views: t.views,
36
+ likes: t.like_count,
37
+ }));
38
+ })()
39
+
40
+ - map:
41
+ rank: ${{ index + 1 }}
42
+ title: ${{ item.title }}
43
+ replies: ${{ item.replies }}
44
+ views: ${{ item.views }}
45
+ likes: ${{ item.likes }}
46
+
47
+ - limit: ${{ args.limit }}
48
+
49
+ columns: [rank, title, replies, views, likes]
@@ -0,0 +1,50 @@
1
+ site: linux-do
2
+ name: hot
3
+ description: linux.do 热门话题
4
+ domain: linux.do
5
+ browser: true
6
+
7
+ args:
8
+ limit:
9
+ type: int
10
+ default: 20
11
+ description: Number of topics
12
+ period:
13
+ type: str
14
+ default: weekly
15
+ description: Time period
16
+ choices: [all, daily, weekly, monthly, yearly]
17
+
18
+ pipeline:
19
+ - navigate: https://linux.do
20
+
21
+ - evaluate: |
22
+ (async () => {
23
+ const period = ${{ args.period | json }};
24
+ const res = await fetch('/top.json?period=' + encodeURIComponent(period), { credentials: 'include' });
25
+ if (!res.ok) throw new Error('HTTP ' + res.status + ' - 请先登录 linux.do');
26
+ let data;
27
+ try { data = await res.json(); } catch { throw new Error('响应不是有效 JSON - 请先登录 linux.do'); }
28
+ const topics = data?.topic_list?.topics || [];
29
+ const cats = data?.topic_list?.categories || data?.categories || [];
30
+ const catMap = Object.fromEntries(cats.map(c => [c.id, c.name]));
31
+ return topics.slice(0, ${{ args.limit }}).map(t => ({
32
+ title: t.title,
33
+ replies: (t.posts_count || 1) - 1,
34
+ views: t.views,
35
+ likes: t.like_count,
36
+ category: catMap[t.category_id] || String(t.category_id),
37
+ }));
38
+ })()
39
+
40
+ - map:
41
+ rank: ${{ index + 1 }}
42
+ title: ${{ item.title }}
43
+ replies: ${{ item.replies }}
44
+ views: ${{ item.views }}
45
+ likes: ${{ item.likes }}
46
+ category: ${{ item.category }}
47
+
48
+ - limit: ${{ args.limit }}
49
+
50
+ columns: [rank, title, replies, views, likes, category]
@@ -0,0 +1,40 @@
1
+ site: linux-do
2
+ name: latest
3
+ description: linux.do 最新话题
4
+ domain: linux.do
5
+ browser: true
6
+
7
+ args:
8
+ limit:
9
+ type: int
10
+ default: 20
11
+ description: Number of topics
12
+
13
+ pipeline:
14
+ - navigate: https://linux.do
15
+
16
+ - evaluate: |
17
+ (async () => {
18
+ const res = await fetch('/latest.json', { credentials: 'include' });
19
+ if (!res.ok) throw new Error('HTTP ' + res.status + ' - 请先登录 linux.do');
20
+ let data;
21
+ try { data = await res.json(); } catch { throw new Error('响应不是有效 JSON - 请先登录 linux.do'); }
22
+ const topics = data?.topic_list?.topics || [];
23
+ return topics.slice(0, ${{ args.limit }}).map(t => ({
24
+ title: t.title,
25
+ replies: (t.posts_count || 1) - 1,
26
+ views: t.views,
27
+ likes: t.like_count,
28
+ }));
29
+ })()
30
+
31
+ - map:
32
+ rank: ${{ index + 1 }}
33
+ title: ${{ item.title }}
34
+ replies: ${{ item.replies }}
35
+ views: ${{ item.views }}
36
+ likes: ${{ item.likes }}
37
+
38
+ - limit: ${{ args.limit }}
39
+
40
+ columns: [rank, title, replies, views, likes]
@@ -0,0 +1,45 @@
1
+ site: linux-do
2
+ name: search
3
+ description: 搜索 linux.do
4
+ domain: linux.do
5
+ browser: true
6
+
7
+ args:
8
+ keyword:
9
+ type: str
10
+ required: true
11
+ description: Search keyword
12
+ limit:
13
+ type: int
14
+ default: 20
15
+ description: Number of results
16
+
17
+ pipeline:
18
+ - navigate: https://linux.do
19
+
20
+ - evaluate: |
21
+ (async () => {
22
+ const keyword = ${{ args.keyword | json }};
23
+ const res = await fetch('/search.json?q=' + encodeURIComponent(keyword), { credentials: 'include' });
24
+ if (!res.ok) throw new Error('HTTP ' + res.status + ' - 请先登录 linux.do');
25
+ let data;
26
+ try { data = await res.json(); } catch { throw new Error('响应不是有效 JSON - 请先登录 linux.do'); }
27
+ const topics = data?.topics || [];
28
+ return topics.slice(0, ${{ args.limit }}).map(t => ({
29
+ title: t.title,
30
+ views: t.views,
31
+ likes: t.like_count,
32
+ replies: (t.posts_count || 1) - 1,
33
+ }));
34
+ })()
35
+
36
+ - map:
37
+ rank: ${{ index + 1 }}
38
+ title: ${{ item.title }}
39
+ views: ${{ item.views }}
40
+ likes: ${{ item.likes }}
41
+ replies: ${{ item.replies }}
42
+
43
+ - limit: ${{ args.limit }}
44
+
45
+ columns: [rank, title, views, likes, replies]
@@ -0,0 +1,38 @@
1
+ site: linux-do
2
+ name: topic
3
+ description: linux.do 帖子详情和回复(首页)
4
+ domain: linux.do
5
+ browser: true
6
+
7
+ args:
8
+ id:
9
+ type: int
10
+ required: true
11
+ description: Topic ID
12
+
13
+ pipeline:
14
+ - navigate: https://linux.do
15
+
16
+ - evaluate: |
17
+ (async () => {
18
+ const res = await fetch('/t/${{ args.id }}.json', { credentials: 'include' });
19
+ if (!res.ok) throw new Error('HTTP ' + res.status + ' - 请先登录 linux.do');
20
+ let data;
21
+ try { data = await res.json(); } catch { throw new Error('响应不是有效 JSON - 请先登录 linux.do'); }
22
+ const strip = (html) => (html || '').replace(/<br\s*\/?>/gi, ' ').replace(/<\/(p|div|li|blockquote|h[1-6])>/gi, ' ').replace(/<[^>]+>/g, '').replace(/&nbsp;/g, ' ').replace(/&amp;/g, '&').replace(/&lt;/g, '<').replace(/&gt;/g, '>').replace(/&quot;/g, '"').replace(/&#(?:(\d+)|x([0-9a-fA-F]+));/g, (_, dec, hex) => { try { return String.fromCodePoint(dec !== undefined ? Number(dec) : parseInt(hex, 16)); } catch { return ''; } }).replace(/\s+/g, ' ').trim();
23
+ const posts = data?.post_stream?.posts || [];
24
+ return posts.map(p => ({
25
+ author: p.username,
26
+ content: strip(p.cooked).slice(0, 200),
27
+ likes: p.like_count,
28
+ created_at: p.created_at,
29
+ }));
30
+ })()
31
+
32
+ - map:
33
+ author: ${{ item.author }}
34
+ content: ${{ item.content }}
35
+ likes: ${{ item.likes }}
36
+ created_at: ${{ item.created_at }}
37
+
38
+ columns: [author, content, likes, created_at]
@@ -0,0 +1 @@
1
+ export declare const exportCommand: import("../../registry.js").CliCommand;
@@ -0,0 +1,31 @@
1
+ import * as fs from 'node:fs';
2
+ import { cli, Strategy } from '../../registry.js';
3
+ export const exportCommand = cli({
4
+ site: 'notion',
5
+ name: 'export',
6
+ description: 'Export the current Notion page as Markdown',
7
+ domain: 'localhost',
8
+ strategy: Strategy.UI,
9
+ browser: true,
10
+ args: [
11
+ { name: 'output', required: false, positional: true, help: 'Output file (default: /tmp/notion-export.md)' },
12
+ ],
13
+ columns: ['Status', 'File'],
14
+ func: async (page, kwargs) => {
15
+ const outputPath = kwargs.output || '/tmp/notion-export.md';
16
+ const result = await page.evaluate(`
17
+ (function() {
18
+ const titleEl = document.querySelector('[data-block-id] [placeholder="Untitled"], h1.notion-title, [class*="title"]');
19
+ const title = titleEl ? (titleEl.textContent || '').trim() : document.title;
20
+
21
+ const frame = document.querySelector('.notion-page-content, [class*="page-content"], main');
22
+ const content = frame ? (frame.innerText || '').trim() : document.body.innerText;
23
+
24
+ return { title, content };
25
+ })()
26
+ `);
27
+ const md = `# ${result.title}\n\n${result.content}`;
28
+ fs.writeFileSync(outputPath, md);
29
+ return [{ Status: 'Success', File: outputPath }];
30
+ },
31
+ });
@@ -0,0 +1 @@
1
+ export declare const favoritesCommand: import("../../registry.js").CliCommand;
@@ -0,0 +1,84 @@
1
+ import { cli, Strategy } from '../../registry.js';
2
+ export const favoritesCommand = cli({
3
+ site: 'notion',
4
+ name: 'favorites',
5
+ description: 'List pages from the Notion Favorites section in the sidebar',
6
+ domain: 'localhost',
7
+ strategy: Strategy.UI,
8
+ browser: true,
9
+ args: [],
10
+ columns: ['Index', 'Title', 'Icon'],
11
+ func: async (page) => {
12
+ const items = await page.evaluate(`
13
+ (function() {
14
+ const results = [];
15
+
16
+ // Strategy 1: Use Notion's own class 'notion-outliner-bookmarks-header-container'
17
+ const headerContainer = document.querySelector('.notion-outliner-bookmarks-header-container');
18
+ if (headerContainer) {
19
+ // Walk up to the section parent that wraps header + items
20
+ let section = headerContainer.parentElement;
21
+ if (section && section.children.length === 1) section = section.parentElement;
22
+
23
+ if (section) {
24
+ const treeItems = section.querySelectorAll('[role="treeitem"]');
25
+ treeItems.forEach((item) => {
26
+ // Title text is in a div.notranslate sibling of the icon area
27
+ const titleEl = item.querySelector('div.notranslate:not(.notion-record-icon)');
28
+ const title = titleEl
29
+ ? titleEl.textContent.trim()
30
+ : (item.textContent || '').trim().substring(0, 80);
31
+
32
+ // Icon/emoji is in the notion-record-icon element
33
+ const iconEl = item.querySelector('.notion-record-icon');
34
+ const icon = iconEl ? iconEl.textContent.trim().substring(0, 4) : '';
35
+
36
+ if (title && title.length > 0) {
37
+ results.push({ Index: results.length + 1, Title: title, Icon: icon || '📄' });
38
+ }
39
+ });
40
+ }
41
+ }
42
+
43
+ // Strategy 2: Fallback — find "Favorites" text node and walk DOM
44
+ if (results.length === 0) {
45
+ const walker = document.createTreeWalker(document.body, NodeFilter.SHOW_TEXT, null);
46
+ let node;
47
+ let favEl = null;
48
+ while (node = walker.nextNode()) {
49
+ const text = node.textContent.trim();
50
+ if (text === 'Favorites' || text === '收藏' || text === '收藏夹') {
51
+ favEl = node.parentElement;
52
+ break;
53
+ }
54
+ }
55
+
56
+ if (favEl) {
57
+ let section = favEl;
58
+ for (let i = 0; i < 6; i++) {
59
+ const p = section.parentElement;
60
+ if (!p || p === document.body) break;
61
+ const treeItems = p.querySelectorAll(':scope > [role="treeitem"]');
62
+ if (treeItems.length > 0) { section = p; break; }
63
+ section = p;
64
+ }
65
+
66
+ const treeItems = section.querySelectorAll('[role="treeitem"]');
67
+ treeItems.forEach((item) => {
68
+ const text = (item.textContent || '').trim().substring(0, 120);
69
+ if (text && text.length > 1 && !text.match(/^(Favorites|收藏夹?)$/)) {
70
+ results.push({ Index: results.length + 1, Title: text, Icon: '📄' });
71
+ }
72
+ });
73
+ }
74
+ }
75
+
76
+ return results;
77
+ })()
78
+ `);
79
+ if (items.length === 0) {
80
+ return [{ Index: 0, Title: 'No favorites found. Make sure sidebar is visible and you have favorites.', Icon: '⚠️' }];
81
+ }
82
+ return items;
83
+ },
84
+ });
@@ -0,0 +1 @@
1
+ export declare const newCommand: import("../../registry.js").CliCommand;
@@ -0,0 +1,34 @@
1
+ import { cli, Strategy } from '../../registry.js';
2
+ export const newCommand = cli({
3
+ site: 'notion',
4
+ name: 'new',
5
+ description: 'Create a new page in Notion',
6
+ domain: 'localhost',
7
+ strategy: Strategy.UI,
8
+ browser: true,
9
+ args: [
10
+ { name: 'title', required: false, positional: true, help: 'Page title (optional)' },
11
+ ],
12
+ columns: ['Status'],
13
+ func: async (page, kwargs) => {
14
+ const title = kwargs.title;
15
+ // Cmd+N creates a new page in Notion
16
+ const isMac = process.platform === 'darwin';
17
+ await page.pressKey(isMac ? 'Meta+N' : 'Control+N');
18
+ await page.wait(1);
19
+ // If title is provided, type it into the title field
20
+ if (title) {
21
+ await page.evaluate(`
22
+ (function(t) {
23
+ const titleEl = document.querySelector('[placeholder="Untitled"], [data-content-editable-leaf] [placeholder]');
24
+ if (titleEl) {
25
+ titleEl.focus();
26
+ document.execCommand('insertText', false, t);
27
+ }
28
+ })(${JSON.stringify(title)})
29
+ `);
30
+ await page.wait(0.5);
31
+ }
32
+ return [{ Status: title ? `Created page: ${title}` : 'New blank page created' }];
33
+ },
34
+ });
@@ -0,0 +1 @@
1
+ export declare const readCommand: import("../../registry.js").CliCommand;
@@ -0,0 +1,30 @@
1
+ import { cli, Strategy } from '../../registry.js';
2
+ export const readCommand = cli({
3
+ site: 'notion',
4
+ name: 'read',
5
+ description: 'Read the content of the currently open Notion page',
6
+ domain: 'localhost',
7
+ strategy: Strategy.UI,
8
+ browser: true,
9
+ args: [],
10
+ columns: ['Title', 'Content'],
11
+ func: async (page) => {
12
+ const result = await page.evaluate(`
13
+ (function() {
14
+ // Get the page title
15
+ const titleEl = document.querySelector('[data-block-id] [placeholder="Untitled"], .notion-page-block .notranslate, h1.notion-title, [class*="title"]');
16
+ const title = titleEl ? (titleEl.textContent || '').trim() : document.title;
17
+
18
+ // Get the page content — Notion renders blocks in a frame
19
+ const frame = document.querySelector('.notion-page-content, [class*="page-content"], .layout-content, main');
20
+ const content = frame ? (frame.innerText || frame.textContent || '').trim() : '';
21
+
22
+ return { title, content };
23
+ })()
24
+ `);
25
+ return [{
26
+ Title: result.title || 'Untitled',
27
+ Content: result.content || '(empty page)',
28
+ }];
29
+ },
30
+ });
@@ -0,0 +1 @@
1
+ export declare const searchCommand: import("../../registry.js").CliCommand;
@@ -0,0 +1,46 @@
1
+ import { cli, Strategy } from '../../registry.js';
2
+ export const searchCommand = cli({
3
+ site: 'notion',
4
+ name: 'search',
5
+ description: 'Search pages and databases in Notion via Quick Find (Cmd+P)',
6
+ domain: 'localhost',
7
+ strategy: Strategy.UI,
8
+ browser: true,
9
+ args: [{ name: 'query', required: true, positional: true, help: 'Search query' }],
10
+ columns: ['Index', 'Title'],
11
+ func: async (page, kwargs) => {
12
+ const query = kwargs.query;
13
+ // Open Quick Find
14
+ const isMac = process.platform === 'darwin';
15
+ await page.pressKey(isMac ? 'Meta+P' : 'Control+P');
16
+ await page.wait(0.5);
17
+ // Type the search query
18
+ await page.evaluate(`
19
+ (function(q) {
20
+ const input = document.querySelector('input[placeholder*="Search"], input[type="text"]');
21
+ if (input) {
22
+ const setter = Object.getOwnPropertyDescriptor(window.HTMLInputElement.prototype, 'value').set;
23
+ setter.call(input, q);
24
+ input.dispatchEvent(new Event('input', { bubbles: true }));
25
+ }
26
+ })(${JSON.stringify(query)})
27
+ `);
28
+ await page.wait(1.5);
29
+ // Scrape results
30
+ const results = await page.evaluate(`
31
+ (function() {
32
+ const items = document.querySelectorAll('[role="option"], [class*="searchResult"], [class*="quick-find"] [role="button"]');
33
+ return Array.from(items).slice(0, 20).map((item, i) => ({
34
+ Index: i + 1,
35
+ Title: (item.textContent || '').trim().substring(0, 120),
36
+ }));
37
+ })()
38
+ `);
39
+ // Close Quick Find
40
+ await page.pressKey('Escape');
41
+ if (results.length === 0) {
42
+ return [{ Index: 0, Title: `No results for "${query}"` }];
43
+ }
44
+ return results;
45
+ },
46
+ });
@@ -0,0 +1 @@
1
+ export declare const sidebarCommand: import("../../registry.js").CliCommand;