@agentrhq/webcmd 0.2.5 → 0.3.0
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/clis/_shared/site-auth.js +3 -3
- package/clis/_shared/site-auth.test.js +4 -4
- package/clis/slock/whoami.test.js +2 -2
- package/dist/src/browser/daemon-client.d.ts +2 -1
- package/dist/src/browser/runtime/local-cloak/actions.js +4 -1
- package/dist/src/browser/runtime/local-cloak/provider.test.js +36 -0
- package/dist/src/browser/runtime/local-cloak/session-manager.d.ts +3 -2
- package/dist/src/browser/runtime/local-cloak/session-manager.js +4 -2
- package/dist/src/cli.js +5 -4
- package/dist/src/cli.test.js +11 -5
- package/dist/src/commands/auth.js +3 -2
- package/dist/src/commands/auth.test.js +6 -6
- package/dist/src/generate-release-notes-cli.test.js +5 -0
- package/dist/src/hosted/args.d.ts +9 -0
- package/dist/src/hosted/args.js +101 -0
- package/dist/src/hosted/args.test.d.ts +1 -0
- package/dist/src/hosted/args.test.js +35 -0
- package/dist/src/hosted/client.d.ts +30 -0
- package/dist/src/hosted/client.js +122 -0
- package/dist/src/hosted/client.test.d.ts +1 -0
- package/dist/src/hosted/client.test.js +119 -0
- package/dist/src/hosted/config.d.ts +50 -0
- package/dist/src/hosted/config.js +90 -0
- package/dist/src/hosted/config.test.d.ts +1 -0
- package/dist/src/hosted/config.test.js +48 -0
- package/dist/src/hosted/manifest.d.ts +9 -0
- package/dist/src/hosted/manifest.js +92 -0
- package/dist/src/hosted/manifest.test.d.ts +1 -0
- package/dist/src/hosted/manifest.test.js +46 -0
- package/dist/src/hosted/runner.d.ts +12 -0
- package/dist/src/hosted/runner.js +404 -0
- package/dist/src/hosted/runner.test.d.ts +1 -0
- package/dist/src/hosted/runner.test.js +189 -0
- package/dist/src/hosted/setup.d.ts +9 -0
- package/dist/src/hosted/setup.js +49 -0
- package/dist/src/hosted/setup.test.d.ts +1 -0
- package/dist/src/hosted/setup.test.js +68 -0
- package/dist/src/hosted/types.d.ts +97 -0
- package/dist/src/hosted/types.js +1 -0
- package/dist/src/main.js +14 -0
- package/dist/src/release-notes.d.ts +6 -1
- package/dist/src/release-notes.js +184 -4
- package/dist/src/release-notes.test.js +128 -1
- package/package.json +1 -1
- package/scripts/generate-release-notes.ts +1 -1
- package/skills/smart-search/SKILL.md +14 -3
- package/skills/webcmd-usage/SKILL.md +14 -7
- package/clis/antigravity/SKILL.md +0 -38
|
@@ -72,7 +72,7 @@ export function registerSiteAuthCommands(config) {
|
|
|
72
72
|
? { refresh: async (page, kwargs) => normalizeRefreshResult(await config.refresh(page, kwargs)) }
|
|
73
73
|
: {}),
|
|
74
74
|
},
|
|
75
|
-
func: async (page) => tryProbe(config, page, 'identity'),
|
|
75
|
+
func: async (page) => [await tryProbe(config, page, 'identity')],
|
|
76
76
|
});
|
|
77
77
|
|
|
78
78
|
cli({
|
|
@@ -92,7 +92,7 @@ export function registerSiteAuthCommands(config) {
|
|
|
92
92
|
columns: ['status', ...commandColumns(config)],
|
|
93
93
|
func: async (page, kwargs) => {
|
|
94
94
|
try {
|
|
95
|
-
return { status: 'already_logged_in', ...await tryProbe(config, page, 'identity') };
|
|
95
|
+
return [{ status: 'already_logged_in', ...await tryProbe(config, page, 'identity') }];
|
|
96
96
|
} catch (error) {
|
|
97
97
|
if (!isAuthRequired(error)) throw error;
|
|
98
98
|
}
|
|
@@ -106,7 +106,7 @@ export function registerSiteAuthCommands(config) {
|
|
|
106
106
|
await page.wait(Math.min(POLL_INTERVAL_MS / 1000, Math.max(0.2, (deadline - Date.now()) / 1000)));
|
|
107
107
|
try {
|
|
108
108
|
const identity = await tryProbe(config, page, 'poll');
|
|
109
|
-
return { status: 'login_complete', ...identity };
|
|
109
|
+
return [{ status: 'login_complete', ...identity }];
|
|
110
110
|
} catch (error) {
|
|
111
111
|
if (!isAuthRequired(error)) throw error;
|
|
112
112
|
lastAuthMessage = getErrorMessage(error);
|
|
@@ -47,11 +47,11 @@ describe('site auth command helper', () => {
|
|
|
47
47
|
const cmd = getRegistry().get('auth-helper-whoami/whoami');
|
|
48
48
|
const page = pageMock();
|
|
49
49
|
|
|
50
|
-
await expect(cmd.func(page, {})).resolves.toEqual({
|
|
50
|
+
await expect(cmd.func(page, {})).resolves.toEqual([{
|
|
51
51
|
logged_in: true,
|
|
52
52
|
site: 'auth-helper-whoami',
|
|
53
53
|
username: 'alice',
|
|
54
|
-
});
|
|
54
|
+
}]);
|
|
55
55
|
expect(page.goto).not.toHaveBeenCalled();
|
|
56
56
|
});
|
|
57
57
|
|
|
@@ -70,12 +70,12 @@ describe('site auth command helper', () => {
|
|
|
70
70
|
const cmd = getRegistry().get('auth-helper-login/login');
|
|
71
71
|
const page = pageMock();
|
|
72
72
|
|
|
73
|
-
await expect(cmd.func(page, { timeout: 1 })).resolves.toEqual({
|
|
73
|
+
await expect(cmd.func(page, { timeout: 1 })).resolves.toEqual([{
|
|
74
74
|
status: 'login_complete',
|
|
75
75
|
logged_in: true,
|
|
76
76
|
site: 'auth-helper-login',
|
|
77
77
|
username: 'alice',
|
|
78
|
-
});
|
|
78
|
+
}]);
|
|
79
79
|
expect(page.goto).toHaveBeenCalledWith('https://example.com/login');
|
|
80
80
|
expect(page.wait).toHaveBeenCalled();
|
|
81
81
|
expect(poll).toHaveBeenCalledTimes(2);
|
|
@@ -14,10 +14,10 @@ function makePage(authMe, status = 200) {
|
|
|
14
14
|
describe('slock whoami', () => {
|
|
15
15
|
const command = getRegistry().get('slock/whoami');
|
|
16
16
|
|
|
17
|
-
it('returns a normalized identity
|
|
17
|
+
it('returns a normalized identity row from /auth/me on 200', async () => {
|
|
18
18
|
const page = makePage({ id: 'u1', name: 'Alice', email: 'a@b.c' });
|
|
19
19
|
const identity = await command.func(page, {});
|
|
20
|
-
expect(identity).toEqual({ logged_in: true, site: 'slock', id: 'u1', name: 'Alice', email: 'a@b.c' });
|
|
20
|
+
expect(identity).toEqual([{ logged_in: true, site: 'slock', id: 'u1', name: 'Alice', email: 'a@b.c' }]);
|
|
21
21
|
expect(page.goto).toHaveBeenCalled();
|
|
22
22
|
});
|
|
23
23
|
});
|
|
@@ -4,7 +4,7 @@
|
|
|
4
4
|
* Provides a typed send() function that posts a Command and returns a Result.
|
|
5
5
|
*/
|
|
6
6
|
import { fetchDaemonStatus, getDaemonHealth, requestDaemonShutdown, type BrowserProfileStatus, type DaemonHealth, type DaemonStatus } from './daemon-transport.js';
|
|
7
|
-
import type { BrowserRuntimeCommand, BrowserRuntimeResult } from './protocol.js';
|
|
7
|
+
import type { BrowserRuntimeCommand, BrowserRuntimeResult, BrowserWindowMode } from './protocol.js';
|
|
8
8
|
export declare function setDaemonCommandTimeoutSeconds(seconds: number | null): void;
|
|
9
9
|
export type DaemonCommand = BrowserRuntimeCommand;
|
|
10
10
|
export type DaemonResult = BrowserRuntimeResult;
|
|
@@ -31,4 +31,5 @@ export declare function bindTab(session: string, opts?: {
|
|
|
31
31
|
preferredContextId?: string;
|
|
32
32
|
page?: string;
|
|
33
33
|
index?: number;
|
|
34
|
+
windowMode?: BrowserWindowMode;
|
|
34
35
|
}): Promise<unknown>;
|
|
@@ -44,6 +44,7 @@ async function resolveLease(manager, command) {
|
|
|
44
44
|
siteSession: command.siteSession,
|
|
45
45
|
idleTimeout: command.idleTimeout,
|
|
46
46
|
freshPage: command.freshPage,
|
|
47
|
+
windowMode: command.windowMode,
|
|
47
48
|
});
|
|
48
49
|
}
|
|
49
50
|
function execTarget(page, frameIndex, pageId) {
|
|
@@ -134,11 +135,12 @@ export async function dispatchCloakAction(manager, command) {
|
|
|
134
135
|
siteSession: command.siteSession,
|
|
135
136
|
idleTimeout: command.idleTimeout,
|
|
136
137
|
url: command.url,
|
|
138
|
+
windowMode: command.windowMode,
|
|
137
139
|
});
|
|
138
140
|
return { id: command.id, ok: true, data: { title: await lease.page.title(), url: lease.page.url() }, page: lease.pageId };
|
|
139
141
|
}
|
|
140
142
|
case 'select': {
|
|
141
|
-
const lease = await manager.selectPage({ profileId: commandProfileId(manager, command), pageId: command.page, index: command.index });
|
|
143
|
+
const lease = await manager.selectPage({ profileId: commandProfileId(manager, command), pageId: command.page, index: command.index, windowMode: command.windowMode });
|
|
142
144
|
if (!lease)
|
|
143
145
|
return { id: command.id, ok: false, errorCode: 'runtime_command_failed', error: 'Tab not found' };
|
|
144
146
|
return { id: command.id, ok: true, data: { selected: true, url: lease.page.url() }, page: lease.pageId };
|
|
@@ -217,6 +219,7 @@ export async function dispatchCloakAction(manager, command) {
|
|
|
217
219
|
surface: command.surface,
|
|
218
220
|
siteSession: command.siteSession,
|
|
219
221
|
idleTimeout: command.idleTimeout,
|
|
222
|
+
windowMode: command.windowMode,
|
|
220
223
|
pageId: command.page,
|
|
221
224
|
index: command.index,
|
|
222
225
|
});
|
|
@@ -231,6 +231,42 @@ describe('LocalCloakRuntimeProvider', () => {
|
|
|
231
231
|
.resolves.toMatchObject({ id: 'close', ok: true, data: { closed: created.page } });
|
|
232
232
|
expect(pages[1].close).toHaveBeenCalled();
|
|
233
233
|
});
|
|
234
|
+
it('does not bring selected tabs to front in background window mode', async () => {
|
|
235
|
+
const { provider, pages } = makeProviderWithFakePage();
|
|
236
|
+
const created = await provider.dispatch({ id: 'new', action: 'tabs', op: 'new', session: 'work', surface: 'browser', url: 'https://second.example/', profileId: 'default' });
|
|
237
|
+
await expect(provider.dispatch({
|
|
238
|
+
id: 'select',
|
|
239
|
+
action: 'tabs',
|
|
240
|
+
op: 'select',
|
|
241
|
+
session: 'work',
|
|
242
|
+
surface: 'browser',
|
|
243
|
+
page: created.page,
|
|
244
|
+
profileId: 'default',
|
|
245
|
+
windowMode: 'background',
|
|
246
|
+
})).resolves.toMatchObject({ id: 'select', ok: true });
|
|
247
|
+
expect(pages[1].bringToFront).not.toHaveBeenCalled();
|
|
248
|
+
});
|
|
249
|
+
it('does not bring bound tabs to front in background window mode', async () => {
|
|
250
|
+
const { provider, pages } = makeProviderWithFakePage();
|
|
251
|
+
const created = await provider.dispatch({ id: 'new', action: 'tabs', op: 'new', session: 'source', surface: 'browser', url: 'https://second.example/', profileId: 'default' });
|
|
252
|
+
await expect(provider.dispatch({
|
|
253
|
+
id: 'bind',
|
|
254
|
+
action: 'bind',
|
|
255
|
+
session: 'target',
|
|
256
|
+
surface: 'browser',
|
|
257
|
+
page: created.page,
|
|
258
|
+
profileId: 'default',
|
|
259
|
+
windowMode: 'background',
|
|
260
|
+
})).resolves.toMatchObject({ id: 'bind', ok: true });
|
|
261
|
+
expect(pages[1].bringToFront).not.toHaveBeenCalled();
|
|
262
|
+
});
|
|
263
|
+
it('brings bound tabs to front by default', async () => {
|
|
264
|
+
const { provider, pages } = makeProviderWithFakePage();
|
|
265
|
+
const created = await provider.dispatch({ id: 'new', action: 'tabs', op: 'new', session: 'source', surface: 'browser', url: 'https://second.example/', profileId: 'default' });
|
|
266
|
+
await expect(provider.dispatch({ id: 'bind', action: 'bind', session: 'target', surface: 'browser', page: created.page, profileId: 'default' }))
|
|
267
|
+
.resolves.toMatchObject({ id: 'bind', ok: true });
|
|
268
|
+
expect(pages[1].bringToFront).toHaveBeenCalledOnce();
|
|
269
|
+
});
|
|
234
270
|
it('closes a window by page identity when command.page is provided', async () => {
|
|
235
271
|
const { provider, pages } = makeProviderWithFakePage();
|
|
236
272
|
const first = await provider.dispatch({ id: 'first', action: 'navigate', session: 'first', surface: 'browser', url: 'https://first.example/', profileId: 'default' });
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
import type { BrowserContext, Page as PlaywrightPage } from 'playwright-core';
|
|
2
2
|
import { launchPersistentContext as cloakLaunchPersistentContext } from 'cloakbrowser';
|
|
3
|
-
import type { BrowserSurface, SiteSessionMode } from '../../protocol.js';
|
|
3
|
+
import type { BrowserSurface, BrowserWindowMode, SiteSessionMode } from '../../protocol.js';
|
|
4
4
|
import { CloakNetworkCapture } from './network.js';
|
|
5
5
|
export type LaunchPersistentContext = typeof cloakLaunchPersistentContext;
|
|
6
6
|
export type RecoverLockedProfile = (userDataDir: string) => Promise<boolean>;
|
|
@@ -10,6 +10,7 @@ export interface SessionKeyInput {
|
|
|
10
10
|
surface?: BrowserSurface;
|
|
11
11
|
siteSession?: SiteSessionMode;
|
|
12
12
|
idleTimeout?: number;
|
|
13
|
+
windowMode?: BrowserWindowMode;
|
|
13
14
|
/** Discard the existing leased page (if any) and create a new one under the same lease. */
|
|
14
15
|
freshPage?: boolean;
|
|
15
16
|
}
|
|
@@ -61,7 +62,7 @@ export declare class CloakSessionManager {
|
|
|
61
62
|
newPage(input: SessionKeyInput & {
|
|
62
63
|
url?: string;
|
|
63
64
|
}): Promise<CloakPageLease>;
|
|
64
|
-
selectPage(input: Pick<SessionKeyInput, 'profileId'> & {
|
|
65
|
+
selectPage(input: Pick<SessionKeyInput, 'profileId' | 'windowMode'> & {
|
|
65
66
|
pageId?: string;
|
|
66
67
|
index?: number;
|
|
67
68
|
}): Promise<CloakPageLease | null>;
|
|
@@ -152,7 +152,8 @@ export class CloakSessionManager {
|
|
|
152
152
|
if (!match)
|
|
153
153
|
return null;
|
|
154
154
|
const [leaseKey, entry] = match;
|
|
155
|
-
|
|
155
|
+
if (input.windowMode !== 'background')
|
|
156
|
+
await entry.page.bringToFront?.().catch(() => { });
|
|
156
157
|
runtime.selectedPageId = entry.pageId;
|
|
157
158
|
runtime.lastSeenAt = Date.now();
|
|
158
159
|
return { profileId, leaseKey, context: runtime.context, page: entry.page, pageId: entry.pageId };
|
|
@@ -183,7 +184,8 @@ export class CloakSessionManager {
|
|
|
183
184
|
entry.siteSession = input.siteSession;
|
|
184
185
|
entry.idleTimeout = input.idleTimeout;
|
|
185
186
|
runtime.pages.set(canonicalKey, entry);
|
|
186
|
-
|
|
187
|
+
if (input.windowMode !== 'background')
|
|
188
|
+
await entry.page.bringToFront?.().catch(() => { });
|
|
187
189
|
this.refreshIdleTimer(runtime, canonicalKey, entry);
|
|
188
190
|
runtime.selectedPageId = entry.pageId;
|
|
189
191
|
runtime.lastSeenAt = Date.now();
|
package/dist/src/cli.js
CHANGED
|
@@ -1022,11 +1022,12 @@ Examples:
|
|
|
1022
1022
|
const profileSelection = getBrowserProfileSelection(command);
|
|
1023
1023
|
const contextId = profileSelection?.contextId;
|
|
1024
1024
|
const routing = profileRouteParams(profileSelection);
|
|
1025
|
+
const windowMode = getBrowserWindowMode(command, 'foreground');
|
|
1025
1026
|
try {
|
|
1026
1027
|
const { BrowserBridge } = await import('./browser/index.js');
|
|
1027
1028
|
const bridge = new BrowserBridge();
|
|
1028
|
-
await bridge.connect({ timeout: DEFAULT_BROWSER_CONNECT_TIMEOUT, session, surface: 'browser', ...routing });
|
|
1029
|
-
await fn({ session, contextId, routing }, opts);
|
|
1029
|
+
await bridge.connect({ timeout: DEFAULT_BROWSER_CONNECT_TIMEOUT, session, surface: 'browser', ...routing, windowMode });
|
|
1030
|
+
await fn({ session, contextId, routing, windowMode }, opts);
|
|
1030
1031
|
}
|
|
1031
1032
|
catch (err) {
|
|
1032
1033
|
if (err instanceof BrowserCommandError) {
|
|
@@ -1044,7 +1045,7 @@ Examples:
|
|
|
1044
1045
|
.description('Bind an existing Cloak runtime tab to the browser session named by <session>')
|
|
1045
1046
|
.option('--page <id>', 'Cloak tab page id from `webcmd browser <session> tab list`')
|
|
1046
1047
|
.option('--index <n>', 'Cloak tab index from `webcmd browser <session> tab list`')
|
|
1047
|
-
.action(browserSessionCommandAction(async ({ session, contextId, routing }, opts) => {
|
|
1048
|
+
.action(browserSessionCommandAction(async ({ session, contextId, routing, windowMode }, opts) => {
|
|
1048
1049
|
const page = typeof opts.page === 'string' && opts.page.trim() ? opts.page.trim() : undefined;
|
|
1049
1050
|
const rawIndex = typeof opts.index === 'string' && opts.index.trim() ? opts.index.trim() : undefined;
|
|
1050
1051
|
if ((page && rawIndex) || (!page && !rawIndex)) {
|
|
@@ -1054,7 +1055,7 @@ Examples:
|
|
|
1054
1055
|
throw new BrowserCommandError('--index must be a non-negative integer.', 'invalid_request');
|
|
1055
1056
|
}
|
|
1056
1057
|
const index = rawIndex === undefined ? undefined : Number.parseInt(rawIndex, 10);
|
|
1057
|
-
const data = await bindTab(session, { ...routing, ...(page && { page }), ...(index !== undefined && { index }) });
|
|
1058
|
+
const data = await bindTab(session, { ...routing, ...(page && { page }), ...(index !== undefined && { index }), windowMode });
|
|
1058
1059
|
saveBrowserTargetState(undefined, getBrowserScope(session, contextId));
|
|
1059
1060
|
console.log(JSON.stringify({ session, ...((data && typeof data === 'object') ? data : { data }) }, null, 2));
|
|
1060
1061
|
}));
|
package/dist/src/cli.test.js
CHANGED
|
@@ -1109,8 +1109,8 @@ describe('browser tab targeting commands', () => {
|
|
|
1109
1109
|
it('binds an existing Cloak tab by page id into a browser session', async () => {
|
|
1110
1110
|
const program = createProgram('', '');
|
|
1111
1111
|
await program.parseAsync(['node', 'webcmd', 'browser', '--session', 'test', 'bind', '--page', 'tab-2']);
|
|
1112
|
-
expect(mockBrowserConnect).toHaveBeenCalledWith({ timeout: 45, session: 'test', surface: 'browser' });
|
|
1113
|
-
expect(mockBindTab).toHaveBeenCalledWith('test', { page: 'tab-2' });
|
|
1112
|
+
expect(mockBrowserConnect).toHaveBeenCalledWith({ timeout: 45, session: 'test', surface: 'browser', windowMode: 'foreground' });
|
|
1113
|
+
expect(mockBindTab).toHaveBeenCalledWith('test', { page: 'tab-2', windowMode: 'foreground' });
|
|
1114
1114
|
const out = lastJsonLog();
|
|
1115
1115
|
expect(out.session).toBe('test');
|
|
1116
1116
|
expect(out.url).toBe('https://user.example/inbox');
|
|
@@ -1118,11 +1118,17 @@ describe('browser tab targeting commands', () => {
|
|
|
1118
1118
|
it('binds an existing Cloak tab by index into a browser session', async () => {
|
|
1119
1119
|
const program = createProgram('', '');
|
|
1120
1120
|
await program.parseAsync(['node', 'webcmd', 'browser', '--session', 'test', 'bind', '--index', '1']);
|
|
1121
|
-
expect(mockBrowserConnect).toHaveBeenCalledWith({ timeout: 45, session: 'test', surface: 'browser' });
|
|
1122
|
-
expect(mockBindTab).toHaveBeenCalledWith('test', { index: 1 });
|
|
1121
|
+
expect(mockBrowserConnect).toHaveBeenCalledWith({ timeout: 45, session: 'test', surface: 'browser', windowMode: 'foreground' });
|
|
1122
|
+
expect(mockBindTab).toHaveBeenCalledWith('test', { index: 1, windowMode: 'foreground' });
|
|
1123
1123
|
const out = lastJsonLog();
|
|
1124
1124
|
expect(out.session).toBe('test');
|
|
1125
1125
|
});
|
|
1126
|
+
it('passes background window mode when binding a Cloak tab', async () => {
|
|
1127
|
+
const program = createProgram('', '');
|
|
1128
|
+
await program.parseAsync(['node', 'webcmd', 'browser', '--session', 'test', '--window', 'background', 'bind', '--page', 'tab-2']);
|
|
1129
|
+
expect(mockBrowserConnect).toHaveBeenCalledWith({ timeout: 45, session: 'test', surface: 'browser', windowMode: 'background' });
|
|
1130
|
+
expect(mockBindTab).toHaveBeenCalledWith('test', { page: 'tab-2', windowMode: 'background' });
|
|
1131
|
+
});
|
|
1126
1132
|
it('requires an explicit Cloak tab target for bind', async () => {
|
|
1127
1133
|
const program = createProgram('', '');
|
|
1128
1134
|
await program.parseAsync(['node', 'webcmd', 'browser', '--session', 'test', 'bind']);
|
|
@@ -1237,7 +1243,7 @@ describe('browser tab targeting commands', () => {
|
|
|
1237
1243
|
it('unbinds a session through the daemon close-window command', async () => {
|
|
1238
1244
|
const program = createProgram('', '');
|
|
1239
1245
|
await program.parseAsync(['node', 'webcmd', 'browser', '--session', 'test', 'unbind']);
|
|
1240
|
-
expect(mockBrowserConnect).toHaveBeenCalledWith({ timeout: 45, session: 'test', surface: 'browser' });
|
|
1246
|
+
expect(mockBrowserConnect).toHaveBeenCalledWith({ timeout: 45, session: 'test', surface: 'browser', windowMode: 'foreground' });
|
|
1241
1247
|
expect(mockSendCommand).toHaveBeenCalledWith('close-window', { session: 'test', surface: 'browser' });
|
|
1242
1248
|
const out = lastJsonLog();
|
|
1243
1249
|
expect(out).toEqual({ unbound: true, session: 'test' });
|
|
@@ -117,9 +117,10 @@ function safeIdentityValue(value) {
|
|
|
117
117
|
return '';
|
|
118
118
|
}
|
|
119
119
|
function identitySummary(result) {
|
|
120
|
-
|
|
120
|
+
const first = Array.isArray(result) ? result[0] : result;
|
|
121
|
+
if (!first || typeof first !== 'object' || Array.isArray(first))
|
|
121
122
|
return '';
|
|
122
|
-
const row =
|
|
123
|
+
const row = first;
|
|
123
124
|
const blocked = /(?:email|phone|real.?name|first.?name|last.?name|cookie|token|session|secret|password|csrf|jwt|bearer|wt2)/i;
|
|
124
125
|
for (const key of ['username', 'handle', 'user_id', 'id', 'name', 'nickname', 'user_type', 'url']) {
|
|
125
126
|
if (blocked.test(key))
|
|
@@ -74,12 +74,12 @@ describe('auth status collection', () => {
|
|
|
74
74
|
});
|
|
75
75
|
it('runs full whoami with --full and returns a safe identity summary', async () => {
|
|
76
76
|
registerWhoami('gamma', {
|
|
77
|
-
identity: {
|
|
78
|
-
|
|
79
|
-
|
|
80
|
-
|
|
81
|
-
|
|
82
|
-
|
|
77
|
+
identity: [{
|
|
78
|
+
logged_in: true,
|
|
79
|
+
site: 'gamma',
|
|
80
|
+
email: 'hidden@example.com',
|
|
81
|
+
username: 'public-handle',
|
|
82
|
+
}],
|
|
83
83
|
});
|
|
84
84
|
const rows = await collectAuthStatus({ sites: 'gamma', full: true });
|
|
85
85
|
expect(rows).toEqual([
|
|
@@ -68,6 +68,11 @@ describe('runGenerateReleaseNotes', () => {
|
|
|
68
68
|
'## Highlights',
|
|
69
69
|
'- Better summaries.',
|
|
70
70
|
'',
|
|
71
|
+
'## Contributors',
|
|
72
|
+
'<a href="https://github.com/alice" title="@alice"><img src="https://github.com/alice.png?size=64" width="64" height="64" alt="@alice" /></a>',
|
|
73
|
+
'',
|
|
74
|
+
'[@alice](https://github.com/alice)',
|
|
75
|
+
'',
|
|
71
76
|
].join('\n'),
|
|
72
77
|
stderr: '',
|
|
73
78
|
});
|
|
@@ -0,0 +1,9 @@
|
|
|
1
|
+
import type { HostedCommand } from './types.js';
|
|
2
|
+
export interface ParsedHostedInvocation {
|
|
3
|
+
args: Record<string, unknown>;
|
|
4
|
+
format: string;
|
|
5
|
+
trace: string;
|
|
6
|
+
profile?: string;
|
|
7
|
+
help: boolean;
|
|
8
|
+
}
|
|
9
|
+
export declare function parseHostedInvocation(command: HostedCommand, argv: string[]): ParsedHostedInvocation;
|
|
@@ -0,0 +1,101 @@
|
|
|
1
|
+
import { ArgumentError } from '../errors.js';
|
|
2
|
+
export function parseHostedInvocation(command, argv) {
|
|
3
|
+
const args = {};
|
|
4
|
+
const positional = command.args.filter((arg) => arg.positional);
|
|
5
|
+
const named = new Map(command.args.filter((arg) => !arg.positional).map((arg) => [arg.name, arg]));
|
|
6
|
+
let positionalIndex = 0;
|
|
7
|
+
let format = String(command.defaultFormat || 'table');
|
|
8
|
+
let trace = 'off';
|
|
9
|
+
let profile;
|
|
10
|
+
let help = false;
|
|
11
|
+
for (let i = 0; i < argv.length; i++) {
|
|
12
|
+
const token = argv[i];
|
|
13
|
+
if (token === '--') {
|
|
14
|
+
for (const rest of argv.slice(i + 1)) {
|
|
15
|
+
const arg = positional[positionalIndex++];
|
|
16
|
+
if (!arg)
|
|
17
|
+
throw new ArgumentError(`Unexpected positional argument: ${rest}`);
|
|
18
|
+
args[arg.name] = coerceValue(rest, arg.type);
|
|
19
|
+
}
|
|
20
|
+
break;
|
|
21
|
+
}
|
|
22
|
+
if (token === '-h' || token === '--help') {
|
|
23
|
+
help = true;
|
|
24
|
+
continue;
|
|
25
|
+
}
|
|
26
|
+
if (token === '-f' || token === '--format') {
|
|
27
|
+
format = readValue(argv, ++i, token);
|
|
28
|
+
continue;
|
|
29
|
+
}
|
|
30
|
+
if (token === '--trace') {
|
|
31
|
+
trace = readValue(argv, ++i, token);
|
|
32
|
+
continue;
|
|
33
|
+
}
|
|
34
|
+
if (token === '--profile') {
|
|
35
|
+
profile = readValue(argv, ++i, token);
|
|
36
|
+
continue;
|
|
37
|
+
}
|
|
38
|
+
if (token.startsWith('--')) {
|
|
39
|
+
const eqIndex = token.indexOf('=');
|
|
40
|
+
const rawName = token.slice(2, eqIndex === -1 ? undefined : eqIndex);
|
|
41
|
+
const arg = named.get(rawName);
|
|
42
|
+
if (!arg)
|
|
43
|
+
throw new ArgumentError(`Unknown option for hosted command ${command.command}: --${rawName}`);
|
|
44
|
+
const inlineValue = eqIndex === -1 ? undefined : token.slice(eqIndex + 1);
|
|
45
|
+
if (arg.type === 'bool' || arg.type === 'boolean') {
|
|
46
|
+
args[arg.name] = inlineValue === undefined ? true : coerceValue(inlineValue, arg.type);
|
|
47
|
+
}
|
|
48
|
+
else {
|
|
49
|
+
args[arg.name] = coerceValue(inlineValue ?? readValue(argv, ++i, token), arg.type);
|
|
50
|
+
}
|
|
51
|
+
continue;
|
|
52
|
+
}
|
|
53
|
+
const arg = positional[positionalIndex++];
|
|
54
|
+
if (!arg)
|
|
55
|
+
throw new ArgumentError(`Unexpected positional argument: ${token}`);
|
|
56
|
+
args[arg.name] = coerceValue(token, arg.type);
|
|
57
|
+
}
|
|
58
|
+
for (const arg of command.args) {
|
|
59
|
+
if (args[arg.name] === undefined && arg.default !== undefined)
|
|
60
|
+
args[arg.name] = arg.default;
|
|
61
|
+
if (arg.required && args[arg.name] === undefined) {
|
|
62
|
+
throw new ArgumentError(`Missing required argument for hosted command ${command.command}: ${arg.name}`);
|
|
63
|
+
}
|
|
64
|
+
}
|
|
65
|
+
return {
|
|
66
|
+
args,
|
|
67
|
+
format,
|
|
68
|
+
trace,
|
|
69
|
+
...(profile ? { profile } : {}),
|
|
70
|
+
help,
|
|
71
|
+
};
|
|
72
|
+
}
|
|
73
|
+
function readValue(argv, index, flag) {
|
|
74
|
+
const value = argv[index];
|
|
75
|
+
if (value === undefined || value.startsWith('--')) {
|
|
76
|
+
throw new ArgumentError(`${flag} requires a value.`);
|
|
77
|
+
}
|
|
78
|
+
return value;
|
|
79
|
+
}
|
|
80
|
+
function coerceValue(value, type) {
|
|
81
|
+
if (type === 'int') {
|
|
82
|
+
const parsed = Number.parseInt(value, 10);
|
|
83
|
+
if (!Number.isFinite(parsed))
|
|
84
|
+
throw new ArgumentError(`Expected integer value, got "${value}".`);
|
|
85
|
+
return parsed;
|
|
86
|
+
}
|
|
87
|
+
if (type === 'number') {
|
|
88
|
+
const parsed = Number(value);
|
|
89
|
+
if (!Number.isFinite(parsed))
|
|
90
|
+
throw new ArgumentError(`Expected number value, got "${value}".`);
|
|
91
|
+
return parsed;
|
|
92
|
+
}
|
|
93
|
+
if (type === 'bool' || type === 'boolean') {
|
|
94
|
+
if (value === 'true' || value === '1')
|
|
95
|
+
return true;
|
|
96
|
+
if (value === 'false' || value === '0')
|
|
97
|
+
return false;
|
|
98
|
+
throw new ArgumentError(`Expected boolean value, got "${value}".`);
|
|
99
|
+
}
|
|
100
|
+
return value;
|
|
101
|
+
}
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export {};
|
|
@@ -0,0 +1,35 @@
|
|
|
1
|
+
import { describe, expect, it } from 'vitest';
|
|
2
|
+
import { parseHostedInvocation } from './args.js';
|
|
3
|
+
const command = {
|
|
4
|
+
site: 'github',
|
|
5
|
+
name: 'search',
|
|
6
|
+
command: 'github/search',
|
|
7
|
+
description: 'Search GitHub',
|
|
8
|
+
access: 'read',
|
|
9
|
+
strategy: 'PUBLIC',
|
|
10
|
+
browser: false,
|
|
11
|
+
args: [
|
|
12
|
+
{ name: 'query', positional: true, required: true, type: 'string' },
|
|
13
|
+
{ name: 'limit', type: 'int', default: 10 },
|
|
14
|
+
{ name: 'include-forks', type: 'boolean', default: false },
|
|
15
|
+
],
|
|
16
|
+
columns: ['name'],
|
|
17
|
+
};
|
|
18
|
+
describe('parseHostedInvocation', () => {
|
|
19
|
+
it('parses positional args, value flags, boolean flags, and output options', () => {
|
|
20
|
+
expect(parseHostedInvocation(command, ['webcmd', '--limit', '5', '--include-forks', '-f', 'json', '--trace', 'on']))
|
|
21
|
+
.toEqual({
|
|
22
|
+
args: {
|
|
23
|
+
query: 'webcmd',
|
|
24
|
+
limit: 5,
|
|
25
|
+
'include-forks': true,
|
|
26
|
+
},
|
|
27
|
+
format: 'json',
|
|
28
|
+
trace: 'on',
|
|
29
|
+
help: false,
|
|
30
|
+
});
|
|
31
|
+
});
|
|
32
|
+
it('rejects missing required positional args', () => {
|
|
33
|
+
expect(() => parseHostedInvocation(command, [])).toThrow('Missing required argument');
|
|
34
|
+
});
|
|
35
|
+
});
|
|
@@ -0,0 +1,30 @@
|
|
|
1
|
+
import { CliError, type ExitCode } from '../errors.js';
|
|
2
|
+
import type { HostedBrowserActionRequest, HostedBrowserActionResponse, HostedBrowserFinishRequest, HostedBrowserFinishResponse, HostedBrowserRunActionInput, HostedBrowserRunActionResponse, HostedBrowserRunRequest, HostedBrowserRunResponse, HostedExecuteResponse, HostedManifest } from './types.js';
|
|
3
|
+
export interface HostedClientOptions {
|
|
4
|
+
apiBaseUrl: string;
|
|
5
|
+
apiKey: string;
|
|
6
|
+
fetchImpl?: typeof fetch;
|
|
7
|
+
}
|
|
8
|
+
export declare class HostedClientError extends CliError {
|
|
9
|
+
constructor(code: string, message: string, help?: string, exitCode?: ExitCode);
|
|
10
|
+
}
|
|
11
|
+
export declare class HostedClient {
|
|
12
|
+
private readonly apiBaseUrl;
|
|
13
|
+
private readonly apiKey;
|
|
14
|
+
private readonly fetchImpl;
|
|
15
|
+
constructor(options: HostedClientOptions);
|
|
16
|
+
getMe(): Promise<unknown>;
|
|
17
|
+
getManifest(): Promise<HostedManifest>;
|
|
18
|
+
execute(input: {
|
|
19
|
+
command: string;
|
|
20
|
+
args: Record<string, unknown>;
|
|
21
|
+
format?: string;
|
|
22
|
+
trace?: string;
|
|
23
|
+
profile?: string;
|
|
24
|
+
}): Promise<HostedExecuteResponse>;
|
|
25
|
+
startBrowserRun(session: string, input: HostedBrowserRunRequest): Promise<HostedBrowserRunResponse>;
|
|
26
|
+
browserAction(session: string, executionId: string, input: HostedBrowserActionRequest): Promise<HostedBrowserActionResponse>;
|
|
27
|
+
finishBrowserRun(session: string, executionId: string, input: HostedBrowserFinishRequest): Promise<HostedBrowserFinishResponse>;
|
|
28
|
+
runBrowserAction(session: string, input: HostedBrowserRunActionInput): Promise<HostedBrowserRunActionResponse>;
|
|
29
|
+
private request;
|
|
30
|
+
}
|
|
@@ -0,0 +1,122 @@
|
|
|
1
|
+
import { CliError, EXIT_CODES } from '../errors.js';
|
|
2
|
+
export class HostedClientError extends CliError {
|
|
3
|
+
constructor(code, message, help, exitCode = EXIT_CODES.GENERIC_ERROR) {
|
|
4
|
+
super(code, message, help, exitCode);
|
|
5
|
+
}
|
|
6
|
+
}
|
|
7
|
+
export class HostedClient {
|
|
8
|
+
apiBaseUrl;
|
|
9
|
+
apiKey;
|
|
10
|
+
fetchImpl;
|
|
11
|
+
constructor(options) {
|
|
12
|
+
this.apiBaseUrl = options.apiBaseUrl.replace(/\/+$/, '');
|
|
13
|
+
this.apiKey = options.apiKey;
|
|
14
|
+
this.fetchImpl = options.fetchImpl ?? fetch;
|
|
15
|
+
}
|
|
16
|
+
async getMe() {
|
|
17
|
+
return this.request('/v1/me');
|
|
18
|
+
}
|
|
19
|
+
async getManifest() {
|
|
20
|
+
const body = await this.request('/v1/manifest');
|
|
21
|
+
if (!body.manifest || !Array.isArray(body.manifest.commands)) {
|
|
22
|
+
throw new HostedClientError('HOSTED_PROTOCOL', 'Webcmd Cloud returned an invalid manifest.');
|
|
23
|
+
}
|
|
24
|
+
return body.manifest;
|
|
25
|
+
}
|
|
26
|
+
async execute(input) {
|
|
27
|
+
return this.request('/v1/execute', {
|
|
28
|
+
method: 'POST',
|
|
29
|
+
body: JSON.stringify(input),
|
|
30
|
+
});
|
|
31
|
+
}
|
|
32
|
+
async startBrowserRun(session, input) {
|
|
33
|
+
return this.request(`/v1/browser/${encodeURIComponent(session)}/runs`, {
|
|
34
|
+
method: 'POST',
|
|
35
|
+
body: JSON.stringify(input),
|
|
36
|
+
});
|
|
37
|
+
}
|
|
38
|
+
async browserAction(session, executionId, input) {
|
|
39
|
+
return this.request(`/v1/browser/${encodeURIComponent(session)}/runs/${encodeURIComponent(executionId)}/actions`, {
|
|
40
|
+
method: 'POST',
|
|
41
|
+
body: JSON.stringify(input),
|
|
42
|
+
});
|
|
43
|
+
}
|
|
44
|
+
async finishBrowserRun(session, executionId, input) {
|
|
45
|
+
return this.request(`/v1/browser/${encodeURIComponent(session)}/runs/${encodeURIComponent(executionId)}/finish`, {
|
|
46
|
+
method: 'POST',
|
|
47
|
+
body: JSON.stringify(input),
|
|
48
|
+
});
|
|
49
|
+
}
|
|
50
|
+
async runBrowserAction(session, input) {
|
|
51
|
+
const { command, trace, windowMode, action, profile, args } = input;
|
|
52
|
+
const run = await this.startBrowserRun(session, {
|
|
53
|
+
command,
|
|
54
|
+
args,
|
|
55
|
+
...(profile !== undefined ? { profile } : {}),
|
|
56
|
+
...(windowMode !== undefined ? { windowMode } : {}),
|
|
57
|
+
...(trace !== undefined ? { trace } : {}),
|
|
58
|
+
});
|
|
59
|
+
try {
|
|
60
|
+
const actionResponse = await this.browserAction(session, run.run.executionId, {
|
|
61
|
+
action,
|
|
62
|
+
args,
|
|
63
|
+
...(profile !== undefined ? { profile } : {}),
|
|
64
|
+
});
|
|
65
|
+
const finished = await this.finishBrowserRun(session, run.run.executionId, {
|
|
66
|
+
status: 'succeeded',
|
|
67
|
+
...(profile !== undefined ? { profile } : {}),
|
|
68
|
+
});
|
|
69
|
+
return {
|
|
70
|
+
...actionResponse,
|
|
71
|
+
run: run.run,
|
|
72
|
+
execution: finished.execution,
|
|
73
|
+
};
|
|
74
|
+
}
|
|
75
|
+
catch (error) {
|
|
76
|
+
await this.finishBrowserRun(session, run.run.executionId, {
|
|
77
|
+
status: 'failed',
|
|
78
|
+
errorCode: error instanceof HostedClientError ? error.code : 'HOSTED_BROWSER_ACTION_FAILED',
|
|
79
|
+
...(profile !== undefined ? { profile } : {}),
|
|
80
|
+
}).catch(() => undefined);
|
|
81
|
+
throw error;
|
|
82
|
+
}
|
|
83
|
+
}
|
|
84
|
+
async request(path, init = {}) {
|
|
85
|
+
const response = await this.fetchImpl(`${this.apiBaseUrl}${path}`, {
|
|
86
|
+
...init,
|
|
87
|
+
headers: {
|
|
88
|
+
accept: 'application/json',
|
|
89
|
+
...(init.body ? { 'content-type': 'application/json' } : {}),
|
|
90
|
+
authorization: `Bearer ${this.apiKey}`,
|
|
91
|
+
...(init.headers ?? {}),
|
|
92
|
+
},
|
|
93
|
+
});
|
|
94
|
+
const text = await response.text();
|
|
95
|
+
const body = text ? parseJson(text) : {};
|
|
96
|
+
if (!response.ok || isHostedError(body)) {
|
|
97
|
+
const error = isHostedError(body)
|
|
98
|
+
? body.error
|
|
99
|
+
: { code: `HTTP_${response.status}`, message: `Webcmd Cloud request failed with HTTP ${response.status}.` };
|
|
100
|
+
throw new HostedClientError(error.code || `HTTP_${response.status}`, error.message || `Webcmd Cloud request failed with HTTP ${response.status}.`, error.help ?? error.hint, normalizeExitCode(error.exitCode, response.status === 401 ? EXIT_CODES.NOPERM : EXIT_CODES.GENERIC_ERROR));
|
|
101
|
+
}
|
|
102
|
+
return body;
|
|
103
|
+
}
|
|
104
|
+
}
|
|
105
|
+
function normalizeExitCode(value, fallback) {
|
|
106
|
+
const allowed = new Set(Object.values(EXIT_CODES));
|
|
107
|
+
return value !== undefined && allowed.has(value) ? value : fallback;
|
|
108
|
+
}
|
|
109
|
+
function parseJson(text) {
|
|
110
|
+
try {
|
|
111
|
+
return JSON.parse(text);
|
|
112
|
+
}
|
|
113
|
+
catch {
|
|
114
|
+
throw new HostedClientError('HOSTED_PROTOCOL', 'Webcmd Cloud returned non-JSON response.');
|
|
115
|
+
}
|
|
116
|
+
}
|
|
117
|
+
function isHostedError(value) {
|
|
118
|
+
return !!value
|
|
119
|
+
&& typeof value === 'object'
|
|
120
|
+
&& value.ok === false
|
|
121
|
+
&& typeof value.error === 'object';
|
|
122
|
+
}
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export {};
|