@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
package/dist/src/cli.js CHANGED
@@ -36,8 +36,6 @@ import { bindTab, BrowserCommandError, fetchDaemonStatus, sendCommand } from './
36
36
  import { aliasForContextId, loadProfileConfig, renameProfile, resolveProfileContextId, setDefaultProfile } from './browser/profile.js';
37
37
  import { formatDaemonVersion, isDaemonStale } from './browser/daemon-version.js';
38
38
  const CLI_FILE = fileURLToPath(import.meta.url);
39
- const DEFAULT_BROWSER_WORKSPACE = 'browser:default';
40
- const DEFAULT_BOUND_WORKSPACE = 'bound:default';
41
39
  const BROWSER_TAB_OPTION_DESCRIPTION = 'Target tab/page identity returned by "browser open", "browser tab new", or "browser tab list"';
42
40
  const FOLLOW_POLL_MS = 1_000;
43
41
  function parseDurationMs(raw, flagName) {
@@ -245,11 +243,11 @@ export function renderVerifyPreview(rows, opts = {}) {
245
243
  function getBrowserCacheDir() {
246
244
  return process.env.OPENCLI_CACHE_DIR || path.join(os.homedir(), '.opencli', 'cache');
247
245
  }
248
- function getBrowserTargetStatePath(scope = DEFAULT_BROWSER_WORKSPACE) {
249
- const safeWorkspace = scope.replace(/[^a-zA-Z0-9_-]+/g, '_');
250
- return path.join(getBrowserCacheDir(), 'browser-state', `${safeWorkspace}.json`);
246
+ function getBrowserTargetStatePath(scope) {
247
+ const safeSession = scope.replace(/[^a-zA-Z0-9_-]+/g, '_');
248
+ return path.join(getBrowserCacheDir(), 'browser-state', `${safeSession}.json`);
251
249
  }
252
- function loadBrowserTargetState(scope = DEFAULT_BROWSER_WORKSPACE) {
250
+ function loadBrowserTargetState(scope) {
253
251
  try {
254
252
  const raw = fs.readFileSync(getBrowserTargetStatePath(scope), 'utf-8');
255
253
  const parsed = JSON.parse(raw);
@@ -259,7 +257,7 @@ function loadBrowserTargetState(scope = DEFAULT_BROWSER_WORKSPACE) {
259
257
  return null;
260
258
  }
261
259
  }
262
- function saveBrowserTargetState(defaultPage, scope = DEFAULT_BROWSER_WORKSPACE) {
260
+ function saveBrowserTargetState(defaultPage, scope) {
263
261
  const target = getBrowserTargetStatePath(scope);
264
262
  if (!defaultPage) {
265
263
  fs.rmSync(target, { force: true });
@@ -291,7 +289,7 @@ async function resolveBrowserTargetInSession(page, targetPage, opts) {
291
289
  return undefined;
292
290
  }
293
291
  throw new Error(`Target tab ${candidate} could not be validated in the current browser session. ` +
294
- 'The Browser Bridge workspace may have restarted; re-run "opencli browser tab list" and choose a current target.', { cause: err });
292
+ 'The Browser Bridge session may have restarted; re-run "opencli browser tab list" and choose a current target.', { cause: err });
295
293
  }
296
294
  if (Array.isArray(tabs) && hasBrowserTabTarget(tabs, candidate)) {
297
295
  return candidate;
@@ -301,32 +299,33 @@ async function resolveBrowserTargetInSession(page, targetPage, opts) {
301
299
  return undefined;
302
300
  }
303
301
  throw new Error(`Target tab ${candidate} is not part of the current browser session. ` +
304
- 'The Browser Bridge workspace may have restarted; re-run "opencli browser tab list" and choose a current target.');
302
+ 'The Browser Bridge session may have restarted; re-run "opencli browser tab list" and choose a current target.');
305
303
  }
306
- function getBrowserScope(workspace, contextId) {
307
- return contextId ? `${contextId}:${workspace}` : workspace;
304
+ function getBrowserScope(session, contextId) {
305
+ return contextId ? `${contextId}:${session}` : session;
308
306
  }
309
- async function resolveStoredBrowserTarget(page, scope = DEFAULT_BROWSER_WORKSPACE) {
307
+ async function resolveStoredBrowserTarget(page, scope) {
310
308
  const defaultPage = loadBrowserTargetState(scope)?.defaultPage?.trim();
311
309
  if (!defaultPage)
312
310
  return undefined;
313
311
  return resolveBrowserTargetInSession(page, defaultPage, { scope, source: 'saved' });
314
312
  }
315
- /** Create a browser page for browser commands. Uses a dedicated browser workspace for session persistence. */
316
- async function getBrowserPage(targetPage, workspace = DEFAULT_BROWSER_WORKSPACE, contextId) {
313
+ /** Create a browser page for browser commands. Uses a named browser session for continuity. */
314
+ async function getBrowserPage(session, targetPage, contextId, opts = {}) {
317
315
  const { BrowserBridge } = await import('./browser/index.js');
318
316
  const bridge = new BrowserBridge();
319
- // Idle timeout: how long the browser workspace lease stays alive between commands
320
- // (controls when the automation tab is released). Not the per-command runtime timeout.
317
+ // Internal GC timeout for browser sessions. Not the per-command runtime timeout.
321
318
  const envTimeout = process.env.OPENCLI_BROWSER_IDLE_TIMEOUT;
322
319
  const idleTimeout = envTimeout ? parseInt(envTimeout, 10) : undefined;
323
320
  const page = await bridge.connect({
324
321
  timeout: 30,
325
- workspace,
322
+ session,
323
+ surface: 'browser',
326
324
  ...(contextId && { contextId }),
327
325
  ...(idleTimeout && idleTimeout > 0 && { idleTimeout }),
326
+ windowMode: opts.windowMode ?? getBrowserWindowMode(undefined, 'foreground'),
328
327
  });
329
- const targetScope = getBrowserScope(workspace, contextId);
328
+ const targetScope = getBrowserScope(session, contextId);
330
329
  const resolvedTargetPage = targetPage
331
330
  ? await resolveBrowserTargetInSession(page, targetPage, { scope: targetScope, source: 'explicit' })
332
331
  : await resolveStoredBrowserTarget(page, targetScope);
@@ -338,6 +337,21 @@ async function getBrowserPage(targetPage, workspace = DEFAULT_BROWSER_WORKSPACE,
338
337
  }
339
338
  return page;
340
339
  }
340
+ function getBrowserWindowMode(command, defaultMode) {
341
+ const optionRaw = getCommandOption(command, 'window');
342
+ if (optionRaw !== undefined && optionRaw !== '') {
343
+ if (optionRaw === 'foreground' || optionRaw === 'background')
344
+ return optionRaw;
345
+ throw new Error(`--window must be one of: foreground, background. Received: "${String(optionRaw)}"`);
346
+ }
347
+ const envRaw = process.env.OPENCLI_WINDOW;
348
+ if (envRaw !== undefined && envRaw !== '') {
349
+ if (envRaw === 'foreground' || envRaw === 'background')
350
+ return envRaw;
351
+ throw new Error(`OPENCLI_WINDOW must be one of: foreground, background. Received: "${envRaw}"`);
352
+ }
353
+ return defaultMode;
354
+ }
341
355
  function addBrowserTabOption(command) {
342
356
  return command.option('--tab <targetId>', BROWSER_TAB_OPTION_DESCRIPTION);
343
357
  }
@@ -357,21 +371,25 @@ function getCommandOption(command, option) {
357
371
  }
358
372
  return undefined;
359
373
  }
360
- function getBrowserWorkspace(command) {
361
- const raw = getCommandOption(command, 'workspace');
362
- return typeof raw === 'string' && raw.trim() ? raw.trim() : DEFAULT_BROWSER_WORKSPACE;
374
+ function getBrowserSession(command) {
375
+ const raw = getCommandOption(command, 'session');
376
+ if (typeof raw === 'string' && raw.trim())
377
+ return raw.trim();
378
+ throw new Error('--session <name> is required for opencli browser commands');
363
379
  }
364
380
  function getBrowserContextId(command) {
365
381
  const raw = getCommandOption(command, 'profile');
366
382
  return resolveProfileContextId(typeof raw === 'string' && raw.trim() ? raw.trim() : undefined);
367
383
  }
368
- function getPageWorkspace(page) {
369
- const workspace = page.workspace;
370
- return typeof workspace === 'string' && workspace.trim() ? workspace.trim() : DEFAULT_BROWSER_WORKSPACE;
384
+ function getPageSession(page) {
385
+ const session = page.session;
386
+ if (typeof session === 'string' && session.trim())
387
+ return session.trim();
388
+ throw new Error('Browser page is missing a session');
371
389
  }
372
390
  function getPageScope(page) {
373
391
  const contextId = page.contextId;
374
- return getBrowserScope(getPageWorkspace(page), typeof contextId === 'string' && contextId.trim() ? contextId.trim() : undefined);
392
+ return getBrowserScope(getPageSession(page), typeof contextId === 'string' && contextId.trim() ? contextId.trim() : undefined);
375
393
  }
376
394
  function snapshotMetricText(snapshot) {
377
395
  return typeof snapshot === 'string' ? snapshot : JSON.stringify(snapshot, null, 2);
@@ -582,7 +600,8 @@ export function createProgram(BUILTIN_CLIS, USER_CLIS) {
582
600
  // All commands wrapped in browserAction() for consistent error handling.
583
601
  const browser = program
584
602
  .command('browser')
585
- .option('--workspace <name>', 'Browser workspace to use (default: browser:default; bound tabs use bound:<name>)')
603
+ .option('--session <name>', 'Browser session to use')
604
+ .option('--window <mode>', 'Browser window mode: foreground or background')
586
605
  .description('Browser control — navigate, click, type, extract, wait (no LLM needed)');
587
606
  const originalBrowserDescription = browser.description();
588
607
  /**
@@ -646,12 +665,14 @@ export function createProgram(BUILTIN_CLIS, USER_CLIS) {
646
665
  /** Wrap browser actions with error handling and optional --json output */
647
666
  function browserAction(fn) {
648
667
  return async (...args) => {
668
+ let page = null;
649
669
  try {
650
670
  const command = args.at(-1) instanceof Command ? args.at(-1) : undefined;
651
671
  const targetPage = getBrowserTargetId(command);
652
- const workspace = getBrowserWorkspace(command);
672
+ const session = getBrowserSession(command);
653
673
  const contextId = getBrowserContextId(command);
654
- const page = await getBrowserPage(targetPage, workspace, contextId);
674
+ const windowMode = getBrowserWindowMode(command, 'foreground');
675
+ page = await getBrowserPage(session, targetPage, contextId, { windowMode });
655
676
  await fn(page, ...args);
656
677
  }
657
678
  catch (err) {
@@ -702,40 +723,19 @@ export function createProgram(BUILTIN_CLIS, USER_CLIS) {
702
723
  };
703
724
  }
704
725
  browser.command('bind')
705
- .option('--domain <host>', 'Only bind a current/visible tab whose hostname matches this domain')
706
- .option('--path-prefix <path>', 'Only bind a current/visible tab whose pathname starts with this prefix')
707
- .option('--workspace <name>', 'Bound workspace name (must start with bound:)')
708
- .description('Bind a bound:* workspace to the current Chrome tab/window')
726
+ .option('--session <name>', 'Browser session name to bind')
727
+ .description('Bind the current Chrome tab/window to a browser session')
709
728
  .action(async (optsOrCommand, maybeCommand) => {
710
729
  const command = optsOrCommand instanceof Command ? optsOrCommand : maybeCommand;
711
- const opts = command?.opts() ?? optsOrCommand ?? {};
712
- const rawWorkspace = getCommandOption(command, 'workspace');
713
- const workspace = typeof rawWorkspace === 'string' && rawWorkspace.trim()
714
- ? rawWorkspace.trim()
715
- : DEFAULT_BOUND_WORKSPACE;
716
- if (!workspace.startsWith('bound:')) {
717
- console.log(JSON.stringify({
718
- error: {
719
- code: 'invalid_bind_workspace',
720
- message: `--workspace must start with "bound:", got "${workspace}"`,
721
- hint: 'Use the default bound:default or pass --workspace bound:<name>.',
722
- },
723
- }, null, 2));
724
- process.exitCode = EXIT_CODES.USAGE_ERROR;
725
- return;
726
- }
730
+ const session = getBrowserSession(command);
727
731
  try {
728
732
  const { BrowserBridge } = await import('./browser/index.js');
729
733
  const bridge = new BrowserBridge();
730
734
  const contextId = getBrowserContextId(command);
731
- await bridge.connect({ timeout: 30, workspace, ...(contextId && { contextId }) });
732
- const data = await bindTab(workspace, {
733
- ...(contextId && { contextId }),
734
- ...(typeof opts.domain === 'string' && opts.domain.trim() ? { matchDomain: opts.domain.trim() } : {}),
735
- ...(typeof opts.pathPrefix === 'string' && opts.pathPrefix.trim() ? { matchPathPrefix: opts.pathPrefix.trim() } : {}),
736
- });
737
- saveBrowserTargetState(undefined, getBrowserScope(workspace, contextId));
738
- console.log(JSON.stringify({ workspace, ...((data && typeof data === 'object') ? data : { data }) }, null, 2));
735
+ await bridge.connect({ timeout: 30, session, surface: 'browser', ...(contextId && { contextId }) });
736
+ const data = await bindTab(session, { ...(contextId && { contextId }) });
737
+ saveBrowserTargetState(undefined, getBrowserScope(session, contextId));
738
+ console.log(JSON.stringify({ session, ...((data && typeof data === 'object') ? data : { data }) }, null, 2));
739
739
  }
740
740
  catch (err) {
741
741
  if (err instanceof BrowserCommandError && err.code) {
@@ -750,39 +750,23 @@ export function createProgram(BUILTIN_CLIS, USER_CLIS) {
750
750
  log.error(err instanceof Error ? err.message : String(err));
751
751
  if (err instanceof BrowserCommandError && err.hint)
752
752
  log.error(`Hint: ${err.hint}`);
753
- process.exitCode = err instanceof BrowserCommandError && err.code === 'invalid_bind_workspace'
754
- ? EXIT_CODES.USAGE_ERROR
755
- : EXIT_CODES.GENERIC_ERROR;
753
+ process.exitCode = EXIT_CODES.GENERIC_ERROR;
756
754
  }
757
755
  });
758
756
  browser.command('unbind')
759
- .option('--workspace <name>', 'Bound workspace name to detach')
760
- .description('Detach a bound:* workspace without closing the user tab/window')
757
+ .option('--session <name>', 'Browser session name to detach')
758
+ .description('Detach a bound browser session without closing the user tab/window')
761
759
  .action(async (optsOrCommand, maybeCommand) => {
762
760
  const command = optsOrCommand instanceof Command ? optsOrCommand : maybeCommand;
763
- const rawWorkspace = getCommandOption(command, 'workspace');
764
- const workspace = typeof rawWorkspace === 'string' && rawWorkspace.trim()
765
- ? rawWorkspace.trim()
766
- : DEFAULT_BOUND_WORKSPACE;
767
- if (!workspace.startsWith('bound:')) {
768
- console.log(JSON.stringify({
769
- error: {
770
- code: 'invalid_bind_workspace',
771
- message: `--workspace must start with "bound:", got "${workspace}"`,
772
- hint: 'Use the default bound:default or pass --workspace bound:<name>.',
773
- },
774
- }, null, 2));
775
- process.exitCode = EXIT_CODES.USAGE_ERROR;
776
- return;
777
- }
761
+ const session = getBrowserSession(command);
778
762
  try {
779
763
  const { BrowserBridge } = await import('./browser/index.js');
780
764
  const bridge = new BrowserBridge();
781
765
  const contextId = getBrowserContextId(command);
782
- await bridge.connect({ timeout: 30, workspace, ...(contextId && { contextId }) });
783
- await sendCommand('close-window', { workspace, ...(contextId && { contextId }) });
784
- saveBrowserTargetState(undefined, getBrowserScope(workspace, contextId));
785
- console.log(JSON.stringify({ unbound: true, workspace }, null, 2));
766
+ await bridge.connect({ timeout: 30, session, surface: 'browser', ...(contextId && { contextId }) });
767
+ await sendCommand('close-window', { session, surface: 'browser', ...(contextId && { contextId }) });
768
+ saveBrowserTargetState(undefined, getBrowserScope(session, contextId));
769
+ console.log(JSON.stringify({ unbound: true, session }, null, 2));
786
770
  }
787
771
  catch (err) {
788
772
  if (err instanceof BrowserCommandError && err.code) {
@@ -802,9 +786,9 @@ export function createProgram(BUILTIN_CLIS, USER_CLIS) {
802
786
  });
803
787
  const browserTab = browser
804
788
  .command('tab')
805
- .description('Tab management — list, create, and close tabs in the automation window');
789
+ .description('Tab management — list, create, and close tabs in the browser session');
806
790
  browserTab.command('list')
807
- .description('List tabs in the automation window with target IDs')
791
+ .description('List tabs in the browser session with target IDs')
808
792
  .action(browserAction(async (page) => {
809
793
  const tabs = await page.tabs();
810
794
  console.log(JSON.stringify(tabs, null, 2));
@@ -871,16 +855,11 @@ export function createProgram(BUILTIN_CLIS, USER_CLIS) {
871
855
  * capped at 200 entries, bounding worst-case in-page memory.
872
856
  */
873
857
  const NETWORK_INTERCEPTOR_JS = `(function(){if(window.__opencli_net)return;window.__opencli_net=[];var M=200,B=1048576,F=window.fetch;function capture(url,method,status,text,ct){if(window.__opencli_net.length>=M)return;var full=text?text.length:0,trunc=full>B,stored=trunc?text.slice(0,B):text,body=null;if(stored){if(trunc){body=stored}else{try{body=JSON.parse(stored)}catch(e){body=stored}}}var e={url:url,method:method||'GET',status:status,size:full,ct:ct,body:body,timestamp:Date.now()};if(trunc){e.bodyTruncated=true;e.bodyFullSize=full}window.__opencli_net.push(e)}window.fetch=async function(){var r=await F.apply(this,arguments);try{var ct=r.headers.get('content-type')||'';if(ct.includes('json')||ct.includes('text')){var c=r.clone(),t=await c.text();capture(r.url||(arguments[0]&&arguments[0].url)||String(arguments[0]),(arguments[1]&&arguments[1].method)||'GET',r.status,t,ct)}}catch(e){}return r};var X=XMLHttpRequest.prototype,O=X.open,S=X.send;X.open=function(m,u){this._om=m;this._ou=u;return O.apply(this,arguments)};X.send=function(){var x=this;x.addEventListener('load',function(){try{var ct=x.getResponseHeader('content-type')||'';if(ct.includes('json')||ct.includes('text')){capture(x._ou,x._om||'GET',x.status,x.responseText||'',ct)}}catch(e){}});return S.apply(this,arguments)}})()`;
874
- addBrowserTabOption(browser.command('open').argument('<url>').option('--allow-navigate-bound', 'Allow navigating a bound user tab', false).description('Open URL in automation window'))
858
+ addBrowserTabOption(browser.command('open').argument('<url>').description('Open URL in the browser session'))
875
859
  .action(browserAction(async (page, url, opts) => {
876
860
  // Start session-level capture before navigation (catches initial requests)
877
861
  const hasSessionCapture = await page.startNetworkCapture?.() ?? false;
878
- if (opts.allowNavigateBound === true) {
879
- await page.goto(url, { allowBoundNavigation: true });
880
- }
881
- else {
882
- await page.goto(url);
883
- }
862
+ await page.goto(url);
884
863
  await page.wait(2);
885
864
  // Fallback: inject JS interceptor when session capture is unavailable
886
865
  if (!hasSessionCapture) {
@@ -894,19 +873,8 @@ export function createProgram(BUILTIN_CLIS, USER_CLIS) {
894
873
  ...(page.getActivePage?.() ? { page: page.getActivePage?.() } : {}),
895
874
  }, null, 2));
896
875
  }));
897
- addBrowserTabOption(browser.command('back').option('--allow-navigate-bound', 'Allow history navigation in a bound user tab', false).description('Go back in browser history'))
876
+ addBrowserTabOption(browser.command('back').description('Go back in browser history'))
898
877
  .action(browserAction(async (page, opts) => {
899
- if (getPageWorkspace(page).startsWith('bound:') && opts.allowNavigateBound !== true) {
900
- console.log(JSON.stringify({
901
- error: {
902
- code: 'bound_navigation_blocked',
903
- message: `Workspace "${getPageWorkspace(page)}" is bound to a user tab; history navigation is blocked by default.`,
904
- hint: 'Pass --allow-navigate-bound only if you intentionally want to navigate the bound tab.',
905
- },
906
- }, null, 2));
907
- process.exitCode = EXIT_CODES.GENERIC_ERROR;
908
- return;
909
- }
910
878
  await page.evaluate('history.back()');
911
879
  await page.wait(2);
912
880
  console.log('Navigated back');
@@ -1037,7 +1005,7 @@ export function createProgram(BUILTIN_CLIS, USER_CLIS) {
1037
1005
  }
1038
1006
  const messages = filter(normalize(await page.consoleMessages(opts.level)));
1039
1007
  console.log(JSON.stringify({
1040
- workspace: getPageWorkspace(page),
1008
+ session: getPageSession(page),
1041
1009
  captured_at: new Date().toISOString(),
1042
1010
  count: messages.length,
1043
1011
  messages: messages.map((message) => ({
@@ -2129,7 +2097,7 @@ export function createProgram(BUILTIN_CLIS, USER_CLIS) {
2129
2097
  // Default output is JSON (agent-native). Each entry carries a stable `key`
2130
2098
  // (GraphQL operationName or `METHOD host+pathname`) so agents can fetch
2131
2099
  // full bodies with `--detail <key>` even after subsequent commands.
2132
- // Captures are persisted per workspace under ~/.opencli/cache/browser-network/.
2100
+ // Captures are persisted per browser session under ~/.opencli/cache/browser-network/.
2133
2101
  addBrowserTabOption(browser.command('network'))
2134
2102
  .option('--detail <key>', 'Emit full body for the entry with this key')
2135
2103
  .option('--all', 'Include static resources (js/css/images/telemetry)')
@@ -2144,7 +2112,7 @@ export function createProgram(BUILTIN_CLIS, USER_CLIS) {
2144
2112
  .description('Capture network requests as shape previews; retrieve full bodies by key')
2145
2113
  .action(browserAction(async (page, opts) => {
2146
2114
  const ttlMs = parsePositiveIntOption(opts.ttl, 'ttl', DEFAULT_TTL_MS);
2147
- const workspace = getPageWorkspace(page);
2115
+ const session = getPageSession(page);
2148
2116
  const hasDetail = typeof opts.detail === 'string' && opts.detail.length > 0;
2149
2117
  const hasFilter = typeof opts.filter === 'string';
2150
2118
  const sinceMs = parseDurationMs(opts.since, 'since');
@@ -2180,9 +2148,9 @@ export function createProgram(BUILTIN_CLIS, USER_CLIS) {
2180
2148
  }
2181
2149
  // --detail short-circuits: read from cache only, no live capture needed.
2182
2150
  if (hasDetail) {
2183
- const res = loadNetworkCache(workspace, { ttlMs });
2151
+ const res = loadNetworkCache(session, { ttlMs });
2184
2152
  if (res.status === 'missing') {
2185
- emitNetworkError('cache_missing', `No cached capture. Run "browser network" first (in workspace "${workspace}").`);
2153
+ emitNetworkError('cache_missing', `No cached capture. Run "browser network" first (in session "${session}").`);
2186
2154
  return;
2187
2155
  }
2188
2156
  if (res.status === 'expired') {
@@ -2303,7 +2271,7 @@ export function createProgram(BUILTIN_CLIS, USER_CLIS) {
2303
2271
  // via the output envelope rather than erroring out the whole command.
2304
2272
  let cacheWarning = null;
2305
2273
  try {
2306
- saveNetworkCache(workspace, cacheEntries);
2274
+ saveNetworkCache(session, cacheEntries);
2307
2275
  }
2308
2276
  catch (err) {
2309
2277
  cacheWarning = `Could not persist capture cache: ${err.message}. --detail lookups may miss this capture.`;
@@ -2319,7 +2287,7 @@ export function createProgram(BUILTIN_CLIS, USER_CLIS) {
2319
2287
  : shaped;
2320
2288
  const filterDropped = filterFields ? shaped.length - visible.length : 0;
2321
2289
  const envelope = {
2322
- workspace,
2290
+ session,
2323
2291
  captured_at: new Date().toISOString(),
2324
2292
  count: visible.length,
2325
2293
  filtered_out: filteredOut,
@@ -2384,19 +2352,7 @@ export function createProgram(BUILTIN_CLIS, USER_CLIS) {
2384
2352
  console.log(`Adapter already exists: ${filePath}`);
2385
2353
  return;
2386
2354
  }
2387
- // Try to detect domain from the last browser session
2388
2355
  let domain = site;
2389
- try {
2390
- const page = await getBrowserPage();
2391
- const url = await page.getCurrentUrl?.();
2392
- if (url) {
2393
- try {
2394
- domain = new URL(url).hostname;
2395
- }
2396
- catch { }
2397
- }
2398
- }
2399
- catch { /* no active session */ }
2400
2356
  const template = `import { cli, Strategy } from '@jackwener/opencli/registry';
2401
2357
 
2402
2358
  cli({
@@ -2584,22 +2540,20 @@ cli({
2584
2540
  }
2585
2541
  });
2586
2542
  // ── Session ──
2587
- browser.command('close').description('Release the current automation tab lease')
2543
+ browser.command('close').description('Release the current browser session tab lease')
2588
2544
  .action(browserAction(async (page) => {
2589
2545
  await page.closeWindow?.();
2590
- console.log('Automation tab lease released');
2546
+ console.log('Browser session tab lease released');
2591
2547
  }));
2592
2548
  // ── Built-in: doctor / completion ──────────────────────────────────────────
2593
2549
  program
2594
2550
  .command('doctor')
2595
2551
  .description('Diagnose opencli browser bridge connectivity')
2596
- .option('--no-live', 'Skip live browser connectivity test')
2597
- .option('--sessions', 'Show active automation sessions', false)
2598
2552
  .option('-v, --verbose', 'Debug output')
2599
2553
  .action(async (opts) => {
2600
2554
  applyVerbose(opts);
2601
2555
  const { runBrowserDoctor, renderBrowserDoctorReport } = await import('./doctor.js');
2602
- const report = await runBrowserDoctor({ live: opts.live, sessions: opts.sessions, cliVersion: PKG_VERSION });
2556
+ const report = await runBrowserDoctor({ cliVersion: PKG_VERSION });
2603
2557
  console.log(renderBrowserDoctorReport(report));
2604
2558
  });
2605
2559
  program
@@ -2611,6 +2565,8 @@ cli({
2611
2565
  });
2612
2566
  // ── Plugin management ──────────────────────────────────────────────────────
2613
2567
  const pluginCmd = program.command('plugin').description('Manage opencli plugins');
2568
+ // Snapshot before applyRootSubcommandSummaries() rewrites .description() to a child-name listing.
2569
+ const originalPluginDescription = pluginCmd.description();
2614
2570
  pluginCmd
2615
2571
  .command('install')
2616
2572
  .description('Install a plugin from a git repository')
@@ -2797,6 +2753,8 @@ cli({
2797
2753
  });
2798
2754
  // ── Built-in: adapter management ─────────────────────────────────────────
2799
2755
  const adapterCmd = program.command('adapter').description('Manage CLI adapters');
2756
+ // Snapshot before applyRootSubcommandSummaries() rewrites .description() to a child-name listing.
2757
+ const originalAdapterDescription = adapterCmd.description();
2800
2758
  adapterCmd
2801
2759
  .command('status')
2802
2760
  .description('Show which sites have local overrides vs using official baseline')
@@ -2905,6 +2863,8 @@ cli({
2905
2863
  });
2906
2864
  // ── Built-in: browser profile selection ──────────────────────────────────
2907
2865
  const profileCmd = program.command('profile').description('Manage Browser Bridge Chrome profiles');
2866
+ // Snapshot before applyRootSubcommandSummaries() rewrites .description() to a child-name listing.
2867
+ const originalProfileDescription = profileCmd.description();
2908
2868
  profileCmd
2909
2869
  .command('list')
2910
2870
  .description('List Chrome profiles connected through the Browser Bridge extension')
@@ -2982,6 +2942,8 @@ cli({
2982
2942
  });
2983
2943
  // ── Built-in: daemon ──────────────────────────────────────────────────────
2984
2944
  const daemonCmd = program.command('daemon').description('Manage the opencli daemon');
2945
+ // Snapshot before applyRootSubcommandSummaries() rewrites .description() to a child-name listing.
2946
+ const originalDaemonDescription = daemonCmd.description();
2985
2947
  daemonCmd
2986
2948
  .command('status')
2987
2949
  .description('Show daemon status')
@@ -3107,6 +3069,10 @@ cli({
3107
3069
  const adapterGroups = { external: externalNames, apps, sites };
3108
3070
  const adapterNameSet = new Set([...externalNames, ...siteNames]);
3109
3071
  installCommanderNamespaceStructuredHelp(browser, { globalCommand: program, description: originalBrowserDescription });
3072
+ installCommanderNamespaceStructuredHelp(daemonCmd, { globalCommand: program, description: originalDaemonDescription });
3073
+ installCommanderNamespaceStructuredHelp(pluginCmd, { globalCommand: program, description: originalPluginDescription });
3074
+ installCommanderNamespaceStructuredHelp(adapterCmd, { globalCommand: program, description: originalAdapterDescription });
3075
+ installCommanderNamespaceStructuredHelp(profileCmd, { globalCommand: program, description: originalProfileDescription });
3110
3076
  program.configureHelp({
3111
3077
  visibleCommands: (command) => command.commands.filter(child => command !== program || !adapterNameSet.has(child.name())),
3112
3078
  });