@agentrhq/webcmd 0.3.4 → 0.4.1
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 +43 -268
- package/cli-manifest.json +184 -311
- package/clis/_shared/site-auth.js +37 -40
- package/clis/_shared/site-auth.test.js +109 -22
- package/clis/amazon/auth.js +0 -6
- package/clis/band/auth.js +0 -6
- package/clis/blinkit/auth.js +13 -40
- package/clis/blinkit/blinkit.test.js +39 -2
- package/clis/chatgpt/auth.js +0 -6
- package/clis/claude/auth.js +0 -6
- package/clis/coupang/auth.js +0 -6
- package/clis/district/auth.js +0 -2
- package/clis/facebook/auth.js +0 -6
- package/clis/gemini/auth.js +0 -6
- package/clis/github/auth.js +0 -6
- package/clis/grok/auth.js +0 -6
- package/clis/hackernews/hackernews.test.js +20 -1
- package/clis/hackernews/jobs.js +1 -1
- package/clis/hf/auth.js +2 -7
- package/clis/instagram/auth.js +0 -6
- package/clis/linkedin/auth.js +0 -6
- package/clis/linkedin-learning/auth.js +0 -6
- package/clis/manus/auth.js +0 -6
- package/clis/notebooklm/auth.js +0 -6
- package/clis/pixiv/auth.js +1 -7
- package/clis/practo/login.js +12 -38
- package/clis/practo/practo.test.js +13 -9
- package/clis/reddit/auth.js +0 -6
- package/clis/reuters/auth.js +0 -14
- package/clis/suno/auth.js +0 -6
- package/clis/tiktok/auth.js +0 -6
- package/clis/twitter/auth.js +0 -6
- package/clis/upwork/auth.js +0 -6
- package/clis/youtube/auth.js +0 -6
- package/clis/zepto/auth.js +16 -29
- package/clis/zepto/zepto.test.js +45 -2
- package/dist/src/browser/cdp.js +3 -1
- package/dist/src/browser/cdp.test.js +12 -0
- package/dist/src/browser/page.js +6 -2
- package/dist/src/browser/page.test.js +1 -0
- package/dist/src/browser/protocol.d.ts +2 -0
- package/dist/src/browser/runtime/local-cloak/actions.js +42 -21
- package/dist/src/browser/runtime/local-cloak/provider.test.js +33 -5
- package/dist/src/build-manifest.js +3 -3
- package/dist/src/build-manifest.test.js +14 -0
- package/dist/src/completion-shared.js +1 -1
- package/dist/src/docs-sync-review.js +15 -7
- package/dist/src/docs-sync-review.test.js +38 -2
- package/dist/src/generate-release-notes-cli.test.js +1 -1
- package/dist/src/hosted/client.d.ts +14 -2
- package/dist/src/hosted/client.js +46 -5
- package/dist/src/hosted/client.test.js +73 -20
- package/dist/src/hosted/main-lifecycle.test.js +2 -1
- package/dist/src/hosted/manifest.test.js +4 -2
- package/dist/src/hosted/output-parity.test.js +2 -1
- package/dist/src/hosted/root-command-surface.test.js +8 -1
- package/dist/src/hosted/runner.js +93 -0
- package/dist/src/hosted/runner.test.js +134 -5
- package/dist/src/hosted/types.d.ts +9 -2
- package/dist/src/skills.test.js +54 -0
- package/hosted-contract.json +191 -366
- package/package.json +4 -4
- package/skills/webcmd-adapter-author/SKILL.md +2 -0
- package/skills/webcmd-autofix/SKILL.md +5 -3
- package/skills/webcmd-browser/SKILL.md +12 -11
- package/skills/webcmd-usage/SKILL.md +8 -0
- package/clis/practo/whoami.js +0 -28
|
@@ -9,13 +9,17 @@ import { rewriteBrowserArgv } from '../cli-argv-preprocess.js';
|
|
|
9
9
|
import { createProgram } from '../cli.js';
|
|
10
10
|
import { formatRootHelp } from '../command-presentation.js';
|
|
11
11
|
import { HOSTED_ROOT_HELP } from '../completion-shared.js';
|
|
12
|
-
import {
|
|
12
|
+
import { PKG_VERSION } from '../version.js';
|
|
13
|
+
import { makeHostedConfig, makeLocalConfig } from './config.js';
|
|
13
14
|
import { runHostedCli } from './runner.js';
|
|
15
|
+
const [packageMajor, packageMinor] = PKG_VERSION.split('.');
|
|
16
|
+
const compatiblePatchVersion = `${packageMajor}.${packageMinor}.99`;
|
|
17
|
+
const incompatibleMinorVersion = `${packageMajor}.${Number(packageMinor) + 1}.0`;
|
|
14
18
|
const manifest = {
|
|
15
19
|
userId: 'user_demo',
|
|
16
20
|
metadata: {
|
|
17
21
|
contractSchemaVersion: 1,
|
|
18
|
-
webcmdPackageVersion:
|
|
22
|
+
webcmdPackageVersion: PKG_VERSION,
|
|
19
23
|
generatedAt: '2026-07-08T00:00:00.000Z',
|
|
20
24
|
},
|
|
21
25
|
commands: [
|
|
@@ -219,6 +223,131 @@ function captureLocalBrowserStructure(argv) {
|
|
|
219
223
|
}
|
|
220
224
|
}
|
|
221
225
|
describe('runHostedCli', () => {
|
|
226
|
+
const publicProfile = {
|
|
227
|
+
id: 'profile_work',
|
|
228
|
+
name: 'Work',
|
|
229
|
+
userId: 'user_64256',
|
|
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, creates, resolves, gets, 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 === 'POST') {
|
|
246
|
+
return new Response(JSON.stringify({ ok: true, profile: publicProfile }), { status: 201 });
|
|
247
|
+
}
|
|
248
|
+
if (request.method === 'DELETE') {
|
|
249
|
+
return new Response(JSON.stringify({ ok: true, deleted: true }));
|
|
250
|
+
}
|
|
251
|
+
return new Response(JSON.stringify({ ok: true, profiles: [publicProfile] }));
|
|
252
|
+
});
|
|
253
|
+
for (const argv of [
|
|
254
|
+
['profile', 'list', '-f', 'json'],
|
|
255
|
+
['profile', 'create', 'Work', '--user-id', 'user_64256', '-f', 'json'],
|
|
256
|
+
['profile', 'get', 'Work', '-f', 'json'],
|
|
257
|
+
['profile', 'get', 'user_64256', '-f', 'json'],
|
|
258
|
+
['profile', 'get', 'profile_work', '-f', 'json'],
|
|
259
|
+
['profile', 'delete', 'profile_work', '-f', 'json'],
|
|
260
|
+
]) {
|
|
261
|
+
const stdout = sink();
|
|
262
|
+
const stderr = sink();
|
|
263
|
+
const result = await runHostedCli(argv, {
|
|
264
|
+
config: makeHostedConfig({ apiBaseUrl: 'https://api.example.com', apiKey: 'key' }),
|
|
265
|
+
stdout: stdout.stream,
|
|
266
|
+
stderr: stderr.stream,
|
|
267
|
+
fetchImpl,
|
|
268
|
+
});
|
|
269
|
+
expect(result).toEqual({ handled: true, exitCode: 0 });
|
|
270
|
+
expect(stderr.text()).toBe('');
|
|
271
|
+
expect(stdout.text()).toContain(argv[1] === 'delete' ? '"deleted": true' : '"id": "profile_work"');
|
|
272
|
+
}
|
|
273
|
+
expect(requests).toEqual([
|
|
274
|
+
{ url: 'https://api.example.com/v1/profiles', method: 'GET' },
|
|
275
|
+
{
|
|
276
|
+
url: 'https://api.example.com/v1/profiles',
|
|
277
|
+
method: 'POST',
|
|
278
|
+
body: { name: 'Work', userId: 'user_64256' },
|
|
279
|
+
},
|
|
280
|
+
{ url: 'https://api.example.com/v1/profiles', method: 'GET' },
|
|
281
|
+
{ url: 'https://api.example.com/v1/profiles', method: 'GET' },
|
|
282
|
+
{ url: 'https://api.example.com/v1/profiles', method: 'GET' },
|
|
283
|
+
{ url: 'https://api.example.com/v1/profiles/profile_work', method: 'DELETE' },
|
|
284
|
+
]);
|
|
285
|
+
expect(requests.some(request => request.url.endsWith('/v1/manifest'))).toBe(false);
|
|
286
|
+
});
|
|
287
|
+
it('deduplicates universal profile matches and rejects missing or ambiguous selectors', async () => {
|
|
288
|
+
const cases = [
|
|
289
|
+
{
|
|
290
|
+
selector: 'same',
|
|
291
|
+
profiles: [{ ...publicProfile, id: 'profile_same', name: 'same', userId: 'same' }],
|
|
292
|
+
exitCode: 0,
|
|
293
|
+
stdout: '"id": "profile_same"',
|
|
294
|
+
stderr: '',
|
|
295
|
+
},
|
|
296
|
+
{
|
|
297
|
+
selector: 'missing',
|
|
298
|
+
profiles: [publicProfile],
|
|
299
|
+
exitCode: 66,
|
|
300
|
+
stdout: '',
|
|
301
|
+
stderr: 'code: PROFILE_NOT_FOUND',
|
|
302
|
+
},
|
|
303
|
+
{
|
|
304
|
+
selector: 'shared',
|
|
305
|
+
profiles: [
|
|
306
|
+
{ ...publicProfile, id: 'profile_one', name: 'shared' },
|
|
307
|
+
{ ...publicProfile, id: 'profile_two', name: 'Other', userId: 'shared' },
|
|
308
|
+
],
|
|
309
|
+
exitCode: 75,
|
|
310
|
+
stdout: '',
|
|
311
|
+
stderr: 'code: AMBIGUOUS_PROFILE',
|
|
312
|
+
},
|
|
313
|
+
];
|
|
314
|
+
for (const testCase of cases) {
|
|
315
|
+
const stdout = sink();
|
|
316
|
+
const stderr = sink();
|
|
317
|
+
const fetchImpl = vi.fn(async () => new Response(JSON.stringify({ ok: true, profiles: testCase.profiles })));
|
|
318
|
+
const result = await runHostedCli(['profile', 'get', testCase.selector, '-f', 'json'], {
|
|
319
|
+
config: makeHostedConfig({ apiBaseUrl: 'https://api.example.com', apiKey: 'key' }),
|
|
320
|
+
stdout: stdout.stream,
|
|
321
|
+
stderr: stderr.stream,
|
|
322
|
+
fetchImpl,
|
|
323
|
+
});
|
|
324
|
+
expect(result.exitCode).toBe(testCase.exitCode);
|
|
325
|
+
expect(stdout.text()).toContain(testCase.stdout);
|
|
326
|
+
expect(stderr.text()).toContain(testCase.stderr);
|
|
327
|
+
if (testCase.selector === 'shared') {
|
|
328
|
+
expect(stderr.text()).toContain('Use the immutable profile ID returned by webcmd profile list.');
|
|
329
|
+
}
|
|
330
|
+
expect(fetchImpl).toHaveBeenCalledTimes(1);
|
|
331
|
+
}
|
|
332
|
+
});
|
|
333
|
+
it.each(['rename', 'use'])('rejects local-only profile %s in hosted mode without an API call', async (command) => {
|
|
334
|
+
const stderr = sink();
|
|
335
|
+
const fetchImpl = vi.fn();
|
|
336
|
+
const result = await runHostedCli(['profile', command, 'value'], {
|
|
337
|
+
config: makeHostedConfig({ apiBaseUrl: 'https://api.example.com', apiKey: 'key' }),
|
|
338
|
+
stderr: stderr.stream,
|
|
339
|
+
fetchImpl,
|
|
340
|
+
});
|
|
341
|
+
expect(result).toEqual({ handled: true, exitCode: 78 });
|
|
342
|
+
expect(stderr.text()).toContain(`webcmd profile ${command} is not available in hosted mode.`);
|
|
343
|
+
expect(fetchImpl).not.toHaveBeenCalled();
|
|
344
|
+
});
|
|
345
|
+
it.each(['list', 'rename', 'use'])('leaves profile %s to the existing local command surface', async (command) => {
|
|
346
|
+
const result = await runHostedCli(['profile', command, 'value'], {
|
|
347
|
+
config: makeLocalConfig(),
|
|
348
|
+
});
|
|
349
|
+
expect(result).toEqual({ handled: false, exitCode: 0 });
|
|
350
|
+
});
|
|
222
351
|
it.each([
|
|
223
352
|
['missing-site'],
|
|
224
353
|
['missing-site', 'child'],
|
|
@@ -1084,7 +1213,7 @@ describe('runHostedCli', () => {
|
|
|
1084
1213
|
const stdout = sink();
|
|
1085
1214
|
const patchBumped = {
|
|
1086
1215
|
...manifest,
|
|
1087
|
-
metadata: { ...manifest.metadata, webcmdPackageVersion:
|
|
1216
|
+
metadata: { ...manifest.metadata, webcmdPackageVersion: compatiblePatchVersion },
|
|
1088
1217
|
};
|
|
1089
1218
|
const result = await runHostedCli(['github', 'whoami', '-f', 'json'], {
|
|
1090
1219
|
config: makeHostedConfig({ apiBaseUrl: 'https://api.example.com', apiKey: 'key' }),
|
|
@@ -1108,7 +1237,7 @@ describe('runHostedCli', () => {
|
|
|
1108
1237
|
const stderr = sink();
|
|
1109
1238
|
const mismatched = {
|
|
1110
1239
|
...manifest,
|
|
1111
|
-
metadata: { ...manifest.metadata, webcmdPackageVersion:
|
|
1240
|
+
metadata: { ...manifest.metadata, webcmdPackageVersion: incompatibleMinorVersion },
|
|
1112
1241
|
};
|
|
1113
1242
|
const result = await runHostedCli(['github', 'whoami'], {
|
|
1114
1243
|
config: makeHostedConfig({ apiBaseUrl: 'https://api.example.com', apiKey: 'key' }),
|
|
@@ -1527,7 +1656,7 @@ describe('runHostedCli', () => {
|
|
|
1527
1656
|
const stderr = sink();
|
|
1528
1657
|
const mismatched = {
|
|
1529
1658
|
...manifest,
|
|
1530
|
-
metadata: { ...manifest.metadata, webcmdPackageVersion:
|
|
1659
|
+
metadata: { ...manifest.metadata, webcmdPackageVersion: incompatibleMinorVersion },
|
|
1531
1660
|
};
|
|
1532
1661
|
const result = await runHostedCli(['browser', 'work', 'state'], {
|
|
1533
1662
|
config: makeHostedConfig({ apiBaseUrl: 'https://api.example.com', apiKey: 'key' }),
|
|
@@ -40,16 +40,23 @@ export interface HostedManifest {
|
|
|
40
40
|
commands: HostedCommand[];
|
|
41
41
|
}
|
|
42
42
|
export interface HostedPublicProfile {
|
|
43
|
-
|
|
43
|
+
id: string;
|
|
44
|
+
name: string | null;
|
|
45
|
+
userId: 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 {
|
|
50
53
|
ok: true;
|
|
51
54
|
profiles: HostedPublicProfile[];
|
|
52
55
|
}
|
|
56
|
+
export interface HostedProfileResponse {
|
|
57
|
+
ok: true;
|
|
58
|
+
profile: HostedPublicProfile;
|
|
59
|
+
}
|
|
53
60
|
export interface HostedExecution {
|
|
54
61
|
id: string;
|
|
55
62
|
command: string;
|
package/dist/src/skills.test.js
CHANGED
|
@@ -42,6 +42,9 @@ function makePackageRoot(label = 'current') {
|
|
|
42
42
|
function real(filePath) {
|
|
43
43
|
return fs.realpathSync(filePath);
|
|
44
44
|
}
|
|
45
|
+
function bundledSkill(name) {
|
|
46
|
+
return fs.readFileSync(path.join(process.cwd(), 'skills', name, 'SKILL.md'), 'utf8');
|
|
47
|
+
}
|
|
45
48
|
describe('webcmd skills content', () => {
|
|
46
49
|
it('keeps bundled skill frontmatter valid yaml', () => {
|
|
47
50
|
const skillsRoot = path.join(process.cwd(), 'skills');
|
|
@@ -80,6 +83,57 @@ describe('webcmd skills content', () => {
|
|
|
80
83
|
'webcmd-usage',
|
|
81
84
|
]);
|
|
82
85
|
});
|
|
86
|
+
it('enforces mode-neutral authentication and human handoff policy', () => {
|
|
87
|
+
const browser = bundledSkill('webcmd-browser');
|
|
88
|
+
const usage = bundledSkill('webcmd-usage');
|
|
89
|
+
const autofix = bundledSkill('webcmd-autofix');
|
|
90
|
+
const author = bundledSkill('webcmd-adapter-author');
|
|
91
|
+
const skills = [browser, usage, autofix, author];
|
|
92
|
+
const handoffSkills = [browser, usage, autofix];
|
|
93
|
+
const autofixAuthRequired = autofix.match(/^- \*\*`AUTH_REQUIRED`\*\*[\s\S]*?(?=\n- \*\*)/m)?.[0] ?? '';
|
|
94
|
+
const autofixAuthRequiredRow = autofix.split('\n')
|
|
95
|
+
.find((line) => line.startsWith('| AUTH_REQUIRED |')) ?? '';
|
|
96
|
+
expect(browser).toContain('webcmd <site> login');
|
|
97
|
+
expect(browser).toContain('webcmd <site> whoami');
|
|
98
|
+
expect(browser).toContain('CAPTCHA');
|
|
99
|
+
expect(browser).toContain('fresh browser state');
|
|
100
|
+
expect(browser).not.toContain('hunter2');
|
|
101
|
+
expect(browser).not.toMatch(/browser login type/i);
|
|
102
|
+
expect(usage).toContain('AUTH_REQUIRED');
|
|
103
|
+
expect(usage).toContain('action_required');
|
|
104
|
+
expect(autofix).toContain('webcmd <site> login');
|
|
105
|
+
expect(author).toContain('registerSiteAuthCommands');
|
|
106
|
+
for (const skill of handoffSkills) {
|
|
107
|
+
expect(skill).toContain('handoff.status');
|
|
108
|
+
expect(skill).toContain('handoff.viewUrl');
|
|
109
|
+
expect(skill).toContain('handoff.verifyCommand');
|
|
110
|
+
expect(skill).toContain('Webcmd browser:');
|
|
111
|
+
expect(skill).not.toMatch(/\bhosted\b|\bKernel\b|\blocal mode\b|\blocally\b/i);
|
|
112
|
+
}
|
|
113
|
+
for (const skill of [browser, usage]) {
|
|
114
|
+
expect(skill).toContain('already_logged_in');
|
|
115
|
+
expect(skill).toContain('in_progress');
|
|
116
|
+
expect(skill).toContain('action_url');
|
|
117
|
+
expect(skill).toContain('view_url');
|
|
118
|
+
expect(skill).toMatch(/in_progress[^\n]*(?:do not ask the user|do not wait for user confirmation)/i);
|
|
119
|
+
expect(skill).toMatch(/(?:action_url|view_url|handoff\.viewUrl|Webcmd browser:)[\s\S]{0,300}user/i);
|
|
120
|
+
}
|
|
121
|
+
for (const skill of skills) {
|
|
122
|
+
expect(skill).toContain('action_required');
|
|
123
|
+
expect(skill).toContain('verify_command');
|
|
124
|
+
expect(skill).toMatch(/verify_command[\s\S]{0,250}verification must succeed[\s\S]{0,250}retry/i);
|
|
125
|
+
expect(skill).toMatch(/verify_command[\s\S]{0,250}user[\s\S]{0,250}(?:done|complet)/i);
|
|
126
|
+
expect(skill).toMatch(/CAPTCHA[\s\S]{0,250}(?:human handoff|stop(?:s)? automation)/i);
|
|
127
|
+
expect(skill).toMatch(/(?:must not|never).*?(?:password|secret|credential)/i);
|
|
128
|
+
}
|
|
129
|
+
for (const skill of [browser, usage, autofix]) {
|
|
130
|
+
expect(skill).toMatch(/(?:no (?:site )?login command|without a verifier)[\s\S]{0,500}fresh browser state[\s\S]{0,500}(?:identity check|post-action state)[\s\S]{0,250}before (?:any )?retry/i);
|
|
131
|
+
}
|
|
132
|
+
expect(autofixAuthRequired).toMatch(/if (?:a|the) site login command exists[\s\S]*webcmd <site> login[\s\S]*returned `verify_command`[\s\S]*verification must succeed[\s\S]*retry/i);
|
|
133
|
+
expect(autofixAuthRequired).toMatch(/no site login command[\s\S]*stop (?:browser )?writes[\s\S]*visible browser[\s\S]*fresh browser state[\s\S]*(?:identity check|post-action state)[\s\S]*before retry[\s\S]*report alone is not verification/i);
|
|
134
|
+
expect(autofixAuthRequiredRow).toMatch(/conditional[^|]*Safety Boundaries|no site login command/i);
|
|
135
|
+
expect(autofix).toMatch(/CAPTCHA[\s\S]{0,250}stop automation[\s\S]{0,250}verification must succeed/i);
|
|
136
|
+
});
|
|
83
137
|
it('adds bundled skills once and refreshes them after package updates', () => {
|
|
84
138
|
const firstRoot = makePackageRoot('first');
|
|
85
139
|
const secondRoot = makePackageRoot('second');
|