@jackwener/opencli 1.7.16 → 1.7.18

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 (174) hide show
  1. package/README.md +11 -9
  2. package/README.zh-CN.md +10 -8
  3. package/cli-manifest.json +377 -271
  4. package/clis/chatgpt/ask.js +1 -1
  5. package/clis/chatgpt/commands.test.js +2 -2
  6. package/clis/chatgpt/detail.js +1 -1
  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 +1 -1
  12. package/clis/chatgpt/send.js +1 -1
  13. package/clis/chatgpt/status.js +1 -1
  14. package/clis/chatgpt/utils.js +208 -16
  15. package/clis/chatgpt/utils.test.js +131 -2
  16. package/clis/claude/ask.js +1 -1
  17. package/clis/claude/detail.js +1 -1
  18. package/clis/claude/history.js +1 -1
  19. package/clis/claude/new.js +1 -1
  20. package/clis/claude/read.js +1 -1
  21. package/clis/claude/send.js +1 -1
  22. package/clis/claude/status.js +1 -1
  23. package/clis/deepseek/ask.js +1 -1
  24. package/clis/deepseek/detail.js +1 -1
  25. package/clis/deepseek/history.js +1 -1
  26. package/clis/deepseek/new.js +1 -1
  27. package/clis/deepseek/read.js +1 -1
  28. package/clis/deepseek/send.js +1 -1
  29. package/clis/deepseek/status.js +1 -1
  30. package/clis/doubao/ask.js +1 -1
  31. package/clis/doubao/detail.js +1 -1
  32. package/clis/doubao/history.js +1 -1
  33. package/clis/doubao/meeting-summary.js +1 -1
  34. package/clis/doubao/meeting-transcript.js +1 -1
  35. package/clis/doubao/new.js +1 -1
  36. package/clis/doubao/read.js +1 -1
  37. package/clis/doubao/send.js +1 -1
  38. package/clis/doubao/status.js +1 -1
  39. package/clis/doubao/utils.js +17 -0
  40. package/clis/doubao/utils.test.js +61 -0
  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/notebooklm/current.js +1 -1
  55. package/clis/notebooklm/get.js +1 -1
  56. package/clis/notebooklm/history.js +1 -1
  57. package/clis/notebooklm/note-list.js +1 -1
  58. package/clis/notebooklm/notes-get.js +1 -1
  59. package/clis/notebooklm/open.js +2 -2
  60. package/clis/notebooklm/open.test.js +1 -1
  61. package/clis/notebooklm/source-fulltext.js +1 -1
  62. package/clis/notebooklm/source-get.js +1 -1
  63. package/clis/notebooklm/source-guide.js +1 -1
  64. package/clis/notebooklm/source-list.js +1 -1
  65. package/clis/notebooklm/summary.js +1 -1
  66. package/clis/qwen/ask.js +1 -1
  67. package/clis/qwen/detail.js +1 -1
  68. package/clis/qwen/history.js +1 -1
  69. package/clis/qwen/image.js +1 -1
  70. package/clis/qwen/new.js +1 -1
  71. package/clis/qwen/read.js +1 -1
  72. package/clis/qwen/send.js +1 -1
  73. package/clis/qwen/status.js +1 -1
  74. package/clis/reddit/comment.js +1 -1
  75. package/clis/reddit/frontpage.js +1 -1
  76. package/clis/reddit/popular.js +1 -1
  77. package/clis/reddit/read.js +1 -1
  78. package/clis/reddit/read.test.js +2 -2
  79. package/clis/reddit/reply.js +182 -0
  80. package/clis/reddit/reply.test.js +89 -0
  81. package/clis/reddit/save.js +1 -1
  82. package/clis/reddit/saved.js +1 -1
  83. package/clis/reddit/search.js +1 -1
  84. package/clis/reddit/subreddit.js +1 -1
  85. package/clis/reddit/subscribe.js +1 -1
  86. package/clis/reddit/upvote.js +1 -1
  87. package/clis/reddit/upvoted.js +1 -1
  88. package/clis/reddit/user-comments.js +1 -1
  89. package/clis/reddit/user-posts.js +1 -1
  90. package/clis/reddit/user.js +1 -1
  91. package/clis/rednote/comments.js +76 -0
  92. package/clis/rednote/download.js +59 -0
  93. package/clis/rednote/feed.js +95 -0
  94. package/clis/rednote/navigation.test.js +26 -0
  95. package/clis/rednote/note.js +68 -0
  96. package/clis/rednote/notifications.js +139 -0
  97. package/clis/rednote/rednote.test.js +157 -0
  98. package/clis/rednote/search.js +97 -0
  99. package/clis/rednote/user.js +55 -0
  100. package/clis/twitter/article.js +1 -1
  101. package/clis/twitter/bookmark-folder.js +1 -1
  102. package/clis/twitter/bookmark-folders.js +1 -1
  103. package/clis/twitter/bookmarks.js +1 -1
  104. package/clis/twitter/download.js +1 -1
  105. package/clis/twitter/followers.js +1 -1
  106. package/clis/twitter/following.js +1 -1
  107. package/clis/twitter/likes.js +1 -1
  108. package/clis/twitter/list-tweets.js +1 -1
  109. package/clis/twitter/lists.js +1 -1
  110. package/clis/twitter/notifications.js +1 -1
  111. package/clis/twitter/profile.js +1 -1
  112. package/clis/twitter/search.js +1 -1
  113. package/clis/twitter/thread.js +1 -1
  114. package/clis/twitter/timeline.js +1 -1
  115. package/clis/twitter/trending.js +1 -1
  116. package/clis/twitter/tweets.js +1 -1
  117. package/clis/xiaohongshu/comments.js +34 -24
  118. package/clis/xiaohongshu/download.js +32 -23
  119. package/clis/xiaohongshu/feed.js +23 -15
  120. package/clis/xiaohongshu/note-helpers.js +16 -6
  121. package/clis/xiaohongshu/note.js +26 -20
  122. package/clis/xiaohongshu/notifications.js +26 -19
  123. package/clis/xiaohongshu/search.js +37 -28
  124. package/clis/xiaohongshu/user-helpers.js +13 -4
  125. package/clis/xiaohongshu/user-helpers.test.js +20 -0
  126. package/clis/xiaohongshu/user.js +9 -4
  127. package/clis/youtube/transcript.js +28 -3
  128. package/clis/youtube/transcript.test.js +90 -1
  129. package/clis/yuanbao/ask.js +1 -1
  130. package/clis/yuanbao/detail.js +1 -1
  131. package/clis/yuanbao/history.js +1 -1
  132. package/clis/yuanbao/new.js +1 -1
  133. package/clis/yuanbao/read.js +1 -1
  134. package/clis/yuanbao/send.js +1 -1
  135. package/clis/yuanbao/status.js +1 -1
  136. package/dist/src/browser/bridge.d.ts +3 -1
  137. package/dist/src/browser/bridge.js +3 -1
  138. package/dist/src/browser/cdp.d.ts +3 -1
  139. package/dist/src/browser/daemon-client.d.ts +7 -14
  140. package/dist/src/browser/daemon-client.js +2 -6
  141. package/dist/src/browser/network-cache.d.ts +5 -5
  142. package/dist/src/browser/network-cache.js +8 -8
  143. package/dist/src/browser/network-cache.test.js +4 -4
  144. package/dist/src/browser/page.d.ts +8 -7
  145. package/dist/src/browser/page.js +23 -16
  146. package/dist/src/browser/page.test.js +60 -30
  147. package/dist/src/build-manifest.js +1 -1
  148. package/dist/src/cli.js +60 -162
  149. package/dist/src/cli.test.js +184 -198
  150. package/dist/src/commanderAdapter.js +2 -0
  151. package/dist/src/discovery.js +1 -1
  152. package/dist/src/doctor.d.ts +0 -4
  153. package/dist/src/doctor.js +14 -73
  154. package/dist/src/doctor.test.js +28 -97
  155. package/dist/src/execution.d.ts +1 -0
  156. package/dist/src/execution.js +20 -21
  157. package/dist/src/execution.test.js +27 -31
  158. package/dist/src/help.js +7 -1
  159. package/dist/src/main.js +0 -19
  160. package/dist/src/manifest-types.d.ts +2 -4
  161. package/dist/src/observation/artifact.js +1 -1
  162. package/dist/src/observation/artifact.test.js +3 -3
  163. package/dist/src/observation/events.d.ts +1 -1
  164. package/dist/src/observation/manager.js +1 -1
  165. package/dist/src/observation/manager.test.js +3 -3
  166. package/dist/src/registry-api.d.ts +1 -1
  167. package/dist/src/registry.d.ts +3 -12
  168. package/dist/src/registry.js +6 -10
  169. package/dist/src/runtime.d.ts +7 -2
  170. package/dist/src/runtime.js +3 -1
  171. package/dist/src/serialization.d.ts +1 -1
  172. package/dist/src/serialization.js +1 -1
  173. package/dist/src/types.d.ts +0 -15
  174. package/package.json +1 -1
