@jackwener/opencli 1.7.16 → 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 (149) hide show
  1. package/README.md +8 -9
  2. package/README.zh-CN.md +8 -8
  3. package/cli-manifest.json +97 -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/gemini/ask.js +1 -1
  40. package/clis/gemini/deep-research-result.js +1 -1
  41. package/clis/gemini/deep-research.js +1 -1
  42. package/clis/gemini/image.js +1 -1
  43. package/clis/gemini/new.js +1 -1
  44. package/clis/grok/ask.js +1 -1
  45. package/clis/grok/detail.js +1 -1
  46. package/clis/grok/history.js +1 -1
  47. package/clis/grok/image.js +1 -1
  48. package/clis/grok/new.js +1 -1
  49. package/clis/grok/read.js +1 -1
  50. package/clis/grok/send.js +1 -1
  51. package/clis/grok/status.js +1 -1
  52. package/clis/notebooklm/current.js +1 -1
  53. package/clis/notebooklm/get.js +1 -1
  54. package/clis/notebooklm/history.js +1 -1
  55. package/clis/notebooklm/note-list.js +1 -1
  56. package/clis/notebooklm/notes-get.js +1 -1
  57. package/clis/notebooklm/open.js +2 -2
  58. package/clis/notebooklm/open.test.js +1 -1
  59. package/clis/notebooklm/source-fulltext.js +1 -1
  60. package/clis/notebooklm/source-get.js +1 -1
  61. package/clis/notebooklm/source-guide.js +1 -1
  62. package/clis/notebooklm/source-list.js +1 -1
  63. package/clis/notebooklm/summary.js +1 -1
  64. package/clis/qwen/ask.js +1 -1
  65. package/clis/qwen/detail.js +1 -1
  66. package/clis/qwen/history.js +1 -1
  67. package/clis/qwen/image.js +1 -1
  68. package/clis/qwen/new.js +1 -1
  69. package/clis/qwen/read.js +1 -1
  70. package/clis/qwen/send.js +1 -1
  71. package/clis/qwen/status.js +1 -1
  72. package/clis/reddit/comment.js +1 -1
  73. package/clis/reddit/frontpage.js +1 -1
  74. package/clis/reddit/popular.js +1 -1
  75. package/clis/reddit/read.js +1 -1
  76. package/clis/reddit/read.test.js +2 -2
  77. package/clis/reddit/save.js +1 -1
  78. package/clis/reddit/saved.js +1 -1
  79. package/clis/reddit/search.js +1 -1
  80. package/clis/reddit/subreddit.js +1 -1
  81. package/clis/reddit/subscribe.js +1 -1
  82. package/clis/reddit/upvote.js +1 -1
  83. package/clis/reddit/upvoted.js +1 -1
  84. package/clis/reddit/user-comments.js +1 -1
  85. package/clis/reddit/user-posts.js +1 -1
  86. package/clis/reddit/user.js +1 -1
  87. package/clis/twitter/article.js +1 -1
  88. package/clis/twitter/bookmark-folder.js +1 -1
  89. package/clis/twitter/bookmark-folders.js +1 -1
  90. package/clis/twitter/bookmarks.js +1 -1
  91. package/clis/twitter/download.js +1 -1
  92. package/clis/twitter/followers.js +1 -1
  93. package/clis/twitter/following.js +1 -1
  94. package/clis/twitter/likes.js +1 -1
  95. package/clis/twitter/list-tweets.js +1 -1
  96. package/clis/twitter/lists.js +1 -1
  97. package/clis/twitter/notifications.js +1 -1
  98. package/clis/twitter/profile.js +1 -1
  99. package/clis/twitter/search.js +1 -1
  100. package/clis/twitter/thread.js +1 -1
  101. package/clis/twitter/timeline.js +1 -1
  102. package/clis/twitter/trending.js +1 -1
  103. package/clis/twitter/tweets.js +1 -1
  104. package/clis/yuanbao/ask.js +1 -1
  105. package/clis/yuanbao/detail.js +1 -1
  106. package/clis/yuanbao/history.js +1 -1
  107. package/clis/yuanbao/new.js +1 -1
  108. package/clis/yuanbao/read.js +1 -1
  109. package/clis/yuanbao/send.js +1 -1
  110. package/clis/yuanbao/status.js +1 -1
  111. package/dist/src/browser/bridge.d.ts +3 -1
  112. package/dist/src/browser/bridge.js +3 -1
  113. package/dist/src/browser/cdp.d.ts +3 -1
  114. package/dist/src/browser/daemon-client.d.ts +7 -14
  115. package/dist/src/browser/daemon-client.js +2 -6
  116. package/dist/src/browser/network-cache.d.ts +5 -5
  117. package/dist/src/browser/network-cache.js +8 -8
  118. package/dist/src/browser/network-cache.test.js +4 -4
  119. package/dist/src/browser/page.d.ts +8 -7
  120. package/dist/src/browser/page.js +23 -16
  121. package/dist/src/browser/page.test.js +60 -30
  122. package/dist/src/build-manifest.js +1 -1
  123. package/dist/src/cli.js +60 -162
  124. package/dist/src/cli.test.js +178 -197
  125. package/dist/src/commanderAdapter.js +2 -0
  126. package/dist/src/discovery.js +1 -1
  127. package/dist/src/doctor.d.ts +0 -4
  128. package/dist/src/doctor.js +8 -72
  129. package/dist/src/doctor.test.js +26 -97
  130. package/dist/src/execution.d.ts +1 -0
  131. package/dist/src/execution.js +20 -21
  132. package/dist/src/execution.test.js +27 -31
  133. package/dist/src/help.js +7 -1
  134. package/dist/src/main.js +0 -19
  135. package/dist/src/manifest-types.d.ts +2 -4
  136. package/dist/src/observation/artifact.js +1 -1
  137. package/dist/src/observation/artifact.test.js +3 -3
  138. package/dist/src/observation/events.d.ts +1 -1
  139. package/dist/src/observation/manager.js +1 -1
  140. package/dist/src/observation/manager.test.js +3 -3
  141. package/dist/src/registry-api.d.ts +1 -1
  142. package/dist/src/registry.d.ts +3 -12
  143. package/dist/src/registry.js +6 -10
  144. package/dist/src/runtime.d.ts +7 -2
  145. package/dist/src/runtime.js +3 -1
  146. package/dist/src/serialization.d.ts +1 -1
  147. package/dist/src/serialization.js +1 -1
  148. package/dist/src/types.d.ts +0 -15
  149. package/package.json +1 -1
