@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
@@ -2,7 +2,7 @@
2
2
  * YouTube unsubscribe — unsubscribe from a channel via InnerTube subscription API.
3
3
  */
4
4
  import { cli, Strategy } from '@jackwener/opencli/registry';
5
- import { prepareYoutubeApiPage, SAPISID_HASH_FN, RESOLVE_CHANNEL_HANDLE_FN } from './utils.js';
5
+ import { prepareYoutubeApiPage, readYoutubeSapisid, SAPISID_HASH_FN, RESOLVE_CHANNEL_HANDLE_FN } from './utils.js';
6
6
  import { CommandExecutionError, AuthRequiredError } from '@jackwener/opencli/errors';
7
7
 
8
8
  cli({
@@ -19,6 +19,10 @@ cli({
19
19
  func: async (page, kwargs) => {
20
20
  const channelInput = String(kwargs.channel);
21
21
  await prepareYoutubeApiPage(page);
22
+ // Read SAPISID directly from the cookie store via CDP — zero document.cookie round-trip
23
+ const sapisid = await readYoutubeSapisid(page);
24
+ if (!sapisid)
25
+ throw new AuthRequiredError('www.youtube.com', 'Not logged in (SAPISID cookie missing)');
22
26
  const result = await page.evaluate(`
23
27
  (async () => {
24
28
  ${SAPISID_HASH_FN}
@@ -28,7 +32,7 @@ cli({
28
32
  const context = cfg.INNERTUBE_CONTEXT;
29
33
  if (!apiKey || !context) return { error: 'config', message: 'YouTube config not found' };
30
34
 
31
- const authHash = await getSapisidHash('https://www.youtube.com');
35
+ const authHash = await getSapisidHash(${JSON.stringify(sapisid)}, 'https://www.youtube.com');
32
36
  if (!authHash) return { error: 'auth', message: 'Not logged in (SAPISID cookie missing)' };
33
37
 
34
38
  ${RESOLVE_CHANNEL_HANDLE_FN}
@@ -189,21 +189,13 @@ async function resolveChannelHandle(input, apiKey, context) {
189
189
  * Inline SAPISIDHASH helper for use inside page.evaluate() strings.
190
190
  * YouTube write APIs (like, subscribe) require:
191
191
  * Authorization: SAPISIDHASH {time}_{SHA1(time + " " + SAPISID + " " + origin)}
192
+ *
193
+ * The SAPISID cookie value must be hoisted from the cookie store on the Node side
194
+ * (via `readYoutubeSapisid(page)`) and passed in here — keeps `crypto.subtle.digest`
195
+ * (browser Web Crypto) call site, but no `document.cookie` round-trip.
192
196
  */
193
197
  export const SAPISID_HASH_FN = `
194
- async function getSapisidHash(origin) {
195
- const cookies = document.cookie.split('; ');
196
- let sapisid = '';
197
- for (const c of cookies) {
198
- const eq = c.indexOf('=');
199
- if (eq === -1) continue;
200
- const name = c.slice(0, eq);
201
- const val = c.slice(eq + 1);
202
- if (name === '__Secure-3PAPISID' || name === 'SAPISID') {
203
- sapisid = val;
204
- if (name === '__Secure-3PAPISID') break;
205
- }
206
- }
198
+ async function getSapisidHash(sapisid, origin) {
207
199
  if (!sapisid) return null;
208
200
  const time = Math.floor(Date.now() / 1000);
209
201
  const msgBuffer = new TextEncoder().encode(time + ' ' + sapisid + ' ' + origin);
@@ -212,3 +204,17 @@ async function getSapisidHash(origin) {
212
204
  return 'SAPISIDHASH ' + time + '_' + hashHex;
213
205
  }
214
206
  `;
207
+
208
+ /**
209
+ * Read the YouTube SAPISID cookie via CDP, preferring `__Secure-3PAPISID`
210
+ * (current first-party cookie) and falling back to the legacy `SAPISID` name.
211
+ * Returns the cookie value, or null if neither is present.
212
+ */
213
+ export async function readYoutubeSapisid(page) {
214
+ const cookies = await page.getCookies({ url: 'https://www.youtube.com' });
215
+ return (
216
+ cookies.find((c) => c.name === '__Secure-3PAPISID')?.value
217
+ || cookies.find((c) => c.name === 'SAPISID')?.value
218
+ || null
219
+ );
220
+ }
@@ -1,5 +1,5 @@
1
1
  import { describe, expect, it, vi } from 'vitest';
2
- import { extractJsonAssignmentFromHtml, extractSubscriptionChannel, prepareYoutubeApiPage } from './utils.js';
2
+ import { extractJsonAssignmentFromHtml, extractSubscriptionChannel, prepareYoutubeApiPage, readYoutubeSapisid } from './utils.js';
3
3
  describe('youtube utils', () => {
4
4
  it('extractJsonAssignmentFromHtml parses bootstrap objects with nested braces in strings', () => {
5
5
  const html = `
@@ -34,6 +34,22 @@ describe('youtube utils', () => {
34
34
  expect(page.goto).toHaveBeenCalledWith('https://www.youtube.com', { waitUntil: 'none' });
35
35
  expect(page.wait).toHaveBeenCalledWith(2);
36
36
  });
37
+ it('readYoutubeSapisid reads URL-scoped cookies and prefers secure SAPISID', async () => {
38
+ const page = {
39
+ getCookies: vi.fn().mockResolvedValue([
40
+ { name: 'SAPISID', value: 'legacy' },
41
+ { name: '__Secure-3PAPISID', value: 'secure' },
42
+ ]),
43
+ };
44
+ await expect(readYoutubeSapisid(page)).resolves.toBe('secure');
45
+ expect(page.getCookies).toHaveBeenCalledWith({ url: 'https://www.youtube.com' });
46
+ });
47
+ it('readYoutubeSapisid falls back to legacy SAPISID', async () => {
48
+ const page = {
49
+ getCookies: vi.fn().mockResolvedValue([{ name: 'SAPISID', value: 'legacy' }]),
50
+ };
51
+ await expect(readYoutubeSapisid(page)).resolves.toBe('legacy');
52
+ });
37
53
  it('extractSubscriptionChannel prefers explicit handle and subscriber count fields', () => {
38
54
  expect(extractSubscriptionChannel({
39
55
  title: { simpleText: 'OpenAI' },
@@ -305,7 +305,7 @@ export const askCommand = cli({
305
305
  domain: YUANBAO_DOMAIN,
306
306
  strategy: Strategy.COOKIE,
307
307
  browser: true,
308
- browserSession: { reuse: 'site' },
308
+ siteSession: 'persistent',
309
309
  navigateBefore: false,
310
310
  defaultFormat: 'plain',
311
311
  args: [
@@ -18,7 +18,7 @@ cli({
18
18
  domain: YUANBAO_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
  {
@@ -17,7 +17,7 @@ cli({
17
17
  domain: YUANBAO_DOMAIN,
18
18
  strategy: Strategy.COOKIE,
19
19
  browser: true,
20
- browserSession: { reuse: 'site' },
20
+ siteSession: 'persistent',
21
21
  navigateBefore: false,
22
22
  args: [
23
23
  { name: 'limit', type: 'int', default: 20, help: 'Max conversations to list (sidebar virtual scroll caps actual count)' },
@@ -55,7 +55,7 @@ export const newCommand = cli({
55
55
  domain: YUANBAO_DOMAIN,
56
56
  strategy: Strategy.COOKIE,
57
57
  browser: true,
58
- browserSession: { reuse: 'site' },
58
+ siteSession: 'persistent',
59
59
  navigateBefore: false,
60
60
  args: [],
61
61
  columns: ['Status', 'Action'],
@@ -14,7 +14,7 @@ cli({
14
14
  domain: YUANBAO_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
  columns: ['Role', 'Text'],
@@ -18,7 +18,7 @@ cli({
18
18
  domain: YUANBAO_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', positional: true, required: true, help: 'Prompt to send to Yuanbao' },
@@ -15,7 +15,7 @@ cli({
15
15
  domain: YUANBAO_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', 'ModelId', 'AgentId', 'SessionId', 'Url'],
@@ -14,9 +14,12 @@ export declare class BrowserBridge implements IBrowserFactory {
14
14
  get state(): BrowserBridgeState;
15
15
  connect(opts?: {
16
16
  timeout?: number;
17
- workspace?: string;
17
+ session?: string;
18
18
  idleTimeout?: number;
19
19
  contextId?: string;
20
+ windowMode?: 'foreground' | 'background';
21
+ surface?: 'browser' | 'adapter';
22
+ siteSession?: 'ephemeral' | 'persistent';
20
23
  }): Promise<IPage>;
21
24
  close(): Promise<void>;
22
25
  private _ensureDaemon;
@@ -32,7 +32,9 @@ export class BrowserBridge {
32
32
  try {
33
33
  const contextId = opts.contextId ?? resolveProfileContextId();
34
34
  await this._ensureDaemon(opts.timeout, contextId);
35
- this._page = new Page(opts.workspace, opts.idleTimeout, contextId);
35
+ if (!opts.session?.trim())
36
+ throw new Error('Browser session is required');
37
+ this._page = new Page(opts.session.trim(), opts.idleTimeout, contextId, opts.windowMode, opts.surface, opts.siteSession);
36
38
  this._state = 'connected';
37
39
  return this._page;
38
40
  }
@@ -23,10 +23,13 @@ export declare class CDPBridge implements IBrowserFactory {
23
23
  private _eventListeners;
24
24
  connect(opts?: {
25
25
  timeout?: number;
26
- workspace?: string;
26
+ session?: string;
27
27
  cdpEndpoint?: string;
28
28
  contextId?: string;
29
29
  idleTimeout?: number;
30
+ windowMode?: 'foreground' | 'background';
31
+ surface?: 'browser' | 'adapter';
32
+ siteSession?: 'ephemeral' | 'persistent';
30
33
  }): Promise<IPage>;
31
34
  close(): Promise<void>;
32
35
  send(method: string, params?: Record<string, unknown>, timeoutMs?: number): Promise<unknown>;
@@ -3,20 +3,20 @@
3
3
  *
4
4
  * Provides a typed send() function that posts a Command and returns a Result.
5
5
  */
6
- import type { BrowserSessionInfo } from '../types.js';
7
6
  export interface DaemonCommand {
8
7
  id: string;
9
- action: 'exec' | 'navigate' | 'tabs' | 'cookies' | 'screenshot' | 'close-window' | 'sessions' | 'set-file-input' | 'insert-text' | 'bind' | 'network-capture-start' | 'network-capture-read' | 'wait-download' | 'cdp' | 'frames';
8
+ action: 'exec' | 'navigate' | 'tabs' | 'cookies' | 'screenshot' | 'close-window' | 'set-file-input' | 'insert-text' | 'bind' | 'network-capture-start' | 'network-capture-read' | 'wait-download' | 'cdp' | 'frames';
10
9
  /** Target page identity (targetId). Cross-layer contract with the extension. */
11
10
  page?: string;
12
11
  code?: string;
13
- workspace?: string;
12
+ session?: string;
13
+ surface?: 'browser' | 'adapter';
14
+ /** Adapter site session lifecycle. Persistent site sessions do not idle-expire. */
15
+ siteSession?: 'ephemeral' | 'persistent';
14
16
  url?: string;
15
17
  op?: string;
16
18
  index?: number;
17
19
  domain?: string;
18
- matchDomain?: string;
19
- matchPathPrefix?: string;
20
20
  format?: 'png' | 'jpeg';
21
21
  quality?: number;
22
22
  fullPage?: boolean;
@@ -36,12 +36,10 @@ export interface DaemonCommand {
36
36
  timeoutMs?: number;
37
37
  cdpMethod?: string;
38
38
  cdpParams?: Record<string, unknown>;
39
- /** When true, the owned automation container is created in the foreground */
40
- windowFocused?: boolean;
41
- /** Custom idle timeout in seconds for this workspace session. Overrides the default. */
39
+ /** Window foreground/background policy for owned Browser Bridge containers. */
40
+ windowMode?: 'foreground' | 'background';
41
+ /** Custom idle timeout in seconds for this session. Overrides the default. */
42
42
  idleTimeout?: number;
43
- /** Explicitly allow navigation inside a borrowed bound tab. */
44
- allowBoundNavigation?: boolean;
45
43
  /** Frame index for cross-frame operations (0-based, from 'frames' action) */
46
44
  frameIndex?: number;
47
45
  /** Browser profile/context to route the command to. */
@@ -129,11 +127,6 @@ export declare function sendCommandFull(action: DaemonCommand['action'], params?
129
127
  data: unknown;
130
128
  page?: string;
131
129
  }>;
132
- export declare function listSessions(opts?: {
133
- contextId?: string;
134
- }): Promise<BrowserSessionInfo[]>;
135
- export declare function bindTab(workspace: string, opts?: {
136
- matchDomain?: string;
137
- matchPathPrefix?: string;
130
+ export declare function bindTab(session: string, opts?: {
138
131
  contextId?: string;
139
132
  }): Promise<unknown>;
@@ -89,10 +89,13 @@ async function sendCommandRaw(action, params) {
89
89
  const maxRetries = 4;
90
90
  for (let attempt = 1; attempt <= maxRetries; attempt++) {
91
91
  const id = generateId();
92
- const wf = process.env.OPENCLI_WINDOW_FOCUSED;
93
- const windowFocused = (wf === '1' || wf === 'true') ? true : undefined;
92
+ const rawWindowMode = process.env.OPENCLI_WINDOW;
93
+ const envWindowMode = rawWindowMode === 'foreground' || rawWindowMode === 'background'
94
+ ? rawWindowMode
95
+ : undefined;
94
96
  const contextId = params.contextId ?? resolveProfileContextId();
95
- const command = { id, action, ...params, ...(contextId && { contextId }), ...(windowFocused && { windowFocused }) };
97
+ const windowMode = params.windowMode ?? envWindowMode;
98
+ const command = { id, action, ...params, ...(contextId && { contextId }), ...(windowMode && { windowMode }) };
96
99
  try {
97
100
  const res = await requestDaemon('/command', {
98
101
  method: 'POST',
@@ -143,10 +146,6 @@ export async function sendCommandFull(action, params = {}) {
143
146
  const result = await sendCommandRaw(action, params);
144
147
  return { data: result.data, page: result.page };
145
148
  }
146
- export async function listSessions(opts) {
147
- const result = await sendCommand('sessions', { ...(opts?.contextId && { contextId: opts.contextId }) });
148
- return Array.isArray(result) ? result : [];
149
- }
150
- export async function bindTab(workspace, opts = {}) {
151
- return sendCommand('bind', { workspace, ...opts });
149
+ export async function bindTab(session, opts = {}) {
150
+ return sendCommand('bind', { session, surface: 'browser', ...opts });
152
151
  }
@@ -144,6 +144,16 @@ describe('daemon-client', () => {
144
144
  const body = JSON.parse(String(vi.mocked(fetch).mock.calls[0][1]?.body));
145
145
  expect(body.contextId).toBe('work');
146
146
  });
147
+ it('sendCommand uses explicit windowMode before OPENCLI_WINDOW env fallback', async () => {
148
+ vi.stubEnv('OPENCLI_WINDOW', 'foreground');
149
+ vi.mocked(fetch).mockResolvedValue({
150
+ status: 200,
151
+ json: () => Promise.resolve({ id: 'server', ok: true, data: 'ok' }),
152
+ });
153
+ await sendCommand('exec', { code: '1 + 1', windowMode: 'background' });
154
+ const body = JSON.parse(String(vi.mocked(fetch).mock.calls[0][1]?.body));
155
+ expect(body.windowMode).toBe('background');
156
+ });
147
157
  it('sendCommand retries with a new id when daemon reports a duplicate pending id', async () => {
148
158
  vi.spyOn(Date, 'now').mockReturnValue(1_763_000_000_123);
149
159
  const fetchMock = vi.mocked(fetch);
@@ -6,7 +6,7 @@
6
6
  * stable references to request bodies after running other commands,
7
7
  * so every `browser network` call snapshots its results to disk.
8
8
  *
9
- * Layout: <cacheDir>/browser-network/<workspace>.json
9
+ * Layout: <cacheDir>/browser-network/<session>.json
10
10
  * Entries expire after DEFAULT_TTL_MS (24h).
11
11
  */
12
12
  export declare const DEFAULT_TTL_MS: number;
@@ -29,12 +29,12 @@ export interface CachedNetworkEntry {
29
29
  }
30
30
  export interface NetworkCacheFile {
31
31
  version: 1;
32
- workspace: string;
32
+ session: string;
33
33
  savedAt: string;
34
34
  entries: CachedNetworkEntry[];
35
35
  }
36
- export declare function getCachePath(workspace: string, baseDir?: string): string;
37
- export declare function saveNetworkCache(workspace: string, entries: CachedNetworkEntry[], baseDir?: string): void;
36
+ export declare function getCachePath(session: string, baseDir?: string): string;
37
+ export declare function saveNetworkCache(session: string, entries: CachedNetworkEntry[], baseDir?: string): void;
38
38
  export interface LoadOptions {
39
39
  baseDir?: string;
40
40
  ttlMs?: number;
@@ -45,5 +45,5 @@ export interface LoadResult {
45
45
  file?: NetworkCacheFile;
46
46
  ageMs?: number;
47
47
  }
48
- export declare function loadNetworkCache(workspace: string, opts?: LoadOptions): LoadResult;
48
+ export declare function loadNetworkCache(session: string, opts?: LoadOptions): LoadResult;
49
49
  export declare function findEntry(file: NetworkCacheFile, key: string): CachedNetworkEntry | null;
@@ -6,7 +6,7 @@
6
6
  * stable references to request bodies after running other commands,
7
7
  * so every `browser network` call snapshots its results to disk.
8
8
  *
9
- * Layout: <cacheDir>/browser-network/<workspace>.json
9
+ * Layout: <cacheDir>/browser-network/<session>.json
10
10
  * Entries expire after DEFAULT_TTL_MS (24h).
11
11
  */
12
12
  import * as fs from 'node:fs';
@@ -16,23 +16,23 @@ export const DEFAULT_TTL_MS = 24 * 60 * 60 * 1000;
16
16
  function getDefaultCacheDir() {
17
17
  return process.env.OPENCLI_CACHE_DIR || path.join(os.homedir(), '.opencli', 'cache');
18
18
  }
19
- export function getCachePath(workspace, baseDir = getDefaultCacheDir()) {
20
- const safe = workspace.replace(/[^a-zA-Z0-9_-]+/g, '_');
19
+ export function getCachePath(session, baseDir = getDefaultCacheDir()) {
20
+ const safe = session.replace(/[^a-zA-Z0-9_-]+/g, '_');
21
21
  return path.join(baseDir, 'browser-network', `${safe}.json`);
22
22
  }
23
- export function saveNetworkCache(workspace, entries, baseDir) {
24
- const target = getCachePath(workspace, baseDir);
23
+ export function saveNetworkCache(session, entries, baseDir) {
24
+ const target = getCachePath(session, baseDir);
25
25
  fs.mkdirSync(path.dirname(target), { recursive: true });
26
26
  const payload = {
27
27
  version: 1,
28
- workspace,
28
+ session,
29
29
  savedAt: new Date().toISOString(),
30
30
  entries,
31
31
  };
32
32
  fs.writeFileSync(target, JSON.stringify(payload), 'utf-8');
33
33
  }
34
- export function loadNetworkCache(workspace, opts = {}) {
35
- const target = getCachePath(workspace, opts.baseDir);
34
+ export function loadNetworkCache(session, opts = {}) {
35
+ const target = getCachePath(session, opts.baseDir);
36
36
  let raw;
37
37
  try {
38
38
  raw = fs.readFileSync(target, 'utf-8');
@@ -14,9 +14,9 @@ describe('network-cache', () => {
14
14
  afterEach(() => {
15
15
  fs.rmSync(baseDir, { recursive: true, force: true });
16
16
  });
17
- it('sanitizes workspace names into safe filenames', () => {
18
- const p = getCachePath('browser:default', baseDir);
19
- expect(path.basename(p)).toBe('browser_default.json');
17
+ it('sanitizes session names into safe filenames', () => {
18
+ const p = getCachePath('twitter/agent 1', baseDir);
19
+ expect(path.basename(p)).toBe('twitter_agent_1.json');
20
20
  });
21
21
  it('round-trips entries through save + load', () => {
22
22
  saveNetworkCache('ws', [makeEntry('UserTweets'), makeEntry('UserByScreenName')], baseDir);
@@ -49,7 +49,7 @@ describe('network-cache', () => {
49
49
  });
50
50
  it('findEntry returns matching entry or null', () => {
51
51
  const file = {
52
- version: 1, workspace: 'ws', savedAt: new Date().toISOString(),
52
+ version: 1, session: 'ws', savedAt: new Date().toISOString(),
53
53
  entries: [makeEntry('A'), makeEntry('B')],
54
54
  };
55
55
  expect(findEntry(file, 'B')?.key).toBe('B');
@@ -14,22 +14,24 @@ import { BasePage } from './base-page.js';
14
14
  * Page — implements IPage by talking to the daemon via HTTP.
15
15
  */
16
16
  export declare class Page extends BasePage {
17
- private readonly workspace;
17
+ private readonly session;
18
18
  readonly contextId?: string | undefined;
19
+ private readonly windowMode?;
20
+ private readonly surface;
21
+ private readonly siteSession?;
19
22
  private readonly _idleTimeout;
20
- constructor(workspace?: string, idleTimeout?: number, contextId?: string | undefined);
23
+ constructor(session: string, idleTimeout?: number, contextId?: string | undefined, windowMode?: "foreground" | "background" | undefined, surface?: 'browser' | 'adapter', siteSession?: "ephemeral" | "persistent" | undefined);
21
24
  /** Active page identity (targetId), set after navigate and used in all subsequent commands */
22
25
  private _page;
23
26
  private _networkCaptureUnsupported;
24
27
  private _networkCaptureWarned;
25
- /** Helper: spread workspace into command params */
26
- private _wsOpt;
27
- /** Helper: spread workspace + page identity into command params */
28
+ /** Helper: spread session into command params */
29
+ private _sessionOpts;
30
+ /** Helper: spread session + page identity into command params */
28
31
  private _cmdOpts;
29
32
  goto(url: string, options?: {
30
33
  waitUntil?: 'load' | 'none';
31
34
  settleMs?: number;
32
- allowBoundNavigation?: boolean;
33
35
  }): Promise<void>;
34
36
  /** Get the active page identity (targetId) */
35
37
  getActivePage(): string | undefined;
@@ -41,7 +43,7 @@ export declare class Page extends BasePage {
41
43
  domain?: string;
42
44
  url?: string;
43
45
  }): Promise<BrowserCookie[]>;
44
- /** Release the current automation tab lease in the extension */
46
+ /** Release the current browser session lease in the extension */
45
47
  closeWindow(): Promise<void>;
46
48
  tabs(): Promise<unknown[]>;
47
49
  newTab(url?: string): Promise<string | undefined>;
@@ -26,41 +26,52 @@ function isUnsupportedNetworkCaptureError(err) {
26
26
  * Page — implements IPage by talking to the daemon via HTTP.
27
27
  */
28
28
  export class Page extends BasePage {
29
- workspace;
29
+ session;
30
30
  contextId;
31
+ windowMode;
32
+ surface;
33
+ siteSession;
31
34
  _idleTimeout;
32
- constructor(workspace = 'default', idleTimeout, contextId) {
35
+ constructor(session, idleTimeout, contextId, windowMode, surface = 'browser', siteSession) {
33
36
  super();
34
- this.workspace = workspace;
37
+ this.session = session;
35
38
  this.contextId = contextId;
39
+ this.windowMode = windowMode;
40
+ this.surface = surface;
41
+ this.siteSession = siteSession;
36
42
  this._idleTimeout = idleTimeout;
37
43
  }
38
44
  /** Active page identity (targetId), set after navigate and used in all subsequent commands */
39
45
  _page;
40
46
  _networkCaptureUnsupported = false;
41
47
  _networkCaptureWarned = false;
42
- /** Helper: spread workspace into command params */
43
- _wsOpt() {
48
+ /** Helper: spread session into command params */
49
+ _sessionOpts() {
44
50
  return {
45
- workspace: this.workspace,
51
+ session: this.session,
52
+ surface: this.surface,
46
53
  ...(this.contextId && { contextId: this.contextId }),
47
54
  ...(this._idleTimeout != null && { idleTimeout: this._idleTimeout }),
55
+ ...(this.windowMode && { windowMode: this.windowMode }),
56
+ ...(this.siteSession && { siteSession: this.siteSession }),
48
57
  };
49
58
  }
50
- /** Helper: spread workspace + page identity into command params */
59
+ /** Helper: spread session + page identity into command params */
51
60
  _cmdOpts() {
52
61
  return {
53
- workspace: this.workspace,
62
+ session: this.session,
63
+ surface: this.surface,
54
64
  ...(this.contextId && { contextId: this.contextId }),
55
65
  ...(this._page !== undefined && { page: this._page }),
56
66
  ...(this._idleTimeout != null && { idleTimeout: this._idleTimeout }),
67
+ ...(this.windowMode && { windowMode: this.windowMode }),
68
+ ...(this.siteSession && { siteSession: this.siteSession }),
57
69
  };
58
70
  }
59
71
  async goto(url, options) {
60
72
  const result = await sendCommandFull('navigate', {
61
73
  url,
62
74
  ...this._cmdOpts(),
63
- ...(options?.allowBoundNavigation === true && { allowBoundNavigation: true }),
64
75
  });
65
76
  // Remember the page identity (targetId) for subsequent calls
66
77
  if (result.page) {
@@ -140,13 +151,13 @@ export class Page extends BasePage {
140
151
  }
141
152
  }
142
153
  async getCookies(opts = {}) {
143
- const result = await sendCommand('cookies', { ...this._wsOpt(), ...opts });
154
+ const result = await sendCommand('cookies', { ...this._sessionOpts(), ...opts });
144
155
  return Array.isArray(result) ? result : [];
145
156
  }
146
- /** Release the current automation tab lease in the extension */
157
+ /** Release the current browser session lease in the extension */
147
158
  async closeWindow() {
148
159
  try {
149
- await sendCommand('close-window', { ...this._wsOpt() });
160
+ await sendCommand('close-window', { ...this._sessionOpts() });
150
161
  }
151
162
  catch {
152
163
  // Window may already be closed or daemon may be down
@@ -159,20 +170,20 @@ export class Page extends BasePage {
159
170
  }
160
171
  }
161
172
  async tabs() {
162
- const result = await sendCommand('tabs', { op: 'list', ...this._wsOpt() });
173
+ const result = await sendCommand('tabs', { op: 'list', ...this._sessionOpts() });
163
174
  return Array.isArray(result) ? result : [];
164
175
  }
165
176
  async newTab(url) {
166
177
  const result = await sendCommandFull('tabs', {
167
178
  op: 'new',
168
179
  ...(url !== undefined && { url }),
169
- ...this._wsOpt(),
180
+ ...this._sessionOpts(),
170
181
  });
171
182
  this._lastUrl = null;
172
183
  return result.page;
173
184
  }
174
185
  async closeTab(target) {
175
- const params = { op: 'close', ...this._wsOpt() };
186
+ const params = { op: 'close', ...this._sessionOpts() };
176
187
  if (typeof target === 'number')
177
188
  params.index = target;
178
189
  else if (typeof target === 'string')
@@ -190,7 +201,7 @@ export class Page extends BasePage {
190
201
  const result = await sendCommandFull('tabs', {
191
202
  op: 'select',
192
203
  ...(typeof target === 'number' ? { index: target } : { page: target }),
193
- ...this._wsOpt(),
204
+ ...this._sessionOpts(),
194
205
  });
195
206
  if (result.page)
196
207
  this._page = result.page;