@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,47 @@
1
+ import { cli, Strategy } from '../../registry.js';
2
+ import type { IPage } from '../../types.js';
3
+
4
+ export const historyCommand = cli({
5
+ site: 'codex',
6
+ name: 'history',
7
+ description: 'List recent conversation threads in Codex',
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
+ // Codex thread list items
18
+ const entries = document.querySelectorAll('[data-testid*="thread"], [class*="thread-list"] a, [role="listbox"] [role="option"]');
19
+
20
+ entries.forEach((item, i) => {
21
+ const title = (item.textContent || item.innerText || '').trim().substring(0, 100);
22
+ if (title) results.push({ Index: i + 1, Title: title });
23
+ });
24
+
25
+ // Fallback: sidebar/nav links
26
+ if (results.length === 0) {
27
+ const nav = document.querySelector('nav, [role="navigation"], aside');
28
+ if (nav) {
29
+ const links = nav.querySelectorAll('a, button');
30
+ links.forEach((link, i) => {
31
+ const text = (link.textContent || '').trim().substring(0, 100);
32
+ if (text && text.length > 3) results.push({ Index: i + 1, Title: text });
33
+ });
34
+ }
35
+ }
36
+
37
+ return results;
38
+ })()
39
+ `);
40
+
41
+ if (items.length === 0) {
42
+ return [{ Index: 0, Title: 'No threads found. Try opening the thread list first.' }];
43
+ }
44
+
45
+ return items;
46
+ },
47
+ });
@@ -1,4 +1,5 @@
1
1
  import { cli, Strategy } from '../../registry.js';
2
+ import type { IPage } from '../../types.js';
2
3
 
3
4
  export const readCommand = cli({
4
5
  site: 'codex',
@@ -7,31 +8,29 @@ export const readCommand = cli({
7
8
  domain: 'localhost',
8
9
  strategy: Strategy.UI,
9
10
  browser: true,
10
- columns: ['Thread_Content'],
11
- func: async (page) => {
11
+ args: [],
12
+ columns: ['Content'],
13
+ func: async (page: IPage) => {
12
14
  const historyText = await page.evaluate(`
13
15
  (function() {
14
- // Precise Codex selector for chat messages
15
16
  const turns = Array.from(document.querySelectorAll('[data-content-search-turn-key]'));
16
17
  if (turns.length > 0) {
17
18
  return turns.map(t => t.innerText || t.textContent).join('\\n\\n---\\n\\n');
18
19
  }
19
20
 
20
- // Fallback robust scraping heuristic for chat history panes
21
21
  const threadContainer = document.querySelector('[role="log"], [data-testid="conversation"], .thread-container, .messages-list, main');
22
22
 
23
23
  if (threadContainer) {
24
24
  return threadContainer.innerText || threadContainer.textContent;
25
25
  }
26
26
 
27
- // If specific containers fail, just dump the whole body's readable text minus the navigation
28
27
  return document.body.innerText;
29
28
  })()
30
29
  `);
31
30
 
32
31
  return [
33
32
  {
34
- Thread_Content: historyText,
33
+ Content: historyText,
35
34
  },
36
35
  ];
37
36
  },
@@ -0,0 +1,33 @@
1
+ import * as fs from 'node:fs';
2
+ import { cli, Strategy } from '../../registry.js';
3
+ import type { IPage } from '../../types.js';
4
+
5
+ export const screenshotCommand = cli({
6
+ site: 'codex',
7
+ name: 'screenshot',
8
+ description: 'Capture a snapshot of the current Codex window (DOM + Accessibility tree)',
9
+ domain: 'localhost',
10
+ strategy: Strategy.UI,
11
+ browser: true,
12
+ args: [
13
+ { name: 'output', required: false, positional: true, help: 'Output file path (default: /tmp/codex-snapshot.txt)' },
14
+ ],
15
+ columns: ['Status', 'File'],
16
+ func: async (page: IPage, kwargs: any) => {
17
+ const outputPath = (kwargs.output as string) || '/tmp/codex-snapshot.txt';
18
+
19
+ const snap = await page.snapshot({ compact: true });
20
+ const html = await page.evaluate('document.documentElement.outerHTML');
21
+
22
+ const htmlPath = outputPath.replace(/\.\w+$/, '') + '-dom.html';
23
+ const snapPath = outputPath.replace(/\.\w+$/, '') + '-a11y.txt';
24
+
25
+ fs.writeFileSync(htmlPath, html);
26
+ fs.writeFileSync(snapPath, typeof snap === 'string' ? snap : JSON.stringify(snap, null, 2));
27
+
28
+ return [
29
+ { Status: 'Success', File: htmlPath },
30
+ { Status: 'Success', File: snapPath },
31
+ ];
32
+ },
33
+ });
@@ -1,4 +1,5 @@
1
1
  import { cli, Strategy } from '../../registry.js';
2
+ import type { IPage } from '../../types.js';
2
3
 
3
4
  export const sendCommand = cli({
4
5
  site: 'codex',
@@ -9,19 +10,16 @@ export const sendCommand = cli({
9
10
  browser: true,
10
11
  args: [{ name: 'text', required: true, positional: true, help: 'Text, command (e.g. /review), or skill (e.g. $imagegen)' }],
11
12
  columns: ['Status', 'InjectedText'],
12
- func: async (page, kwargs) => {
13
+ func: async (page: IPage, kwargs: any) => {
13
14
  const textToInsert = kwargs.text as string;
14
15
 
15
- // We use evaluate to inject text bypassing complex nested shadow roots or contenteditables
16
16
  await page.evaluate(`
