@jackwener/opencli 1.7.16 → 1.7.17

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (149) hide show
  1. package/README.md +8 -9
  2. package/README.zh-CN.md +8 -8
  3. package/cli-manifest.json +97 -271
  4. package/clis/chatgpt/ask.js +1 -1
  5. package/clis/chatgpt/commands.test.js +2 -2
  6. package/clis/chatgpt/detail.js +1 -1
  7. package/clis/chatgpt/history.js +1 -1
  8. package/clis/chatgpt/image.js +38 -4
  9. package/clis/chatgpt/image.test.js +68 -1
  10. package/clis/chatgpt/new.js +1 -1
  11. package/clis/chatgpt/read.js +1 -1
  12. package/clis/chatgpt/send.js +1 -1
  13. package/clis/chatgpt/status.js +1 -1
  14. package/clis/chatgpt/utils.js +208 -16
  15. package/clis/chatgpt/utils.test.js +131 -2
  16. package/clis/claude/ask.js +1 -1
  17. package/clis/claude/detail.js +1 -1
  18. package/clis/claude/history.js +1 -1
  19. package/clis/claude/new.js +1 -1
  20. package/clis/claude/read.js +1 -1
  21. package/clis/claude/send.js +1 -1
  22. package/clis/claude/status.js +1 -1
  23. package/clis/deepseek/ask.js +1 -1
  24. package/clis/deepseek/detail.js +1 -1
  25. package/clis/deepseek/history.js +1 -1
  26. package/clis/deepseek/new.js +1 -1
  27. package/clis/deepseek/read.js +1 -1
  28. package/clis/deepseek/send.js +1 -1
  29. package/clis/deepseek/status.js +1 -1
  30. package/clis/doubao/ask.js +1 -1
  31. package/clis/doubao/detail.js +1 -1
  32. package/clis/doubao/history.js +1 -1
  33. package/clis/doubao/meeting-summary.js +1 -1
  34. package/clis/doubao/meeting-transcript.js +1 -1
  35. package/clis/doubao/new.js +1 -1
  36. package/clis/doubao/read.js +1 -1
  37. package/clis/doubao/send.js +1 -1
  38. package/clis/doubao/status.js +1 -1
  39. package/clis/gemini/ask.js +1 -1
  40. package/clis/gemini/deep-research-result.js +1 -1
  41. package/clis/gemini/deep-research.js +1 -1
  42. package/clis/gemini/image.js +1 -1
  43. package/clis/gemini/new.js +1 -1
  44. package/clis/grok/ask.js +1 -1
  45. package/clis/grok/detail.js +1 -1
  46. package/clis/grok/history.js +1 -1
  47. package/clis/grok/image.js +1 -1
  48. package/clis/grok/new.js +1 -1
  49. package/clis/grok/read.js +1 -1
  50. package/clis/grok/send.js +1 -1
  51. package/clis/grok/status.js +1 -1
  52. package/clis/notebooklm/current.js +1 -1
  53. package/clis/notebooklm/get.js +1 -1
  54. package/clis/notebooklm/history.js +1 -1
  55. package/clis/notebooklm/note-list.js +1 -1
  56. package/clis/notebooklm/notes-get.js +1 -1
  57. package/clis/notebooklm/open.js +2 -2
  58. package/clis/notebooklm/open.test.js +1 -1
  59. package/clis/notebooklm/source-fulltext.js +1 -1
  60. package/clis/notebooklm/source-get.js +1 -1
  61. package/clis/notebooklm/source-guide.js +1 -1
  62. package/clis/notebooklm/source-list.js +1 -1
  63. package/clis/notebooklm/summary.js +1 -1
  64. package/clis/qwen/ask.js +1 -1
  65. package/clis/qwen/detail.js +1 -1
  66. package/clis/qwen/history.js +1 -1
  67. package/clis/qwen/image.js +1 -1
  68. package/clis/qwen/new.js +1 -1
  69. package/clis/qwen/read.js +1 -1
  70. package/clis/qwen/send.js +1 -1
  71. package/clis/qwen/status.js +1 -1
  72. package/clis/reddit/comment.js +1 -1
  73. package/clis/reddit/frontpage.js +1 -1
  74. package/clis/reddit/popular.js +1 -1
  75. package/clis/reddit/read.js +1 -1
  76. package/clis/reddit/read.test.js +2 -2
  77. package/clis/reddit/save.js +1 -1
  78. package/clis/reddit/saved.js +1 -1
  79. package/clis/reddit/search.js +1 -1
  80. package/clis/reddit/subreddit.js +1 -1
  81. package/clis/reddit/subscribe.js +1 -1
  82. package/clis/reddit/upvote.js +1 -1
  83. package/clis/reddit/upvoted.js +1 -1
  84. package/clis/reddit/user-comments.js +1 -1
  85. package/clis/reddit/user-posts.js +1 -1
  86. package/clis/reddit/user.js +1 -1
  87. package/clis/twitter/article.js +1 -1
  88. package/clis/twitter/bookmark-folder.js +1 -1
  89. package/clis/twitter/bookmark-folders.js +1 -1
  90. package/clis/twitter/bookmarks.js +1 -1
  91. package/clis/twitter/download.js +1 -1
  92. package/clis/twitter/followers.js +1 -1
  93. package/clis/twitter/following.js +1 -1
  94. package/clis/twitter/likes.js +1 -1
  95. package/clis/twitter/list-tweets.js +1 -1
  96. package/clis/twitter/lists.js +1 -1
  97. package/clis/twitter/notifications.js +1 -1
  98. package/clis/twitter/profile.js +1 -1
  99. package/clis/twitter/search.js +1 -1
  100. package/clis/twitter/thread.js +1 -1
  101. package/clis/twitter/timeline.js +1 -1
  102. package/clis/twitter/trending.js +1 -1
  103. package/clis/twitter/tweets.js +1 -1
  104. package/clis/yuanbao/ask.js +1 -1
  105. package/clis/yuanbao/detail.js +1 -1
  106. package/clis/yuanbao/history.js +1 -1
  107. package/clis/yuanbao/new.js +1 -1
  108. package/clis/yuanbao/read.js +1 -1
  109. package/clis/yuanbao/send.js +1 -1
  110. package/clis/yuanbao/status.js +1 -1
  111. package/dist/src/browser/bridge.d.ts +3 -1
  112. package/dist/src/browser/bridge.js +3 -1
  113. package/dist/src/browser/cdp.d.ts +3 -1
  114. package/dist/src/browser/daemon-client.d.ts +7 -14
  115. package/dist/src/browser/daemon-client.js +2 -6
  116. package/dist/src/browser/network-cache.d.ts +5 -5
  117. package/dist/src/browser/network-cache.js +8 -8
  118. package/dist/src/browser/network-cache.test.js +4 -4
  119. package/dist/src/browser/page.d.ts +8 -7
  120. package/dist/src/browser/page.js +23 -16
  121. package/dist/src/browser/page.test.js +60 -30
  122. package/dist/src/build-manifest.js +1 -1
  123. package/dist/src/cli.js +60 -162
  124. package/dist/src/cli.test.js +178 -197
  125. package/dist/src/commanderAdapter.js +2 -0
  126. package/dist/src/discovery.js +1 -1
  127. package/dist/src/doctor.d.ts +0 -4
  128. package/dist/src/doctor.js +8 -72
  129. package/dist/src/doctor.test.js +26 -97
  130. package/dist/src/execution.d.ts +1 -0
  131. package/dist/src/execution.js +20 -21
  132. package/dist/src/execution.test.js +27 -31
  133. package/dist/src/help.js +7 -1
  134. package/dist/src/main.js +0 -19
  135. package/dist/src/manifest-types.d.ts +2 -4
  136. package/dist/src/observation/artifact.js +1 -1
  137. package/dist/src/observation/artifact.test.js +3 -3
  138. package/dist/src/observation/events.d.ts +1 -1
  139. package/dist/src/observation/manager.js +1 -1
  140. package/dist/src/observation/manager.test.js +3 -3
  141. package/dist/src/registry-api.d.ts +1 -1
  142. package/dist/src/registry.d.ts +3 -12
  143. package/dist/src/registry.js +6 -10
  144. package/dist/src/runtime.d.ts +7 -2
  145. package/dist/src/runtime.js +3 -1
  146. package/dist/src/serialization.d.ts +1 -1
  147. package/dist/src/serialization.js +1 -1
  148. package/dist/src/types.d.ts +0 -15
  149. package/package.json +1 -1