@@ -51,6 +51,7 @@ export function registerCommandToProgram(siteCmd, cmd) {
51
51
  if (cmd.browser) {
52
52
  subCmd
53
53
  .option('--window <mode>', 'Browser window mode: foreground or background')
54
+ .option('--site-session <mode>', 'Adapter site session lifecycle: ephemeral or persistent')
54
55
  .option('--keep-tab <bool>', 'Keep the browser tab lease after the command finishes');
55
56
  }
56
57
  const originalHelpInformation = subCmd.helpInformation.bind(subCmd);
@@ -108,6 +109,7 @@ export function registerCommandToProgram(siteCmd, cmd) {
108
109
  ...(typeof globals.profile === 'string' && globals.profile.trim() ? { profile: globals.profile.trim() } : {}),
109
110
  ...(typeof optionsRecord.trace === 'string' && optionsRecord.trace !== 'off' ? { trace: optionsRecord.trace } : {}),
110
111
  ...(cmd.browser && typeof optionsRecord.window === 'string' ? { windowMode: optionsRecord.window } : {}),
112
+ ...(cmd.browser && typeof optionsRecord.siteSession === 'string' ? { siteSession: optionsRecord.siteSession } : {}),
111
113
  ...(cmd.browser && typeof optionsRecord.keepTab === 'string' ? { keepTab: optionsRecord.keepTab } : {}),
112
114
  });
