@jackwener/opencli 1.7.15 → 1.7.17

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 (172) hide show
  1. package/README.md +15 -13
  2. package/README.zh-CN.md +15 -12
  3. package/cli-manifest.json +165 -209
  4. package/clis/chatgpt/ask.js +3 -2
  5. package/clis/chatgpt/commands.test.js +2 -2
  6. package/clis/chatgpt/detail.js +7 -2
  7. package/clis/chatgpt/history.js +1 -1
  8. package/clis/chatgpt/image.js +38 -4
  9. package/clis/chatgpt/image.test.js +68 -1
  10. package/clis/chatgpt/new.js +1 -1
  11. package/clis/chatgpt/read.js +3 -2
  12. package/clis/chatgpt/send.js +3 -2
  13. package/clis/chatgpt/status.js +1 -1
  14. package/clis/chatgpt/utils.js +259 -25
  15. package/clis/chatgpt/utils.test.js +166 -2
  16. package/clis/claude/ask.js +23 -8
  17. package/clis/claude/detail.js +10 -3
  18. package/clis/claude/history.js +1 -1
  19. package/clis/claude/new.js +9 -3
  20. package/clis/claude/read.js +3 -2
  21. package/clis/claude/send.js +9 -4
  22. package/clis/claude/status.js +1 -1
  23. package/clis/claude/utils.js +27 -4
  24. package/clis/deepseek/ask.js +22 -9
  25. package/clis/deepseek/detail.js +10 -2
  26. package/clis/deepseek/history.js +1 -1
  27. package/clis/deepseek/new.js +14 -3
  28. package/clis/deepseek/read.js +3 -2
  29. package/clis/deepseek/send.js +1 -1
  30. package/clis/deepseek/status.js +1 -1
  31. package/clis/deepseek/utils.js +8 -1
  32. package/clis/doubao/ask.js +1 -1
  33. package/clis/doubao/detail.js +1 -1
  34. package/clis/doubao/history.js +1 -1
  35. package/clis/doubao/meeting-summary.js +1 -1
  36. package/clis/doubao/meeting-transcript.js +1 -1
  37. package/clis/doubao/new.js +1 -1
  38. package/clis/doubao/read.js +1 -1
  39. package/clis/doubao/send.js +1 -1
  40. package/clis/doubao/status.js +1 -1
  41. package/clis/gemini/ask.js +1 -1
  42. package/clis/gemini/deep-research-result.js +1 -1
  43. package/clis/gemini/deep-research.js +1 -1
  44. package/clis/gemini/image.js +1 -1
  45. package/clis/gemini/new.js +1 -1
  46. package/clis/grok/ask.js +1 -1
  47. package/clis/grok/detail.js +1 -1
  48. package/clis/grok/history.js +1 -1
  49. package/clis/grok/image.js +1 -1
  50. package/clis/grok/new.js +1 -1
  51. package/clis/grok/read.js +1 -1
  52. package/clis/grok/send.js +1 -1
  53. package/clis/grok/status.js +1 -1
  54. package/clis/linkedin/search.js +8 -11
  55. package/clis/maimai/search-talents.js +10 -6
  56. package/clis/notebooklm/current.js +1 -1
  57. package/clis/notebooklm/get.js +1 -1
  58. package/clis/notebooklm/history.js +1 -1
  59. package/clis/notebooklm/note-list.js +1 -1
  60. package/clis/notebooklm/notes-get.js +1 -1
  61. package/clis/notebooklm/open.js +2 -2
  62. package/clis/notebooklm/open.test.js +1 -1
  63. package/clis/notebooklm/source-fulltext.js +1 -1
  64. package/clis/notebooklm/source-get.js +1 -1
  65. package/clis/notebooklm/source-guide.js +1 -1
  66. package/clis/notebooklm/source-list.js +1 -1
  67. package/clis/notebooklm/summary.js +1 -1
  68. package/clis/openreview/author.js +58 -0
  69. package/clis/openreview/openreview.test.js +83 -1
  70. package/clis/openreview/utils.js +14 -0
  71. package/clis/qwen/ask.js +1 -1
  72. package/clis/qwen/detail.js +1 -1
  73. package/clis/qwen/history.js +1 -1
  74. package/clis/qwen/image.js +1 -1
  75. package/clis/qwen/new.js +1 -1
  76. package/clis/qwen/read.js +1 -1
  77. package/clis/qwen/send.js +1 -1
  78. package/clis/qwen/status.js +1 -1
  79. package/clis/reddit/comment.js +1 -0
  80. package/clis/reddit/frontpage.js +1 -0
  81. package/clis/reddit/popular.js +1 -0
  82. package/clis/reddit/read.js +2 -0
  83. package/clis/reddit/read.test.js +4 -0
  84. package/clis/reddit/save.js +1 -0
  85. package/clis/reddit/saved.js +1 -0
  86. package/clis/reddit/search.js +1 -0
  87. package/clis/reddit/subreddit.js +1 -0
  88. package/clis/reddit/subscribe.js +1 -0
  89. package/clis/reddit/upvote.js +1 -0
  90. package/clis/reddit/upvoted.js +1 -0
  91. package/clis/reddit/user-comments.js +1 -0
  92. package/clis/reddit/user-posts.js +1 -0
  93. package/clis/reddit/user.js +1 -0
  94. package/clis/twitter/article.js +7 -4
  95. package/clis/twitter/bookmark-folder.js +3 -5
  96. package/clis/twitter/bookmark-folder.test.js +5 -2
  97. package/clis/twitter/bookmark-folders.js +3 -5
  98. package/clis/twitter/bookmark-folders.test.js +3 -1
  99. package/clis/twitter/bookmarks.js +3 -5
  100. package/clis/twitter/download.js +1 -0
  101. package/clis/twitter/followers.js +1 -0
  102. package/clis/twitter/following.js +3 -6
  103. package/clis/twitter/following.test.js +2 -1
  104. package/clis/twitter/likes.js +3 -5
  105. package/clis/twitter/list-add.js +4 -3
  106. package/clis/twitter/list-add.test.js +23 -1
  107. package/clis/twitter/list-remove.js +4 -3
  108. package/clis/twitter/list-remove.test.js +23 -1
  109. package/clis/twitter/list-tweets.js +3 -5
  110. package/clis/twitter/lists.js +3 -5
  111. package/clis/twitter/notifications.js +1 -0
  112. package/clis/twitter/profile.js +7 -4
  113. package/clis/twitter/search.js +1 -0
  114. package/clis/twitter/thread.js +5 -7
  115. package/clis/twitter/timeline.js +5 -7
  116. package/clis/twitter/trending.js +4 -4
  117. package/clis/twitter/tweets.js +3 -6
  118. package/clis/youtube/like.js +6 -2
  119. package/clis/youtube/subscribe.js +6 -2
  120. package/clis/youtube/unlike.js +6 -2
  121. package/clis/youtube/unsubscribe.js +6 -2
  122. package/clis/youtube/utils.js +19 -13
  123. package/clis/youtube/utils.test.js +17 -1
  124. package/clis/yuanbao/ask.js +1 -1
  125. package/clis/yuanbao/detail.js +1 -1
  126. package/clis/yuanbao/history.js +1 -1
  127. package/clis/yuanbao/new.js +1 -1
  128. package/clis/yuanbao/read.js +1 -1
  129. package/clis/yuanbao/send.js +1 -1
  130. package/clis/yuanbao/status.js +1 -1
  131. package/dist/src/browser/bridge.d.ts +4 -1
  132. package/dist/src/browser/bridge.js +3 -1
  133. package/dist/src/browser/cdp.d.ts +4 -1
  134. package/dist/src/browser/daemon-client.d.ts +9 -16
  135. package/dist/src/browser/daemon-client.js +8 -9
  136. package/dist/src/browser/daemon-client.test.js +10 -0
  137. package/dist/src/browser/network-cache.d.ts +5 -5
  138. package/dist/src/browser/network-cache.js +8 -8
  139. package/dist/src/browser/network-cache.test.js +4 -4
  140. package/dist/src/browser/page.d.ts +9 -7
  141. package/dist/src/browser/page.js +27 -16
  142. package/dist/src/browser/page.test.js +60 -30
  143. package/dist/src/build-manifest.js +1 -1
  144. package/dist/src/cli.js +91 -125
  145. package/dist/src/cli.test.js +293 -180
  146. package/dist/src/commanderAdapter.js +9 -0
  147. package/dist/src/discovery.js +1 -1
  148. package/dist/src/doctor.d.ts +0 -4
  149. package/dist/src/doctor.js +8 -72
  150. package/dist/src/doctor.test.js +26 -97
  151. package/dist/src/execution.d.ts +3 -0
  152. package/dist/src/execution.js +47 -23
  153. package/dist/src/execution.test.js +68 -45
  154. package/dist/src/external-clis.yaml +24 -0
  155. package/dist/src/help.d.ts +1 -0
  156. package/dist/src/help.js +36 -1
  157. package/dist/src/main.js +0 -29
  158. package/dist/src/manifest-types.d.ts +2 -4
  159. package/dist/src/observation/artifact.js +1 -1
  160. package/dist/src/observation/artifact.test.js +3 -3
  161. package/dist/src/observation/events.d.ts +1 -1
  162. package/dist/src/observation/manager.js +1 -1
  163. package/dist/src/observation/manager.test.js +3 -3
  164. package/dist/src/registry-api.d.ts +1 -1
  165. package/dist/src/registry.d.ts +3 -12
  166. package/dist/src/registry.js +6 -10
  167. package/dist/src/runtime.d.ts +10 -2
  168. package/dist/src/runtime.js +4 -1
  169. package/dist/src/serialization.d.ts +1 -1
  170. package/dist/src/serialization.js +1 -1
  171. package/dist/src/types.d.ts +0 -15
  172. package/package.json +1 -1
