@jackwener/opencli 0.9.6 → 1.0.0

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 (307) 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/CDP.md +1 -1
  14. package/CDP.zh-CN.md +1 -1
  15. package/CLI-ELECTRON.md +89 -36
  16. package/CLI-EXPLORER.md +4 -4
  17. package/CONTRIBUTING.md +167 -0
  18. package/README.md +113 -89
  19. package/README.zh-CN.md +114 -91
  20. package/SKILL.md +10 -8
  21. package/TESTING.md +7 -7
  22. package/dist/browser/daemon-client.d.ts +37 -0
  23. package/dist/browser/daemon-client.js +82 -0
  24. package/dist/browser/discover.d.ts +11 -34
  25. package/dist/browser/discover.js +15 -190
  26. package/dist/browser/errors.d.ts +6 -20
  27. package/dist/browser/errors.js +24 -63
  28. package/dist/browser/index.d.ts +2 -11
  29. package/dist/browser/index.js +5 -11
  30. package/dist/browser/mcp.d.ts +9 -18
  31. package/dist/browser/mcp.js +70 -284
  32. package/dist/browser/page.d.ts +28 -6
  33. package/dist/browser/page.js +210 -85
  34. package/dist/browser.test.js +4 -202
  35. package/dist/build-manifest.d.ts +26 -0
  36. package/dist/build-manifest.js +132 -60
  37. package/dist/build-manifest.test.d.ts +1 -0
  38. package/dist/build-manifest.test.js +26 -0
  39. package/dist/cli-manifest.json +1582 -29
  40. package/dist/clis/bilibili/download.d.ts +10 -0
  41. package/dist/clis/bilibili/download.js +135 -0
  42. package/dist/clis/chatwise/ask.d.ts +1 -0
  43. package/dist/clis/chatwise/ask.js +76 -0
  44. package/dist/clis/chatwise/export.d.ts +1 -0
  45. package/dist/clis/chatwise/export.js +46 -0
  46. package/dist/clis/chatwise/history.d.ts +1 -0
  47. package/dist/clis/chatwise/history.js +43 -0
  48. package/dist/clis/chatwise/model.d.ts +1 -0
  49. package/dist/clis/chatwise/model.js +81 -0
  50. package/dist/clis/chatwise/new.d.ts +1 -0
  51. package/dist/clis/chatwise/new.js +18 -0
  52. package/dist/clis/chatwise/read.d.ts +1 -0
  53. package/dist/clis/chatwise/read.js +39 -0
  54. package/dist/clis/chatwise/screenshot.d.ts +1 -0
  55. package/dist/clis/chatwise/screenshot.js +27 -0
  56. package/dist/clis/chatwise/send.d.ts +1 -0
  57. package/dist/clis/chatwise/send.js +45 -0
  58. package/dist/clis/chatwise/status.d.ts +1 -0
  59. package/dist/clis/chatwise/status.js +22 -0
  60. package/dist/clis/discord-app/channels.d.ts +1 -0
  61. package/dist/clis/discord-app/channels.js +45 -0
  62. package/dist/clis/discord-app/members.d.ts +1 -0
  63. package/dist/clis/discord-app/members.js +38 -0
  64. package/dist/clis/discord-app/read.d.ts +1 -0
  65. package/dist/clis/discord-app/read.js +45 -0
  66. package/dist/clis/discord-app/search.d.ts +1 -0
  67. package/dist/clis/discord-app/search.js +56 -0
  68. package/dist/clis/discord-app/send.d.ts +1 -0
  69. package/dist/clis/discord-app/send.js +27 -0
  70. package/dist/clis/discord-app/servers.d.ts +1 -0
  71. package/dist/clis/discord-app/servers.js +36 -0
  72. package/dist/clis/discord-app/status.d.ts +1 -0
  73. package/dist/clis/discord-app/status.js +16 -0
  74. package/dist/clis/feishu/new.d.ts +1 -0
  75. package/dist/clis/feishu/new.js +27 -0
  76. package/dist/clis/feishu/read.d.ts +1 -0
  77. package/dist/clis/feishu/read.js +40 -0
  78. package/dist/clis/feishu/search.d.ts +1 -0
  79. package/dist/clis/feishu/search.js +30 -0
  80. package/dist/clis/feishu/send.d.ts +1 -0
  81. package/dist/clis/feishu/send.js +39 -0
  82. package/dist/clis/feishu/status.d.ts +1 -0
  83. package/dist/clis/feishu/status.js +28 -0
  84. package/dist/clis/grok/ask.d.ts +1 -0
  85. package/dist/clis/grok/ask.js +82 -0
  86. package/dist/clis/grok/debug.d.ts +1 -0
  87. package/dist/clis/grok/debug.js +45 -0
  88. package/dist/clis/jimeng/generate.yaml +84 -0
  89. package/dist/clis/jimeng/history.yaml +47 -0
  90. package/dist/clis/linux-do/categories.yaml +41 -0
  91. package/dist/clis/linux-do/category.yaml +49 -0
  92. package/dist/clis/linux-do/hot.yaml +50 -0
  93. package/dist/clis/linux-do/latest.yaml +40 -0
  94. package/dist/clis/linux-do/search.yaml +45 -0
  95. package/dist/clis/linux-do/topic.yaml +38 -0
  96. package/dist/clis/neteasemusic/like.d.ts +1 -0
  97. package/dist/clis/neteasemusic/like.js +25 -0
  98. package/dist/clis/neteasemusic/lyrics.d.ts +1 -0
  99. package/dist/clis/neteasemusic/lyrics.js +47 -0
  100. package/dist/clis/neteasemusic/next.d.ts +1 -0
  101. package/dist/clis/neteasemusic/next.js +26 -0
  102. package/dist/clis/neteasemusic/play.d.ts +1 -0
  103. package/dist/clis/neteasemusic/play.js +26 -0
  104. package/dist/clis/neteasemusic/playing.d.ts +1 -0
  105. package/dist/clis/neteasemusic/playing.js +59 -0
  106. package/dist/clis/neteasemusic/playlist.d.ts +1 -0
  107. package/dist/clis/neteasemusic/playlist.js +46 -0
  108. package/dist/clis/neteasemusic/prev.d.ts +1 -0
  109. package/dist/clis/neteasemusic/prev.js +25 -0
  110. package/dist/clis/neteasemusic/search.d.ts +1 -0
  111. package/dist/clis/neteasemusic/search.js +52 -0
  112. package/dist/clis/neteasemusic/status.d.ts +1 -0
  113. package/dist/clis/neteasemusic/status.js +16 -0
  114. package/dist/clis/neteasemusic/volume.d.ts +1 -0
  115. package/dist/clis/neteasemusic/volume.js +54 -0
  116. package/dist/clis/notion/export.d.ts +1 -0
  117. package/dist/clis/notion/export.js +31 -0
  118. package/dist/clis/notion/favorites.d.ts +1 -0
  119. package/dist/clis/notion/favorites.js +84 -0
  120. package/dist/clis/notion/new.d.ts +1 -0
  121. package/dist/clis/notion/new.js +34 -0
  122. package/dist/clis/notion/read.d.ts +1 -0
  123. package/dist/clis/notion/read.js +30 -0
  124. package/dist/clis/notion/search.d.ts +1 -0
  125. package/dist/clis/notion/search.js +46 -0
  126. package/dist/clis/notion/sidebar.d.ts +1 -0
  127. package/dist/clis/notion/sidebar.js +41 -0
  128. package/dist/clis/notion/status.d.ts +1 -0
  129. package/dist/clis/notion/status.js +16 -0
  130. package/dist/clis/notion/write.d.ts +1 -0
  131. package/dist/clis/notion/write.js +40 -0
  132. package/dist/clis/twitter/download.d.ts +8 -0
  133. package/dist/clis/twitter/download.js +204 -0
  134. package/dist/clis/wechat/chats.d.ts +1 -0
  135. package/dist/clis/wechat/chats.js +28 -0
  136. package/dist/clis/wechat/contacts.d.ts +1 -0
  137. package/dist/clis/wechat/contacts.js +28 -0
  138. package/dist/clis/wechat/read.d.ts +1 -0
  139. package/dist/clis/wechat/read.js +58 -0
  140. package/dist/clis/wechat/search.d.ts +1 -0
  141. package/dist/clis/wechat/search.js +31 -0
  142. package/dist/clis/wechat/send.d.ts +1 -0
  143. package/dist/clis/wechat/send.js +42 -0
  144. package/dist/clis/wechat/status.d.ts +1 -0
  145. package/dist/clis/wechat/status.js +29 -0
  146. package/dist/clis/xiaohongshu/creator-note-detail.d.ts +10 -0
  147. package/dist/clis/xiaohongshu/creator-note-detail.js +88 -0
  148. package/dist/clis/xiaohongshu/creator-notes.d.ts +11 -0
  149. package/dist/clis/xiaohongshu/creator-notes.js +109 -0
  150. package/dist/clis/xiaohongshu/creator-profile.d.ts +10 -0
  151. package/dist/clis/xiaohongshu/creator-profile.js +54 -0
  152. package/dist/clis/xiaohongshu/creator-stats.d.ts +10 -0
  153. package/dist/clis/xiaohongshu/creator-stats.js +74 -0
  154. package/dist/clis/xiaohongshu/download.d.ts +7 -0
  155. package/dist/clis/xiaohongshu/download.js +155 -0
  156. package/dist/clis/xiaohongshu/search.js +1 -1
  157. package/dist/clis/xiaohongshu/user-helpers.d.ts +15 -0
  158. package/dist/clis/xiaohongshu/user-helpers.js +67 -0
  159. package/dist/clis/xiaohongshu/user-helpers.test.d.ts +1 -0
  160. package/dist/clis/xiaohongshu/user-helpers.test.js +81 -0
  161. package/dist/clis/xiaohongshu/user.js +46 -29
  162. package/dist/clis/zhihu/download.d.ts +11 -0
  163. package/dist/clis/zhihu/download.js +186 -0
  164. package/dist/clis/zhihu/download.test.d.ts +1 -0
  165. package/dist/clis/zhihu/download.test.js +10 -0
  166. package/dist/daemon.d.ts +13 -0
  167. package/dist/daemon.js +187 -0
  168. package/dist/doctor.d.ts +27 -61
  169. package/dist/doctor.js +70 -601
  170. package/dist/doctor.test.js +30 -170
  171. package/dist/download/index.d.ts +79 -0
  172. package/dist/download/index.js +325 -0
  173. package/dist/download/progress.d.ts +36 -0
  174. package/dist/download/progress.js +111 -0
  175. package/dist/engine.test.js +15 -0
  176. package/dist/main.js +22 -28
  177. package/dist/pipeline/executor.test.js +1 -0
  178. package/dist/pipeline/registry.js +2 -0
  179. package/dist/pipeline/steps/browser.js +2 -2
  180. package/dist/pipeline/steps/download.d.ts +34 -0
  181. package/dist/pipeline/steps/download.js +251 -0
  182. package/dist/pipeline/steps/intercept.js +1 -2
  183. package/dist/pipeline/template.js +28 -0
  184. package/dist/setup.d.ts +6 -0
  185. package/dist/setup.js +46 -160
  186. package/dist/types.d.ts +6 -0
  187. package/extension/icons/icon-128.png +0 -0
  188. package/extension/icons/icon-16.png +0 -0
  189. package/extension/icons/icon-32.png +0 -0
  190. package/extension/icons/icon-48.png +0 -0
  191. package/extension/manifest.json +31 -0
  192. package/extension/package.json +16 -0
  193. package/extension/src/background.ts +293 -0
  194. package/extension/src/cdp.ts +125 -0
  195. package/extension/src/protocol.ts +57 -0
  196. package/extension/store-assets/screenshot-1280x800.png +0 -0
  197. package/extension/tsconfig.json +15 -0
  198. package/extension/vite.config.ts +18 -0
  199. package/package.json +8 -7
  200. package/scripts/test-site.mjs +70 -0
  201. package/src/browser/daemon-client.ts +113 -0
  202. package/src/browser/discover.ts +18 -216
  203. package/src/browser/errors.ts +30 -100
  204. package/src/browser/index.ts +6 -12
  205. package/src/browser/mcp.ts +78 -278
  206. package/src/browser/page.ts +222 -88
  207. package/src/browser.test.ts +3 -210
  208. package/src/build-manifest.test.ts +28 -0
  209. package/src/build-manifest.ts +147 -57
  210. package/src/clis/bilibili/download.ts +161 -0
  211. package/src/clis/chatgpt/README.md +1 -1
  212. package/src/clis/chatgpt/README.zh-CN.md +1 -1
  213. package/src/clis/chatwise/README.md +38 -0
  214. package/src/clis/chatwise/README.zh-CN.md +38 -0
  215. package/src/clis/chatwise/ask.ts +87 -0
  216. package/src/clis/chatwise/export.ts +51 -0
  217. package/src/clis/chatwise/history.ts +47 -0
  218. package/src/clis/chatwise/model.ts +87 -0
  219. package/src/clis/chatwise/new.ts +21 -0
  220. package/src/clis/chatwise/read.ts +42 -0
  221. package/src/clis/chatwise/screenshot.ts +33 -0
  222. package/src/clis/chatwise/send.ts +50 -0
  223. package/src/clis/chatwise/status.ts +25 -0
  224. package/src/clis/discord-app/README.md +28 -0
  225. package/src/clis/discord-app/README.zh-CN.md +28 -0
  226. package/src/clis/discord-app/channels.ts +48 -0
  227. package/src/clis/discord-app/members.ts +41 -0
  228. package/src/clis/discord-app/read.ts +49 -0
  229. package/src/clis/discord-app/search.ts +64 -0
  230. package/src/clis/discord-app/send.ts +32 -0
  231. package/src/clis/discord-app/servers.ts +39 -0
  232. package/src/clis/discord-app/status.ts +18 -0
  233. package/src/clis/feishu/README.md +20 -0
  234. package/src/clis/feishu/README.zh-CN.md +20 -0
  235. package/src/clis/feishu/new.ts +32 -0
  236. package/src/clis/feishu/read.ts +48 -0
  237. package/src/clis/feishu/search.ts +35 -0
  238. package/src/clis/feishu/send.ts +46 -0
  239. package/src/clis/feishu/status.ts +34 -0
  240. package/src/clis/grok/ask.ts +90 -0
  241. package/src/clis/grok/debug.ts +49 -0
  242. package/src/clis/jimeng/generate.yaml +84 -0
  243. package/src/clis/jimeng/history.yaml +47 -0
  244. package/src/clis/linux-do/categories.yaml +41 -0
  245. package/src/clis/linux-do/category.yaml +49 -0
  246. package/src/clis/linux-do/hot.yaml +50 -0
  247. package/src/clis/linux-do/latest.yaml +40 -0
  248. package/src/clis/linux-do/search.yaml +45 -0
  249. package/src/clis/linux-do/topic.yaml +38 -0
  250. package/src/clis/neteasemusic/README.md +31 -0
  251. package/src/clis/neteasemusic/README.zh-CN.md +31 -0
  252. package/src/clis/neteasemusic/like.ts +28 -0
  253. package/src/clis/neteasemusic/lyrics.ts +53 -0
  254. package/src/clis/neteasemusic/next.ts +30 -0
  255. package/src/clis/neteasemusic/play.ts +30 -0
  256. package/src/clis/neteasemusic/playing.ts +62 -0
  257. package/src/clis/neteasemusic/playlist.ts +51 -0
  258. package/src/clis/neteasemusic/prev.ts +29 -0
  259. package/src/clis/neteasemusic/search.ts +58 -0
  260. package/src/clis/neteasemusic/status.ts +18 -0
  261. package/src/clis/neteasemusic/volume.ts +61 -0
  262. package/src/clis/notion/README.md +29 -0
  263. package/src/clis/notion/README.zh-CN.md +29 -0
  264. package/src/clis/notion/export.ts +36 -0
  265. package/src/clis/notion/favorites.ts +87 -0
  266. package/src/clis/notion/new.ts +39 -0
  267. package/src/clis/notion/read.ts +33 -0
  268. package/src/clis/notion/search.ts +54 -0
  269. package/src/clis/notion/sidebar.ts +44 -0
  270. package/src/clis/notion/status.ts +18 -0
  271. package/src/clis/notion/write.ts +45 -0
  272. package/src/clis/twitter/download.ts +227 -0
  273. package/src/clis/wechat/README.md +28 -0
  274. package/src/clis/wechat/README.zh-CN.md +28 -0
  275. package/src/clis/wechat/chats.ts +33 -0
  276. package/src/clis/wechat/contacts.ts +33 -0
  277. package/src/clis/wechat/read.ts +72 -0
  278. package/src/clis/wechat/search.ts +36 -0
  279. package/src/clis/wechat/send.ts +49 -0
  280. package/src/clis/wechat/status.ts +35 -0
  281. package/src/clis/xiaohongshu/creator-note-detail.ts +95 -0
  282. package/src/clis/xiaohongshu/creator-notes.ts +116 -0
  283. package/src/clis/xiaohongshu/creator-profile.ts +60 -0
  284. package/src/clis/xiaohongshu/creator-stats.ts +81 -0
  285. package/src/clis/xiaohongshu/download.ts +173 -0
  286. package/src/clis/xiaohongshu/search.ts +1 -1
  287. package/src/clis/xiaohongshu/user-helpers.test.ts +106 -0
  288. package/src/clis/xiaohongshu/user-helpers.ts +85 -0
  289. package/src/clis/xiaohongshu/user.ts +52 -32
  290. package/src/clis/zhihu/download.test.ts +12 -0
  291. package/src/clis/zhihu/download.ts +223 -0
  292. package/src/daemon.ts +217 -0
  293. package/src/doctor.test.ts +32 -193
  294. package/src/doctor.ts +74 -668
  295. package/src/download/index.ts +395 -0
  296. package/src/download/progress.ts +125 -0
  297. package/src/engine.test.ts +17 -0
  298. package/src/main.ts +18 -26
  299. package/src/pipeline/executor.test.ts +1 -0
  300. package/src/pipeline/registry.ts +2 -0
  301. package/src/pipeline/steps/browser.ts +2 -2
  302. package/src/pipeline/steps/download.ts +310 -0
  303. package/src/pipeline/steps/intercept.ts +1 -2
  304. package/src/pipeline/template.ts +26 -0
  305. package/src/setup.ts +47 -183
  306. package/src/types.ts +1 -0
  307. package/tests/e2e/browser-auth.test.ts +25 -0
