@agentrhq/webcmd 0.4.0 → 0.4.2

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.
@@ -10,7 +10,7 @@ import { createProgram } from '../cli.js';
10
10
  import { formatRootHelp } from '../command-presentation.js';
11
11
  import { HOSTED_ROOT_HELP } from '../completion-shared.js';
12
12
  import { PKG_VERSION } from '../version.js';
13
- import { makeHostedConfig } from './config.js';
13
+ import { makeHostedConfig, makeLocalConfig } from './config.js';
14
14
  import { runHostedCli } from './runner.js';
15
15
  const [packageMajor, packageMinor] = PKG_VERSION.split('.');
16
16
  const compatiblePatchVersion = `${packageMajor}.${packageMinor}.99`;
@@ -223,6 +223,107 @@ function captureLocalBrowserStructure(argv) {
223
223
  }
224
224
  }
225
225
  describe('runHostedCli', () => {
226
+ const publicProfile = {
227
+ id: 'profile_work',
228
+ name: 'Work',
229
+ workspace: 'ws_demo',
230
+ default: false,
231
+ status: 'available',
232
+ createdAt: '2026-07-24T00:00:00.000Z',
233
+ updatedAt: '2026-07-24T00:00:00.000Z',
234
+ lastUsedAt: '2026-07-24T00:00:00.000Z',
235
+ };
236
+ it('lists and deletes hosted profiles without fetching the manifest', async () => {
237
+ const requests = [];
238
+ const fetchImpl = vi.fn(async (url, init) => {
239
+ const request = {
240
+ url: String(url),
241
+ method: init?.method ?? 'GET',
242
+ ...(init?.body ? { body: JSON.parse(String(init.body)) } : {}),
243
+ };
244
+ requests.push(request);
245
+ if (request.method === 'DELETE') {
246
+ return new Response(JSON.stringify({ ok: true, deleted: true }));
247
+ }
248
+ return new Response(JSON.stringify({ ok: true, profiles: [publicProfile] }));
249
+ });
250
+ for (const argv of [
251
+ ['profile', 'list', '-f', 'json'],
252
+ ['profile', 'delete', 'profile_work', '-f', 'json'],
253
+ ]) {
254
+ const stdout = sink();
255
+ const stderr = sink();
256
+ const result = await runHostedCli(argv, {
257
+ config: makeHostedConfig({ apiBaseUrl: 'https://api.example.com', apiKey: 'key' }),
258
+ stdout: stdout.stream,
259
+ stderr: stderr.stream,
260
+ fetchImpl,
261
+ });
262
+ expect(result).toEqual({ handled: true, exitCode: 0 });
263
+ expect(stderr.text()).toBe('');
264
+ expect(stdout.text()).toContain(argv[1] === 'delete' ? '"deleted": true' : '"id": "profile_work"');
265
+ }
266
+ expect(requests).toEqual([
267
+ { url: 'https://api.example.com/v1/profiles', method: 'GET' },
268
+ { url: 'https://api.example.com/v1/profiles/profile_work', method: 'DELETE' },
269
+ ]);
270
+ expect(requests.some(request => request.url.endsWith('/v1/manifest'))).toBe(false);
271
+ });
272
+ it.each(['create', 'get'])('rejects the removed profile %s subcommand', async (command) => {
273
+ const stderr = sink();
274
+ const fetchImpl = vi.fn();
275
+ const result = await runHostedCli(['profile', command, 'Work'], {
276
+ config: makeHostedConfig({ apiBaseUrl: 'https://api.example.com', apiKey: 'key' }),
277
+ stderr: stderr.stream,
278
+ fetchImpl,
279
+ });
280
+ expect(result.handled).toBe(true);
281
+ expect(stderr.text()).toMatch(/unknown command|not supported/i);
282
+ expect(fetchImpl).not.toHaveBeenCalled();
283
+ });
284
+ it('threads the ambient workspace onto hosted profile requests', async () => {
285
+ const cases = [
286
+ { name: 'from WEBCMD_WORKSPACE env', argv: ['profile', 'list', '-f', 'json'], env: { WEBCMD_WORKSPACE: 'ws1' }, expected: 'ws1' },
287
+ { name: 'from --workspace flag', argv: ['--workspace', 'ws2', 'profile', 'list', '-f', 'json'], env: {}, expected: 'ws2' },
288
+ ];
289
+ for (const testCase of cases) {
290
+ let capturedHeader = null;
291
+ const fetchImpl = vi.fn(async (_url, init) => {
292
+ capturedHeader = new Headers(init?.headers).get('x-webcmd-workspace');
293
+ return new Response(JSON.stringify({ ok: true, profiles: [publicProfile] }));
294
+ });
295
+ const stdout = sink();
296
+ const stderr = sink();
297
+ const result = await runHostedCli(testCase.argv, {
298
+ config: makeHostedConfig({ apiBaseUrl: 'https://api.example.com', apiKey: 'key' }),
299
+ stdout: stdout.stream,
300
+ stderr: stderr.stream,
301
+ fetchImpl,
302
+ env: testCase.env,
303
+ });
304
+ expect(result).toEqual({ handled: true, exitCode: 0 });
305
+ expect(stderr.text()).toBe('');
306
+ expect(capturedHeader).toBe(testCase.expected);
307
+ }
308
+ });
309
+ it.each(['rename', 'use'])('rejects local-only profile %s in hosted mode without an API call', async (command) => {
310
+ const stderr = sink();
311
+ const fetchImpl = vi.fn();
312
+ const result = await runHostedCli(['profile', command, 'value'], {
313
+ config: makeHostedConfig({ apiBaseUrl: 'https://api.example.com', apiKey: 'key' }),
314
+ stderr: stderr.stream,
315
+ fetchImpl,
316
+ });
317
+ expect(result).toEqual({ handled: true, exitCode: 78 });
318
+ expect(stderr.text()).toContain(`webcmd profile ${command} is not available in hosted mode.`);
319
+ expect(fetchImpl).not.toHaveBeenCalled();
320
+ });
321
+ it.each(['list', 'rename', 'use'])('leaves profile %s to the existing local command surface', async (command) => {
322
+ const result = await runHostedCli(['profile', command, 'value'], {
323
+ config: makeLocalConfig(),
324
+ });
325
+ expect(result).toEqual({ handled: false, exitCode: 0 });
326
+ });
226
327
  it.each([
227
328
  ['missing-site'],
228
329
  ['missing-site', 'child'],
@@ -40,10 +40,13 @@ export interface HostedManifest {
40
40
  commands: HostedCommand[];
41
41
  }
42
42
  export interface HostedPublicProfile {
43
- name: string;
43
+ id: string;
44
+ name: string | null;
45
+ workspace: string | null;
44
46
  default: boolean;
45
- status: 'available';
47
+ status: 'pending' | 'available';
46
48
  createdAt: string;
49
+ updatedAt: string;
47
50
  lastUsedAt: string;
48
51
  }
49
52
  export interface HostedProfilesResponse {
@@ -35,6 +35,11 @@ export function parseHostedRootCommandSurface(argv) {
35
35
  let stderr = '';
36
36
  const boundary = findRootCommandBoundary(input);
37
37
  const root = configureRootCommandSurface(new Command('webcmd'))
38
+ // Hosted-only: registered here (not in the shared configureRootCommandSurface)
39
+ // so the local CLI surface is unaffected. Lets Commander's structural parse
40
+ // consume `--workspace <id>` before the site/command token instead of
41
+ // throwing an unknown-option error.
42
+ .option('--workspace <id>', 'Hosted workspace id/slug for the request')
38
43
  .exitOverride()
39
44
  .configureOutput({
40
45
  writeOut: value => { stdout += value; },
@@ -89,13 +94,16 @@ export function parseHostedRootCommandSurface(argv) {
89
94
  function findRootCommandBoundary(argv) {
90
95
  for (let index = 0; index < argv.length; index += 1) {
91
96
  const token = argv[index];
92
- if (token === '--profile') {
97
+ if (token === '--profile' || token === '--workspace') {
93
98
  // Commander requires and consumes the next token even when it is `--` or
94
99
  // starts with a dash. Structural failures have already been reported.
100
+ // `--workspace` is hosted-only (not a registered Commander option here)
101
+ // but must still be skipped as a value pair so it and its value are not
102
+ // mistaken for the site/command token and forwarded to the server.
95
103
  index += 1;
96
104
  continue;
97
105
  }
98
- if (token.startsWith('--profile='))
106
+ if (token.startsWith('--profile=') || token.startsWith('--workspace='))
99
107
  continue;
100
108
  if (token === '--')
101
109
  return { separatorIndex: index };
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "schemaVersion": 1,
3
- "webcmdVersion": "0.4.0",
3
+ "webcmdVersion": "0.4.2",
4
4
  "outputFormats": [
5
5
  "table",
6
6
  "plain",
@@ -69,6 +69,334 @@
69
69
  }
70
70
  ],
71
71
  "commands": [
72
+ {
73
+ "command": "amazon-in/checkout",
74
+ "site": "amazon-in",
75
+ "name": "checkout",
76
+ "description": "Prepare a guarded Amazon.in checkout with browser-only payment handoff",
77
+ "access": "write",
78
+ "strategy": "UI",
79
+ "browser": true,
80
+ "domain": "amazon.in",
81
+ "positionals": [
82
+ {
83
+ "name": "input",
84
+ "type": "string",
85
+ "description": "Amazon.in product URL or ASIN",
86
+ "positional": true,
87
+ "required": true,
88
+ "variadic": false
89
+ }
90
+ ],
91
+ "options": [
92
+ {
93
+ "name": "quantity",
94
+ "type": "int",
95
+ "description": "Quantity (1-10)",
96
+ "positional": false,
97
+ "required": false,
98
+ "variadic": false,
99
+ "default": 1
100
+ },
101
+ {
102
+ "name": "size",
103
+ "type": "string",
104
+ "description": "Exact visible size label",
105
+ "positional": false,
106
+ "required": false,
107
+ "variadic": false
108
+ },
109
+ {
110
+ "name": "colour",
111
+ "type": "string",
112
+ "description": "Exact visible colour label",
113
+ "positional": false,
114
+ "required": false,
115
+ "variadic": false
116
+ },
117
+ {
118
+ "name": "payment",
119
+ "type": "string",
120
+ "description": "Payment method; secrets remain browser-only",
121
+ "positional": false,
122
+ "required": true,
123
+ "variadic": false,
124
+ "choices": [
125
+ "upi",
126
+ "saved-card",
127
+ "new-card",
128
+ "cod"
129
+ ]
130
+ },
131
+ {
132
+ "name": "card-last4",
133
+ "type": "string",
134
+ "description": "Saved-card selector: exactly four digits",
135
+ "positional": false,
136
+ "required": false,
137
+ "variadic": false
138
+ },
139
+ {
140
+ "name": "place-order",
141
+ "type": "boolean",
142
+ "description": "Submit the final Amazon action once",
143
+ "positional": false,
144
+ "required": false,
145
+ "variadic": false,
146
+ "default": false
147
+ }
148
+ ],
149
+ "columns": [
150
+ "status",
151
+ "asin",
152
+ "title",
153
+ "size",
154
+ "colour",
155
+ "quantity",
156
+ "item_price",
157
+ "total",
158
+ "payment_method",
159
+ "delivery_date",
160
+ "action",
161
+ "verify_command"
162
+ ],
163
+ "aliases": [],
164
+ "defaultFormat": "table",
165
+ "fileArguments": [],
166
+ "sessionPolicy": "create-or-reuse",
167
+ "availability": {
168
+ "mode": "hosted"
169
+ }
170
+ },
171
+ {
172
+ "command": "amazon-in/checkout-status",
173
+ "site": "amazon-in",
174
+ "name": "checkout-status",
175
+ "description": "Read the current Amazon.in checkout or payment state without clicking",
176
+ "access": "read",
177
+ "strategy": "UI",
178
+ "browser": true,
179
+ "domain": "amazon.in",
180
+ "positionals": [],
181
+ "options": [],
182
+ "columns": [
183
+ "status",
184
+ "order_id",
185
+ "total",
186
+ "payment_method",
187
+ "action"
188
+ ],
189
+ "aliases": [],
190
+ "defaultFormat": "table",
191
+ "fileArguments": [],
192
+ "sessionPolicy": "create-or-reuse",
193
+ "availability": {
194
+ "mode": "hosted"
195
+ }
196
+ },
197
+ {
198
+ "command": "amazon-in/login",
199
+ "site": "amazon-in",
200
+ "name": "login",
201
+ "description": "Open amazon-in login",
202
+ "access": "write",
203
+ "strategy": "COOKIE",
204
+ "browser": true,
205
+ "domain": "amazon.in",
206
+ "positionals": [],
207
+ "options": [],
208
+ "columns": [
209
+ "status",
210
+ "logged_in",
211
+ "site",
212
+ "user_name",
213
+ "action",
214
+ "verify_command"
215
+ ],
216
+ "aliases": [],
217
+ "defaultFormat": "table",
218
+ "fileArguments": [],
219
+ "sessionPolicy": "create-or-reuse",
220
+ "availability": {
221
+ "mode": "hosted"
222
+ }
223
+ },
224
+ {
225
+ "command": "amazon-in/product",
226
+ "site": "amazon-in",
227
+ "name": "product",
228
+ "description": "Fetch the current Amazon.in price and selected product variant",
229
+ "access": "read",
230
+ "strategy": "UI",
231
+ "browser": true,
232
+ "domain": "amazon.in",
233
+ "positionals": [
234
+ {
235
+ "name": "input",
236
+ "type": "string",
237
+ "description": "Amazon.in product URL or ASIN",
238
+ "positional": true,
239
+ "required": true,
240
+ "variadic": false
241
+ }
242
+ ],
243
+ "options": [],
244
+ "columns": [
245
+ "asin",
246
+ "title",
247
+ "price",
248
+ "mrp",
249
+ "discount",
250
+ "availability",
251
+ "size",
252
+ "colour",
253
+ "image_url",
254
+ "product_url"
255
+ ],
256
+ "aliases": [],
257
+ "defaultFormat": "table",
258
+ "fileArguments": [],
259
+ "sessionPolicy": "create-or-reuse",
260
+ "availability": {
261
+ "mode": "hosted"
262
+ }
263
+ },
264
+ {
265
+ "command": "amazon-in/search",
266
+ "site": "amazon-in",
267
+ "name": "search",
268
+ "description": "Search Amazon.in products with inclusive INR price bounds and images",
269
+ "access": "read",
270
+ "strategy": "UI",
271
+ "browser": true,
272
+ "domain": "amazon.in",
273
+ "positionals": [
274
+ {
275
+ "name": "query",
276
+ "type": "string",
277
+ "description": "Product search query",
278
+ "positional": true,
279
+ "required": true,
280
+ "variadic": false
281
+ }
282
+ ],
283
+ "options": [
284
+ {
285
+ "name": "min-price",
286
+ "type": "number",
287
+ "description": "Inclusive minimum price in rupees",
288
+ "positional": false,
289
+ "required": false,
290
+ "variadic": false
291
+ },
292
+ {
293
+ "name": "max-price",
294
+ "type": "number",
295
+ "description": "Inclusive maximum price in rupees",
296
+ "positional": false,
297
+ "required": false,
298
+ "variadic": false
299
+ },
300
+ {
301
+ "name": "limit",
302
+ "type": "int",
303
+ "description": "Maximum results (1-50)",
304
+ "positional": false,
305
+ "required": false,
306
+ "variadic": false,
307
+ "default": 20
308
+ }
309
+ ],
310
+ "columns": [
311
+ "rank",
312
+ "asin",
313
+ "title",
314
+ "price",
315
+ "mrp",
316
+ "rating",
317
+ "review_count",
318
+ "image_url",
319
+ "product_url",
320
+ "is_sponsored"
321
+ ],
322
+ "aliases": [],
323
+ "defaultFormat": "table",
324
+ "fileArguments": [],
325
+ "sessionPolicy": "create-or-reuse",
326
+ "availability": {
327
+ "mode": "hosted"
328
+ }
329
+ },
330
+ {
331
+ "command": "amazon-in/whoami",
332
+ "site": "amazon-in",
333
+ "name": "whoami",
334
+ "description": "Show the current logged-in amazon-in account",
335
+ "access": "read",
336
+ "strategy": "COOKIE",
337
+ "browser": true,
338
+ "domain": "amazon.in",
339
+ "positionals": [],
340
+ "options": [],
341
+ "columns": [
342
+ "logged_in",
343
+ "site",
344
+ "user_name"
345
+ ],
346
+ "aliases": [],
347
+ "defaultFormat": "table",
348
+ "fileArguments": [],
349
+ "sessionPolicy": "create-or-reuse",
350
+ "availability": {
351
+ "mode": "hosted"
352
+ }
353
+ },
354
+ {
355
+ "command": "amazon-in/wishlist",
356
+ "site": "amazon-in",
357
+ "name": "wishlist",
358
+ "description": "Fetch current prices for products in the default Amazon.in wishlist",
359
+ "access": "read",
360
+ "strategy": "UI",
361
+ "browser": true,
362
+ "domain": "amazon.in",
363
+ "positionals": [],
364
+ "options": [
365
+ {
366
+ "name": "filter",
367
+ "type": "string",
368
+ "description": "Wishlist items to include",
369
+ "positional": false,
370
+ "required": false,
371
+ "variadic": false,
372
+ "default": "unpurchased",
373
+ "choices": [
374
+ "unpurchased",
375
+ "all"
376
+ ]
377
+ }
378
+ ],
379
+ "columns": [
380
+ "list_name",
381
+ "item_id",
382
+ "asin",
383
+ "title",
384
+ "price",
385
+ "mrp",
386
+ "availability",
387
+ "size",
388
+ "colour",
389
+ "image_url",
390
+ "product_url"
391
+ ],
392
+ "aliases": [],
393
+ "defaultFormat": "table",
394
+ "fileArguments": [],
395
+ "sessionPolicy": "create-or-reuse",
396
+ "availability": {
397
+ "mode": "hosted"
398
+ }
399
+ },
72
400
  {
73
401
  "command": "amazon/bestsellers",
74
402
  "site": "amazon",
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@agentrhq/webcmd",
3
- "version": "0.4.0",
3
+ "version": "0.4.2",
4
4
  "description": "Turn websites, browser sessions, desktop apps, and local tools into deterministic CLI surfaces for humans and AI agents.",
5
5
  "engines": {
6
6
  "node": ">=20.0.0"
@@ -114,4 +114,4 @@
114
114
  "overrides": {
115
115
  "postcss": "^8.5.10"
116
116
  }
117
- }
117
+ }