@@ -343,7 +343,7 @@ 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');
@@ -351,8 +351,8 @@ describe('createProgram root help descriptions', () => {
351
351
  expect(data.command_count).toBeGreaterThan(20);
352
352
  expect(data.namespace_options).toEqual(expect.arrayContaining([
353
353
  expect.objectContaining({
354
- name: 'workspace',
355
- flags: '--workspace <name>',
354
+ name: 'session',
355
+ flags: '--session <name>',
356
356
  takes_value: 'required',
357
357
  }),
358
358
  expect.objectContaining({
@@ -360,11 +360,6 @@ describe('createProgram root help descriptions', () => {
360
360
  flags: '--window <mode>',
361
361
  takes_value: 'required',
362
362
  }),
363
- expect.objectContaining({
364
- name: 'keepTab',
365
- flags: '--keep-tab <bool>',
366
- takes_value: 'required',
367
- }),
368
363
  ]));
369
364
  expect(data.global_options).toEqual(expect.arrayContaining([
370
365
  expect.objectContaining({
@@ -411,7 +406,7 @@ describe('createProgram root help descriptions', () => {
411
406
  const browser = program.commands.find(cmd => cmd.name() === 'browser');
412
407
  const tab = browser.commands.find(cmd => cmd.name() === 'tab');
413
408
  expect(tab).toBeTruthy();
414
- process.argv = ['node', 'opencli', 'browser', 'tab', '--help', '-f', 'yaml'];
409
+ process.argv = ['node', 'opencli', 'browser', '--session', 'test', 'tab', '--help', '-f', 'yaml'];
415
410
  const data = yaml.load(tab.helpInformation());
416
411
  expect(data).toMatchObject({
417
412
  namespace: 'browser',
@@ -431,7 +426,7 @@ describe('createProgram root help descriptions', () => {
431
426
  usage: 'opencli browser tab close [targetId] [options]',
432
427
  positionals: [{ name: 'targetId', help: 'Target tab/page identity returned by "browser open", "browser tab new", or "browser tab list"' }],
433
428
  });
434
- expect(data.namespace_options.map((option) => option.name)).toEqual(['workspace', 'window', 'keepTab']);
429
+ expect(data.namespace_options.map((option) => option.name)).toEqual(['session', 'window']);
435
430
  expect(data.structured_help).toMatchObject({
436
431
  usage: 'opencli browser tab --help -f yaml',
437
432
  });
@@ -447,7 +442,7 @@ describe('createProgram root help descriptions', () => {
447
442
  const browser = program.commands.find(cmd => cmd.name() === 'browser');
448
443
  const click = browser.commands.find(cmd => cmd.name() === 'click');
449
444
  expect(click).toBeTruthy();
450
- process.argv = ['node', 'opencli', 'browser', 'click', '--help', '-f', 'yaml'];
445
+ process.argv = ['node', 'opencli', 'browser', '--session', 'test', 'click', '--help', '-f', 'yaml'];
451
446
  const data = yaml.load(click.helpInformation());
452
447
  expect(data).toMatchObject({
453
448
  namespace: 'browser',
@@ -460,7 +455,7 @@ describe('createProgram root help descriptions', () => {
460
455
  },
461
456
  });
462
457
  expect(data.command_options.map((option) => option.name)).toEqual(['role', 'name', 'label', 'text', 'testid', 'nth', 'tab']);
463
- expect(data.namespace_options.map((option) => option.name)).toEqual(['workspace', 'window', 'keepTab']);
458
+ expect(data.namespace_options.map((option) => option.name)).toEqual(['session', 'window']);
464
459
  expect(data.global_options.map((option) => option.name)).toContain('profile');
465
460
  }
466
461
  finally {
@@ -664,7 +659,7 @@ describe('browser verify', () => {
664
659
  fs.mkdirSync(adapterDir, { recursive: true });
665
660
  fs.writeFileSync(path.join(adapterDir, 'top.js'), 'export default {};\n', 'utf-8');
666
661
  const program = createProgram('', '');
667
- 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']);
668
663
  expect(mockExecFileSync).toHaveBeenCalledTimes(1);
669
664
  const [, execArgs] = mockExecFileSync.mock.calls[0];
670
665
  expect(execArgs.slice(-6)).toEqual(['hn', 'top', '--trace', 'retain-on-failure', '--format', 'json']);
@@ -692,7 +687,7 @@ describe('browser verify', () => {
692
687
  fs.mkdirSync(adapterDir, { recursive: true });
693
688
  fs.writeFileSync(path.join(adapterDir, 'top.js'), 'export default {};\n', 'utf-8');
694
689
  const program = createProgram('', '');
695
- 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']);
696
691
  expect(mockExecFileSync).toHaveBeenCalledTimes(1);
697
692
  const [, execArgs] = mockExecFileSync.mock.calls[0];
698
693
  expect(execArgs.slice(-5)).toEqual(['hn', 'top', 'opencli-verify', '--format', 'json']);
@@ -721,7 +716,7 @@ describe('browser verify', () => {
721
716
  fs.mkdirSync(adapterDir, { recursive: true });
722
717
  fs.writeFileSync(path.join(adapterDir, 'top.js'), 'export default {};\n', 'utf-8');
723
718
  const program = createProgram('', '');
724
- 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']);
725
720
  const fixtureFile = path.join(fakeHome, '.opencli', 'sites', 'hn', 'verify', 'top.json');
726
721
  const fixture = JSON.parse(fs.readFileSync(fixtureFile, 'utf-8'));
727
722
  expect(fixture.args).toEqual(['opencli-verify']);
@@ -753,7 +748,7 @@ describe('browser verify', () => {
753
748
  fs.mkdirSync(adapterDir, { recursive: true });
754
749
  fs.writeFileSync(path.join(adapterDir, 'top.js'), 'export default {};\n', 'utf-8');
755
750
  const program = createProgram('', '');
756
- 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']);
757
752
  expect(process.exitCode).toBe(1);
758
753
  const output = consoleLogSpy.mock.calls.map((args) => args.join(' ')).join('\n');
759
754
  expect(output).toContain('Adapter output violates row shape conventions');
@@ -827,8 +822,8 @@ describe('profile list', () => {
827
822
  describe('browser tab targeting commands', () => {
828
823
  const consoleLogSpy = vi.spyOn(console, 'log').mockImplementation(() => { });
829
824
  const stderrSpy = vi.spyOn(process.stderr, 'write').mockImplementation(() => true);
830
- function getBrowserStateFile(cacheDir) {
831
- return path.join(cacheDir, 'browser-state', 'browser_default.json');
825
+ function getBrowserStateFile(cacheDir, session = 'test') {
826
+ return path.join(cacheDir, 'browser-state', `${session}.json`);
832
827
  }
833
828
  beforeEach(() => {
834
829
  process.exitCode = undefined;
@@ -838,9 +833,8 @@ describe('browser tab targeting commands', () => {
838
833
  mockBrowserConnect.mockClear();
839
834
  mockBrowserClose.mockReset().mockResolvedValue(undefined);
840
835
  delete process.env.OPENCLI_WINDOW;
841
- delete process.env.OPENCLI_KEEP_TAB;
842
836
  mockBindTab.mockReset().mockResolvedValue({
843
- workspace: 'bound:default',
837
+ session: 'test',
844
838
  page: 'tab-2',
845
839
  url: 'https://user.example/inbox',
846
840
  title: 'Inbox',
@@ -879,6 +873,7 @@ describe('browser tab targeting commands', () => {
879
873
  state: 'complete',
880
874
  elapsedMs: 10,
881
875
  }),
876
+ session: 'test',
882
877
  };
883
878
  });
884
879
  function lastJsonLog() {
@@ -890,55 +885,37 @@ describe('browser tab targeting commands', () => {
890
885
  throw new Error(`Expected string arg to console.log, got ${typeof last}`);
891
886
  return JSON.parse(last);
892
887
  }
893
- it('binds the current Chrome tab into a bound workspace', async () => {
888
+ it('binds the current Chrome tab into a browser session', async () => {
894
889
  const program = createProgram('', '');
895
- await program.parseAsync(['node', 'opencli', 'browser', 'bind', '--domain', 'user.example', '--path-prefix', '/inbox']);
896
- expect(mockBrowserConnect).toHaveBeenCalledWith({ timeout: 30, workspace: 'bound:default' });
897
- expect(mockBindTab).toHaveBeenCalledWith('bound:default', {
898
- matchDomain: 'user.example',
899
- matchPathPrefix: '/inbox',
900
- });
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', {});
901
893
  const out = lastJsonLog();
902
- expect(out.workspace).toBe('bound:default');
894
+ expect(out.session).toBe('test');
903
895
  expect(out.url).toBe('https://user.example/inbox');
904
896
  });
905
- it('rejects bind workspaces outside the bound namespace', async () => {
897
+ it('requires an explicit session for browser commands', async () => {
906
898
  const program = createProgram('', '');
907
- await program.parseAsync(['node', 'opencli', 'browser', 'bind', '--workspace', 'browser:default']);
899
+ await program.parseAsync(['node', 'opencli', 'browser', 'state']);
908
900
  expect(mockBrowserConnect).not.toHaveBeenCalled();
909
- expect(mockBindTab).not.toHaveBeenCalled();
910
- const out = lastJsonLog();
911
- expect(out.error.code).toBe('invalid_bind_workspace');
901
+ expect(stderrSpy.mock.calls.flat().join('')).toContain('--session <name> is required');
912
902
  expect(process.exitCode).toBeDefined();
913
903
  });
914
- it('runs browser commands against an explicit bound workspace', async () => {
904
+ it('runs browser commands against an explicit session', async () => {
915
905
  const program = createProgram('', '');
916
- await program.parseAsync(['node', 'opencli', 'browser', '--workspace', 'bound:default', 'state']);
917
- expect(mockBrowserConnect).toHaveBeenCalledWith({ timeout: 30, workspace: 'bound:default', windowMode: 'foreground' });
906
+ await program.parseAsync(['node', 'opencli', 'browser', '--session', 'test', 'state']);
907
+ expect(mockBrowserConnect).toHaveBeenCalledWith({ timeout: 30, session: 'test', surface: 'browser', windowMode: 'foreground' });
918
908
  expect(browserState.page?.snapshot).toHaveBeenCalled();
919
909
  });
920
910
  it('passes browser --window through Commander options without relying on env pre-processing', async () => {
921
911
  const program = createProgram('', '');
922
- await program.parseAsync(['node', 'opencli', 'browser', '--window', 'background', 'state']);
923
- expect(mockBrowserConnect).toHaveBeenCalledWith({ timeout: 30, workspace: 'browser:default', windowMode: 'background' });
912
+ await program.parseAsync(['node', 'opencli', 'browser', '--session', 'test', '--window', 'background', 'state']);
913
+ expect(mockBrowserConnect).toHaveBeenCalledWith({ timeout: 30, session: 'test', surface: 'browser', windowMode: 'background' });
924
914
  expect(browserState.page?.snapshot).toHaveBeenCalled();
925
915
  });
926
- it('releases non-bound browser tab leases when --keep-tab=false', async () => {
927
- const program = createProgram('', '');
928
- await program.parseAsync(['node', 'opencli', 'browser', '--keep-tab', 'false', 'state']);
929
- expect(browserState.page?.snapshot).toHaveBeenCalled();
930
- expect(browserState.page?.closeWindow).toHaveBeenCalled();
931
- });
932
- it('does not auto-release explicit bound workspaces when --keep-tab=false', async () => {
933
- const program = createProgram('', '');
934
- await program.parseAsync(['node', 'opencli', 'browser', '--workspace', 'bound:default', '--keep-tab', 'false', 'state']);
935
- expect(browserState.page?.snapshot).toHaveBeenCalled();
936
- expect(browserState.page?.closeWindow).not.toHaveBeenCalled();
937
- expect(stderrSpy.mock.calls.flat().join('')).toContain('--window/--keep-tab ignored for bound:* workspaces');
938
- });
939
916
  it('passes the opt-in AX source to browser state', async () => {
940
917
  const program = createProgram('', '');
941
- await program.parseAsync(['node', 'opencli', 'browser', 'state', '--source', 'ax']);
918
+ await program.parseAsync(['node', 'opencli', 'browser', '--session', 'test', 'state', '--source', 'ax']);
942
919
  expect(browserState.page?.snapshot).toHaveBeenCalledWith({ viewportExpand: 2000, source: 'ax' });
943
920
  });
944
921
  it('prints DOM vs AX snapshot metrics without changing default state output', async () => {
@@ -952,7 +929,7 @@ describe('browser tab targeting commands', () => {
952
929
  }),
953
930
  };
954
931
  const program = createProgram('', '');
955
- await program.parseAsync(['node', 'opencli', 'browser', 'state', '--compare-sources']);
932
+ await program.parseAsync(['node', 'opencli', 'browser', '--session', 'test', 'state', '--compare-sources']);
956
933
  expect(browserState.page?.snapshot).toHaveBeenCalledWith({ viewportExpand: 2000, source: 'dom' });
957
934
  expect(browserState.page?.snapshot).toHaveBeenCalledWith({ viewportExpand: 2000, source: 'ax' });
958
935
  const out = lastJsonLog();
@@ -970,7 +947,7 @@ describe('browser tab targeting commands', () => {
970
947
  }),
971
948
  };
972
949
  const program = createProgram('', '');
973
- await program.parseAsync(['node', 'opencli', 'browser', 'state', '--compare-sources']);
950
+ await program.parseAsync(['node', 'opencli', 'browser', '--session', 'test', 'state', '--compare-sources']);
974
951
  const out = lastJsonLog();
975
952
  expect(out.sources.dom).toMatchObject({ ok: true, refs: 1 });
976
953
  expect(out.sources.ax).toMatchObject({
@@ -980,7 +957,7 @@ describe('browser tab targeting commands', () => {
980
957
  });
981
958
  it('rejects unknown browser state sources before touching the page', async () => {
982
959
  const program = createProgram('', '');
983
- await program.parseAsync(['node', 'opencli', 'browser', 'state', '--source', 'magic']);
960
+ await program.parseAsync(['node', 'opencli', 'browser', '--session', 'test', 'state', '--source', 'magic']);
984
961
  expect(browserState.page?.snapshot).not.toHaveBeenCalled();
985
962
  const out = lastJsonLog();
986
963
  expect(out.error.code).toBe('invalid_source');
@@ -988,7 +965,7 @@ describe('browser tab targeting commands', () => {
988
965
  });
989
966
  it('captures annotated screenshots through the visual ref overlay path', async () => {
990
967
  const program = createProgram('', '');
991
- await program.parseAsync(['node', 'opencli', 'browser', 'screenshot', '--annotate']);
968
+ await program.parseAsync(['node', 'opencli', 'browser', '--session', 'test', 'screenshot', '--annotate']);
992
969
  expect(browserState.page?.annotatedScreenshot).toHaveBeenCalledWith({
993
970
  fullPage: false,
994
971
  annotate: true,
@@ -999,38 +976,36 @@ describe('browser tab targeting commands', () => {
999
976
  expect(browserState.page?.screenshot).not.toHaveBeenCalled();
1000
977
  expect(consoleLogSpy).toHaveBeenLastCalledWith('annotated-base64-shot');
1001
978
  });
1002
- it('blocks history navigation on bound workspaces unless explicitly allowed', async () => {
979
+ it('allows history navigation in a bound session', async () => {
1003
980
  browserState.page = {
1004
981
  ...browserState.page,
1005
- workspace: 'bound:default',
1006
982
  evaluate: vi.fn(),
1007
983
  wait: vi.fn(),
984
+ session: 'test',
1008
985
  };
1009
986
  const program = createProgram('', '');
1010
- await program.parseAsync(['node', 'opencli', 'browser', '--workspace', 'bound:default', 'back']);
1011
- expect(browserState.page?.evaluate).not.toHaveBeenCalled();
1012
- const out = lastJsonLog();
1013
- 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()');
1014
989
  });
1015
- it('unbinds a bound workspace through the daemon close-window command', async () => {
990
+ it('unbinds a session through the daemon close-window command', async () => {
1016
991
  const program = createProgram('', '');
1017
- await program.parseAsync(['node', 'opencli', 'browser', 'unbind', '--workspace', 'bound:default']);
1018
- expect(mockBrowserConnect).toHaveBeenCalledWith({ timeout: 30, workspace: 'bound:default' });
1019
- 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' });
1020
995
  const out = lastJsonLog();
1021
- expect(out).toEqual({ unbound: true, workspace: 'bound:default' });
996
+ expect(out).toEqual({ unbound: true, session: 'test' });
1022
997
  });
1023
998
  it('does not print false success when unbind fails', async () => {
1024
- 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.'));
1025
1000
  const program = createProgram('', '');
1026
- await program.parseAsync(['node', 'opencli', 'browser', 'unbind', '--workspace', 'bound:default']);
1001
+ await program.parseAsync(['node', 'opencli', 'browser', '--session', 'test', 'unbind']);
1027
1002
  const out = lastJsonLog();
1028
1003
  expect(out.error.code).toBe('bound_session_missing');
1029
1004
  expect(process.exitCode).toBeDefined();
1030
1005
  });
1031
1006
  it('accepts JavaScript dialogs through the browser dialog command', async () => {
1032
1007
  const program = createProgram('', '');
1033
- await program.parseAsync(['node', 'opencli', 'browser', 'dialog', 'accept', '--text', 'ok']);
1008
+ await program.parseAsync(['node', 'opencli', 'browser', '--session', 'test', 'dialog', 'accept', '--text', 'ok']);
1034
1009
  expect(browserState.page?.handleJavaScriptDialog).toHaveBeenCalledWith(true, 'ok');
1035
1010
  const out = lastJsonLog();
1036
1011
  expect(out).toEqual({ handled: true, action: 'accept', text: 'ok' });
@@ -1041,7 +1016,7 @@ describe('browser tab targeting commands', () => {
1041
1016
  evaluate: vi.fn().mockRejectedValue(new Error('JavaScript dialog showing')),
1042
1017
  };
1043
1018
  const program = createProgram('', '');
1044
- await program.parseAsync(['node', 'opencli', 'browser', 'eval', 'document.title']);
1019
+ await program.parseAsync(['node', 'opencli', 'browser', '--session', 'test', 'eval', 'document.title']);
1045
1020
  const out = lastJsonLog();
1046
1021
  expect(out.error.code).toBe('javascript_dialog_open');
1047
1022
  expect(out.error.hint).toContain('browser dialog accept');
@@ -1049,7 +1024,7 @@ describe('browser tab targeting commands', () => {
1049
1024
  });
1050
1025
  it('binds browser commands to an explicit target tab via --tab', async () => {
1051
1026
  const program = createProgram('', '');
1052
- 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']);
1053
1028
  expect(browserState.page?.setActivePage).toHaveBeenCalledWith('tab-2');
1054
1029
  expect(browserState.page?.evaluate).toHaveBeenCalledWith('document.title');
1055
1030
  });
@@ -1061,7 +1036,7 @@ describe('browser tab targeting commands', () => {
1061
1036
  evaluate: vi.fn(),
1062
1037
  };
1063
1038
  const program = createProgram('', '');
1064
- 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']);
1065
1040
  expect(process.exitCode).toBeDefined();
1066
1041
  expect(browserState.page?.setActivePage).not.toHaveBeenCalled();
1067
1042
  expect(browserState.page?.evaluate).not.toHaveBeenCalled();
@@ -1069,50 +1044,50 @@ describe('browser tab targeting commands', () => {
1069
1044
  });
1070
1045
  it('lists tabs with target IDs via browser tab list', async () => {
1071
1046
  const program = createProgram('', '');
1072
- await program.parseAsync(['node', 'opencli', 'browser', 'tab', 'list']);
1047
+ await program.parseAsync(['node', 'opencli', 'browser', '--session', 'test', 'tab', 'list']);
1073
1048
  expect(browserState.page?.tabs).toHaveBeenCalledTimes(1);
1074
1049
  expect(consoleLogSpy.mock.calls.flat().join('\n')).toContain('"page": "tab-1"');
1075
1050
  expect(consoleLogSpy.mock.calls.flat().join('\n')).toContain('"page": "tab-2"');
1076
1051
  });
1077
1052
  it('creates a new tab and prints its target ID', async () => {
1078
1053
  const program = createProgram('', '');
1079
- 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']);
1080
1055
  expect(browserState.page?.newTab).toHaveBeenCalledWith('https://three.example');
1081
1056
  expect(consoleLogSpy.mock.calls.flat().join('\n')).toContain('"page": "tab-3"');
1082
1057
  });
1083
1058
  it('prints the resolved target ID when browser open creates or navigates a tab', async () => {
1084
1059
  const program = createProgram('', '');
1085
- await program.parseAsync(['node', 'opencli', 'browser', 'open', 'https://example.com']);
1060
+ await program.parseAsync(['node', 'opencli', 'browser', '--session', 'test', 'open', 'https://example.com']);
1086
1061
  expect(browserState.page?.goto).toHaveBeenCalledWith('https://example.com');
1087
1062
  expect(consoleLogSpy.mock.calls.flat().join('\n')).toContain('"url": "https://one.example"');
1088
1063
  expect(consoleLogSpy.mock.calls.flat().join('\n')).toContain('"page": "tab-1"');
1089
1064
  });
1090
1065
  it('lists cross-origin frames via browser frames', async () => {
1091
1066
  const program = createProgram('', '');
1092
- await program.parseAsync(['node', 'opencli', 'browser', 'frames']);
1067
+ await program.parseAsync(['node', 'opencli', 'browser', '--session', 'test', 'frames']);
1093
1068
  expect(browserState.page?.frames).toHaveBeenCalledTimes(1);
1094
1069
  expect(consoleLogSpy.mock.calls.flat().join('\n')).toContain('"frameId": "frame-1"');
1095
1070
  expect(consoleLogSpy.mock.calls.flat().join('\n')).toContain('"url": "https://x.example/embed"');
1096
1071
  });
1097
1072
  it('routes browser eval --frame through frame-targeted evaluation', async () => {
1098
1073
  const program = createProgram('', '');
1099
- 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']);
1100
1075
  expect(browserState.page?.evaluateInFrame).toHaveBeenCalledWith('document.title', 0);
1101
1076
  expect(browserState.page?.evaluate).not.toHaveBeenCalled();
1102
1077
  expect(consoleLogSpy.mock.calls.flat().join('\n')).toContain('inside frame');
1103
1078
  });
1104
1079
  it('does not promote a newly created tab to the persisted default target', async () => {
1105
1080
  const program = createProgram('', '');
1106
- await program.parseAsync(['node', 'opencli', 'browser', 'tab', 'new', 'https://three.example']);
1107
- 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']);
1108
1083
  expect(browserState.page?.newTab).toHaveBeenCalledWith('https://three.example');
1109
1084
  expect(browserState.page?.setActivePage).not.toHaveBeenCalled();
1110
1085
  expect(browserState.page?.evaluate).toHaveBeenCalledWith('document.title');
1111
1086
  });
1112
1087
  it('persists an explicitly selected tab as the default target for later untargeted commands', async () => {
1113
1088
  const program = createProgram('', '');
1114
- await program.parseAsync(['node', 'opencli', 'browser', 'tab', 'select', 'tab-2']);
1115
- 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']);
1116
1091
  expect(browserState.page?.selectTab).toHaveBeenCalledWith('tab-2');
1117
1092
  expect(browserState.page?.setActivePage).toHaveBeenCalledWith('tab-2');
1118
1093
  expect(browserState.page?.evaluate).toHaveBeenCalledWith('document.title');
@@ -1121,7 +1096,7 @@ describe('browser tab targeting commands', () => {
1121
1096
  it('clears a saved default target when it is no longer present in the current session', async () => {
1122
1097
  const cacheDir = String(process.env.OPENCLI_CACHE_DIR);
1123
1098
  const program = createProgram('', '');
1124
- await program.parseAsync(['node', 'opencli', 'browser', 'tab', 'select', 'tab-2']);
1099
+ await program.parseAsync(['node', 'opencli', 'browser', '--session', 'test', 'tab', 'select', 'tab-2']);
1125
1100
  expect(fs.existsSync(getBrowserStateFile(cacheDir))).toBe(true);
1126
1101
  browserState.page = {
1127
1102
  setActivePage: vi.fn(),
@@ -1130,35 +1105,36 @@ describe('browser tab targeting commands', () => {
1130
1105
  evaluate: vi.fn().mockResolvedValue({ ok: true }),
1131
1106
  readNetworkCapture: vi.fn().mockResolvedValue([]),
1132
1107
  };
1133
- await program.parseAsync(['node', 'opencli', 'browser', 'eval', 'document.title']);
1108
+ await program.parseAsync(['node', 'opencli', 'browser', '--session', 'test', 'eval', 'document.title']);
1134
1109
  expect(browserState.page?.setActivePage).not.toHaveBeenCalled();
1135
1110
  expect(browserState.page?.evaluate).toHaveBeenCalledWith('document.title');
1136
1111
  expect(fs.existsSync(getBrowserStateFile(cacheDir))).toBe(false);
1137
1112
  });
1138
1113
  it('clears the persisted default target when that tab is closed', async () => {
1139
1114
  const program = createProgram('', '');
1140
- await program.parseAsync(['node', 'opencli', 'browser', 'tab', 'select', 'tab-2']);
1141
- 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']);
1142
1117
  vi.mocked(browserState.page?.setActivePage).mockClear();
1143
1118
  vi.mocked(browserState.page?.evaluate).mockClear();
1144
- await program.parseAsync(['node', 'opencli', 'browser', 'eval', 'document.title']);
1119
+ await program.parseAsync(['node', 'opencli', 'browser', '--session', 'test', 'eval', 'document.title']);
1145
1120
  expect(browserState.page?.closeTab).toHaveBeenCalledWith('tab-2');
1146
1121
  expect(browserState.page?.setActivePage).not.toHaveBeenCalled();
1147
1122
  expect(browserState.page?.evaluate).toHaveBeenCalledWith('document.title');
1148
1123
  });
1149
1124
  it('closes a tab by target ID', async () => {
1150
1125
  const program = createProgram('', '');
1151
- await program.parseAsync(['node', 'opencli', 'browser', 'tab', 'close', 'tab-2']);
1126
+ await program.parseAsync(['node', 'opencli', 'browser', '--session', 'test', 'tab', 'close', 'tab-2']);
1152
1127
  expect(browserState.page?.closeTab).toHaveBeenCalledWith('tab-2');
1153
1128
  expect(consoleLogSpy.mock.calls.flat().join('\n')).toContain('"closed": "tab-2"');
1154
1129
  });
1155
1130
  it('rejects closing a stale tab target ID that is no longer in the current session', async () => {
1156
1131
  browserState.page = {
1132
+ session: 'test',
1157
1133
  tabs: vi.fn().mockResolvedValue([]),
1158
1134
  closeTab: vi.fn(),
1159
1135
  };
1160
1136
  const program = createProgram('', '');
1161
- await program.parseAsync(['node', 'opencli', 'browser', 'tab', 'close', 'tab-stale']);
1137
+ await program.parseAsync(['node', 'opencli', 'browser', '--session', 'test', 'tab', 'close', 'tab-stale']);
1162
1138
  expect(process.exitCode).toBeDefined();
1163
1139
  expect(browserState.page?.closeTab).not.toHaveBeenCalled();
1164
1140
  expect(stderrSpy.mock.calls.flat().join('\n')).toContain('Target tab tab-stale is not part of the current browser session');
@@ -1205,7 +1181,7 @@ describe('browser tab targeting commands', () => {
1205
1181
  ]),
1206
1182
  };
1207
1183
  const program = createProgram('', '');
1208
- await program.parseAsync(['node', 'opencli', 'browser', 'analyze', 'https://target.example/']);
1184
+ await program.parseAsync(['node', 'opencli', 'browser', '--session', 'test', 'analyze', 'https://target.example/']);
1209
1185
  const out = lastJsonLog();
1210
1186
  expect(browserState.page?.readNetworkCapture).toHaveBeenCalledTimes(2);
1211
1187
  expect(out.anti_bot.vendor).toBe('cloudflare');
@@ -1266,7 +1242,7 @@ describe('browser tab targeting commands', () => {
1266
1242
  readNetworkCapture: vi.fn().mockResolvedValue([]),
1267
1243
  };
1268
1244
  const program = createProgram('', '');
1269
- await program.parseAsync(['node', 'opencli', 'browser', 'analyze', 'https://target.example/']);
1245
+ await program.parseAsync(['node', 'opencli', 'browser', '--session', 'test', 'analyze', 'https://target.example/']);
1270
1246
  const out = lastJsonLog();
1271
1247
  expect(browserState.page?.readNetworkCapture).toHaveBeenCalledTimes(2);
1272
1248
  expect(bufferReads).toBe(2);
@@ -1305,7 +1281,7 @@ describe('browser tab targeting commands', () => {
1305
1281
  ]),
1306
1282
  };
1307
1283
  const program = createProgram('', '');
1308
- 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']);
1309
1285
  const out = lastJsonLog();
1310
1286
  expect(browserState.page?.startNetworkCapture).toHaveBeenCalledTimes(1);
1311
1287
  expect(browserState.page?.evaluate).toHaveBeenCalledWith(expect.stringContaining('window.__opencli_net'));
@@ -1353,7 +1329,7 @@ describe('browser tab targeting commands', () => {
1353
1329
  readNetworkCapture: vi.fn().mockResolvedValue([]),
1354
1330
  };
1355
1331
  const program = createProgram('', '');
1356
- 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']);
1357
1333
  const out = lastJsonLog();
1358
1334
  expect(browserState.page?.startNetworkCapture).toHaveBeenCalledTimes(1);
1359
1335
  expect(browserState.page?.readNetworkCapture).toHaveBeenCalledTimes(2);
@@ -1377,7 +1353,7 @@ describe('browser tab targeting commands', () => {
1377
1353
  tabs: vi.fn().mockResolvedValue([{ index: 0, page: 'tab-1', url: 'https://target.example', title: 'Target', active: true }]),
1378
1354
  };
1379
1355
  const program = createProgram('', '');
1380
- 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']);
1381
1357
  expect(browserState.page?.waitForDownload).toHaveBeenCalledWith('receipt', 900);
1382
1358
  expect(lastJsonLog()).toEqual({
1383
1359
  downloaded: true,
@@ -1403,7 +1379,7 @@ describe('browser tab targeting commands', () => {
1403
1379
  tabs: vi.fn().mockResolvedValue([{ index: 0, page: 'tab-1', url: 'https://target.example', title: 'Target', active: true }]),
1404
1380
  };
1405
1381
  const program = createProgram('', '');
1406
- 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']);
1407
1383
  const out = lastJsonLog();
1408
1384
  expect(out.error.code).toBe('download_not_seen');
1409
1385
  expect(out.download.elapsedMs).toBe(900);
@@ -1413,10 +1389,10 @@ describe('browser tab targeting commands', () => {
1413
1389
  describe('browser network command', () => {
1414
1390
  const consoleLogSpy = vi.spyOn(console, 'log').mockImplementation(() => { });
1415
1391
  function getNetworkCachePath(cacheDir) {
1416
- return path.join(cacheDir, 'browser-network', 'browser_default.json');
1392
+ return path.join(cacheDir, 'browser-network', 'test.json');
1417
1393
  }
1418
- function getBoundNetworkCachePath(cacheDir) {
1419
- return path.join(cacheDir, 'browser-network', 'bound_default.json');
1394
+ function getCustomNetworkCachePath(cacheDir) {
1395
+ return path.join(cacheDir, 'browser-network', 'custom.json');
1420
1396
  }
1421
1397
  function lastJsonLog() {
1422
1398
  const calls = consoleLogSpy.mock.calls;
@@ -1434,6 +1410,7 @@ describe('browser network command', () => {
1434
1410
  mockBrowserConnect.mockClear();
1435
1411
  mockBrowserClose.mockReset().mockResolvedValue(undefined);
1436
1412
  browserState.page = {
1413
+ session: 'test',
1437
1414
  setActivePage: vi.fn(),
1438
1415
  getActivePage: vi.fn().mockReturnValue('tab-1'),
1439
1416
  tabs: vi.fn().mockResolvedValue([{ page: 'tab-1', active: true }]),
@@ -1460,7 +1437,7 @@ describe('browser network command', () => {
1460
1437
  it('emits JSON with shape previews and persists the capture to disk', async () => {
1461
1438
  const cacheDir = String(process.env.OPENCLI_CACHE_DIR);
1462
1439
  const program = createProgram('', '');
1463
- await program.parseAsync(['node', 'opencli', 'browser', 'network']);
1440
+ await program.parseAsync(['node', 'opencli', 'browser', '--session', 'test', 'network']);
1464
1441
  const out = lastJsonLog();
1465
1442
  expect(out.count).toBe(1);
1466
1443
  expect(out.filtered_out).toBe(1);
@@ -1469,22 +1446,22 @@ describe('browser network command', () => {
1469
1446
  expect(out.entries[0]).not.toHaveProperty('body');
1470
1447
  expect(fs.existsSync(getNetworkCachePath(cacheDir))).toBe(true);
1471
1448
  });
1472
- it('uses the selected browser workspace for network cache scope', async () => {
1449
+ it('uses the selected browser session for network cache scope', async () => {
1473
1450
  const cacheDir = String(process.env.OPENCLI_CACHE_DIR);
1474
1451
  browserState.page = {
1475
1452
  ...browserState.page,
1476
- workspace: 'bound:default',
1453
+ session: 'custom',
1477
1454
  };
1478
1455
  const program = createProgram('', '');
1479
- await program.parseAsync(['node', 'opencli', 'browser', '--workspace', 'bound:default', 'network']);
1456
+ await program.parseAsync(['node', 'opencli', 'browser', '--session', 'custom', 'network']);
1480
1457
  const out = lastJsonLog();
1481
- expect(out.workspace).toBe('bound:default');
1482
- expect(fs.existsSync(getBoundNetworkCachePath(cacheDir))).toBe(true);
1458
+ expect(out.session).toBe('custom');
1459
+ expect(fs.existsSync(getCustomNetworkCachePath(cacheDir))).toBe(true);
1483
1460
  expect(fs.existsSync(getNetworkCachePath(cacheDir))).toBe(false);
1484
1461
  });
1485
1462
  it('--all includes static resources that the default filter drops', async () => {
1486
1463
  const program = createProgram('', '');
1487
- await program.parseAsync(['node', 'opencli', 'browser', 'network', '--all']);
1464
+ await program.parseAsync(['node', 'opencli', 'browser', '--session', 'test', 'network', '--all']);
1488
1465
  const out = lastJsonLog();
1489
1466
  expect(out.count).toBe(2);
1490
1467
  expect(out.entries.map((e) => e.key)).toContain('UserTweets');
@@ -1519,7 +1496,7 @@ describe('browser network command', () => {
1519
1496
  },
1520
1497
  ]);
1521
1498
  const program = createProgram('', '');
1522
- await program.parseAsync(['node', 'opencli', 'browser', 'network', '--since', '120s', '--failed']);
1499
+ await program.parseAsync(['node', 'opencli', 'browser', '--session', 'test', 'network', '--since', '120s', '--failed']);
1523
1500
  const out = lastJsonLog();
1524
1501
  expect(out.count).toBe(1);
1525
1502
  expect(out.entries[0].url).toBe('https://api.example.com/new-fail');
@@ -1543,7 +1520,7 @@ describe('browser network command', () => {
1543
1520
  },
1544
1521
  ]);
1545
1522
  const program = createProgram('', '');
1546
- await program.parseAsync(['node', 'opencli', 'browser', 'network']);
1523
+ await program.parseAsync(['node', 'opencli', 'browser', '--session', 'test', 'network']);
1547
1524
  const out = lastJsonLog();
1548
1525
  expect(out.count).toBe(1);
1549
1526
  expect(out.filtered_out).toBe(1);
@@ -1553,16 +1530,16 @@ describe('browser network command', () => {
1553
1530
  });
1554
1531
  it('--raw emits full bodies inline for every entry', async () => {
1555
1532
  const program = createProgram('', '');
1556
- await program.parseAsync(['node', 'opencli', 'browser', 'network', '--raw']);
1533
+ await program.parseAsync(['node', 'opencli', 'browser', '--session', 'test', 'network', '--raw']);
1557
1534
  const out = lastJsonLog();
1558
1535
  expect(out.entries[0].body).toEqual({ data: { user: { rest_id: '42' } } });
1559
1536
  expect(out.entries[0].timestamp).toMatch(/T/);
1560
1537
  });
1561
1538
  it('--detail <key> returns the full body for the requested entry', async () => {
1562
1539
  const program = createProgram('', '');
1563
- await program.parseAsync(['node', 'opencli', 'browser', 'network']);
1540
+ await program.parseAsync(['node', 'opencli', 'browser', '--session', 'test', 'network']);
1564
1541
  consoleLogSpy.mockClear();
1565
- await program.parseAsync(['node', 'opencli', 'browser', 'network', '--detail', 'UserTweets']);
1542
+ await program.parseAsync(['node', 'opencli', 'browser', '--session', 'test', 'network', '--detail', 'UserTweets']);
1566
1543
  const out = lastJsonLog();
1567
1544
  expect(out.key).toBe('UserTweets');
1568
1545
  expect(out.body).toEqual({ data: { user: { rest_id: '42' } } });
@@ -1571,9 +1548,9 @@ describe('browser network command', () => {
1571
1548
  });
1572
1549
  it('--detail reports key_not_found with the list of available keys', async () => {
1573
1550
  const program = createProgram('', '');
1574
- await program.parseAsync(['node', 'opencli', 'browser', 'network']);
1551
+ await program.parseAsync(['node', 'opencli', 'browser', '--session', 'test', 'network']);
1575
1552
  consoleLogSpy.mockClear();
1576
- await program.parseAsync(['node', 'opencli', 'browser', 'network', '--detail', 'NopeOp']);
1553
+ await program.parseAsync(['node', 'opencli', 'browser', '--session', 'test', 'network', '--detail', 'NopeOp']);
1577
1554
  const out = lastJsonLog();
1578
1555
  expect(out.error.code).toBe('key_not_found');
1579
1556
  expect(out.error.available_keys).toContain('UserTweets');
@@ -1581,7 +1558,7 @@ describe('browser network command', () => {
1581
1558
  });
1582
1559
  it('--detail reports cache_missing when no capture has been persisted yet', async () => {
1583
1560
  const program = createProgram('', '');
1584
- await program.parseAsync(['node', 'opencli', 'browser', 'network', '--detail', 'UserTweets']);
1561
+ await program.parseAsync(['node', 'opencli', 'browser', '--session', 'test', 'network', '--detail', 'UserTweets']);
1585
1562
  const out = lastJsonLog();
1586
1563
  expect(out.error.code).toBe('cache_missing');
1587
1564
  expect(process.exitCode).toBeDefined();
@@ -1589,7 +1566,7 @@ describe('browser network command', () => {
1589
1566
  it('emits capture_failed when readNetworkCapture throws', async () => {
1590
1567
  browserState.page.readNetworkCapture = vi.fn().mockRejectedValue(new Error('CDP disconnected'));
1591
1568
  const program = createProgram('', '');
1592
- await program.parseAsync(['node', 'opencli', 'browser', 'network']);
1569
+ await program.parseAsync(['node', 'opencli', 'browser', '--session', 'test', 'network']);
1593
1570
  const out = lastJsonLog();
1594
1571
  expect(out.error.code).toBe('capture_failed');
1595
1572
  expect(out.error.message).toContain('CDP disconnected');
@@ -1602,7 +1579,7 @@ describe('browser network command', () => {
1602
1579
  const clashDir = path.join(cacheDir, 'browser-network');
1603
1580
  fs.writeFileSync(clashDir, 'not-a-directory');
1604
1581
  const program = createProgram('', '');
1605
- await program.parseAsync(['node', 'opencli', 'browser', 'network']);
1582
+ await program.parseAsync(['node', 'opencli', 'browser', '--session', 'test', 'network']);
1606
1583
  const out = lastJsonLog();
1607
1584
  expect(out.cache_warning).toMatch(/Could not persist capture cache/);
1608
1585
  expect(out.count).toBe(1);
@@ -1627,7 +1604,7 @@ describe('browser network command', () => {
1627
1604
  });
1628
1605
  it('narrows entries to those whose shape has ALL named fields', async () => {
1629
1606
  const program = createProgram('', '');
1630
- 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']);
1631
1608
  const out = lastJsonLog();
1632
1609
  expect(out.count).toBe(1);
1633
1610
  expect(out.filter).toEqual(['author', 'text', 'likes']);
@@ -1636,14 +1613,14 @@ describe('browser network command', () => {
1636
1613
  });
1637
1614
  it('matches container segments too, not just leaf names (any-segment rule)', async () => {
1638
1615
  const program = createProgram('', '');
1639
- await program.parseAsync(['node', 'opencli', 'browser', 'network', '--filter', 'data,items']);
1616
+ await program.parseAsync(['node', 'opencli', 'browser', '--session', 'test', 'network', '--filter', 'data,items']);
1640
1617
  const out = lastJsonLog();
1641
1618
  expect(out.count).toBe(1);
1642
1619
  expect(out.entries[0].key).toBe('UserTweets');
1643
1620
  });
1644
1621
  it('drops entries that are missing any required field (AND semantics)', async () => {
1645
1622
  const program = createProgram('', '');
1646
- await program.parseAsync(['node', 'opencli', 'browser', 'network', '--filter', 'author,followers']);
1623
+ await program.parseAsync(['node', 'opencli', 'browser', '--session', 'test', 'network', '--filter', 'author,followers']);
1647
1624
  const out = lastJsonLog();
1648
1625
  expect(out.count).toBe(0);
1649
1626
  expect(out.entries).toEqual([]);
@@ -1652,7 +1629,7 @@ describe('browser network command', () => {
1652
1629
  });
1653
1630
  it('returns empty entries (not an error) when nothing matches', async () => {
1654
1631
  const program = createProgram('', '');
1655
- await program.parseAsync(['node', 'opencli', 'browser', 'network', '--filter', 'nonexistent_field']);
1632
+ await program.parseAsync(['node', 'opencli', 'browser', '--session', 'test', 'network', '--filter', 'nonexistent_field']);
1656
1633
  const out = lastJsonLog();
1657
1634
  expect(out.count).toBe(0);
1658
1635
  expect(out.entries).toEqual([]);
@@ -1661,43 +1638,43 @@ describe('browser network command', () => {
1661
1638
  });
1662
1639
  it('is case-sensitive so agents do not conflate `Id` with `id`', async () => {
1663
1640
  const program = createProgram('', '');
1664
- await program.parseAsync(['node', 'opencli', 'browser', 'network', '--filter', 'Data']);
1641
+ await program.parseAsync(['node', 'opencli', 'browser', '--session', 'test', 'network', '--filter', 'Data']);
1665
1642
  const out = lastJsonLog();
1666
1643
  expect(out.count).toBe(0);
1667
1644
  });
1668
1645
  it('persists the full (unfiltered) capture so --detail lookups still find filtered-out keys', async () => {
1669
1646
  const program = createProgram('', '');
1670
- 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']);
1671
1648
  consoleLogSpy.mockClear();
1672
- await program.parseAsync(['node', 'opencli', 'browser', 'network', '--detail', 'UserProfile']);
1649
+ await program.parseAsync(['node', 'opencli', 'browser', '--session', 'test', 'network', '--detail', 'UserProfile']);
1673
1650
  const out = lastJsonLog();
1674
1651
  expect(out.key).toBe('UserProfile');
1675
1652
  expect(out.body).toEqual({ data: { user: { id: 'u1', followers: 10 } } });
1676
1653
  });
1677
1654
  it('composes with --raw: entries keep full bodies, filter still narrows', async () => {
1678
1655
  const program = createProgram('', '');
1679
- await program.parseAsync(['node', 'opencli', 'browser', 'network', '--filter', 'author', '--raw']);
1656
+ await program.parseAsync(['node', 'opencli', 'browser', '--session', 'test', 'network', '--filter', 'author', '--raw']);
1680
1657
  const out = lastJsonLog();
1681
1658
  expect(out.count).toBe(1);
1682
1659
  expect(out.entries[0].body).toEqual({ data: { items: [{ author: 'a', text: 't', likes: 1 }] } });
1683
1660
  });
1684
1661
  it('reports invalid_filter for empty value', async () => {
1685
1662
  const program = createProgram('', '');
1686
- await program.parseAsync(['node', 'opencli', 'browser', 'network', '--filter', '']);
1663
+ await program.parseAsync(['node', 'opencli', 'browser', '--session', 'test', 'network', '--filter', '']);
1687
1664
  const out = lastJsonLog();
1688
1665
  expect(out.error.code).toBe('invalid_filter');
1689
1666
  expect(process.exitCode).toBeDefined();
1690
1667
  });
1691
1668
  it('reports invalid_filter for commas-only value', async () => {
1692
1669
  const program = createProgram('', '');
1693
- await program.parseAsync(['node', 'opencli', 'browser', 'network', '--filter', ',,,']);
1670
+ await program.parseAsync(['node', 'opencli', 'browser', '--session', 'test', 'network', '--filter', ',,,']);
1694
1671
  const out = lastJsonLog();
1695
1672
  expect(out.error.code).toBe('invalid_filter');
1696
1673
  expect(process.exitCode).toBeDefined();
1697
1674
  });
1698
1675
  it('rejects --filter combined with --detail as invalid_args', async () => {
1699
1676
  const program = createProgram('', '');
1700
- 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']);
1701
1678
  const out = lastJsonLog();
1702
1679
  expect(out.error.code).toBe('invalid_args');
1703
1680
  expect(out.error.message).toContain('--filter');
@@ -1719,7 +1696,7 @@ describe('browser network command', () => {
1719
1696
  },
1720
1697
  ]);
1721
1698
  const program = createProgram('', '');
1722
- await program.parseAsync(['node', 'opencli', 'browser', 'network']);
1699
+ await program.parseAsync(['node', 'opencli', 'browser', '--session', 'test', 'network']);
1723
1700
  const out = lastJsonLog();
1724
1701
  expect(out.body_truncated_count).toBe(1);
1725
1702
  expect(out.entries[0].body_truncated).toBe(true);
@@ -1738,9 +1715,9 @@ describe('browser network command', () => {
1738
1715
  },
1739
1716
  ]);
1740
1717
  const program = createProgram('', '');
1741
- await program.parseAsync(['node', 'opencli', 'browser', 'network']);
1718
+ await program.parseAsync(['node', 'opencli', 'browser', '--session', 'test', 'network']);
1742
1719
  consoleLogSpy.mockClear();
1743
- 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']);
1744
1721
  const out = lastJsonLog();
1745
1722
  expect(out.body_truncated).toBe(true);
1746
1723
  expect(out.body_full_size).toBe(50_000_000);
@@ -1758,10 +1735,10 @@ describe('browser network command', () => {
1758
1735
  },
1759
1736
  ]);
1760
1737
  const program = createProgram('', '');
1761
- await program.parseAsync(['node', 'opencli', 'browser', 'network']);
1738
+ await program.parseAsync(['node', 'opencli', 'browser', '--session', 'test', 'network']);
1762
1739
  consoleLogSpy.mockClear();
1763
1740
  await program.parseAsync([
1764
- 'node', 'opencli', 'browser', 'network',
1741
+ 'node', 'opencli', 'browser', '--session', 'test', 'network',
1765
1742
  '--detail', 'GET api.example.com/plain',
1766
1743
  '--max-body', '100',
1767
1744
  ]);
@@ -1783,10 +1760,10 @@ describe('browser network command', () => {
1783
1760
  },
1784
1761
  ]);
1785
1762
  const program = createProgram('', '');
1786
- await program.parseAsync(['node', 'opencli', 'browser', 'network']);
1763
+ await program.parseAsync(['node', 'opencli', 'browser', '--session', 'test', 'network']);
1787
1764
  consoleLogSpy.mockClear();
1788
1765
  await program.parseAsync([
1789
- 'node', 'opencli', 'browser', 'network',
1766
+ 'node', 'opencli', 'browser', '--session', 'test', 'network',
1790
1767
  '--detail', 'GET api.example.com/json',
1791
1768
  '--max-body', '10',
1792
1769
  ]);
@@ -1807,10 +1784,10 @@ describe('browser network command', () => {
1807
1784
  },
1808
1785
  ]);
1809
1786
  const program = createProgram('', '');
1810
- await program.parseAsync(['node', 'opencli', 'browser', 'network']);
1787
+ await program.parseAsync(['node', 'opencli', 'browser', '--session', 'test', 'network']);
1811
1788
  consoleLogSpy.mockClear();
1812
1789
  await program.parseAsync([
1813
- 'node', 'opencli', 'browser', 'network',
1790
+ 'node', 'opencli', 'browser', '--session', 'test', 'network',
1814
1791
  '--detail', 'GET api.example.com/x',
1815
1792
  '--max-body', 'abc',
1816
1793
  ]);
@@ -1830,7 +1807,7 @@ describe('browser network command', () => {
1830
1807
  },
1831
1808
  ]);
1832
1809
  const program = createProgram('', '');
1833
- await program.parseAsync(['node', 'opencli', 'browser', 'network', '--raw']);
1810
+ await program.parseAsync(['node', 'opencli', 'browser', '--session', 'test', 'network', '--raw']);
1834
1811
  const out = lastJsonLog();
1835
1812
  expect(out.entries).toHaveLength(1);
1836
1813
  const entry = out.entries[0];
@@ -1851,6 +1828,7 @@ describe('browser console command', () => {
1851
1828
  mockBrowserClose.mockReset().mockResolvedValue(undefined);
1852
1829
  const now = Date.now();
1853
1830
  browserState.page = {
1831
+ session: 'test',
1854
1832
  setActivePage: vi.fn(),
1855
1833
  getActivePage: vi.fn().mockReturnValue('tab-1'),
1856
1834
  tabs: vi.fn().mockResolvedValue([{ page: 'tab-1', active: true }]),
@@ -1872,7 +1850,7 @@ describe('browser console command', () => {
1872
1850
  }
1873
1851
  it('filters console messages by level and time window', async () => {
1874
1852
  const program = createProgram('', '');
1875
- 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']);
1876
1854
  const out = lastJsonLog();
1877
1855
  expect(out.count).toBe(1);
1878
1856
  expect(out.messages[0]).toMatchObject({ type: 'error', text: 'boom' });
@@ -1909,14 +1887,14 @@ describe('browser get html command', () => {
1909
1887
  const big = '<div>' + 'x'.repeat(100_000) + '</div>';
1910
1888
  browserState.page.evaluate.mockResolvedValueOnce({ kind: 'ok', html: big });
1911
1889
  const program = createProgram('', '');
1912
- await program.parseAsync(['node', 'opencli', 'browser', 'get', 'html']);
1890
+ await program.parseAsync(['node', 'opencli', 'browser', '--session', 'test', 'get', 'html']);
1913
1891
  expect(lastLogArg()).toBe(big);
1914
1892
  });
1915
1893
  it('caps output with --max and prepends a visible truncation marker', async () => {
1916
1894
  const big = '<div>' + 'x'.repeat(500) + '</div>';
1917
1895
  browserState.page.evaluate.mockResolvedValueOnce({ kind: 'ok', html: big });
1918
1896
  const program = createProgram('', '');
1919
- await program.parseAsync(['node', 'opencli', 'browser', 'get', 'html', '--max', '100']);
1897
+ await program.parseAsync(['node', 'opencli', 'browser', '--session', 'test', 'get', 'html', '--max', '100']);
1920
1898
  const out = String(lastLogArg());
1921
1899
  expect(out.startsWith('<!-- opencli: truncated 100 of')).toBe(true);
1922
1900
  expect(out.length).toBeGreaterThan(100);
@@ -1924,21 +1902,21 @@ describe('browser get html command', () => {
1924
1902
  });
1925
1903
  it('rejects negative --max with invalid_max error', async () => {
1926
1904
  const program = createProgram('', '');
1927
- await program.parseAsync(['node', 'opencli', 'browser', 'get', 'html', '--max', '-1']);
1905
+ await program.parseAsync(['node', 'opencli', 'browser', '--session', 'test', 'get', 'html', '--max', '-1']);
1928
1906
  expect(lastJsonLog().error.code).toBe('invalid_max');
1929
1907
  expect(process.exitCode).toBeDefined();
1930
1908
  expect(browserState.page.evaluate).not.toHaveBeenCalled();
1931
1909
  });
1932
1910
  it('rejects fractional --max with invalid_max error', async () => {
1933
1911
  const program = createProgram('', '');
1934
- 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']);
1935
1913
  expect(lastJsonLog().error.code).toBe('invalid_max');
1936
1914
  expect(process.exitCode).toBeDefined();
1937
1915
  expect(browserState.page.evaluate).not.toHaveBeenCalled();
1938
1916
  });
1939
1917
  it('rejects non-numeric --max (e.g. "10abc") with invalid_max error', async () => {
1940
1918
  const program = createProgram('', '');
1941
- await program.parseAsync(['node', 'opencli', 'browser', 'get', 'html', '--max', '10abc']);
1919
+ await program.parseAsync(['node', 'opencli', 'browser', '--session', 'test', 'get', 'html', '--max', '10abc']);
1942
1920
  expect(lastJsonLog().error.code).toBe('invalid_max');
1943
1921
  expect(process.exitCode).toBeDefined();
1944
1922
  expect(browserState.page.evaluate).not.toHaveBeenCalled();
@@ -1950,7 +1928,7 @@ describe('browser get html command', () => {
1950
1928
  tree: { tag: 'div', attrs: { class: 'hero' }, text: 'Hi', children: [] },
1951
1929
  });
1952
1930
  const program = createProgram('', '');
1953
- 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']);
1954
1932
  const out = lastJsonLog();
1955
1933
  expect(out.matched).toBe(1);
1956
1934
  expect(out.tree.tag).toBe('div');
@@ -1959,14 +1937,14 @@ describe('browser get html command', () => {
1959
1937
  it('--as json emits selector_not_found when matched is 0', async () => {
1960
1938
  browserState.page.evaluate.mockResolvedValueOnce({ selector: '.missing', matched: 0, tree: null });
1961
1939
  const program = createProgram('', '');
1962
- 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']);
1963
1941
  expect(lastJsonLog().error.code).toBe('selector_not_found');
1964
1942
  expect(process.exitCode).toBeDefined();
1965
1943
  });
1966
1944
  it('raw mode emits selector_not_found when the selector matches nothing', async () => {
1967
1945
  browserState.page.evaluate.mockResolvedValueOnce({ kind: 'ok', html: null });
1968
1946
  const program = createProgram('', '');
1969
- await program.parseAsync(['node', 'opencli', 'browser', 'get', 'html', '--selector', '.missing']);
1947
+ await program.parseAsync(['node', 'opencli', 'browser', '--session', 'test', 'get', 'html', '--selector', '.missing']);
1970
1948
  expect(lastJsonLog().error.code).toBe('selector_not_found');
1971
1949
  expect(process.exitCode).toBeDefined();
1972
1950
  });
@@ -1976,7 +1954,7 @@ describe('browser get html command', () => {
1976
1954
  reason: "'##$@@' is not a valid selector",
1977
1955
  });
1978
1956
  const program = createProgram('', '');
1979
- await program.parseAsync(['node', 'opencli', 'browser', 'get', 'html', '--selector', '##$@@']);
1957
+ await program.parseAsync(['node', 'opencli', 'browser', '--session', 'test', 'get', 'html', '--selector', '##$@@']);
1980
1958
  const err = lastJsonLog().error;
1981
1959
  expect(err.code).toBe('invalid_selector');
1982
1960
  expect(err.message).toContain('##$@@');
@@ -1990,7 +1968,7 @@ describe('browser get html command', () => {
1990
1968
  reason: "'##$@@' is not a valid selector",
1991
1969
  });
1992
1970
  const program = createProgram('', '');
1993
- 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']);
1994
1972
  const err = lastJsonLog().error;
1995
1973
  expect(err.code).toBe('invalid_selector');
1996
1974
  expect(err.message).toContain('##$@@');
@@ -1998,7 +1976,7 @@ describe('browser get html command', () => {
1998
1976
  });
1999
1977
  it('rejects unknown --as format with invalid_format error', async () => {
2000
1978
  const program = createProgram('', '');
2001
- await program.parseAsync(['node', 'opencli', 'browser', 'get', 'html', '--as', 'yaml']);
1979
+ await program.parseAsync(['node', 'opencli', 'browser', '--session', 'test', 'get', 'html', '--as', 'yaml']);
2002
1980
  expect(lastJsonLog().error.code).toBe('invalid_format');
2003
1981
  expect(process.exitCode).toBeDefined();
2004
1982
  });
@@ -2031,6 +2009,7 @@ function installSelectorFirstTestHarness(label, pageOverrides) {
2031
2009
  setActivePage: vi.fn(),
2032
2010
  getActivePage: vi.fn().mockReturnValue('tab-1'),
2033
2011
  tabs: vi.fn().mockResolvedValue([{ page: 'tab-1', active: true }]),
2012
+ session: 'test',
2034
2013
  ...pageOverrides(),
2035
2014
  };
2036
2015
  });
@@ -2052,7 +2031,7 @@ describe('browser find command', () => {
2052
2031
  ],
2053
2032
  });
2054
2033
  const program = createProgram('', '');
2055
- await program.parseAsync(['node', 'opencli', 'browser', 'find', '--css', '.btn']);
2034
+ await program.parseAsync(['node', 'opencli', 'browser', '--session', 'test', 'find', '--css', '.btn']);
2056
2035
  const out = lastJsonLog();
2057
2036
  expect(out.matches_n).toBe(2);
2058
2037
  expect(out.entries).toHaveLength(2);
@@ -2068,7 +2047,7 @@ describe('browser find command', () => {
2068
2047
  ],
2069
2048
  });
2070
2049
  const program = createProgram('', '');
2071
- 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']);
2072
2051
  const js = browserState.page.evaluate.mock.calls[0][0];
2073
2052
  expect(js).toContain('CRITERIA');
2074
2053
  expect(js).toContain('function accessibleName');
@@ -2082,7 +2061,7 @@ describe('browser find command', () => {
2082
2061
  it('forwards --limit / --text-max into the generated JS', async () => {
2083
2062
  browserState.page.evaluate.mockResolvedValueOnce({ matches_n: 0, entries: [] });
2084
2063
  const program = createProgram('', '');
2085
- 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']);
2086
2065
  const js = browserState.page.evaluate.mock.calls[0][0];
2087
2066
  expect(js).toContain('LIMIT = 3');
2088
2067
  expect(js).toContain('TEXT_MAX = 20');
@@ -2092,7 +2071,7 @@ describe('browser find command', () => {
2092
2071
  error: { code: 'invalid_selector', message: 'Invalid CSS selector: ">>>"', hint: 'Check the selector syntax.' },
2093
2072
  });
2094
2073
  const program = createProgram('', '');
2095
- await program.parseAsync(['node', 'opencli', 'browser', 'find', '--css', '>>>']);
2074
+ await program.parseAsync(['node', 'opencli', 'browser', '--session', 'test', 'find', '--css', '>>>']);
2096
2075
  expect(lastJsonLog().error.code).toBe('invalid_selector');
2097
2076
  expect(process.exitCode).toBeDefined();
2098
2077
  });
@@ -2101,20 +2080,20 @@ describe('browser find command', () => {
2101
2080
  error: { code: 'selector_not_found', message: 'CSS selector ".missing" matched 0 elements', hint: 'Use browser state to inspect the page.' },
2102
2081
  });
2103
2082
  const program = createProgram('', '');
2104
- await program.parseAsync(['node', 'opencli', 'browser', 'find', '--css', '.missing']);
2083
+ await program.parseAsync(['node', 'opencli', 'browser', '--session', 'test', 'find', '--css', '.missing']);
2105
2084
  expect(lastJsonLog().error.code).toBe('selector_not_found');
2106
2085
  expect(process.exitCode).toBeDefined();
2107
2086
  });
2108
2087
  it('rejects missing --css with usage_error (no evaluate call)', async () => {
2109
2088
  const program = createProgram('', '');
2110
- await program.parseAsync(['node', 'opencli', 'browser', 'find']);
2089
+ await program.parseAsync(['node', 'opencli', 'browser', '--session', 'test', 'find']);
2111
2090
  expect(lastJsonLog().error.code).toBe('usage_error');
2112
2091
  expect(browserState.page.evaluate).not.toHaveBeenCalled();
2113
2092
  expect(process.exitCode).toBeDefined();
2114
2093
  });
2115
2094
  it('rejects malformed --limit with usage_error (no evaluate call)', async () => {
2116
2095
  const program = createProgram('', '');
2117
- 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']);
2118
2097
  expect(lastJsonLog().error.code).toBe('usage_error');
2119
2098
  expect(browserState.page.evaluate).not.toHaveBeenCalled();
2120
2099
  expect(process.exitCode).toBeDefined();
@@ -2131,7 +2110,7 @@ describe('browser get text/value/attributes commands', () => {
2131
2110
  // 2nd call: getTextResolvedJs -> the element's text
2132
2111
  evalMock.mockResolvedValueOnce('Hello world');
2133
2112
  const program = createProgram('', '');
2134
- await program.parseAsync(['node', 'opencli', 'browser', 'get', 'text', '7']);
2113
+ await program.parseAsync(['node', 'opencli', 'browser', '--session', 'test', 'get', 'text', '7']);
2135
2114
  expect(lastJsonLog()).toEqual({ value: 'Hello world', matches_n: 1, match_level: 'exact' });
2136
2115
  });
2137
2116
  it('resolves a semantic locator to a ref before get text', async () => {
@@ -2145,7 +2124,7 @@ describe('browser get text/value/attributes commands', () => {
2145
2124
  evalMock.mockResolvedValueOnce({ ok: true, matches_n: 1, match_level: 'exact' });
2146
2125
  evalMock.mockResolvedValueOnce('Save');
2147
2126
  const program = createProgram('', '');
2148
- 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']);
2149
2128
  expect(evalMock.mock.calls[0][0]).toContain('function accessibleName');
2150
2129
  expect(evalMock.mock.calls[1][0]).toContain('const ref = "12"');
2151
2130
  expect(lastJsonLog()).toEqual({ value: 'Save', matches_n: 1, match_level: 'exact' });
@@ -2163,7 +2142,7 @@ describe('browser get text/value/attributes commands', () => {
2163
2142
  evalMock.mockResolvedValueOnce({ ok: true, matches_n: 1, match_level: 'exact' });
2164
2143
  evalMock.mockResolvedValueOnce('Save');
2165
2144
  const program = createProgram('', '');
2166
- 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']);
2167
2146
  expect(evalMock.mock.calls[0][0]).toContain('const LIMIT = 6');
2168
2147
  expect(evalMock.mock.calls[1][0]).toContain('const ref = "12"');
2169
2148
  expect(lastJsonLog()).toEqual({ value: 'Save', matches_n: 1, match_level: 'exact', total_matches: 3 });
@@ -2173,7 +2152,7 @@ describe('browser get text/value/attributes commands', () => {
2173
2152
  evalMock.mockResolvedValueOnce({ ok: true, matches_n: 3, match_level: 'exact' });
2174
2153
  evalMock.mockResolvedValueOnce('first');
2175
2154
  const program = createProgram('', '');
2176
- await program.parseAsync(['node', 'opencli', 'browser', 'get', 'text', '.btn']);
2155
+ await program.parseAsync(['node', 'opencli', 'browser', '--session', 'test', 'get', 'text', '.btn']);
2177
2156
  expect(lastJsonLog()).toEqual({ value: 'first', matches_n: 3, match_level: 'exact' });
2178
2157
  });
2179
2158
  it('parses the attributes payload back into a real object', async () => {
@@ -2182,7 +2161,7 @@ describe('browser get text/value/attributes commands', () => {
2182
2161
  // getAttributesResolvedJs returns a JSON-encoded string — the CLI must parse it
2183
2162
  evalMock.mockResolvedValueOnce(JSON.stringify({ id: 'nav', class: 'hero' }));
2184
2163
  const program = createProgram('', '');
2185
- await program.parseAsync(['node', 'opencli', 'browser', 'get', 'attributes', '#nav']);
2164
+ await program.parseAsync(['node', 'opencli', 'browser', '--session', 'test', 'get', 'attributes', '#nav']);
2186
2165
  const out = lastJsonLog();
2187
2166
  expect(out.matches_n).toBe(1);
2188
2167
  expect(out.match_level).toBe('exact');
@@ -2196,7 +2175,7 @@ describe('browser get text/value/attributes commands', () => {
2196
2175
  hint: 'Try a less specific selector.',
2197
2176
  });
2198
2177
  const program = createProgram('', '');
2199
- await program.parseAsync(['node', 'opencli', 'browser', 'get', 'text', '.missing']);
2178
+ await program.parseAsync(['node', 'opencli', 'browser', '--session', 'test', 'get', 'text', '.missing']);
2200
2179
  expect(lastJsonLog().error.code).toBe('selector_not_found');
2201
2180
  expect(process.exitCode).toBeDefined();
2202
2181
  });
@@ -2205,7 +2184,7 @@ describe('browser get text/value/attributes commands', () => {
2205
2184
  evalMock.mockResolvedValueOnce({ ok: true, matches_n: 4, match_level: 'exact' });
2206
2185
  evalMock.mockResolvedValueOnce('second');
2207
2186
  const program = createProgram('', '');
2208
- 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']);
2209
2188
  const resolveJs = evalMock.mock.calls[0][0];
2210
2189
  // resolveTargetJs embeds nth as a raw number literal; look for the binding
2211
2190
  expect(resolveJs).toContain('const nth = 1');
@@ -2213,7 +2192,7 @@ describe('browser get text/value/attributes commands', () => {
2213
2192
  });
2214
2193
  it('rejects malformed --nth with usage_error before touching the page', async () => {
2215
2194
  const program = createProgram('', '');
2216
- 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']);
2217
2196
  expect(lastJsonLog().error.code).toBe('usage_error');
2218
2197
  expect(browserState.page.evaluate).not.toHaveBeenCalled();
2219
2198
  expect(process.exitCode).toBeDefined();
@@ -2261,7 +2240,7 @@ describe('browser click/type commands', () => {
2261
2240
  it('emits {clicked, target, matches_n, match_level} on success', async () => {
2262
2241
  browserState.page.click.mockResolvedValueOnce({ matches_n: 1, match_level: 'exact' });
2263
2242
  const program = createProgram('', '');
2264
- await program.parseAsync(['node', 'opencli', 'browser', 'click', '#save']);
2243
+ await program.parseAsync(['node', 'opencli', 'browser', '--session', 'test', 'click', '#save']);
2265
2244
  expect(browserState.page.click).toHaveBeenCalledWith('#save', {});
2266
2245
  expect(lastJsonLog()).toEqual({ clicked: true, target: '#save', matches_n: 1, match_level: 'exact' });
2267
2246
  });
@@ -2274,7 +2253,7 @@ describe('browser click/type commands', () => {
2274
2253
  });
2275
2254
  browserState.page.click.mockResolvedValueOnce({ matches_n: 1, match_level: 'exact' });
2276
2255
  const program = createProgram('', '');
2277
- 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']);
2278
2257
  expect(browserState.page.click).toHaveBeenCalledWith('23', {});
2279
2258
  expect(lastJsonLog()).toEqual({ clicked: true, target: '23', matches_n: 1, match_level: 'exact' });
2280
2259
  });
@@ -2287,7 +2266,7 @@ describe('browser click/type commands', () => {
2287
2266
  ],
2288
2267
  });
2289
2268
  const program = createProgram('', '');
2290
- 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']);
2291
2270
  const err = lastJsonLog().error;
2292
2271
  expect(err.code).toBe('semantic_ambiguous');
2293
2272
  expect(err.matches_n).toBe(2);
@@ -2302,7 +2281,7 @@ describe('browser click/type commands', () => {
2302
2281
  ],
2303
2282
  });
2304
2283
  const program = createProgram('', '');
2305
- 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']);
2306
2285
  expect(browserState.page.hover).toHaveBeenCalledWith('31', {});
2307
2286
  expect(lastJsonLog()).toEqual({ hovered: true, target: '31', matches_n: 1, match_level: 'exact' });
2308
2287
  });
@@ -2315,7 +2294,7 @@ describe('browser click/type commands', () => {
2315
2294
  });
2316
2295
  browserState.page.setChecked.mockResolvedValueOnce({ checked: true, changed: false, matches_n: 1, match_level: 'exact', kind: 'checkbox' });
2317
2296
  const program = createProgram('', '');
2318
- 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']);
2319
2298
  expect(browserState.page.setChecked).toHaveBeenCalledWith('32', true, {});
2320
2299
  expect(lastJsonLog()).toEqual({ checked: true, changed: false, target: '32', matches_n: 1, match_level: 'exact', kind: 'checkbox' });
2321
2300
  });
@@ -2339,7 +2318,7 @@ describe('browser click/type commands', () => {
2339
2318
  multiple: false,
2340
2319
  });
2341
2320
  const program = createProgram('', '');
2342
- 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]);
2343
2322
  expect(browserState.page.uploadFiles).toHaveBeenCalledWith('33', [file], {});
2344
2323
  expect(lastJsonLog()).toMatchObject({ uploaded: true, target: '33', files: 1 });
2345
2324
  });
@@ -2355,7 +2334,7 @@ describe('browser click/type commands', () => {
2355
2334
  browserState.page.click.mockResolvedValueOnce({ matches_n: 1, match_level: 'exact' });
2356
2335
  browserState.page.typeText.mockResolvedValueOnce({ matches_n: 1, match_level: 'exact' });
2357
2336
  const program = createProgram('', '');
2358
- 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']);
2359
2338
  expect(browserState.page.click).toHaveBeenCalledWith('34', {});
2360
2339
  expect(browserState.page.typeText).toHaveBeenCalledWith('34', 'me@example.com', {});
2361
2340
  expect(lastJsonLog()).toMatchObject({ typed: true, target: '34', text: 'me@example.com' });
@@ -2377,7 +2356,7 @@ describe('browser click/type commands', () => {
2377
2356
  match_level: 'exact',
2378
2357
  });
2379
2358
  const program = createProgram('', '');
2380
- 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']);
2381
2360
  expect(browserState.page.fillText).toHaveBeenCalledWith('35', 'me@example.com', {});
2382
2361
  expect(lastJsonLog()).toMatchObject({ filled: true, verified: true, target: '35', text: 'me@example.com' });
2383
2362
  });
@@ -2409,6 +2388,8 @@ describe('browser click/type commands', () => {
2409
2388
  'node',
2410
2389
  'opencli',
2411
2390
  'browser',
2391
+ '--session',
2392
+ 'test',
2412
2393
  'drag',
2413
2394
  '--from-role',
2414
2395
  'button',
@@ -2425,13 +2406,13 @@ describe('browser click/type commands', () => {
2425
2406
  it('surfaces match_level=stable when resolver falls back to fingerprint match', async () => {
2426
2407
  browserState.page.click.mockResolvedValueOnce({ matches_n: 1, match_level: 'stable' });
2427
2408
  const program = createProgram('', '');
2428
- await program.parseAsync(['node', 'opencli', 'browser', 'click', '7']);
2409
+ await program.parseAsync(['node', 'opencli', 'browser', '--session', 'test', 'click', '7']);
2429
2410
  expect(lastJsonLog()).toEqual({ clicked: true, target: '7', matches_n: 1, match_level: 'stable' });
2430
2411
  });
2431
2412
  it('forwards --nth as ResolveOptions.nth to page.click', async () => {
2432
2413
  browserState.page.click.mockResolvedValueOnce({ matches_n: 3, match_level: 'exact' });
2433
2414
  const program = createProgram('', '');
2434
- await program.parseAsync(['node', 'opencli', 'browser', 'click', '.btn', '--nth', '2']);
2415
+ await program.parseAsync(['node', 'opencli', 'browser', '--session', 'test', 'click', '.btn', '--nth', '2']);
2435
2416
  expect(browserState.page.click).toHaveBeenCalledWith('.btn', { nth: 2 });
2436
2417
  expect(lastJsonLog()).toEqual({ clicked: true, target: '.btn', matches_n: 3, match_level: 'exact' });
2437
2418
  });
@@ -2443,7 +2424,7 @@ describe('browser click/type commands', () => {
2443
2424
  matches_n: 3,
2444
2425
  }));
2445
2426
  const program = createProgram('', '');
2446
- await program.parseAsync(['node', 'opencli', 'browser', 'click', '.btn']);
2427
+ await program.parseAsync(['node', 'opencli', 'browser', '--session', 'test', 'click', '.btn']);
2447
2428
  const err = lastJsonLog().error;
2448
2429
  expect(err.code).toBe('selector_ambiguous');
2449
2430
  expect(err.matches_n).toBe(3);
@@ -2457,13 +2438,13 @@ describe('browser click/type commands', () => {
2457
2438
  matches_n: 3,
2458
2439
  }));
2459
2440
  const program = createProgram('', '');
2460
- await program.parseAsync(['node', 'opencli', 'browser', 'click', '.btn', '--nth', '99']);
2441
+ await program.parseAsync(['node', 'opencli', 'browser', '--session', 'test', 'click', '.btn', '--nth', '99']);
2461
2442
  expect(lastJsonLog().error.code).toBe('selector_nth_out_of_range');
2462
2443
  expect(process.exitCode).toBeDefined();
2463
2444
  });
2464
2445
  it('rejects malformed --nth on click with usage_error before touching the page', async () => {
2465
2446
  const program = createProgram('', '');
2466
- await program.parseAsync(['node', 'opencli', 'browser', 'click', '.btn', '--nth', 'abc']);
2447
+ await program.parseAsync(['node', 'opencli', 'browser', '--session', 'test', 'click', '.btn', '--nth', 'abc']);
2467
2448
  expect(lastJsonLog().error.code).toBe('usage_error');
2468
2449
  expect(browserState.page.click).not.toHaveBeenCalled();
2469
2450
  expect(process.exitCode).toBeDefined();
@@ -2471,21 +2452,21 @@ describe('browser click/type commands', () => {
2471
2452
  it('hover: delegates to page.hover and emits a structured envelope', async () => {
2472
2453
  browserState.page.hover.mockResolvedValueOnce({ matches_n: 2, match_level: 'exact' });
2473
2454
  const program = createProgram('', '');
2474
- await program.parseAsync(['node', 'opencli', 'browser', 'hover', '.menu', '--nth', '1']);
2455
+ await program.parseAsync(['node', 'opencli', 'browser', '--session', 'test', 'hover', '.menu', '--nth', '1']);
2475
2456
  expect(browserState.page.hover).toHaveBeenCalledWith('.menu', { nth: 1 });
2476
2457
  expect(lastJsonLog()).toEqual({ hovered: true, target: '.menu', matches_n: 2, match_level: 'exact' });
2477
2458
  });
2478
2459
  it('focus: delegates to page.focus and reports whether the element took focus', async () => {
2479
2460
  browserState.page.focus.mockResolvedValueOnce({ focused: true, matches_n: 1, match_level: 'stable' });
2480
2461
  const program = createProgram('', '');
2481
- await program.parseAsync(['node', 'opencli', 'browser', 'focus', '7']);
2462
+ await program.parseAsync(['node', 'opencli', 'browser', '--session', 'test', 'focus', '7']);
2482
2463
  expect(browserState.page.focus).toHaveBeenCalledWith('7', {});
2483
2464
  expect(lastJsonLog()).toEqual({ focused: true, target: '7', matches_n: 1, match_level: 'stable' });
2484
2465
  });
2485
2466
  it('dblclick: delegates to page.dblClick and emits a structured envelope', async () => {
2486
2467
  browserState.page.dblClick.mockResolvedValueOnce({ matches_n: 1, match_level: 'exact' });
2487
2468
  const program = createProgram('', '');
2488
- await program.parseAsync(['node', 'opencli', 'browser', 'dblclick', '#row']);
2469
+ await program.parseAsync(['node', 'opencli', 'browser', '--session', 'test', 'dblclick', '#row']);
2489
2470
  expect(browserState.page.dblClick).toHaveBeenCalledWith('#row', {});
2490
2471
  expect(lastJsonLog()).toEqual({ dblclicked: true, target: '#row', matches_n: 1, match_level: 'exact' });
2491
2472
  });
@@ -2498,7 +2479,7 @@ describe('browser click/type commands', () => {
2498
2479
  kind: 'checkbox',
2499
2480
  });
2500
2481
  const program = createProgram('', '');
2501
- await program.parseAsync(['node', 'opencli', 'browser', 'check', '.todo', '--nth', '1']);
2482
+ await program.parseAsync(['node', 'opencli', 'browser', '--session', 'test', 'check', '.todo', '--nth', '1']);
2502
2483
  expect(browserState.page.setChecked).toHaveBeenCalledWith('.todo', true, { nth: 1 });
2503
2484
  expect(lastJsonLog()).toEqual({ checked: true, changed: true, target: '.todo', matches_n: 2, match_level: 'exact', kind: 'checkbox' });
2504
2485
  });
@@ -2511,7 +2492,7 @@ describe('browser click/type commands', () => {
2511
2492
  kind: 'checkbox',
2512
2493
  });
2513
2494
  const program = createProgram('', '');
2514
- await program.parseAsync(['node', 'opencli', 'browser', 'uncheck', '#subscribe']);
2495
+ await program.parseAsync(['node', 'opencli', 'browser', '--session', 'test', 'uncheck', '#subscribe']);
2515
2496
  expect(browserState.page.setChecked).toHaveBeenCalledWith('#subscribe', false, {});
2516
2497
  expect(lastJsonLog()).toEqual({ checked: false, changed: false, target: '#subscribe', matches_n: 1, match_level: 'stable', kind: 'checkbox' });
2517
2498
  });
@@ -2529,7 +2510,7 @@ describe('browser click/type commands', () => {
2529
2510
  multiple: false,
2530
2511
  });
2531
2512
  const program = createProgram('', '');
2532
- await program.parseAsync(['node', 'opencli', 'browser', 'upload', '#file', file]);
2513
+ await program.parseAsync(['node', 'opencli', 'browser', '--session', 'test', 'upload', '#file', file]);
2533
2514
  expect(browserState.page.uploadFiles).toHaveBeenCalledWith('#file', [file], {});
2534
2515
  expect(lastJsonLog()).toEqual({
2535
2516
  uploaded: true,
@@ -2543,7 +2524,7 @@ describe('browser click/type commands', () => {
2543
2524
  });
2544
2525
  it('upload: rejects missing files before touching the page', async () => {
2545
2526
  const program = createProgram('', '');
2546
- 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']);
2547
2528
  expect(lastJsonLog().error.code).toBe('file_not_found');
2548
2529
  expect(browserState.page.uploadFiles).not.toHaveBeenCalled();
2549
2530
  expect(process.exitCode).toBeDefined();
@@ -2559,7 +2540,7 @@ describe('browser click/type commands', () => {
2559
2540
  target_match_level: 'stable',
2560
2541
  });
2561
2542
  const program = createProgram('', '');
2562
- 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']);
2563
2544
  expect(browserState.page.drag).toHaveBeenCalledWith('.card', '.lane', { from: { nth: 2 }, to: { nth: 1 } });
2564
2545
  expect(lastJsonLog()).toEqual({
2565
2546
  dragged: true,
@@ -2573,7 +2554,7 @@ describe('browser click/type commands', () => {
2573
2554
  });
2574
2555
  it('drag: rejects malformed --from-nth before touching the page', async () => {
2575
2556
  const program = createProgram('', '');
2576
- 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']);
2577
2558
  expect(lastJsonLog().error.code).toBe('usage_error');
2578
2559
  expect(browserState.page.drag).not.toHaveBeenCalled();
2579
2560
  expect(process.exitCode).toBeDefined();
@@ -2583,7 +2564,7 @@ describe('browser click/type commands', () => {
2583
2564
  browserState.page.typeText.mockResolvedValueOnce({ matches_n: 1, match_level: 'exact' });
2584
2565
  browserState.page.evaluate.mockResolvedValueOnce(false); // isAutocomplete
2585
2566
  const program = createProgram('', '');
2586
- await program.parseAsync(['node', 'opencli', 'browser', 'type', '#q', 'hello']);
2567
+ await program.parseAsync(['node', 'opencli', 'browser', '--session', 'test', 'type', '#q', 'hello']);
2587
2568
  expect(browserState.page.click).toHaveBeenCalledWith('#q', {});
2588
2569
  expect(browserState.page.wait).toHaveBeenCalledWith(0.3);
2589
2570
  expect(browserState.page.typeText).toHaveBeenCalledWith('#q', 'hello', {});
@@ -2596,7 +2577,7 @@ describe('browser click/type commands', () => {
2596
2577
  browserState.page.typeText.mockResolvedValueOnce({ matches_n: 1, match_level: 'exact' });
2597
2578
  browserState.page.evaluate.mockResolvedValueOnce(true);
2598
2579
  const program = createProgram('', '');
2599
- await program.parseAsync(['node', 'opencli', 'browser', 'type', '#q', 'hi']);
2580
+ await program.parseAsync(['node', 'opencli', 'browser', '--session', 'test', 'type', '#q', 'hi']);
2600
2581
  const waitCalls = browserState.page.wait.mock.calls;
2601
2582
  expect(waitCalls).toContainEqual([0.3]);
2602
2583
  expect(waitCalls).toContainEqual([0.4]);
@@ -2608,7 +2589,7 @@ describe('browser click/type commands', () => {
2608
2589
  browserState.page.typeText.mockResolvedValueOnce({ matches_n: 1, match_level: 'reidentified' });
2609
2590
  browserState.page.evaluate.mockResolvedValueOnce(false);
2610
2591
  const program = createProgram('', '');
2611
- await program.parseAsync(['node', 'opencli', 'browser', 'type', '9', 'hi']);
2592
+ await program.parseAsync(['node', 'opencli', 'browser', '--session', 'test', 'type', '9', 'hi']);
2612
2593
  // The typeText call is the authoritative match_level source for the `type` envelope.
2613
2594
  expect(lastJsonLog().match_level).toBe('reidentified');
2614
2595
  });
@@ -2616,7 +2597,7 @@ describe('browser click/type commands', () => {
2616
2597
  browserState.page.click.mockResolvedValueOnce({ matches_n: 5, match_level: 'exact' });
2617
2598
  browserState.page.typeText.mockResolvedValueOnce({ matches_n: 5, match_level: 'exact' });
2618
2599
  const program = createProgram('', '');
2619
- 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']);
2620
2601
  expect(browserState.page.click).toHaveBeenCalledWith('.field', { nth: 3 });
2621
2602
  expect(browserState.page.typeText).toHaveBeenCalledWith('.field', 'x', { nth: 3 });
2622
2603
  });
@@ -2632,7 +2613,7 @@ describe('browser click/type commands', () => {
2632
2613
  mode: 'textarea',
2633
2614
  });
2634
2615
  const program = createProgram('', '');
2635
- 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']);
2636
2617
  expect(browserState.page.fillText).toHaveBeenCalledWith('#msg', 'line1\\n/ / raw', {});
2637
2618
  expect(lastJsonLog()).toEqual({
2638
2619
  filled: true,
@@ -2658,7 +2639,7 @@ describe('browser click/type commands', () => {
2658
2639
  match_level: 'exact',
2659
2640
  });
2660
2641
  const program = createProgram('', '');
2661
- await program.parseAsync(['node', 'opencli', 'browser', 'fill', '#msg', 'expected']);
2642
+ await program.parseAsync(['node', 'opencli', 'browser', '--session', 'test', 'fill', '#msg', 'expected']);
2662
2643
  expect(lastJsonLog()).toEqual({
2663
2644
  filled: true,
2664
2645
  verified: false,
@@ -2673,7 +2654,7 @@ describe('browser click/type commands', () => {
2673
2654
  });
2674
2655
  it('fill: forwards --nth to page.fillText', async () => {
2675
2656
  const program = createProgram('', '');
2676
- 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']);
2677
2658
  expect(browserState.page.fillText).toHaveBeenCalledWith('.field', 'x', { nth: 2 });
2678
2659
  });
2679
2660
  });
@@ -2686,7 +2667,7 @@ describe('browser select command', () => {
2686
2667
  evalMock.mockResolvedValueOnce({ ok: true, matches_n: 1, match_level: 'exact' });
2687
2668
  evalMock.mockResolvedValueOnce({ selected: 'US' });
2688
2669
  const program = createProgram('', '');
2689
- await program.parseAsync(['node', 'opencli', 'browser', 'select', '#country', 'US']);
2670
+ await program.parseAsync(['node', 'opencli', 'browser', '--session', 'test', 'select', '#country', 'US']);
2690
2671
  expect(lastJsonLog()).toEqual({ selected: 'US', target: '#country', matches_n: 1, match_level: 'exact' });
2691
2672
  });
2692
2673
  it('maps "Not a <select>" to a not_a_select error envelope', async () => {
@@ -2694,7 +2675,7 @@ describe('browser select command', () => {
2694
2675
  evalMock.mockResolvedValueOnce({ ok: true, matches_n: 1, match_level: 'exact' });
2695
2676
  evalMock.mockResolvedValueOnce({ error: 'Not a <select>' });
2696
2677
  const program = createProgram('', '');
2697
- await program.parseAsync(['node', 'opencli', 'browser', 'select', '#not-select', 'US']);
2678
+ await program.parseAsync(['node', 'opencli', 'browser', '--session', 'test', 'select', '#not-select', 'US']);
2698
2679
  const err = lastJsonLog().error;
2699
2680
  expect(err.code).toBe('not_a_select');
2700
2681
  expect(err.matches_n).toBe(1);
@@ -2705,7 +2686,7 @@ describe('browser select command', () => {
2705
2686
  evalMock.mockResolvedValueOnce({ ok: true, matches_n: 1, match_level: 'exact' });
2706
2687
  evalMock.mockResolvedValueOnce({ error: 'Option "XX" not found', available: ['US', 'CA'] });
2707
2688
  const program = createProgram('', '');
2708
- await program.parseAsync(['node', 'opencli', 'browser', 'select', '#country', 'XX']);
2689
+ await program.parseAsync(['node', 'opencli', 'browser', '--session', 'test', 'select', '#country', 'XX']);
2709
2690
  const err = lastJsonLog().error;
2710
2691
  expect(err.code).toBe('option_not_found');
2711
2692
  expect(err.available).toEqual(['US', 'CA']);
@@ -2723,7 +2704,7 @@ describe('browser select command', () => {
2723
2704
  .mockResolvedValueOnce({ ok: true, matches_n: 1, match_level: 'exact' })
2724
2705
  .mockResolvedValueOnce({ selected: 'Uruguay' });
2725
2706
  const program = createProgram('', '');
2726
- await program.parseAsync(['node', 'opencli', 'browser', 'select', '--label', 'Country', 'Uruguay']);
2707
+ await program.parseAsync(['node', 'opencli', 'browser', '--session', 'test', 'select', '--label', 'Country', 'Uruguay']);
2727
2708
  expect(lastJsonLog()).toEqual({ selected: 'Uruguay', target: '36', matches_n: 1, match_level: 'exact' });
2728
2709
  });
2729
2710
  it('surfaces selector_ambiguous from the resolver before calling selectResolvedJs', async () => {
@@ -2735,7 +2716,7 @@ describe('browser select command', () => {
2735
2716
  matches_n: 2,
2736
2717
  });
2737
2718
  const program = createProgram('', '');
2738
- await program.parseAsync(['node', 'opencli', 'browser', 'select', '.dropdown', 'US']);
2719
+ await program.parseAsync(['node', 'opencli', 'browser', '--session', 'test', 'select', '.dropdown', 'US']);
2739
2720
  expect(lastJsonLog().error.code).toBe('selector_ambiguous');
2740
2721
  // The select payload JS must not fire when resolution fails
2741
2722
  expect(browserState.page.evaluate.mock.calls).toHaveLength(1);