@@ -0,0 +1,87 @@
1
+ import { cli, Strategy } from '../../registry.js';
2
+ import type { IPage } from '../../types.js';
3
+
4
+ export const favoritesCommand = cli({
5
+ site: 'notion',
6
+ name: 'favorites',
7
+ description: 'List pages from the Notion Favorites section in the sidebar',
8
+ domain: 'localhost',
9
+ strategy: Strategy.UI,
10
+ browser: true,
11
+ args: [],
12
+ columns: ['Index', 'Title', 'Icon'],
13
+ func: async (page: IPage) => {
14
+ const items = await page.evaluate(`
15
+ (function() {
16
+ const results = [];
17
+
18
+ // Strategy 1: Use Notion's own class 'notion-outliner-bookmarks-header-container'
19
+ const headerContainer = document.querySelector('.notion-outliner-bookmarks-header-container');
20
+ if (headerContainer) {
21
+ // Walk up to the section parent that wraps header + items
22
+ let section = headerContainer.parentElement;
23
+ if (section && section.children.length === 1) section = section.parentElement;
24
+
25
+ if (section) {
26
+ const treeItems = section.querySelectorAll('[role="treeitem"]');
27
+ treeItems.forEach((item) => {
28
+ // Title text is in a div.notranslate sibling of the icon area
29
+ const titleEl = item.querySelector('div.notranslate:not(.notion-record-icon)');
30
+ const title = titleEl
31
+ ? titleEl.textContent.trim()
32
+ : (item.textContent || '').trim().substring(0, 80);
33
+
34
+ // Icon/emoji is in the notion-record-icon element
35
+ const iconEl = item.querySelector('.notion-record-icon');
36
+ const icon = iconEl ? iconEl.textContent.trim().substring(0, 4) : '';
37
+
38
+ if (title && title.length > 0) {
39
+ results.push({ Index: results.length + 1, Title: title, Icon: icon || '📄' });
40
+ }
41
+ });
42
+ }
43
+ }
44
+
45
+ // Strategy 2: Fallback — find "Favorites" text node and walk DOM
46
+ if (results.length === 0) {
47
+ const walker = document.createTreeWalker(document.body, NodeFilter.SHOW_TEXT, null);
48
+ let node;
49
+ let favEl = null;
50
+ while (node = walker.nextNode()) {
51
+ const text = node.textContent.trim();
52
+ if (text === 'Favorites' || text === '收藏' || text === '收藏夹') {
53
+ favEl = node.parentElement;
54
+ break;
55
+ }
56
+ }
57
+
58
+ if (favEl) {
59
+ let section = favEl;
60
+ for (let i = 0; i < 6; i++) {
61
+ const p = section.parentElement;
62
+ if (!p || p === document.body) break;
63
+ const treeItems = p.querySelectorAll(':scope > [role="treeitem"]');
64
+ if (treeItems.length > 0) { section = p; break; }
65
+ section = p;
66
+ }
67
+
68
+ const treeItems = section.querySelectorAll('[role="treeitem"]');
69
+ treeItems.forEach((item) => {
70
+ const text = (item.textContent || '').trim().substring(0, 120);
71
+ if (text && text.length > 1 && !text.match(/^(Favorites|收藏夹?)$/)) {
72
+ results.push({ Index: results.length + 1, Title: text, Icon: '📄' });
73
+ }
74
+ });
75
+ }
76
+ }
77
+
78
+ return results;
79
+ })()
80
+ `);
81
+
82
+ if (items.length === 0) {
83
+ return [{ Index: 0, Title: 'No favorites found. Make sure sidebar is visible and you have favorites.', Icon: '⚠️' }];
84
+ }
85
+ return items;
86
+ },
87
+ });
@@ -0,0 +1,39 @@
1
+ import { cli, Strategy } from '../../registry.js';
2
+ import type { IPage } from '../../types.js';
3
+
4
+ export const newCommand = cli({
5
+ site: 'notion',
6
+ name: 'new',
7
+ description: 'Create a new page in Notion',
8
+ domain: 'localhost',
9
+ strategy: Strategy.UI,
10
+ browser: true,
11
+ args: [
12
+ { name: 'title', required: false, positional: true, help: 'Page title (optional)' },
13
+ ],
14
+ columns: ['Status'],
15
+ func: async (page: IPage, kwargs: any) => {
16
+ const title = kwargs.title as string | undefined;
17
+
18
+ // Cmd+N creates a new page in Notion
19
+ const isMac = process.platform === 'darwin';
20
+ await page.pressKey(isMac ? 'Meta+N' : 'Control+N');
21
+ await page.wait(1);
22
+
23
+ // If title is provided, type it into the title field
24
+ if (title) {
25
+ await page.evaluate(`
26
+ (function(t) {
27
+ const titleEl = document.querySelector('[placeholder="Untitled"], [data-content-editable-leaf] [placeholder]');
28
+ if (titleEl) {
29
+ titleEl.focus();
30
+ document.execCommand('insertText', false, t);
31
+ }
32
+ })(${JSON.stringify(title)})
33
+ `);
34
+ await page.wait(0.5);
35
+ }
36
+
37
+ return [{ Status: title ? `Created page: ${title}` : 'New blank page created' }];
38
+ },
39
+ });
@@ -0,0 +1,33 @@
1
+ import { cli, Strategy } from '../../registry.js';
2
+ import type { IPage } from '../../types.js';
3
+
4
+ export const readCommand = cli({
5
+ site: 'notion',
6
+ name: 'read',
7
+ description: 'Read the content of the currently open Notion page',
8
+ domain: 'localhost',
9
+ strategy: Strategy.UI,
10
+ browser: true,
11
+ args: [],
12
+ columns: ['Title', 'Content'],
13
+ func: async (page: IPage) => {
14
+ const result = await page.evaluate(`
15
+ (function() {
16
+ // Get the page title
17
+ const titleEl = document.querySelector('[data-block-id] [placeholder="Untitled"], .notion-page-block .notranslate, h1.notion-title, [class*="title"]');
18
+ const title = titleEl ? (titleEl.textContent || '').trim() : document.title;
19
+
20
+ // Get the page content — Notion renders blocks in a frame
21
+ const frame = document.querySelector('.notion-page-content, [class*="page-content"], .layout-content, main');
22
+ const content = frame ? (frame.innerText || frame.textContent || '').trim() : '';
23
+
24
+ return { title, content };
25
+ })()
26
+ `);
27
+
28
+ return [{
29
+ Title: result.title || 'Untitled',
30
+ Content: result.content || '(empty page)',
31
+ }];
32
+ },
33
+ });
@@ -0,0 +1,54 @@
1
+ import { cli, Strategy } from '../../registry.js';
2
+ import type { IPage } from '../../types.js';
3
+
4
+ export const searchCommand = cli({
5
+ site: 'notion',
6
+ name: 'search',
7
+ description: 'Search pages and databases in Notion via Quick Find (Cmd+P)',
8
+ domain: 'localhost',
9
+ strategy: Strategy.UI,
10
+ browser: true,
11
+ args: [{ name: 'query', required: true, positional: true, help: 'Search query' }],
12
+ columns: ['Index', 'Title'],
13
+ func: async (page: IPage, kwargs: any) => {
14
+ const query = kwargs.query as string;
15
+
16
+ // Open Quick Find
17
+ const isMac = process.platform === 'darwin';
18
+ await page.pressKey(isMac ? 'Meta+P' : 'Control+P');
19
+ await page.wait(0.5);
20
+
21
+ // Type the search query
22
+ await page.evaluate(`
23
+ (function(q) {
24
+ const input = document.querySelector('input[placeholder*="Search"], input[type="text"]');
25
+ if (input) {
26
+ const setter = Object.getOwnPropertyDescriptor(window.HTMLInputElement.prototype, 'value').set;
27
+ setter.call(input, q);
28
+ input.dispatchEvent(new Event('input', { bubbles: true }));
29
+ }
30
+ })(${JSON.stringify(query)})
31
+ `);
32
+
33
+ await page.wait(1.5);
34
+
35
+ // Scrape results
36
+ const results = await page.evaluate(`
37
+ (function() {
38
+ const items = document.querySelectorAll('[role="option"], [class*="searchResult"], [class*="quick-find"] [role="button"]');
39
+ return Array.from(items).slice(0, 20).map((item, i) => ({
40
+ Index: i + 1,
41
+ Title: (item.textContent || '').trim().substring(0, 120),
42
+ }));
43
+ })()
44
+ `);
45
+
46
+ // Close Quick Find
47
+ await page.pressKey('Escape');
48
+
49
+ if (results.length === 0) {
50
+ return [{ Index: 0, Title: `No results for "${query}"` }];
51
+ }
52
+ return results;
53
+ },
54
+ });
@@ -0,0 +1,44 @@
1
+ import { cli, Strategy } from '../../registry.js';
2
+ import type { IPage } from '../../types.js';
3
+
4
+ export const sidebarCommand = cli({
5
+ site: 'notion',
6
+ name: 'sidebar',
7
+ description: 'List pages and databases from the Notion sidebar',
8
+ domain: 'localhost',
9
+ strategy: Strategy.UI,
10
+ browser: true,
11
+ args: [],
12
+ columns: ['Index', 'Title'],
13
+ func: async (page: IPage) => {
14
+ const items = await page.evaluate(`
15
+ (function() {
16
+ const results = [];
17
+ // Notion sidebar items
18
+ const selectors = [
19
+ '[class*="sidebar"] [role="treeitem"]',
20
+ '[class*="sidebar"] a',
21
+ '.notion-sidebar [role="button"]',
22
+ 'nav [role="treeitem"]',
23
+ ];
24
+
25
+ for (const sel of selectors) {
26
+ const nodes = document.querySelectorAll(sel);
27
+ if (nodes.length > 0) {
28
+ nodes.forEach((n, i) => {
29
+ const text = (n.textContent || '').trim().substring(0, 100);
30
+ if (text && text.length > 1) results.push({ Index: i + 1, Title: text });
31
+ });
32
+ break;
33
+ }
34
+ }
35
+ return results;
36
+ })()
37
+ `);
38
+
39
+ if (items.length === 0) {
40
+ return [{ Index: 0, Title: 'No sidebar items found. Toggle the sidebar first.' }];
41
+ }
42
+ return items;
43
+ },
44
+ });
@@ -0,0 +1,18 @@
1
+ import { cli, Strategy } from '../../registry.js';
2
+ import type { IPage } from '../../types.js';
3
+
4
+ export const statusCommand = cli({
5
+ site: 'notion',
6
+ name: 'status',
7
+ description: 'Check active CDP connection to Notion Desktop',
8
+ domain: 'localhost',
9
+ strategy: Strategy.UI,
10
+ browser: true,
11
+ args: [],
12
+ columns: ['Status', 'Url', 'Title'],
13
+ func: async (page: IPage) => {
14
+ const url = await page.evaluate('window.location.href');
15
+ const title = await page.evaluate('document.title');
16
+ return [{ Status: 'Connected', Url: url, Title: title }];
17
+ },
18
+ });
@@ -0,0 +1,45 @@
1
+ import { cli, Strategy } from '../../registry.js';
2
+ import type { IPage } from '../../types.js';
3
+
4
+ export const writeCommand = cli({
5
+ site: 'notion',
6
+ name: 'write',
7
+ description: 'Append text content to the currently open Notion page',
8
+ domain: 'localhost',
9
+ strategy: Strategy.UI,
10
+ browser: true,
11
+ args: [{ name: 'text', required: true, positional: true, help: 'Text to append to the page' }],
12
+ columns: ['Status'],
13
+ func: async (page: IPage, kwargs: any) => {
14
+ const text = kwargs.text as string;
15
+
16
+ // Focus the page body and move to the end
17
+ await page.evaluate(`
18
+ (function(text) {
19
+ // Find the editable area in Notion
20
+ const editables = document.querySelectorAll('.notion-page-content [contenteditable="true"], [class*="page-content"] [contenteditable="true"]');
21
+ let target = editables.length > 0 ? editables[editables.length - 1] : null;
22
+
23
+ if (!target) {
24
+ // Fallback: just find any contenteditable
25
+ const all = document.querySelectorAll('[contenteditable="true"]');
26
+ target = all.length > 0 ? all[all.length - 1] : null;
27
+ }
28
+
29
+ if (!target) throw new Error('Could not find editable area in Notion page');
30
+
31
+ target.focus();
32
+ // Move to end
33
+ const sel = window.getSelection();
34
+ sel.selectAllChildren(target);
35
+ sel.collapseToEnd();
36
+
37
+ document.execCommand('insertText', false, text);
38
+ })(${JSON.stringify(text)})
39
+ `);
40
+
41
+ await page.wait(0.5);
42
+
43
+ return [{ Status: 'Text appended successfully' }];
44
+ },
45
+ });
@@ -0,0 +1,227 @@
1
+ /**
2
+ * Twitter/X download — download images and videos from tweets.
3
+ *
4
+ * Usage:
5
+ * opencli twitter download --username elonmusk --limit 10 --output ./twitter
6
+ * opencli twitter download --tweet-url https://x.com/xxx/status/123 --output ./twitter
7
+ */
8
+
9
+ import * as fs from 'node:fs';
10
+ import * as path from 'node:path';
11
+ import { cli, Strategy } from '../../registry.js';
12
+ import {
13
+ httpDownload,
14
+ ytdlpDownload,
15
+ checkYtdlp,
16
+ sanitizeFilename,
17
+ getTempDir,
18
+ exportCookiesToNetscape,
19
+ } from '../../download/index.js';
20
+ import { DownloadProgressTracker, formatBytes } from '../../download/progress.js';
21
+
22
+ cli({
23
+ site: 'twitter',
24
+ name: 'download',
25
+ description: '下载 Twitter/X 媒体(图片和视频)',
26
+ domain: 'x.com',
27
+ strategy: Strategy.COOKIE,
28
+ args: [
29
+ { name: 'username', help: 'Twitter username (downloads from media tab)' },
30
+ { name: 'tweet-url', help: 'Single tweet URL to download' },
31
+ { name: 'limit', type: 'int', default: 10, help: 'Number of tweets to scan' },
32
+ { name: 'output', default: './twitter-downloads', help: 'Output directory' },
33
+ ],
34
+ columns: ['index', 'type', 'status', 'size'],
35
+ func: async (page, kwargs) => {
36
+ const username = kwargs.username;
37
+ const tweetUrl = kwargs['tweet-url'];
38
+ const limit = kwargs.limit;
39
+ const output = kwargs.output;
40
+
41
+ if (!username && !tweetUrl) {
42
+ return [{
43
+ index: 0,
44
+ type: '-',
45
+ status: 'failed',
46
+ size: 'Must provide --username or --tweet-url',
47
+ }];
48
+ }
49
+
50
+ // Navigate to the appropriate page
51
+ if (tweetUrl) {
52
+ await page.goto(tweetUrl);
53
+ } else {
54
+ await page.goto(`https://x.com/${username}/media`);
55
+ }
56
+ await page.wait(3);
57
+
58
+ // Scroll to load more content
59
+ if (!tweetUrl) {
60
+ await page.autoScroll({ times: Math.ceil(limit / 5) });
61
+ }
62
+
63
+ // Extract media URLs
64
+ const data = await page.evaluate(`
65
+ (() => {
66
+ const media = [];
67
+
68
+ // Find images (high quality)
69
+ document.querySelectorAll('img[src*="pbs.twimg.com/media"]').forEach(img => {
70
+ let src = img.src || '';
71
+ // Get large version
72
+ src = src.replace(/&name=\\w+$/, '&name=large');
73
+ src = src.replace(/\\?format=/, '?format=');
74
+ if (!src.includes('&name=')) {
75
+ src = src + '&name=large';
76
+ }
77
+ media.push({ type: 'image', url: src });
78
+ });
79
+
80
+ // Find videos
81
+ document.querySelectorAll('video').forEach(video => {
82
+ const src = video.src || '';
83
+ if (src) {
84
+ media.push({ type: 'video', url: src, poster: video.poster || '' });
85
+ }
86
+ });
87
+
88
+ // Find video tweets (for yt-dlp)
89
+ document.querySelectorAll('[data-testid="videoPlayer"]').forEach(player => {
90
+ const tweetLink = player.closest('article')?.querySelector('a[href*="/status/"]');
91
+ const href = tweetLink?.getAttribute('href') || '';
92
+ if (href) {
93
+ const tweetUrl = 'https://x.com' + href;
94
+ media.push({ type: 'video-tweet', url: tweetUrl });
95
+ }
96
+ });
97
+
98
+ return media;
99
+ })()
100
+ `);
101
+
102
+ if (!data || data.length === 0) {
103
+ return [{
104
+ index: 0,
105
+ type: '-',
106
+ status: 'failed',
107
+ size: 'No media found',
108
+ }];
109
+ }
110
+
111
+ // Extract cookies
112
+ const cookieString = await page.evaluate(`(() => document.cookie)()`);
113
+
114
+ // Create output directory
115
+ const outputDir = tweetUrl
116
+ ? path.join(output, 'tweets')
117
+ : path.join(output, username || 'media');
118
+ fs.mkdirSync(outputDir, { recursive: true });
119
+
120
+ // Export cookies for yt-dlp
121
+ let cookiesFile: string | undefined;
122
+ if (typeof cookieString === 'string' && cookieString) {
123
+ const tempDir = getTempDir();
124
+ fs.mkdirSync(tempDir, { recursive: true });
125
+ 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
+ exportCookiesToNetscape(cookies, cookiesFile);
140
+ }
141
+
142
+ // Deduplicate media
143
+ const seen = new Set<string>();
144
+ const uniqueMedia = data.filter((m: any) => {
145
+ if (seen.has(m.url)) return false;
146
+ seen.add(m.url);
147
+ return true;
148
+ }).slice(0, limit);
149
+
150
+ const tracker = new DownloadProgressTracker(uniqueMedia.length, true);
151
+ const results: any[] = [];
152
+
153
+ for (let i = 0; i < uniqueMedia.length; i++) {
154
+ const media = uniqueMedia[i];
155
+ const ext = media.type === 'image' ? 'jpg' : 'mp4';
156
+ const filename = `${username || 'tweet'}_${i + 1}.${ext}`;
157
+ const destPath = path.join(outputDir, filename);
158
+
159
+ const progressBar = tracker.onFileStart(filename, i);
160
+
161
+ try {
162
+ let result: { success: boolean; size: number; error?: string };
163
+
164
+ if (media.type === 'video-tweet' && checkYtdlp()) {
165
+ // Use yt-dlp for video tweets
166
+ result = await ytdlpDownload(media.url, destPath, {
167
+ cookiesFile,
168
+ extraArgs: ['--merge-output-format', 'mp4'],
169
+ onProgress: (percent) => {
170
+ if (progressBar) progressBar.update(percent, 100);
171
+ },
172
+ });
173
+ } else if (media.type === 'image') {
174
+ // Direct HTTP download for images
175
+ result = await httpDownload(media.url, destPath, {
176
+ cookies: typeof cookieString === 'string' ? cookieString : '',
177
+ timeout: 30000,
178
+ onProgress: (received, total) => {
179
+ if (progressBar) progressBar.update(received, total);
180
+ },
181
+ });
182
+ } else {
183
+ // Direct HTTP download for direct video URLs
184
+ result = await httpDownload(media.url, destPath, {
185
+ cookies: typeof cookieString === 'string' ? cookieString : '',
186
+ timeout: 60000,
187
+ onProgress: (received, total) => {
188
+ if (progressBar) progressBar.update(received, total);
189
+ },
190
+ });
191
+ }
192
+
193
+ if (progressBar) {
194
+ progressBar.complete(result.success, result.success ? formatBytes(result.size) : undefined);
195
+ }
196
+
197
+ tracker.onFileComplete(result.success);
198
+
199
+ results.push({
200
+ index: i + 1,
201
+ type: media.type === 'video-tweet' ? 'video' : media.type,
202
+ status: result.success ? 'success' : 'failed',
203
+ size: result.success ? formatBytes(result.size) : (result.error || 'unknown error'),
204
+ });
205
+ } catch (err: any) {
206
+ if (progressBar) progressBar.fail(err.message);
207
+ tracker.onFileComplete(false);
208
+
209
+ results.push({
210
+ index: i + 1,
211
+ type: media.type,
212
+ status: 'failed',
213
+ size: err.message,
214
+ });
215
+ }
216
+ }
217
+
218
+ tracker.finish();
219
+
220
+ // Cleanup cookies file
221
+ if (cookiesFile && fs.existsSync(cookiesFile)) {
222
+ fs.unlinkSync(cookiesFile);
223
+ }
224
+
225
+ return results;
226
+ },
227
+ });
@@ -0,0 +1,28 @@
1
+ # WeChat (微信) Desktop Adapter
2
+
3
+ Control **WeChat Mac Desktop** from the terminal via AppleScript + Accessibility API.
4
+
5
+ > **Note:** WeChat is a native macOS app (not Electron), so CDP is not available. This adapter uses AppleScript keyboard simulation and clipboard operations.
6
+
7
+ ## Prerequisites
8
+
9
+ 1. WeChat must be running and logged in
10
+ 2. Terminal must have **Accessibility permission** (System Settings → Privacy & Security → Accessibility)
11
+
12
+ ## Commands
13
+
14
+ | Command | Description |
15
+ |---------|-------------|
16
+ | `wechat status` | Check if WeChat is running |
17
+ | `wechat send "msg"` | Send message in the active chat (clipboard paste + Enter) |
18
+ | `wechat read` | Read current chat content (Cmd+A → Cmd+C) |
19
+ | `wechat search "keyword"` | Open search and type a query (Cmd+F) |
20
+ | `wechat chats` | Switch to Chats tab (Cmd+1) |
21
+ | `wechat contacts` | Switch to Contacts tab (Cmd+2) |
22
+
23
+ ## Limitations
24
+
25
+ - **No CDP support** — WeChat is native Cocoa, not Electron
26
+ - `send` requires the correct conversation to be already open
27
+ - `read` captures whatever is visible via select-all + copy
28
+ - `search` types the query but cannot programmatically click results
@@ -0,0 +1,28 @@
1
+ # 微信桌面端适配器
2
+
3
+ 通过 AppleScript + Accessibility API 在终端中控制**微信 Mac 桌面端**。
4
+
5
+ > **注意:** 微信是原生 macOS 应用(非 Electron),因此无法使用 CDP。此适配器使用 AppleScript 键盘模拟和剪贴板操作。
6
+
7
+ ## 前置条件
8
+
9
+ 1. 微信必须正在运行且已登录
10
+ 2. Terminal 需要 **辅助功能权限**(系统设置 → 隐私与安全性 → 辅助功能)
11
+
12
+ ## 命令
13
+
14
+ | 命令 | 说明 |
15
+ |------|------|
16
+ | `wechat status` | 检查微信是否在运行 |
17
+ | `wechat send "消息"` | 发送消息(剪贴板粘贴 + 回车) |
18
+ | `wechat read` | 读取当前聊天内容(Cmd+A → Cmd+C) |
19
+ | `wechat search "关键词"` | 打开搜索并输入关键词(Cmd+F) |
20
+ | `wechat chats` | 切换到聊天列表(Cmd+1) |
21
+ | `wechat contacts` | 切换到通讯录(Cmd+2) |
22
+
23
+ ## 限制
24
+
25
+ - **不支持 CDP** — 微信是原生 Cocoa 应用
26
+ - `send` 需要先手动打开正确的会话
27
+ - `read` 通过全选+复制来获取可见内容
28
+ - `search` 可以输入关键词但无法自动点击结果
@@ -0,0 +1,33 @@
1
+ import { execSync } from 'node:child_process';
2
+ import { cli, Strategy } from '../../registry.js';
3
+ import type { IPage } from '../../types.js';
4
+
5
+ export const chatsCommand = cli({
6
+ site: 'wechat',
7
+ name: 'chats',
8
+ description: 'Open the WeChat chats panel (conversation list)',
9
+ domain: 'localhost',
10
+ strategy: Strategy.PUBLIC,
11
+ browser: false,
12
+ args: [],
13
+ columns: ['Status'],
14
+ func: async (page: IPage | null) => {
15
+ try {
16
+ // Activate WeChat
17
+ execSync("osascript -e 'tell application \"WeChat\" to activate'");
18
+ execSync("osascript -e 'delay 0.3'");
19
+
20
+ // Cmd+1 switches to Chats tab in WeChat Mac
21
+ execSync(
22
+ "osascript " +
23
+ "-e 'tell application \"System Events\"' " +
24
+ "-e 'keystroke \"1\" using command down' " +
25
+ "-e 'end tell'"
26
+ );
27
+
28
+ return [{ Status: 'Chats panel opened (Cmd+1)' }];
29
+ } catch (err: any) {
30
+ return [{ Status: 'Error: ' + err.message }];
31
+ }
32
+ },
33
+ });
@@ -0,0 +1,33 @@
1
+ import { execSync } from 'node:child_process';
2
+ import { cli, Strategy } from '../../registry.js';
3
+ import type { IPage } from '../../types.js';
4
+
5
+ export const contactsCommand = cli({
6
+ site: 'wechat',
7
+ name: 'contacts',
8
+ description: 'Open the WeChat contacts panel',
9
+ domain: 'localhost',
10
+ strategy: Strategy.PUBLIC,
11
+ browser: false,
12
+ args: [],
13
+ columns: ['Status'],
14
+ func: async (page: IPage | null) => {
15
+ try {
16
+ // Activate WeChat
17
+ execSync("osascript -e 'tell application \"WeChat\" to activate'");
18
+ execSync("osascript -e 'delay 0.3'");
19
+
20
+ // Cmd+2 switches to Contacts tab in WeChat Mac
21
+ execSync(
22
+ "osascript " +
23
+ "-e 'tell application \"System Events\"' " +
24
+ "-e 'keystroke \"2\" using command down' " +
25
+ "-e 'end tell'"
26
+ );
27
+
28
+ return [{ Status: 'Contacts panel opened (Cmd+2)' }];
29
+ } catch (err: any) {
30
+ return [{ Status: 'Error: ' + err.message }];
31
+ }
32
+ },
33
+ });