@agentrhq/webcmd 0.4.1 → 0.4.3

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 (74) hide show
  1. package/README.md +3 -0
  2. package/cli-manifest.json +955 -96
  3. package/clis/_shared/site-auth.js +0 -1
  4. package/clis/_shared/site-auth.test.js +0 -1
  5. package/clis/amazon-in/auth.js +44 -0
  6. package/clis/amazon-in/checkout-status.js +54 -0
  7. package/clis/amazon-in/checkout.js +478 -0
  8. package/clis/amazon-in/parsers.js +252 -0
  9. package/clis/amazon-in/parsers.test.js +230 -0
  10. package/clis/amazon-in/product.js +72 -0
  11. package/clis/amazon-in/search.js +76 -0
  12. package/clis/amazon-in/shared.js +40 -0
  13. package/clis/amazon-in/wishlist.js +98 -0
  14. package/clis/blinkit/checkout.js +0 -1
  15. package/clis/blinkit/place-order.js +0 -1
  16. package/clis/chatgpt/model.js +2 -2
  17. package/clis/chatgpt/model.test.js +7 -1
  18. package/clis/chatgpt/utils.js +8 -0
  19. package/clis/chatgpt/utils.test.js +92 -1
  20. package/clis/district/checkout.js +0 -1
  21. package/clis/district/seats.js +0 -1
  22. package/clis/district/set-location.js +0 -1
  23. package/clis/district/showtimes.js +0 -1
  24. package/clis/google/images.js +456 -0
  25. package/clis/google/images.test.js +375 -0
  26. package/clis/instagram/user.js +5 -13
  27. package/clis/instagram/user.test.js +66 -0
  28. package/clis/mercury/check-login.js +0 -1
  29. package/clis/mercury/reimbursement-draft.js +0 -1
  30. package/clis/practo/book-confirm.js +0 -1
  31. package/clis/practo/cancel.js +0 -1
  32. package/clis/trip/attraction.js +74 -0
  33. package/clis/trip/car.js +74 -0
  34. package/clis/trip/deals.js +61 -0
  35. package/clis/trip/flight-round.js +88 -0
  36. package/clis/trip/flight.js +83 -0
  37. package/clis/trip/hotel-search.js +80 -0
  38. package/clis/trip/hotel.js +54 -0
  39. package/clis/trip/package.js +93 -0
  40. package/clis/trip/search.js +43 -0
  41. package/clis/trip/tour.js +84 -0
  42. package/clis/trip/train.js +76 -0
  43. package/clis/trip/transfer.js +82 -0
  44. package/clis/trip/trip.test.js +1420 -0
  45. package/clis/trip/utils.js +911 -0
  46. package/dist/src/build-manifest.js +0 -1
  47. package/dist/src/build-manifest.test.js +4 -0
  48. package/dist/src/cli.js +7 -7
  49. package/dist/src/cli.test.js +26 -10
  50. package/dist/src/command-presentation.js +1 -1
  51. package/dist/src/command-presentation.test.js +3 -0
  52. package/dist/src/command-surface.js +1 -1
  53. package/dist/src/command-surface.test.js +4 -0
  54. package/dist/src/commands/auth.js +0 -2
  55. package/dist/src/commands/auth.test.js +0 -2
  56. package/dist/src/discovery.js +0 -1
  57. package/dist/src/execution.js +3 -3
  58. package/dist/src/execution.test.js +68 -15
  59. package/dist/src/hosted/browser-args.js +2 -2
  60. package/dist/src/hosted/browser-args.test.js +10 -0
  61. package/dist/src/hosted/client.d.ts +8 -10
  62. package/dist/src/hosted/client.js +26 -34
  63. package/dist/src/hosted/client.test.js +79 -24
  64. package/dist/src/hosted/runner.js +7 -35
  65. package/dist/src/hosted/runner.test.js +27 -51
  66. package/dist/src/hosted/types.d.ts +1 -5
  67. package/dist/src/manifest-types.d.ts +0 -2
  68. package/dist/src/registry.d.ts +0 -2
  69. package/dist/src/registry.js +0 -1
  70. package/dist/src/root-command-surface.js +10 -2
  71. package/hosted-contract.json +2627 -1535
  72. package/package.json +2 -2
  73. package/skills/webcmd-browser/SKILL.md +2 -1
  74. package/skills/webcmd-usage/SKILL.md +1 -1
@@ -107,7 +107,6 @@ function toManifestEntry(cmd, modulePath, sourceFile) {
107
107
  navigateBefore: cmd.navigateBefore,
108
108
  siteSession: cmd.siteSession,
109
109
  freshPage: cmd.freshPage,
110
- defaultWindowMode: cmd.defaultWindowMode,
111
110
  };
112
111
  }