113
115
  if (result === null || result === undefined) {
@@ -135,7 +135,7 @@ async function loadFromManifest(manifestPath, clisDir) {
135
135
  pipeline: entry.pipeline,
136
136
  source: entry.sourceFile ? path.resolve(clisDir, entry.sourceFile) : modulePath,
137
137
  navigateBefore: entry.navigateBefore,
138
- browserSession: entry.browserSession,
138
+ siteSession: entry.siteSession,
139
139
  _lazy: true,
140
140
  _modulePath: modulePath,
141
141
  };
@@ -3,13 +3,10 @@
3
3
  *
4
4
  * Simplified for the daemon-based architecture.
5
5
  */
6
- import type { BrowserSessionInfo } from './types.js';
7
6
  import type { BrowserProfileStatus } from './browser/daemon-client.js';
8
7
  import { type AdapterShadow } from './adapter-shadow.js';
9
8
  export type DoctorOptions = {
10
9
  yes?: boolean;
11
- live?: boolean;
12
- sessions?: boolean;
13
10
  cliVersion?: string;
14
11
  };
15
12
  export type ConnectivityResult = {
@@ -28,7 +25,6 @@ export type DoctorReport = {
28
25
  extensionVersion?: string;
29
26
  latestExtensionVersion?: string;
30
27
  connectivity?: ConnectivityResult;
31
- sessions?: BrowserSessionInfo[];
32
28
  profiles?: BrowserProfileStatus[];
33
29
  adapterShadows?: AdapterShadow[];
34
30
  issues: string[];
@@ -6,7 +6,7 @@
6
6
  import { styleText } from 'node:util';
7
7
  import { DEFAULT_DAEMON_PORT } from './constants.js';
8
8
  import { BrowserBridge } from './browser/index.js';
9
- import { getDaemonHealth, listSessions } from './browser/daemon-client.js';
9
+ import { getDaemonHealth } from './browser/daemon-client.js';
10
10
  import { getErrorMessage } from './errors.js';
11
11
  import { getRuntimeLabel } from './runtime-detect.js';
12
12
  import { getCachedLatestExtensionVersion } from './update-check.js';
@@ -66,47 +66,17 @@ export async function checkConnectivity(opts) {
66
66
  }
67
67
  }
68
68
  export async function runBrowserDoctor(opts = {}) {
69
- // Live connectivity check doubles as auto-start (bridge.connect spawns daemon).
70
- let connectivity;
71
- if (opts.live) {
72
- connectivity = await checkConnectivity();
73
- }
74
- else {
75
- // No live probe — daemon may have idle-exited. Do a minimal auto-start
76
- // so we don't misreport a lazy-lifecycle stop as a real failure.
77
- const initialHealth = await getDaemonHealth();
78
- if (initialHealth.state === 'stopped') {
79
- try {
80
- const bridge = new BrowserBridge();
81
- await bridge.connect({ timeout: 5 });
82
- await bridge.close();
83
- }
84
- catch {
85
- // Auto-start failed; we'll report it below.
86
- }
87
- }
88
- }
89
- // Single status read *after* all side-effects (live check / auto-start) settle.
69
+ // Live connectivity check is the core of doctor — it doubles as auto-start
70
+ // (bridge.connect spawns daemon) and validates end-to-end browser bridge health.
71
+ const connectivity = await checkConnectivity();
72
+ // Single status read *after* connectivity side-effects settle.
90
73
  const health = await getDaemonHealth();
91
74
  const daemonRunning = health.state !== 'stopped';
92
75
  const extensionConnected = health.state === 'ready';
93
- const daemonFlaky = !!(connectivity?.ok && !daemonRunning);
94
- const extensionFlaky = !!(connectivity?.ok && daemonRunning && !extensionConnected);
76
+ const daemonFlaky = connectivity.ok && !daemonRunning;
77
+ const extensionFlaky = connectivity.ok && daemonRunning && !extensionConnected;
95
78
  const daemonStale = isDaemonStale(health.status, opts.cliVersion);
96
79
  const profiles = health.status?.profiles;
97
- let sessions;
98
- if (opts.sessions) {
99
- if (profiles && profiles.length > 0) {
100
- const grouped = await Promise.all(profiles.map(async (profile) => {
101
- const rows = await listSessions({ contextId: profile.contextId }).catch(() => []);
102
- return rows.map((row) => ({ ...row, contextId: row.contextId ?? profile.contextId }));
103
- }));
104
- sessions = grouped.flat();
105
- }
106
- else if (health.state === 'ready') {
107
- sessions = await listSessions();
108
- }
109
- }
110
80
  const extensionVersion = health.status?.extensionVersion;
111
81
  const adapterShadows = findShadowedUserAdapters();
112
82
  const issues = [];
@@ -147,7 +117,7 @@ export async function runBrowserDoctor(opts = {}) {
147
117
  ' This usually means an outdated Browser Bridge extension.\n' +
148
118
  ' Reload or reinstall the extension from: https://github.com/jackwener/opencli/releases');
149
119
  }
150
- if (connectivity && !connectivity.ok) {
120
+ if (!connectivity.ok) {
151
121
  issues.push(`Browser connectivity test failed: ${connectivity.error ?? 'unknown'}`);
152
122
  }
153
123
  const extensionCompatRange = health.status?.extensionCompatRange;
@@ -187,7 +157,6 @@ export async function runBrowserDoctor(opts = {}) {
187
157
  extensionVersion,
188
158
  latestExtensionVersion,
189
159
  connectivity,
190
- sessions,
191
160
  profiles,
192
161
  adapterShadows,
193
162
  issues,
@@ -244,39 +213,6 @@ export function renderBrowserDoctorReport(report) {
244
213
  : `failed (${report.connectivity.error ?? 'unknown'})`;
245
214
  lines.push(`${connIcon} Connectivity: ${detail}`);
246
215
  }
247
- else {
248
- lines.push(`${styleText('dim', '[SKIP]')} Connectivity: skipped (--no-live)`);
249
- }
250
- if (report.sessions) {
251
- lines.push('', styleText('bold', 'Sessions:'));
252
- if (report.sessions.length === 0) {
253
- lines.push(styleText('dim', ' • no active automation sessions'));
254
- }
255
- else {
256
- const byContext = new Map();
257
- for (const session of report.sessions) {
258
- const contextId = typeof session.contextId === 'string' && session.contextId ? session.contextId : 'default';
259
- const rows = byContext.get(contextId) ?? [];
260
- rows.push(session);
261
- byContext.set(contextId, rows);
262
- }
263
- for (const [contextId, rows] of byContext) {
264
- if (byContext.size > 1)
265
- lines.push(styleText('dim', ` [profile: ${contextId}]`));
266
- for (const session of rows) {
267
- const idle = session.idleMsRemaining == null
268
- ? 'none'
269
- : `${Math.ceil(session.idleMsRemaining / 1000)}s`;
270
- const target = session.preferredTabId != null
271
- ? `tab ${session.preferredTabId}`
272
- : `window ${session.windowId ?? 'unknown'}`;
273
- const mode = session.ownership ?? (session.owned === false ? 'borrowed' : 'owned');
274
- const windowRole = session.windowRole ? `, window=${session.windowRole}` : '';
275
- lines.push(styleText('dim', ` • ${session.workspace ?? 'default'} → ${target}, mode=${mode}${windowRole}, tabs=${session.tabCount ?? 0}, idle=${idle}`));
276
- }
277
- }
278
- }
279
- }
280
216
  if (report.issues.length) {
281
217
  lines.push('', styleText('yellow', 'Issues:'));
282
218
  for (const issue of report.issues) {
@@ -1,14 +1,12 @@
1
1
  import { beforeEach, describe, expect, it, vi } from 'vitest';
2
- const { mockGetDaemonHealth, mockListSessions, mockConnect, mockClose, mockFindShadowedUserAdapters } = vi.hoisted(() => ({
2
+ const { mockGetDaemonHealth, mockConnect, mockClose, mockFindShadowedUserAdapters } = vi.hoisted(() => ({
3
3
  mockGetDaemonHealth: vi.fn(),
4
- mockListSessions: vi.fn(),
5
4
  mockConnect: vi.fn(),
6
5
  mockClose: vi.fn(),
7
6
  mockFindShadowedUserAdapters: vi.fn(),
8
7
  }));
9
8
  vi.mock('./browser/daemon-client.js', () => ({
10
9
  getDaemonHealth: mockGetDaemonHealth,
11
- listSessions: mockListSessions,
12
10
  }));
13
11
  vi.mock('./browser/index.js', () => ({
14
12
  BrowserBridge: class {
@@ -29,6 +27,12 @@ describe('doctor report rendering', () => {
29
27
  beforeEach(() => {
30
28
  vi.clearAllMocks();
31
29
  mockFindShadowedUserAdapters.mockReturnValue([]);
30
+ // Doctor always runs live connectivity. Tests that want connect to fail override.
31
+ mockConnect.mockResolvedValue({
32
+ evaluate: vi.fn().mockResolvedValue(2),
33
+ closeWindow: vi.fn().mockResolvedValue(undefined),
34
+ });
35
+ mockClose.mockResolvedValue(undefined);
32
36
  });
33
37
  it('renders OK-style report when daemon and extension connected', () => {
34
38
  const text = strip(renderBrowserDoctorReport({
@@ -97,34 +101,7 @@ describe('doctor report rendering', () => {
97
101
  }));
98
102
  expect(text).toContain('[OK] Connectivity: connected in 1.2s');
99
103
  });
100
- it('renders connectivity SKIP when not tested', () => {
101
- const text = strip(renderBrowserDoctorReport({
102
- daemonRunning: true,
103
- extensionConnected: true,
104
- issues: [],
105
- }));
106
- expect(text).toContain('[SKIP] Connectivity: skipped (--no-live)');
107
- });
108
- it('renders sessions with tab leases and no idle timer', () => {
109
- const text = strip(renderBrowserDoctorReport({
110
- daemonRunning: true,
111
- extensionConnected: true,
112
- issues: [],
113
- sessions: [
114
- {
115
- workspace: 'bound:default',
116
- windowId: 2,
117
- preferredTabId: 42,
118
- ownership: 'borrowed',
119
- windowRole: 'borrowed-user',
120
- tabCount: 1,
121
- idleMsRemaining: null,
122
- },
123
- ],
124
- }));
125
- expect(text).toContain('bound:default → tab 42, mode=borrowed, window=borrowed-user, tabs=1, idle=none');
126
- });
127
- it('renders connected profiles and groups sessions by profile', () => {
104
+ it('renders connected profiles when multiple are present', () => {
128
105
  const text = strip(renderBrowserDoctorReport({
129
106
  daemonRunning: true,
130
107
  extensionConnected: false,
@@ -133,35 +110,10 @@ describe('doctor report rendering', () => {
133
110
  { contextId: 'personal', extensionConnected: true, extensionVersion: '1.2.3', pending: 0 },
134
111
  ],
135
112
  issues: [],
136
- sessions: [
137
- {
138
- contextId: 'work',
139
- workspace: 'bound:default',
140
- windowId: 2,
141
- preferredTabId: 42,
142
- ownership: 'borrowed',
143
- windowRole: 'borrowed-user',
144
- tabCount: 1,
145
- idleMsRemaining: null,
146
- },
147
- {
148
- contextId: 'personal',
149
- workspace: 'site:foo',
150
- windowId: 1,
151
- preferredTabId: 10,
152
- ownership: 'owned',
153
- windowRole: 'automation',
154
- tabCount: 1,
155
- idleMsRemaining: 1000,
156
- },
157
- ],
158
113
  }));
159
114
  expect(text).toContain('Profiles:');
160
115
  expect(text).toContain('work: connected v1.2.3');
161
- expect(text).toContain('[profile: work]');
162
- expect(text).toContain('[profile: personal]');
163
- expect(text).toContain('bound:default → tab 42');
164
- expect(text).toContain('site:foo → tab 10');
116
+ expect(text).toContain('personal: connected v1.2.3');
165
117
  });
166
118
  it('renders unstable extension state when live connectivity and status disagree', () => {
167
119
  const text = strip(renderBrowserDoctorReport({
@@ -185,25 +137,20 @@ describe('doctor report rendering', () => {
185
137
  expect(text).toContain('[WARN] Daemon: unstable');
186
138
  expect(text).toContain('Daemon connectivity is unstable.');
187
139
  });
188
- it('reports daemon not running when no-live and auto-start fails', async () => {
189
- mockGetDaemonHealth.mockResolvedValueOnce({ state: 'stopped', status: null });
140
+ it('reports daemon not running when connectivity fails and daemon stays stopped', async () => {
190
141
  mockConnect.mockRejectedValueOnce(new Error('Could not start daemon'));
191
142
  mockGetDaemonHealth.mockResolvedValueOnce({ state: 'stopped', status: null });
192
- const report = await runBrowserDoctor({ live: false });
143
+ const report = await runBrowserDoctor();
193
144
  expect(report.daemonRunning).toBe(false);
194
145
  expect(report.extensionConnected).toBe(false);
195
- expect(mockGetDaemonHealth).toHaveBeenCalledTimes(2);
146
+ expect(report.connectivity?.ok).toBe(false);
196
147
  expect(report.issues).toEqual(expect.arrayContaining([
197
148
  expect.stringContaining('Daemon is not running'),
198
149
  ]));
199
150
  });
200
151
  it('reports flapping when live check succeeds but final status shows extension disconnected', async () => {
201
- mockConnect.mockResolvedValueOnce({
202
- evaluate: vi.fn().mockResolvedValue(2),
203
- });
204
- mockClose.mockResolvedValueOnce(undefined);
205
152
  mockGetDaemonHealth.mockResolvedValueOnce({ state: 'no-extension', status: { extensionConnected: false } });
206
- const report = await runBrowserDoctor({ live: true });
153
+ const report = await runBrowserDoctor();
207
154
  expect(report.daemonRunning).toBe(true);
208
155
  expect(report.extensionConnected).toBe(false);
209
156
  expect(report.extensionFlaky).toBe(true);
@@ -212,12 +159,8 @@ describe('doctor report rendering', () => {
212
159
  ]));
213
160
  });
214
161
  it('reports daemon flapping when live check succeeds but daemon disappears afterward', async () => {
215
- mockConnect.mockResolvedValueOnce({
216
- evaluate: vi.fn().mockResolvedValue(2),
217
- });
218
- mockClose.mockResolvedValueOnce(undefined);
219
162
  mockGetDaemonHealth.mockResolvedValueOnce({ state: 'stopped', status: null });
220
- const report = await runBrowserDoctor({ live: true });
163
+ const report = await runBrowserDoctor();
221
164
  expect(report.daemonRunning).toBe(false);
222
165
  expect(report.daemonFlaky).toBe(true);
223
166
  expect(report.extensionConnected).toBe(false);
@@ -235,20 +178,11 @@ describe('doctor report rendering', () => {
235
178
  closeWindow,
236
179
  };
237
180
  });
238
- mockClose.mockResolvedValueOnce(undefined);
239
181
  mockGetDaemonHealth.mockResolvedValueOnce({ state: 'ready', status: { extensionConnected: true } });
240
- await runBrowserDoctor({ live: true });
182
+ await runBrowserDoctor();
241
183
  expect(timeoutSeen).toBe(8);
242
184
  expect(closeWindow).toHaveBeenCalledTimes(1);
243
185
  });
244
- it('skips auto-start in no-live mode when daemon is already running', async () => {
245
- mockGetDaemonHealth.mockResolvedValueOnce({ state: 'no-extension', status: { extensionConnected: false } });
246
- mockGetDaemonHealth.mockResolvedValueOnce({ state: 'no-extension', status: { extensionConnected: false } });
247
- const report = await runBrowserDoctor({ live: false });
248
- expect(mockConnect).not.toHaveBeenCalled();
249
- expect(report.daemonRunning).toBe(true);
250
- expect(report.extensionConnected).toBe(false);
251
- });
252
186
  it('reports an issue when the extension is connected but does not report a version', async () => {
253
187
  const status = {
254
188
  state: 'ready',
@@ -257,10 +191,8 @@ describe('doctor report rendering', () => {
257
191
  extensionVersion: undefined,
258
192
  },
259
193
  };
260
- mockGetDaemonHealth
261
- .mockResolvedValueOnce(status)
262
- .mockResolvedValueOnce(status);
263
- const report = await runBrowserDoctor({ live: false });
194
+ mockGetDaemonHealth.mockResolvedValue(status);
195
+ const report = await runBrowserDoctor();
264
196
  expect(report.issues).toEqual(expect.arrayContaining([
265
197
  expect.stringContaining('did not report a version'),
266
198
  ]));
@@ -274,10 +206,8 @@ describe('doctor report rendering', () => {
274
206
  extensionVersion: '1.0.3',
275
207
  },
276
208
  };
277
- mockGetDaemonHealth
278
- .mockResolvedValueOnce(status)
279
- .mockResolvedValueOnce(status);
280
- const report = await runBrowserDoctor({ live: false, cliVersion: '1.7.9' });
209
+ mockGetDaemonHealth.mockResolvedValue(status);
210
+ const report = await runBrowserDoctor({ cliVersion: '1.7.9' });
281
211
  expect(report.daemonStale).toBe(true);
282
212
  expect(report.issues).toEqual(expect.arrayContaining([
283
213
  expect.stringContaining('Stale daemon detected: daemon v1.7.6 != CLI v1.7.9'),
@@ -292,9 +222,7 @@ describe('doctor report rendering', () => {
292
222
  extensionVersion: '1.0.3',
293
223
  },
294
224
  };
295
- mockGetDaemonHealth
296
- .mockResolvedValueOnce(status)
297
- .mockResolvedValueOnce(status);
225
+ mockGetDaemonHealth.mockResolvedValue(status);
298
226
  mockFindShadowedUserAdapters.mockReturnValueOnce([
299
227
  {
300
228
  name: 'instagram/saved',
@@ -302,7 +230,7 @@ describe('doctor report rendering', () => {
302
230
  builtinPath: '/pkg/clis/instagram/saved.js',
303
231
  },
304
232
  ]);
305
- const report = await runBrowserDoctor({ live: false, cliVersion: '1.7.9' });
233
+ const report = await runBrowserDoctor({ cliVersion: '1.7.9' });
306
234
  expect(report.adapterShadows).toHaveLength(1);
307
235
  expect(report.issues).toEqual(expect.arrayContaining([
308
236
  expect.stringContaining('Local adapter overrides shadow packaged adapters'),
@@ -320,10 +248,11 @@ describe('doctor report rendering', () => {
320
248
  ],
321
249
  },
322
250
  };
323
- mockGetDaemonHealth
324
- .mockResolvedValueOnce(status)
325
- .mockResolvedValueOnce(status);
326
- const report = await runBrowserDoctor({ live: false });
251
+ mockGetDaemonHealth.mockResolvedValue(status);
252
+ // Real connectivity would fail in profile-required state; force it here so
253
+ // the test exercises the profile-required issue path, not the flaky path.
254
+ mockConnect.mockRejectedValueOnce(new Error('profile required'));
255
+ const report = await runBrowserDoctor();
327
256
  expect(report.profiles).toHaveLength(2);
328
257
  expect(report.issues).toEqual(expect.arrayContaining([
329
258
  expect.stringContaining('Multiple Chrome profiles are connected'),
@@ -18,6 +18,7 @@ export declare function executeCommand(cmd: CliCommand, rawKwargs: CommandArgs,
18
18
  trace?: string;
19
19
  keepTab?: string;
20
20
  windowMode?: string;
21
+ siteSession?: string;
21
22
  onTraceExport?: (trace: ObservationExportResult) => void;
22
23
  }): Promise<unknown>;
23
24
  export declare function prepareCommandArgs(cmd: CliCommand, rawKwargs: CommandArgs): CommandArgs;
@@ -29,7 +29,6 @@ const _loadedModules = new Map();
29
29
  /** Track mtime of loaded user adapter files for hot-reload in daemon mode. */
30
30
  const _moduleMtimes = new Map();
31
31
  const _userClisDir = `${os.homedir()}/.opencli/clis/`;
32
- const INTERACTIVE_BROWSER_IDLE_TIMEOUT_SECONDS = 600;
33
32
  function normalizeTraceMode(raw) {
34
33
  if (raw === undefined || raw === null || raw === '' || raw === 'off')
35
34
  return 'off';
@@ -164,8 +163,8 @@ function isDomainRootPreNav(preNavUrl, domain) {
164
163
  return false;
165
164
  }
166
165
  }
167
- async function shouldRunPreNav(cmd, page, reuse, preNavUrl) {
168
- if (reuse !== 'site' || !cmd.domain)
166
+ async function shouldRunPreNav(cmd, page, siteSession, preNavUrl) {
167
+ if (siteSession !== 'persistent' || !cmd.domain)
169
168
  return true;
170
169
  if (!isDomainRootPreNav(preNavUrl, cmd.domain))
171
170
  return true;
@@ -212,10 +211,9 @@ export async function executeCommand(cmd, rawKwargs, debug = false, opts = {}) {
212
211
  const BrowserFactory = getBrowserFactory(cmd.site);
213
212
  const contextId = resolveProfileContextId(opts.profile);
214
213
  const internal = cmd;
215
- const browserReuse = resolveBrowserSessionReuse(cmd);
216
- const workspace = resolveBrowserWorkspace(cmd, browserReuse);
217
- const idleTimeout = browserReuse === 'site' ? INTERACTIVE_BROWSER_IDLE_TIMEOUT_SECONDS : undefined;
218
- const keepTab = resolveKeepTab(browserReuse, opts.keepTab);
214
+ const siteSession = resolveSiteSession(cmd, opts.siteSession);
215
+ const session = resolveAdapterBrowserSession(cmd, siteSession);
216
+ const keepTab = resolveKeepTab(siteSession, opts.keepTab);
219
217
  const windowMode = resolveBrowserWindowMode('background', opts.windowMode);
220
218
  result = await browserSession(BrowserFactory, async (page) => {
221
219
  const observation = traceMode === 'off'
@@ -223,7 +221,7 @@ export async function executeCommand(cmd, rawKwargs, debug = false, opts = {}) {
223
221
  : new ObservationSession({
224
222
  scope: {
225
223
  contextId,
226
- workspace,
224
+ session,
227
225
  target: page.getActivePage?.(),
228
226
  site: cmd.site,
229
227
  command: fullName(cmd),
@@ -240,7 +238,7 @@ export async function executeCommand(cmd, rawKwargs, debug = false, opts = {}) {
240
238
  await page.startNetworkCapture?.().catch(() => false);
241
239
  }
242
240
  const preNavUrl = resolvePreNav(cmd);
243
- if (preNavUrl && await shouldRunPreNav(cmd, page, browserReuse, preNavUrl)) {
241
+ if (preNavUrl && await shouldRunPreNav(cmd, page, siteSession, preNavUrl)) {
244
242
  observation?.record({
245
243
  stream: 'action',
246
244
  name: 'pre_navigate',
@@ -332,7 +330,7 @@ export async function executeCommand(cmd, rawKwargs, debug = false, opts = {}) {
332
330
  await page.closeWindow?.().catch(() => { });
333
331
  throw err;
334
332
  }
335
- }, { workspace, cdpEndpoint, contextId, idleTimeout, windowMode });
333
+ }, { session, cdpEndpoint, contextId, windowMode, surface: 'adapter', siteSession });
336
334
  }
337
335
  else {
338
336
  // Non-browser commands: enforce a timeout only when the command exposes
@@ -442,19 +440,18 @@ export function prepareCommandArgs(cmd, rawKwargs) {
442
440
  * the runtime kills the Promise.
443
441
  */
444
442
  const RUNTIME_TIMEOUT_PADDING_SECONDS = 30;
445
- function readEnvBrowserSessionReuse() {
446
- const raw = process.env.OPENCLI_BROWSER_REUSE;
447
- if (raw === undefined || raw === '')
443
+ function normalizeSiteSession(raw) {
444
+ if (raw === undefined || raw === null || raw === '')
448
445
  return null;
449
- if (raw === 'none' || raw === 'site')
446
+ if (raw === 'ephemeral' || raw === 'persistent')
450
447
  return raw;
451
- throw new ArgumentError(`--reuse must be one of: none, site. Received: "${raw}"`);
448
+ throw new ArgumentError(`--site-session must be one of: ephemeral, persistent. Received: "${String(raw)}"`);
452
449
  }
453
- function resolveBrowserSessionReuse(cmd) {
454
- return readEnvBrowserSessionReuse() ?? cmd.browserSession?.reuse ?? 'none';
450
+ function resolveSiteSession(cmd, rawOption) {
451
+ return normalizeSiteSession(rawOption) ?? cmd.siteSession ?? 'ephemeral';
455
452
  }
456
- function resolveBrowserWorkspace(cmd, reuse) {
457
- if (reuse === 'site')
453
+ function resolveAdapterBrowserSession(cmd, siteSession) {
454
+ if (siteSession === 'persistent')
458
455
  return `site:${cmd.site}`;
459
456
  return `site:${cmd.site}:${crypto.randomUUID()}`;
460
457
  }
@@ -467,10 +464,12 @@ function normalizeBooleanOption(name, raw) {
467
464
  return false;
468
465
  throw new ArgumentError(`${name} must be one of: true, false. Received: "${String(raw)}"`);
469
466
  }
470
- function resolveKeepTab(reuse, rawOption) {
467
+ function resolveKeepTab(siteSession, rawOption) {
468
+ if (siteSession === 'persistent')
469
+ return true;
471
470
  return normalizeBooleanOption('--keep-tab', rawOption)
472
471
  ?? normalizeBooleanOption('OPENCLI_KEEP_TAB', process.env.OPENCLI_KEEP_TAB)
473
- ?? reuse !== 'none';
472
+ ?? false;
474
473
  }
475
474
  function normalizeWindowMode(name, raw) {
476
475
  if (raw === undefined || raw === '')
@@ -136,7 +136,7 @@ describe('executeCommand — non-browser timeout', () => {
136
136
  });
137
137
  vi.restoreAllMocks();
138
138
  });
139
- it('reuses a site-scoped browser workspace and keeps the tab lease open', async () => {
139
+ it('reuses a persistent site browser session and keeps the tab lease open', async () => {
140
140
  const closeWindow = vi.fn().mockResolvedValue(undefined);
141
141
  const mockPage = { closeWindow };
142
142
  const sessionOpts = [];
@@ -147,22 +147,24 @@ describe('executeCommand — non-browser timeout', () => {
147
147
  });
148
148
  const cmd = cli({
149
149
  site: 'test-execution',
150
- name: 'browser-reuse-site', access: 'read',
151
- description: 'test site-scoped browser reuse',
150
+ name: 'site-session-persistent', access: 'read',
151
+ description: 'test persistent site session',
152
152
  browser: true,
153
153
  strategy: Strategy.PUBLIC,
154
- browserSession: { reuse: 'site' },
154
+ siteSession: 'persistent',
155
155
  func: async () => [{ ok: true }],
156
156
  });
157
157
  await executeCommand(cmd, {});
158
- await executeCommand(cmd, {});
158
+ await executeCommand(cmd, {}, false, { keepTab: 'false' });
159
159
  expect(sessionOpts).toHaveLength(2);
160
- expect(sessionOpts[0]).toMatchObject({ workspace: 'site:test-execution', idleTimeout: 600, windowMode: 'background' });
161
- expect(sessionOpts[1]).toMatchObject({ workspace: 'site:test-execution', idleTimeout: 600, windowMode: 'background' });
160
+ expect(sessionOpts[0]).toMatchObject({ session: 'site:test-execution', windowMode: 'background', siteSession: 'persistent' });
161
+ expect(sessionOpts[1]).toMatchObject({ session: 'site:test-execution', windowMode: 'background', siteSession: 'persistent' });
162
+ expect(sessionOpts[0]?.idleTimeout).toBeUndefined();
163
+ expect(sessionOpts[1]?.idleTimeout).toBeUndefined();
162
164
  expect(closeWindow).not.toHaveBeenCalled();
163
165
  vi.restoreAllMocks();
164
166
  });
165
- it('keeps default browser commands on one-shot workspaces', async () => {
167
+ it('keeps default browser commands on one-shot adapter sessions', async () => {
166
168
  const closeWindow = vi.fn().mockResolvedValue(undefined);
167
169
  const mockPage = { closeWindow };
168
170
  const sessionOpts = [];
@@ -173,8 +175,8 @@ describe('executeCommand — non-browser timeout', () => {
173
175
  });
174
176
  const cmd = cli({
175
177
  site: 'test-execution',
176
- name: 'browser-reuse-default', access: 'read',
177
- description: 'test default one-shot browser workspace',
178
+ name: 'site-session-default', access: 'read',
179
+ description: 'test default one-shot browser session',
178
180
  browser: true,
179
181
  strategy: Strategy.PUBLIC,
180
182
  func: async () => [{ ok: true }],
@@ -182,9 +184,9 @@ describe('executeCommand — non-browser timeout', () => {
182
184
  await executeCommand(cmd, {});
183
185
  await executeCommand(cmd, {});
184
186
  expect(sessionOpts).toHaveLength(2);
185
- expect(sessionOpts[0]?.workspace).toMatch(/^site:test-execution:/);
186
- expect(sessionOpts[1]?.workspace).toMatch(/^site:test-execution:/);
187
- expect(sessionOpts[0]?.workspace).not.toBe(sessionOpts[1]?.workspace);
187
+ expect(sessionOpts[0]?.session).toMatch(/^site:test-execution:/);
188
+ expect(sessionOpts[1]?.session).toMatch(/^site:test-execution:/);
189
+ expect(sessionOpts[0]?.session).not.toBe(sessionOpts[1]?.session);
188
190
  expect(sessionOpts[0]?.idleTimeout).toBeUndefined();
189
191
  expect(sessionOpts[1]?.idleTimeout).toBeUndefined();
190
192
  expect(sessionOpts[0]?.windowMode).toBe('background');
@@ -192,7 +194,7 @@ describe('executeCommand — non-browser timeout', () => {
192
194
  expect(closeWindow).toHaveBeenCalledTimes(2);
193
195
  vi.restoreAllMocks();
194
196
  });
195
- it('lets user --reuse none override adapter reuse metadata', async () => {
197
+ it('lets user --site-session ephemeral override adapter persistent metadata', async () => {
196
198
  const closeWindow = vi.fn().mockResolvedValue(undefined);
197
199
  const mockPage = { closeWindow };
198
200
  const sessionOpts = [];
@@ -201,33 +203,27 @@ describe('executeCommand — non-browser timeout', () => {
201
203
  sessionOpts.push(opts ?? {});
202
204
  return fn(mockPage);
203
205
  });
204
- const prev = process.env.OPENCLI_BROWSER_REUSE;
205
- process.env.OPENCLI_BROWSER_REUSE = 'none';
206
206
  try {
207
207
  const cmd = cli({
208
208
  site: 'test-execution',
209
- name: 'browser-reuse-override-none', access: 'read',
210
- description: 'test user reuse override',
209
+ name: 'site-session-override-ephemeral', access: 'read',
210
+ description: 'test user site-session override',
211
211
  browser: true,
212
212
  strategy: Strategy.PUBLIC,
213
- browserSession: { reuse: 'site' },
213
+ siteSession: 'persistent',
214
214
  func: async () => [{ ok: true }],
215
215
  });
216
- await executeCommand(cmd, {});
216
+ await executeCommand(cmd, {}, false, { siteSession: 'ephemeral' });
217
217
  expect(sessionOpts).toHaveLength(1);
218
- expect(sessionOpts[0]?.workspace).toMatch(/^site:test-execution:/);
218
+ expect(sessionOpts[0]?.session).toMatch(/^site:test-execution:/);
219
219
  expect(sessionOpts[0]?.idleTimeout).toBeUndefined();
220
220
  expect(closeWindow).toHaveBeenCalledTimes(1);
221
221
  }
222
222
  finally {
223
- if (prev === undefined)
224
- delete process.env.OPENCLI_BROWSER_REUSE;
225
- else
226
- process.env.OPENCLI_BROWSER_REUSE = prev;
227
223
  vi.restoreAllMocks();
228
224
  }
229
225
  });
230
- it('skips repeated domain pre-navigation for site-reused browser sessions', async () => {
226
+ it('skips repeated domain pre-navigation for persistent site sessions', async () => {
231
227
  const closeWindow = vi.fn().mockResolvedValue(undefined);
232
228
  const goto = vi.fn().mockResolvedValue(undefined);
233
229
  const mockPage = {
@@ -239,12 +235,12 @@ describe('executeCommand — non-browser timeout', () => {
239
235
  vi.spyOn(runtime, 'browserSession').mockImplementation(async (_Factory, fn) => fn(mockPage));
240
236
  const cmd = cli({
241
237
  site: 'test-execution',
242
- name: 'browser-reuse-skip-prenav', access: 'read',
238
+ name: 'site-session-skip-prenav', access: 'read',
243
239
  description: 'test reused same-domain tabs do not reset conversation state',
244
240
  browser: true,
245
241
  strategy: Strategy.COOKIE,
246
242
  domain: 'grok.com',
247
- browserSession: { reuse: 'site' },
243
+ siteSession: 'persistent',
248
244
  func: async () => [{ ok: true }],
249
245
  });
250
246
  await executeCommand(cmd, {});
@@ -252,7 +248,7 @@ describe('executeCommand — non-browser timeout', () => {
252
248
  expect(closeWindow).not.toHaveBeenCalled();
253
249
  vi.restoreAllMocks();
254
250
  });
255
- it('keeps explicit path pre-navigation for site-reused browser sessions', async () => {
251
+ it('keeps explicit path pre-navigation for persistent site sessions', async () => {
256
252
  const closeWindow = vi.fn().mockResolvedValue(undefined);
257
253
  const goto = vi.fn().mockResolvedValue(undefined);
258
254
  const mockPage = {
@@ -264,13 +260,13 @@ describe('executeCommand — non-browser timeout', () => {
264
260
  vi.spyOn(runtime, 'browserSession').mockImplementation(async (_Factory, fn) => fn(mockPage));
265
261
  const cmd = cli({
266
262
  site: 'test-execution',
267
- name: 'browser-reuse-path-prenav', access: 'read',
263
+ name: 'site-session-path-prenav', access: 'read',
268
264
  description: 'test explicit path pre-navigation still runs',
269
265
  browser: true,
270
266
  strategy: Strategy.COOKIE,
271
267
  domain: 'example.com',
272
268
  navigateBefore: 'https://example.com/dashboard',
273
- browserSession: { reuse: 'site' },
269
+ siteSession: 'persistent',
274
270
  func: async () => [{ ok: true }],
275
271
  });
276
272
  await executeCommand(cmd, {});