@jackwener/opencli 1.7.8 → 1.7.10

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 (281) hide show
  1. package/README.md +49 -14
  2. package/README.zh-CN.md +30 -10
  3. package/cli-manifest.json +646 -30
  4. package/clis/36kr/news.js +1 -1
  5. package/clis/apple-podcasts/commands.test.js +4 -4
  6. package/clis/apple-podcasts/episodes.js +1 -1
  7. package/clis/apple-podcasts/search.js +1 -1
  8. package/clis/apple-podcasts/top.js +1 -1
  9. package/clis/arxiv/paper.js +1 -1
  10. package/clis/arxiv/search.js +1 -1
  11. package/clis/band/mentions.js +3 -3
  12. package/clis/bbc/news.js +1 -1
  13. package/clis/bilibili/subtitle.js +2 -2
  14. package/clis/bloomberg/businessweek.js +1 -1
  15. package/clis/bloomberg/economics.js +1 -1
  16. package/clis/bloomberg/industries.js +1 -1
  17. package/clis/bloomberg/main.js +1 -1
  18. package/clis/bloomberg/markets.js +1 -1
  19. package/clis/bloomberg/opinions.js +1 -1
  20. package/clis/bloomberg/politics.js +1 -1
  21. package/clis/bloomberg/tech.js +1 -1
  22. package/clis/boss/search.js +49 -8
  23. package/clis/boss/search.test.js +78 -0
  24. package/clis/boss/send.js +3 -3
  25. package/clis/chatgpt/image.js +37 -8
  26. package/clis/chatgpt/image.test.js +92 -0
  27. package/clis/chatgpt/utils.js +39 -6
  28. package/clis/chatgpt/utils.test.js +63 -0
  29. package/clis/chatgpt-app/ask.js +1 -1
  30. package/clis/chatgpt-app/ax.js +4 -2
  31. package/clis/chatgpt-app/ax.test.js +12 -0
  32. package/clis/chatgpt-app/model.js +1 -1
  33. package/clis/chatgpt-app/new.js +1 -1
  34. package/clis/chatgpt-app/read.js +1 -1
  35. package/clis/chatgpt-app/send.js +1 -1
  36. package/clis/chatgpt-app/status.js +1 -1
  37. package/clis/chatwise/ask.js +2 -2
  38. package/clis/chatwise/model.js +2 -2
  39. package/clis/chatwise/send.js +2 -2
  40. package/clis/claude/ask.js +128 -0
  41. package/clis/claude/ask.test.js +338 -0
  42. package/clis/claude/commands.test.js +118 -0
  43. package/clis/claude/detail.js +29 -0
  44. package/clis/claude/history.js +31 -0
  45. package/clis/claude/new.js +21 -0
  46. package/clis/claude/read.js +24 -0
  47. package/clis/claude/send.js +41 -0
  48. package/clis/claude/status.js +24 -0
  49. package/clis/claude/utils.js +440 -0
  50. package/clis/claude/utils.test.js +148 -0
  51. package/clis/codex/ask.js +2 -2
  52. package/clis/codex/send.js +2 -2
  53. package/clis/ctrip/search.js +1 -1
  54. package/clis/ctrip/search.test.js +4 -4
  55. package/clis/cursor/ask.js +2 -2
  56. package/clis/cursor/composer.js +2 -2
  57. package/clis/cursor/send.js +2 -2
  58. package/clis/deepseek/ask.js +17 -4
  59. package/clis/deepseek/ask.test.js +46 -0
  60. package/clis/deepseek/utils.js +55 -16
  61. package/clis/deepseek/utils.test.js +124 -5
  62. package/clis/doubao/utils.js +53 -11
  63. package/clis/doubao/utils.test.js +22 -2
  64. package/clis/eastmoney/announcement.js +1 -1
  65. package/clis/eastmoney/convertible.js +1 -1
  66. package/clis/eastmoney/etf.js +1 -1
  67. package/clis/eastmoney/holders.js +1 -1
  68. package/clis/eastmoney/index-board.js +1 -1
  69. package/clis/eastmoney/kline.js +1 -1
  70. package/clis/eastmoney/kuaixun.js +1 -1
  71. package/clis/eastmoney/longhu.js +1 -1
  72. package/clis/eastmoney/money-flow.js +1 -1
  73. package/clis/eastmoney/northbound.js +1 -1
  74. package/clis/eastmoney/quote.js +1 -1
  75. package/clis/eastmoney/rank.js +1 -1
  76. package/clis/eastmoney/sectors.js +1 -1
  77. package/clis/facebook/marketplace-inbox.js +83 -0
  78. package/clis/facebook/marketplace-listings.js +83 -0
  79. package/clis/facebook/marketplace.test.js +91 -0
  80. package/clis/google/news.js +1 -1
  81. package/clis/google/suggest.js +1 -1
  82. package/clis/google/trends.js +1 -1
  83. package/clis/google-scholar/cite.js +74 -0
  84. package/clis/google-scholar/cite.test.js +47 -0
  85. package/clis/google-scholar/profile.js +92 -0
  86. package/clis/google-scholar/profile.test.js +49 -0
  87. package/clis/google-scholar/search.js +1 -1
  88. package/clis/google-scholar/search.test.js +15 -0
  89. package/clis/hf/top.js +1 -1
  90. package/clis/instagram/collection-create.js +57 -0
  91. package/clis/instagram/saved.js +21 -7
  92. package/clis/jd/item.js +679 -47
  93. package/clis/jd/item.test.js +318 -7
  94. package/clis/jd/item.test.ts +517 -0
  95. package/clis/lesswrong/comments.js +1 -1
  96. package/clis/lesswrong/curated.js +1 -1
  97. package/clis/lesswrong/frontpage.js +1 -1
  98. package/clis/lesswrong/new.js +1 -1
  99. package/clis/lesswrong/read.js +1 -1
  100. package/clis/lesswrong/sequences.js +1 -1
  101. package/clis/lesswrong/shortform.js +1 -1
  102. package/clis/lesswrong/tag.js +1 -1
  103. package/clis/lesswrong/tags.js +1 -1
  104. package/clis/lesswrong/top-month.js +1 -1
  105. package/clis/lesswrong/top-week.js +1 -1
  106. package/clis/lesswrong/top-year.js +1 -1
  107. package/clis/lesswrong/top.js +1 -1
  108. package/clis/lesswrong/user-posts.js +1 -1
  109. package/clis/lesswrong/user.js +1 -1
  110. package/clis/paperreview/commands.test.js +6 -6
  111. package/clis/paperreview/feedback.js +1 -1
  112. package/clis/paperreview/review.js +1 -1
  113. package/clis/paperreview/submit.js +1 -1
  114. package/clis/producthunt/posts.js +1 -1
  115. package/clis/producthunt/today.js +1 -1
  116. package/clis/sinablog/search.js +1 -1
  117. package/clis/sinafinance/news.js +1 -1
  118. package/clis/sinafinance/stock.js +1 -1
  119. package/clis/sinafinance/stock.test.js +2 -2
  120. package/clis/spotify/spotify.js +6 -6
  121. package/clis/substack/search.js +1 -1
  122. package/clis/toutiao/articles.js +5 -6
  123. package/clis/toutiao/articles.test.js +22 -15
  124. package/clis/twitter/followers.js +2 -2
  125. package/clis/twitter/following.js +224 -73
  126. package/clis/twitter/following.test.js +277 -0
  127. package/clis/twitter/post.js +184 -47
  128. package/clis/twitter/post.test.js +114 -34
  129. package/clis/uiverse/_shared.js +63 -4
  130. package/clis/uiverse/_shared.test.js +7 -0
  131. package/clis/uiverse/code.js +1 -0
  132. package/clis/uiverse/navigation.test.js +12 -0
  133. package/clis/uiverse/preview.js +1 -0
  134. package/clis/web/read.js +319 -81
  135. package/clis/web/read.test.js +221 -5
  136. package/clis/weibo/favorites.js +169 -0
  137. package/clis/weibo/favorites.test.js +114 -0
  138. package/clis/weibo/publish.js +282 -0
  139. package/clis/weibo/publish.test.js +183 -0
  140. package/clis/weread/ranking.js +1 -1
  141. package/clis/weread/search-regression.test.js +8 -8
  142. package/clis/weread/search.js +1 -1
  143. package/clis/wikipedia/random.js +1 -1
  144. package/clis/wikipedia/search.js +1 -1
  145. package/clis/wikipedia/summary.js +1 -1
  146. package/clis/wikipedia/trending.js +1 -1
  147. package/clis/xianyu/chat.js +3 -3
  148. package/clis/xianyu/item.js +2 -2
  149. package/clis/xianyu/item.test.js +3 -3
  150. package/clis/xiaohongshu/search.js +17 -2
  151. package/clis/xiaohongshu/search.test.js +37 -1
  152. package/clis/xiaoyuzhou/download.js +1 -1
  153. package/clis/xiaoyuzhou/download.test.js +3 -3
  154. package/clis/xiaoyuzhou/episode.js +1 -1
  155. package/clis/xiaoyuzhou/podcast-episodes.js +1 -1
  156. package/clis/xiaoyuzhou/podcast-episodes.test.js +2 -2
  157. package/clis/xiaoyuzhou/podcast.js +1 -1
  158. package/clis/xiaoyuzhou/transcript.js +1 -1
  159. package/clis/xiaoyuzhou/transcript.test.js +5 -5
  160. package/clis/yollomi/models.js +1 -1
  161. package/clis/youtube/channel.js +24 -1
  162. package/clis/youtube/channel.test.js +59 -0
  163. package/clis/zhihu/answer.js +21 -162
  164. package/clis/zhihu/answer.test.js +26 -53
  165. package/clis/zhihu/collection.js +197 -0
  166. package/clis/zhihu/collection.test.js +290 -0
  167. package/clis/zhihu/collections.js +127 -0
  168. package/clis/zhihu/collections.test.js +182 -0
  169. package/clis/zhihu/comment.js +24 -305
  170. package/clis/zhihu/comment.test.js +31 -35
  171. package/clis/zhihu/favorite.js +44 -182
  172. package/clis/zhihu/favorite.test.js +30 -167
  173. package/clis/zhihu/follow.js +25 -56
  174. package/clis/zhihu/follow.test.js +20 -23
  175. package/clis/zhihu/like.js +22 -67
  176. package/clis/zhihu/like.test.js +19 -42
  177. package/clis/zhihu/search.js +3 -2
  178. package/clis/zhihu/write-shared.js +8 -1
  179. package/clis/zhihu/write-shared.test.js +1 -0
  180. package/clis/zlibrary/commands.test.js +75 -0
  181. package/clis/zlibrary/info.js +47 -0
  182. package/clis/zlibrary/search.js +46 -0
  183. package/clis/zlibrary/utils.js +136 -0
  184. package/dist/src/adapter-source.d.ts +11 -0
  185. package/dist/src/adapter-source.js +24 -0
  186. package/dist/src/adapter-source.test.js +29 -0
  187. package/dist/src/browser/base-page.d.ts +3 -1
  188. package/dist/src/browser/base-page.js +76 -1
  189. package/dist/src/browser/base-page.test.d.ts +1 -0
  190. package/dist/src/browser/base-page.test.js +74 -0
  191. package/dist/src/browser/bridge.d.ts +1 -2
  192. package/dist/src/browser/bridge.js +40 -41
  193. package/dist/src/browser/cdp.d.ts +1 -0
  194. package/dist/src/browser/cdp.js +3 -3
  195. package/dist/src/browser/daemon-client.d.ts +38 -4
  196. package/dist/src/browser/daemon-client.js +24 -7
  197. package/dist/src/browser/daemon-client.test.js +49 -0
  198. package/dist/src/browser/daemon-lifecycle.d.ts +23 -0
  199. package/dist/src/browser/daemon-lifecycle.js +67 -0
  200. package/dist/src/browser/daemon-version.d.ts +4 -0
  201. package/dist/src/browser/daemon-version.js +12 -0
  202. package/dist/src/browser/errors.js +3 -0
  203. package/dist/src/browser/errors.test.js +3 -0
  204. package/dist/src/browser/network-cache.d.ts +1 -0
  205. package/dist/src/browser/page.d.ts +3 -1
  206. package/dist/src/browser/page.js +10 -2
  207. package/dist/src/browser/profile.d.ts +14 -0
  208. package/dist/src/browser/profile.js +85 -0
  209. package/dist/src/build-manifest.d.ts +2 -0
  210. package/dist/src/build-manifest.js +13 -3
  211. package/dist/src/build-manifest.test.js +20 -2
  212. package/dist/src/cli.d.ts +6 -0
  213. package/dist/src/cli.js +477 -35
  214. package/dist/src/cli.test.js +303 -2
  215. package/dist/src/commanderAdapter.js +17 -9
  216. package/dist/src/commanderAdapter.test.js +67 -2
  217. package/dist/src/commands/daemon.d.ts +2 -0
  218. package/dist/src/commands/daemon.js +42 -1
  219. package/dist/src/commands/daemon.test.js +103 -2
  220. package/dist/src/completion-shared.js +1 -2
  221. package/dist/src/completion.test.js +3 -2
  222. package/dist/src/daemon.js +125 -41
  223. package/dist/src/doctor.d.ts +5 -6
  224. package/dist/src/doctor.js +77 -19
  225. package/dist/src/doctor.test.js +117 -0
  226. package/dist/src/engine.test.js +6 -5
  227. package/dist/src/errors.d.ts +14 -8
  228. package/dist/src/errors.js +36 -30
  229. package/dist/src/errors.test.js +5 -5
  230. package/dist/src/execution.d.ts +4 -0
  231. package/dist/src/execution.js +173 -25
  232. package/dist/src/execution.test.js +171 -1
  233. package/dist/src/main.js +10 -0
  234. package/dist/src/observation/artifact.d.ts +16 -0
  235. package/dist/src/observation/artifact.js +260 -0
  236. package/dist/src/observation/artifact.test.d.ts +1 -0
  237. package/dist/src/observation/artifact.test.js +121 -0
  238. package/dist/src/observation/events.d.ts +89 -0
  239. package/dist/src/observation/events.js +1 -0
  240. package/dist/src/observation/index.d.ts +7 -0
  241. package/dist/src/observation/index.js +7 -0
  242. package/dist/src/observation/manager.d.ts +9 -0
  243. package/dist/src/observation/manager.js +27 -0
  244. package/dist/src/observation/manager.test.d.ts +1 -0
  245. package/dist/src/observation/manager.test.js +13 -0
  246. package/dist/src/observation/redaction.d.ts +11 -0
  247. package/dist/src/observation/redaction.js +81 -0
  248. package/dist/src/observation/redaction.test.d.ts +1 -0
  249. package/dist/src/observation/redaction.test.js +32 -0
  250. package/dist/src/observation/retention.d.ts +32 -0
  251. package/dist/src/observation/retention.js +160 -0
  252. package/dist/src/observation/retention.test.d.ts +1 -0
  253. package/dist/src/observation/retention.test.js +118 -0
  254. package/dist/src/observation/ring-buffer.d.ts +22 -0
  255. package/dist/src/observation/ring-buffer.js +45 -0
  256. package/dist/src/observation/ring-buffer.test.d.ts +1 -0
  257. package/dist/src/observation/ring-buffer.test.js +22 -0
  258. package/dist/src/observation/session.d.ts +25 -0
  259. package/dist/src/observation/session.js +50 -0
  260. package/dist/src/pipeline/executor.test.js +1 -0
  261. package/dist/src/pipeline/steps/download.test.js +1 -0
  262. package/dist/src/pipeline/steps/fetch.js +1 -21
  263. package/dist/src/pipeline/steps/fetch.test.js +6 -12
  264. package/dist/src/plugin-scaffold.js +1 -1
  265. package/dist/src/plugin-scaffold.test.js +1 -1
  266. package/dist/src/registry.d.ts +40 -9
  267. package/dist/src/registry.js +3 -1
  268. package/dist/src/runtime-detect.d.ts +10 -0
  269. package/dist/src/runtime-detect.js +19 -0
  270. package/dist/src/runtime-detect.test.js +12 -1
  271. package/dist/src/runtime.d.ts +2 -0
  272. package/dist/src/runtime.js +1 -0
  273. package/dist/src/types.d.ts +22 -0
  274. package/dist/src/update-check.d.ts +31 -1
  275. package/dist/src/update-check.js +62 -16
  276. package/dist/src/update-check.test.js +86 -1
  277. package/package.json +1 -1
  278. package/dist/src/diagnostic.d.ts +0 -63
  279. package/dist/src/diagnostic.js +0 -292
  280. package/dist/src/diagnostic.test.js +0 -302
  281. /package/dist/src/{diagnostic.test.d.ts → adapter-source.test.d.ts} +0 -0