113
112
  /**
@@ -428,6 +428,10 @@ describe('manifest helper rules', () => {
428
428
  // Unknown flags are ignored.
429
429
  expect(parseBuildManifestArgs(['--something-else']).allowRemovals).toBe(0);
430
430
  });
431
+ it('does not publish per-command browser window defaults', () => {
432
+ const manifest = JSON.parse(fs.readFileSync(path.join(process.cwd(), 'cli-manifest.json'), 'utf8'));
433
+ expect(manifest.filter((entry) => Object.hasOwn(entry, 'defaultWindowMode'))).toEqual([]);
434
+ });
431
435
  it('keeps every browser login on the local handoff contract', () => {
432
436
  const manifest = JSON.parse(fs.readFileSync(path.join(process.cwd(), 'cli-manifest.json'), 'utf8'));
433
437
  const logins = manifest.filter((entry) => entry.browser === true && entry.name === 'login');
package/dist/src/cli.js CHANGED
@@ -526,7 +526,7 @@ async function getBrowserPage(session, targetPage, profileSelection, opts = {})
526
526
  surface: 'browser',
527
527
  ...profileRouteParams(profileSelection),
528
528
  ...(idleTimeout && idleTimeout > 0 && { idleTimeout }),
529
- windowMode: opts.windowMode ?? getBrowserWindowMode(undefined, 'foreground'),
529
+ windowMode: opts.windowMode ?? getBrowserWindowMode(),
530
530
  });
531
531
  const targetScope = getBrowserScope(session, profileSelection?.contextId);
532
532
  const resolvedTargetPage = targetPage
@@ -540,7 +540,7 @@ async function getBrowserPage(session, targetPage, profileSelection, opts = {})
540
540
  }
541
541
  return page;
542
542
  }
543
- function getBrowserWindowMode(command, defaultMode) {
543
+ function getBrowserWindowMode(command) {
544
544
  const optionRaw = getCommandOption(command, 'window');
545
545
  if (optionRaw !== undefined && optionRaw !== '') {
546
546
  if (optionRaw === 'foreground' || optionRaw === 'background')
@@ -553,7 +553,7 @@ function getBrowserWindowMode(command, defaultMode) {
553
553
  return envRaw;
554
554
  throw new Error(`WEBCMD_WINDOW must be one of: foreground, background. Received: "${envRaw}"`);
555
555
  }
556
- return defaultMode;
556
+ return 'background';
557
557
  }
558
558
  function addBrowserTabOption(command) {
559
559
  return command.option('--tab <targetId>', BROWSER_TAB_OPTION_DESCRIPTION);
@@ -824,7 +824,7 @@ export function createProgram(BUILTIN_CLIS, USER_CLIS) {
824
824
  // program.parseAsync callers (tests). User-facing surface is the <session>
825
825
  // positional; main.ts argv preprocessor rewrites positional -> --session.
826
826
  .addOption(new Option('--session <name>', 'Internal — set automatically from the <session> positional').hideHelp())
827
- .option('--window <mode>', 'Browser window mode: foreground or background')
827
+ .option('--window <mode>', 'Browser window mode: foreground or background (default: background)')
828
828
  .description('Browser control — navigate, click, type, extract, wait (no LLM needed)')
829
829
  .usage('<session> <command> [options]')
830
830
  .addHelpText('after', `
@@ -832,7 +832,7 @@ export function createProgram(BUILTIN_CLIS, USER_CLIS) {
832
832
 
833
833
  Examples:
834
834
  $ webcmd browser work open https://x.com
835
- $ webcmd browser work open https://x.com --window background
835
+ $ webcmd browser work open https://x.com --window foreground
836
836
  $ webcmd browser work click 12
837
837
  $ webcmd browser work state
838
838
  $ webcmd browser work tab list
@@ -923,7 +923,7 @@ Examples:
923
923
  const targetPage = getBrowserTargetId(command);
924
924
  const session = getBrowserSession(command);
925
925
  const profileSelection = getBrowserProfileSelection(command);
926
- const windowMode = getBrowserWindowMode(command, 'foreground');
926
+ const windowMode = getBrowserWindowMode(command);
927
927
  page = await getBrowserPage(session, targetPage, profileSelection, { windowMode });
928
928
  await fn(page, ...args);
929
929
  }
@@ -976,7 +976,7 @@ Examples:
976
976
  const profileSelection = getBrowserProfileSelection(command);
977
977
  const contextId = profileSelection?.contextId;
978
978
  const routing = profileRouteParams(profileSelection);
979
- const windowMode = getBrowserWindowMode(command, 'foreground');
979
+ const windowMode = getBrowserWindowMode(command);
980
980
  try {
981
981
  const { BrowserBridge } = await import('./browser/index.js');
982
982
  const bridge = new BrowserBridge();
@@ -554,6 +554,7 @@ describe('createProgram root help descriptions', () => {
554
554
  expect.objectContaining({
555
555
  name: 'window',
556
556
  flags: '--window <mode>',
557
+ help: 'Browser window mode: foreground or background (default: background)',
557
558
  takes_value: 'required',
558
559
  }),
559
560
  ]));
@@ -1164,21 +1165,22 @@ describe('browser tab targeting commands', () => {
1164
1165
  it('binds an existing Cloak tab by page id into a browser session', async () => {
1165
1166
  const program = createProgram('', '');
1166
1167
  await program.parseAsync(['node', 'webcmd', 'browser', '--session', 'test', 'bind', '--page', 'tab-2']);
1167
- expect(mockBrowserConnect).toHaveBeenCalledWith({ timeout: 45, session: 'test', surface: 'browser', windowMode: 'foreground' });
1168
- expect(mockBindTab).toHaveBeenCalledWith('test', { page: 'tab-2', windowMode: 'foreground' });
1168
+ expect(mockBrowserConnect).toHaveBeenCalledWith({ timeout: 45, session: 'test', surface: 'browser', windowMode: 'background' });
1169
+ expect(mockBindTab).toHaveBeenCalledWith('test', { page: 'tab-2', windowMode: 'background' });
1169
1170
  const out = lastJsonLog();
1170
1171
  expect(out.session).toBe('test');
1171
1172
  expect(out.url).toBe('https://user.example/inbox');
1172
1173
  });
1173
- it('binds an existing Cloak tab by index into a browser session', async () => {
1174
+ it('binds an existing Cloak tab in the foreground when explicitly requested', async () => {
1174
1175
  const program = createProgram('', '');
1175
- await program.parseAsync(['node', 'webcmd', 'browser', '--session', 'test', 'bind', '--index', '1']);
1176
+ await program.parseAsync(['node', 'webcmd', 'browser', '--session', 'test', '--window', 'foreground', 'bind', '--index', '1']);
1176
1177
  expect(mockBrowserConnect).toHaveBeenCalledWith({ timeout: 45, session: 'test', surface: 'browser', windowMode: 'foreground' });
1177
1178
  expect(mockBindTab).toHaveBeenCalledWith('test', { index: 1, windowMode: 'foreground' });
1178
1179
  const out = lastJsonLog();
1179
1180
  expect(out.session).toBe('test');
1180
1181
  });
1181
- it('passes background window mode when binding a Cloak tab', async () => {
1182
+ it('uses the direct-browser CLI window mode before WEBCMD_WINDOW', async () => {
1183
+ process.env.WEBCMD_WINDOW = 'foreground';
1182
1184
  const program = createProgram('', '');
1183
1185
  await program.parseAsync(['node', 'webcmd', 'browser', '--session', 'test', '--window', 'background', 'bind', '--page', 'tab-2']);
1184
1186
  expect(mockBrowserConnect).toHaveBeenCalledWith({ timeout: 45, session: 'test', surface: 'browser', windowMode: 'background' });
@@ -1212,15 +1214,29 @@ describe('browser tab targeting commands', () => {
1212
1214
  it('runs browser commands against an explicit session', async () => {
1213
1215
  const program = createProgram('', '');
1214
1216
  await program.parseAsync(['node', 'webcmd', 'browser', '--session', 'test', 'state']);
1215
- expect(mockBrowserConnect).toHaveBeenCalledWith({ timeout: 45, session: 'test', surface: 'browser', windowMode: 'foreground' });
1217
+ expect(mockBrowserConnect).toHaveBeenCalledWith({ timeout: 45, session: 'test', surface: 'browser', windowMode: 'background' });
1216
1218
  expect(browserState.page?.snapshot).toHaveBeenCalled();
1217
1219
  });
1218
- it('passes browser --window through Commander options without relying on env pre-processing', async () => {
1220
+ it('uses WEBCMD_WINDOW as an explicit direct-browser override', async () => {
1221
+ process.env.WEBCMD_WINDOW = 'foreground';
1219
1222
  const program = createProgram('', '');
1220
- await program.parseAsync(['node', 'webcmd', 'browser', '--session', 'test', '--window', 'background', 'state']);
1221
- expect(mockBrowserConnect).toHaveBeenCalledWith({ timeout: 45, session: 'test', surface: 'browser', windowMode: 'background' });
1223
+ await program.parseAsync(['node', 'webcmd', 'browser', '--session', 'test', 'state']);
1224
+ expect(mockBrowserConnect).toHaveBeenCalledWith({ timeout: 45, session: 'test', surface: 'browser', windowMode: 'foreground' });
1222
1225
  expect(browserState.page?.snapshot).toHaveBeenCalled();
1223
1226
  });
1227
+ it('rejects an invalid direct-browser --window value', async () => {
1228
+ const program = createProgram('', '');
1229
+ await program.parseAsync(['node', 'webcmd', 'browser', '--session', 'test', '--window', 'sideways', 'state']);
1230
+ expect(stderrSpy.mock.calls.flat().join('')).toContain('--window must be one of: foreground, background. Received: "sideways"');
1231
+ expect(process.exitCode).toBeDefined();
1232
+ });
1233
+ it('rejects an invalid direct-browser WEBCMD_WINDOW value', async () => {
1234
+ process.env.WEBCMD_WINDOW = 'sideways';
1235
+ const program = createProgram('', '');
1236
+ await program.parseAsync(['node', 'webcmd', 'browser', '--session', 'test', 'state']);
1237
+ expect(stderrSpy.mock.calls.flat().join('')).toContain('WEBCMD_WINDOW must be one of: foreground, background. Received: "sideways"');
1238
+ expect(process.exitCode).toBeDefined();
1239
+ });
1224
1240
  it('passes the opt-in AX source to browser state', async () => {
1225
1241
  const program = createProgram('', '');
1226
1242
  await program.parseAsync(['node', 'webcmd', 'browser', '--session', 'test', 'state', '--source', 'ax']);
@@ -1298,7 +1314,7 @@ describe('browser tab targeting commands', () => {
1298
1314
  it('unbinds a session through the daemon close-window command', async () => {
1299
1315
  const program = createProgram('', '');
1300
1316
  await program.parseAsync(['node', 'webcmd', 'browser', '--session', 'test', 'unbind']);
1301
- expect(mockBrowserConnect).toHaveBeenCalledWith({ timeout: 45, session: 'test', surface: 'browser', windowMode: 'foreground' });
1317
+ expect(mockBrowserConnect).toHaveBeenCalledWith({ timeout: 45, session: 'test', surface: 'browser', windowMode: 'background' });
1302
1318
  expect(mockSendCommand).toHaveBeenCalledWith('close-window', { session: 'test', surface: 'browser' });
1303
1319
  const out = lastJsonLog();
1304
1320
  expect(out).toEqual({ unbound: true, session: 'test' });
@@ -30,7 +30,7 @@ const BROWSER_COMMON_OPTIONS = [
30
30
  {
31
31
  flags: '--window <mode>',
32
32
  name: 'window',
33
- help: 'Browser window mode: foreground or background',
33
+ help: 'Browser window mode: foreground or background (default: background)',
34
34
  choices: ['foreground', 'background'],
35
35
  },
36
36
  {
@@ -54,6 +54,9 @@ describe('shared command presentation', () => {
54
54
  expect(formatSiteHelp('github', [hosted])).toBe(formatSiteHelp('github', [local]));
55
55
  expect(formatCommandHelp(hosted)).toBe(formatCommandHelp(local));
56
56
  });
57
+ it('states the background default in adapter browser help', () => {
58
+ expect(formatCommandHelp(toPresentableCommand(localCommand))).toContain('Browser window mode: foreground or background (default: background)');
59
+ });
57
60
  it('builds byte-identical structured and display list rows', () => {
58
61
  const local = toPresentableCommand(localCommand);
59
62
  const hosted = toPresentableCommand(hostedCommand);
@@ -46,7 +46,7 @@ export function configureCommandSurface(command, metadata) {
46
46
  .option('-v, --verbose', 'Debug output', false);
47
47
  if (metadata.browser) {
48
48
  command
49
- .option('--window <mode>', `Browser window mode: ${BROWSER_WINDOW_MODES.join(' or ')}`)
49
+ .option('--window <mode>', `Browser window mode: ${BROWSER_WINDOW_MODES.join(' or ')} (default: background)`)
50
50
  .option('--site-session <mode>', `Adapter site session lifecycle: ${SITE_SESSION_MODES.join(' or ')}`)
51
51
  .option('--keep-tab <bool>', 'Keep the browser tab lease after the command finishes');
52
52
  }
@@ -143,6 +143,10 @@ describe('configureCommandSurface', () => {
143
143
  const longFlags = command.options.map((option) => option.long);
144
144
  for (const flag of expected)
145
145
  expect(longFlags).toContain(flag);
146
+ if (browser) {
147
+ expect(command.options.find((option) => option.long === '--window')?.description)
148
+ .toBe('Browser window mode: foreground or background (default: background)');
149
+ }
146
150
  if (!browser) {
147
151
  expect(longFlags).not.toContain('--window');
148
152
  expect(longFlags).not.toContain('--site-session');
@@ -96,7 +96,6 @@ function quickCheckCommand(cmd, timeoutSeconds) {
96
96
  func: cmd.authStatus.quickCheck,
97
97
  navigateBefore: false,
98
98
  siteSession: 'ephemeral',
99
- defaultWindowMode: 'background',
100
99
  }, timeoutSeconds);
101
100
  }
102
101
  function normalizeQuickResult(result) {
@@ -179,7 +178,6 @@ function refreshCommand(cmd, timeoutSeconds) {
179
178
  func: refreshFunc,
180
179
  navigateBefore: false,
181
180
  siteSession: 'persistent',
182
- defaultWindowMode: 'background',
183
181
  }, timeoutSeconds);
184
182
  }
185
183
  function normalizeRefreshStatus(result) {
@@ -54,7 +54,6 @@ describe('auth status collection', () => {
54
54
  name: 'whoami',
55
55
  navigateBefore: false,
56
56
  siteSession: 'ephemeral',
57
- defaultWindowMode: 'background',
58
57
  });
59
58
  });
60
59
  it('marks sites without quickCheck as unknown unless --full is used', async () => {
@@ -116,7 +115,6 @@ describe('auth refresh collection', () => {
116
115
  name: 'whoami',
117
116
  navigateBefore: false,
118
117
  siteSession: 'persistent',
119
- defaultWindowMode: 'background',
120
118
  });
121
119
  expect(executeCommandMock.mock.calls[0]?.[3]).toMatchObject({
122
120
  siteSession: 'persistent',
@@ -212,7 +212,6 @@ async function loadFromManifest(manifestPath, clisDir) {
212
212
  navigateBefore: entry.navigateBefore,
213
213
  siteSession: entry.siteSession,
214
214
  freshPage: entry.freshPage,
215
- defaultWindowMode: entry.defaultWindowMode,
216
215
  _lazy: true,
217
216
  _modulePath: modulePath,
218
217
  };
@@ -183,7 +183,7 @@ export async function executeCommand(cmd, rawKwargs, debug = false, opts = {}) {
183
183
  const siteSession = resolveSiteSession(cmd, opts.siteSession);
184
184
  const session = resolveAdapterBrowserSession(cmd, siteSession);
185
185
  const keepTab = resolveKeepTab(siteSession, opts.keepTab);
186
- const windowMode = resolveBrowserWindowMode(cmd.defaultWindowMode ?? 'background', opts.windowMode);
186
+ const windowMode = resolveBrowserWindowMode(opts.windowMode);
187
187
  const surface = 'adapter';
188
188
  const canonicalCommand = fullName(cmd);
189
189
  const leaseEligible = surface === 'adapter'
@@ -490,10 +490,10 @@ function normalizeWindowMode(name, raw) {
490
490
  return raw;
491
491
  throw new ArgumentError(`${name} must be one of: foreground, background. Received: "${String(raw)}"`);
492
492
  }
493
- function resolveBrowserWindowMode(defaultMode = 'background', rawOption) {
493
+ function resolveBrowserWindowMode(rawOption) {
494
494
  return normalizeWindowMode('--window', rawOption)
495
495
  ?? normalizeWindowMode('WEBCMD_WINDOW', process.env.WEBCMD_WINDOW)
496
- ?? defaultMode;
496
+ ?? 'background';
497
497
  }
498
498
  /**
499
499
  * Resolve the user-controllable `--timeout` arg, in seconds.
@@ -848,7 +848,8 @@ describe('executeCommand — non-browser timeout', () => {
848
848
  vi.restoreAllMocks();
849
849
  }
850
850
  });
851
- it('lets browser common options override adapter window and keep-tab defaults', async () => {
851
+ it('uses explicit adapter options before WEBCMD_WINDOW', async () => {
852
+ vi.stubEnv('WEBCMD_WINDOW', 'background');
852
853
  const closeWindow = vi.fn().mockResolvedValue(undefined);
853
854
  const mockPage = { closeWindow };
854
855
  const sessionOpts = [];
@@ -865,15 +866,21 @@ describe('executeCommand — non-browser timeout', () => {
865
866
  strategy: Strategy.PUBLIC,
866
867
  func: async () => [{ ok: true }],
867
868
  });
868
- await executeCommand(cmd, {}, false, {
869
- windowMode: 'foreground',
870
- keepTab: 'true',
871
- });
872
- expect(sessionOpts[0]).toMatchObject({ windowMode: 'foreground' });
873
- expect(closeWindow).not.toHaveBeenCalled();
874
- vi.restoreAllMocks();
869
+ try {
870
+ await executeCommand(cmd, {}, false, {
871
+ windowMode: 'foreground',
872
+ keepTab: 'true',
873
+ });
874
+ expect(sessionOpts[0]).toMatchObject({ windowMode: 'foreground' });
875
+ expect(closeWindow).not.toHaveBeenCalled();
876
+ }
877
+ finally {
878
+ vi.unstubAllEnvs();
879
+ vi.restoreAllMocks();
880
+ }
875
881
  });
876
- it('uses command defaultWindowMode when the user does not pass --window', async () => {
882
+ it('uses WEBCMD_WINDOW as an explicit adapter override', async () => {
883
+ vi.stubEnv('WEBCMD_WINDOW', 'foreground');
877
884
  const closeWindow = vi.fn().mockResolvedValue(undefined);
878
885
  const mockPage = { closeWindow };
879
886
  const sessionOpts = [];
@@ -884,16 +891,62 @@ describe('executeCommand — non-browser timeout', () => {
884
891
  });
885
892
  const cmd = cli({
886
893
  site: 'test-execution',
887
- name: 'browser-default-window-mode', access: 'write',
888
- description: 'test command default window mode',
894
+ name: 'browser-env-window-mode', access: 'read',
895
+ description: 'test browser environment window mode',
889
896
  browser: true,
890
897
  strategy: Strategy.PUBLIC,
891
- defaultWindowMode: 'foreground',
892
898
  func: async () => [{ ok: true }],
893
899
  });
894
- await executeCommand(cmd, {});
895
- expect(sessionOpts[0]).toMatchObject({ windowMode: 'foreground' });
896
- vi.restoreAllMocks();
900
+ try {
901
+ await executeCommand(cmd, {});
902
+ expect(sessionOpts[0]).toMatchObject({ windowMode: 'foreground' });
903
+ }
904
+ finally {
905
+ vi.unstubAllEnvs();
906
+ vi.restoreAllMocks();
907
+ }
908
+ });
909
+ it('rejects an invalid adapter --window value', async () => {
910
+ const mockPage = { closeWindow: vi.fn().mockResolvedValue(undefined) };
911
+ vi.spyOn(capRouting, 'shouldUseBrowserSession').mockReturnValue(true);
912
+ vi.spyOn(runtime, 'browserSession').mockImplementation(async (_Factory, fn) => fn(mockPage));
913
+ const cmd = cli({
914
+ site: 'test-execution',
915
+ name: 'browser-invalid-window-mode', access: 'read',
916
+ description: 'test invalid browser window mode',
917
+ browser: true,
918
+ strategy: Strategy.PUBLIC,
919
+ func: async () => [{ ok: true }],
920
+ });
921
+ try {
922
+ await expect(executeCommand(cmd, {}, false, {
923
+ windowMode: 'sideways',
924
+ })).rejects.toThrow('--window must be one of: foreground, background. Received: "sideways"');
925
+ }
926
+ finally {
927
+ vi.restoreAllMocks();
928
+ }
929
+ });
930
+ it('rejects an invalid adapter WEBCMD_WINDOW value', async () => {
931
+ vi.stubEnv('WEBCMD_WINDOW', 'sideways');
932
+ const mockPage = { closeWindow: vi.fn().mockResolvedValue(undefined) };
933
+ vi.spyOn(capRouting, 'shouldUseBrowserSession').mockReturnValue(true);
934
+ vi.spyOn(runtime, 'browserSession').mockImplementation(async (_Factory, fn) => fn(mockPage));
935
+ const cmd = cli({
936
+ site: 'test-execution',
937
+ name: 'browser-invalid-env-window-mode', access: 'read',
938
+ description: 'test invalid browser environment window mode',
939
+ browser: true,
940
+ strategy: Strategy.PUBLIC,
941
+ func: async () => [{ ok: true }],
942
+ });
943
+ try {
944
+ await expect(executeCommand(cmd, {})).rejects.toThrow('WEBCMD_WINDOW must be one of: foreground, background. Received: "sideways"');
945
+ }
946
+ finally {
947
+ vi.unstubAllEnvs();
948
+ vi.restoreAllMocks();
949
+ }
897
950
  });
898
951
  it('does not re-run custom validation when args are already prepared', async () => {
899
952
  const validateArgs = vi.fn();
@@ -21,7 +21,7 @@ export function parseHostedBrowserStructure(argv) {
21
21
  const browser = root
22
22
  .command('browser')
23
23
  .addOption(new Option('--session <name>', 'Internal — set automatically from the <session> positional').hideHelp())
24
- .option('--window <mode>', 'Browser window mode: foreground or background')
24
+ .option('--window <mode>', 'Browser window mode: foreground or background (default: background)')
25
25
  .description('Browser control — navigate, click, type, extract, wait (no LLM needed)')
26
26
  .usage('<session> <command> [options]')
27
27
  .addHelpText('after', `
@@ -29,7 +29,7 @@ export function parseHostedBrowserStructure(argv) {
29
29
 
30
30
  Examples:
31
31
  $ webcmd browser work open https://x.com
32
- $ webcmd browser work open https://x.com --window background
32
+ $ webcmd browser work open https://x.com --window foreground
33
33
  $ webcmd browser work click 12
34
34
  $ webcmd browser work state
35
35
  $ webcmd browser work tab list
@@ -67,6 +67,16 @@ function parseHosted(argv) {
67
67
  return parseHostedBrowserStructure(rewriteBrowserArgv(argv));
68
68
  }
69
69
  describe('hosted browser canonical Commander values', () => {
70
+ it('states the background default in hosted browser help', () => {
71
+ try {
72
+ parseHostedBrowserStructure(['browser', '--help']);
73
+ throw new Error('Expected hosted browser help');
74
+ }
75
+ catch (error) {
76
+ expect(error).toBeInstanceOf(HostedBrowserHelp);
77
+ expect(error.output).toContain('Browser window mode: foreground or background (default: background)');
78
+ }
79
+ });
70
80
  it('matches the actual local action boundary for every catalogued browser leaf', () => {
71
81
  for (const contract of browserCommandCatalog) {
72
82
  const argv = ['browser', 'work', ...contract.command.split('/'), ...allPositionals(contract.command)];
@@ -1,10 +1,15 @@
1
1
  import { CliError, type ExitCode } from '../errors.js';
2
- import type { HostedBrowserActionRequest, HostedBrowserActionResponse, HostedBrowserFinishRequest, HostedBrowserFinishResponse, HostedBrowserRunActionInput, HostedBrowserRunActionResponse, HostedBrowserRunRequest, HostedBrowserRunResponse, HostedExecution, HostedExecuteResponse, HostedPrepareExecutionResponse, HostedProfileResponse, HostedProfilesResponse, HostedUploadArtifactResponse, HostedManifest, HostedTraceReceipt } from './types.js';
2
+ import type { HostedBrowserActionRequest, HostedBrowserActionResponse, HostedBrowserFinishRequest, HostedBrowserFinishResponse, HostedBrowserRunActionInput, HostedBrowserRunActionResponse, HostedBrowserRunRequest, HostedBrowserRunResponse, HostedExecution, HostedExecuteResponse, HostedPrepareExecutionResponse, HostedProfilesResponse, HostedUploadArtifactResponse, HostedManifest, HostedTraceReceipt } from './types.js';
3
3
  export interface HostedClientOptions {
4
4
  apiBaseUrl: string;
5
5
  apiKey: string;
6
+ workspace?: string;
6
7
  fetchImpl?: typeof fetch;
7
8
  }
9
+ /**
10
+ * Resolves the active workspace from CLI flags/env, precedence: --workspace flag > WEBCMD_WORKSPACE env > undefined.
11
+ */
12
+ export declare function resolveWorkspace(argv: readonly string[], env: NodeJS.ProcessEnv): string | undefined;
8
13
  export declare class HostedClientError extends CliError {
9
14
  readonly execution?: HostedExecution;
10
15
  readonly trace?: HostedTraceReceipt;
@@ -16,19 +21,12 @@ export declare class HostedClientError extends CliError {
16
21
  export declare class HostedClient {
17
22
  private readonly apiBaseUrl;
18
23
  private readonly apiKey;
24
+ private readonly workspace;
19
25
  private readonly fetchImpl;
20
26
  constructor(options: HostedClientOptions);
21
27
  getMe(): Promise<unknown>;
22
28
  getManifest(): Promise<HostedManifest>;
23
- listProfiles(filters?: {
24
- name?: string;
25
- userId?: string;
26
- }): Promise<HostedProfilesResponse>;
27
- createProfile(input: {
28
- name: string;
29
- userId?: string;
30
- }): Promise<HostedProfileResponse>;
31
- getProfile(profileId: string): Promise<HostedProfileResponse>;
29
+ listProfiles(): Promise<HostedProfilesResponse>;
32
30
  deleteProfile(profileId: string): Promise<{
33
31
  ok: true;
34
32
  deleted: true;
@@ -1,4 +1,20 @@
1
1
  import { attachTraceReceipt, CliError, EXIT_CODES } from '../errors.js';
2
+ /**
3
+ * Resolves the active workspace from CLI flags/env, precedence: --workspace flag > WEBCMD_WORKSPACE env > undefined.
4
+ */
5
+ export function resolveWorkspace(argv, env) {
6
+ const idx = argv.indexOf('--workspace');
7
+ if (idx >= 0 && argv[idx + 1])
8
+ return argv[idx + 1];
9
+ const equalsForm = argv.find(arg => arg.startsWith('--workspace='));
10
+ if (equalsForm !== undefined) {
11
+ const value = equalsForm.slice('--workspace='.length).trim();
12
+ if (value)
13
+ return value;
14
+ }
15
+ const fromEnv = env.WEBCMD_WORKSPACE?.trim();
16
+ return fromEnv ? fromEnv : undefined;
17
+ }
2
18
  export class HostedClientError extends CliError {
3
19
  execution;
4
20
  trace;
@@ -13,10 +29,12 @@ export class HostedClientError extends CliError {
13
29
  export class HostedClient {
14
30
  apiBaseUrl;
15
31
  apiKey;
32
+ workspace;
16
33
  fetchImpl;
17
34
  constructor(options) {
18
35
  this.apiBaseUrl = options.apiBaseUrl.replace(/\/+$/, '');
19
36
  this.apiKey = options.apiKey;
37
+ this.workspace = options.workspace;
20
38
  this.fetchImpl = options.fetchImpl ?? fetch;
21
39
  }
22
40
  async getMe() {
@@ -29,36 +47,13 @@ export class HostedClient {
29
47
  }
30
48
  return body.manifest;
31
49
  }
32
- async listProfiles(filters = {}) {
33
- const params = new URLSearchParams();
34
- if (filters.name !== undefined)
35
- params.set('name', filters.name);
36
- if (filters.userId !== undefined)
37
- params.set('userId', filters.userId);
38
- const query = params.toString();
39
- const body = await this.request(`/v1/profiles${query ? `?${query}` : ''}`);
50
+ async listProfiles() {
51
+ const body = await this.request('/v1/profiles');
40
52
  if (!isHostedProfilesResponse(body)) {
41
53
  throw protocolError('Webcmd Cloud returned an invalid profiles response.');
42
54
  }
43
55
  return body;
44
56
  }
45
- async createProfile(input) {
46
- const body = await this.request('/v1/profiles', {
47
- method: 'POST',
48
- body: JSON.stringify(input),
49
- });
50
- if (!isHostedProfileResponse(body)) {
51
- throw protocolError('Webcmd Cloud returned an invalid profile response.');
52
- }
53
- return body;
54
- }
55
- async getProfile(profileId) {
56
- const body = await this.request(`/v1/profiles/${encodeURIComponent(profileId)}`);
57
- if (!isHostedProfileResponse(body)) {
58
- throw protocolError('Webcmd Cloud returned an invalid profile response.');
59
- }
60
- return body;
61
- }
62
57
  async deleteProfile(profileId) {
63
58
  const body = await this.request(`/v1/profiles/${encodeURIComponent(profileId)}`, { method: 'DELETE' });
64
59
  if (!hasExactKeys(body, ['ok', 'deleted']) || body.ok !== true || body.deleted !== true) {
@@ -124,6 +119,7 @@ export class HostedClient {
124
119
  headers: {
125
120
  accept: 'application/octet-stream',
126
121
  authorization: `Bearer ${this.apiKey}`,
122
+ ...(this.workspace ? { 'x-webcmd-workspace': this.workspace } : {}),
127
123
  },
128
124
  });
129
125
  if (!response.ok) {
@@ -189,6 +185,7 @@ export class HostedClient {
189
185
  accept: 'application/json',
190
186
  ...(init.body ? { 'content-type': 'application/json' } : {}),
191
187
  authorization: `Bearer ${this.apiKey}`,
188
+ ...(this.workspace ? { 'x-webcmd-workspace': this.workspace } : {}),
192
189
  ...(init.headers ?? {}),
193
190
  },
194
191
  });
@@ -311,19 +308,14 @@ function isHostedProfilesResponse(value) {
311
308
  && Array.isArray(value.profiles)
312
309
  && value.profiles.every(isHostedPublicProfile);
313
310
  }
314
- function isHostedProfileResponse(value) {
315
- return hasExactKeys(value, ['ok', 'profile'])
316
- && value.ok === true
317
- && isHostedPublicProfile(value.profile);
318
- }
319
311
  function isHostedPublicProfile(value) {
320
312
  return hasExactKeys(value, [
321
- 'id', 'name', 'userId', 'default', 'status',
313
+ 'id', 'name', 'workspace', 'default', 'status',
322
314
  'createdAt', 'updatedAt', 'lastUsedAt',
323
315
  ])
324
316
  && typeof value.id === 'string'
325
317
  && (value.name === null || typeof value.name === 'string')
326
- && (value.userId === null || typeof value.userId === 'string')
318
+ && (value.workspace === null || typeof value.workspace === 'string')
327
319
  && typeof value.default === 'boolean'
328
320
  && (value.status === 'pending' || value.status === 'available')
329
321
  && typeof value.createdAt === 'string'
@@ -334,7 +326,7 @@ function isHostedManifestCommand(value) {
334
326
  if (!hasOnlyKeys(value, [
335
327
  'site', 'name', 'aliases', 'command', 'description', 'access', 'example', 'domain', 'strategy', 'browser',
336
328
  'args', 'columns', 'pipeline', 'defaultFormat', 'type', 'modulePath', 'sourceFile', 'navigateBefore',
337
- 'siteSession', 'freshPage', 'defaultWindowMode', 'adapterPackageId', 'adapterPackageName', 'adapterPackageVersion',
329
+ 'siteSession', 'freshPage', 'adapterPackageId', 'adapterPackageName', 'adapterPackageVersion',
338
330
  ]))
339
331
  return false;
340
332
  if (typeof value.site !== 'string' || typeof value.name !== 'string' || typeof value.command !== 'string')
@@ -355,7 +347,7 @@ function isHostedManifestCommand(value) {
355
347
  return false;
356
348
  if (value.pipeline !== undefined && (!Array.isArray(value.pipeline) || !value.pipeline.every(isRecord)))
357
349
  return false;
358
- for (const key of ['type', 'modulePath', 'sourceFile', 'siteSession', 'defaultWindowMode', 'adapterPackageId', 'adapterPackageName', 'adapterPackageVersion']) {
350
+ for (const key of ['type', 'modulePath', 'sourceFile', 'siteSession', 'adapterPackageId', 'adapterPackageName', 'adapterPackageVersion']) {
359
351
  if (value[key] !== undefined && typeof value[key] !== 'string')
360
352
  return false;
361
353
  }