@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
@@ -343,19 +343,24 @@ describe('createProgram root help descriptions', () => {
343
343
  const program = createProgram('', '');
344
344
  const browser = program.commands.find(cmd => cmd.name() === 'browser');
345
345
  expect(browser).toBeTruthy();
346
- process.argv = ['node', 'opencli', 'browser', '--help', '-f', 'yaml'];
346
+ process.argv = ['node', 'opencli', 'browser', '--session', 'test', '--help', '-f', 'yaml'];
347
347
  const data = yaml.load(browser.helpInformation());
348
348
  expect(data.namespace).toBe('browser');
349
349
  expect(data.command).toBe('opencli browser');
350
350
  expect(data.description).toBe('Browser control — navigate, click, type, extract, wait (no LLM needed)');
351
351
  expect(data.command_count).toBeGreaterThan(20);
352
- expect(data.namespace_options).toMatchObject([
353
- {
354
- name: 'workspace',
355
- flags: '--workspace <name>',
352
+ expect(data.namespace_options).toEqual(expect.arrayContaining([
353
+ expect.objectContaining({
354
+ name: 'session',
355
+ flags: '--session <name>',
356
356
  takes_value: 'required',
357
- },
358
- ]);
357
+ }),
358
+ expect.objectContaining({
359
+ name: 'window',
360
+ flags: '--window <mode>',
361
+ takes_value: 'required',
362
+ }),
363
+ ]));
359
364
  expect(data.global_options).toEqual(expect.arrayContaining([
360
365
  expect.objectContaining({
361
366
  name: 'version',
@@ -401,7 +406,7 @@ describe('createProgram root help descriptions', () => {
401
406
  const browser = program.commands.find(cmd => cmd.name() === 'browser');
402
407
  const tab = browser.commands.find(cmd => cmd.name() === 'tab');
403
408
  expect(tab).toBeTruthy();
404
- process.argv = ['node', 'opencli', 'browser', 'tab', '--help', '-f', 'yaml'];
409
+ process.argv = ['node', 'opencli', 'browser', '--session', 'test', 'tab', '--help', '-f', 'yaml'];
405
410
  const data = yaml.load(tab.helpInformation());
406
411
  expect(data).toMatchObject({
407
412
  namespace: 'browser',
@@ -421,7 +426,7 @@ describe('createProgram root help descriptions', () => {
421
426
  usage: 'opencli browser tab close [targetId] [options]',
422
427
  positionals: [{ name: 'targetId', help: 'Target tab/page identity returned by "browser open", "browser tab new", or "browser tab list"' }],
423
428
  });
424
- expect(data.namespace_options.map((option) => option.name)).toEqual(['workspace']);
429
+ expect(data.namespace_options.map((option) => option.name)).toEqual(['session', 'window']);
425
430
  expect(data.structured_help).toMatchObject({
426
431
  usage: 'opencli browser tab --help -f yaml',
427
432
  });
@@ -437,7 +442,7 @@ describe('createProgram root help descriptions', () => {
437
442
  const browser = program.commands.find(cmd => cmd.name() === 'browser');
438
443
  const click = browser.commands.find(cmd => cmd.name() === 'click');
439
444
  expect(click).toBeTruthy();
440
- process.argv = ['node', 'opencli', 'browser', 'click', '--help', '-f', 'yaml'];
445
+ process.argv = ['node', 'opencli', 'browser', '--session', 'test', 'click', '--help', '-f', 'yaml'];
441
446
  const data = yaml.load(click.helpInformation());
442
447
  expect(data).toMatchObject({
443
448
  namespace: 'browser',
@@ -450,13 +455,113 @@ describe('createProgram root help descriptions', () => {
450
455
  },
451
456
  });
452
457
  expect(data.command_options.map((option) => option.name)).toEqual(['role', 'name', 'label', 'text', 'testid', 'nth', 'tab']);
453
- expect(data.namespace_options.map((option) => option.name)).toEqual(['workspace']);
458
+ expect(data.namespace_options.map((option) => option.name)).toEqual(['session', 'window']);
454
459
  expect(data.global_options.map((option) => option.name)).toContain('profile');
455
460
  }
456
461
  finally {
457
462
  process.argv = argv;
458
463
  }
459
464
  });
465
+ it('renders daemon namespace structured help with leaves and global options', () => {
466
+ const argv = process.argv;
467
+ try {
468
+ const program = createProgram('', '');
469
+ const daemon = program.commands.find(cmd => cmd.name() === 'daemon');
470
+ expect(daemon).toBeTruthy();
471
+ process.argv = ['node', 'opencli', 'daemon', '--help', '-f', 'yaml'];
472
+ const data = yaml.load(daemon.helpInformation());
473
+ expect(data).toMatchObject({
474
+ namespace: 'daemon',
475
+ command: 'opencli daemon',
476
+ usage: 'opencli daemon <command> [args] [options]',
477
+ description: 'Manage the opencli daemon',
478
+ command_count: 3,
479
+ namespace_options: [],
480
+ structured_help: { usage: 'opencli daemon --help -f yaml' },
481
+ });
482
+ expect(data.commands.map((cmd) => cmd.name)).toEqual(['restart', 'status', 'stop']);
483
+ expect(data.global_options.map((option) => option.name)).toEqual(expect.arrayContaining(['version', 'profile']));
484
+ }
485
+ finally {
486
+ process.argv = argv;
487
+ }
488
+ });
489
+ it('renders plugin namespace structured help with positional + option leaves', () => {
490
+ const argv = process.argv;
491
+ try {
492
+ const program = createProgram('', '');
493
+ const plugin = program.commands.find(cmd => cmd.name() === 'plugin');
494
+ expect(plugin).toBeTruthy();
495
+ process.argv = ['node', 'opencli', 'plugin', '--help', '-f', 'yaml'];
496
+ const data = yaml.load(plugin.helpInformation());
497
+ expect(data).toMatchObject({
498
+ namespace: 'plugin',
499
+ command: 'opencli plugin',
500
+ description: 'Manage opencli plugins',
501
+ namespace_options: [],
502
+ });
503
+ expect(data.commands.map((cmd) => cmd.name)).toEqual(['create', 'install', 'list', 'uninstall', 'update']);
504
+ const update = data.commands.find((cmd) => cmd.name === 'update');
505
+ expect(update).toMatchObject({
506
+ usage: 'opencli plugin update [name] [options]',
507
+ positionals: [{ name: 'name' }],
508
+ });
509
+ expect(update.command_options.map((option) => option.name)).toEqual(['all']);
510
+ }
511
+ finally {
512
+ process.argv = argv;
513
+ }
514
+ });
515
+ it('renders adapter namespace structured help preserving original description after applyRootSubcommandSummaries', () => {
516
+ const argv = process.argv;
517
+ try {
518
+ const program = createProgram('', '');
519
+ const adapter = program.commands.find(cmd => cmd.name() === 'adapter');
520
+ expect(adapter).toBeTruthy();
521
+ process.argv = ['node', 'opencli', 'adapter', '--help', '-f', 'yaml'];
522
+ const data = yaml.load(adapter.helpInformation());
523
+ // applyRootSubcommandSummaries() rewrites .description() to a child-name listing;
524
+ // structured help must surface the original product description via the snapshot.
525
+ expect(data.description).toBe('Manage CLI adapters');
526
+ expect(data.commands.map((cmd) => cmd.name)).toEqual(['eject', 'reset', 'status']);
527
+ const reset = data.commands.find((cmd) => cmd.name === 'reset');
528
+ expect(reset).toMatchObject({
529
+ usage: 'opencli adapter reset [site] [options]',
530
+ positionals: [{ name: 'site' }],
531
+ });
532
+ expect(reset.command_options.map((option) => option.name)).toEqual(['all']);
533
+ }
534
+ finally {
535
+ process.argv = argv;
536
+ }
537
+ });
538
+ it('renders profile namespace structured help including required positionals', () => {
539
+ const argv = process.argv;
540
+ try {
541
+ const program = createProgram('', '');
542
+ const profile = program.commands.find(cmd => cmd.name() === 'profile');
543
+ expect(profile).toBeTruthy();
544
+ process.argv = ['node', 'opencli', 'profile', '--help', '-f', 'yaml'];
545
+ const data = yaml.load(profile.helpInformation());
546
+ expect(data).toMatchObject({
547
+ namespace: 'profile',
548
+ description: 'Manage Browser Bridge Chrome profiles',
549
+ command_count: 3,
550
+ });
551
+ expect(data.commands.map((cmd) => cmd.name)).toEqual(['list', 'rename', 'use']);
552
+ const rename = data.commands.find((cmd) => cmd.name === 'rename');
553
+ expect(rename).toMatchObject({
554
+ usage: 'opencli profile rename <contextId> <alias> [options]',
555
+ positionals: [
556
+ { name: 'contextId', required: true },
557
+ { name: 'alias', required: true },
558
+ ],
559
+ });
560
+ }
561
+ finally {
562
+ process.argv = argv;
563
+ }
564
+ });
460
565
  });
461
566
  describe('resolveBrowserVerifyInvocation', () => {
462
567
  it('prefers the built entry declared in package metadata', () => {
@@ -554,7 +659,7 @@ describe('browser verify', () => {
554
659
  fs.mkdirSync(adapterDir, { recursive: true });
555
660
  fs.writeFileSync(path.join(adapterDir, 'top.js'), 'export default {};\n', 'utf-8');
556
661
  const program = createProgram('', '');
557
- await program.parseAsync(['node', 'opencli', 'browser', 'verify', 'hn/top', '--no-fixture', '--trace', 'retain-on-failure']);
662
+ await program.parseAsync(['node', 'opencli', 'browser', '--session', 'test', 'verify', 'hn/top', '--no-fixture', '--trace', 'retain-on-failure']);
558
663
  expect(mockExecFileSync).toHaveBeenCalledTimes(1);
559
664
  const [, execArgs] = mockExecFileSync.mock.calls[0];
560
665
  expect(execArgs.slice(-6)).toEqual(['hn', 'top', '--trace', 'retain-on-failure', '--format', 'json']);
@@ -582,7 +687,7 @@ describe('browser verify', () => {
582
687
  fs.mkdirSync(adapterDir, { recursive: true });
583
688
  fs.writeFileSync(path.join(adapterDir, 'top.js'), 'export default {};\n', 'utf-8');
584
689
  const program = createProgram('', '');
585
- await program.parseAsync(['node', 'opencli', 'browser', 'verify', 'hn/top', '--no-fixture', '--seed-args', 'opencli-verify']);
690
+ await program.parseAsync(['node', 'opencli', 'browser', '--session', 'test', 'verify', 'hn/top', '--no-fixture', '--seed-args', 'opencli-verify']);
586
691
  expect(mockExecFileSync).toHaveBeenCalledTimes(1);
587
692
  const [, execArgs] = mockExecFileSync.mock.calls[0];
588
693
  expect(execArgs.slice(-5)).toEqual(['hn', 'top', 'opencli-verify', '--format', 'json']);
@@ -611,7 +716,7 @@ describe('browser verify', () => {
611
716
  fs.mkdirSync(adapterDir, { recursive: true });
612
717
  fs.writeFileSync(path.join(adapterDir, 'top.js'), 'export default {};\n', 'utf-8');
613
718
  const program = createProgram('', '');
614
- await program.parseAsync(['node', 'opencli', 'browser', 'verify', 'hn/top', '--write-fixture', '--seed-args', 'opencli-verify']);
719
+ await program.parseAsync(['node', 'opencli', 'browser', '--session', 'test', 'verify', 'hn/top', '--write-fixture', '--seed-args', 'opencli-verify']);
615
720
  const fixtureFile = path.join(fakeHome, '.opencli', 'sites', 'hn', 'verify', 'top.json');
616
721
  const fixture = JSON.parse(fs.readFileSync(fixtureFile, 'utf-8'));
617
722
  expect(fixture.args).toEqual(['opencli-verify']);
@@ -643,7 +748,7 @@ describe('browser verify', () => {
643
748
  fs.mkdirSync(adapterDir, { recursive: true });
644
749
  fs.writeFileSync(path.join(adapterDir, 'top.js'), 'export default {};\n', 'utf-8');
645
750
  const program = createProgram('', '');
646
- await program.parseAsync(['node', 'opencli', 'browser', 'verify', 'hn/top', '--no-fixture']);
751
+ await program.parseAsync(['node', 'opencli', 'browser', '--session', 'test', 'verify', 'hn/top', '--no-fixture']);
647
752
  expect(process.exitCode).toBe(1);
648
753
  const output = consoleLogSpy.mock.calls.map((args) => args.join(' ')).join('\n');
649
754
  expect(output).toContain('Adapter output violates row shape conventions');
@@ -717,8 +822,8 @@ describe('profile list', () => {
717
822
  describe('browser tab targeting commands', () => {
718
823
  const consoleLogSpy = vi.spyOn(console, 'log').mockImplementation(() => { });
719
824
  const stderrSpy = vi.spyOn(process.stderr, 'write').mockImplementation(() => true);
720
- function getBrowserStateFile(cacheDir) {
721
- return path.join(cacheDir, 'browser-state', 'browser_default.json');
825
+ function getBrowserStateFile(cacheDir, session = 'test') {
826
+ return path.join(cacheDir, 'browser-state', `${session}.json`);
722
827
  }
723
828
  beforeEach(() => {
724
829
  process.exitCode = undefined;
@@ -727,8 +832,9 @@ describe('browser tab targeting commands', () => {
727
832
  stderrSpy.mockClear();
728
833
  mockBrowserConnect.mockClear();
729
834
  mockBrowserClose.mockReset().mockResolvedValue(undefined);
835
+ delete process.env.OPENCLI_WINDOW;
730
836
  mockBindTab.mockReset().mockResolvedValue({
731
- workspace: 'bound:default',
837
+ session: 'test',
732
838
  page: 'tab-2',
733
839
  url: 'https://user.example/inbox',
734
840
  title: 'Inbox',
@@ -759,6 +865,7 @@ describe('browser tab targeting commands', () => {
759
865
  screenshot: vi.fn().mockResolvedValue('base64-shot'),
760
866
  annotatedScreenshot: vi.fn().mockResolvedValue('annotated-base64-shot'),
761
867
  readNetworkCapture: vi.fn().mockResolvedValue([]),
868
+ closeWindow: vi.fn().mockResolvedValue(undefined),
762
869
  waitForDownload: vi.fn().mockResolvedValue({
763
870
  downloaded: true,
764
871
  filename: '/tmp/receipt.pdf',
@@ -766,6 +873,7 @@ describe('browser tab targeting commands', () => {
766
873
  state: 'complete',
767
874
  elapsedMs: 10,
768
875
  }),
876
+ session: 'test',
769
877
  };
770
878
  });
771
879
  function lastJsonLog() {
@@ -777,36 +885,37 @@ describe('browser tab targeting commands', () => {
777
885
  throw new Error(`Expected string arg to console.log, got ${typeof last}`);
778
886
  return JSON.parse(last);
779
887
  }
780
- it('binds the current Chrome tab into a bound workspace', async () => {
888
+ it('binds the current Chrome tab into a browser session', async () => {
781
889
  const program = createProgram('', '');
782
- await program.parseAsync(['node', 'opencli', 'browser', 'bind', '--domain', 'user.example', '--path-prefix', '/inbox']);
783
- expect(mockBrowserConnect).toHaveBeenCalledWith({ timeout: 30, workspace: 'bound:default' });
784
- expect(mockBindTab).toHaveBeenCalledWith('bound:default', {
785
- matchDomain: 'user.example',
786
- matchPathPrefix: '/inbox',
787
- });
890
+ await program.parseAsync(['node', 'opencli', 'browser', '--session', 'test', 'bind']);
891
+ expect(mockBrowserConnect).toHaveBeenCalledWith({ timeout: 30, session: 'test', surface: 'browser' });
892
+ expect(mockBindTab).toHaveBeenCalledWith('test', {});
788
893
  const out = lastJsonLog();
789
- expect(out.workspace).toBe('bound:default');
894
+ expect(out.session).toBe('test');
790
895
  expect(out.url).toBe('https://user.example/inbox');
791
896
  });
792
- it('rejects bind workspaces outside the bound namespace', async () => {
897
+ it('requires an explicit session for browser commands', async () => {
793
898
  const program = createProgram('', '');
794
- await program.parseAsync(['node', 'opencli', 'browser', 'bind', '--workspace', 'browser:default']);
899
+ await program.parseAsync(['node', 'opencli', 'browser', 'state']);
795
900
  expect(mockBrowserConnect).not.toHaveBeenCalled();
796
- expect(mockBindTab).not.toHaveBeenCalled();
797
- const out = lastJsonLog();
798
- expect(out.error.code).toBe('invalid_bind_workspace');
901
+ expect(stderrSpy.mock.calls.flat().join('')).toContain('--session <name> is required');
799
902
  expect(process.exitCode).toBeDefined();
800
903
  });
801
- it('runs browser commands against an explicit bound workspace', async () => {
904
+ it('runs browser commands against an explicit session', async () => {
802
905
  const program = createProgram('', '');
803
- await program.parseAsync(['node', 'opencli', 'browser', '--workspace', 'bound:default', 'state']);
804
- expect(mockBrowserConnect).toHaveBeenCalledWith({ timeout: 30, workspace: 'bound:default' });
906
+ await program.parseAsync(['node', 'opencli', 'browser', '--session', 'test', 'state']);
907
+ expect(mockBrowserConnect).toHaveBeenCalledWith({ timeout: 30, session: 'test', surface: 'browser', windowMode: 'foreground' });
908
+ expect(browserState.page?.snapshot).toHaveBeenCalled();
909
+ });
910
+ it('passes browser --window through Commander options without relying on env pre-processing', async () => {
911
+ const program = createProgram('', '');
912
+ await program.parseAsync(['node', 'opencli', 'browser', '--session', 'test', '--window', 'background', 'state']);
913
+ expect(mockBrowserConnect).toHaveBeenCalledWith({ timeout: 30, session: 'test', surface: 'browser', windowMode: 'background' });
805
914
  expect(browserState.page?.snapshot).toHaveBeenCalled();
806
915
  });
807
916
  it('passes the opt-in AX source to browser state', async () => {
808
917
  const program = createProgram('', '');
809
- await program.parseAsync(['node', 'opencli', 'browser', 'state', '--source', 'ax']);
918
+ await program.parseAsync(['node', 'opencli', 'browser', '--session', 'test', 'state', '--source', 'ax']);
810
919
  expect(browserState.page?.snapshot).toHaveBeenCalledWith({ viewportExpand: 2000, source: 'ax' });
811
920
  });
812
921
  it('prints DOM vs AX snapshot metrics without changing default state output', async () => {
@@ -820,7 +929,7 @@ describe('browser tab targeting commands', () => {
820
929
  }),
821
930
  };
822
931
  const program = createProgram('', '');
823
- await program.parseAsync(['node', 'opencli', 'browser', 'state', '--compare-sources']);
932
+ await program.parseAsync(['node', 'opencli', 'browser', '--session', 'test', 'state', '--compare-sources']);
824
933
  expect(browserState.page?.snapshot).toHaveBeenCalledWith({ viewportExpand: 2000, source: 'dom' });
825
934
  expect(browserState.page?.snapshot).toHaveBeenCalledWith({ viewportExpand: 2000, source: 'ax' });
826
935
  const out = lastJsonLog();
@@ -838,7 +947,7 @@ describe('browser tab targeting commands', () => {
838
947
  }),
839
948
  };
840
949
  const program = createProgram('', '');
841
- await program.parseAsync(['node', 'opencli', 'browser', 'state', '--compare-sources']);
950
+ await program.parseAsync(['node', 'opencli', 'browser', '--session', 'test', 'state', '--compare-sources']);
842
951
  const out = lastJsonLog();
843
952
  expect(out.sources.dom).toMatchObject({ ok: true, refs: 1 });
844
953
  expect(out.sources.ax).toMatchObject({
@@ -848,7 +957,7 @@ describe('browser tab targeting commands', () => {
848
957
  });
849
958
  it('rejects unknown browser state sources before touching the page', async () => {
850
959
  const program = createProgram('', '');
851
- await program.parseAsync(['node', 'opencli', 'browser', 'state', '--source', 'magic']);
960
+ await program.parseAsync(['node', 'opencli', 'browser', '--session', 'test', 'state', '--source', 'magic']);
852
961
  expect(browserState.page?.snapshot).not.toHaveBeenCalled();
853
962
  const out = lastJsonLog();
854
963
  expect(out.error.code).toBe('invalid_source');
@@ -856,7 +965,7 @@ describe('browser tab targeting commands', () => {
856
965
  });
857
966
  it('captures annotated screenshots through the visual ref overlay path', async () => {
858
967
  const program = createProgram('', '');
859
- await program.parseAsync(['node', 'opencli', 'browser', 'screenshot', '--annotate']);
968
+ await program.parseAsync(['node', 'opencli', 'browser', '--session', 'test', 'screenshot', '--annotate']);
860
969
  expect(browserState.page?.annotatedScreenshot).toHaveBeenCalledWith({
861
970
  fullPage: false,
862
971
  annotate: true,
@@ -867,38 +976,36 @@ describe('browser tab targeting commands', () => {
867
976
  expect(browserState.page?.screenshot).not.toHaveBeenCalled();
868
977
  expect(consoleLogSpy).toHaveBeenLastCalledWith('annotated-base64-shot');
869
978
  });
870
- it('blocks history navigation on bound workspaces unless explicitly allowed', async () => {
979
+ it('allows history navigation in a bound session', async () => {
871
980
  browserState.page = {
872
981
  ...browserState.page,
873
- workspace: 'bound:default',
874
982
  evaluate: vi.fn(),
875
983
  wait: vi.fn(),
984
+ session: 'test',
876
985
  };
877
986
  const program = createProgram('', '');
878
- await program.parseAsync(['node', 'opencli', 'browser', '--workspace', 'bound:default', 'back']);
879
- expect(browserState.page?.evaluate).not.toHaveBeenCalled();
880
- const out = lastJsonLog();
881
- expect(out.error.code).toBe('bound_navigation_blocked');
987
+ await program.parseAsync(['node', 'opencli', 'browser', '--session', 'test', 'back']);
988
+ expect(browserState.page?.evaluate).toHaveBeenCalledWith('history.back()');
882
989
  });
883
- it('unbinds a bound workspace through the daemon close-window command', async () => {
990
+ it('unbinds a session through the daemon close-window command', async () => {
884
991
  const program = createProgram('', '');
885
- await program.parseAsync(['node', 'opencli', 'browser', 'unbind', '--workspace', 'bound:default']);
886
- expect(mockBrowserConnect).toHaveBeenCalledWith({ timeout: 30, workspace: 'bound:default' });
887
- expect(mockSendCommand).toHaveBeenCalledWith('close-window', { workspace: 'bound:default' });
992
+ await program.parseAsync(['node', 'opencli', 'browser', '--session', 'test', 'unbind']);
993
+ expect(mockBrowserConnect).toHaveBeenCalledWith({ timeout: 30, session: 'test', surface: 'browser' });
994
+ expect(mockSendCommand).toHaveBeenCalledWith('close-window', { session: 'test', surface: 'browser' });
888
995
  const out = lastJsonLog();
889
- expect(out).toEqual({ unbound: true, workspace: 'bound:default' });
996
+ expect(out).toEqual({ unbound: true, session: 'test' });
890
997
  });
891
998
  it('does not print false success when unbind fails', async () => {
892
- mockSendCommand.mockRejectedValueOnce(new BrowserCommandError('Workspace "bound:default" is not attached to a tab.', 'bound_session_missing', 'Run bind again, then retry the browser command.'));
999
+ mockSendCommand.mockRejectedValueOnce(new BrowserCommandError('Session "test" is not attached to a tab.', 'bound_session_missing', 'Run bind again, then retry the browser command.'));
893
1000
  const program = createProgram('', '');
894
- await program.parseAsync(['node', 'opencli', 'browser', 'unbind', '--workspace', 'bound:default']);
1001
+ await program.parseAsync(['node', 'opencli', 'browser', '--session', 'test', 'unbind']);
895
1002
  const out = lastJsonLog();
896
1003
  expect(out.error.code).toBe('bound_session_missing');
897
1004
  expect(process.exitCode).toBeDefined();
898
1005
  });
899
1006
  it('accepts JavaScript dialogs through the browser dialog command', async () => {
900
1007
  const program = createProgram('', '');
901
- await program.parseAsync(['node', 'opencli', 'browser', 'dialog', 'accept', '--text', 'ok']);
1008
+ await program.parseAsync(['node', 'opencli', 'browser', '--session', 'test', 'dialog', 'accept', '--text', 'ok']);
902
1009
  expect(browserState.page?.handleJavaScriptDialog).toHaveBeenCalledWith(true, 'ok');
903
1010
  const out = lastJsonLog();
904
1011
  expect(out).toEqual({ handled: true, action: 'accept', text: 'ok' });
@@ -909,7 +1016,7 @@ describe('browser tab targeting commands', () => {
909
1016
  evaluate: vi.fn().mockRejectedValue(new Error('JavaScript dialog showing')),
910
1017
  };
911
1018
  const program = createProgram('', '');
912
- await program.parseAsync(['node', 'opencli', 'browser', 'eval', 'document.title']);
1019
+ await program.parseAsync(['node', 'opencli', 'browser', '--session', 'test', 'eval', 'document.title']);
913
1020
  const out = lastJsonLog();
914
1021
  expect(out.error.code).toBe('javascript_dialog_open');
915
1022
  expect(out.error.hint).toContain('browser dialog accept');
@@ -917,7 +1024,7 @@ describe('browser tab targeting commands', () => {
917
1024
  });
918
1025
  it('binds browser commands to an explicit target tab via --tab', async () => {
919
1026
  const program = createProgram('', '');
920
- await program.parseAsync(['node', 'opencli', 'browser', 'eval', '--tab', 'tab-2', 'document.title']);
1027
+ await program.parseAsync(['node', 'opencli', 'browser', '--session', 'test', 'eval', '--tab', 'tab-2', 'document.title']);
921
1028
  expect(browserState.page?.setActivePage).toHaveBeenCalledWith('tab-2');
922
1029
  expect(browserState.page?.evaluate).toHaveBeenCalledWith('document.title');
923
1030
  });
@@ -929,7 +1036,7 @@ describe('browser tab targeting commands', () => {
929
1036
  evaluate: vi.fn(),
930
1037
  };
931
1038
  const program = createProgram('', '');
932
- await program.parseAsync(['node', 'opencli', 'browser', 'eval', '--tab', 'tab-stale', 'document.title']);
1039
+ await program.parseAsync(['node', 'opencli', 'browser', '--session', 'test', 'eval', '--tab', 'tab-stale', 'document.title']);
933
1040
  expect(process.exitCode).toBeDefined();
934
1041
  expect(browserState.page?.setActivePage).not.toHaveBeenCalled();
935
1042
  expect(browserState.page?.evaluate).not.toHaveBeenCalled();
@@ -937,50 +1044,50 @@ describe('browser tab targeting commands', () => {
937
1044
  });
938
1045
  it('lists tabs with target IDs via browser tab list', async () => {
939
1046
  const program = createProgram('', '');
940
- await program.parseAsync(['node', 'opencli', 'browser', 'tab', 'list']);
1047
+ await program.parseAsync(['node', 'opencli', 'browser', '--session', 'test', 'tab', 'list']);
941
1048
  expect(browserState.page?.tabs).toHaveBeenCalledTimes(1);
942
1049
  expect(consoleLogSpy.mock.calls.flat().join('\n')).toContain('"page": "tab-1"');
943
1050
  expect(consoleLogSpy.mock.calls.flat().join('\n')).toContain('"page": "tab-2"');
944
1051
  });
945
1052
  it('creates a new tab and prints its target ID', async () => {
946
1053
  const program = createProgram('', '');
947
- await program.parseAsync(['node', 'opencli', 'browser', 'tab', 'new', 'https://three.example']);
1054
+ await program.parseAsync(['node', 'opencli', 'browser', '--session', 'test', 'tab', 'new', 'https://three.example']);
948
1055
  expect(browserState.page?.newTab).toHaveBeenCalledWith('https://three.example');
949
1056
  expect(consoleLogSpy.mock.calls.flat().join('\n')).toContain('"page": "tab-3"');
950
1057
  });
951
1058
  it('prints the resolved target ID when browser open creates or navigates a tab', async () => {
952
1059
  const program = createProgram('', '');
953
- await program.parseAsync(['node', 'opencli', 'browser', 'open', 'https://example.com']);
1060
+ await program.parseAsync(['node', 'opencli', 'browser', '--session', 'test', 'open', 'https://example.com']);
954
1061
  expect(browserState.page?.goto).toHaveBeenCalledWith('https://example.com');
955
1062
  expect(consoleLogSpy.mock.calls.flat().join('\n')).toContain('"url": "https://one.example"');
956
1063
  expect(consoleLogSpy.mock.calls.flat().join('\n')).toContain('"page": "tab-1"');
957
1064
  });
958
1065
  it('lists cross-origin frames via browser frames', async () => {
959
1066
  const program = createProgram('', '');
960
- await program.parseAsync(['node', 'opencli', 'browser', 'frames']);
1067
+ await program.parseAsync(['node', 'opencli', 'browser', '--session', 'test', 'frames']);
961
1068
  expect(browserState.page?.frames).toHaveBeenCalledTimes(1);
962
1069
  expect(consoleLogSpy.mock.calls.flat().join('\n')).toContain('"frameId": "frame-1"');
963
1070
  expect(consoleLogSpy.mock.calls.flat().join('\n')).toContain('"url": "https://x.example/embed"');
964
1071
  });
965
1072
  it('routes browser eval --frame through frame-targeted evaluation', async () => {
966
1073
  const program = createProgram('', '');
967
- await program.parseAsync(['node', 'opencli', 'browser', 'eval', '--frame', '0', 'document.title']);
1074
+ await program.parseAsync(['node', 'opencli', 'browser', '--session', 'test', 'eval', '--frame', '0', 'document.title']);
968
1075
  expect(browserState.page?.evaluateInFrame).toHaveBeenCalledWith('document.title', 0);
969
1076
  expect(browserState.page?.evaluate).not.toHaveBeenCalled();
970
1077
  expect(consoleLogSpy.mock.calls.flat().join('\n')).toContain('inside frame');
971
1078
  });
972
1079
  it('does not promote a newly created tab to the persisted default target', async () => {
973
1080
  const program = createProgram('', '');
974
- await program.parseAsync(['node', 'opencli', 'browser', 'tab', 'new', 'https://three.example']);
975
- await program.parseAsync(['node', 'opencli', 'browser', 'eval', 'document.title']);
1081
+ await program.parseAsync(['node', 'opencli', 'browser', '--session', 'test', 'tab', 'new', 'https://three.example']);
1082
+ await program.parseAsync(['node', 'opencli', 'browser', '--session', 'test', 'eval', 'document.title']);
976
1083
  expect(browserState.page?.newTab).toHaveBeenCalledWith('https://three.example');
977
1084
  expect(browserState.page?.setActivePage).not.toHaveBeenCalled();
978
1085
  expect(browserState.page?.evaluate).toHaveBeenCalledWith('document.title');
979
1086
  });
980
1087
  it('persists an explicitly selected tab as the default target for later untargeted commands', async () => {
981
1088
  const program = createProgram('', '');
982
- await program.parseAsync(['node', 'opencli', 'browser', 'tab', 'select', 'tab-2']);
983
- await program.parseAsync(['node', 'opencli', 'browser', 'eval', 'document.title']);
1089
+ await program.parseAsync(['node', 'opencli', 'browser', '--session', 'test', 'tab', 'select', 'tab-2']);
1090
+ await program.parseAsync(['node', 'opencli', 'browser', '--session', 'test', 'eval', 'document.title']);
984
1091
  expect(browserState.page?.selectTab).toHaveBeenCalledWith('tab-2');
985
1092
  expect(browserState.page?.setActivePage).toHaveBeenCalledWith('tab-2');
986
1093
  expect(browserState.page?.evaluate).toHaveBeenCalledWith('document.title');
@@ -989,7 +1096,7 @@ describe('browser tab targeting commands', () => {
989
1096
  it('clears a saved default target when it is no longer present in the current session', async () => {
990
1097
  const cacheDir = String(process.env.OPENCLI_CACHE_DIR);
991
1098
  const program = createProgram('', '');
992
- await program.parseAsync(['node', 'opencli', 'browser', 'tab', 'select', 'tab-2']);
1099
+ await program.parseAsync(['node', 'opencli', 'browser', '--session', 'test', 'tab', 'select', 'tab-2']);
993
1100
  expect(fs.existsSync(getBrowserStateFile(cacheDir))).toBe(true);
994
1101
  browserState.page = {
995
1102
  setActivePage: vi.fn(),
@@ -998,35 +1105,36 @@ describe('browser tab targeting commands', () => {
998
1105
  evaluate: vi.fn().mockResolvedValue({ ok: true }),
999
1106
  readNetworkCapture: vi.fn().mockResolvedValue([]),
1000
1107
  };
1001
- await program.parseAsync(['node', 'opencli', 'browser', 'eval', 'document.title']);
1108
+ await program.parseAsync(['node', 'opencli', 'browser', '--session', 'test', 'eval', 'document.title']);
1002
1109
  expect(browserState.page?.setActivePage).not.toHaveBeenCalled();
1003
1110
  expect(browserState.page?.evaluate).toHaveBeenCalledWith('document.title');
1004
1111
  expect(fs.existsSync(getBrowserStateFile(cacheDir))).toBe(false);
1005
1112
  });
1006
1113
  it('clears the persisted default target when that tab is closed', async () => {
1007
1114
  const program = createProgram('', '');
1008
- await program.parseAsync(['node', 'opencli', 'browser', 'tab', 'select', 'tab-2']);
1009
- await program.parseAsync(['node', 'opencli', 'browser', 'tab', 'close', 'tab-2']);
1115
+ await program.parseAsync(['node', 'opencli', 'browser', '--session', 'test', 'tab', 'select', 'tab-2']);
1116
+ await program.parseAsync(['node', 'opencli', 'browser', '--session', 'test', 'tab', 'close', 'tab-2']);
1010
1117
  vi.mocked(browserState.page?.setActivePage).mockClear();
1011
1118
  vi.mocked(browserState.page?.evaluate).mockClear();
1012
- await program.parseAsync(['node', 'opencli', 'browser', 'eval', 'document.title']);
1119
+ await program.parseAsync(['node', 'opencli', 'browser', '--session', 'test', 'eval', 'document.title']);
1013
1120
  expect(browserState.page?.closeTab).toHaveBeenCalledWith('tab-2');
1014
1121
  expect(browserState.page?.setActivePage).not.toHaveBeenCalled();
1015
1122
  expect(browserState.page?.evaluate).toHaveBeenCalledWith('document.title');
1016
1123
  });
1017
1124
  it('closes a tab by target ID', async () => {
1018
1125
  const program = createProgram('', '');
1019
- await program.parseAsync(['node', 'opencli', 'browser', 'tab', 'close', 'tab-2']);
1126
+ await program.parseAsync(['node', 'opencli', 'browser', '--session', 'test', 'tab', 'close', 'tab-2']);
1020
1127
  expect(browserState.page?.closeTab).toHaveBeenCalledWith('tab-2');
1021
1128
  expect(consoleLogSpy.mock.calls.flat().join('\n')).toContain('"closed": "tab-2"');
1022
1129
  });
1023
1130
  it('rejects closing a stale tab target ID that is no longer in the current session', async () => {
1024
1131
  browserState.page = {
1132
+ session: 'test',
1025
1133
  tabs: vi.fn().mockResolvedValue([]),
1026
1134
  closeTab: vi.fn(),
1027
1135
  };
1028
1136
  const program = createProgram('', '');
1029
- await program.parseAsync(['node', 'opencli', 'browser', 'tab', 'close', 'tab-stale']);
1137
+ await program.parseAsync(['node', 'opencli', 'browser', '--session', 'test', 'tab', 'close', 'tab-stale']);
1030
1138
  expect(process.exitCode).toBeDefined();
1031
1139
  expect(browserState.page?.closeTab).not.toHaveBeenCalled();
1032
1140
  expect(stderrSpy.mock.calls.flat().join('\n')).toContain('Target tab tab-stale is not part of the current browser session');
@@ -1073,7 +1181,7 @@ describe('browser tab targeting commands', () => {
1073
1181
  ]),
1074
1182
  };
1075
1183
  const program = createProgram('', '');
1076
- await program.parseAsync(['node', 'opencli', 'browser', 'analyze', 'https://target.example/']);
1184
+ await program.parseAsync(['node', 'opencli', 'browser', '--session', 'test', 'analyze', 'https://target.example/']);
1077
1185
  const out = lastJsonLog();
1078
1186
  expect(browserState.page?.readNetworkCapture).toHaveBeenCalledTimes(2);
1079
1187
  expect(out.anti_bot.vendor).toBe('cloudflare');
@@ -1134,7 +1242,7 @@ describe('browser tab targeting commands', () => {
1134
1242
  readNetworkCapture: vi.fn().mockResolvedValue([]),
1135
1243
  };
1136
1244
  const program = createProgram('', '');
1137
- await program.parseAsync(['node', 'opencli', 'browser', 'analyze', 'https://target.example/']);
1245
+ await program.parseAsync(['node', 'opencli', 'browser', '--session', 'test', 'analyze', 'https://target.example/']);
1138
1246
  const out = lastJsonLog();
1139
1247
  expect(browserState.page?.readNetworkCapture).toHaveBeenCalledTimes(2);
1140
1248
  expect(bufferReads).toBe(2);
@@ -1173,7 +1281,7 @@ describe('browser tab targeting commands', () => {
1173
1281
  ]),
1174
1282
  };
1175
1283
  const program = createProgram('', '');
1176
- await program.parseAsync(['node', 'opencli', 'browser', 'wait', 'xhr', '/api/target', '--timeout', '900']);
1284
+ await program.parseAsync(['node', 'opencli', 'browser', '--session', 'test', 'wait', 'xhr', '/api/target', '--timeout', '900']);
1177
1285
  const out = lastJsonLog();
1178
1286
  expect(browserState.page?.startNetworkCapture).toHaveBeenCalledTimes(1);
1179
1287
  expect(browserState.page?.evaluate).toHaveBeenCalledWith(expect.stringContaining('window.__opencli_net'));
@@ -1221,7 +1329,7 @@ describe('browser tab targeting commands', () => {
1221
1329
  readNetworkCapture: vi.fn().mockResolvedValue([]),
1222
1330
  };
1223
1331
  const program = createProgram('', '');
1224
- await program.parseAsync(['node', 'opencli', 'browser', 'wait', 'xhr', '/api/target', '--timeout', '900']);
1332
+ await program.parseAsync(['node', 'opencli', 'browser', '--session', 'test', 'wait', 'xhr', '/api/target', '--timeout', '900']);
1225
1333
  const out = lastJsonLog();
1226
1334
  expect(browserState.page?.startNetworkCapture).toHaveBeenCalledTimes(1);
1227
1335
  expect(browserState.page?.readNetworkCapture).toHaveBeenCalledTimes(2);
@@ -1245,7 +1353,7 @@ describe('browser tab targeting commands', () => {
1245
1353
  tabs: vi.fn().mockResolvedValue([{ index: 0, page: 'tab-1', url: 'https://target.example', title: 'Target', active: true }]),
1246
1354
  };
1247
1355
  const program = createProgram('', '');
1248
- await program.parseAsync(['node', 'opencli', 'browser', 'wait', 'download', 'receipt', '--timeout', '900']);
1356
+ await program.parseAsync(['node', 'opencli', 'browser', '--session', 'test', 'wait', 'download', 'receipt', '--timeout', '900']);
1249
1357
  expect(browserState.page?.waitForDownload).toHaveBeenCalledWith('receipt', 900);
1250
1358
  expect(lastJsonLog()).toEqual({
1251
1359
  downloaded: true,
@@ -1271,7 +1379,7 @@ describe('browser tab targeting commands', () => {
1271
1379
  tabs: vi.fn().mockResolvedValue([{ index: 0, page: 'tab-1', url: 'https://target.example', title: 'Target', active: true }]),
1272
1380
  };
1273
1381
  const program = createProgram('', '');
1274
- await program.parseAsync(['node', 'opencli', 'browser', 'wait', 'download', 'receipt', '--timeout', '900']);
1382
+ await program.parseAsync(['node', 'opencli', 'browser', '--session', 'test', 'wait', 'download', 'receipt', '--timeout', '900']);
1275
1383
  const out = lastJsonLog();
1276
1384
  expect(out.error.code).toBe('download_not_seen');
1277
1385
  expect(out.download.elapsedMs).toBe(900);
@@ -1281,10 +1389,10 @@ describe('browser tab targeting commands', () => {
1281
1389
  describe('browser network command', () => {
1282
1390
  const consoleLogSpy = vi.spyOn(console, 'log').mockImplementation(() => { });
1283
1391
  function getNetworkCachePath(cacheDir) {
1284
- return path.join(cacheDir, 'browser-network', 'browser_default.json');
1392
+ return path.join(cacheDir, 'browser-network', 'test.json');
1285
1393
  }
1286
- function getBoundNetworkCachePath(cacheDir) {
1287
- return path.join(cacheDir, 'browser-network', 'bound_default.json');
1394
+ function getCustomNetworkCachePath(cacheDir) {
1395
+ return path.join(cacheDir, 'browser-network', 'custom.json');
1288
1396
  }
1289
1397
  function lastJsonLog() {
1290
1398
  const calls = consoleLogSpy.mock.calls;
@@ -1302,6 +1410,7 @@ describe('browser network command', () => {
1302
1410
  mockBrowserConnect.mockClear();
1303
1411
  mockBrowserClose.mockReset().mockResolvedValue(undefined);
1304
1412
  browserState.page = {
1413
+ session: 'test',
1305
1414
  setActivePage: vi.fn(),
1306
1415
  getActivePage: vi.fn().mockReturnValue('tab-1'),
1307
1416
  tabs: vi.fn().mockResolvedValue([{ page: 'tab-1', active: true }]),
@@ -1328,7 +1437,7 @@ describe('browser network command', () => {
1328
1437
  it('emits JSON with shape previews and persists the capture to disk', async () => {
1329
1438
  const cacheDir = String(process.env.OPENCLI_CACHE_DIR);
1330
1439
  const program = createProgram('', '');
1331
- await program.parseAsync(['node', 'opencli', 'browser', 'network']);
1440
+ await program.parseAsync(['node', 'opencli', 'browser', '--session', 'test', 'network']);
1332
1441
  const out = lastJsonLog();
1333
1442
  expect(out.count).toBe(1);
1334
1443
  expect(out.filtered_out).toBe(1);
@@ -1337,22 +1446,22 @@ describe('browser network command', () => {
1337
1446
  expect(out.entries[0]).not.toHaveProperty('body');
1338
1447
  expect(fs.existsSync(getNetworkCachePath(cacheDir))).toBe(true);
1339
1448
  });
1340
- it('uses the selected browser workspace for network cache scope', async () => {
1449
+ it('uses the selected browser session for network cache scope', async () => {
1341
1450
  const cacheDir = String(process.env.OPENCLI_CACHE_DIR);
1342
1451
  browserState.page = {
1343
1452
  ...browserState.page,
1344
- workspace: 'bound:default',
1453
+ session: 'custom',
1345
1454
  };
1346
1455
  const program = createProgram('', '');
1347
- await program.parseAsync(['node', 'opencli', 'browser', '--workspace', 'bound:default', 'network']);
1456
+ await program.parseAsync(['node', 'opencli', 'browser', '--session', 'custom', 'network']);
1348
1457
  const out = lastJsonLog();
1349
- expect(out.workspace).toBe('bound:default');
1350
- expect(fs.existsSync(getBoundNetworkCachePath(cacheDir))).toBe(true);
1458
+ expect(out.session).toBe('custom');
1459
+ expect(fs.existsSync(getCustomNetworkCachePath(cacheDir))).toBe(true);
1351
1460
  expect(fs.existsSync(getNetworkCachePath(cacheDir))).toBe(false);
1352
1461
  });
1353
1462
  it('--all includes static resources that the default filter drops', async () => {
1354
1463
  const program = createProgram('', '');
1355
- await program.parseAsync(['node', 'opencli', 'browser', 'network', '--all']);
1464
+ await program.parseAsync(['node', 'opencli', 'browser', '--session', 'test', 'network', '--all']);
1356
1465
  const out = lastJsonLog();
1357
1466
  expect(out.count).toBe(2);
1358
1467
  expect(out.entries.map((e) => e.key)).toContain('UserTweets');
@@ -1387,7 +1496,7 @@ describe('browser network command', () => {
1387
1496
  },
1388
1497
  ]);
1389
1498
  const program = createProgram('', '');
1390
- await program.parseAsync(['node', 'opencli', 'browser', 'network', '--since', '120s', '--failed']);
1499
+ await program.parseAsync(['node', 'opencli', 'browser', '--session', 'test', 'network', '--since', '120s', '--failed']);
1391
1500
  const out = lastJsonLog();
1392
1501
  expect(out.count).toBe(1);
1393
1502
  expect(out.entries[0].url).toBe('https://api.example.com/new-fail');
@@ -1411,7 +1520,7 @@ describe('browser network command', () => {
1411
1520
  },
1412
1521
  ]);
1413
1522
  const program = createProgram('', '');
1414
- await program.parseAsync(['node', 'opencli', 'browser', 'network']);
1523
+ await program.parseAsync(['node', 'opencli', 'browser', '--session', 'test', 'network']);
1415
1524
  const out = lastJsonLog();
1416
1525
  expect(out.count).toBe(1);
1417
1526
  expect(out.filtered_out).toBe(1);
@@ -1421,16 +1530,16 @@ describe('browser network command', () => {
1421
1530
  });
1422
1531
  it('--raw emits full bodies inline for every entry', async () => {
1423
1532
  const program = createProgram('', '');
1424
- await program.parseAsync(['node', 'opencli', 'browser', 'network', '--raw']);
1533
+ await program.parseAsync(['node', 'opencli', 'browser', '--session', 'test', 'network', '--raw']);
1425
1534
  const out = lastJsonLog();
1426
1535
  expect(out.entries[0].body).toEqual({ data: { user: { rest_id: '42' } } });
1427
1536
  expect(out.entries[0].timestamp).toMatch(/T/);
1428
1537
  });
1429
1538
  it('--detail <key> returns the full body for the requested entry', async () => {
1430
1539
  const program = createProgram('', '');
1431
- await program.parseAsync(['node', 'opencli', 'browser', 'network']);
1540
+ await program.parseAsync(['node', 'opencli', 'browser', '--session', 'test', 'network']);
1432
1541
  consoleLogSpy.mockClear();
1433
- await program.parseAsync(['node', 'opencli', 'browser', 'network', '--detail', 'UserTweets']);
1542
+ await program.parseAsync(['node', 'opencli', 'browser', '--session', 'test', 'network', '--detail', 'UserTweets']);
1434
1543
  const out = lastJsonLog();
1435
1544
  expect(out.key).toBe('UserTweets');
1436
1545
  expect(out.body).toEqual({ data: { user: { rest_id: '42' } } });
@@ -1439,9 +1548,9 @@ describe('browser network command', () => {
1439
1548
  });
1440
1549
  it('--detail reports key_not_found with the list of available keys', async () => {
1441
1550
  const program = createProgram('', '');
1442
- await program.parseAsync(['node', 'opencli', 'browser', 'network']);
1551
+ await program.parseAsync(['node', 'opencli', 'browser', '--session', 'test', 'network']);
1443
1552
  consoleLogSpy.mockClear();
1444
- await program.parseAsync(['node', 'opencli', 'browser', 'network', '--detail', 'NopeOp']);
1553
+ await program.parseAsync(['node', 'opencli', 'browser', '--session', 'test', 'network', '--detail', 'NopeOp']);
1445
1554
  const out = lastJsonLog();
1446
1555
  expect(out.error.code).toBe('key_not_found');
1447
1556
  expect(out.error.available_keys).toContain('UserTweets');
@@ -1449,7 +1558,7 @@ describe('browser network command', () => {
1449
1558
  });
1450
1559
  it('--detail reports cache_missing when no capture has been persisted yet', async () => {
1451
1560
  const program = createProgram('', '');
1452
- await program.parseAsync(['node', 'opencli', 'browser', 'network', '--detail', 'UserTweets']);
1561
+ await program.parseAsync(['node', 'opencli', 'browser', '--session', 'test', 'network', '--detail', 'UserTweets']);
1453
1562
  const out = lastJsonLog();
1454
1563
  expect(out.error.code).toBe('cache_missing');
1455
1564
  expect(process.exitCode).toBeDefined();
@@ -1457,7 +1566,7 @@ describe('browser network command', () => {
1457
1566
  it('emits capture_failed when readNetworkCapture throws', async () => {
1458
1567
  browserState.page.readNetworkCapture = vi.fn().mockRejectedValue(new Error('CDP disconnected'));
1459
1568
  const program = createProgram('', '');
1460
- await program.parseAsync(['node', 'opencli', 'browser', 'network']);
1569
+ await program.parseAsync(['node', 'opencli', 'browser', '--session', 'test', 'network']);
1461
1570
  const out = lastJsonLog();
1462
1571
  expect(out.error.code).toBe('capture_failed');
1463
1572
  expect(out.error.message).toContain('CDP disconnected');
@@ -1470,7 +1579,7 @@ describe('browser network command', () => {
1470
1579
  const clashDir = path.join(cacheDir, 'browser-network');
1471
1580
  fs.writeFileSync(clashDir, 'not-a-directory');
1472
1581
  const program = createProgram('', '');
1473
- await program.parseAsync(['node', 'opencli', 'browser', 'network']);
1582
+ await program.parseAsync(['node', 'opencli', 'browser', '--session', 'test', 'network']);
1474
1583
  const out = lastJsonLog();
1475
1584
  expect(out.cache_warning).toMatch(/Could not persist capture cache/);
1476
1585
  expect(out.count).toBe(1);
@@ -1495,7 +1604,7 @@ describe('browser network command', () => {
1495
1604
  });
1496
1605
  it('narrows entries to those whose shape has ALL named fields', async () => {
1497
1606
  const program = createProgram('', '');
1498
- await program.parseAsync(['node', 'opencli', 'browser', 'network', '--filter', 'author,text,likes']);
1607
+ await program.parseAsync(['node', 'opencli', 'browser', '--session', 'test', 'network', '--filter', 'author,text,likes']);
1499
1608
  const out = lastJsonLog();
1500
1609
  expect(out.count).toBe(1);
1501
1610
  expect(out.filter).toEqual(['author', 'text', 'likes']);
@@ -1504,14 +1613,14 @@ describe('browser network command', () => {
1504
1613
  });
1505
1614
  it('matches container segments too, not just leaf names (any-segment rule)', async () => {
1506
1615
  const program = createProgram('', '');
1507
- await program.parseAsync(['node', 'opencli', 'browser', 'network', '--filter', 'data,items']);
1616
+ await program.parseAsync(['node', 'opencli', 'browser', '--session', 'test', 'network', '--filter', 'data,items']);
1508
1617
  const out = lastJsonLog();
1509
1618
  expect(out.count).toBe(1);
1510
1619
  expect(out.entries[0].key).toBe('UserTweets');
1511
1620
  });
1512
1621
  it('drops entries that are missing any required field (AND semantics)', async () => {
1513
1622
  const program = createProgram('', '');
1514
- await program.parseAsync(['node', 'opencli', 'browser', 'network', '--filter', 'author,followers']);
1623
+ await program.parseAsync(['node', 'opencli', 'browser', '--session', 'test', 'network', '--filter', 'author,followers']);
1515
1624
  const out = lastJsonLog();
1516
1625
  expect(out.count).toBe(0);
1517
1626
  expect(out.entries).toEqual([]);
@@ -1520,7 +1629,7 @@ describe('browser network command', () => {
1520
1629
  });
1521
1630
  it('returns empty entries (not an error) when nothing matches', async () => {
1522
1631
  const program = createProgram('', '');
1523
- await program.parseAsync(['node', 'opencli', 'browser', 'network', '--filter', 'nonexistent_field']);
1632
+ await program.parseAsync(['node', 'opencli', 'browser', '--session', 'test', 'network', '--filter', 'nonexistent_field']);
1524
1633
  const out = lastJsonLog();
1525
1634
  expect(out.count).toBe(0);
1526
1635
  expect(out.entries).toEqual([]);
@@ -1529,43 +1638,43 @@ describe('browser network command', () => {
1529
1638
  });
1530
1639
  it('is case-sensitive so agents do not conflate `Id` with `id`', async () => {
1531
1640
  const program = createProgram('', '');
1532
- await program.parseAsync(['node', 'opencli', 'browser', 'network', '--filter', 'Data']);
1641
+ await program.parseAsync(['node', 'opencli', 'browser', '--session', 'test', 'network', '--filter', 'Data']);
1533
1642
  const out = lastJsonLog();
1534
1643
  expect(out.count).toBe(0);
1535
1644
  });
1536
1645
  it('persists the full (unfiltered) capture so --detail lookups still find filtered-out keys', async () => {
1537
1646
  const program = createProgram('', '');
1538
- await program.parseAsync(['node', 'opencli', 'browser', 'network', '--filter', 'author,text,likes']);
1647
+ await program.parseAsync(['node', 'opencli', 'browser', '--session', 'test', 'network', '--filter', 'author,text,likes']);
1539
1648
  consoleLogSpy.mockClear();
1540
- await program.parseAsync(['node', 'opencli', 'browser', 'network', '--detail', 'UserProfile']);
1649
+ await program.parseAsync(['node', 'opencli', 'browser', '--session', 'test', 'network', '--detail', 'UserProfile']);
1541
1650
  const out = lastJsonLog();
1542
1651
  expect(out.key).toBe('UserProfile');
1543
1652
  expect(out.body).toEqual({ data: { user: { id: 'u1', followers: 10 } } });
1544
1653
  });
1545
1654
  it('composes with --raw: entries keep full bodies, filter still narrows', async () => {
1546
1655
  const program = createProgram('', '');
1547
- await program.parseAsync(['node', 'opencli', 'browser', 'network', '--filter', 'author', '--raw']);
1656
+ await program.parseAsync(['node', 'opencli', 'browser', '--session', 'test', 'network', '--filter', 'author', '--raw']);
1548
1657
  const out = lastJsonLog();
1549
1658
  expect(out.count).toBe(1);
1550
1659
  expect(out.entries[0].body).toEqual({ data: { items: [{ author: 'a', text: 't', likes: 1 }] } });
1551
1660
  });
1552
1661
  it('reports invalid_filter for empty value', async () => {
1553
1662
  const program = createProgram('', '');
1554
- await program.parseAsync(['node', 'opencli', 'browser', 'network', '--filter', '']);
1663
+ await program.parseAsync(['node', 'opencli', 'browser', '--session', 'test', 'network', '--filter', '']);
1555
1664
  const out = lastJsonLog();
1556
1665
  expect(out.error.code).toBe('invalid_filter');
1557
1666
  expect(process.exitCode).toBeDefined();
1558
1667
  });
1559
1668
  it('reports invalid_filter for commas-only value', async () => {
1560
1669
  const program = createProgram('', '');
1561
- await program.parseAsync(['node', 'opencli', 'browser', 'network', '--filter', ',,,']);
1670
+ await program.parseAsync(['node', 'opencli', 'browser', '--session', 'test', 'network', '--filter', ',,,']);
1562
1671
  const out = lastJsonLog();
1563
1672
  expect(out.error.code).toBe('invalid_filter');
1564
1673
  expect(process.exitCode).toBeDefined();
1565
1674
  });
1566
1675
  it('rejects --filter combined with --detail as invalid_args', async () => {
1567
1676
  const program = createProgram('', '');
1568
- await program.parseAsync(['node', 'opencli', 'browser', 'network', '--filter', 'author', '--detail', 'UserTweets']);
1677
+ await program.parseAsync(['node', 'opencli', 'browser', '--session', 'test', 'network', '--filter', 'author', '--detail', 'UserTweets']);
1569
1678
  const out = lastJsonLog();
1570
1679
  expect(out.error.code).toBe('invalid_args');
1571
1680
  expect(out.error.message).toContain('--filter');
@@ -1587,7 +1696,7 @@ describe('browser network command', () => {
1587
1696
  },
1588
1697
  ]);
1589
1698
  const program = createProgram('', '');
1590
- await program.parseAsync(['node', 'opencli', 'browser', 'network']);
1699
+ await program.parseAsync(['node', 'opencli', 'browser', '--session', 'test', 'network']);
1591
1700
  const out = lastJsonLog();
1592
1701
  expect(out.body_truncated_count).toBe(1);
1593
1702
  expect(out.entries[0].body_truncated).toBe(true);
@@ -1606,9 +1715,9 @@ describe('browser network command', () => {
1606
1715
  },
1607
1716
  ]);
1608
1717
  const program = createProgram('', '');
1609
- await program.parseAsync(['node', 'opencli', 'browser', 'network']);
1718
+ await program.parseAsync(['node', 'opencli', 'browser', '--session', 'test', 'network']);
1610
1719
  consoleLogSpy.mockClear();
1611
- await program.parseAsync(['node', 'opencli', 'browser', 'network', '--detail', 'GET api.example.com/huge']);
1720
+ await program.parseAsync(['node', 'opencli', 'browser', '--session', 'test', 'network', '--detail', 'GET api.example.com/huge']);
1612
1721
  const out = lastJsonLog();
1613
1722
  expect(out.body_truncated).toBe(true);
1614
1723
  expect(out.body_full_size).toBe(50_000_000);
@@ -1626,10 +1735,10 @@ describe('browser network command', () => {
1626
1735
  },
1627
1736
  ]);
1628
1737
  const program = createProgram('', '');
1629
- await program.parseAsync(['node', 'opencli', 'browser', 'network']);
1738
+ await program.parseAsync(['node', 'opencli', 'browser', '--session', 'test', 'network']);
1630
1739
  consoleLogSpy.mockClear();
1631
1740
  await program.parseAsync([
1632
- 'node', 'opencli', 'browser', 'network',
1741
+ 'node', 'opencli', 'browser', '--session', 'test', 'network',
1633
1742
  '--detail', 'GET api.example.com/plain',
1634
1743
  '--max-body', '100',
1635
1744
  ]);
@@ -1651,10 +1760,10 @@ describe('browser network command', () => {
1651
1760
  },
1652
1761
  ]);
1653
1762
  const program = createProgram('', '');
1654
- await program.parseAsync(['node', 'opencli', 'browser', 'network']);
1763
+ await program.parseAsync(['node', 'opencli', 'browser', '--session', 'test', 'network']);
1655
1764
  consoleLogSpy.mockClear();
1656
1765
  await program.parseAsync([
1657
- 'node', 'opencli', 'browser', 'network',
1766
+ 'node', 'opencli', 'browser', '--session', 'test', 'network',
1658
1767
  '--detail', 'GET api.example.com/json',
1659
1768
  '--max-body', '10',
1660
1769
  ]);
@@ -1675,10 +1784,10 @@ describe('browser network command', () => {
1675
1784
  },
1676
1785
  ]);
1677
1786
  const program = createProgram('', '');
1678
- await program.parseAsync(['node', 'opencli', 'browser', 'network']);
1787
+ await program.parseAsync(['node', 'opencli', 'browser', '--session', 'test', 'network']);
1679
1788
  consoleLogSpy.mockClear();
1680
1789
  await program.parseAsync([
1681
- 'node', 'opencli', 'browser', 'network',
1790
+ 'node', 'opencli', 'browser', '--session', 'test', 'network',
1682
1791
  '--detail', 'GET api.example.com/x',
1683
1792
  '--max-body', 'abc',
1684
1793
  ]);
@@ -1698,7 +1807,7 @@ describe('browser network command', () => {
1698
1807
  },
1699
1808
  ]);
1700
1809
  const program = createProgram('', '');
1701
- await program.parseAsync(['node', 'opencli', 'browser', 'network', '--raw']);
1810
+ await program.parseAsync(['node', 'opencli', 'browser', '--session', 'test', 'network', '--raw']);
1702
1811
  const out = lastJsonLog();
1703
1812
  expect(out.entries).toHaveLength(1);
1704
1813
  const entry = out.entries[0];
@@ -1719,6 +1828,7 @@ describe('browser console command', () => {
1719
1828
  mockBrowserClose.mockReset().mockResolvedValue(undefined);
1720
1829
  const now = Date.now();
1721
1830
  browserState.page = {
1831
+ session: 'test',
1722
1832
  setActivePage: vi.fn(),
1723
1833
  getActivePage: vi.fn().mockReturnValue('tab-1'),
1724
1834
  tabs: vi.fn().mockResolvedValue([{ page: 'tab-1', active: true }]),
@@ -1740,7 +1850,7 @@ describe('browser console command', () => {
1740
1850
  }
1741
1851
  it('filters console messages by level and time window', async () => {
1742
1852
  const program = createProgram('', '');
1743
- await program.parseAsync(['node', 'opencli', 'browser', 'console', '--level', 'error', '--since', '120s']);
1853
+ await program.parseAsync(['node', 'opencli', 'browser', '--session', 'test', 'console', '--level', 'error', '--since', '120s']);
1744
1854
  const out = lastJsonLog();
1745
1855
  expect(out.count).toBe(1);
1746
1856
  expect(out.messages[0]).toMatchObject({ type: 'error', text: 'boom' });
@@ -1777,14 +1887,14 @@ describe('browser get html command', () => {
1777
1887
  const big = '<div>' + 'x'.repeat(100_000) + '</div>';
1778
1888
  browserState.page.evaluate.mockResolvedValueOnce({ kind: 'ok', html: big });
1779
1889
  const program = createProgram('', '');
1780
- await program.parseAsync(['node', 'opencli', 'browser', 'get', 'html']);
1890
+ await program.parseAsync(['node', 'opencli', 'browser', '--session', 'test', 'get', 'html']);
1781
1891
  expect(lastLogArg()).toBe(big);
1782
1892
  });
1783
1893
  it('caps output with --max and prepends a visible truncation marker', async () => {
1784
1894
  const big = '<div>' + 'x'.repeat(500) + '</div>';
1785
1895
  browserState.page.evaluate.mockResolvedValueOnce({ kind: 'ok', html: big });
1786
1896
  const program = createProgram('', '');
1787
- await program.parseAsync(['node', 'opencli', 'browser', 'get', 'html', '--max', '100']);
1897
+ await program.parseAsync(['node', 'opencli', 'browser', '--session', 'test', 'get', 'html', '--max', '100']);
1788
1898
  const out = String(lastLogArg());
1789
1899
  expect(out.startsWith('<!-- opencli: truncated 100 of')).toBe(true);
1790
1900
  expect(out.length).toBeGreaterThan(100);
@@ -1792,21 +1902,21 @@ describe('browser get html command', () => {
1792
1902
  });
1793
1903
  it('rejects negative --max with invalid_max error', async () => {
1794
1904
  const program = createProgram('', '');
1795
- await program.parseAsync(['node', 'opencli', 'browser', 'get', 'html', '--max', '-1']);
1905
+ await program.parseAsync(['node', 'opencli', 'browser', '--session', 'test', 'get', 'html', '--max', '-1']);
1796
1906
  expect(lastJsonLog().error.code).toBe('invalid_max');
1797
1907
  expect(process.exitCode).toBeDefined();
1798
1908
  expect(browserState.page.evaluate).not.toHaveBeenCalled();
1799
1909
  });
1800
1910
  it('rejects fractional --max with invalid_max error', async () => {
1801
1911
  const program = createProgram('', '');
1802
- await program.parseAsync(['node', 'opencli', 'browser', 'get', 'html', '--max', '1.5']);
1912
+ await program.parseAsync(['node', 'opencli', 'browser', '--session', 'test', 'get', 'html', '--max', '1.5']);
1803
1913
  expect(lastJsonLog().error.code).toBe('invalid_max');
1804
1914
  expect(process.exitCode).toBeDefined();
1805
1915
  expect(browserState.page.evaluate).not.toHaveBeenCalled();
1806
1916
  });
1807
1917
  it('rejects non-numeric --max (e.g. "10abc") with invalid_max error', async () => {
1808
1918
  const program = createProgram('', '');
1809
- await program.parseAsync(['node', 'opencli', 'browser', 'get', 'html', '--max', '10abc']);
1919
+ await program.parseAsync(['node', 'opencli', 'browser', '--session', 'test', 'get', 'html', '--max', '10abc']);
1810
1920
  expect(lastJsonLog().error.code).toBe('invalid_max');
1811
1921
  expect(process.exitCode).toBeDefined();
1812
1922
  expect(browserState.page.evaluate).not.toHaveBeenCalled();
@@ -1818,7 +1928,7 @@ describe('browser get html command', () => {
1818
1928
  tree: { tag: 'div', attrs: { class: 'hero' }, text: 'Hi', children: [] },
1819
1929
  });
1820
1930
  const program = createProgram('', '');
1821
- await program.parseAsync(['node', 'opencli', 'browser', 'get', 'html', '--selector', '.hero', '--as', 'json']);
1931
+ await program.parseAsync(['node', 'opencli', 'browser', '--session', 'test', 'get', 'html', '--selector', '.hero', '--as', 'json']);
1822
1932
  const out = lastJsonLog();
1823
1933
  expect(out.matched).toBe(1);
1824
1934
  expect(out.tree.tag).toBe('div');
@@ -1827,14 +1937,14 @@ describe('browser get html command', () => {
1827
1937
  it('--as json emits selector_not_found when matched is 0', async () => {
1828
1938
  browserState.page.evaluate.mockResolvedValueOnce({ selector: '.missing', matched: 0, tree: null });
1829
1939
  const program = createProgram('', '');
1830
- await program.parseAsync(['node', 'opencli', 'browser', 'get', 'html', '--selector', '.missing', '--as', 'json']);
1940
+ await program.parseAsync(['node', 'opencli', 'browser', '--session', 'test', 'get', 'html', '--selector', '.missing', '--as', 'json']);
1831
1941
  expect(lastJsonLog().error.code).toBe('selector_not_found');
1832
1942
  expect(process.exitCode).toBeDefined();
1833
1943
  });
1834
1944
  it('raw mode emits selector_not_found when the selector matches nothing', async () => {
1835
1945
  browserState.page.evaluate.mockResolvedValueOnce({ kind: 'ok', html: null });
1836
1946
  const program = createProgram('', '');
1837
- await program.parseAsync(['node', 'opencli', 'browser', 'get', 'html', '--selector', '.missing']);
1947
+ await program.parseAsync(['node', 'opencli', 'browser', '--session', 'test', 'get', 'html', '--selector', '.missing']);
1838
1948
  expect(lastJsonLog().error.code).toBe('selector_not_found');
1839
1949
  expect(process.exitCode).toBeDefined();
1840
1950
  });
@@ -1844,7 +1954,7 @@ describe('browser get html command', () => {
1844
1954
  reason: "'##$@@' is not a valid selector",
1845
1955
  });
1846
1956
  const program = createProgram('', '');
1847
- await program.parseAsync(['node', 'opencli', 'browser', 'get', 'html', '--selector', '##$@@']);
1957
+ await program.parseAsync(['node', 'opencli', 'browser', '--session', 'test', 'get', 'html', '--selector', '##$@@']);
1848
1958
  const err = lastJsonLog().error;
1849
1959
  expect(err.code).toBe('invalid_selector');
1850
1960
  expect(err.message).toContain('##$@@');
@@ -1858,7 +1968,7 @@ describe('browser get html command', () => {
1858
1968
  reason: "'##$@@' is not a valid selector",
1859
1969
  });
1860
1970
  const program = createProgram('', '');
1861
- await program.parseAsync(['node', 'opencli', 'browser', 'get', 'html', '--selector', '##$@@', '--as', 'json']);
1971
+ await program.parseAsync(['node', 'opencli', 'browser', '--session', 'test', 'get', 'html', '--selector', '##$@@', '--as', 'json']);
1862
1972
  const err = lastJsonLog().error;
1863
1973
  expect(err.code).toBe('invalid_selector');
1864
1974
  expect(err.message).toContain('##$@@');
@@ -1866,7 +1976,7 @@ describe('browser get html command', () => {
1866
1976
  });
1867
1977
  it('rejects unknown --as format with invalid_format error', async () => {
1868
1978
  const program = createProgram('', '');
1869
- await program.parseAsync(['node', 'opencli', 'browser', 'get', 'html', '--as', 'yaml']);
1979
+ await program.parseAsync(['node', 'opencli', 'browser', '--session', 'test', 'get', 'html', '--as', 'yaml']);
1870
1980
  expect(lastJsonLog().error.code).toBe('invalid_format');
1871
1981
  expect(process.exitCode).toBeDefined();
1872
1982
  });
@@ -1899,6 +2009,7 @@ function installSelectorFirstTestHarness(label, pageOverrides) {
1899
2009
  setActivePage: vi.fn(),
1900
2010
  getActivePage: vi.fn().mockReturnValue('tab-1'),
1901
2011
  tabs: vi.fn().mockResolvedValue([{ page: 'tab-1', active: true }]),
2012
+ session: 'test',
1902
2013
  ...pageOverrides(),
1903
2014
  };
1904
2015
  });
@@ -1920,7 +2031,7 @@ describe('browser find command', () => {
1920
2031
  ],
1921
2032
  });
1922
2033
  const program = createProgram('', '');
1923
- await program.parseAsync(['node', 'opencli', 'browser', 'find', '--css', '.btn']);
2034
+ await program.parseAsync(['node', 'opencli', 'browser', '--session', 'test', 'find', '--css', '.btn']);
1924
2035
  const out = lastJsonLog();
1925
2036
  expect(out.matches_n).toBe(2);
1926
2037
  expect(out.entries).toHaveLength(2);
@@ -1936,7 +2047,7 @@ describe('browser find command', () => {
1936
2047
  ],
1937
2048
  });
1938
2049
  const program = createProgram('', '');
1939
- await program.parseAsync(['node', 'opencli', 'browser', 'find', '--role', 'button', '--name', 'Save']);
2050
+ await program.parseAsync(['node', 'opencli', 'browser', '--session', 'test', 'find', '--role', 'button', '--name', 'Save']);
1940
2051
  const js = browserState.page.evaluate.mock.calls[0][0];
1941
2052
  expect(js).toContain('CRITERIA');
1942
2053
  expect(js).toContain('function accessibleName');
@@ -1950,7 +2061,7 @@ describe('browser find command', () => {
1950
2061
  it('forwards --limit / --text-max into the generated JS', async () => {
1951
2062
  browserState.page.evaluate.mockResolvedValueOnce({ matches_n: 0, entries: [] });
1952
2063
  const program = createProgram('', '');
1953
- await program.parseAsync(['node', 'opencli', 'browser', 'find', '--css', '.btn', '--limit', '3', '--text-max', '20']);
2064
+ await program.parseAsync(['node', 'opencli', 'browser', '--session', 'test', 'find', '--css', '.btn', '--limit', '3', '--text-max', '20']);
1954
2065
  const js = browserState.page.evaluate.mock.calls[0][0];
1955
2066
  expect(js).toContain('LIMIT = 3');
1956
2067
  expect(js).toContain('TEXT_MAX = 20');
@@ -1960,7 +2071,7 @@ describe('browser find command', () => {
1960
2071
  error: { code: 'invalid_selector', message: 'Invalid CSS selector: ">>>"', hint: 'Check the selector syntax.' },
1961
2072
  });
1962
2073
  const program = createProgram('', '');
1963
- await program.parseAsync(['node', 'opencli', 'browser', 'find', '--css', '>>>']);
2074
+ await program.parseAsync(['node', 'opencli', 'browser', '--session', 'test', 'find', '--css', '>>>']);
1964
2075
  expect(lastJsonLog().error.code).toBe('invalid_selector');
1965
2076
  expect(process.exitCode).toBeDefined();
1966
2077
  });
@@ -1969,20 +2080,20 @@ describe('browser find command', () => {
1969
2080
  error: { code: 'selector_not_found', message: 'CSS selector ".missing" matched 0 elements', hint: 'Use browser state to inspect the page.' },
1970
2081
  });
1971
2082
  const program = createProgram('', '');
1972
- await program.parseAsync(['node', 'opencli', 'browser', 'find', '--css', '.missing']);
2083
+ await program.parseAsync(['node', 'opencli', 'browser', '--session', 'test', 'find', '--css', '.missing']);
1973
2084
  expect(lastJsonLog().error.code).toBe('selector_not_found');
1974
2085
  expect(process.exitCode).toBeDefined();
1975
2086
  });
1976
2087
  it('rejects missing --css with usage_error (no evaluate call)', async () => {
1977
2088
  const program = createProgram('', '');
1978
- await program.parseAsync(['node', 'opencli', 'browser', 'find']);
2089
+ await program.parseAsync(['node', 'opencli', 'browser', '--session', 'test', 'find']);
1979
2090
  expect(lastJsonLog().error.code).toBe('usage_error');
1980
2091
  expect(browserState.page.evaluate).not.toHaveBeenCalled();
1981
2092
  expect(process.exitCode).toBeDefined();
1982
2093
  });
1983
2094
  it('rejects malformed --limit with usage_error (no evaluate call)', async () => {
1984
2095
  const program = createProgram('', '');
1985
- await program.parseAsync(['node', 'opencli', 'browser', 'find', '--css', '.btn', '--limit', 'abc']);
2096
+ await program.parseAsync(['node', 'opencli', 'browser', '--session', 'test', 'find', '--css', '.btn', '--limit', 'abc']);
1986
2097
  expect(lastJsonLog().error.code).toBe('usage_error');
1987
2098
  expect(browserState.page.evaluate).not.toHaveBeenCalled();
1988
2099
  expect(process.exitCode).toBeDefined();
@@ -1999,7 +2110,7 @@ describe('browser get text/value/attributes commands', () => {
1999
2110
  // 2nd call: getTextResolvedJs -> the element's text
2000
2111
  evalMock.mockResolvedValueOnce('Hello world');
2001
2112
  const program = createProgram('', '');
2002
- await program.parseAsync(['node', 'opencli', 'browser', 'get', 'text', '7']);
2113
+ await program.parseAsync(['node', 'opencli', 'browser', '--session', 'test', 'get', 'text', '7']);
2003
2114
  expect(lastJsonLog()).toEqual({ value: 'Hello world', matches_n: 1, match_level: 'exact' });
2004
2115
  });
2005
2116
  it('resolves a semantic locator to a ref before get text', async () => {
@@ -2013,7 +2124,7 @@ describe('browser get text/value/attributes commands', () => {
2013
2124
  evalMock.mockResolvedValueOnce({ ok: true, matches_n: 1, match_level: 'exact' });
2014
2125
  evalMock.mockResolvedValueOnce('Save');
2015
2126
  const program = createProgram('', '');
2016
- await program.parseAsync(['node', 'opencli', 'browser', 'get', 'text', '--role', 'button', '--name', 'Save']);
2127
+ await program.parseAsync(['node', 'opencli', 'browser', '--session', 'test', 'get', 'text', '--role', 'button', '--name', 'Save']);
2017
2128
  expect(evalMock.mock.calls[0][0]).toContain('function accessibleName');
2018
2129
  expect(evalMock.mock.calls[1][0]).toContain('const ref = "12"');
2019
2130
  expect(lastJsonLog()).toEqual({ value: 'Save', matches_n: 1, match_level: 'exact' });
@@ -2031,7 +2142,7 @@ describe('browser get text/value/attributes commands', () => {
2031
2142
  evalMock.mockResolvedValueOnce({ ok: true, matches_n: 1, match_level: 'exact' });
2032
2143
  evalMock.mockResolvedValueOnce('Save');
2033
2144
  const program = createProgram('', '');
2034
- await program.parseAsync(['node', 'opencli', 'browser', 'get', 'text', '--role', 'button', '--name', 'Save']);
2145
+ await program.parseAsync(['node', 'opencli', 'browser', '--session', 'test', 'get', 'text', '--role', 'button', '--name', 'Save']);
2035
2146
  expect(evalMock.mock.calls[0][0]).toContain('const LIMIT = 6');
2036
2147
  expect(evalMock.mock.calls[1][0]).toContain('const ref = "12"');
2037
2148
  expect(lastJsonLog()).toEqual({ value: 'Save', matches_n: 1, match_level: 'exact', total_matches: 3 });
@@ -2041,7 +2152,7 @@ describe('browser get text/value/attributes commands', () => {
2041
2152
  evalMock.mockResolvedValueOnce({ ok: true, matches_n: 3, match_level: 'exact' });
2042
2153
  evalMock.mockResolvedValueOnce('first');
2043
2154
  const program = createProgram('', '');
2044
- await program.parseAsync(['node', 'opencli', 'browser', 'get', 'text', '.btn']);
2155
+ await program.parseAsync(['node', 'opencli', 'browser', '--session', 'test', 'get', 'text', '.btn']);
2045
2156
  expect(lastJsonLog()).toEqual({ value: 'first', matches_n: 3, match_level: 'exact' });
2046
2157
  });
2047
2158
  it('parses the attributes payload back into a real object', async () => {
@@ -2050,7 +2161,7 @@ describe('browser get text/value/attributes commands', () => {
2050
2161
  // getAttributesResolvedJs returns a JSON-encoded string — the CLI must parse it
2051
2162
  evalMock.mockResolvedValueOnce(JSON.stringify({ id: 'nav', class: 'hero' }));
2052
2163
  const program = createProgram('', '');
2053
- await program.parseAsync(['node', 'opencli', 'browser', 'get', 'attributes', '#nav']);
2164
+ await program.parseAsync(['node', 'opencli', 'browser', '--session', 'test', 'get', 'attributes', '#nav']);
2054
2165
  const out = lastJsonLog();
2055
2166
  expect(out.matches_n).toBe(1);
2056
2167
  expect(out.match_level).toBe('exact');
@@ -2064,7 +2175,7 @@ describe('browser get text/value/attributes commands', () => {
2064
2175
  hint: 'Try a less specific selector.',
2065
2176
  });
2066
2177
  const program = createProgram('', '');
2067
- await program.parseAsync(['node', 'opencli', 'browser', 'get', 'text', '.missing']);
2178
+ await program.parseAsync(['node', 'opencli', 'browser', '--session', 'test', 'get', 'text', '.missing']);
2068
2179
  expect(lastJsonLog().error.code).toBe('selector_not_found');
2069
2180
  expect(process.exitCode).toBeDefined();
2070
2181
  });
@@ -2073,7 +2184,7 @@ describe('browser get text/value/attributes commands', () => {
2073
2184
  evalMock.mockResolvedValueOnce({ ok: true, matches_n: 4, match_level: 'exact' });
2074
2185
  evalMock.mockResolvedValueOnce('second');
2075
2186
  const program = createProgram('', '');
2076
- await program.parseAsync(['node', 'opencli', 'browser', 'get', 'value', '.btn', '--nth', '1']);
2187
+ await program.parseAsync(['node', 'opencli', 'browser', '--session', 'test', 'get', 'value', '.btn', '--nth', '1']);
2077
2188
  const resolveJs = evalMock.mock.calls[0][0];
2078
2189
  // resolveTargetJs embeds nth as a raw number literal; look for the binding
2079
2190
  expect(resolveJs).toContain('const nth = 1');
@@ -2081,7 +2192,7 @@ describe('browser get text/value/attributes commands', () => {
2081
2192
  });
2082
2193
  it('rejects malformed --nth with usage_error before touching the page', async () => {
2083
2194
  const program = createProgram('', '');
2084
- await program.parseAsync(['node', 'opencli', 'browser', 'get', 'text', '.btn', '--nth', 'abc']);
2195
+ await program.parseAsync(['node', 'opencli', 'browser', '--session', 'test', 'get', 'text', '.btn', '--nth', 'abc']);
2085
2196
  expect(lastJsonLog().error.code).toBe('usage_error');
2086
2197
  expect(browserState.page.evaluate).not.toHaveBeenCalled();
2087
2198
  expect(process.exitCode).toBeDefined();
@@ -2129,7 +2240,7 @@ describe('browser click/type commands', () => {
2129
2240
  it('emits {clicked, target, matches_n, match_level} on success', async () => {
2130
2241
  browserState.page.click.mockResolvedValueOnce({ matches_n: 1, match_level: 'exact' });
2131
2242
  const program = createProgram('', '');
2132
- await program.parseAsync(['node', 'opencli', 'browser', 'click', '#save']);
2243
+ await program.parseAsync(['node', 'opencli', 'browser', '--session', 'test', 'click', '#save']);
2133
2244
  expect(browserState.page.click).toHaveBeenCalledWith('#save', {});
2134
2245
  expect(lastJsonLog()).toEqual({ clicked: true, target: '#save', matches_n: 1, match_level: 'exact' });
2135
2246
  });
@@ -2142,7 +2253,7 @@ describe('browser click/type commands', () => {
2142
2253
  });
2143
2254
  browserState.page.click.mockResolvedValueOnce({ matches_n: 1, match_level: 'exact' });
2144
2255
  const program = createProgram('', '');
2145
- await program.parseAsync(['node', 'opencli', 'browser', 'click', '--role', 'button', '--name', 'Submit']);
2256
+ await program.parseAsync(['node', 'opencli', 'browser', '--session', 'test', 'click', '--role', 'button', '--name', 'Submit']);
2146
2257
  expect(browserState.page.click).toHaveBeenCalledWith('23', {});
2147
2258
  expect(lastJsonLog()).toEqual({ clicked: true, target: '23', matches_n: 1, match_level: 'exact' });
2148
2259
  });
@@ -2155,7 +2266,7 @@ describe('browser click/type commands', () => {
2155
2266
  ],
2156
2267
  });
2157
2268
  const program = createProgram('', '');
2158
- await program.parseAsync(['node', 'opencli', 'browser', 'click', '--role', 'button', '--name', 'Save']);
2269
+ await program.parseAsync(['node', 'opencli', 'browser', '--session', 'test', 'click', '--role', 'button', '--name', 'Save']);
2159
2270
  const err = lastJsonLog().error;
2160
2271
  expect(err.code).toBe('semantic_ambiguous');
2161
2272
  expect(err.matches_n).toBe(2);
@@ -2170,7 +2281,7 @@ describe('browser click/type commands', () => {
2170
2281
  ],
2171
2282
  });
2172
2283
  const program = createProgram('', '');
2173
- await program.parseAsync(['node', 'opencli', 'browser', 'hover', '--role', 'button', '--name', 'Settings']);
2284
+ await program.parseAsync(['node', 'opencli', 'browser', '--session', 'test', 'hover', '--role', 'button', '--name', 'Settings']);
2174
2285
  expect(browserState.page.hover).toHaveBeenCalledWith('31', {});
2175
2286
  expect(lastJsonLog()).toEqual({ hovered: true, target: '31', matches_n: 1, match_level: 'exact' });
2176
2287
  });
@@ -2183,7 +2294,7 @@ describe('browser click/type commands', () => {
2183
2294
  });
2184
2295
  browserState.page.setChecked.mockResolvedValueOnce({ checked: true, changed: false, matches_n: 1, match_level: 'exact', kind: 'checkbox' });
2185
2296
  const program = createProgram('', '');
2186
- await program.parseAsync(['node', 'opencli', 'browser', 'check', '--role', 'checkbox', '--name', 'Accept']);
2297
+ await program.parseAsync(['node', 'opencli', 'browser', '--session', 'test', 'check', '--role', 'checkbox', '--name', 'Accept']);
2187
2298
  expect(browserState.page.setChecked).toHaveBeenCalledWith('32', true, {});
2188
2299
  expect(lastJsonLog()).toEqual({ checked: true, changed: false, target: '32', matches_n: 1, match_level: 'exact', kind: 'checkbox' });
2189
2300
  });
@@ -2207,7 +2318,7 @@ describe('browser click/type commands', () => {
2207
2318
  multiple: false,
2208
2319
  });
2209
2320
  const program = createProgram('', '');
2210
- await program.parseAsync(['node', 'opencli', 'browser', 'upload', '--role', 'button', '--name', 'Upload receipt', file]);
2321
+ await program.parseAsync(['node', 'opencli', 'browser', '--session', 'test', 'upload', '--role', 'button', '--name', 'Upload receipt', file]);
2211
2322
  expect(browserState.page.uploadFiles).toHaveBeenCalledWith('33', [file], {});
2212
2323
  expect(lastJsonLog()).toMatchObject({ uploaded: true, target: '33', files: 1 });
2213
2324
  });
@@ -2223,7 +2334,7 @@ describe('browser click/type commands', () => {
2223
2334
  browserState.page.click.mockResolvedValueOnce({ matches_n: 1, match_level: 'exact' });
2224
2335
  browserState.page.typeText.mockResolvedValueOnce({ matches_n: 1, match_level: 'exact' });
2225
2336
  const program = createProgram('', '');
2226
- await program.parseAsync(['node', 'opencli', 'browser', 'type', '--label', 'Email', 'me@example.com']);
2337
+ await program.parseAsync(['node', 'opencli', 'browser', '--session', 'test', 'type', '--label', 'Email', 'me@example.com']);
2227
2338
  expect(browserState.page.click).toHaveBeenCalledWith('34', {});
2228
2339
  expect(browserState.page.typeText).toHaveBeenCalledWith('34', 'me@example.com', {});
2229
2340
  expect(lastJsonLog()).toMatchObject({ typed: true, target: '34', text: 'me@example.com' });
@@ -2245,7 +2356,7 @@ describe('browser click/type commands', () => {
2245
2356
  match_level: 'exact',
2246
2357
  });
2247
2358
  const program = createProgram('', '');
2248
- await program.parseAsync(['node', 'opencli', 'browser', 'fill', '--label', 'Email', 'me@example.com']);
2359
+ await program.parseAsync(['node', 'opencli', 'browser', '--session', 'test', 'fill', '--label', 'Email', 'me@example.com']);
2249
2360
  expect(browserState.page.fillText).toHaveBeenCalledWith('35', 'me@example.com', {});
2250
2361
  expect(lastJsonLog()).toMatchObject({ filled: true, verified: true, target: '35', text: 'me@example.com' });
2251
2362
  });
@@ -2277,6 +2388,8 @@ describe('browser click/type commands', () => {
2277
2388
  'node',
2278
2389
  'opencli',
2279
2390
  'browser',
2391
+ '--session',
2392
+ 'test',
2280
2393
  'drag',
2281
2394
  '--from-role',
2282
2395
  'button',
@@ -2293,13 +2406,13 @@ describe('browser click/type commands', () => {
2293
2406
  it('surfaces match_level=stable when resolver falls back to fingerprint match', async () => {
2294
2407
  browserState.page.click.mockResolvedValueOnce({ matches_n: 1, match_level: 'stable' });
2295
2408
  const program = createProgram('', '');
2296
- await program.parseAsync(['node', 'opencli', 'browser', 'click', '7']);
2409
+ await program.parseAsync(['node', 'opencli', 'browser', '--session', 'test', 'click', '7']);
2297
2410
  expect(lastJsonLog()).toEqual({ clicked: true, target: '7', matches_n: 1, match_level: 'stable' });
2298
2411
  });
2299
2412
  it('forwards --nth as ResolveOptions.nth to page.click', async () => {
2300
2413
  browserState.page.click.mockResolvedValueOnce({ matches_n: 3, match_level: 'exact' });
2301
2414
  const program = createProgram('', '');
2302
- await program.parseAsync(['node', 'opencli', 'browser', 'click', '.btn', '--nth', '2']);
2415
+ await program.parseAsync(['node', 'opencli', 'browser', '--session', 'test', 'click', '.btn', '--nth', '2']);
2303
2416
  expect(browserState.page.click).toHaveBeenCalledWith('.btn', { nth: 2 });
2304
2417
  expect(lastJsonLog()).toEqual({ clicked: true, target: '.btn', matches_n: 3, match_level: 'exact' });
2305
2418
  });
@@ -2311,7 +2424,7 @@ describe('browser click/type commands', () => {
2311
2424
  matches_n: 3,
2312
2425
  }));
2313
2426
  const program = createProgram('', '');
2314
- await program.parseAsync(['node', 'opencli', 'browser', 'click', '.btn']);
2427
+ await program.parseAsync(['node', 'opencli', 'browser', '--session', 'test', 'click', '.btn']);
2315
2428
  const err = lastJsonLog().error;
2316
2429
  expect(err.code).toBe('selector_ambiguous');
2317
2430
  expect(err.matches_n).toBe(3);
@@ -2325,13 +2438,13 @@ describe('browser click/type commands', () => {
2325
2438
  matches_n: 3,
2326
2439
  }));
2327
2440
  const program = createProgram('', '');
2328
- await program.parseAsync(['node', 'opencli', 'browser', 'click', '.btn', '--nth', '99']);
2441
+ await program.parseAsync(['node', 'opencli', 'browser', '--session', 'test', 'click', '.btn', '--nth', '99']);
2329
2442
  expect(lastJsonLog().error.code).toBe('selector_nth_out_of_range');
2330
2443
  expect(process.exitCode).toBeDefined();
2331
2444
  });
2332
2445
  it('rejects malformed --nth on click with usage_error before touching the page', async () => {
2333
2446
  const program = createProgram('', '');
2334
- await program.parseAsync(['node', 'opencli', 'browser', 'click', '.btn', '--nth', 'abc']);
2447
+ await program.parseAsync(['node', 'opencli', 'browser', '--session', 'test', 'click', '.btn', '--nth', 'abc']);
2335
2448
  expect(lastJsonLog().error.code).toBe('usage_error');
2336
2449
  expect(browserState.page.click).not.toHaveBeenCalled();
2337
2450
  expect(process.exitCode).toBeDefined();
@@ -2339,21 +2452,21 @@ describe('browser click/type commands', () => {
2339
2452
  it('hover: delegates to page.hover and emits a structured envelope', async () => {
2340
2453
  browserState.page.hover.mockResolvedValueOnce({ matches_n: 2, match_level: 'exact' });
2341
2454
  const program = createProgram('', '');
2342
- await program.parseAsync(['node', 'opencli', 'browser', 'hover', '.menu', '--nth', '1']);
2455
+ await program.parseAsync(['node', 'opencli', 'browser', '--session', 'test', 'hover', '.menu', '--nth', '1']);
2343
2456
  expect(browserState.page.hover).toHaveBeenCalledWith('.menu', { nth: 1 });
2344
2457
  expect(lastJsonLog()).toEqual({ hovered: true, target: '.menu', matches_n: 2, match_level: 'exact' });
2345
2458
  });
2346
2459
  it('focus: delegates to page.focus and reports whether the element took focus', async () => {
2347
2460
  browserState.page.focus.mockResolvedValueOnce({ focused: true, matches_n: 1, match_level: 'stable' });
2348
2461
  const program = createProgram('', '');
2349
- await program.parseAsync(['node', 'opencli', 'browser', 'focus', '7']);
2462
+ await program.parseAsync(['node', 'opencli', 'browser', '--session', 'test', 'focus', '7']);
2350
2463
  expect(browserState.page.focus).toHaveBeenCalledWith('7', {});
2351
2464
  expect(lastJsonLog()).toEqual({ focused: true, target: '7', matches_n: 1, match_level: 'stable' });
2352
2465
  });
2353
2466
  it('dblclick: delegates to page.dblClick and emits a structured envelope', async () => {
2354
2467
  browserState.page.dblClick.mockResolvedValueOnce({ matches_n: 1, match_level: 'exact' });
2355
2468
  const program = createProgram('', '');
2356
- await program.parseAsync(['node', 'opencli', 'browser', 'dblclick', '#row']);
2469
+ await program.parseAsync(['node', 'opencli', 'browser', '--session', 'test', 'dblclick', '#row']);
2357
2470
  expect(browserState.page.dblClick).toHaveBeenCalledWith('#row', {});
2358
2471
  expect(lastJsonLog()).toEqual({ dblclicked: true, target: '#row', matches_n: 1, match_level: 'exact' });
2359
2472
  });
@@ -2366,7 +2479,7 @@ describe('browser click/type commands', () => {
2366
2479
  kind: 'checkbox',
2367
2480
  });
2368
2481
  const program = createProgram('', '');
2369
- await program.parseAsync(['node', 'opencli', 'browser', 'check', '.todo', '--nth', '1']);
2482
+ await program.parseAsync(['node', 'opencli', 'browser', '--session', 'test', 'check', '.todo', '--nth', '1']);
2370
2483
  expect(browserState.page.setChecked).toHaveBeenCalledWith('.todo', true, { nth: 1 });
2371
2484
  expect(lastJsonLog()).toEqual({ checked: true, changed: true, target: '.todo', matches_n: 2, match_level: 'exact', kind: 'checkbox' });
2372
2485
  });
@@ -2379,7 +2492,7 @@ describe('browser click/type commands', () => {
2379
2492
  kind: 'checkbox',
2380
2493
  });
2381
2494
  const program = createProgram('', '');
2382
- await program.parseAsync(['node', 'opencli', 'browser', 'uncheck', '#subscribe']);
2495
+ await program.parseAsync(['node', 'opencli', 'browser', '--session', 'test', 'uncheck', '#subscribe']);
2383
2496
  expect(browserState.page.setChecked).toHaveBeenCalledWith('#subscribe', false, {});
2384
2497
  expect(lastJsonLog()).toEqual({ checked: false, changed: false, target: '#subscribe', matches_n: 1, match_level: 'stable', kind: 'checkbox' });
2385
2498
  });
@@ -2397,7 +2510,7 @@ describe('browser click/type commands', () => {
2397
2510
  multiple: false,
2398
2511
  });
2399
2512
  const program = createProgram('', '');
2400
- await program.parseAsync(['node', 'opencli', 'browser', 'upload', '#file', file]);
2513
+ await program.parseAsync(['node', 'opencli', 'browser', '--session', 'test', 'upload', '#file', file]);
2401
2514
  expect(browserState.page.uploadFiles).toHaveBeenCalledWith('#file', [file], {});
2402
2515
  expect(lastJsonLog()).toEqual({
2403
2516
  uploaded: true,
@@ -2411,7 +2524,7 @@ describe('browser click/type commands', () => {
2411
2524
  });
2412
2525
  it('upload: rejects missing files before touching the page', async () => {
2413
2526
  const program = createProgram('', '');
2414
- await program.parseAsync(['node', 'opencli', 'browser', 'upload', '#file', '/tmp/opencli-missing-file']);
2527
+ await program.parseAsync(['node', 'opencli', 'browser', '--session', 'test', 'upload', '#file', '/tmp/opencli-missing-file']);
2415
2528
  expect(lastJsonLog().error.code).toBe('file_not_found');
2416
2529
  expect(browserState.page.uploadFiles).not.toHaveBeenCalled();
2417
2530
  expect(process.exitCode).toBeDefined();
@@ -2427,7 +2540,7 @@ describe('browser click/type commands', () => {
2427
2540
  target_match_level: 'stable',
2428
2541
  });
2429
2542
  const program = createProgram('', '');
2430
- await program.parseAsync(['node', 'opencli', 'browser', 'drag', '.card', '.lane', '--from-nth', '2', '--to-nth', '1']);
2543
+ await program.parseAsync(['node', 'opencli', 'browser', '--session', 'test', 'drag', '.card', '.lane', '--from-nth', '2', '--to-nth', '1']);
2431
2544
  expect(browserState.page.drag).toHaveBeenCalledWith('.card', '.lane', { from: { nth: 2 }, to: { nth: 1 } });
2432
2545
  expect(lastJsonLog()).toEqual({
2433
2546
  dragged: true,
@@ -2441,7 +2554,7 @@ describe('browser click/type commands', () => {
2441
2554
  });
2442
2555
  it('drag: rejects malformed --from-nth before touching the page', async () => {
2443
2556
  const program = createProgram('', '');
2444
- await program.parseAsync(['node', 'opencli', 'browser', 'drag', '.card', '.lane', '--from-nth', 'abc']);
2557
+ await program.parseAsync(['node', 'opencli', 'browser', '--session', 'test', 'drag', '.card', '.lane', '--from-nth', 'abc']);
2445
2558
  expect(lastJsonLog().error.code).toBe('usage_error');
2446
2559
  expect(browserState.page.drag).not.toHaveBeenCalled();
2447
2560
  expect(process.exitCode).toBeDefined();
@@ -2451,7 +2564,7 @@ describe('browser click/type commands', () => {
2451
2564
  browserState.page.typeText.mockResolvedValueOnce({ matches_n: 1, match_level: 'exact' });
2452
2565
  browserState.page.evaluate.mockResolvedValueOnce(false); // isAutocomplete
2453
2566
  const program = createProgram('', '');
2454
- await program.parseAsync(['node', 'opencli', 'browser', 'type', '#q', 'hello']);
2567
+ await program.parseAsync(['node', 'opencli', 'browser', '--session', 'test', 'type', '#q', 'hello']);
2455
2568
  expect(browserState.page.click).toHaveBeenCalledWith('#q', {});
2456
2569
  expect(browserState.page.wait).toHaveBeenCalledWith(0.3);
2457
2570
  expect(browserState.page.typeText).toHaveBeenCalledWith('#q', 'hello', {});
@@ -2464,7 +2577,7 @@ describe('browser click/type commands', () => {
2464
2577
  browserState.page.typeText.mockResolvedValueOnce({ matches_n: 1, match_level: 'exact' });
2465
2578
  browserState.page.evaluate.mockResolvedValueOnce(true);
2466
2579
  const program = createProgram('', '');
2467
- await program.parseAsync(['node', 'opencli', 'browser', 'type', '#q', 'hi']);
2580
+ await program.parseAsync(['node', 'opencli', 'browser', '--session', 'test', 'type', '#q', 'hi']);
2468
2581
  const waitCalls = browserState.page.wait.mock.calls;
2469
2582
  expect(waitCalls).toContainEqual([0.3]);
2470
2583
  expect(waitCalls).toContainEqual([0.4]);
@@ -2476,7 +2589,7 @@ describe('browser click/type commands', () => {
2476
2589
  browserState.page.typeText.mockResolvedValueOnce({ matches_n: 1, match_level: 'reidentified' });
2477
2590
  browserState.page.evaluate.mockResolvedValueOnce(false);
2478
2591
  const program = createProgram('', '');
2479
- await program.parseAsync(['node', 'opencli', 'browser', 'type', '9', 'hi']);
2592
+ await program.parseAsync(['node', 'opencli', 'browser', '--session', 'test', 'type', '9', 'hi']);
2480
2593
  // The typeText call is the authoritative match_level source for the `type` envelope.
2481
2594
  expect(lastJsonLog().match_level).toBe('reidentified');
2482
2595
  });
@@ -2484,7 +2597,7 @@ describe('browser click/type commands', () => {
2484
2597
  browserState.page.click.mockResolvedValueOnce({ matches_n: 5, match_level: 'exact' });
2485
2598
  browserState.page.typeText.mockResolvedValueOnce({ matches_n: 5, match_level: 'exact' });
2486
2599
  const program = createProgram('', '');
2487
- await program.parseAsync(['node', 'opencli', 'browser', 'type', '.field', 'x', '--nth', '3']);
2600
+ await program.parseAsync(['node', 'opencli', 'browser', '--session', 'test', 'type', '.field', 'x', '--nth', '3']);
2488
2601
  expect(browserState.page.click).toHaveBeenCalledWith('.field', { nth: 3 });
2489
2602
  expect(browserState.page.typeText).toHaveBeenCalledWith('.field', 'x', { nth: 3 });
2490
2603
  });
@@ -2500,7 +2613,7 @@ describe('browser click/type commands', () => {
2500
2613
  mode: 'textarea',
2501
2614
  });
2502
2615
  const program = createProgram('', '');
2503
- await program.parseAsync(['node', 'opencli', 'browser', 'fill', '#msg', 'line1\\n/ / raw']);
2616
+ await program.parseAsync(['node', 'opencli', 'browser', '--session', 'test', 'fill', '#msg', 'line1\\n/ / raw']);
2504
2617
  expect(browserState.page.fillText).toHaveBeenCalledWith('#msg', 'line1\\n/ / raw', {});
2505
2618
  expect(lastJsonLog()).toEqual({
2506
2619
  filled: true,
@@ -2526,7 +2639,7 @@ describe('browser click/type commands', () => {
2526
2639
  match_level: 'exact',
2527
2640
  });
2528
2641
  const program = createProgram('', '');
2529
- await program.parseAsync(['node', 'opencli', 'browser', 'fill', '#msg', 'expected']);
2642
+ await program.parseAsync(['node', 'opencli', 'browser', '--session', 'test', 'fill', '#msg', 'expected']);
2530
2643
  expect(lastJsonLog()).toEqual({
2531
2644
  filled: true,
2532
2645
  verified: false,
@@ -2541,7 +2654,7 @@ describe('browser click/type commands', () => {
2541
2654
  });
2542
2655
  it('fill: forwards --nth to page.fillText', async () => {
2543
2656
  const program = createProgram('', '');
2544
- await program.parseAsync(['node', 'opencli', 'browser', 'fill', '.field', 'x', '--nth', '2']);
2657
+ await program.parseAsync(['node', 'opencli', 'browser', '--session', 'test', 'fill', '.field', 'x', '--nth', '2']);
2545
2658
  expect(browserState.page.fillText).toHaveBeenCalledWith('.field', 'x', { nth: 2 });
2546
2659
  });
2547
2660
  });
@@ -2554,7 +2667,7 @@ describe('browser select command', () => {
2554
2667
  evalMock.mockResolvedValueOnce({ ok: true, matches_n: 1, match_level: 'exact' });
2555
2668
  evalMock.mockResolvedValueOnce({ selected: 'US' });
2556
2669
  const program = createProgram('', '');
2557
- await program.parseAsync(['node', 'opencli', 'browser', 'select', '#country', 'US']);
2670
+ await program.parseAsync(['node', 'opencli', 'browser', '--session', 'test', 'select', '#country', 'US']);
2558
2671
  expect(lastJsonLog()).toEqual({ selected: 'US', target: '#country', matches_n: 1, match_level: 'exact' });
2559
2672
  });
2560
2673
  it('maps "Not a <select>" to a not_a_select error envelope', async () => {
@@ -2562,7 +2675,7 @@ describe('browser select command', () => {
2562
2675
  evalMock.mockResolvedValueOnce({ ok: true, matches_n: 1, match_level: 'exact' });
2563
2676
  evalMock.mockResolvedValueOnce({ error: 'Not a <select>' });
2564
2677
  const program = createProgram('', '');
2565
- await program.parseAsync(['node', 'opencli', 'browser', 'select', '#not-select', 'US']);
2678
+ await program.parseAsync(['node', 'opencli', 'browser', '--session', 'test', 'select', '#not-select', 'US']);
2566
2679
  const err = lastJsonLog().error;
2567
2680
  expect(err.code).toBe('not_a_select');
2568
2681
  expect(err.matches_n).toBe(1);
@@ -2573,7 +2686,7 @@ describe('browser select command', () => {
2573
2686
  evalMock.mockResolvedValueOnce({ ok: true, matches_n: 1, match_level: 'exact' });
2574
2687
  evalMock.mockResolvedValueOnce({ error: 'Option "XX" not found', available: ['US', 'CA'] });
2575
2688
  const program = createProgram('', '');
2576
- await program.parseAsync(['node', 'opencli', 'browser', 'select', '#country', 'XX']);
2689
+ await program.parseAsync(['node', 'opencli', 'browser', '--session', 'test', 'select', '#country', 'XX']);
2577
2690
  const err = lastJsonLog().error;
2578
2691
  expect(err.code).toBe('option_not_found');
2579
2692
  expect(err.available).toEqual(['US', 'CA']);
@@ -2591,7 +2704,7 @@ describe('browser select command', () => {
2591
2704
  .mockResolvedValueOnce({ ok: true, matches_n: 1, match_level: 'exact' })
2592
2705
  .mockResolvedValueOnce({ selected: 'Uruguay' });
2593
2706
  const program = createProgram('', '');
2594
- await program.parseAsync(['node', 'opencli', 'browser', 'select', '--label', 'Country', 'Uruguay']);
2707
+ await program.parseAsync(['node', 'opencli', 'browser', '--session', 'test', 'select', '--label', 'Country', 'Uruguay']);
2595
2708
  expect(lastJsonLog()).toEqual({ selected: 'Uruguay', target: '36', matches_n: 1, match_level: 'exact' });
2596
2709
  });
2597
2710
  it('surfaces selector_ambiguous from the resolver before calling selectResolvedJs', async () => {
@@ -2603,7 +2716,7 @@ describe('browser select command', () => {
2603
2716
  matches_n: 2,
2604
2717
  });
2605
2718
  const program = createProgram('', '');
2606
- await program.parseAsync(['node', 'opencli', 'browser', 'select', '.dropdown', 'US']);
2719
+ await program.parseAsync(['node', 'opencli', 'browser', '--session', 'test', 'select', '.dropdown', 'US']);
2607
2720
  expect(lastJsonLog().error.code).toBe('selector_ambiguous');
2608
2721
  // The select payload JS must not fire when resolution fails
2609
2722
  expect(browserState.page.evaluate.mock.calls).toHaveLength(1);