@@ -7,11 +7,21 @@ export const CHATGPT_DOMAIN = 'chatgpt.com';
7
7
  export const CHATGPT_URL = 'https://chatgpt.com';
8
8
 
9
9
  // Selectors
10
- const COMPOSER_SELECTOR = '[aria-label="Chat with ChatGPT"]';
10
+ const COMPOSER_SELECTORS = [
11
+ '[aria-label="Chat with ChatGPT"]',
12
+ '[placeholder="Ask anything"]',
13
+ '#prompt-textarea',
14
+ ];
11
15
  const SEND_BTN_SELECTOR = 'button[aria-label="Send prompt"]';
12
16
 
17
+ function isSameChatGPTConversation(currentUrl, expectedUrl) {
18
+ if (!currentUrl || !expectedUrl) return false;
19
+ return currentUrl === expectedUrl
20
+ || currentUrl.startsWith(`${expectedUrl}?`)
21
+ || currentUrl.startsWith(`${expectedUrl}#`);
22
+ }
23
+
13
24
  function buildComposerLocatorScript() {
14
- const selectorsJson = JSON.stringify([COMPOSER_SELECTOR]);
15
25
  const markerAttr = 'data-opencli-chatgpt-composer';
16
26
  return `
17
27
  const isVisible = (el) => {
@@ -33,7 +43,7 @@ function buildComposerLocatorScript() {
33
43
  const marked = document.querySelector('[' + markerAttr + '="1"]');
34
44
  if (marked instanceof HTMLElement && isVisible(marked)) return marked;
35
45
 
36
- for (const selector of ${JSON.stringify([COMPOSER_SELECTOR])}) {
46
+ for (const selector of ${JSON.stringify(COMPOSER_SELECTORS)}) {
37
47
  const node = Array.from(document.querySelectorAll(selector)).find(c => c instanceof HTMLElement && isVisible(c));
38
48
  if (node instanceof HTMLElement) {
39
49
  node.setAttribute(markerAttr, '1');
@@ -89,7 +99,9 @@ export async function sendChatGPTMessage(page, text) {
89
99
  // Fallback: use execCommand
90
100
  await page.evaluate(`
91
101
  (() => {
92
- const composer = document.querySelector('[aria-label="Chat with ChatGPT"]');
102
+ var composer = null;
103
+ var sels = ${JSON.stringify(COMPOSER_SELECTORS)};
104
+ for (var si = 0; si < sels.length; si++) { composer = document.querySelector(sels[si]); if (composer) break; }
93
105
  if (!composer) return;
94
106
  composer.focus();
95
107
  document.execCommand('insertText', false, ${JSON.stringify(text)});
@@ -181,7 +193,7 @@ export async function getChatGPTVisibleImageUrls(page) {
181
193
  /**
182
194
  * Wait for new images to appear after sending a prompt.
183
195
  */
184
- export async function waitForChatGPTImages(page, beforeUrls, timeoutSeconds) {
196
+ export async function waitForChatGPTImages(page, beforeUrls, timeoutSeconds, convUrl) {
185
197
  const beforeSet = new Set(beforeUrls);
186
198
  const pollIntervalSeconds = 3;
187
199
  const maxPolls = Math.max(1, Math.ceil(timeoutSeconds / pollIntervalSeconds));
@@ -191,10 +203,26 @@ export async function waitForChatGPTImages(page, beforeUrls, timeoutSeconds) {
191
203
  for (let i = 0; i < maxPolls; i++) {
192
204
  await page.wait(i === 0 ? 3 : pollIntervalSeconds);
193
205
 
194
- // Check if still generating
206
+ let currentUrl = '';
207
+ if (convUrl && convUrl.includes('/c/')) {
208
+ currentUrl = await page.evaluate('window.location.href').catch(() => '');
209
+ if (currentUrl && !isSameChatGPTConversation(currentUrl, convUrl)) {
210
+ await page.goto(convUrl);
211
+ await page.wait(3);
212
+ }
213
+ }
214
+
195
215
  const generating = await isGenerating(page);
196
216
  if (generating) continue;
197
217
 
218
+ if (convUrl && convUrl.includes('/c/') && i > 0 && i % 5 === 0) {
219
+ const onConversation = !currentUrl || isSameChatGPTConversation(currentUrl, convUrl);
220
+ if (onConversation) {
221
+ await page.goto(convUrl);
222
+ await page.wait(3);
223
+ }
224
+ }
225
+
198
226
  const urls = (await getChatGPTVisibleImageUrls(page)).filter(url => !beforeSet.has(url));
199
227
  if (urls.length === 0) continue;
200
228
 
@@ -214,6 +242,11 @@ export async function waitForChatGPTImages(page, beforeUrls, timeoutSeconds) {
214
242
  return lastUrls;
215
243
  }
216
244
 
245
+ export const __test__ = {
246
+ COMPOSER_SELECTORS,
247
+ isSameChatGPTConversation,
248
+ };
249
+
217
250
  /**
218
251
  * Export images by URL: fetch from ChatGPT backend API and convert to base64 data URLs.
219
252
  */
@@ -0,0 +1,63 @@
1
+ import { describe, expect, it, vi } from 'vitest';
2
+ import { __test__, waitForChatGPTImages } from './utils.js';
3
+
4
+ function createPageMock({ location = '', generating = [], imageUrls = [] } = {}) {
5
+ let generatingIndex = 0;
6
+ let imageIndex = 0;
7
+ return {
8
+ wait: vi.fn().mockResolvedValue(undefined),
9
+ goto: vi.fn().mockResolvedValue(undefined),
10
+ evaluate: vi.fn((script) => {
11
+ if (script === 'window.location.href') return Promise.resolve(location);
12
+ if (script.includes('Stop generating') || script.includes('Thinking')) {
13
+ const value = generating[Math.min(generatingIndex, generating.length - 1)] ?? false;
14
+ generatingIndex += 1;
15
+ return Promise.resolve(value);
16
+ }
17
+ if (script.includes("document.querySelectorAll('img')")) {
18
+ const value = imageUrls[Math.min(imageIndex, imageUrls.length - 1)] ?? [];
19
+ imageIndex += 1;
20
+ return Promise.resolve(value);
21
+ }
22
+ return Promise.resolve(undefined);
23
+ }),
24
+ };
25
+ }
26
+
27
+ describe('chatgpt image wait contract', () => {
28
+ it('does not periodically reload the conversation while generation is still active', async () => {
29
+ const convUrl = 'https://chatgpt.com/c/demo';
30
+ const page = createPageMock({
31
+ location: convUrl,
32
+ generating: [true, true, true, true, true, true],
33
+ });
34
+
35
+ await expect(waitForChatGPTImages(page, [], 18, convUrl)).resolves.toEqual([]);
36
+ expect(page.goto).not.toHaveBeenCalled();
37
+ });
38
+
39
+ it('jumps back to the captured conversation when the page drifts away', async () => {
40
+ const convUrl = 'https://chatgpt.com/c/demo';
41
+ const page = createPageMock({
42
+ location: 'https://chatgpt.com/',
43
+ generating: [false],
44
+ imageUrls: [['https://cdn.openai.com/generated/demo.png']],
45
+ });
46
+
47
+ await expect(waitForChatGPTImages(page, [], 3, convUrl)).resolves.toEqual([
48
+ 'https://cdn.openai.com/generated/demo.png',
49
+ ]);
50
+ expect(page.goto).toHaveBeenCalledWith(convUrl);
51
+ });
52
+
53
+ it('treats query and hash variants as the same conversation', () => {
54
+ expect(__test__.isSameChatGPTConversation(
55
+ 'https://chatgpt.com/c/demo?model=gpt-image-1',
56
+ 'https://chatgpt.com/c/demo',
57
+ )).toBe(true);
58
+ expect(__test__.isSameChatGPTConversation(
59
+ 'https://chatgpt.com/c/other',
60
+ 'https://chatgpt.com/c/demo',
61
+ )).toBe(false);
62
+ });
63
+ });
@@ -15,7 +15,7 @@ export const askCommand = cli({
15
15
  { name: 'timeout', required: false, help: 'Max seconds to wait for response (default: 30)', default: '30' },
16
16
  ],
17
17
  columns: ['Role', 'Text'],
18
- func: async (page, kwargs) => {
18
+ func: async (kwargs) => {
19
19
  if (process.platform !== 'darwin') {
20
20
  throw new ConfigError('ChatGPT Desktop integration requires macOS (osascript is not available on this platform)');
21
21
  }
@@ -156,7 +156,7 @@ guard s(input, kAXValueAttribute as String) == text else {
156
156
  exit(1)
157
157
  }
158
158
 
159
- guard let sendButton = findByDescriptions(win, ["发送", "Send"]) else {
159
+ guard let sendButton = findByDescriptions(win, ["发送", "傳送", "Send"]) else {
160
160
  fputs("Could not find send button\\n", stderr)
161
161
  exit(1)
162
162
  }
@@ -240,10 +240,11 @@ let args = CommandLine.arguments
240
240
  let target = args.count > 1 ? args[1] : ""
241
241
  let needsLegacy = args.count > 2 && args[2] == "legacy"
242
242
 
243
- // Step 1: Click the "Options" button to open the popover (support both English and Chinese UI)
243
+ // Step 1: Click the "Options" button to open the popover (support English, Simplified and Traditional Chinese UI)
244
244
  var optionsBtn: AXUIElement? = nil
245
245
  if let btn = findByDesc(win, "Options") { optionsBtn = btn }
246
246
  else if let btn = findByDesc(win, "选项") { optionsBtn = btn }
247
+ else if let btn = findByDesc(win, "選項") { optionsBtn = btn }
247
248
  guard let options = optionsBtn else {
248
249
  fputs("Could not find Options button\\n", stderr); exit(1)
249
250
  }
@@ -379,5 +380,6 @@ export function getVisibleChatMessages() {
379
380
  }
380
381
  export const __test__ = {
381
382
  AX_SEND_SCRIPT,
383
+ AX_MODEL_SCRIPT,
382
384
  AX_GENERATING_SCRIPT,
383
385
  };
@@ -13,6 +13,18 @@ describe('chatgpt-app AX send script', () => {
13
13
  it('does not report success until the prompt leaves the composer after send', () => {
14
14
  expect(__test__.AX_SEND_SCRIPT).toContain('Prompt did not leave input after pressing send');
15
15
  });
16
+
17
+ it('supports english, zh-CN, and zh-TW send button labels', () => {
18
+ expect(__test__.AX_SEND_SCRIPT).toContain('["发送", "傳送", "Send"]');
19
+ });
20
+ });
21
+
22
+ describe('chatgpt-app AX model script', () => {
23
+ it('supports english, zh-CN, and zh-TW options button labels', () => {
24
+ expect(__test__.AX_MODEL_SCRIPT).toContain('findByDesc(win, "Options")');
25
+ expect(__test__.AX_MODEL_SCRIPT).toContain('findByDesc(win, "选项")');
26
+ expect(__test__.AX_MODEL_SCRIPT).toContain('findByDesc(win, "選項")');
27
+ });
16
28
  });
17
29
 
18
30
  describe('chatgpt-app generating detection', () => {
@@ -12,7 +12,7 @@ export const modelCommand = cli({
12
12
  { name: 'model', required: true, positional: true, help: 'Model to switch to', choices: MODEL_CHOICES },
13
13
  ],
14
14
  columns: ['Status', 'Model'],
15
- func: async (page, kwargs) => {
15
+ func: async (kwargs) => {
16
16
  if (process.platform !== 'darwin') {
17
17
  throw new ConfigError('ChatGPT Desktop integration requires macOS');
18
18
  }
@@ -10,7 +10,7 @@ export const newCommand = cli({
10
10
  browser: false,
11
11
  args: [],
12
12
  columns: ['Status'],
13
- func: async (page) => {
13
+ func: async () => {
14
14
  if (process.platform !== 'darwin') {
15
15
  throw new ConfigError('ChatGPT Desktop integration requires macOS (osascript is not available on this platform)');
16
16
  }
@@ -11,7 +11,7 @@ export const readCommand = cli({
11
11
  browser: false,
12
12
  args: [],
13
13
  columns: ['Role', 'Text'],
14
- func: async (page) => {
14
+ func: async () => {
15
15
  if (process.platform !== 'darwin') {
16
16
  throw new ConfigError('ChatGPT Desktop integration requires macOS (osascript is not available on this platform)');
17
17
  }
@@ -13,7 +13,7 @@ export const sendCommand = cli({
13
13
  { name: 'model', required: false, help: 'Model/mode to use: auto, instant, thinking, 5.2-instant, 5.2-thinking', choices: MODEL_CHOICES },
14
14
  ],
15
15
  columns: ['Status'],
16
- func: async (page, kwargs) => {
16
+ func: async (kwargs) => {
17
17
  const text = kwargs.text;
18
18
  const model = kwargs.model;
19
19
  try {
@@ -10,7 +10,7 @@ export const statusCommand = cli({
10
10
  browser: false,
11
11
  args: [],
12
12
  columns: ['Status'],
13
- func: async (page) => {
13
+ func: async () => {
14
14
  if (process.platform !== 'darwin') {
15
15
  throw new ConfigError('ChatGPT Desktop integration requires macOS (osascript is not available on this platform)');
16
16
  }
@@ -1,5 +1,5 @@
1
1
  import { cli, Strategy } from '@jackwener/opencli/registry';
2
- import { SelectorError } from '@jackwener/opencli/errors';
2
+ import { selectorError } from '@jackwener/opencli/errors';
3
3
  export const askCommand = cli({
4
4
  site: 'chatwise',
5
5
  name: 'ask',
@@ -43,7 +43,7 @@ export const askCommand = cli({
43
43
  })(${JSON.stringify(text)})
44
44
  `);
45
45
  if (!injected)
46
- throw new SelectorError('ChatWise input element');
46
+ throw selectorError('ChatWise input element');
47
47
  await page.wait(0.5);
48
48
  await page.pressKey('Enter');
49
49
  // Poll for response
@@ -1,5 +1,5 @@
1
1
  import { cli, Strategy } from '@jackwener/opencli/registry';
2
- import { SelectorError } from '@jackwener/opencli/errors';
2
+ import { selectorError } from '@jackwener/opencli/errors';
3
3
  export const modelCommand = cli({
4
4
  site: 'chatwise',
5
5
  name: 'model',
@@ -58,7 +58,7 @@ export const modelCommand = cli({
58
58
  })(${JSON.stringify(desiredModel)})
59
59
  `);
60
60
  if (!opened)
61
- throw new SelectorError('ChatWise model selector');
61
+ throw selectorError('ChatWise model selector');
62
62
  await page.wait(0.5);
63
63
  // Find and click the target model in the dropdown
64
64
  const found = await page.evaluate(`
@@ -1,5 +1,5 @@
1
1
  import { cli, Strategy } from '@jackwener/opencli/registry';
2
- import { SelectorError } from '@jackwener/opencli/errors';
2
+ import { selectorError } from '@jackwener/opencli/errors';
3
3
  export const sendCommand = cli({
4
4
  site: 'chatwise',
5
5
  name: 'send',
@@ -36,7 +36,7 @@ export const sendCommand = cli({
36
36
  })(${JSON.stringify(text)})
37
37
  `);
38
38
  if (!injected)
39
- throw new SelectorError('ChatWise input element');
39
+ throw selectorError('ChatWise input element');
40
40
  await page.wait(0.5);
41
41
  await page.pressKey('Enter');
42
42
  return [
@@ -0,0 +1,128 @@
1
+ import { cli, Strategy } from '@jackwener/opencli/registry';
2
+ import { ArgumentError, CommandExecutionError, EmptyResultError } from '@jackwener/opencli/errors';
3
+ import {
4
+ CLAUDE_DOMAIN, CLAUDE_URL, ensureOnClaude, selectModel, setAdaptiveThinking,
5
+ sendMessage, sendWithFile, getBubbleCount, waitForResponse, parseBoolFlag, withRetry,
6
+ ensureClaudeComposer, requireNonEmptyPrompt, requirePositiveInt,
7
+ } from './utils.js';
8
+
9
+ export const askCommand = cli({
10
+ site: 'claude',
11
+ name: 'ask',
12
+ description: 'Send a prompt to Claude and get the response',
13
+ domain: CLAUDE_DOMAIN,
14
+ strategy: Strategy.COOKIE,
15
+ browser: true,
16
+ navigateBefore: false,
17
+ timeoutSeconds: 180,
18
+ args: [
19
+ { name: 'prompt', positional: true, required: true, help: 'Prompt to send' },
20
+ { name: 'timeout', type: 'int', default: 120, help: 'Max seconds to wait for response' },
21
+ { name: 'new', type: 'boolean', default: false, help: 'Start a new chat before sending' },
22
+ { name: 'model', default: 'sonnet', choices: ['sonnet', 'opus', 'haiku'], help: 'Model to use: sonnet, opus, or haiku' },
23
+ { name: 'think', type: 'boolean', default: false, help: 'Enable Adaptive thinking' },
24
+ { name: 'file', help: 'Attach a file (image, PDF, text) with the prompt' },
25
+ ],
26
+ columns: ['response'],
27
+
28
+ func: async (page, kwargs) => {
29
+ const prompt = requireNonEmptyPrompt(kwargs.prompt, 'claude ask');
30
+ const timeoutSeconds = requirePositiveInt(
31
+ Number(kwargs.timeout ?? 120),
32
+ 'claude ask --timeout',
33
+ 'Example: opencli claude ask "hello" --timeout 120',
34
+ );
35
+ const timeoutMs = timeoutSeconds * 1000;
36
+ const wantThink = parseBoolFlag(kwargs.think);
37
+
38
+ if (parseBoolFlag(kwargs.new)) {
39
+ await page.goto(CLAUDE_URL);
40
+ await page.wait(3);
41
+ } else {
42
+ const navigated = await ensureOnClaude(page);
43
+ if (navigated) {
44
+ // Workspace was recycled; try to resume the most recent
45
+ // conversation instead of starting a new one.
46
+ await page.evaluate(`(() => {
47
+ var link = document.querySelector('a[href*="/chat/"]');
48
+ if (link) link.click();
49
+ })()`);
50
+ await page.wait(2);
51
+ }
52
+ }
53
+
54
+ await page.wait(2);
55
+ await withRetry(() => ensureClaudeComposer(page, 'Claude ask requires a visible composer on the current page.'));
56
+
57
+ // Model selector is only available on the new-chat page, not inside
58
+ // an existing conversation. Skip it when we resumed a prior thread.
59
+ const currentUrl = await page.evaluate('window.location.href') || '';
60
+ const inConversation = currentUrl.includes('/chat/');
61
+ const modelExplicit = kwargs.__opencliOptionSources?.model === 'cli';
62
+
63
+ const wantModel = kwargs.model || 'sonnet';
64
+ if (inConversation && modelExplicit) {
65
+ throw new ArgumentError(
66
+ `Cannot switch to ${wantModel} model inside an existing conversation.`,
67
+ 'Re-run with --new to start a fresh chat before selecting a model.',
68
+ );
69
+ }
70
+
71
+ if (!inConversation) {
72
+ const modelResult = await withRetry(() => selectModel(page, wantModel));
73
+ if (!modelResult?.ok) {
74
+ if (modelResult?.upgrade) {
75
+ throw new ArgumentError(
76
+ `${wantModel} model requires a paid Claude plan.`,
77
+ 'Pick --model sonnet or --model haiku, or upgrade your account.',
78
+ );
79
+ }
80
+ throw new CommandExecutionError(`Could not switch to ${wantModel} model`);
81
+ }
82
+ if (modelResult?.toggled) await page.wait(0.5);
83
+ }
84
+
85
+ const thinkResult = await withRetry(() => setAdaptiveThinking(page, wantThink));
86
+ if (!thinkResult?.ok && wantThink) {
87
+ throw new CommandExecutionError('Could not enable Adaptive thinking');
88
+ }
89
+ if (thinkResult?.toggled) await page.wait(0.5);
90
+
91
+ if (kwargs.file) {
92
+ const baseline = await withRetry(() => getBubbleCount(page));
93
+ try {
94
+ const fileResult = await sendWithFile(page, kwargs.file, prompt);
95
+ if (fileResult && !fileResult.ok) {
96
+ throw new CommandExecutionError(fileResult.reason || 'Failed to attach file');
97
+ }
98
+ } catch (err) {
99
+ // SPA navigates after send; "Promise was collected" means send succeeded
100
+ if (!String(err?.message || err).includes('Promise was collected')) throw err;
101
+ }
102
+ await page.wait(3);
103
+ const result = await waitForResponse(page, baseline, prompt, timeoutMs);
104
+ if (!result) {
105
+ throw new EmptyResultError(
106
+ 'claude ask',
107
+ `No Claude response appeared within ${timeoutSeconds}s. Re-run with a higher --timeout if the model is still generating.`,
108
+ );
109
+ }
110
+ return [{ response: result }];
111
+ }
112
+
113
+ const baseline = await withRetry(() => getBubbleCount(page));
114
+ const sendResult = await withRetry(() => sendMessage(page, prompt));
115
+ if (!sendResult?.ok) {
116
+ throw new CommandExecutionError(sendResult?.reason || 'Failed to send message');
117
+ }
118
+
119
+ const result = await waitForResponse(page, baseline, prompt, timeoutMs);
120
+ if (!result) {
121
+ throw new EmptyResultError(
122
+ 'claude ask',
123
+ `No Claude response appeared within ${timeoutSeconds}s. Re-run with a higher --timeout if the model is still generating.`,
124
+ );
125
+ }
126
+ return [{ response: result }];
127
+ },
128
+ });