17
17
  (function(text) {
18
- // Attempt 1: Look for standard textarea/composer input
19
18
  let composer = document.querySelector('textarea, [contenteditable="true"]');
20
19
 
21
- // Basic heuristic: prioritize elements that are deeply nested, visible, and have 'composer' or 'input' classes
22
20
  const editables = Array.from(document.querySelectorAll('[contenteditable="true"]'));
23
21
  if (editables.length > 0) {
24
- composer = editables[editables.length - 1]; // Often the active input is appended near the end
22
+ composer = editables[editables.length - 1];
25
23
  }
26
24
 
27
25
  if (!composer) {
@@ -29,12 +27,13 @@ export const sendCommand = cli({
29
27
  }
30
28
 
31
29
  composer.focus();
32
-
33
- // This handles Lexical/ProseMirror/Monaco rich-text editors effectively by mimicking human paste/type deeply.
34
30
  document.execCommand('insertText', false, text);
35
31
  })(${JSON.stringify(textToInsert)})
36
32
  `);
37
33
 
34
+ // Wait for the UI to register the input
35
+ await page.wait(0.5);
36
+
38
37
  // Simulate Enter key to submit
39
38
  await page.pressKey('Enter');
40
39
 
@@ -1,14 +1,16 @@
1
1
  import { cli, Strategy } from '../../registry.js';
2
+ import type { IPage } from '../../types.js';
2
3
 
3
4
  export const statusCommand = cli({
4
5
  site: 'codex',
5
6
  name: 'status',
6
7
  description: 'Check active CDP connection to OpenAI Codex App',
7
8
  domain: 'localhost',
8
- strategy: Strategy.UI, // Interactive UI manipulation
9
+ strategy: Strategy.UI,
9
10
  browser: true,
11
+ args: [],
10
12
  columns: ['Status', 'Url', 'Title'],
11
- func: async (page) => {
13
+ func: async (page: IPage) => {
12
14
  const url = await page.evaluate('window.location.href');
13
15
  const title = await page.evaluate('document.title');
14
16
 
@@ -0,0 +1,81 @@
1
+ import { cli, Strategy } from '../../registry.js';
2
+ import type { IPage } from '../../types.js';
3
+
4
+ export const askCommand = cli({
5
+ site: 'cursor',
6
+ name: 'ask',
7
+ description: 'Send a prompt and wait for the AI response (send + wait + read)',
8
+ domain: 'localhost',
9
+ strategy: Strategy.UI,
10
+ browser: true,
11
+ args: [
12
+ { name: 'text', required: true, positional: true, help: 'Prompt to send' },
13
+ { name: 'timeout', required: false, help: 'Max seconds to wait for response (default: 30)', default: '30' },
14
+ ],
15
+ columns: ['Role', 'Text'],
16
+ func: async (page: IPage, kwargs: any) => {
17
+ const text = kwargs.text as string;
18
+ const timeout = parseInt(kwargs.timeout as string, 10) || 30;
19
+
20
+ // Count existing messages before sending
21
+ const beforeCount = await page.evaluate(`
22
+ document.querySelectorAll('[data-message-role]').length
23
+ `);
24
+
25
+ // Inject text into the active editor and submit
26
+ const injected = await page.evaluate(
27
+ `(function(text) {
28
+ let editor = document.querySelector('.aislash-editor-input, [data-lexical-editor="true"], [contenteditable="true"]');
29
+ if (!editor) return false;
30
+ editor.focus();
31
+ document.execCommand('insertText', false, text);
32
+ return true;
33
+ })(${JSON.stringify(text)})`
34
+ );
35
+
36
+ if (!injected) throw new Error('Could not find input element.');
37
+ await page.wait(0.5);
38
+ await page.pressKey('Enter');
39
+
40
+ // Poll until a new assistant message appears or timeout
41
+ const pollInterval = 2; // seconds
42
+ const maxPolls = Math.ceil(timeout / pollInterval);
43
+ let response = '';
44
+
45
+ for (let i = 0; i < maxPolls; i++) {
46
+ await page.wait(pollInterval);
47
+
48
+ const result = await page.evaluate(`
49
+ (function(prevCount) {
50
+ const msgs = document.querySelectorAll('[data-message-role]');
51
+ if (msgs.length <= prevCount) return null;
52
+
53
+ const lastMsg = msgs[msgs.length - 1];
54
+ const role = lastMsg.getAttribute('data-message-role');
55
+ if (role === 'human') return null; // Still waiting for assistant
56
+
57
+ const root = lastMsg.querySelector('.markdown-root');
58
+ const text = root ? root.innerText : lastMsg.innerText;
59
+ return text ? text.trim() : null;
60
+ })(${beforeCount})
61
+ `);
62
+
63
+ if (result) {
64
+ response = result;
65
+ break;
66
+ }
67
+ }
68
+
69
+ if (!response) {
70
+ return [
71
+ { Role: 'User', Text: text },
72
+ { Role: 'System', Text: `No response received within ${timeout}s. The AI may still be generating.` },
73
+ ];
74
+ }
75
+
76
+ return [
77
+ { Role: 'User', Text: text },
78
+ { Role: 'Assistant', Text: response },
79
+ ];
80
+ },
81
+ });
@@ -13,37 +13,18 @@ export const composerCommand = cli({
13
13
  func: async (page: IPage, kwargs: any) => {
14
14
  const textToInsert = kwargs.text as string;
15
15
 
16
- const injected = await page.evaluate(
17
- `(async function() {
18
- let isComposerVisible = document.querySelector('.composer-bar') !== null || document.querySelector('#composer-toolbar-section') !== null;
19
- return isComposerVisible;
20
- })()`
21
- );
22
-
23
- if (!injected) {
24
- await page.pressKey('Meta+I');
25
- await page.wait(1.0);
26
- } else {
27
- // Just focus it if it's open but unfocused (we can't easily know if it's focused without triggering something)
28
- await page.pressKey('Meta+I');
29
- await page.wait(0.2);
30
- const isStillVisible = await page.evaluate('document.querySelector(".composer-bar") !== null');
31
- if (!isStillVisible) {
32
- await page.pressKey('Meta+I'); // Re-open
33
- await page.wait(0.5);
34
- }
35
- }
16
+ // Open/Focus Composer via shortcut — always works regardless of current state
17
+ await page.pressKey('Meta+I');
18
+ await page.wait(1);
36
19
 
37
20
  const typed = await page.evaluate(
38
21
  `(function(text) {
39
- let composer = document.querySelector('.composer-bar [data-lexical-editor="true"], [id*="composer"] [contenteditable="true"], .aislash-editor-input');
40
-
41
- if (!composer) {
42
- composer = document.activeElement;
43
- if (!composer || !composer.isContentEditable) {
44
- return false;
45
- }
22
+ let composer = document.activeElement;
23
+ if (!composer || !composer.isContentEditable) {
24
+ composer = document.querySelector('.composer-bar [data-lexical-editor="true"], [id*="composer"] [contenteditable="true"], .aislash-editor-input');
46
25
  }
26
+
27
+ if (!composer) return false;
47
28
 
48
29
  composer.focus();
49
30
  document.execCommand('insertText', false, text);
@@ -55,15 +36,13 @@ export const composerCommand = cli({
55
36
  throw new Error('Could not find Cursor Composer input element after pressing Cmd+I.');
56
37
  }
57
38
 
58
- // Submit the command. In Cursor Composer, Enter usually submits if it's not a multi-line edit.
59
- // Sometimes Cmd+Enter is needed? We'll just submit standard Enter.
60
39
  await page.wait(0.5);
61
40
  await page.pressKey('Enter');
62
41
  await page.wait(1);
63
42
 
64
43
  return [
65
44
  {
66
- Status: 'Success (Composer)',
45
+ Status: 'Success',
67
46
  InjectedText: textToInsert,
68
47
  },
69
48
  ];
@@ -0,0 +1,57 @@
1
+ import * as fs from 'node:fs';
2
+ import { cli, Strategy } from '../../registry.js';
3
+ import type { IPage } from '../../types.js';
4
+
5
+ function makeExportCommand(site: string, readSelector: string) {
6
+ return cli({
7
+ site,
8
+ name: 'export',
9
+ description: `Export the current ${site} conversation to a Markdown file`,
10
+ domain: 'localhost',
11
+ strategy: Strategy.UI,
12
+ browser: true,
13
+ args: [
14
+ { name: 'output', required: false, positional: true, help: `Output file (default: /tmp/${site}-export.md)` },
15
+ ],
16
+ columns: ['Status', 'File', 'Messages'],
17
+ func: async (page: IPage, kwargs: any) => {
18
+ const outputPath = (kwargs.output as string) || `/tmp/${site}-export.md`;
19
+
20
+ const md = await page.evaluate(`
21
+ (function() {
22
+ const selectors = ${JSON.stringify(readSelector)}.split(',');
23
+ let messages = [];
24
+
25
+ for (const sel of selectors) {
26
+ const nodes = document.querySelectorAll(sel.trim());
27
+ if (nodes.length > 0) {
28
+ messages = Array.from(nodes).map(n => n.innerText || n.textContent);
29
+ break;
30
+ }
31
+ }
32
+
33
+ if (messages.length === 0) {
34
+ const main = document.querySelector('main, [role="main"], .messages-list, [role="log"]');
35
+ if (main) messages = [main.innerText || main.textContent];
36
+ }
37
+
38
+ if (messages.length === 0) messages = [document.body.innerText];
39
+
40
+ return messages.map((m, i) => '## Message ' + (i + 1) + '\\n\\n' + m.trim()).join('\\n\\n---\\n\\n');
41
+ })()
42
+ `);
43
+
44
+ fs.writeFileSync(outputPath, `# ${site} Conversation Export\\n\\n` + md);
45
+
46
+ return [
47
+ {
48
+ Status: 'Success',
49
+ File: outputPath,
50
+ Messages: md.split('## Message').length - 1,
51
+ },
52
+ ];
53
+ },
54
+ });
55
+ }
56
+
57
+ export const cursorExport = makeExportCommand('cursor', '[data-message-role]');
@@ -0,0 +1,47 @@
1
+ import { cli, Strategy } from '../../registry.js';
2
+ import type { IPage } from '../../types.js';
3
+
4
+ export const historyCommand = cli({
5
+ site: 'cursor',
6
+ name: 'history',
7
+ description: 'List recent chat sessions from the Cursor 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
+ // Cursor chat history lives in sidebar items
18
+ const entries = document.querySelectorAll('.agent-sidebar-list-item, [data-testid="chat-history-item"], .chat-history-item, .tree-item');
19
+
20
+ entries.forEach((item, i) => {
21
+ const title = (item.textContent || item.innerText || '').trim().substring(0, 100);
22
+ if (title) results.push({ Index: i + 1, Title: title });
23
+ });
24
+
25
+ // Fallback: try to find sidebar text items
26
+ if (results.length === 0) {
27
+ const sidebar = document.querySelector('.sidebar, [class*="sidebar"], .agent-sidebar, .side-bar-container');
28
+ if (sidebar) {
29
+ const links = sidebar.querySelectorAll('a, [role="treeitem"], [role="option"]');
30
+ links.forEach((link, i) => {
31
+ const text = (link.textContent || '').trim().substring(0, 100);
32
+ if (text) results.push({ Index: i + 1, Title: text });
33
+ });
34
+ }
35
+ }
36
+
37
+ return results;
38
+ })()
39
+ `);
40
+
41
+ if (items.length === 0) {
42
+ return [{ Index: 0, Title: 'No chat history found. Open the AI sidebar first.' }];
43
+ }
44
+
45
+ return items;
46
+ },
47
+ });
@@ -8,23 +8,12 @@ export const newCommand = cli({
8
8
  domain: 'localhost',
9
9
  strategy: Strategy.UI,
10
10
  browser: true,
11
+ args: [],
11
12
  columns: ['Status'],
12
13
  func: async (page: IPage) => {
13
- const success = await page.evaluate(`
14
- (function() {
15
- const newChatButton = document.querySelector('[aria-label="New Chat"], [aria-label="New Chat (⌘N)"], .agent-sidebar-new-agent-button');
16
- if (newChatButton) {
17
- newChatButton.click();
18
- return true;
19
- }
20
- return false;
21
- })()
22
- `);
23
-
24
- if (!success) {
25
- throw new Error('Could not find New Chat button in Cursor DOM.');
26
- }
27
-
14
+ // Use keyboard shortcut — most robust approach, avoids brittle DOM selectors
15
+ const isMac = process.platform === 'darwin';
16
+ await page.pressKey(isMac ? 'Meta+N' : 'Control+N');
28
17
  await page.wait(1);
29
18
 
30
19
  return [{ Status: 'Success' }];
@@ -0,0 +1,38 @@
1
+ import * as fs from 'node:fs';
2
+ import { cli, Strategy } from '../../registry.js';
3
+ import type { IPage } from '../../types.js';
4
+
5
+ function makeScreenshotCommand(site: string) {
6
+ return cli({
7
+ site,
8
+ name: 'screenshot',
9
+ description: `Capture a snapshot of the current ${site} window (DOM + Accessibility tree)`,
10
+ domain: 'localhost',
11
+ strategy: Strategy.UI,
12
+ browser: true,
13
+ args: [
14
+ { name: 'output', required: false, positional: true, help: `Output file path (default: /tmp/${site}-snapshot.txt)` },
15
+ ],
16
+ columns: ['Status', 'File'],
17
+ func: async (page: IPage, kwargs: any) => {
18
+ const outputPath = (kwargs.output as string) || `/tmp/${site}-snapshot.txt`;
19
+
20
+ // Get both the accessibility snapshot and the raw DOM HTML
21
+ const snap = await page.snapshot({ compact: true });
22
+ const html = await page.evaluate('document.documentElement.outerHTML');
23
+
24
+ const htmlPath = outputPath.replace(/\.\w+$/, '') + '-dom.html';
25
+ const snapPath = outputPath.replace(/\.\w+$/, '') + '-a11y.txt';
26
+
27
+ fs.writeFileSync(htmlPath, html);
28
+ fs.writeFileSync(snapPath, typeof snap === 'string' ? snap : JSON.stringify(snap, null, 2));
29
+
30
+ return [
31
+ { Status: 'Success', File: htmlPath },
32
+ { Status: 'Success', File: snapPath },
33
+ ];
34
+ },
35
+ });
36
+ }
37
+
38
+ export const screenshotCursor = makeScreenshotCommand('cursor');
@@ -0,0 +1,28 @@
1
+ # Discord Desktop Adapter
2
+
3
+ Control the **Discord Desktop App** from the terminal via Chrome DevTools Protocol (CDP).
4
+
5
+ ## Prerequisites
6
+
7
+ Launch with remote debugging port:
8
+ ```bash
9
+ /Applications/Discord.app/Contents/MacOS/Discord --remote-debugging-port=9232
10
+ ```
11
+
12
+ ## Setup
13
+
14
+ ```bash
15
+ export OPENCLI_CDP_ENDPOINT="http://127.0.0.1:9232"
16
+ ```
17
+
18
+ ## Commands
19
+
20
+ | Command | Description |
21
+ |---------|-------------|
22
+ | `discord status` | Check CDP connection |
23
+ | `discord send "message"` | Send a message in the active channel |
24
+ | `discord read` | Read recent messages |
25
+ | `discord channels` | List channels in the current server |
26
+ | `discord servers` | List all joined servers |
27
+ | `discord search "query"` | Search messages (Cmd+F) |
28
+ | `discord members` | List online members |
@@ -0,0 +1,28 @@
1
+ # Discord 桌面端适配器
2
+
3
+ 通过 Chrome DevTools Protocol (CDP) 在终端中控制 **Discord 桌面应用**。
4
+
5
+ ## 前置条件
6
+
7
+ 通过远程调试端口启动:
8
+ ```bash
9
+ /Applications/Discord.app/Contents/MacOS/Discord --remote-debugging-port=9232
10
+ ```
11
+
12
+ ## 配置
13
+
14
+ ```bash
15
+ export OPENCLI_CDP_ENDPOINT="http://127.0.0.1:9232"
16
+ ```
17
+
18
+ ## 命令
19
+
20
+ | 命令 | 说明 |
21
+ |------|------|
22
+ | `discord status` | 检查 CDP 连接 |
23
+ | `discord send "消息"` | 在当前频道发送消息 |
24
+ | `discord read` | 读取最近消息 |
25
+ | `discord channels` | 列出当前服务器的频道 |
26
+ | `discord servers` | 列出已加入的服务器 |
27
+ | `discord search "关键词"` | 搜索消息(Cmd+F) |
28
+ | `discord members` | 列出在线成员 |
@@ -0,0 +1,48 @@
1
+ import { cli, Strategy } from '../../registry.js';
2
+ import type { IPage } from '../../types.js';
3
+
4
+ export const channelsCommand = cli({
5
+ site: 'discord-app',
6
+ name: 'channels',
7
+ description: 'List channels in the current Discord server',
8
+ domain: 'localhost',
9
+ strategy: Strategy.UI,
10
+ browser: true,
11
+ args: [],
12
+ columns: ['Index', 'Channel', 'Type'],
13
+ func: async (page: IPage) => {
14
+ const channels = await page.evaluate(`
15
+ (function() {
16
+ const results = [];
17
+ // Discord channel list items
18
+ const items = document.querySelectorAll('[data-list-item-id*="channels___"], [class*="containerDefault_"]');
19
+
20
+ items.forEach((item, i) => {
21
+ const nameEl = item.querySelector('[class*="name_"], [class*="channelName"]');
22
+ const name = nameEl ? nameEl.textContent.trim() : (item.textContent || '').trim().substring(0, 50);
23
+
24
+ if (!name || name.length < 1) return;
25
+
26
+ // Detect channel type from icon or aria-label
27
+ const iconEl = item.querySelector('[class*="icon"]');
28
+ let type = 'Text';
29
+ if (iconEl) {
30
+ const cls = iconEl.className || '';
31
+ if (cls.includes('voice') || cls.includes('speaker')) type = 'Voice';
32
+ else if (cls.includes('forum')) type = 'Forum';
33
+ else if (cls.includes('announcement')) type = 'Announcement';
34
+ }
35
+
36
+ results.push({ Index: i + 1, Channel: name, Type: type });
37
+ });
38
+
39
+ return results;
40
+ })()
41
+ `);
42
+
43
+ if (channels.length === 0) {
44
+ return [{ Index: 0, Channel: 'No channels found', Type: '—' }];
45
+ }
46
+ return channels;
47
+ },
48
+ });
@@ -0,0 +1,41 @@
1
+ import { cli, Strategy } from '../../registry.js';
2
+ import type { IPage } from '../../types.js';
3
+
4
+ export const membersCommand = cli({
5
+ site: 'discord-app',
6
+ name: 'members',
7
+ description: 'List online members in the current Discord channel',
8
+ domain: 'localhost',
9
+ strategy: Strategy.UI,
10
+ browser: true,
11
+ args: [],
12
+ columns: ['Index', 'Name', 'Status'],
13
+ func: async (page: IPage) => {
14
+ const members = await page.evaluate(`
15
+ (function() {
16
+ const results = [];
17
+ // Discord member list sidebar
18
+ const items = document.querySelectorAll('[class*="member_"], [data-list-item-id*="members"]');
19
+
20
+ items.forEach((item, i) => {
21
+ const nameEl = item.querySelector('[class*="username_"], [class*="nameTag"]');
22
+ const statusEl = item.querySelector('[class*="activity"], [class*="customStatus"]');
23
+
24
+ const name = nameEl ? nameEl.textContent.trim() : (item.textContent || '').trim().substring(0, 50);
25
+ const status = statusEl ? statusEl.textContent.trim() : '';
26
+
27
+ if (name && name.length > 0) {
28
+ results.push({ Index: i + 1, Name: name, Status: status || 'Online' });
29
+ }
30
+ });
31
+
32
+ return results.slice(0, 50); // Limit to 50
33
+ })()
34
+ `);
35
+
36
+ if (members.length === 0) {
37
+ return [{ Index: 0, Name: 'No members visible', Status: 'Toggle member list first' }];
38
+ }
39
+ return members;
40
+ },
41
+ });