@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.
- package/README.md +3 -0
- package/cli-manifest.json +955 -96
- package/clis/_shared/site-auth.js +0 -1
- package/clis/_shared/site-auth.test.js +0 -1
- package/clis/amazon-in/auth.js +44 -0
- package/clis/amazon-in/checkout-status.js +54 -0
- package/clis/amazon-in/checkout.js +478 -0
- package/clis/amazon-in/parsers.js +252 -0
- package/clis/amazon-in/parsers.test.js +230 -0
- package/clis/amazon-in/product.js +72 -0
- package/clis/amazon-in/search.js +76 -0
- package/clis/amazon-in/shared.js +40 -0
- package/clis/amazon-in/wishlist.js +98 -0
- package/clis/blinkit/checkout.js +0 -1
- package/clis/blinkit/place-order.js +0 -1
- package/clis/chatgpt/model.js +2 -2
- package/clis/chatgpt/model.test.js +7 -1
- package/clis/chatgpt/utils.js +8 -0
- package/clis/chatgpt/utils.test.js +92 -1
- package/clis/district/checkout.js +0 -1
- package/clis/district/seats.js +0 -1
- package/clis/district/set-location.js +0 -1
- package/clis/district/showtimes.js +0 -1
- package/clis/google/images.js +456 -0
- package/clis/google/images.test.js +375 -0
- package/clis/instagram/user.js +5 -13
- package/clis/instagram/user.test.js +66 -0
- package/clis/mercury/check-login.js +0 -1
- package/clis/mercury/reimbursement-draft.js +0 -1
- package/clis/practo/book-confirm.js +0 -1
- package/clis/practo/cancel.js +0 -1
- package/clis/trip/attraction.js +74 -0
- package/clis/trip/car.js +74 -0
- package/clis/trip/deals.js +61 -0
- package/clis/trip/flight-round.js +88 -0
- package/clis/trip/flight.js +83 -0
- package/clis/trip/hotel-search.js +80 -0
- package/clis/trip/hotel.js +54 -0
- package/clis/trip/package.js +93 -0
- package/clis/trip/search.js +43 -0
- package/clis/trip/tour.js +84 -0
- package/clis/trip/train.js +76 -0
- package/clis/trip/transfer.js +82 -0
- package/clis/trip/trip.test.js +1420 -0
- package/clis/trip/utils.js +911 -0
- package/dist/src/build-manifest.js +0 -1
- package/dist/src/build-manifest.test.js +4 -0
- package/dist/src/cli.js +7 -7
- package/dist/src/cli.test.js +26 -10
- package/dist/src/command-presentation.js +1 -1
- package/dist/src/command-presentation.test.js +3 -0
- package/dist/src/command-surface.js +1 -1
- package/dist/src/command-surface.test.js +4 -0
- package/dist/src/commands/auth.js +0 -2
- package/dist/src/commands/auth.test.js +0 -2
- package/dist/src/discovery.js +0 -1
- package/dist/src/execution.js +3 -3
- package/dist/src/execution.test.js +68 -15
- package/dist/src/hosted/browser-args.js +2 -2
- package/dist/src/hosted/browser-args.test.js +10 -0
- package/dist/src/hosted/client.d.ts +8 -10
- package/dist/src/hosted/client.js +26 -34
- package/dist/src/hosted/client.test.js +79 -24
- package/dist/src/hosted/runner.js +7 -35
- package/dist/src/hosted/runner.test.js +27 -51
- package/dist/src/hosted/types.d.ts +1 -5
- package/dist/src/manifest-types.d.ts +0 -2
- package/dist/src/registry.d.ts +0 -2
- package/dist/src/registry.js +0 -1
- package/dist/src/root-command-surface.js +10 -2
- package/hosted-contract.json +2627 -1535
- package/package.json +2 -2
- package/skills/webcmd-browser/SKILL.md +2 -1
- package/skills/webcmd-usage/SKILL.md +1 -1
|
@@ -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(
|
|
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
|
|
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
|
|
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
|
|
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
|
|
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
|
|
979
|
+
const windowMode = getBrowserWindowMode(command);
|
|
980
980
|
try {
|
|
981
981
|
const { BrowserBridge } = await import('./browser/index.js');
|
|
982
982
|
const bridge = new BrowserBridge();
|
package/dist/src/cli.test.js
CHANGED
|
@@ -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: '
|
|
1168
|
-
expect(mockBindTab).toHaveBeenCalledWith('test', { page: 'tab-2', windowMode: '
|
|
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
|
|
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('
|
|
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: '
|
|
1217
|
+
expect(mockBrowserConnect).toHaveBeenCalledWith({ timeout: 45, session: 'test', surface: 'browser', windowMode: 'background' });
|
|
1216
1218
|
expect(browserState.page?.snapshot).toHaveBeenCalled();
|
|
1217
1219
|
});
|
|
1218
|
-
it('
|
|
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', '
|
|
1221
|
-
expect(mockBrowserConnect).toHaveBeenCalledWith({ timeout: 45, session: 'test', surface: 'browser', windowMode: '
|
|
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: '
|
|
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',
|
package/dist/src/discovery.js
CHANGED
|
@@ -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
|
};
|
package/dist/src/execution.js
CHANGED
|
@@ -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(
|
|
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(
|
|
493
|
+
function resolveBrowserWindowMode(rawOption) {
|
|
494
494
|
return normalizeWindowMode('--window', rawOption)
|
|
495
495
|
?? normalizeWindowMode('WEBCMD_WINDOW', process.env.WEBCMD_WINDOW)
|
|
496
|
-
??
|
|
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('
|
|
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
|
-
|
|
869
|
-
|
|
870
|
-
|
|
871
|
-
|
|
872
|
-
|
|
873
|
-
|
|
874
|
-
|
|
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
|
|
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-
|
|
888
|
-
description: 'test
|
|
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
|
-
|
|
895
|
-
|
|
896
|
-
|
|
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
|
|
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,
|
|
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(
|
|
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(
|
|
33
|
-
const
|
|
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', '
|
|
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.
|
|
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', '
|
|
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', '
|
|
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
|
}
|