@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,45 @@
1
+ import { cli, Strategy } from '../../registry.js';
2
+ export const sendCommand = cli({
3
+ site: 'chatwise',
4
+ name: 'send',
5
+ description: 'Send a message to the active ChatWise conversation',
6
+ domain: 'localhost',
7
+ strategy: Strategy.UI,
8
+ browser: true,
9
+ args: [{ name: 'text', required: true, positional: true, help: 'Message to send' }],
10
+ columns: ['Status', 'InjectedText'],
11
+ func: async (page, kwargs) => {
12
+ const text = kwargs.text;
13
+ await page.evaluate(`
14
+ (function(text) {
15
+ // ChatWise input can be textarea or contenteditable
16
+ let composer = document.querySelector('textarea');
17
+ if (!composer) {
18
+ const editables = Array.from(document.querySelectorAll('[contenteditable="true"]'));
19
+ composer = editables.length > 0 ? editables[editables.length - 1] : null;
20
+ }
21
+
22
+ if (!composer) throw new Error('Could not find ChatWise input element');
23
+
24
+ composer.focus();
25
+
26
+ if (composer.tagName === 'TEXTAREA') {
27
+ // For textarea, set value and dispatch input event
28
+ const nativeInputValueSetter = Object.getOwnPropertyDescriptor(window.HTMLTextAreaElement.prototype, 'value').set;
29
+ nativeInputValueSetter.call(composer, text);
30
+ composer.dispatchEvent(new Event('input', { bubbles: true }));
31
+ } else {
32
+ document.execCommand('insertText', false, text);
33
+ }
34
+ })(${JSON.stringify(text)})
35
+ `);
36
+ await page.wait(0.5);
37
+ await page.pressKey('Enter');
38
+ return [
39
+ {
40
+ Status: 'Success',
41
+ InjectedText: text,
42
+ },
43
+ ];
44
+ },
45
+ });
@@ -0,0 +1 @@
1
+ export declare const statusCommand: import("../../registry.js").CliCommand;
@@ -0,0 +1,22 @@
1
+ import { cli, Strategy } from '../../registry.js';
2
+ export const statusCommand = cli({
3
+ site: 'chatwise',
4
+ name: 'status',
5
+ description: 'Check active CDP connection to ChatWise Desktop',
6
+ domain: 'localhost',
7
+ strategy: Strategy.UI,
8
+ browser: true,
9
+ args: [],
10
+ columns: ['Status', 'Url', 'Title'],
11
+ func: async (page) => {
12
+ const url = await page.evaluate('window.location.href');
13
+ const title = await page.evaluate('document.title');
14
+ return [
15
+ {
16
+ Status: 'Connected',
17
+ Url: url,
18
+ Title: title,
19
+ },
20
+ ];
21
+ },
22
+ });
@@ -0,0 +1 @@
1
+ export declare const askCommand: import("../../registry.js").CliCommand;
@@ -0,0 +1,67 @@
1
+ import { cli, Strategy } from '../../registry.js';
2
+ export const askCommand = cli({
3
+ site: 'codex',
4
+ name: 'ask',
5
+ description: 'Send a prompt and wait for the AI response (send + wait + read)',
6
+ domain: 'localhost',
7
+ strategy: Strategy.UI,
8
+ browser: true,
9
+ args: [
10
+ { name: 'text', required: true, positional: true, help: 'Prompt to send' },
11
+ { name: 'timeout', required: false, help: 'Max seconds to wait for response (default: 60)', default: '60' },
12
+ ],
13
+ columns: ['Role', 'Text'],
14
+ func: async (page, kwargs) => {
15
+ const text = kwargs.text;
16
+ const timeout = parseInt(kwargs.timeout, 10) || 60;
17
+ // Snapshot the current content length before sending
18
+ const beforeLen = await page.evaluate(`
19
+ (function() {
20
+ const turns = document.querySelectorAll('[data-content-search-turn-key]');
21
+ return turns.length;
22
+ })()
23
+ `);
24
+ // Inject and send
25
+ await page.evaluate(`
26
+ (function(text) {
27
+ const editables = Array.from(document.querySelectorAll('[contenteditable="true"]'));
28
+ const composer = editables.length > 0 ? editables[editables.length - 1] : document.querySelector('textarea');
29
+ if (!composer) throw new Error('Could not find Codex input');
30
+ composer.focus();
31
+ document.execCommand('insertText', false, text);
32
+ })(${JSON.stringify(text)})
33
+ `);
34
+ await page.wait(0.5);
35
+ await page.pressKey('Enter');
36
+ // Poll for new content
37
+ const pollInterval = 3;
38
+ const maxPolls = Math.ceil(timeout / pollInterval);
39
+ let response = '';
40
+ for (let i = 0; i < maxPolls; i++) {
41
+ await page.wait(pollInterval);
42
+ const result = await page.evaluate(`
43
+ (function(prevLen) {
44
+ const turns = document.querySelectorAll('[data-content-search-turn-key]');
45
+ if (turns.length <= prevLen) return null;
46
+ const lastTurn = turns[turns.length - 1];
47
+ const text = lastTurn.innerText || lastTurn.textContent;
48
+ return text ? text.trim() : null;
49
+ })(${beforeLen})
50
+ `);
51
+ if (result) {
52
+ response = result;
53
+ break;
54
+ }
55
+ }
56
+ if (!response) {
57
+ return [
58
+ { Role: 'User', Text: text },
59
+ { Role: 'System', Text: `No response within ${timeout}s. The agent may still be working.` },
60
+ ];
61
+ }
62
+ return [
63
+ { Role: 'User', Text: text },
64
+ { Role: 'Assistant', Text: response },
65
+ ];
66
+ },
67
+ });
@@ -0,0 +1 @@
1
+ export declare const exportCommand: import("../../registry.js").CliCommand;
@@ -0,0 +1,37 @@
1
+ import * as fs from 'node:fs';
2
+ import { cli, Strategy } from '../../registry.js';
3
+ export const exportCommand = cli({
4
+ site: 'codex',
5
+ name: 'export',
6
+ description: 'Export the current Codex conversation to a Markdown file',
7
+ domain: 'localhost',
8
+ strategy: Strategy.UI,
9
+ browser: true,
10
+ args: [
11
+ { name: 'output', required: false, positional: true, help: 'Output file (default: /tmp/codex-export.md)' },
12
+ ],
13
+ columns: ['Status', 'File', 'Messages'],
14
+ func: async (page, kwargs) => {
15
+ const outputPath = kwargs.output || '/tmp/codex-export.md';
16
+ const md = await page.evaluate(`
17
+ (function() {
18
+ const turns = document.querySelectorAll('[data-content-search-turn-key]');
19
+ if (turns.length > 0) {
20
+ return Array.from(turns).map((t, i) => '## Turn ' + (i + 1) + '\\n\\n' + (t.innerText || t.textContent).trim()).join('\\n\\n---\\n\\n');
21
+ }
22
+
23
+ const main = document.querySelector('main, [role="main"], [role="log"]');
24
+ if (main) return main.innerText || main.textContent;
25
+ return document.body.innerText;
26
+ })()
27
+ `);
28
+ fs.writeFileSync(outputPath, '# Codex Conversation Export\\n\\n' + md);
29
+ return [
30
+ {
31
+ Status: 'Success',
32
+ File: outputPath,
33
+ Messages: md.split('## Turn').length - 1,
34
+ },
35
+ ];
36
+ },
37
+ });
@@ -0,0 +1 @@
1
+ export declare const historyCommand: import("../../registry.js").CliCommand;
@@ -0,0 +1,43 @@
1
+ import { cli, Strategy } from '../../registry.js';
2
+ export const historyCommand = cli({
3
+ site: 'codex',
4
+ name: 'history',
5
+ description: 'List recent conversation threads in Codex',
6
+ domain: 'localhost',
7
+ strategy: Strategy.UI,
8
+ browser: true,
9
+ args: [],
10
+ columns: ['Index', 'Title'],
11
+ func: async (page) => {
12
+ const items = await page.evaluate(`
13
+ (function() {
14
+ const results = [];
15
+ // Codex thread list items
16
+ const entries = document.querySelectorAll('[data-testid*="thread"], [class*="thread-list"] a, [role="listbox"] [role="option"]');
17
+
18
+ entries.forEach((item, i) => {
19
+ const title = (item.textContent || item.innerText || '').trim().substring(0, 100);
20
+ if (title) results.push({ Index: i + 1, Title: title });
21
+ });
22
+
23
+ // Fallback: sidebar/nav links
24
+ if (results.length === 0) {
25
+ const nav = document.querySelector('nav, [role="navigation"], aside');
26
+ if (nav) {
27
+ const links = nav.querySelectorAll('a, button');
28
+ links.forEach((link, i) => {
29
+ const text = (link.textContent || '').trim().substring(0, 100);
30
+ if (text && text.length > 3) results.push({ Index: i + 1, Title: text });
31
+ });
32
+ }
33
+ }
34
+
35
+ return results;
36
+ })()
37
+ `);
38
+ if (items.length === 0) {
39
+ return [{ Index: 0, Title: 'No threads found. Try opening the thread list first.' }];
40
+ }
41
+ return items;
42
+ },
43
+ });
@@ -6,30 +6,28 @@ export const readCommand = cli({
6
6
  domain: 'localhost',
7
7
  strategy: Strategy.UI,
8
8
  browser: true,
9
- columns: ['Thread_Content'],
9
+ args: [],
10
+ columns: ['Content'],
10
11
  func: async (page) => {
11
12
  const historyText = await page.evaluate(`
12
13
  (function() {
13
- // Precise Codex selector for chat messages
14
14
  const turns = Array.from(document.querySelectorAll('[data-content-search-turn-key]'));
15
15
  if (turns.length > 0) {
16
16
  return turns.map(t => t.innerText || t.textContent).join('\\n\\n---\\n\\n');
17
17
  }
18
18
 
19
- // Fallback robust scraping heuristic for chat history panes
20
19
  const threadContainer = document.querySelector('[role="log"], [data-testid="conversation"], .thread-container, .messages-list, main');
21
20
 
22
21
  if (threadContainer) {
23
22
  return threadContainer.innerText || threadContainer.textContent;
24
23
  }
25
24
 
26
- // If specific containers fail, just dump the whole body's readable text minus the navigation
27
25
  return document.body.innerText;
28
26
  })()
29
27
  `);
30
28
  return [
31
29
  {
32
- Thread_Content: historyText,
30
+ Content: historyText,
33
31
  },
34
32
  ];
35
33
  },
@@ -0,0 +1 @@
1
+ export declare const screenshotCommand: import("../../registry.js").CliCommand;
@@ -0,0 +1,27 @@
1
+ import * as fs from 'node:fs';
2
+ import { cli, Strategy } from '../../registry.js';
3
+ export const screenshotCommand = cli({
4
+ site: 'codex',
5
+ name: 'screenshot',
6
+ description: 'Capture a snapshot of the current Codex window (DOM + Accessibility tree)',
7
+ domain: 'localhost',
8
+ strategy: Strategy.UI,
9
+ browser: true,
10
+ args: [
11
+ { name: 'output', required: false, positional: true, help: 'Output file path (default: /tmp/codex-snapshot.txt)' },
12
+ ],
13
+ columns: ['Status', 'File'],
14
+ func: async (page, kwargs) => {
15
+ const outputPath = kwargs.output || '/tmp/codex-snapshot.txt';
16
+ const snap = await page.snapshot({ compact: true });
17
+ const html = await page.evaluate('document.documentElement.outerHTML');
18
+ const htmlPath = outputPath.replace(/\.\w+$/, '') + '-dom.html';
19
+ const snapPath = outputPath.replace(/\.\w+$/, '') + '-a11y.txt';
20
+ fs.writeFileSync(htmlPath, html);
21
+ fs.writeFileSync(snapPath, typeof snap === 'string' ? snap : JSON.stringify(snap, null, 2));
22
+ return [
23
+ { Status: 'Success', File: htmlPath },
24
+ { Status: 'Success', File: snapPath },
25
+ ];
26
+ },
27
+ });
@@ -10,16 +10,13 @@ export const sendCommand = cli({
10
10
  columns: ['Status', 'InjectedText'],
11
11
  func: async (page, kwargs) => {
12
12
  const textToInsert = kwargs.text;
13
- // We use evaluate to inject text bypassing complex nested shadow roots or contenteditables
14
13
  await page.evaluate(`
15
14
  (function(text) {
16
- // Attempt 1: Look for standard textarea/composer input
17
15
  let composer = document.querySelector('textarea, [contenteditable="true"]');
18
16
 
19
- // Basic heuristic: prioritize elements that are deeply nested, visible, and have 'composer' or 'input' classes
20
17
  const editables = Array.from(document.querySelectorAll('[contenteditable="true"]'));
21
18
  if (editables.length > 0) {
22
- composer = editables[editables.length - 1]; // Often the active input is appended near the end
19
+ composer = editables[editables.length - 1];
23
20
  }
24
21
 
25
22
  if (!composer) {
@@ -27,11 +24,11 @@ export const sendCommand = cli({
27
24
  }
28
25
 
29
26
  composer.focus();
30
-
31
- // This handles Lexical/ProseMirror/Monaco rich-text editors effectively by mimicking human paste/type deeply.
32
27
  document.execCommand('insertText', false, text);
33
28
  })(${JSON.stringify(textToInsert)})
34
29
  `);
30
+ // Wait for the UI to register the input
31
+ await page.wait(0.5);
35
32
  // Simulate Enter key to submit
36
33
  await page.pressKey('Enter');
37
34
  return [
@@ -4,8 +4,9 @@ export const statusCommand = cli({
4
4
  name: 'status',
5
5
  description: 'Check active CDP connection to OpenAI Codex App',
6
6
  domain: 'localhost',
7
- strategy: Strategy.UI, // Interactive UI manipulation
7
+ strategy: Strategy.UI,
8
8
  browser: true,
9
+ args: [],
9
10
  columns: ['Status', 'Url', 'Title'],
10
11
  func: async (page) => {
11
12
  const url = await page.evaluate('window.location.href');
@@ -0,0 +1 @@
1
+ export declare const askCommand: import("../../registry.js").CliCommand;
@@ -0,0 +1,69 @@
1
+ import { cli, Strategy } from '../../registry.js';
2
+ export const askCommand = cli({
3
+ site: 'cursor',
4
+ name: 'ask',
5
+ description: 'Send a prompt and wait for the AI response (send + wait + read)',
6
+ domain: 'localhost',
7
+ strategy: Strategy.UI,
8
+ browser: true,
9
+ args: [
10
+ { name: 'text', required: true, positional: true, help: 'Prompt to send' },
11
+ { name: 'timeout', required: false, help: 'Max seconds to wait for response (default: 30)', default: '30' },
12
+ ],
13
+ columns: ['Role', 'Text'],
14
+ func: async (page, kwargs) => {
15
+ const text = kwargs.text;
16
+ const timeout = parseInt(kwargs.timeout, 10) || 30;
17
+ // Count existing messages before sending
18
+ const beforeCount = await page.evaluate(`
19
+ document.querySelectorAll('[data-message-role]').length
20
+ `);
21
+ // Inject text into the active editor and submit
22
+ const injected = await page.evaluate(`(function(text) {
23
+ let editor = document.querySelector('.aislash-editor-input, [data-lexical-editor="true"], [contenteditable="true"]');
24
+ if (!editor) return false;
25
+ editor.focus();
26
+ document.execCommand('insertText', false, text);
27
+ return true;
28
+ })(${JSON.stringify(text)})`);
29
+ if (!injected)
30
+ throw new Error('Could not find input element.');
31
+ await page.wait(0.5);
32
+ await page.pressKey('Enter');
33
+ // Poll until a new assistant message appears or timeout
34
+ const pollInterval = 2; // seconds
35
+ const maxPolls = Math.ceil(timeout / pollInterval);
36
+ let response = '';
37
+ for (let i = 0; i < maxPolls; i++) {
38
+ await page.wait(pollInterval);
39
+ const result = await page.evaluate(`
40
+ (function(prevCount) {
41
+ const msgs = document.querySelectorAll('[data-message-role]');
42
+ if (msgs.length <= prevCount) return null;
43
+
44
+ const lastMsg = msgs[msgs.length - 1];
45
+ const role = lastMsg.getAttribute('data-message-role');
46
+ if (role === 'human') return null; // Still waiting for assistant
47
+
48
+ const root = lastMsg.querySelector('.markdown-root');
49
+ const text = root ? root.innerText : lastMsg.innerText;
50
+ return text ? text.trim() : null;
51
+ })(${beforeCount})
52
+ `);
53
+ if (result) {
54
+ response = result;
55
+ break;
56
+ }
57
+ }
58
+ if (!response) {
59
+ return [
60
+ { Role: 'User', Text: text },
61
+ { Role: 'System', Text: `No response received within ${timeout}s. The AI may still be generating.` },
62
+ ];
63
+ }
64
+ return [
65
+ { Role: 'User', Text: text },
66
+ { Role: 'Assistant', Text: response },
67
+ ];
68
+ },
69
+ });
@@ -10,33 +10,16 @@ export const composerCommand = cli({
10
10
  columns: ['Status', 'InjectedText'],
11
11
  func: async (page, kwargs) => {
12
12
  const textToInsert = kwargs.text;
13
- const injected = await page.evaluate(`(async function() {
14
- let isComposerVisible = document.querySelector('.composer-bar') !== null || document.querySelector('#composer-toolbar-section') !== null;
15
- return isComposerVisible;
16
- })()`);
17
- if (!injected) {
18
- await page.pressKey('Meta+I');
19
- await page.wait(1.0);
20
- }
21
- else {
22
- // Just focus it if it's open but unfocused (we can't easily know if it's focused without triggering something)
23
- await page.pressKey('Meta+I');
24
- await page.wait(0.2);
25
- const isStillVisible = await page.evaluate('document.querySelector(".composer-bar") !== null');
26
- if (!isStillVisible) {
27
- await page.pressKey('Meta+I'); // Re-open
28
- await page.wait(0.5);
29
- }
30
- }
13
+ // Open/Focus Composer via shortcut always works regardless of current state
14
+ await page.pressKey('Meta+I');
15
+ await page.wait(1);
31
16
  const typed = await page.evaluate(`(function(text) {
32
- let composer = document.querySelector('.composer-bar [data-lexical-editor="true"], [id*="composer"] [contenteditable="true"], .aislash-editor-input');
33
-
34
- if (!composer) {
35
- composer = document.activeElement;
36
- if (!composer || !composer.isContentEditable) {
37
- return false;
38
- }
17
+ let composer = document.activeElement;
18
+ if (!composer || !composer.isContentEditable) {
19
+ composer = document.querySelector('.composer-bar [data-lexical-editor="true"], [id*="composer"] [contenteditable="true"], .aislash-editor-input');
39
20
  }
21
+
22
+ if (!composer) return false;
40
23
 
41
24
  composer.focus();
42
25
  document.execCommand('insertText', false, text);
@@ -45,14 +28,12 @@ export const composerCommand = cli({
45
28
  if (!typed) {
46
29
  throw new Error('Could not find Cursor Composer input element after pressing Cmd+I.');
47
30
  }
48
- // Submit the command. In Cursor Composer, Enter usually submits if it's not a multi-line edit.
49
- // Sometimes Cmd+Enter is needed? We'll just submit standard Enter.
50
31
  await page.wait(0.5);
51
32
  await page.pressKey('Enter');
52
33
  await page.wait(1);
53
34
  return [
54
35
  {
55
- Status: 'Success (Composer)',
36
+ Status: 'Success',
56
37
  InjectedText: textToInsert,
57
38
  },
58
39
  ];
@@ -0,0 +1 @@
1
+ export declare const cursorExport: import("../../registry.js").CliCommand;
@@ -0,0 +1,51 @@
1
+ import * as fs from 'node:fs';
2
+ import { cli, Strategy } from '../../registry.js';
3
+ function makeExportCommand(site, readSelector) {
4
+ return cli({
5
+ site,
6
+ name: 'export',
7
+ description: `Export the current ${site} conversation to a Markdown file`,
8
+ domain: 'localhost',
9
+ strategy: Strategy.UI,
10
+ browser: true,
11
+ args: [
12
+ { name: 'output', required: false, positional: true, help: `Output file (default: /tmp/${site}-export.md)` },
13
+ ],
14
+ columns: ['Status', 'File', 'Messages'],
15
+ func: async (page, kwargs) => {
16
+ const outputPath = kwargs.output || `/tmp/${site}-export.md`;
17
+ const md = await page.evaluate(`
18
+ (function() {
19
+ const selectors = ${JSON.stringify(readSelector)}.split(',');
20
+ let messages = [];
21
+
22
+ for (const sel of selectors) {
23
+ const nodes = document.querySelectorAll(sel.trim());
24
+ if (nodes.length > 0) {
25
+ messages = Array.from(nodes).map(n => n.innerText || n.textContent);
26
+ break;
27
+ }
28
+ }
29
+
30
+ if (messages.length === 0) {
31
+ const main = document.querySelector('main, [role="main"], .messages-list, [role="log"]');
32
+ if (main) messages = [main.innerText || main.textContent];
33
+ }
34
+
35
+ if (messages.length === 0) messages = [document.body.innerText];
36
+
37
+ return messages.map((m, i) => '## Message ' + (i + 1) + '\\n\\n' + m.trim()).join('\\n\\n---\\n\\n');
38
+ })()
39
+ `);
40
+ fs.writeFileSync(outputPath, `# ${site} Conversation Export\\n\\n` + md);
41
+ return [
42
+ {
43
+ Status: 'Success',
44
+ File: outputPath,
45
+ Messages: md.split('## Message').length - 1,
46
+ },
47
+ ];
48
+ },
49
+ });
50
+ }
51
+ export const cursorExport = makeExportCommand('cursor', '[data-message-role]');
@@ -0,0 +1 @@
1
+ export declare const historyCommand: import("../../registry.js").CliCommand;
@@ -0,0 +1,43 @@
1
+ import { cli, Strategy } from '../../registry.js';
2
+ export const historyCommand = cli({
3
+ site: 'cursor',
4
+ name: 'history',
5
+ description: 'List recent chat sessions from the Cursor sidebar',
6
+ domain: 'localhost',
7
+ strategy: Strategy.UI,
8
+ browser: true,
9
+ args: [],
10
+ columns: ['Index', 'Title'],
11
+ func: async (page) => {
12
+ const items = await page.evaluate(`
13
+ (function() {
14
+ const results = [];
15
+ // Cursor chat history lives in sidebar items
16
+ const entries = document.querySelectorAll('.agent-sidebar-list-item, [data-testid="chat-history-item"], .chat-history-item, .tree-item');
17
+
18
+ entries.forEach((item, i) => {
19
+ const title = (item.textContent || item.innerText || '').trim().substring(0, 100);
20
+ if (title) results.push({ Index: i + 1, Title: title });
21
+ });
22
+
23
+ // Fallback: try to find sidebar text items
24
+ if (results.length === 0) {
25
+ const sidebar = document.querySelector('.sidebar, [class*="sidebar"], .agent-sidebar, .side-bar-container');
26
+ if (sidebar) {
27
+ const links = sidebar.querySelectorAll('a, [role="treeitem"], [role="option"]');
28
+ links.forEach((link, i) => {
29
+ const text = (link.textContent || '').trim().substring(0, 100);
30
+ if (text) results.push({ Index: i + 1, Title: text });
31
+ });
32
+ }
33
+ }
34
+
35
+ return results;
36
+ })()
37
+ `);
38
+ if (items.length === 0) {
39
+ return [{ Index: 0, Title: 'No chat history found. Open the AI sidebar first.' }];
40
+ }
41
+ return items;
42
+ },
43
+ });
@@ -6,21 +6,12 @@ export const newCommand = cli({
6
6
  domain: 'localhost',
7
7
  strategy: Strategy.UI,
8
8
  browser: true,
9
+ args: [],
9
10
  columns: ['Status'],
10
11
  func: async (page) => {
11
- const success = await page.evaluate(`
12
- (function() {
13
- const newChatButton = document.querySelector('[aria-label="New Chat"], [aria-label="New Chat (⌘N)"], .agent-sidebar-new-agent-button');
14
- if (newChatButton) {
15
- newChatButton.click();
16
- return true;
17
- }
18
- return false;
19
- })()
20
- `);
21
- if (!success) {
22
- throw new Error('Could not find New Chat button in Cursor DOM.');
23
- }
12
+ // Use keyboard shortcut — most robust approach, avoids brittle DOM selectors
13
+ const isMac = process.platform === 'darwin';
14
+ await page.pressKey(isMac ? 'Meta+N' : 'Control+N');
24
15
  await page.wait(1);
25
16
  return [{ Status: 'Success' }];
26
17
  },
@@ -0,0 +1 @@
1
+ export declare const screenshotCursor: import("../../registry.js").CliCommand;
@@ -0,0 +1,31 @@
1
+ import * as fs from 'node:fs';
2
+ import { cli, Strategy } from '../../registry.js';
3
+ function makeScreenshotCommand(site) {
4
+ return cli({
5
+ site,
6
+ name: 'screenshot',
7
+ description: `Capture a snapshot of the current ${site} window (DOM + Accessibility tree)`,
8
+ domain: 'localhost',
9
+ strategy: Strategy.UI,
10
+ browser: true,
11
+ args: [
12
+ { name: 'output', required: false, positional: true, help: `Output file path (default: /tmp/${site}-snapshot.txt)` },
13
+ ],
14
+ columns: ['Status', 'File'],
15
+ func: async (page, kwargs) => {
16
+ const outputPath = kwargs.output || `/tmp/${site}-snapshot.txt`;
17
+ // Get both the accessibility snapshot and the raw DOM HTML
18
+ const snap = await page.snapshot({ compact: true });
19
+ const html = await page.evaluate('document.documentElement.outerHTML');
20
+ const htmlPath = outputPath.replace(/\.\w+$/, '') + '-dom.html';
21
+ const snapPath = outputPath.replace(/\.\w+$/, '') + '-a11y.txt';
22
+ fs.writeFileSync(htmlPath, html);
23
+ fs.writeFileSync(snapPath, typeof snap === 'string' ? snap : JSON.stringify(snap, null, 2));
24
+ return [
25
+ { Status: 'Success', File: htmlPath },
26
+ { Status: 'Success', File: snapPath },
27
+ ];
28
+ },
29
+ });
30
+ }
31
+ export const screenshotCursor = makeScreenshotCommand('cursor');
@@ -0,0 +1 @@
1
+ export declare const channelsCommand: import("../../registry.js").CliCommand;