@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
|
@@ -1,32 +1,37 @@
|
|
|
1
|
-
import { AuthRequiredError
|
|
1
|
+
import { AuthRequiredError } from '@agentrhq/webcmd/errors';
|
|
2
2
|
import { cli, Strategy } from '@agentrhq/webcmd/registry';
|
|
3
3
|
|
|
4
|
-
const
|
|
5
|
-
const POLL_INTERVAL_MS = 2000;
|
|
4
|
+
const LOGIN_ACTION = 'Complete sign-in in the opened Webcmd browser, then tell the agent when you are done.';
|
|
6
5
|
|
|
7
|
-
function normalizeIdentity(
|
|
6
|
+
function normalizeIdentity(config, identity) {
|
|
8
7
|
const row = identity && typeof identity === 'object' && !Array.isArray(identity)
|
|
9
8
|
? identity
|
|
10
9
|
: {};
|
|
11
|
-
return { logged_in: true, site
|
|
10
|
+
return { ...blankIdentity(config), ...row, logged_in: true, site: config.site };
|
|
12
11
|
}
|
|
13
12
|
|
|
14
13
|
function isAuthRequired(error) {
|
|
15
14
|
return error instanceof AuthRequiredError;
|
|
16
15
|
}
|
|
17
16
|
|
|
18
|
-
async function tryProbe(config, page
|
|
19
|
-
|
|
20
|
-
return normalizeIdentity(config.site, await probe(page, { phase }));
|
|
17
|
+
async function tryProbe(config, page) {
|
|
18
|
+
return normalizeIdentity(config, await config.verify(page, { phase: 'identity' }));
|
|
21
19
|
}
|
|
22
20
|
|
|
23
|
-
function
|
|
24
|
-
return
|
|
21
|
+
function identityColumns(config) {
|
|
22
|
+
return config.columns ?? ['id', 'username', 'name'];
|
|
23
|
+
}
|
|
24
|
+
|
|
25
|
+
function blankIdentity(config) {
|
|
26
|
+
return Object.fromEntries(identityColumns(config).map((column) => [column, '']));
|
|
25
27
|
}
|
|
26
28
|
|
|
27
29
|
function commandColumns(config) {
|
|
28
|
-
|
|
29
|
-
|
|
30
|
+
return ['logged_in', 'site', ...identityColumns(config)];
|
|
31
|
+
}
|
|
32
|
+
|
|
33
|
+
function loginColumns(config) {
|
|
34
|
+
return ['status', ...commandColumns(config), 'action', 'verify_command'];
|
|
30
35
|
}
|
|
31
36
|
|
|
32
37
|
function normalizeQuickCheck(result) {
|
|
@@ -62,6 +67,7 @@ export function registerSiteAuthCommands(config) {
|
|
|
62
67
|
browser: true,
|
|
63
68
|
navigateBefore: false,
|
|
64
69
|
siteSession: 'persistent',
|
|
70
|
+
aliases: config.whoamiAliases ?? [],
|
|
65
71
|
args: [],
|
|
66
72
|
columns: commandColumns(config),
|
|
67
73
|
authStatus: {
|
|
@@ -72,52 +78,43 @@ export function registerSiteAuthCommands(config) {
|
|
|
72
78
|
? { refresh: async (page, kwargs) => normalizeRefreshResult(await config.refresh(page, kwargs)) }
|
|
73
79
|
: {}),
|
|
74
80
|
},
|
|
75
|
-
func: async (page) => [await tryProbe(config, page
|
|
81
|
+
func: async (page) => [await tryProbe(config, page)],
|
|
76
82
|
});
|
|
77
83
|
|
|
78
84
|
cli({
|
|
79
85
|
site: config.site,
|
|
80
86
|
name: 'login',
|
|
81
87
|
access: 'write',
|
|
82
|
-
description: config.loginDescription ?? `Open ${config.site} login
|
|
88
|
+
description: config.loginDescription ?? `Open ${config.site} login`,
|
|
83
89
|
domain: config.domain,
|
|
84
90
|
strategy: Strategy.COOKIE,
|
|
85
91
|
browser: true,
|
|
86
92
|
navigateBefore: false,
|
|
87
93
|
defaultWindowMode: 'foreground',
|
|
88
94
|
siteSession: 'persistent',
|
|
89
|
-
args: [
|
|
90
|
-
|
|
91
|
-
|
|
92
|
-
columns: ['status', ...commandColumns(config)],
|
|
93
|
-
func: async (page, kwargs) => {
|
|
95
|
+
args: [],
|
|
96
|
+
columns: loginColumns(config),
|
|
97
|
+
func: async (page) => {
|
|
94
98
|
try {
|
|
95
|
-
return [{
|
|
99
|
+
return [{
|
|
100
|
+
status: 'already_logged_in',
|
|
101
|
+
...await tryProbe(config, page),
|
|
102
|
+
action: '',
|
|
103
|
+
verify_command: '',
|
|
104
|
+
}];
|
|
96
105
|
} catch (error) {
|
|
97
106
|
if (!isAuthRequired(error)) throw error;
|
|
98
107
|
}
|
|
99
108
|
|
|
100
109
|
await openLogin(page);
|
|
101
|
-
|
|
102
|
-
|
|
103
|
-
|
|
104
|
-
|
|
105
|
-
|
|
106
|
-
|
|
107
|
-
|
|
108
|
-
|
|
109
|
-
return [{ status: 'login_complete', ...identity }];
|
|
110
|
-
} catch (error) {
|
|
111
|
-
if (!isAuthRequired(error)) throw error;
|
|
112
|
-
lastAuthMessage = getErrorMessage(error);
|
|
113
|
-
}
|
|
114
|
-
}
|
|
115
|
-
|
|
116
|
-
throw new TimeoutError(
|
|
117
|
-
`${config.site} login`,
|
|
118
|
-
timeoutSeconds,
|
|
119
|
-
lastAuthMessage ? `${authHint(config)} Last auth check: ${lastAuthMessage}` : authHint(config),
|
|
120
|
-
);
|
|
110
|
+
return [{
|
|
111
|
+
status: 'action_required',
|
|
112
|
+
logged_in: false,
|
|
113
|
+
site: config.site,
|
|
114
|
+
...blankIdentity(config),
|
|
115
|
+
action: LOGIN_ACTION,
|
|
116
|
+
verify_command: `webcmd ${config.site} whoami`,
|
|
117
|
+
}];
|
|
121
118
|
},
|
|
122
119
|
});
|
|
123
120
|
}
|
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
import { describe, expect, it, vi } from 'vitest';
|
|
2
|
-
import { AuthRequiredError
|
|
2
|
+
import { AuthRequiredError } from '@agentrhq/webcmd/errors';
|
|
3
3
|
import { getRegistry } from '@agentrhq/webcmd/registry';
|
|
4
4
|
import { registerSiteAuthCommands } from './site-auth.js';
|
|
5
5
|
|
|
@@ -11,12 +11,13 @@ function pageMock() {
|
|
|
11
11
|
}
|
|
12
12
|
|
|
13
13
|
describe('site auth command helper', () => {
|
|
14
|
-
it('registers whoami and foreground login
|
|
14
|
+
it('registers whoami aliases and foreground login columns', () => {
|
|
15
15
|
registerSiteAuthCommands({
|
|
16
16
|
site: 'auth-helper-registration',
|
|
17
17
|
domain: 'example.com',
|
|
18
18
|
loginUrl: 'https://example.com/login',
|
|
19
19
|
columns: ['username'],
|
|
20
|
+
whoamiAliases: ['auth-status'],
|
|
20
21
|
verify: async () => ({ username: 'alice' }),
|
|
21
22
|
});
|
|
22
23
|
|
|
@@ -24,16 +25,23 @@ describe('site auth command helper', () => {
|
|
|
24
25
|
access: 'read',
|
|
25
26
|
browser: true,
|
|
26
27
|
navigateBefore: false,
|
|
28
|
+
aliases: ['auth-status'],
|
|
27
29
|
columns: ['logged_in', 'site', 'username'],
|
|
28
30
|
});
|
|
29
|
-
expect(getRegistry().get('auth-helper-registration/
|
|
31
|
+
expect(getRegistry().get('auth-helper-registration/auth-status'))
|
|
32
|
+
.toBe(getRegistry().get('auth-helper-registration/whoami'));
|
|
33
|
+
const login = getRegistry().get('auth-helper-registration/login');
|
|
34
|
+
expect(login).toMatchObject({
|
|
30
35
|
access: 'write',
|
|
31
36
|
browser: true,
|
|
32
37
|
navigateBefore: false,
|
|
33
38
|
defaultWindowMode: 'foreground',
|
|
34
39
|
siteSession: 'persistent',
|
|
35
|
-
columns: ['status', 'logged_in', 'site', 'username'],
|
|
36
40
|
});
|
|
41
|
+
expect(login.args).toEqual([]);
|
|
42
|
+
expect(login.columns).toEqual([
|
|
43
|
+
'status', 'logged_in', 'site', 'username', 'action', 'verify_command',
|
|
44
|
+
]);
|
|
37
45
|
});
|
|
38
46
|
|
|
39
47
|
it('whoami returns normalized identity without opening login', async () => {
|
|
@@ -55,44 +63,123 @@ describe('site auth command helper', () => {
|
|
|
55
63
|
expect(page.goto).not.toHaveBeenCalled();
|
|
56
64
|
});
|
|
57
65
|
|
|
58
|
-
it('login
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
|
|
66
|
+
it('login returns the existing authenticated identity', async () => {
|
|
67
|
+
registerSiteAuthCommands({
|
|
68
|
+
site: 'auth-helper-authenticated',
|
|
69
|
+
domain: 'example.com',
|
|
70
|
+
loginUrl: 'https://example.com/login',
|
|
71
|
+
columns: ['username'],
|
|
72
|
+
verify: async () => ({ username: 'alice' }),
|
|
73
|
+
});
|
|
74
|
+
const login = getRegistry().get('auth-helper-authenticated/login');
|
|
75
|
+
const page = pageMock();
|
|
76
|
+
|
|
77
|
+
await expect(login.func(page, {})).resolves.toEqual([{
|
|
78
|
+
status: 'already_logged_in',
|
|
79
|
+
logged_in: true,
|
|
80
|
+
site: 'auth-helper-authenticated',
|
|
81
|
+
username: 'alice',
|
|
82
|
+
action: '',
|
|
83
|
+
verify_command: '',
|
|
84
|
+
}]);
|
|
85
|
+
expect(page.goto).not.toHaveBeenCalled();
|
|
86
|
+
});
|
|
87
|
+
|
|
88
|
+
it('completes and canonicalizes successful identity rows', async () => {
|
|
89
|
+
registerSiteAuthCommands({
|
|
90
|
+
site: 'auth-helper-canonical',
|
|
91
|
+
domain: 'example.com',
|
|
92
|
+
loginUrl: 'https://example.com/login',
|
|
93
|
+
columns: ['username', 'name'],
|
|
94
|
+
verify: async () => ({
|
|
95
|
+
logged_in: false,
|
|
96
|
+
site: 'wrong-site',
|
|
97
|
+
username: 'alice',
|
|
98
|
+
extra: 'preserved',
|
|
99
|
+
}),
|
|
100
|
+
});
|
|
101
|
+
const page = pageMock();
|
|
102
|
+
const identity = {
|
|
103
|
+
logged_in: true,
|
|
104
|
+
site: 'auth-helper-canonical',
|
|
105
|
+
username: 'alice',
|
|
106
|
+
name: '',
|
|
107
|
+
extra: 'preserved',
|
|
108
|
+
};
|
|
109
|
+
|
|
110
|
+
await expect(getRegistry().get('auth-helper-canonical/whoami').func(page, {}))
|
|
111
|
+
.resolves.toEqual([identity]);
|
|
112
|
+
await expect(getRegistry().get('auth-helper-canonical/login').func(page, {}))
|
|
113
|
+
.resolves.toEqual([{
|
|
114
|
+
status: 'already_logged_in',
|
|
115
|
+
...identity,
|
|
116
|
+
action: '',
|
|
117
|
+
verify_command: '',
|
|
118
|
+
}]);
|
|
119
|
+
});
|
|
120
|
+
|
|
121
|
+
it('opens the default login URL and returns an immediate handoff', async () => {
|
|
62
122
|
registerSiteAuthCommands({
|
|
63
123
|
site: 'auth-helper-login',
|
|
64
124
|
domain: 'example.com',
|
|
65
125
|
loginUrl: 'https://example.com/login',
|
|
66
126
|
columns: ['username'],
|
|
67
127
|
verify: async () => { throw new AuthRequiredError('example.com', 'missing'); },
|
|
68
|
-
poll,
|
|
69
128
|
});
|
|
70
|
-
const
|
|
129
|
+
const login = getRegistry().get('auth-helper-login/login');
|
|
71
130
|
const page = pageMock();
|
|
72
131
|
|
|
73
|
-
|
|
74
|
-
|
|
75
|
-
logged_in
|
|
132
|
+
expect(login.args).toEqual([]);
|
|
133
|
+
expect(login.columns).toEqual([
|
|
134
|
+
'status', 'logged_in', 'site', 'username', 'action', 'verify_command',
|
|
135
|
+
]);
|
|
136
|
+
await expect(login.func(page, {})).resolves.toEqual([{
|
|
137
|
+
status: 'action_required',
|
|
138
|
+
logged_in: false,
|
|
76
139
|
site: 'auth-helper-login',
|
|
77
|
-
username: '
|
|
140
|
+
username: '',
|
|
141
|
+
action: 'Complete sign-in in the opened Webcmd browser, then tell the agent when you are done.',
|
|
142
|
+
verify_command: 'webcmd auth-helper-login whoami',
|
|
78
143
|
}]);
|
|
79
144
|
expect(page.goto).toHaveBeenCalledWith('https://example.com/login');
|
|
80
|
-
expect(page.wait).toHaveBeenCalled();
|
|
81
|
-
expect(poll).toHaveBeenCalledTimes(2);
|
|
145
|
+
expect(page.wait).not.toHaveBeenCalled();
|
|
82
146
|
});
|
|
83
147
|
|
|
84
|
-
it('
|
|
148
|
+
it('uses a custom opener for the immediate handoff', async () => {
|
|
149
|
+
const openLogin = vi.fn().mockResolvedValue(undefined);
|
|
85
150
|
registerSiteAuthCommands({
|
|
86
|
-
site: 'auth-helper-
|
|
151
|
+
site: 'auth-helper-custom-login',
|
|
87
152
|
domain: 'example.com',
|
|
88
153
|
loginUrl: 'https://example.com/login',
|
|
89
154
|
verify: async () => { throw new AuthRequiredError('example.com', 'missing'); },
|
|
90
|
-
|
|
155
|
+
openLogin,
|
|
91
156
|
});
|
|
92
|
-
const
|
|
157
|
+
const login = getRegistry().get('auth-helper-custom-login/login');
|
|
93
158
|
const page = pageMock();
|
|
94
159
|
|
|
95
|
-
await
|
|
96
|
-
expect(
|
|
160
|
+
await login.func(page, {});
|
|
161
|
+
expect(openLogin).toHaveBeenCalledOnce();
|
|
162
|
+
expect(page.goto).not.toHaveBeenCalled();
|
|
163
|
+
});
|
|
164
|
+
|
|
165
|
+
it('propagates non-auth probe and opener errors', async () => {
|
|
166
|
+
registerSiteAuthCommands({
|
|
167
|
+
site: 'auth-helper-probe-error',
|
|
168
|
+
domain: 'example.com',
|
|
169
|
+
loginUrl: 'https://example.com/login',
|
|
170
|
+
verify: async () => { throw new Error('probe broke'); },
|
|
171
|
+
});
|
|
172
|
+
registerSiteAuthCommands({
|
|
173
|
+
site: 'auth-helper-open-error',
|
|
174
|
+
domain: 'example.com',
|
|
175
|
+
loginUrl: 'https://example.com/login',
|
|
176
|
+
verify: async () => { throw new AuthRequiredError('example.com', 'missing'); },
|
|
177
|
+
openLogin: async () => { throw new Error('open broke'); },
|
|
178
|
+
});
|
|
179
|
+
|
|
180
|
+
await expect(getRegistry().get('auth-helper-probe-error/login').func(pageMock(), {}))
|
|
181
|
+
.rejects.toThrow('probe broke');
|
|
182
|
+
await expect(getRegistry().get('auth-helper-open-error/login').func(pageMock(), {}))
|
|
183
|
+
.rejects.toThrow('open broke');
|
|
97
184
|
});
|
|
98
185
|
});
|
package/clis/amazon/auth.js
CHANGED
|
@@ -44,10 +44,4 @@ registerSiteAuthCommands({
|
|
|
44
44
|
columns: ['user_name'],
|
|
45
45
|
quickCheck: hasAmazonSessionCookies,
|
|
46
46
|
verify: verifyAmazonIdentity,
|
|
47
|
-
poll: async (page) => {
|
|
48
|
-
if (!await hasAmazonSessionCookies(page)) {
|
|
49
|
-
throw new AuthRequiredError('amazon.com', 'Waiting for Amazon at-main / x-main cookie');
|
|
50
|
-
}
|
|
51
|
-
return verifyAmazonIdentity(page);
|
|
52
|
-
},
|
|
53
47
|
});
|
package/clis/band/auth.js
CHANGED
|
@@ -53,10 +53,4 @@ registerSiteAuthCommands({
|
|
|
53
53
|
columns: ['user_id'],
|
|
54
54
|
quickCheck: hasBandSessionCookie,
|
|
55
55
|
verify: verifyBandIdentity,
|
|
56
|
-
poll: async (page) => {
|
|
57
|
-
if (!await hasBandSessionCookie(page)) {
|
|
58
|
-
throw new AuthRequiredError('band.us', 'Waiting for Band band_session cookie');
|
|
59
|
-
}
|
|
60
|
-
return verifyBandIdentity(page);
|
|
61
|
-
},
|
|
62
56
|
});
|
package/clis/blinkit/auth.js
CHANGED
|
@@ -1,9 +1,7 @@
|
|
|
1
|
-
import { AuthRequiredError
|
|
2
|
-
import {
|
|
1
|
+
import { AuthRequiredError } from '@agentrhq/webcmd/errors';
|
|
2
|
+
import { registerSiteAuthCommands } from '../_shared/site-auth.js';
|
|
3
3
|
import { BASE, DOMAIN } from './utils.js';
|
|
4
4
|
|
|
5
|
-
const DEFAULT_TIMEOUT_SECONDS = 300;
|
|
6
|
-
|
|
7
5
|
async function probeBlinkitIdentity(page) {
|
|
8
6
|
const probe = await page.evaluate(`
|
|
9
7
|
(() => {
|
|
@@ -50,46 +48,21 @@ function buildOpenLoginEvaluate() {
|
|
|
50
48
|
`;
|
|
51
49
|
}
|
|
52
50
|
|
|
53
|
-
|
|
51
|
+
registerSiteAuthCommands({
|
|
54
52
|
site: 'blinkit',
|
|
55
|
-
name: 'login',
|
|
56
|
-
access: 'write',
|
|
57
|
-
description: 'Open Blinkit login and wait until the browser session is authenticated',
|
|
58
53
|
domain: DOMAIN,
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
defaultWindowMode: 'foreground',
|
|
63
|
-
siteSession: 'persistent',
|
|
64
|
-
args: [
|
|
65
|
-
{ name: 'timeout', type: 'int', default: DEFAULT_TIMEOUT_SECONDS, help: 'Maximum seconds to wait for the user to finish login' },
|
|
66
|
-
],
|
|
67
|
-
columns: ['status', 'logged_in', 'phone', 'user_id'],
|
|
68
|
-
func: async (page, kwargs) => {
|
|
54
|
+
loginUrl: BASE,
|
|
55
|
+
columns: ['phone', 'user_id'],
|
|
56
|
+
verify: async (page) => {
|
|
69
57
|
await page.goto(BASE);
|
|
70
58
|
await page.wait(1);
|
|
71
|
-
|
|
72
|
-
|
|
73
|
-
|
|
74
|
-
|
|
75
|
-
|
|
76
|
-
|
|
77
|
-
|
|
78
|
-
const opened = await page.evaluate(buildOpenLoginEvaluate()).catch(() => null);
|
|
79
|
-
const timeoutSeconds = Number(kwargs.timeout ?? DEFAULT_TIMEOUT_SECONDS);
|
|
80
|
-
const deadline = Date.now() + timeoutSeconds * 1000;
|
|
81
|
-
let lastMessage = opened?.opened ? '' : opened?.detail || '';
|
|
82
|
-
while (Date.now() < deadline) {
|
|
83
|
-
await page.wait(2);
|
|
84
|
-
try {
|
|
85
|
-
const identity = await probeBlinkitIdentity(page);
|
|
86
|
-
return [{ status: 'login_complete', logged_in: true, ...identity }];
|
|
87
|
-
} catch (error) {
|
|
88
|
-
if (!(error instanceof AuthRequiredError)) throw error;
|
|
89
|
-
lastMessage = error.message;
|
|
90
|
-
}
|
|
91
|
-
}
|
|
92
|
-
throw new TimeoutError('blinkit login', timeoutSeconds, lastMessage || 'Finish OTP login in the opened Blinkit tab and retry.');
|
|
59
|
+
return probeBlinkitIdentity(page);
|
|
60
|
+
},
|
|
61
|
+
openLogin: async (page) => {
|
|
62
|
+
await page.goto(BASE);
|
|
63
|
+
await page.wait(1);
|
|
64
|
+
const result = await page.evaluate(buildOpenLoginEvaluate());
|
|
65
|
+
if (!result?.opened) throw new Error(`Blinkit login dialog did not open: ${result?.detail || 'unknown reason'}`);
|
|
93
66
|
},
|
|
94
67
|
});
|
|
95
68
|
|
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import { describe, expect, it } from 'vitest';
|
|
1
|
+
import { describe, expect, it, vi } from 'vitest';
|
|
2
2
|
import { ArgumentError } from '@agentrhq/webcmd/errors';
|
|
3
3
|
import { getRegistry } from '@agentrhq/webcmd/registry';
|
|
4
4
|
import { __test__ as authTest } from './auth.js';
|
|
@@ -143,11 +143,48 @@ describe('blinkit helpers', () => {
|
|
|
143
143
|
|
|
144
144
|
describe('blinkit registry shape', () => {
|
|
145
145
|
it('registers the buying-path commands', () => {
|
|
146
|
-
for (const name of ['login', 'location', 'search', 'product', 'add-to-cart', 'cart', 'checkout', 'place-order']) {
|
|
146
|
+
for (const name of ['login', 'whoami', 'location', 'search', 'product', 'add-to-cart', 'cart', 'checkout', 'place-order']) {
|
|
147
147
|
expect(getRegistry().get(`blinkit/${name}`)).toBeDefined();
|
|
148
148
|
}
|
|
149
149
|
});
|
|
150
150
|
|
|
151
|
+
it('opens login without waiting for manual authentication', async () => {
|
|
152
|
+
const login = getRegistry().get('blinkit/login');
|
|
153
|
+
const whoami = getRegistry().get('blinkit/whoami');
|
|
154
|
+
const page = {
|
|
155
|
+
goto: vi.fn().mockResolvedValue(undefined),
|
|
156
|
+
wait: vi.fn().mockResolvedValue(undefined),
|
|
157
|
+
evaluate: vi.fn()
|
|
158
|
+
.mockResolvedValueOnce({ kind: 'auth', detail: 'Blinkit login button is still visible' })
|
|
159
|
+
.mockResolvedValueOnce({ opened: true }),
|
|
160
|
+
};
|
|
161
|
+
|
|
162
|
+
expect(login.args).toEqual([]);
|
|
163
|
+
expect(login.columns).toEqual(expect.arrayContaining(['action', 'verify_command']));
|
|
164
|
+
expect(whoami).toBeDefined();
|
|
165
|
+
await expect(login.func(page, {})).resolves.toEqual([expect.objectContaining({
|
|
166
|
+
status: 'action_required',
|
|
167
|
+
logged_in: false,
|
|
168
|
+
site: 'blinkit',
|
|
169
|
+
verify_command: 'webcmd blinkit whoami',
|
|
170
|
+
})]);
|
|
171
|
+
expect(page.wait).not.toHaveBeenCalledWith(2);
|
|
172
|
+
});
|
|
173
|
+
|
|
174
|
+
it('rejects when the Blinkit login dialog cannot be opened', async () => {
|
|
175
|
+
const login = getRegistry().get('blinkit/login');
|
|
176
|
+
const page = {
|
|
177
|
+
goto: vi.fn().mockResolvedValue(undefined),
|
|
178
|
+
wait: vi.fn().mockResolvedValue(undefined),
|
|
179
|
+
evaluate: vi.fn()
|
|
180
|
+
.mockResolvedValueOnce({ kind: 'auth', detail: 'login required' })
|
|
181
|
+
.mockResolvedValueOnce({ opened: false, detail: 'login_button_missing' }),
|
|
182
|
+
};
|
|
183
|
+
|
|
184
|
+
await expect(login.func(page, {}))
|
|
185
|
+
.rejects.toThrow('Blinkit login dialog did not open: login_button_missing');
|
|
186
|
+
});
|
|
187
|
+
|
|
151
188
|
it('marks only cart-changing commands as write', () => {
|
|
152
189
|
expect(getRegistry().get('blinkit/search').access).toBe('read');
|
|
153
190
|
expect(getRegistry().get('blinkit/product').access).toBe('read');
|
package/clis/chatgpt/auth.js
CHANGED
|
@@ -43,10 +43,4 @@ registerSiteAuthCommands({
|
|
|
43
43
|
columns: ['user_id', 'name'],
|
|
44
44
|
quickCheck: hasChatgptSessionCookie,
|
|
45
45
|
verify: verifyChatgptIdentity,
|
|
46
|
-
poll: async (page) => {
|
|
47
|
-
if (!await hasChatgptSessionCookie(page)) {
|
|
48
|
-
throw new AuthRequiredError('chatgpt.com', 'Waiting for ChatGPT session cookie');
|
|
49
|
-
}
|
|
50
|
-
return verifyChatgptIdentity(page);
|
|
51
|
-
},
|
|
52
46
|
});
|
package/clis/claude/auth.js
CHANGED
|
@@ -46,10 +46,4 @@ registerSiteAuthCommands({
|
|
|
46
46
|
columns: ['user_id', 'org_name', 'org_uuid'],
|
|
47
47
|
quickCheck: hasClaudeSessionCookie,
|
|
48
48
|
verify: verifyClaudeIdentity,
|
|
49
|
-
poll: async (page) => {
|
|
50
|
-
if (!await hasClaudeSessionCookie(page)) {
|
|
51
|
-
throw new AuthRequiredError('claude.ai', 'Waiting for Claude sessionKey cookie');
|
|
52
|
-
}
|
|
53
|
-
return verifyClaudeIdentity(page);
|
|
54
|
-
},
|
|
55
49
|
});
|
package/clis/coupang/auth.js
CHANGED
|
@@ -39,10 +39,4 @@ registerSiteAuthCommands({
|
|
|
39
39
|
loginUrl: 'https://login.coupang.com/login/login.pang',
|
|
40
40
|
columns: ['name'],
|
|
41
41
|
verify: verifyCoupangIdentity,
|
|
42
|
-
poll: async (page) => {
|
|
43
|
-
if (!await hasCoupangSessionCookie(page)) {
|
|
44
|
-
throw new AuthRequiredError('coupang.com', 'Waiting for Coupang session cookies');
|
|
45
|
-
}
|
|
46
|
-
return verifyCoupangIdentity(page);
|
|
47
|
-
},
|
|
48
42
|
});
|
package/clis/district/auth.js
CHANGED
|
@@ -37,8 +37,6 @@ registerSiteAuthCommands({
|
|
|
37
37
|
await page.wait(1);
|
|
38
38
|
return profileProbe(page);
|
|
39
39
|
},
|
|
40
|
-
// While the OTP modal is open, probe without navigating away from it.
|
|
41
|
-
poll: async (page) => profileProbe(page),
|
|
42
40
|
quickCheck: async (page) => {
|
|
43
41
|
try {
|
|
44
42
|
return { logged_in: true, ...await profileProbe(page) };
|
package/clis/facebook/auth.js
CHANGED
|
@@ -34,10 +34,4 @@ registerSiteAuthCommands({
|
|
|
34
34
|
columns: ['user_id', 'vanity', 'profile_url'],
|
|
35
35
|
quickCheck: hasFacebookCUserCookie,
|
|
36
36
|
verify: verifyFacebookIdentity,
|
|
37
|
-
poll: async (page) => {
|
|
38
|
-
if (!await hasFacebookCUserCookie(page)) {
|
|
39
|
-
throw new AuthRequiredError('www.facebook.com', 'Waiting for Facebook c_user cookie');
|
|
40
|
-
}
|
|
41
|
-
return verifyFacebookIdentity(page);
|
|
42
|
-
},
|
|
43
37
|
});
|
package/clis/gemini/auth.js
CHANGED
|
@@ -39,10 +39,4 @@ registerSiteAuthCommands({
|
|
|
39
39
|
columns: ['name'],
|
|
40
40
|
quickCheck: hasGoogleSessionCookie,
|
|
41
41
|
verify: verifyGeminiIdentity,
|
|
42
|
-
poll: async (page) => {
|
|
43
|
-
if (!await hasGoogleSessionCookie(page)) {
|
|
44
|
-
throw new AuthRequiredError('gemini.google.com', 'Waiting for Google session cookies');
|
|
45
|
-
}
|
|
46
|
-
return verifyGeminiIdentity(page);
|
|
47
|
-
},
|
|
48
42
|
});
|
package/clis/github/auth.js
CHANGED
|
@@ -35,10 +35,4 @@ registerSiteAuthCommands({
|
|
|
35
35
|
columns: ['id', 'username', 'name', 'url'],
|
|
36
36
|
quickCheck: hasGithubSessionCookies,
|
|
37
37
|
verify: verifyGithubIdentity,
|
|
38
|
-
poll: async (page) => {
|
|
39
|
-
if (!await hasGithubSessionCookies(page)) {
|
|
40
|
-
throw new AuthRequiredError('github.com', 'Waiting for GitHub session cookies');
|
|
41
|
-
}
|
|
42
|
-
return verifyGithubIdentity(page);
|
|
43
|
-
},
|
|
44
38
|
});
|
package/clis/grok/auth.js
CHANGED
|
@@ -43,10 +43,4 @@ registerSiteAuthCommands({
|
|
|
43
43
|
columns: ['user_id', 'name'],
|
|
44
44
|
quickCheck: hasGrokSessionCookie,
|
|
45
45
|
verify: verifyGrokIdentity,
|
|
46
|
-
poll: async (page) => {
|
|
47
|
-
if (!await hasGrokSessionCookie(page)) {
|
|
48
|
-
throw new AuthRequiredError('grok.com', 'Waiting for Grok session cookie');
|
|
49
|
-
}
|
|
50
|
-
return verifyGrokIdentity(page);
|
|
51
|
-
},
|
|
52
46
|
});
|
|
@@ -1,6 +1,7 @@
|
|
|
1
1
|
import { afterEach, describe, expect, it, vi } from 'vitest';
|
|
2
2
|
import { getRegistry } from '@agentrhq/webcmd/registry';
|
|
3
3
|
import { ArgumentError, EmptyResultError } from '@agentrhq/webcmd/errors';
|
|
4
|
+
import { executePipeline } from '@agentrhq/webcmd/pipeline';
|
|
4
5
|
import './top.js';
|
|
5
6
|
import './best.js';
|
|
6
7
|
import './ask.js';
|
|
@@ -34,10 +35,28 @@ describe('hackernews listing adapters expose item id', () => {
|
|
|
34
35
|
expect(cmd?.columns).toEqual(['rank', 'id', 'title', 'author', 'url']);
|
|
35
36
|
expect(cmd?.pipeline?.[5]?.map).toMatchObject({
|
|
36
37
|
id: '${{ item.id }}',
|
|
37
|
-
url: '${{ item.url }}',
|
|
38
|
+
url: '${{ item.url || "https://news.ycombinator.com/item?id=" + item.id }}',
|
|
38
39
|
});
|
|
39
40
|
});
|
|
40
41
|
|
|
42
|
+
it('hackernews/jobs preserves external URLs and falls back to item pages', async () => {
|
|
43
|
+
const jobs = {
|
|
44
|
+
48995037: { id: 48995037, title: 'Text-only job', by: 'author' },
|
|
45
|
+
48954074: { id: 48954074, title: 'External job', by: 'author', url: 'https://example.com/job' },
|
|
46
|
+
};
|
|
47
|
+
vi.stubGlobal('fetch', vi.fn(async (url) => new Response(JSON.stringify(
|
|
48
|
+
String(url).endsWith('jobstories.json') ? [48995037, 48954074] : jobs[String(url).match(/item\/(\d+)\.json$/)?.[1]],
|
|
49
|
+
), { status: 200 })));
|
|
50
|
+
|
|
51
|
+
const cmd = getRegistry().get('hackernews/jobs');
|
|
52
|
+
const result = await executePipeline(null, cmd?.pipeline ?? [], { args: { limit: 2 } });
|
|
53
|
+
|
|
54
|
+
expect(result).toEqual([
|
|
55
|
+
{ rank: 1, id: 48995037, title: 'Text-only job', author: 'author', url: 'https://news.ycombinator.com/item?id=48995037' },
|
|
56
|
+
{ rank: 2, id: 48954074, title: 'External job', author: 'author', url: 'https://example.com/job' },
|
|
57
|
+
]);
|
|
58
|
+
});
|
|
59
|
+
|
|
41
60
|
it('hackernews/search surfaces id (algolia objectID) alongside the existing columns', () => {
|
|
42
61
|
const cmd = getRegistry().get('hackernews/search');
|
|
43
62
|
expect(cmd?.columns).toEqual(['rank', 'id', 'title', 'score', 'author', 'comments', 'url']);
|
package/clis/hackernews/jobs.js
CHANGED
package/clis/hf/auth.js
CHANGED
|
@@ -1,8 +1,8 @@
|
|
|
1
1
|
import { AuthRequiredError, CommandExecutionError } from '@agentrhq/webcmd/errors';
|
|
2
2
|
import { registerSiteAuthCommands } from '../_shared/site-auth.js';
|
|
3
3
|
|
|
4
|
-
// Hugging Face's `token` cookie is httpOnly;
|
|
5
|
-
//
|
|
4
|
+
// Hugging Face's `token` cookie is httpOnly; use the documented
|
|
5
|
+
// /api/whoami-v2 endpoint (401 when anonymous) via a no-nav probe.
|
|
6
6
|
const WHOAMI_PROBE = `(async () => {
|
|
7
7
|
try {
|
|
8
8
|
const r = await fetch('/api/whoami-v2', { credentials: 'include', headers: { Accept: 'application/json' } });
|
|
@@ -33,9 +33,4 @@ registerSiteAuthCommands({
|
|
|
33
33
|
loginUrl: 'https://huggingface.co/login',
|
|
34
34
|
columns: ['username', 'fullname', 'type'],
|
|
35
35
|
verify: verifyHfIdentity,
|
|
36
|
-
poll: async (page) => {
|
|
37
|
-
const probe = await page.evaluate(WHOAMI_PROBE);
|
|
38
|
-
if (!probe?.ok) throw new AuthRequiredError('huggingface.co', 'Waiting for Hugging Face login');
|
|
39
|
-
return { username: probe.username, fullname: probe.fullname, type: probe.type };
|
|
40
|
-
},
|
|
41
36
|
});
|
package/clis/instagram/auth.js
CHANGED
|
@@ -48,10 +48,4 @@ registerSiteAuthCommands({
|
|
|
48
48
|
columns: ['user_id', 'username', 'full_name'],
|
|
49
49
|
quickCheck: hasInstagramSessionCookie,
|
|
50
50
|
verify: verifyInstagramIdentity,
|
|
51
|
-
poll: async (page) => {
|
|
52
|
-
if (!await hasInstagramSessionCookie(page)) {
|
|
53
|
-
throw new AuthRequiredError('www.instagram.com', 'Waiting for Instagram sessionid cookie');
|
|
54
|
-
}
|
|
55
|
-
return verifyInstagramIdentity(page);
|
|
56
|
-
},
|
|
57
51
|
});
|
package/clis/linkedin/auth.js
CHANGED
|
@@ -53,10 +53,4 @@ registerSiteAuthCommands({
|
|
|
53
53
|
columns: ['public_id', 'plain_id', 'name'],
|
|
54
54
|
quickCheck: hasLinkedinSessionCookie,
|
|
55
55
|
verify: verifyLinkedinIdentity,
|
|
56
|
-
poll: async (page) => {
|
|
57
|
-
if (!await hasLinkedinSessionCookie(page)) {
|
|
58
|
-
throw new AuthRequiredError('linkedin.com', 'Waiting for LinkedIn li_at cookie');
|
|
59
|
-
}
|
|
60
|
-
return verifyLinkedinIdentity(page);
|
|
61
|
-
},
|
|
62
56
|
});
|