@@ -22,7 +22,7 @@ export const askCommand = cli({
22
22
  domain: CHATGPT_DOMAIN,
23
23
  strategy: Strategy.COOKIE,
24
24
  browser: true,
25
- browserSession: { reuse: 'site' },
25
+ siteSession: 'persistent',
26
26
  navigateBefore: false,
27
27
  args: [
28
28
  { name: 'prompt', positional: true, required: true, help: 'Prompt to send' },
@@ -43,7 +43,8 @@ export const askCommand = cli({
43
43
  } else {
44
44
  await ensureOnChatGPT(page);
45
45
  }
46
- await page.wait(2);
46
+ // startNewChat / ensureOnChatGPT now wait for the composer selector
47
+ // after navigating, so the previous standalone 2 s settle is redundant.
47
48
  await ensureChatGPTComposer(page, 'ChatGPT ask requires a logged-in ChatGPT session with a visible composer.');
48
49
 
49
50
  const baseline = await getBubbleCount(page);
@@ -10,7 +10,7 @@ import './status.js';
10
10
  import './image.js';
11
11
 
12
12
  describe('chatgpt browser command registration', () => {
13
- it('registers the baseline web chat commands with site-level reuse', () => {
13
+ it('registers the baseline web chat commands with persistent site sessions', () => {
14
14
  const expectedAccess = {
15
15
  ask: 'write',
16
16
  send: 'write',
@@ -29,7 +29,7 @@ describe('chatgpt browser command registration', () => {
29
29
  expect(cmd.domain).toBe('chatgpt.com');
30
30
  expect(cmd.strategy).toBe('cookie');
31
31
  expect(cmd.browser).toBe(true);
32
- expect(cmd.browserSession).toEqual({ reuse: 'site' });
32
+ expect(cmd.siteSession).toBe('persistent');
33
33
  expect(cmd.navigateBefore).toBe(false);
34
34
  expect(cmd.access).toBe(access);
35
35
  }
@@ -3,6 +3,7 @@ import { EmptyResultError } from '@jackwener/opencli/errors';
3
3
  import {
4
4
  CHATGPT_DOMAIN,
5
5
  CHATGPT_URL,
6
+ CONVERSATION_MESSAGE_SELECTOR,
6
7
  ensureChatGPTLogin,
7
8
  getVisibleMessages,
8
9
  messageHtmlToMarkdown,
@@ -18,7 +19,7 @@ export const detailCommand = cli({
18
19
  domain: CHATGPT_DOMAIN,
19
20
  strategy: Strategy.COOKIE,
20
21
  browser: true,
21
- browserSession: { reuse: 'site' },
22
+ siteSession: 'persistent',
22
23
  navigateBefore: false,
23
24
  args: [
24
25
  { name: 'id', positional: true, required: true, help: 'Conversation ID or full /c/<id> URL' },
@@ -29,7 +30,11 @@ export const detailCommand = cli({
29
30
  const id = parseChatGPTConversationId(kwargs.id);
30
31
  const wantMarkdown = normalizeBooleanFlag(kwargs.markdown, false);
31
32
  await page.goto(`${CHATGPT_URL}/c/${id}`, { settleMs: 2000 });
32
- await page.wait(4);
33
+ try {
34
+ await page.wait({ selector: CONVERSATION_MESSAGE_SELECTOR, timeout: 10 });
35
+ } catch {
36
+ // Empty conversation, missing access, or login redirect — handled by ensureChatGPTLogin / EmptyResultError below.
37
+ }
33
38
  await ensureChatGPTLogin(page, 'ChatGPT detail requires a logged-in ChatGPT session.');
34
39
  const messages = await getVisibleMessages(page);
35
40
  if (!messages.length) {
@@ -16,7 +16,7 @@ export const historyCommand = cli({
16
16
  domain: CHATGPT_DOMAIN,
17
17
  strategy: Strategy.COOKIE,
18
18
  browser: true,
19
- browserSession: { reuse: 'site' },
19
+ siteSession: 'persistent',
20
20
  navigateBefore: false,
21
21
  args: [
22
22
  { name: 'limit', type: 'int', default: 20, help: 'Max conversations to show' },
@@ -4,7 +4,7 @@ import * as fs from 'node:fs';
4
4
  import { cli, Strategy } from '@jackwener/opencli/registry';
5
5
  import { saveBase64ToFile } from '@jackwener/opencli/utils';
6
6
  import { ArgumentError, CommandExecutionError, EmptyResultError } from '@jackwener/opencli/errors';
7
- import { getChatGPTVisibleImageUrls, normalizeBooleanFlag, sendChatGPTMessage, waitForChatGPTImages, getChatGPTImageAssets } from './utils.js';
7
+ import { clearChatGPTDraft, getChatGPTVisibleImageUrls, normalizeBooleanFlag, prepareChatGPTImagePaths, sendChatGPTMessage, waitForChatGPTImages, getChatGPTImageAssets, uploadChatGPTImages } from './utils.js';
8
8
 
9
9
  const CHATGPT_DOMAIN = 'chatgpt.com';
10
10
 
@@ -36,6 +36,23 @@ export function nextAvailablePath(dir, baseName, ext, existsSync = fs.existsSync
36
36
  return candidate;
37
37
  }
38
38
 
39
+ export function parseImagePaths(value) {
40
+ if (Array.isArray(value)) {
41
+ return value.flatMap(item => parseImagePaths(item));
42
+ }
43
+ return String(value ?? '')
44
+ .split(',')
45
+ .map(item => item.trim())
46
+ .filter(Boolean);
47
+ }
48
+
49
+ function buildPrompt(prompt, imageCount) {
50
+ if (imageCount > 0) {
51
+ return `Edit the attached image${imageCount === 1 ? '' : 's'}: ${prompt}`;
52
+ }
53
+ return `Generate an image of: ${prompt}`;
54
+ }
55
+
39
56
  async function currentChatGPTLink(page) {
40
57
  const url = await page.evaluate('window.location.href').catch(() => '');
41
58
  return typeof url === 'string' && url ? url : 'https://chatgpt.com';
@@ -49,11 +66,12 @@ export const imageCommand = cli({
49
66
  domain: CHATGPT_DOMAIN,
50
67
  strategy: Strategy.COOKIE,
51
68
  browser: true,
52
- browserSession: { reuse: 'site' },
69
+ siteSession: 'persistent',
53
70
  navigateBefore: false,
54
71
  defaultFormat: 'plain',
55
72
  args: [
56
73
  { name: 'prompt', positional: true, required: true, help: 'Image prompt to send to ChatGPT' },
74
+ { name: 'image', help: 'Local image path to attach before prompting; comma-separated paths are supported' },
57
75
  { name: 'op', help: 'Output directory (default: ~/Pictures/chatgpt)' },
58
76
  { name: 'sd', type: 'boolean', default: false, help: 'Skip download shorthand; only show ChatGPT link' },
59
77
  { name: 'timeout', type: 'int', required: false, default: 240, help: 'Max seconds for the overall command (default: 240)' },
@@ -61,6 +79,7 @@ export const imageCommand = cli({
61
79
  columns: ['status', 'file', 'link'],
62
80
  func: async (page, kwargs) => {
63
81
  const prompt = kwargs.prompt;
82
+ const imagePaths = parseImagePaths(kwargs.image);
64
83
  const outputDir = resolveOutputDir(kwargs.op);
65
84
  const skipDownloadRaw = kwargs.sd;
66
85
  const skipDownload = skipDownloadRaw === '' || skipDownloadRaw === true || normalizeBooleanFlag(skipDownloadRaw);
@@ -68,14 +87,29 @@ export const imageCommand = cli({
68
87
  if (!Number.isInteger(timeout) || timeout < 1) {
69
88
  throw new ArgumentError('--timeout must be a positive integer (seconds)');
70
89
  }
90
+ const preparedImages = imagePaths.length ? await prepareChatGPTImagePaths(imagePaths) : { ok: true, paths: [] };
91
+ if (!preparedImages.ok) {
92
+ throw new ArgumentError(preparedImages.reason);
93
+ }
71
94
 
72
95
  // Navigate to chatgpt.com/new with full reload to clear React sidebar state
73
96
  await page.goto(`https://${CHATGPT_DOMAIN}/new`, { settleMs: 2000 });
97
+ await clearChatGPTDraft(page);
98
+
99
+ if (imagePaths.length) {
100
+ let upload;
101
+ try {
102
+ upload = await uploadChatGPTImages(page, preparedImages.paths);
103
+ } catch (err) {
104
+ throw new CommandExecutionError(`Failed to upload image to ChatGPT: ${err instanceof Error ? err.message : String(err)}`);
105
+ }
106
+ if (!upload?.ok) throw new CommandExecutionError(upload?.reason || 'Failed to upload image to ChatGPT');
107
+ }
74
108
 
75
109
  const beforeUrls = await getChatGPTVisibleImageUrls(page);
76
110
 
77
- // Send the image generation prompt - must be explicit
78
- const sent = await sendChatGPTMessage(page, `Generate an image of: ${prompt}`);
111
+ // Send an explicit generation/editing prompt so ChatGPT returns image assets.
112
+ const sent = await sendChatGPTMessage(page, buildPrompt(prompt, imagePaths.length));
79
113
  if (!sent) {
80
114
  throw new CommandExecutionError(
81
115
  'Failed to send image prompt to ChatGPT',
@@ -4,13 +4,17 @@ import { beforeEach, describe, expect, it, vi } from 'vitest';
4
4
 
5
5
  const mocks = vi.hoisted(() => ({
6
6
  getChatGPTVisibleImageUrls: vi.fn(),
7
+ clearChatGPTDraft: vi.fn(),
8
+ prepareChatGPTImagePaths: vi.fn(),
7
9
  sendChatGPTMessage: vi.fn(),
10
+ uploadChatGPTImages: vi.fn(),
8
11
  waitForChatGPTImages: vi.fn(),
9
12
  getChatGPTImageAssets: vi.fn(),
10
13
  saveBase64ToFile: vi.fn(),
11
14
  }));
12
15
 
13
16
  vi.mock('./utils.js', () => ({
17
+ clearChatGPTDraft: mocks.clearChatGPTDraft,
14
18
  getChatGPTVisibleImageUrls: mocks.getChatGPTVisibleImageUrls,
15
19
  normalizeBooleanFlag: (value, fallback = false) => {
16
20
  if (typeof value === 'boolean') return value;
@@ -18,7 +22,9 @@ vi.mock('./utils.js', () => ({
18
22
  const normalized = String(value).trim().toLowerCase();
19
23
  return normalized === 'true' || normalized === '1' || normalized === 'yes' || normalized === 'on';
20
24
  },
25
+ prepareChatGPTImagePaths: mocks.prepareChatGPTImagePaths,
21
26
  sendChatGPTMessage: mocks.sendChatGPTMessage,
27
+ uploadChatGPTImages: mocks.uploadChatGPTImages,
22
28
  waitForChatGPTImages: mocks.waitForChatGPTImages,
23
29
  getChatGPTImageAssets: mocks.getChatGPTImageAssets,
24
30
  }));
@@ -27,7 +33,7 @@ vi.mock('@jackwener/opencli/utils', () => ({
27
33
  saveBase64ToFile: mocks.saveBase64ToFile,
28
34
  }));
29
35
 
30
- const { imageCommand, nextAvailablePath, resolveOutputDir } = await import('./image.js');
36
+ const { imageCommand, nextAvailablePath, parseImagePaths, resolveOutputDir } = await import('./image.js');
31
37
 
32
38
  function createPage() {
33
39
  return {
@@ -39,8 +45,11 @@ function createPage() {
39
45
 
40
46
  beforeEach(() => {
41
47
  vi.restoreAllMocks();
48
+ mocks.clearChatGPTDraft.mockReset().mockResolvedValue(undefined);
49
+ mocks.prepareChatGPTImagePaths.mockReset().mockImplementation(async (paths) => ({ ok: true, paths }));
42
50
  mocks.getChatGPTVisibleImageUrls.mockReset().mockResolvedValue([]);
43
51
  mocks.sendChatGPTMessage.mockReset().mockResolvedValue(true);
52
+ mocks.uploadChatGPTImages.mockReset().mockResolvedValue({ ok: true });
44
53
  mocks.waitForChatGPTImages.mockReset().mockResolvedValue(['https://images.example/generated.png']);
45
54
  mocks.getChatGPTImageAssets.mockReset().mockResolvedValue([{
46
55
  url: 'https://images.example/generated.png',
@@ -66,6 +75,64 @@ describe('chatgpt image output paths', () => {
66
75
 
67
76
  expect(nextAvailablePath(dir, 'chatgpt_123', '.png', (file) => taken.has(file))).toBe(path.join(dir, 'chatgpt_123_2.png'));
68
77
  });
78
+
79
+ it('parses comma-separated image paths', () => {
80
+ expect(parseImagePaths('/tmp/a.png, /tmp/b.jpg')).toEqual(['/tmp/a.png', '/tmp/b.jpg']);
81
+ expect(parseImagePaths([' /tmp/a.png ', '/tmp/b.jpg,/tmp/c.webp'])).toEqual(['/tmp/a.png', '/tmp/b.jpg', '/tmp/c.webp']);
82
+ });
83
+ });
84
+
85
+ describe('chatgpt image upload flow', () => {
86
+ it('uploads local images before sending an edit prompt', async () => {
87
+ mocks.prepareChatGPTImagePaths.mockResolvedValue({ ok: true, paths: ['/abs/cat.png', '/abs/dog.jpg'] });
88
+ await imageCommand.func(createPage(), {
89
+ prompt: 'make the background blue',
90
+ image: '/tmp/cat.png,/tmp/dog.jpg',
91
+ op: '',
92
+ sd: true,
93
+ timeout: 240,
94
+ });
95
+
96
+ expect(mocks.clearChatGPTDraft).toHaveBeenCalled();
97
+ expect(mocks.uploadChatGPTImages).toHaveBeenCalledWith(expect.anything(), ['/abs/cat.png', '/abs/dog.jpg']);
98
+ expect(mocks.uploadChatGPTImages.mock.invocationCallOrder[0]).toBeLessThan(
99
+ mocks.getChatGPTVisibleImageUrls.mock.invocationCallOrder[0],
100
+ );
101
+ expect(mocks.sendChatGPTMessage).toHaveBeenCalledWith(expect.anything(), 'Edit the attached images: make the background blue');
102
+ });
103
+
104
+ it('rejects invalid local image paths before browser navigation', async () => {
105
+ mocks.prepareChatGPTImagePaths.mockResolvedValue({ ok: false, reason: 'Image not found: /tmp/missing.png' });
106
+ const page = createPage();
107
+
108
+ await expect(imageCommand.func(page, {
109
+ prompt: 'make the background blue',
110
+ image: '/tmp/missing.png',
111
+ op: '',
112
+ sd: false,
113
+ timeout: 240,
114
+ })).rejects.toMatchObject({
115
+ code: 'ARGUMENT',
116
+ message: expect.stringContaining('Image not found'),
117
+ });
118
+ expect(page.goto).not.toHaveBeenCalled();
119
+ expect(mocks.uploadChatGPTImages).not.toHaveBeenCalled();
120
+ });
121
+
122
+ it('surfaces upload failures as command execution errors', async () => {
123
+ mocks.uploadChatGPTImages.mockResolvedValue({ ok: false, reason: 'image upload preview did not appear' });
124
+
125
+ await expect(imageCommand.func(createPage(), {
126
+ prompt: 'make the background blue',
127
+ image: '/tmp/cat.png',
128
+ op: '',
129
+ sd: false,
130
+ timeout: 240,
131
+ })).rejects.toMatchObject({
132
+ code: 'COMMAND_EXEC',
133
+ message: expect.stringContaining('image upload preview did not appear'),
134
+ });
135
+ });
69
136
  });
70
137
 
71
138
  describe('chatgpt image failure contracts', () => {
@@ -13,7 +13,7 @@ export const newCommand = cli({
13
13
  domain: CHATGPT_DOMAIN,
14
14
  strategy: Strategy.COOKIE,
15
15
  browser: true,
16
- browserSession: { reuse: 'site' },
16
+ siteSession: 'persistent',
17
17
  navigateBefore: false,
18
18
  args: [],
19
19
  columns: ['Status'],
@@ -17,7 +17,7 @@ export const readCommand = cli({
17
17
  domain: CHATGPT_DOMAIN,
18
18
  strategy: Strategy.COOKIE,
19
19
  browser: true,
20
- browserSession: { reuse: 'site' },
20
+ siteSession: 'persistent',
21
21
  navigateBefore: false,
22
22
  args: [
23
23
  { name: 'markdown', type: 'boolean', default: false, help: 'Emit assistant replies as markdown' },
@@ -25,8 +25,9 @@ export const readCommand = cli({
25
25
  columns: ['Index', 'Role', 'Text'],
26
26
  func: async (page, kwargs) => {
27
27
  const wantMarkdown = normalizeBooleanFlag(kwargs.markdown, false);
28
+ // ensureOnChatGPT now waits for the composer selector after navigating,
29
+ // so the previous standalone 2 s settle is redundant.
28
30
  await ensureOnChatGPT(page);
29
- await page.wait(2);
30
31
  await ensureChatGPTLogin(page, 'ChatGPT read requires a logged-in ChatGPT session.');
31
32
  const messages = await getVisibleMessages(page);
32
33
  if (!messages.length) {
@@ -19,7 +19,7 @@ export const sendCommand = cli({
19
19
  domain: CHATGPT_DOMAIN,
20
20
  strategy: Strategy.COOKIE,
21
21
  browser: true,
22
- browserSession: { reuse: 'site' },
22
+ siteSession: 'persistent',
23
23
  navigateBefore: false,
24
24
  args: [
25
25
  { name: 'prompt', positional: true, required: true, help: 'Prompt to send' },
@@ -34,7 +34,8 @@ export const sendCommand = cli({
34
34
  } else {
35
35
  await ensureOnChatGPT(page);
36
36
  }
37
- await page.wait(2);
37
+ // startNewChat / ensureOnChatGPT now wait for the composer selector
38
+ // after navigating, so the previous standalone 2 s settle is redundant.
38
39
  await ensureChatGPTComposer(page, 'ChatGPT send requires a logged-in ChatGPT session with a visible composer.');
39
40
 
40
41
  const sent = await sendChatGPTMessage(page, prompt);
@@ -13,7 +13,7 @@ export const statusCommand = cli({
13
13
  domain: CHATGPT_DOMAIN,
14
14
  strategy: Strategy.COOKIE,
15
15
  browser: true,
16
- browserSession: { reuse: 'site' },
16
+ siteSession: 'persistent',
17
17
  navigateBefore: false,
18
18
  args: [],
19
19
  columns: ['Status', 'Login', 'Url'],