@@ -1,5 +1,17 @@
1
- import { describe, expect, it, vi } from 'vitest';
2
- import { __test__, sendChatGPTMessage, waitForChatGPTImages } from './utils.js';
1
+ import fs from 'node:fs';
2
+ import os from 'node:os';
3
+ import path from 'node:path';
4
+ import { afterEach, describe, expect, it, vi } from 'vitest';
5
+ import { __test__, prepareChatGPTImagePaths, sendChatGPTMessage, uploadChatGPTImages, waitForChatGPTImages } from './utils.js';
6
+
7
+ const tempDirs = [];
8
+
9
+ afterEach(() => {
10
+ vi.restoreAllMocks();
11
+ while (tempDirs.length) {
12
+ fs.rmSync(tempDirs.pop(), { recursive: true, force: true });
13
+ }
14
+ });
3
15
 
4
16
  function createPageMock({ location = '', generating = [], imageUrls = [] } = {}) {
5
17
  let generatingIndex = 0;
@@ -96,6 +108,26 @@ describe('chatgpt send selectors', () => {
96
108
  await expect(sendChatGPTMessage(page, 'hello')).resolves.toBe(true);
97
109
  });
98
110
 
111
+ it('uses the composer submit fallback consistently for readiness and click', async () => {
112
+ const page = {
113
+ wait: vi.fn().mockResolvedValue(undefined),
114
+ nativeType: vi.fn().mockResolvedValue(undefined),
115
+ evaluate: vi.fn((script) => {
116
+ if (script.includes('findComposer')) return Promise.resolve(true);
117
+ if (script.includes('sendBtnFound')) {
118
+ expect(script).toContain('#composer-submit-button:not([disabled])');
119
+ return Promise.resolve({ sendBtnFound: true });
120
+ }
121
+ if (script.includes('if (sendBtn) sendBtn.click')) {
122
+ expect(script).toContain('#composer-submit-button:not([disabled])');
123
+ }
124
+ return Promise.resolve(undefined);
125
+ }),
126
+ };
127
+
128
+ await expect(sendChatGPTMessage(page, 'hello')).resolves.toBe(true);
129
+ });
130
+
99
131
  it('keeps zh-CN aria and placeholder fallbacks without replacing English selectors', () => {
100
132
  expect(__test__.COMPOSER_SELECTORS).toEqual(expect.arrayContaining([
101
133
  '[aria-label="Chat with ChatGPT"]',
@@ -105,7 +137,104 @@ describe('chatgpt send selectors', () => {
105
137
  '[data-testid="prompt-textarea"]',
106
138
  ]));
107
139
  expect(__test__.SEND_BUTTON_SELECTOR).toBe('button[data-testid="send-button"]:not([disabled])');
140
+ expect(__test__.SEND_BUTTON_FALLBACK_SELECTORS).toContain('#composer-submit-button:not([disabled])');
108
141
  expect(__test__.SEND_BUTTON_LABELS).toEqual(expect.arrayContaining(['Send prompt', 'Send message', 'Send', '发送提示']));
109
142
  expect(__test__.CLOSE_SIDEBAR_LABELS).toEqual(expect.arrayContaining(['Close sidebar', '关闭边栏']));
110
143
  });
111
144
  });
145
+
146
+ describe('chatgpt image upload helper', () => {
147
+ it('validates local images without a browser page', async () => {
148
+ const dir = fs.mkdtempSync(path.join(os.tmpdir(), 'opencli-chatgpt-'));
149
+ tempDirs.push(dir);
150
+ const filePath = path.join(dir, 'cat.png');
151
+ fs.writeFileSync(filePath, 'fake-png');
152
+
153
+ await expect(prepareChatGPTImagePaths([filePath])).resolves.toEqual({ ok: true, paths: [filePath] });
154
+ await expect(prepareChatGPTImagePaths([path.join(dir, 'missing.png')])).resolves.toMatchObject({
155
+ ok: false,
156
+ reason: expect.stringContaining('Image not found'),
157
+ });
158
+ });
159
+
160
+ it('prefers Browser Bridge file input upload and waits for a preview', async () => {
161
+ const dir = fs.mkdtempSync(path.join(os.tmpdir(), 'opencli-chatgpt-'));
162
+ tempDirs.push(dir);
163
+ const filePath = path.join(dir, 'cat.png');
164
+ fs.writeFileSync(filePath, 'fake-png');
165
+
166
+ const page = {
167
+ setFileInput: vi.fn().mockResolvedValue(undefined),
168
+ wait: vi.fn().mockResolvedValue(undefined),
169
+ evaluate: vi.fn().mockResolvedValue(true),
170
+ };
171
+
172
+ const result = await uploadChatGPTImages(page, [filePath]);
173
+
174
+ expect(result).toEqual({ ok: true, files: [filePath] });
175
+ expect(page.setFileInput).toHaveBeenCalledWith([filePath], 'input[type="file"]');
176
+ });
177
+
178
+ it('rejects missing files before touching the page', async () => {
179
+ const page = {
180
+ setFileInput: vi.fn(),
181
+ wait: vi.fn(),
182
+ evaluate: vi.fn(),
183
+ };
184
+
185
+ const result = await uploadChatGPTImages(page, ['/no/such/cat.png']);
186
+
187
+ expect(result.ok).toBe(false);
188
+ expect(result.reason).toContain('Image not found');
189
+ expect(page.setFileInput).not.toHaveBeenCalled();
190
+ });
191
+
192
+ it('rejects non-image extensions', async () => {
193
+ const dir = fs.mkdtempSync(path.join(os.tmpdir(), 'opencli-chatgpt-'));
194
+ tempDirs.push(dir);
195
+ const filePath = path.join(dir, 'report.pdf');
196
+ fs.writeFileSync(filePath, 'fake');
197
+
198
+ const page = {
199
+ setFileInput: vi.fn(),
200
+ wait: vi.fn(),
201
+ evaluate: vi.fn(),
202
+ };
203
+
204
+ const result = await uploadChatGPTImages(page, [filePath]);
205
+
206
+ expect(result.ok).toBe(false);
207
+ expect(result.reason).toContain('Unsupported image type');
208
+ expect(page.setFileInput).not.toHaveBeenCalled();
209
+ });
210
+
211
+ it('passes a React-compatible change event in fallback upload', async () => {
212
+ const dir = fs.mkdtempSync(path.join(os.tmpdir(), 'opencli-chatgpt-'));
213
+ tempDirs.push(dir);
214
+ const filePath = path.join(dir, 'cat.png');
215
+ fs.writeFileSync(filePath, 'fake-png');
216
+
217
+ const page = {
218
+ setFileInput: vi.fn().mockRejectedValue(new Error('No element found')),
219
+ wait: vi.fn().mockResolvedValue(undefined),
220
+ evaluate: vi.fn((script) => {
221
+ return Promise.resolve({ ok: true });
222
+ }),
223
+ };
224
+
225
+ const result = await uploadChatGPTImages(page, [filePath]);
226
+
227
+ expect(result).toEqual({ ok: true, files: [filePath] });
228
+ const fallbackScript = page.evaluate.mock.calls
229
+ .map(([script]) => String(script))
230
+ .find(script => script.includes('new DataTransfer()'));
231
+ expect(fallbackScript).toContain('preventDefault()');
232
+ expect(fallbackScript).toContain('stopPropagation()');
233
+ });
234
+
235
+ it('exposes image MIME inference for fallback upload', () => {
236
+ expect(__test__.imageMimeFromPath('/tmp/a.png')).toBe('image/png');
237
+ expect(__test__.imageMimeFromPath('/tmp/a.webp')).toBe('image/webp');
238
+ expect(__test__.imageMimeFromPath('/tmp/a.jpg')).toBe('image/jpeg');
239
+ });
240
+ });
@@ -15,7 +15,7 @@ export const askCommand = cli({
15
15
  domain: CLAUDE_DOMAIN,
16
16
  strategy: Strategy.COOKIE,
17
17
  browser: true,
18
- browserSession: { reuse: 'site' },
18
+ siteSession: 'persistent',
19
19
  navigateBefore: false,
20
20
  args: [
21
21
  { name: 'prompt', positional: true, required: true, help: 'Prompt to send' },
@@ -10,7 +10,7 @@ export const detailCommand = cli({
10
10
  domain: CLAUDE_DOMAIN,
11
11
  strategy: Strategy.COOKIE,
12
12
  browser: true,
13
- browserSession: { reuse: 'site' },
13
+ siteSession: 'persistent',
14
14
  navigateBefore: false,
15
15
  args: [
16
16
  { name: 'id', positional: true, required: true, help: 'Conversation ID (UUID from /chat/<id>)' },
@@ -10,7 +10,7 @@ export const historyCommand = cli({
10
10
  domain: CLAUDE_DOMAIN,
11
11
  strategy: Strategy.COOKIE,
12
12
  browser: true,
13
- browserSession: { reuse: 'site' },
13
+ siteSession: 'persistent',
14
14
  navigateBefore: false,
15
15
  args: [
16
16
  { name: 'limit', type: 'int', default: 20, help: 'Max conversations to show' },
@@ -9,7 +9,7 @@ export const newCommand = cli({
9
9
  domain: CLAUDE_DOMAIN,
10
10
  strategy: Strategy.COOKIE,
11
11
  browser: true,
12
- browserSession: { reuse: 'site' },
12
+ siteSession: 'persistent',
13
13
  navigateBefore: false,
14
14
  args: [],
15
15
  columns: ['Status'],
@@ -10,7 +10,7 @@ export const readCommand = cli({
10
10
  domain: CLAUDE_DOMAIN,
11
11
  strategy: Strategy.COOKIE,
12
12
  browser: true,
13
- browserSession: { reuse: 'site' },
13
+ siteSession: 'persistent',
14
14
  navigateBefore: false,
15
15
  args: [],
16
16
  columns: ['Index', 'Role', 'Text'],
@@ -10,7 +10,7 @@ export const sendCommand = cli({
10
10
  domain: CLAUDE_DOMAIN,
11
11
  strategy: Strategy.COOKIE,
12
12
  browser: true,
13
- browserSession: { reuse: 'site' },
13
+ siteSession: 'persistent',
14
14
  navigateBefore: false,
15
15
  args: [
16
16
  { name: 'prompt', positional: true, required: true, help: 'Prompt to send' },
@@ -9,7 +9,7 @@ export const statusCommand = cli({
9
9
  domain: CLAUDE_DOMAIN,
10
10
  strategy: Strategy.COOKIE,
11
11
  browser: true,
12
- browserSession: { reuse: 'site' },
12
+ siteSession: 'persistent',
13
13
  navigateBefore: false,
14
14
  args: [],
15
15
  columns: ['Status', 'Login', 'Url'],
@@ -14,7 +14,7 @@ export const askCommand = cli({
14
14
  domain: DEEPSEEK_DOMAIN,
15
15
  strategy: Strategy.COOKIE,
16
16
  browser: true,
17
- browserSession: { reuse: 'site' },
17
+ siteSession: 'persistent',
18
18
  navigateBefore: false,
19
19
  args: [
20
20
  { name: 'prompt', positional: true, required: true, help: 'Prompt to send' },
@@ -16,7 +16,7 @@ export const detailCommand = cli({
16
16
  domain: DEEPSEEK_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: 'id', required: true, positional: true, help: 'Conversation ID (UUID) or full /a/chat/s/<id> URL' },
@@ -9,7 +9,7 @@ export const historyCommand = cli({
9
9
  domain: DEEPSEEK_DOMAIN,
10
10
  strategy: Strategy.COOKIE,
11
11
  browser: true,
12
- browserSession: { reuse: 'site' },
12
+ siteSession: 'persistent',
13
13
  navigateBefore: false,
14
14
  args: [
15
15
  { name: 'limit', type: 'int', default: 20, help: 'Max conversations to show' },
@@ -10,7 +10,7 @@ export const newCommand = cli({
10
10
  domain: DEEPSEEK_DOMAIN,
11
11
  strategy: Strategy.COOKIE,
12
12
  browser: true,
13
- browserSession: { reuse: 'site' },
13
+ siteSession: 'persistent',
14
14
  navigateBefore: false,
15
15
  args: [],
16
16
  columns: ['Status'],
@@ -9,7 +9,7 @@ export const readCommand = cli({
9
9
  domain: DEEPSEEK_DOMAIN,
10
10
  strategy: Strategy.COOKIE,
11
11
  browser: true,
12
- browserSession: { reuse: 'site' },
12
+ siteSession: 'persistent',
13
13
  navigateBefore: false,
14
14
  args: [],
15
15
  columns: ['Role', 'Text'],
@@ -15,7 +15,7 @@ export const sendCommand = cli({
15
15
  domain: DEEPSEEK_DOMAIN,
16
16
  strategy: Strategy.COOKIE,
17
17
  browser: true,
18
- browserSession: { reuse: 'site' },
18
+ siteSession: 'persistent',
19
19
  navigateBefore: false,
20
20
  args: [
21
21
  { name: 'id', required: true, positional: true, help: 'Conversation ID (UUID) or full /a/chat/s/<id> URL' },
@@ -9,7 +9,7 @@ export const statusCommand = cli({
9
9
  domain: DEEPSEEK_DOMAIN,
10
10
  strategy: Strategy.COOKIE,
11
11
  browser: true,
12
- browserSession: { reuse: 'site' },
12
+ siteSession: 'persistent',
13
13
  navigateBefore: false,
14
14
  args: [],
15
15
  columns: ['Status', 'Login', 'Url'],
@@ -9,7 +9,7 @@ export const askCommand = cli({
9
9
  domain: DOUBAO_DOMAIN,
10
10
  strategy: Strategy.COOKIE,
11
11
  browser: true,
12
- browserSession: { reuse: 'site' },
12
+ siteSession: 'persistent',
13
13
  navigateBefore: false,
14
14
  args: [
15
15
  { name: 'text', required: true, positional: true, help: 'Prompt to send' },
@@ -8,7 +8,7 @@ export const detailCommand = cli({
8
8
  domain: DOUBAO_DOMAIN,
9
9
  strategy: Strategy.COOKIE,
10
10
  browser: true,
11
- browserSession: { reuse: 'site' },
11
+ siteSession: 'persistent',
12
12
  navigateBefore: false,
13
13
  args: [
14
14
  { name: 'id', required: true, positional: true, help: 'Conversation ID (numeric or full URL)' },
@@ -8,7 +8,7 @@ export const historyCommand = cli({
8
8
  domain: DOUBAO_DOMAIN,
9
9
  strategy: Strategy.COOKIE,
10
10
  browser: true,
11
- browserSession: { reuse: 'site' },
11
+ siteSession: 'persistent',
12
12
  navigateBefore: false,
13
13
  args: [
14
14
  { name: 'limit', required: false, help: 'Max number of conversations to show', default: '50' },
@@ -8,7 +8,7 @@ export const meetingSummaryCommand = cli({
8
8
  domain: DOUBAO_DOMAIN,
9
9
  strategy: Strategy.COOKIE,
10
10
  browser: true,
11
- browserSession: { reuse: 'site' },
11
+ siteSession: 'persistent',
12
12
  navigateBefore: false,
13
13
  args: [
14
14
  { name: 'id', required: true, positional: true, help: 'Conversation ID (numeric or full URL)' },
@@ -8,7 +8,7 @@ export const meetingTranscriptCommand = cli({
8
8
  domain: DOUBAO_DOMAIN,
9
9
  strategy: Strategy.COOKIE,
10
10
  browser: true,
11
- browserSession: { reuse: 'site' },
11
+ siteSession: 'persistent',
12
12
  navigateBefore: false,
13
13
  args: [
14
14
  { name: 'id', required: true, positional: true, help: 'Conversation ID (numeric or full URL)' },
@@ -8,7 +8,7 @@ export const newCommand = cli({
8
8
  domain: DOUBAO_DOMAIN,
9
9
  strategy: Strategy.COOKIE,
10
10
  browser: true,
11
- browserSession: { reuse: 'site' },
11
+ siteSession: 'persistent',
12
12
  navigateBefore: false,
13
13
  args: [],
14
14
  columns: ['Status', 'Action'],
@@ -8,7 +8,7 @@ export const readCommand = cli({
8
8
  domain: DOUBAO_DOMAIN,
9
9
  strategy: Strategy.COOKIE,
10
10
  browser: true,
11
- browserSession: { reuse: 'site' },
11
+ siteSession: 'persistent',
12
12
  navigateBefore: false,
13
13
  args: [],
14
14
  columns: ['Role', 'Text'],
@@ -8,7 +8,7 @@ export const sendCommand = cli({
8
8
  domain: DOUBAO_DOMAIN,
9
9
  strategy: Strategy.COOKIE,
10
10
  browser: true,
11
- browserSession: { reuse: 'site' },
11
+ siteSession: 'persistent',
12
12
  navigateBefore: false,
13
13
  args: [{ name: 'text', required: true, positional: true, help: 'Message to send' }],
14
14
  columns: ['Status', 'SubmittedBy', 'InjectedText'],
@@ -8,7 +8,7 @@ export const statusCommand = cli({
8
8
  domain: DOUBAO_DOMAIN,
9
9
  strategy: Strategy.COOKIE,
10
10
  browser: true,
11
- browserSession: { reuse: 'site' },
11
+ siteSession: 'persistent',
12
12
  navigateBefore: false,
13
13
  args: [],
14
14
  columns: ['Status', 'Login', 'Url', 'Title'],
@@ -163,6 +163,19 @@ function getTurnsScript() {
163
163
  ) {
164
164
  return 'Assistant';
165
165
  }
166
+ // 2026-05 Doubao DOM refactor: no more receive-message / bg-g-receive-msg-bubble
167
+ // markers on assistant turns. Wrappers are now [class*="inner-item-"] /
168
+ // [class*="top-item-"] and the only reliable assistant signal is the
169
+ // .flow-markdown-body content container WITHOUT any send-bubble marker.
170
+ if (
171
+ (root.matches('[class*="inner-item-"], [class*="top-item-"]')
172
+ || root.closest('[class*="inner-item-"], [class*="top-item-"]'))
173
+ && (root.matches('.flow-markdown-body') || root.querySelector('.flow-markdown-body'))
174
+ && !root.matches('[class*="bg-g-send-msg-bubble"]')
175
+ && !root.querySelector('[class*="bg-g-send-msg-bubble"]')
176
+ ) {
177
+ return 'Assistant';
178
+ }
166
179
  return '';
167
180
  };
168
181
 
@@ -223,6 +236,10 @@ function getTurnsScript() {
223
236
  if (!messageList) return [];
224
237
 
225
238
  const itemSelectors = [
239
+ // 2026-05 Doubao DOM refactor wrappers (prepended; outer ones win via
240
+ // ancestor-keep dedup below).
241
+ '[class*="inner-item-"]',
242
+ '[class*="top-item-"]',
226
243
  '[class*="item-kDun2N"]',
227
244
  '[data-testid="union_message"]',
228
245
  '[data-testid="message-block-container"]',
@@ -1,3 +1,4 @@
1
+ import { JSDOM } from 'jsdom';
1
2
  import { describe, expect, it, vi } from 'vitest';
2
3
  import { CommandExecutionError } from '@jackwener/opencli/errors';
3
4
  import {
@@ -145,6 +146,28 @@ describe('doubao send strategy', () => {
145
146
  });
146
147
  });
147
148
  describe('doubao receive strategy', () => {
149
+ function runTurnsScript(html) {
150
+ const dom = new JSDOM(html, { url: 'https://www.doubao.com/chat', runScripts: 'outside-only' });
151
+ Object.defineProperty(dom.window.HTMLElement.prototype, 'innerText', {
152
+ configurable: true,
153
+ get() {
154
+ return this.textContent || '';
155
+ },
156
+ });
157
+ dom.window.HTMLElement.prototype.getBoundingClientRect = () => ({
158
+ width: 100,
159
+ height: 24,
160
+ top: 0,
161
+ left: 0,
162
+ right: 100,
163
+ bottom: 24,
164
+ x: 0,
165
+ y: 0,
166
+ toJSON: () => ({}),
167
+ });
168
+ return dom.window.eval(__test__.getTurnsScript());
169
+ }
170
+
148
171
  it('keeps both the new skin selectors and the older structural fallbacks in the turns script', () => {
149
172
  const turnsScript = __test__.getTurnsScript();
150
173
  expect(turnsScript).toContain('[class*="message-list-S2Fv2S"]');
@@ -157,6 +180,44 @@ describe('doubao receive strategy', () => {
157
180
  expect(turnsScript).toContain('[data-testid="message-block-container"]');
158
181
  });
159
182
 
183
+ it('includes the 2026-05 doubao DOM-refactor inner-item / top-item wrappers and the flow-markdown-body assistant fallback', () => {
184
+ const turnsScript = __test__.getTurnsScript();
185
+ // New wrappers added to itemSelectors so message roots resolve under the
186
+ // refactored DOM where the legacy item-kDun2N / union_message / message-block-container
187
+ // / data-message-id selectors no longer match.
188
+ expect(turnsScript).toContain('[class*="inner-item-"]');
189
+ expect(turnsScript).toContain('[class*="top-item-"]');
190
+ // Assistant fallback: post-refactor doubao no longer emits receive-message /
191
+ // bg-g-receive-msg-bubble markup. Only signal is .flow-markdown-body content
192
+ // container without send-bubble.
193
+ expect(turnsScript).toContain('.flow-markdown-body');
194
+ });
195
+
196
+ it('extracts clean assistant turns from the 2026-05 wrapper DOM without using whole-page chrome', () => {
197
+ const turns = runTurnsScript(`
198
+ <main>
199
+ <aside>历史对话</aside>
200
+ <section class="message-list-S2Fv2S">
201
+ <div class="top-item-user">
202
+ <div class="inner-item-user">
203
+ <div class="bg-g-send-msg-bubble">测试一下,只回复OK</div>
204
+ </div>
205
+ </div>
206
+ <div class="top-item-assistant">
207
+ <div class="inner-item-assistant">
208
+ <div class="flow-markdown-body"><p>OK</p></div>
209
+ </div>
210
+ </div>
211
+ </section>
212
+ </main>
213
+ `);
214
+
215
+ expect(turns).toEqual([
216
+ { Role: 'User', Text: '测试一下,只回复OK' },
217
+ { Role: 'Assistant', Text: 'OK' },
218
+ ]);
219
+ });
220
+
160
221
  it('extends transcript-noise cleanup for the current zh-CN chrome copy', () => {
161
222
  const transcriptScript = __test__.getTranscriptLinesScript();
162
223
  expect(transcriptScript).toContain('请仔细甄别');
@@ -16,7 +16,7 @@ export const askCommand = cli({
16
16
  domain: GEMINI_DOMAIN,
17
17
  strategy: Strategy.COOKIE,
18
18
  browser: true,
19
- browserSession: { reuse: 'site' },
19
+ siteSession: 'persistent',
20
20
  navigateBefore: false,
21
21
  defaultFormat: 'plain',
22
22
  args: [
@@ -44,7 +44,7 @@ export const deepResearchResultCommand = cli({
44
44
  domain: GEMINI_DOMAIN,
45
45
  strategy: Strategy.COOKIE,
46
46
  browser: true,
47
- browserSession: { reuse: 'site' },
47
+ siteSession: 'persistent',
48
48
  navigateBefore: false,
49
49
  defaultFormat: 'plain',
50
50
  args: [
@@ -23,7 +23,7 @@ export const deepResearchCommand = cli({
23
23
  domain: GEMINI_DOMAIN,
24
24
  strategy: Strategy.COOKIE,
25
25
  browser: true,
26
- browserSession: { reuse: 'site' },
26
+ siteSession: 'persistent',
27
27
  navigateBefore: false,
28
28
  defaultFormat: 'plain',
29
29
  args: [
@@ -52,7 +52,7 @@ export const imageCommand = cli({
52
52
  domain: GEMINI_DOMAIN,
53
53
  strategy: Strategy.COOKIE,
54
54
  browser: true,
55
- browserSession: { reuse: 'site' },
55
+ siteSession: 'persistent',
56
56
  navigateBefore: false,
57
57
  defaultFormat: 'plain',
58
58
  args: [
@@ -8,7 +8,7 @@ export const newCommand = cli({
8
8
  domain: GEMINI_DOMAIN,
9
9
  strategy: Strategy.COOKIE,
10
10
  browser: true,
11
- browserSession: { reuse: 'site' },
11
+ siteSession: 'persistent',
12
12
  navigateBefore: false,
13
13
  args: [],
14
14
  columns: ['Status', 'Action'],
package/clis/grok/ask.js CHANGED
@@ -29,7 +29,7 @@ export const askCommand = cli({
29
29
  domain: 'grok.com',
30
30
  strategy: Strategy.COOKIE,
31
31
  browser: true,
32
- browserSession: { reuse: 'site' },
32
+ siteSession: 'persistent',
33
33
  args: [
34
34
  { name: 'prompt', positional: true, type: 'string', required: true, help: 'Prompt to send to Grok' },
35
35
  { name: 'timeout', type: 'int', default: 120, help: 'Max seconds to wait for response (default: 120)' },
@@ -17,7 +17,7 @@ cli({
17
17
  strategy: Strategy.COOKIE,
18
18
  browser: true,
19
19
  navigateBefore: false,
20
- browserSession: { reuse: 'site' },
20
+ siteSession: 'persistent',
21
21
  args: [
22
22
  { name: 'id', positional: true, required: true, help: 'Session ID (UUID) or full https://grok.com/c/<id> URL' },
23
23
  { name: 'markdown', type: 'boolean', default: false, help: 'Emit assistant replies as markdown' },
@@ -16,7 +16,7 @@ cli({
16
16
  domain: GROK_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 (default 20, max 100)' },
@@ -253,7 +253,7 @@ export const imageCommand = cli({
253
253
  domain: 'grok.com',
254
254
  strategy: Strategy.COOKIE,
255
255
  browser: true,
256
- browserSession: { reuse: 'site' },
256
+ siteSession: 'persistent',
257
257
  args: [
258
258
  { name: 'prompt', positional: true, type: 'string', required: true, help: 'Image generation prompt' },
259
259
  { name: 'timeout', type: 'int', default: 240, help: 'Max seconds to wait for the image (default: 240)' },
package/clis/grok/new.js CHANGED
@@ -9,7 +9,7 @@ cli({
9
9
  domain: GROK_DOMAIN,
10
10
  strategy: Strategy.COOKIE,
11
11
  browser: true,
12
- browserSession: { reuse: 'site' },
12
+ siteSession: 'persistent',
13
13
  navigateBefore: false,
14
14
  args: [],
15
15
  columns: ['Status'],
package/clis/grok/read.js CHANGED
@@ -15,7 +15,7 @@ cli({
15
15
  domain: GROK_DOMAIN,
16
16
  strategy: Strategy.COOKIE,
17
17
  browser: true,
18
- browserSession: { reuse: 'site' },
18
+ siteSession: 'persistent',
19
19
  navigateBefore: false,
20
20
  args: [
21
21
  { name: 'markdown', type: 'boolean', default: false, help: 'Emit assistant replies as markdown' },
package/clis/grok/send.js CHANGED
@@ -18,7 +18,7 @@ cli({
18
18
  domain: GROK_DOMAIN,
19
19
  strategy: Strategy.COOKIE,
20
20
  browser: true,
21
- browserSession: { reuse: 'site' },
21
+ siteSession: 'persistent',
22
22
  navigateBefore: false,
23
23
  args: [
24
24
  { name: 'prompt', required: true, positional: true, help: 'Prompt to send to Grok' },
@@ -15,7 +15,7 @@ cli({
15
15
  domain: GROK_DOMAIN,
16
16
  strategy: Strategy.COOKIE,
17
17
  browser: true,
18
- browserSession: { reuse: 'site' },
18
+ siteSession: 'persistent',
19
19
  navigateBefore: false,
20
20
  args: [],
21
21
  columns: ['Status', 'Login', 'Model', 'SessionId', 'Url'],
@@ -17,7 +17,7 @@ cli({
17
17
  await requireNotebooklmSession(page);
18
18
  const state = await getNotebooklmPageState(page);
19
19
  if (state.kind !== 'notebook') {
20
- throw new EmptyResultError('opencli notebooklm current', 'No NotebookLM notebook is open in the automation workspace. Run `opencli notebooklm open <notebook>` first.');
20
+ throw new EmptyResultError('opencli notebooklm current', 'No NotebookLM notebook is open in the adapter session. Run `opencli notebooklm open <notebook>` first.');
21
21
  }
22
22
  const current = await readCurrentNotebooklm(page);
23
23
  if (!current) {