@jackwener/opencli 1.7.16 → 1.7.18

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