@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
@@ -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' },
@@ -1,5 +1,5 @@
1
1
  import { cli, Strategy } from '@jackwener/opencli/registry';
2
- import { CLAUDE_DOMAIN, CLAUDE_URL, ensureClaudeComposer } from './utils.js';
2
+ import { CLAUDE_DOMAIN, CLAUDE_URL, COMPOSER_SELECTOR, ensureClaudeComposer } from './utils.js';
3
3
 
4
4
  export const newCommand = cli({
5
5
  site: 'claude',
@@ -9,14 +9,20 @@ 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'],
16
16
 
17
17
  func: async (page) => {
18
18
  await page.goto(CLAUDE_URL);
19
- await page.wait(2);
19
+ // Wait for the composer to mount instead of a fixed 2 s sleep. If it
20
+ // never mounts, swallow and let ensureClaudeComposer surface a typed error.
21
+ try {
22
+ await page.wait({ selector: COMPOSER_SELECTOR, timeout: 8 });
23
+ } catch {
24
+ // Login or error page — ensureClaudeComposer below throws AuthRequiredError / CommandExecutionError.
25
+ }
20
26
  await ensureClaudeComposer(page, 'Claude new requires a logged-in Claude session with a visible composer.');
21
27
  return [{ Status: 'New chat started' }];
22
28
  },
@@ -10,14 +10,15 @@ 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'],
17
17
 
18
18
  func: async (page) => {
19
+ // ensureOnClaude now waits for the composer selector; the previous post-nav
20
+ // 3 s settle is covered by that event-based wait.
19
21
  await ensureOnClaude(page);
20
- await page.wait(3);
21
22
  await ensureClaudeLogin(page, 'Claude read requires a logged-in Claude session.');
22
23
  const messages = await getVisibleMessages(page);
23
24
  if (messages.length > 0) return messages;
@@ -1,6 +1,6 @@
1
1
  import { cli, Strategy } from '@jackwener/opencli/registry';
2
2
  import { CommandExecutionError } from '@jackwener/opencli/errors';
3
- import { CLAUDE_DOMAIN, CLAUDE_URL, ensureOnClaude, sendMessage, parseBoolFlag, withRetry, ensureClaudeComposer, requireNonEmptyPrompt } from './utils.js';
3
+ import { CLAUDE_DOMAIN, CLAUDE_URL, COMPOSER_SELECTOR, ensureOnClaude, sendMessage, parseBoolFlag, withRetry, ensureClaudeComposer, requireNonEmptyPrompt } from './utils.js';
4
4
 
5
5
  export const sendCommand = cli({
6
6
  site: 'claude',
@@ -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' },
@@ -23,10 +23,15 @@ export const sendCommand = cli({
23
23
 
24
24
  if (parseBoolFlag(kwargs.new)) {
25
25
  await page.goto(CLAUDE_URL);
26
- await page.wait(3);
26
+ try {
27
+ await page.wait({ selector: COMPOSER_SELECTOR, timeout: 8 });
28
+ } catch {
29
+ // Composer didn't mount; ensureClaudeComposer below surfaces a typed error.
30
+ }
27
31
  } else {
32
+ // ensureOnClaude now waits for the composer selector; the previous
33
+ // post-nav 2 s settle is covered by that event-based wait.
28
34
  await ensureOnClaude(page);
29
- await page.wait(2);
30
35
  }
31
36
  await withRetry(() => ensureClaudeComposer(page, 'Claude send requires a visible composer on the current page.'));
32
37
 
@@ -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'],
@@ -26,7 +26,14 @@ export async function isOnClaude(page) {
26
26
  export async function ensureOnClaude(page) {
27
27
  if (await isOnClaude(page)) return false;
28
28
  await page.goto(CLAUDE_URL);
29
- await page.wait(3);
29
+ // Wait for the composer textarea instead of a fixed 3 s sleep. On the login
30
+ // page it never mounts; swallow the timeout so callers (read / detail /
31
+ // send) can still inspect page state and produce typed errors.
32
+ try {
33
+ await page.wait({ selector: COMPOSER_SELECTOR, timeout: 8 });
34
+ } catch {
35
+ // Login or error page — downstream ensureClaudeLogin / ensureClaudeComposer surfaces a typed error.
36
+ }
30
37
  return true;
31
38
  }
32
39
 
@@ -111,7 +118,13 @@ export async function getVisibleMessages(page) {
111
118
  export async function getConversationList(page) {
112
119
  if (!(await isOnClaude(page)) || !(await page.evaluate('window.location.href') || '').includes('/recents')) {
113
120
  await page.goto('https://claude.ai/recents');
114
- await page.wait(3);
121
+ // Recents list mounts <a href="/chat/...">; an empty history is also
122
+ // valid (returns []), so swallow the timeout instead of raising.
123
+ try {
124
+ await page.wait({ selector: 'a[href*="/chat/"]', timeout: 8 });
125
+ } catch {
126
+ // Empty history or login page — downstream evaluate returns [].
127
+ }
115
128
  }
116
129
  const items = await page.evaluate(`(() => {
117
130
  var links = Array.from(document.querySelectorAll('a[href*="/chat/"]'));
@@ -147,7 +160,12 @@ export async function selectModel(page, modelName) {
147
160
  if (!opened?.ok) return opened;
148
161
  if (!opened.opened) return opened;
149
162
 
150
- await page.wait(0.6);
163
+ // Wait for the dropdown menu items to mount instead of a fixed 0.6 s sleep.
164
+ try {
165
+ await page.wait({ selector: 'div[role="menuitemradio"]', timeout: 3 });
166
+ } catch {
167
+ // Dropdown didn't open — next evaluate finds no target and returns { ok: false }.
168
+ }
151
169
 
152
170
  return page.evaluate(`(() => {
153
171
  var items = Array.from(document.querySelectorAll('div[role="menuitemradio"]'));
@@ -175,7 +193,12 @@ export async function setAdaptiveThinking(page, enabled) {
175
193
  })()`);
176
194
  if (!opened?.ok) return { ok: false };
177
195
 
178
- await page.wait(0.6);
196
+ // Wait for the dropdown menu items to mount instead of a fixed 0.6 s sleep.
197
+ try {
198
+ await page.wait({ selector: 'div[role="menuitem"]', timeout: 3 });
199
+ } catch {
200
+ // Dropdown didn't open — next evaluate finds no target and returns { ok: false }.
201
+ }
179
202
 
180
203
  return page.evaluate(`(() => {
181
204
  var items = Array.from(document.querySelectorAll('div[role="menuitem"]'));
@@ -3,7 +3,7 @@ import { CliError, CommandExecutionError, EXIT_CODES } from '@jackwener/opencli/
3
3
  import {
4
4
  DEEPSEEK_DOMAIN, DEEPSEEK_URL, ensureOnDeepSeek, selectModel, setFeature,
5
5
  sendMessage, sendWithFile, getBubbleCount, waitForResponse, parseBoolFlag, withRetry,
6
- pickResumeUrl,
6
+ pickResumeUrl, TEXTAREA_SELECTOR,
7
7
  } from './utils.js';
8
8
 
9
9
  export const askCommand = cli({
@@ -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' },
@@ -35,7 +35,13 @@ export const askCommand = cli({
35
35
 
36
36
  if (parseBoolFlag(kwargs.new)) {
37
37
  await page.goto(DEEPSEEK_URL);
38
- await page.wait(3);
38
+ // Wait for the composer to mount instead of a fixed 3 s sleep.
39
+ try {
40
+ await page.wait({ selector: TEXTAREA_SELECTOR, timeout: 8 });
41
+ } catch {
42
+ // Selector still missing → downstream selectModel/sendMessage
43
+ // will surface the failure with a typed error.
44
+ }
39
45
  } else {
40
46
  const navigated = await ensureOnDeepSeek(page);
41
47
  if (navigated) {
@@ -49,12 +55,15 @@ export const askCommand = cli({
49
55
  );
50
56
  }
51
57
  await page.goto(resumeUrl);
52
- await page.wait(2);
58
+ try {
59
+ await page.wait({ selector: TEXTAREA_SELECTOR, timeout: 5 });
60
+ } catch {
61
+ // Conversation page may still be loading; subsequent steps
62
+ // will retry or report.
63
+ }
53
64
  }
54
65
  }
55
66
 
56
- await page.wait(2);
57
-
58
67
  // Model selector is only available on the new-chat page, not inside
59
68
  // an existing conversation. Skip it when we resumed a prior thread.
60
69
  const currentUrl = await page.evaluate('window.location.href') || '';
@@ -76,7 +85,9 @@ export const askCommand = cli({
76
85
  if (!modelResult?.ok) {
77
86
  throw new CommandExecutionError(`Could not switch to ${wantModel} model`);
78
87
  }
79
- if (modelResult?.toggled) await page.wait(0.5);
88
+ // The 0.5 s settle previously here was redundant: each subsequent
89
+ // step (setFeature, sendMessage) issues a fresh CDP eval, giving
90
+ // React more than enough time to flush the toggle's state update.
80
91
  }
81
92
 
82
93
  const thinkResult = await withRetry(() => setFeature(page, 'DeepThink', wantThink));
@@ -102,7 +113,8 @@ export const askCommand = cli({
102
113
  }
103
114
  }
104
115
 
105
- if (thinkResult?.toggled || searchResult?.toggled) await page.wait(0.5);
116
+ // No settle wait after toggles: the next CDP eval below already gives
117
+ // React time to flush the aria-checked state.
106
118
 
107
119
  if (kwargs.file) {
108
120
  const baseline = await withRetry(() => getBubbleCount(page));
@@ -115,7 +127,8 @@ export const askCommand = cli({
115
127
  // SPA navigates after send; "Promise was collected" means send succeeded
116
128
  if (!String(err?.message || err).includes('Promise was collected')) throw err;
117
129
  }
118
- await page.wait(3);
130
+ // waitForResponse polls every 3 s for new bubbles, so the previous
131
+ // 3 s settle here was a redundant sleep on top of the first poll.
119
132
  const result = await waitForResponse(page, baseline, prompt, timeoutMs, wantThink);
120
133
  if (!result) {
121
134
  return [{ response: `[NO RESPONSE] No reply within ${kwargs.timeout}s.` }];
@@ -2,6 +2,7 @@ import { cli, Strategy } from '@jackwener/opencli/registry';
2
2
  import { EmptyResultError } from '@jackwener/opencli/errors';
3
3
  import {
4
4
  DEEPSEEK_DOMAIN,
5
+ MESSAGE_SELECTOR,
5
6
  ensureOnDeepSeek,
6
7
  getVisibleMessages,
7
8
  parseDeepSeekConversationId,
@@ -15,7 +16,7 @@ export const detailCommand = cli({
15
16
  domain: DEEPSEEK_DOMAIN,
16
17
  strategy: Strategy.COOKIE,
17
18
  browser: true,
18
- browserSession: { reuse: 'site' },
19
+ siteSession: 'persistent',
19
20
  navigateBefore: false,
20
21
  args: [
21
22
  { name: 'id', required: true, positional: true, help: 'Conversation ID (UUID) or full /a/chat/s/<id> URL' },
@@ -25,7 +26,14 @@ export const detailCommand = cli({
25
26
  const id = parseDeepSeekConversationId(kwargs.id);
26
27
  await ensureOnDeepSeek(page);
27
28
  await page.goto(`https://chat.deepseek.com/a/chat/s/${id}`);
28
- await page.wait(5);
29
+ // Wait for at least one rendered bubble instead of a fixed 5 s sleep.
30
+ // Empty / invalid conversations fall through to the EmptyResultError
31
+ // below.
32
+ try {
33
+ await page.wait({ selector: MESSAGE_SELECTOR, timeout: 10 });
34
+ } catch {
35
+ // No bubble mounted within 10 s; treated as empty by the check below.
36
+ }
29
37
  const messages = await getVisibleMessages(page);
30
38
  if (messages.length === 0) {
31
39
  throw new EmptyResultError(
@@ -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' },
@@ -1,5 +1,6 @@
1
1
  import { cli, Strategy } from '@jackwener/opencli/registry';
2
- import { DEEPSEEK_DOMAIN, DEEPSEEK_URL } from './utils.js';
2
+ import { CommandExecutionError } from '@jackwener/opencli/errors';
3
+ import { DEEPSEEK_DOMAIN, DEEPSEEK_URL, TEXTAREA_SELECTOR } from './utils.js';
3
4
 
4
5
  export const newCommand = cli({
5
6
  site: 'deepseek',
@@ -9,14 +10,24 @@ export const newCommand = cli({
9
10
  domain: DEEPSEEK_DOMAIN,
10
11
  strategy: Strategy.COOKIE,
11
12
  browser: true,
12
- browserSession: { reuse: 'site' },
13
+ siteSession: 'persistent',
13
14
  navigateBefore: false,
14
15
  args: [],
15
16
  columns: ['Status'],
16
17
 
17
18
  func: async (page) => {
18
19
  await page.goto(DEEPSEEK_URL);
19
- await page.wait(2);
20
+ // Confirm the composer mounted before reporting success. The previous
21
+ // 2 s blind sleep would return "New chat started" even when the page
22
+ // was still loading or the user was logged out.
23
+ try {
24
+ await page.wait({ selector: TEXTAREA_SELECTOR, timeout: 8 });
25
+ } catch {
26
+ throw new CommandExecutionError(
27
+ 'DeepSeek composer did not mount within 8 s',
28
+ 'Verify you are logged into chat.deepseek.com.',
29
+ );
30
+ }
20
31
  return [{ Status: 'New chat started' }];
21
32
  },
22
33
  });
@@ -9,14 +9,15 @@ 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'],
16
16
 
17
17
  func: async (page) => {
18
+ // ensureOnDeepSeek already waits for the composer to mount; the
19
+ // follow-up 5 s sleep was redundant.
18
20
  await ensureOnDeepSeek(page);
19
- await page.wait(5);
20
21
  const messages = await getVisibleMessages(page);
21
22
  if (messages.length > 0) return messages;
22
23
  return [{ Role: 'system', Text: 'No visible messages found.' }];
@@ -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'],
@@ -43,7 +43,14 @@ export async function isOnDeepSeek(page) {
43
43
  export async function ensureOnDeepSeek(page) {
44
44
  if (await isOnDeepSeek(page)) return false;
45
45
  await page.goto(DEEPSEEK_URL);
46
- await page.wait(3);
46
+ // Wait for the composer textarea instead of a fixed 3 s sleep. On the login
47
+ // page it never mounts; swallow the timeout so callers (status / read /
48
+ // history) can still inspect page state.
49
+ try {
50
+ await page.wait({ selector: TEXTAREA_SELECTOR, timeout: 8 });
51
+ } catch {
52
+ // Login or error page — downstream will see hasTextarea=false / empty results.
53
+ }
47
54
  return true;
48
55
  }
49
56
 
@@ -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'],
@@ -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'],
@@ -245,23 +245,20 @@ async function fetchJobCards(page, input) {
245
245
  const MAX_BATCH = 25;
246
246
  const allJobs = [];
247
247
  let offset = input.start;
248
+ // Read JSESSIONID directly from the cookie store via CDP — zero page.evaluate round-trip
249
+ const cookies = await page.getCookies({ url: 'https://www.linkedin.com' });
250
+ const jsession = cookies.find((c) => c.name === 'JSESSIONID')?.value;
251
+ if (!jsession) {
252
+ throw new AuthRequiredError(LINKEDIN_DOMAIN, 'LinkedIn JSESSIONID cookie not found. Please sign in to LinkedIn in the browser.');
253
+ }
254
+ const csrf = jsession.replace(/^"|"$/g, '');
248
255
  while (allJobs.length < input.limit) {
249
256
  const count = Math.min(MAX_BATCH, input.limit - allJobs.length);
250
257
  const apiPath = buildVoyagerUrl(input, offset, count);
251
258
  const batch = await page.evaluate(`(async () => {
252
- const jsession = document.cookie.split(';').map(p => p.trim())
253
- .find(p => p.startsWith('JSESSIONID='))?.slice('JSESSIONID='.length);
254
- if (!jsession) {
255
- return {
256
- authRequired: true,
257
- error: 'LinkedIn JSESSIONID cookie not found. Please sign in to LinkedIn in the browser.'
258
- };
259
- }
260
-
261
- const csrf = jsession.replace(/^"|"$/g, '');
262
259
  const res = await fetch(${JSON.stringify(apiPath)}, {
263
260
  credentials: 'include',
264
- headers: { 'csrf-token': csrf, 'x-restli-protocol-version': '2.0.0' },
261
+ headers: { 'csrf-token': ${JSON.stringify(csrf)}, 'x-restli-protocol-version': '2.0.0' },
265
262
  });
266
263
  if (res.status === 401 || res.status === 403) {
267
264
  const text = await res.text();