@agentrhq/webcmd 0.2.0 → 0.2.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.
- package/README.md +58 -31
- package/cli-manifest.json +506 -0
- package/clis/_shared/site-auth.js +6 -1
- package/clis/district/_lib.js +566 -0
- package/clis/district/auth.js +49 -0
- package/clis/district/checkout.js +278 -0
- package/clis/district/listings.js +158 -0
- package/clis/district/locations.js +211 -0
- package/clis/district/search.js +218 -0
- package/clis/district/seats.js +233 -0
- package/clis/district/set-location.js +82 -0
- package/clis/district/showtimes.js +433 -0
- package/clis/reddit/popular.js +12 -1
- package/clis/reddit/popular.test.js +12 -3
- package/clis/twitter/delete.js +14 -6
- package/clis/twitter/delete.test.js +74 -1
- package/clis/twitter/timeline.js +3 -1
- package/clis/twitter/timeline.test.js +4 -0
- package/dist/src/browser/bridge-readiness.test.js +1 -1
- package/dist/src/browser/bridge.d.ts +2 -0
- package/dist/src/browser/bridge.js +6 -4
- package/dist/src/browser/cdp.d.ts +1 -0
- package/dist/src/browser/daemon-client.d.ts +1 -0
- package/dist/src/browser/daemon-client.js +7 -2
- package/dist/src/browser/daemon-client.test.js +33 -30
- package/dist/src/browser/daemon-transport.js +3 -19
- package/dist/src/browser/page.d.ts +5 -1
- package/dist/src/browser/page.js +16 -1
- package/dist/src/browser/profile.d.ts +9 -0
- package/dist/src/browser/profile.js +18 -7
- package/dist/src/browser/profile.test.d.ts +1 -0
- package/dist/src/browser/profile.test.js +44 -0
- package/dist/src/browser/protocol.d.ts +3 -0
- package/dist/src/browser/runtime/local-cloak/actions.js +25 -10
- package/dist/src/browser/runtime/local-cloak/session-manager.d.ts +3 -0
- package/dist/src/browser/runtime/local-cloak/session-manager.js +15 -2
- package/dist/src/browser/runtime/local-cloak/session-manager.test.js +145 -0
- package/dist/src/build-manifest.js +1 -0
- package/dist/src/build-manifest.test.js +34 -0
- package/dist/src/cli.js +133 -45
- package/dist/src/cli.test.js +2 -2
- package/dist/src/commands/daemon.test.js +13 -13
- package/dist/src/constants.d.ts +2 -3
- package/dist/src/constants.js +3 -10
- package/dist/src/daemon.js +1 -5
- package/dist/src/discovery.js +1 -0
- package/dist/src/doctor.js +12 -0
- package/dist/src/doctor.test.js +30 -3
- package/dist/src/engine.test.js +62 -0
- package/dist/src/execution.js +5 -3
- package/dist/src/external.js +19 -1
- package/dist/src/external.test.js +37 -1
- package/dist/src/main.js +0 -5
- package/dist/src/manifest-types.d.ts +2 -0
- package/dist/src/node-network.test.js +3 -3
- package/dist/src/registry.d.ts +10 -0
- package/dist/src/registry.js +5 -3
- package/dist/src/registry.test.js +25 -0
- package/dist/src/runtime-identity.test.js +0 -8
- package/dist/src/runtime.d.ts +4 -0
- package/dist/src/runtime.js +2 -0
- package/dist/src/skills.d.ts +23 -5
- package/dist/src/skills.js +87 -45
- package/dist/src/skills.test.js +80 -23
- package/package.json +3 -3
- package/skills/smart-search/SKILL.md +156 -0
- package/skills/smart-search/references/sources-ai.md +74 -0
- package/skills/smart-search/references/sources-info.md +43 -0
- package/skills/smart-search/references/sources-media.md +40 -0
- package/skills/smart-search/references/sources-other.md +32 -0
- package/skills/smart-search/references/sources-shopping.md +21 -0
- package/skills/smart-search/references/sources-social.md +36 -0
- package/skills/smart-search/references/sources-tech.md +38 -0
- package/skills/smart-search/references/sources-travel.md +26 -0
- package/skills/webcmd-adapter-author/SKILL.md +1 -0
- package/skills/webcmd-adapter-author/references/adapter-template.md +2 -0
- package/skills/webcmd-autofix/SKILL.md +8 -0
- package/skills/webcmd-sitemap-author/SKILL.md +1 -1
|
@@ -1,5 +1,7 @@
|
|
|
1
1
|
import { afterEach, describe, expect, it, vi } from 'vitest';
|
|
2
|
+
import path from 'node:path';
|
|
2
3
|
import { CloakSessionManager } from './session-manager.js';
|
|
4
|
+
import { dispatchCloakAction } from './actions.js';
|
|
3
5
|
function fakeContext() {
|
|
4
6
|
const page = {
|
|
5
7
|
goto: vi.fn().mockResolvedValue(undefined),
|
|
@@ -20,6 +22,9 @@ function fakeContext() {
|
|
|
20
22
|
page,
|
|
21
23
|
};
|
|
22
24
|
}
|
|
25
|
+
function expectedProfileDir(profileId) {
|
|
26
|
+
return path.join('/tmp/webcmd-test', 'cloak', 'profiles', profileId);
|
|
27
|
+
}
|
|
23
28
|
describe('CloakSessionManager', () => {
|
|
24
29
|
afterEach(() => {
|
|
25
30
|
vi.useRealTimers();
|
|
@@ -37,6 +42,64 @@ describe('CloakSessionManager', () => {
|
|
|
37
42
|
expect(launchPersistentContext).toHaveBeenCalledTimes(1);
|
|
38
43
|
expect(launchPersistentContext.mock.calls[0][0]).toMatchObject({ headless: false });
|
|
39
44
|
});
|
|
45
|
+
it('freshPage closes the existing persistent lease page and creates a new one', async () => {
|
|
46
|
+
const makePage = () => ({
|
|
47
|
+
goto: vi.fn().mockResolvedValue(undefined),
|
|
48
|
+
evaluate: vi.fn().mockResolvedValue('ok'),
|
|
49
|
+
title: vi.fn().mockResolvedValue('Title'),
|
|
50
|
+
url: vi.fn().mockReturnValue('about:blank'),
|
|
51
|
+
isClosed: vi.fn().mockReturnValue(false),
|
|
52
|
+
close: vi.fn().mockResolvedValue(undefined),
|
|
53
|
+
});
|
|
54
|
+
const openPages = [];
|
|
55
|
+
const context = {
|
|
56
|
+
pages: vi.fn(() => openPages),
|
|
57
|
+
newPage: vi.fn(async () => {
|
|
58
|
+
const page = makePage();
|
|
59
|
+
openPages.push(page);
|
|
60
|
+
return page;
|
|
61
|
+
}),
|
|
62
|
+
cookies: vi.fn().mockResolvedValue([]),
|
|
63
|
+
close: vi.fn().mockResolvedValue(undefined),
|
|
64
|
+
};
|
|
65
|
+
const manager = new CloakSessionManager({
|
|
66
|
+
baseDir: '/tmp/webcmd-test',
|
|
67
|
+
launchPersistentContext: vi.fn().mockResolvedValue(context),
|
|
68
|
+
});
|
|
69
|
+
const key = { profileId: 'default', session: 'site:district', surface: 'adapter', siteSession: 'persistent' };
|
|
70
|
+
const first = await manager.getPage(key);
|
|
71
|
+
expect((await manager.getPage(key)).page).toBe(first.page);
|
|
72
|
+
const fresh = await manager.getPage({ ...key, freshPage: true });
|
|
73
|
+
expect(first.page.close).toHaveBeenCalled();
|
|
74
|
+
expect(fresh.page).not.toBe(first.page);
|
|
75
|
+
const reused = await manager.getPage(key);
|
|
76
|
+
expect(reused.page).toBe(fresh.page);
|
|
77
|
+
});
|
|
78
|
+
it('freshPage never adopts a leftover context tab', async () => {
|
|
79
|
+
const leftover = {
|
|
80
|
+
goto: vi.fn(),
|
|
81
|
+
isClosed: vi.fn().mockReturnValue(false),
|
|
82
|
+
close: vi.fn().mockResolvedValue(undefined),
|
|
83
|
+
};
|
|
84
|
+
const created = {
|
|
85
|
+
goto: vi.fn(),
|
|
86
|
+
isClosed: vi.fn().mockReturnValue(false),
|
|
87
|
+
close: vi.fn().mockResolvedValue(undefined),
|
|
88
|
+
};
|
|
89
|
+
const context = {
|
|
90
|
+
pages: vi.fn().mockReturnValue([leftover]),
|
|
91
|
+
newPage: vi.fn().mockResolvedValue(created),
|
|
92
|
+
cookies: vi.fn().mockResolvedValue([]),
|
|
93
|
+
close: vi.fn().mockResolvedValue(undefined),
|
|
94
|
+
};
|
|
95
|
+
const manager = new CloakSessionManager({
|
|
96
|
+
baseDir: '/tmp/webcmd-test',
|
|
97
|
+
launchPersistentContext: vi.fn().mockResolvedValue(context),
|
|
98
|
+
});
|
|
99
|
+
const lease = await manager.getPage({ profileId: 'default', session: 'site:district', surface: 'adapter', siteSession: 'persistent', freshPage: true });
|
|
100
|
+
expect(lease.page).toBe(created);
|
|
101
|
+
expect(context.newPage).toHaveBeenCalled();
|
|
102
|
+
});
|
|
40
103
|
it('closes ephemeral adapter sessions when released', async () => {
|
|
41
104
|
const launched = fakeContext();
|
|
42
105
|
const manager = new CloakSessionManager({
|
|
@@ -89,4 +152,86 @@ describe('CloakSessionManager', () => {
|
|
|
89
152
|
expect(lease.page.close).not.toHaveBeenCalled();
|
|
90
153
|
expect(await manager.listPages({ profileId: 'default' })).toHaveLength(1);
|
|
91
154
|
});
|
|
155
|
+
it('launches a preferred profile when no Cloak profile is active', async () => {
|
|
156
|
+
const launched = fakeContext();
|
|
157
|
+
const launchPersistentContext = vi.fn().mockResolvedValue(launched.context);
|
|
158
|
+
const manager = new CloakSessionManager({
|
|
159
|
+
baseDir: '/tmp/webcmd-test',
|
|
160
|
+
launchPersistentContext,
|
|
161
|
+
});
|
|
162
|
+
await dispatchCloakAction(manager, {
|
|
163
|
+
id: 'cmd-preferred',
|
|
164
|
+
action: 'navigate',
|
|
165
|
+
session: 'work',
|
|
166
|
+
surface: 'browser',
|
|
167
|
+
url: 'https://example.com/',
|
|
168
|
+
preferredContextId: 'profile-default',
|
|
169
|
+
});
|
|
170
|
+
expect(launchPersistentContext).toHaveBeenCalledTimes(1);
|
|
171
|
+
expect(launchPersistentContext.mock.calls[0][0].userDataDir).toBe(expectedProfileDir('profile-default'));
|
|
172
|
+
});
|
|
173
|
+
it('falls back to the only active profile when the preferred profile is stale', async () => {
|
|
174
|
+
const launched = fakeContext();
|
|
175
|
+
const launchPersistentContext = vi.fn().mockResolvedValue(launched.context);
|
|
176
|
+
const manager = new CloakSessionManager({
|
|
177
|
+
baseDir: '/tmp/webcmd-test',
|
|
178
|
+
launchPersistentContext,
|
|
179
|
+
});
|
|
180
|
+
await dispatchCloakAction(manager, {
|
|
181
|
+
id: 'cmd-active',
|
|
182
|
+
action: 'navigate',
|
|
183
|
+
session: 'work',
|
|
184
|
+
surface: 'browser',
|
|
185
|
+
url: 'https://example.com/',
|
|
186
|
+
contextId: 'active',
|
|
187
|
+
});
|
|
188
|
+
await dispatchCloakAction(manager, {
|
|
189
|
+
id: 'cmd-stale-default',
|
|
190
|
+
action: 'navigate',
|
|
191
|
+
session: 'work',
|
|
192
|
+
surface: 'browser',
|
|
193
|
+
url: 'https://example.com/next',
|
|
194
|
+
preferredContextId: 'stale-default',
|
|
195
|
+
});
|
|
196
|
+
expect(launchPersistentContext).toHaveBeenCalledTimes(1);
|
|
197
|
+
expect(launchPersistentContext.mock.calls[0][0].userDataDir).toBe(expectedProfileDir('active'));
|
|
198
|
+
});
|
|
199
|
+
it('asks for an explicit profile when a stale preferred profile meets multiple active profiles', async () => {
|
|
200
|
+
const launched = fakeContext();
|
|
201
|
+
const launchPersistentContext = vi.fn().mockResolvedValue(launched.context);
|
|
202
|
+
const manager = new CloakSessionManager({
|
|
203
|
+
baseDir: '/tmp/webcmd-test',
|
|
204
|
+
launchPersistentContext,
|
|
205
|
+
});
|
|
206
|
+
await dispatchCloakAction(manager, {
|
|
207
|
+
id: 'cmd-a',
|
|
208
|
+
action: 'navigate',
|
|
209
|
+
session: 'work-a',
|
|
210
|
+
surface: 'browser',
|
|
211
|
+
url: 'https://example.com/a',
|
|
212
|
+
contextId: 'profile-a',
|
|
213
|
+
});
|
|
214
|
+
await dispatchCloakAction(manager, {
|
|
215
|
+
id: 'cmd-b',
|
|
216
|
+
action: 'navigate',
|
|
217
|
+
session: 'work-b',
|
|
218
|
+
surface: 'browser',
|
|
219
|
+
url: 'https://example.com/b',
|
|
220
|
+
contextId: 'profile-b',
|
|
221
|
+
});
|
|
222
|
+
const result = await dispatchCloakAction(manager, {
|
|
223
|
+
id: 'cmd-stale',
|
|
224
|
+
action: 'navigate',
|
|
225
|
+
session: 'work',
|
|
226
|
+
surface: 'browser',
|
|
227
|
+
url: 'https://example.com/',
|
|
228
|
+
preferredContextId: 'stale-default',
|
|
229
|
+
});
|
|
230
|
+
expect(result).toMatchObject({
|
|
231
|
+
id: 'cmd-stale',
|
|
232
|
+
ok: false,
|
|
233
|
+
errorCode: 'profile_required',
|
|
234
|
+
});
|
|
235
|
+
expect(launchPersistentContext).toHaveBeenCalledTimes(2);
|
|
236
|
+
});
|
|
92
237
|
});
|
|
@@ -81,6 +81,40 @@ describe('manifest helper rules', () => {
|
|
|
81
81
|
expect(entries[0].sourceFile).not.toContain('\\');
|
|
82
82
|
getRegistry().delete(key);
|
|
83
83
|
});
|
|
84
|
+
it('serializes freshPage for persistent browser commands', async () => {
|
|
85
|
+
const site = `manifest-fresh-page-${Date.now()}`;
|
|
86
|
+
const key = `${site}/checkout`;
|
|
87
|
+
const dir = fs.mkdtempSync(path.join(os.tmpdir(), 'webcmd-manifest-'));
|
|
88
|
+
tempDirs.push(dir);
|
|
89
|
+
const file = path.join(dir, `${site}.ts`);
|
|
90
|
+
fs.writeFileSync(file, `export const command = cli({ site: '${site}', name: 'checkout', access: 'write' });`);
|
|
91
|
+
const entries = await loadManifestEntries(file, site, async () => ({
|
|
92
|
+
command: cli({
|
|
93
|
+
site,
|
|
94
|
+
name: 'checkout',
|
|
95
|
+
access: 'write',
|
|
96
|
+
description: 'checkout command',
|
|
97
|
+
strategy: Strategy.COOKIE,
|
|
98
|
+
browser: true,
|
|
99
|
+
siteSession: 'persistent',
|
|
100
|
+
freshPage: true,
|
|
101
|
+
}),
|
|
102
|
+
}));
|
|
103
|
+
expect(entries).toMatchObject([
|
|
104
|
+
{
|
|
105
|
+
site,
|
|
106
|
+
name: 'checkout',
|
|
107
|
+
access: 'write',
|
|
108
|
+
strategy: 'cookie',
|
|
109
|
+
browser: true,
|
|
110
|
+
siteSession: 'persistent',
|
|
111
|
+
freshPage: true,
|
|
112
|
+
type: 'js',
|
|
113
|
+
modulePath: `${site}/${site}.js`,
|
|
114
|
+
},
|
|
115
|
+
]);
|
|
116
|
+
getRegistry().delete(key);
|
|
117
|
+
});
|
|
84
118
|
it('falls back to registry delta for side-effect-only cli modules', async () => {
|
|
85
119
|
const site = `manifest-side-effect-${Date.now()}`;
|
|
86
120
|
const key = `${site}/legacy`;
|
package/dist/src/cli.js
CHANGED
|
@@ -7,6 +7,7 @@
|
|
|
7
7
|
import * as fs from 'node:fs';
|
|
8
8
|
import * as os from 'node:os';
|
|
9
9
|
import * as path from 'node:path';
|
|
10
|
+
import * as readline from 'node:readline/promises';
|
|
10
11
|
import { fileURLToPath } from 'node:url';
|
|
11
12
|
import { Command, InvalidArgumentError, Option } from 'commander';
|
|
12
13
|
import { findPackageRoot, getBuiltEntryCandidates } from './package-paths.js';
|
|
@@ -16,10 +17,10 @@ import { render as renderOutput } from './output.js';
|
|
|
16
17
|
import { PKG_VERSION } from './version.js';
|
|
17
18
|
import { printCompletionScript } from './completion.js';
|
|
18
19
|
import { loadExternalClis, executeExternalCli, installExternalCli, registerExternalCli, isBinaryInstalled, formatExternalCliLabel } from './external.js';
|
|
19
|
-
import { listWebcmdSkills,
|
|
20
|
+
import { installWebcmdSkill, listWebcmdSkills, updateWebcmdSkill } from './skills.js';
|
|
20
21
|
import { registerAllCommands } from './commanderAdapter.js';
|
|
21
22
|
import { classifyAdapter, formatRootAdapterHelpText, installCommanderNamespaceStructuredHelp, installStructuredHelp, leadingPositionalFromUsage, rootHelpData } from './help.js';
|
|
22
|
-
import { EXIT_CODES, getErrorMessage, BrowserConnectError, CliError } from './errors.js';
|
|
23
|
+
import { EXIT_CODES, getErrorMessage, BrowserConnectError, CliError, ArgumentError } from './errors.js';
|
|
23
24
|
import { TargetError } from './browser/target-errors.js';
|
|
24
25
|
import { resolveTargetJs, getTextResolvedJs, getValueResolvedJs, getAttributesResolvedJs, selectResolvedJs, isAutocompleteResolvedJs } from './browser/target-resolver.js';
|
|
25
26
|
import { buildFindJs, buildSemanticFindJs, isFindError } from './browser/find.js';
|
|
@@ -36,7 +37,7 @@ import { daemonRestart, daemonStatus, daemonStop } from './commands/daemon.js';
|
|
|
36
37
|
import { log } from './logger.js';
|
|
37
38
|
import { bindTab, BrowserCommandError, sendCommand } from './browser/daemon-client.js';
|
|
38
39
|
import { fetchDaemonStatus } from './browser/daemon-transport.js';
|
|
39
|
-
import { aliasForContextId, loadProfileConfig, renameProfile,
|
|
40
|
+
import { aliasForContextId, loadProfileConfig, profileRouteParams, renameProfile, resolveProfileSelection, setDefaultProfile } from './browser/profile.js';
|
|
40
41
|
import { formatDaemonVersion, isDaemonStale } from './browser/daemon-version.js';
|
|
41
42
|
import { DEFAULT_BROWSER_CONNECT_TIMEOUT } from './browser/config.js';
|
|
42
43
|
import { CLI_COMMAND } from './brand.js';
|
|
@@ -58,6 +59,70 @@ function parseDurationMs(raw, flagName) {
|
|
|
58
59
|
function timestampFromRaw(value) {
|
|
59
60
|
return typeof value === 'number' && Number.isFinite(value) && value > 0 ? value : Date.now();
|
|
60
61
|
}
|
|
62
|
+
function isInteractiveInstall(opts) {
|
|
63
|
+
return !opts.json && process.stdin.isTTY === true && process.stdout.isTTY === true;
|
|
64
|
+
}
|
|
65
|
+
async function resolveSkillInstallOptions(opts) {
|
|
66
|
+
if (!isInteractiveInstall(opts))
|
|
67
|
+
return opts;
|
|
68
|
+
const rl = readline.createInterface({ input: process.stdin, output: process.stdout });
|
|
69
|
+
try {
|
|
70
|
+
const scope = opts.scope ?? await choosePrompt(rl, 'Where should Webcmd install skills?', [
|
|
71
|
+
{ key: '1', label: 'Global', value: 'user', aliases: ['global', 'user', 'g'] },
|
|
72
|
+
{ key: '2', label: 'Local project', value: 'project', aliases: ['local', 'project', 'l'] },
|
|
73
|
+
], '1');
|
|
74
|
+
const provider = opts.provider ?? (opts.path ? undefined : await choosePrompt(rl, 'Which coding agent should use them?', [
|
|
75
|
+
{ key: '1', label: 'Agents', value: 'agents', aliases: ['agents', 'agent', 'a'] },
|
|
76
|
+
{ key: '2', label: 'Codex', value: 'codex', aliases: ['codex', 'c'] },
|
|
77
|
+
{ key: '3', label: 'Claude', value: 'claude', aliases: ['claude', 'claude-code'] },
|
|
78
|
+
{ key: '4', label: 'Custom path', value: 'custom', aliases: ['custom', 'path'] },
|
|
79
|
+
], '1'));
|
|
80
|
+
const customPath = opts.path ?? (provider === 'custom' ? await nonEmptyPrompt(rl, 'Skills directory path: ') : undefined);
|
|
81
|
+
return { ...opts, scope, provider: provider === 'custom' ? undefined : provider, path: customPath };
|
|
82
|
+
}
|
|
83
|
+
finally {
|
|
84
|
+
rl.close();
|
|
85
|
+
}
|
|
86
|
+
}
|
|
87
|
+
async function choosePrompt(rl, question, choices, defaultKey) {
|
|
88
|
+
console.log(question);
|
|
89
|
+
for (const choice of choices)
|
|
90
|
+
console.log(` ${choice.key}) ${choice.label}`);
|
|
91
|
+
while (true) {
|
|
92
|
+
const answer = (await rl.question(`Choose [${defaultKey}]: `)).trim().toLowerCase() || defaultKey;
|
|
93
|
+
const choice = choices.find((item) => item.key === answer || item.aliases.includes(answer));
|
|
94
|
+
if (choice)
|
|
95
|
+
return choice.value;
|
|
96
|
+
console.log(`Choose one of: ${choices.map((item) => item.key).join(', ')}`);
|
|
97
|
+
}
|
|
98
|
+
}
|
|
99
|
+
async function nonEmptyPrompt(rl, question) {
|
|
100
|
+
while (true) {
|
|
101
|
+
const answer = (await rl.question(question)).trim();
|
|
102
|
+
if (answer)
|
|
103
|
+
return answer;
|
|
104
|
+
console.log('Path is required.');
|
|
105
|
+
}
|
|
106
|
+
}
|
|
107
|
+
async function handleSkillLinkCommand(action, json, verb) {
|
|
108
|
+
try {
|
|
109
|
+
const result = await action();
|
|
110
|
+
if (json) {
|
|
111
|
+
console.log(JSON.stringify(result, null, 2));
|
|
112
|
+
return;
|
|
113
|
+
}
|
|
114
|
+
console.log(`Webcmd skills ${verb}: ${result.skills.length}`);
|
|
115
|
+
for (const skill of result.skills) {
|
|
116
|
+
console.log(`${skill.name}: ${skill.destination ? `${skill.destination} -> ` : ''}${skill.stableLink}`);
|
|
117
|
+
}
|
|
118
|
+
}
|
|
119
|
+
catch (err) {
|
|
120
|
+
console.error(`Error: ${getErrorMessage(err)}`);
|
|
121
|
+
if (err instanceof CliError && err.hint)
|
|
122
|
+
console.error(`Hint: ${err.hint}`);
|
|
123
|
+
process.exitCode = err instanceof CliError ? err.exitCode : EXIT_CODES.GENERIC_ERROR;
|
|
124
|
+
}
|
|
125
|
+
}
|
|
61
126
|
function toIsoTimestamp(timestamp) {
|
|
62
127
|
if (typeof timestamp !== 'number' || !Number.isFinite(timestamp) || timestamp <= 0)
|
|
63
128
|
return undefined;
|
|
@@ -428,7 +493,7 @@ async function resolveStoredBrowserTarget(page, scope) {
|
|
|
428
493
|
return resolveBrowserTargetInSession(page, defaultPage, { scope, source: 'saved' });
|
|
429
494
|
}
|
|
430
495
|
/** Create a browser page for browser commands. Uses a named browser session for continuity. */
|
|
431
|
-
async function getBrowserPage(session, targetPage,
|
|
496
|
+
async function getBrowserPage(session, targetPage, profileSelection, opts = {}) {
|
|
432
497
|
const { BrowserBridge } = await import('./browser/index.js');
|
|
433
498
|
const bridge = new BrowserBridge();
|
|
434
499
|
// Internal GC timeout for browser sessions. Not the per-command runtime timeout.
|
|
@@ -438,11 +503,11 @@ async function getBrowserPage(session, targetPage, contextId, opts = {}) {
|
|
|
438
503
|
timeout: DEFAULT_BROWSER_CONNECT_TIMEOUT,
|
|
439
504
|
session,
|
|
440
505
|
surface: 'browser',
|
|
441
|
-
...(
|
|
506
|
+
...profileRouteParams(profileSelection),
|
|
442
507
|
...(idleTimeout && idleTimeout > 0 && { idleTimeout }),
|
|
443
508
|
windowMode: opts.windowMode ?? getBrowserWindowMode(undefined, 'foreground'),
|
|
444
509
|
});
|
|
445
|
-
const targetScope = getBrowserScope(session, contextId);
|
|
510
|
+
const targetScope = getBrowserScope(session, profileSelection?.contextId);
|
|
446
511
|
const resolvedTargetPage = targetPage
|
|
447
512
|
? await resolveBrowserTargetInSession(page, targetPage, { scope: targetScope, source: 'explicit' })
|
|
448
513
|
: await resolveStoredBrowserTarget(page, targetScope);
|
|
@@ -497,9 +562,9 @@ function getBrowserSession(command) {
|
|
|
497
562
|
return raw.trim();
|
|
498
563
|
throw new Error('<session> is a required positional argument: webcmd browser <session> <command>');
|
|
499
564
|
}
|
|
500
|
-
function
|
|
565
|
+
function getBrowserProfileSelection(command) {
|
|
501
566
|
const raw = getCommandOption(command, 'profile');
|
|
502
|
-
return
|
|
567
|
+
return resolveProfileSelection(typeof raw === 'string' && raw.trim() ? raw.trim() : undefined);
|
|
503
568
|
}
|
|
504
569
|
function getPageSession(page) {
|
|
505
570
|
const session = page.session;
|
|
@@ -508,8 +573,11 @@ function getPageSession(page) {
|
|
|
508
573
|
throw new Error('Browser page is missing a session');
|
|
509
574
|
}
|
|
510
575
|
function getPageScope(page) {
|
|
511
|
-
const contextId = page
|
|
512
|
-
|
|
576
|
+
const { contextId, preferredContextId } = page;
|
|
577
|
+
const selected = typeof contextId === 'string' && contextId.trim()
|
|
578
|
+
? contextId.trim()
|
|
579
|
+
: (typeof preferredContextId === 'string' && preferredContextId.trim() ? preferredContextId.trim() : undefined);
|
|
580
|
+
return getBrowserScope(getPageSession(page), selected);
|
|
513
581
|
}
|
|
514
582
|
function snapshotMetricText(snapshot) {
|
|
515
583
|
return typeof snapshot === 'string' ? snapshot : JSON.stringify(snapshot, null, 2);
|
|
@@ -708,10 +776,20 @@ export function createProgram(BUILTIN_CLIS, USER_CLIS) {
|
|
|
708
776
|
});
|
|
709
777
|
const skillsCmd = program
|
|
710
778
|
.command('skills')
|
|
711
|
-
.description('
|
|
779
|
+
.description('List, install, and update bundled Webcmd skills')
|
|
780
|
+
.action(() => {
|
|
781
|
+
const rows = listWebcmdSkills();
|
|
782
|
+
renderOutput(rows, {
|
|
783
|
+
fmt: 'table',
|
|
784
|
+
fmtExplicit: false,
|
|
785
|
+
columns: ['name', 'description', 'version', 'path'],
|
|
786
|
+
title: 'webcmd/skills/list',
|
|
787
|
+
source: 'webcmd skills',
|
|
788
|
+
});
|
|
789
|
+
});
|
|
712
790
|
skillsCmd
|
|
713
791
|
.command('list')
|
|
714
|
-
.description('List bundled
|
|
792
|
+
.description('List bundled Webcmd skills')
|
|
715
793
|
.option('-f, --format <fmt>', 'Output format: table, json, yaml, md, csv', 'table')
|
|
716
794
|
.action((opts) => {
|
|
717
795
|
const rows = listWebcmdSkills();
|
|
@@ -724,30 +802,38 @@ export function createProgram(BUILTIN_CLIS, USER_CLIS) {
|
|
|
724
802
|
});
|
|
725
803
|
});
|
|
726
804
|
skillsCmd
|
|
727
|
-
.command('
|
|
728
|
-
.description(
|
|
729
|
-
.
|
|
730
|
-
.
|
|
731
|
-
.option('--
|
|
732
|
-
.
|
|
733
|
-
|
|
734
|
-
|
|
735
|
-
|
|
736
|
-
|
|
737
|
-
|
|
738
|
-
|
|
739
|
-
|
|
740
|
-
|
|
741
|
-
|
|
742
|
-
|
|
743
|
-
|
|
744
|
-
|
|
745
|
-
|
|
746
|
-
|
|
747
|
-
|
|
748
|
-
|
|
749
|
-
|
|
750
|
-
|
|
805
|
+
.command('install')
|
|
806
|
+
.description('Install bundled Webcmd skills into an agent skills folder')
|
|
807
|
+
.option('-p, --provider <provider>', 'Agent provider: agents, codex, claude')
|
|
808
|
+
.option('-s, --scope <scope>', 'Install scope: user/global or project/local')
|
|
809
|
+
.option('--path <path>', 'Custom agent skills directory')
|
|
810
|
+
.option('--json', 'Output a JSON envelope', false)
|
|
811
|
+
.action(async (opts) => {
|
|
812
|
+
await handleSkillLinkCommand(async () => {
|
|
813
|
+
const resolved = await resolveSkillInstallOptions(opts);
|
|
814
|
+
if (resolved.provider === 'custom' && !resolved.path) {
|
|
815
|
+
throw new ArgumentError('Custom skill provider requires --path.', 'Pass --path <skills-dir> or run interactively.');
|
|
816
|
+
}
|
|
817
|
+
return installWebcmdSkill({
|
|
818
|
+
provider: resolved.provider,
|
|
819
|
+
scope: resolved.scope,
|
|
820
|
+
customPath: resolved.path,
|
|
821
|
+
});
|
|
822
|
+
}, opts.json, 'installed');
|
|
823
|
+
});
|
|
824
|
+
skillsCmd
|
|
825
|
+
.command('update')
|
|
826
|
+
.description('Refresh bundled Webcmd skill symlinks after updating the package')
|
|
827
|
+
.option('-p, --provider <provider>', 'Also repair provider link: agents, codex, claude')
|
|
828
|
+
.option('-s, --scope <scope>', 'Also repair scoped link: user/global or project/local')
|
|
829
|
+
.option('--path <path>', 'Also repair a custom agent skills directory')
|
|
830
|
+
.option('--json', 'Output a JSON envelope', false)
|
|
831
|
+
.action(async (opts) => {
|
|
832
|
+
await handleSkillLinkCommand(() => updateWebcmdSkill({
|
|
833
|
+
provider: opts.provider,
|
|
834
|
+
scope: opts.scope,
|
|
835
|
+
customPath: opts.path,
|
|
836
|
+
}), opts.json, 'updated');
|
|
751
837
|
});
|
|
752
838
|
const authCmd = registerAuthCommands(program);
|
|
753
839
|
program
|
|
@@ -882,9 +968,9 @@ Examples:
|
|
|
882
968
|
const command = args.at(-1) instanceof Command ? args.at(-1) : undefined;
|
|
883
969
|
const targetPage = getBrowserTargetId(command);
|
|
884
970
|
const session = getBrowserSession(command);
|
|
885
|
-
const
|
|
971
|
+
const profileSelection = getBrowserProfileSelection(command);
|
|
886
972
|
const windowMode = getBrowserWindowMode(command, 'foreground');
|
|
887
|
-
page = await getBrowserPage(session, targetPage,
|
|
973
|
+
page = await getBrowserPage(session, targetPage, profileSelection, { windowMode });
|
|
888
974
|
await fn(page, ...args);
|
|
889
975
|
}
|
|
890
976
|
catch (err) {
|
|
@@ -933,12 +1019,14 @@ Examples:
|
|
|
933
1019
|
? optsOrCommand
|
|
934
1020
|
: {};
|
|
935
1021
|
const session = getBrowserSession(command);
|
|
936
|
-
const
|
|
1022
|
+
const profileSelection = getBrowserProfileSelection(command);
|
|
1023
|
+
const contextId = profileSelection?.contextId;
|
|
1024
|
+
const routing = profileRouteParams(profileSelection);
|
|
937
1025
|
try {
|
|
938
1026
|
const { BrowserBridge } = await import('./browser/index.js');
|
|
939
1027
|
const bridge = new BrowserBridge();
|
|
940
|
-
await bridge.connect({ timeout: DEFAULT_BROWSER_CONNECT_TIMEOUT, session, surface: 'browser', ...
|
|
941
|
-
await fn({ session, contextId }, opts);
|
|
1028
|
+
await bridge.connect({ timeout: DEFAULT_BROWSER_CONNECT_TIMEOUT, session, surface: 'browser', ...routing });
|
|
1029
|
+
await fn({ session, contextId, routing }, opts);
|
|
942
1030
|
}
|
|
943
1031
|
catch (err) {
|
|
944
1032
|
if (err instanceof BrowserCommandError) {
|
|
@@ -956,7 +1044,7 @@ Examples:
|
|
|
956
1044
|
.description('Bind an existing Cloak runtime tab to the browser session named by <session>')
|
|
957
1045
|
.option('--page <id>', 'Cloak tab page id from `webcmd browser <session> tab list`')
|
|
958
1046
|
.option('--index <n>', 'Cloak tab index from `webcmd browser <session> tab list`')
|
|
959
|
-
.action(browserSessionCommandAction(async ({ session, contextId }, opts) => {
|
|
1047
|
+
.action(browserSessionCommandAction(async ({ session, contextId, routing }, opts) => {
|
|
960
1048
|
const page = typeof opts.page === 'string' && opts.page.trim() ? opts.page.trim() : undefined;
|
|
961
1049
|
const rawIndex = typeof opts.index === 'string' && opts.index.trim() ? opts.index.trim() : undefined;
|
|
962
1050
|
if ((page && rawIndex) || (!page && !rawIndex)) {
|
|
@@ -966,14 +1054,14 @@ Examples:
|
|
|
966
1054
|
throw new BrowserCommandError('--index must be a non-negative integer.', 'invalid_request');
|
|
967
1055
|
}
|
|
968
1056
|
const index = rawIndex === undefined ? undefined : Number.parseInt(rawIndex, 10);
|
|
969
|
-
const data = await bindTab(session, { ...
|
|
1057
|
+
const data = await bindTab(session, { ...routing, ...(page && { page }), ...(index !== undefined && { index }) });
|
|
970
1058
|
saveBrowserTargetState(undefined, getBrowserScope(session, contextId));
|
|
971
1059
|
console.log(JSON.stringify({ session, ...((data && typeof data === 'object') ? data : { data }) }, null, 2));
|
|
972
1060
|
}));
|
|
973
1061
|
browser.command('unbind')
|
|
974
1062
|
.description('Compatibility command; release the Cloak browser session named by <session>')
|
|
975
|
-
.action(browserSessionCommandAction(async ({ session, contextId }) => {
|
|
976
|
-
await sendCommand('close-window', { session, surface: 'browser', ...
|
|
1063
|
+
.action(browserSessionCommandAction(async ({ session, contextId, routing }) => {
|
|
1064
|
+
await sendCommand('close-window', { session, surface: 'browser', ...routing });
|
|
977
1065
|
saveBrowserTargetState(undefined, getBrowserScope(session, contextId));
|
|
978
1066
|
console.log(JSON.stringify({ unbound: true, session }, null, 2));
|
|
979
1067
|
}));
|
package/dist/src/cli.test.js
CHANGED
|
@@ -1004,7 +1004,7 @@ describe('profile list', () => {
|
|
|
1004
1004
|
runtimeVersion: '1.0.3',
|
|
1005
1005
|
pending: 0,
|
|
1006
1006
|
memoryMB: 20,
|
|
1007
|
-
port:
|
|
1007
|
+
port: 9777,
|
|
1008
1008
|
}),
|
|
1009
1009
|
});
|
|
1010
1010
|
const program = createProgram('', '');
|
|
@@ -1027,7 +1027,7 @@ describe('profile list', () => {
|
|
|
1027
1027
|
profiles: [],
|
|
1028
1028
|
pending: 0,
|
|
1029
1029
|
memoryMB: 20,
|
|
1030
|
-
port:
|
|
1030
|
+
port: 9777,
|
|
1031
1031
|
}),
|
|
1032
1032
|
});
|
|
1033
1033
|
const program = createProgram('', '');
|
|
@@ -39,7 +39,7 @@ describe('daemonStatus', () => {
|
|
|
39
39
|
runtimeVersion: '1.6.8',
|
|
40
40
|
pending: 0,
|
|
41
41
|
memoryMB: 64,
|
|
42
|
-
port:
|
|
42
|
+
port: 9777,
|
|
43
43
|
});
|
|
44
44
|
await daemonStatus();
|
|
45
45
|
expect(stdoutSpy).toHaveBeenCalledWith(expect.stringContaining('running'));
|
|
@@ -49,7 +49,7 @@ describe('daemonStatus', () => {
|
|
|
49
49
|
expect(stdoutSpy).toHaveBeenCalledWith(expect.stringContaining('connected'));
|
|
50
50
|
expect(stdoutSpy).toHaveBeenCalledWith(expect.stringContaining('v1.6.8'));
|
|
51
51
|
expect(stdoutSpy).toHaveBeenCalledWith(expect.stringContaining('64 MB'));
|
|
52
|
-
expect(stdoutSpy).toHaveBeenCalledWith(expect.stringContaining('
|
|
52
|
+
expect(stdoutSpy).toHaveBeenCalledWith(expect.stringContaining('9777'));
|
|
53
53
|
});
|
|
54
54
|
it('shows disconnected when runtime is not connected', async () => {
|
|
55
55
|
fetchDaemonStatusMock.mockResolvedValue({
|
|
@@ -61,7 +61,7 @@ describe('daemonStatus', () => {
|
|
|
61
61
|
runtimeName: 'fake',
|
|
62
62
|
pending: 0,
|
|
63
63
|
memoryMB: 32,
|
|
64
|
-
port:
|
|
64
|
+
port: 9777,
|
|
65
65
|
});
|
|
66
66
|
await daemonStatus();
|
|
67
67
|
expect(stdoutSpy).toHaveBeenCalledWith(expect.stringContaining('disconnected'));
|
|
@@ -77,7 +77,7 @@ describe('daemonStatus', () => {
|
|
|
77
77
|
runtimeVersion: undefined,
|
|
78
78
|
pending: 0,
|
|
79
79
|
memoryMB: 32,
|
|
80
|
-
port:
|
|
80
|
+
port: 9777,
|
|
81
81
|
});
|
|
82
82
|
await daemonStatus();
|
|
83
83
|
expect(stdoutSpy).toHaveBeenCalledWith(expect.stringContaining('fake connected'));
|
|
@@ -105,7 +105,7 @@ describe('daemonStatus runtime label states (#1575)', () => {
|
|
|
105
105
|
daemonVersion: PKG_VERSION,
|
|
106
106
|
pending: 0,
|
|
107
107
|
memoryMB: 32,
|
|
108
|
-
port:
|
|
108
|
+
port: 9777,
|
|
109
109
|
runtimeName: 'fake',
|
|
110
110
|
...extra,
|
|
111
111
|
});
|
|
@@ -173,7 +173,7 @@ describe('daemonStop', () => {
|
|
|
173
173
|
runtimeName: 'fake',
|
|
174
174
|
pending: 0,
|
|
175
175
|
memoryMB: 50,
|
|
176
|
-
port:
|
|
176
|
+
port: 9777,
|
|
177
177
|
});
|
|
178
178
|
requestDaemonShutdownMock.mockResolvedValue(true);
|
|
179
179
|
await daemonStop();
|
|
@@ -190,7 +190,7 @@ describe('daemonStop', () => {
|
|
|
190
190
|
runtimeName: 'fake',
|
|
191
191
|
pending: 0,
|
|
192
192
|
memoryMB: 50,
|
|
193
|
-
port:
|
|
193
|
+
port: 9777,
|
|
194
194
|
});
|
|
195
195
|
requestDaemonShutdownMock.mockResolvedValue(false);
|
|
196
196
|
await daemonStop();
|
|
@@ -221,7 +221,7 @@ describe('daemonRestart', () => {
|
|
|
221
221
|
profiles: [{ contextId: 'work', runtimeConnected: true, pending: 0 }],
|
|
222
222
|
pending: 0,
|
|
223
223
|
memoryMB: 50,
|
|
224
|
-
port:
|
|
224
|
+
port: 9777,
|
|
225
225
|
});
|
|
226
226
|
restartDaemonMock.mockResolvedValue({
|
|
227
227
|
previousStatus: { daemonVersion: '1.7.6' },
|
|
@@ -237,13 +237,13 @@ describe('daemonRestart', () => {
|
|
|
237
237
|
profiles: [{ contextId: 'work', runtimeConnected: true, pending: 0 }],
|
|
238
238
|
pending: 0,
|
|
239
239
|
memoryMB: 51,
|
|
240
|
-
port:
|
|
240
|
+
port: 9777,
|
|
241
241
|
},
|
|
242
242
|
});
|
|
243
243
|
await daemonRestart();
|
|
244
244
|
expect(restartDaemonMock).toHaveBeenCalledTimes(1);
|
|
245
245
|
expect(stderrSpy).toHaveBeenCalledWith(expect.stringContaining('will disconnect 1 browser profile'));
|
|
246
|
-
expect(stderrSpy).toHaveBeenCalledWith(expect.stringContaining(`Daemon restarted on port
|
|
246
|
+
expect(stderrSpy).toHaveBeenCalledWith(expect.stringContaining(`Daemon restarted on port 9777 (v${PKG_VERSION})`));
|
|
247
247
|
expect(stderrSpy).toHaveBeenCalledWith(expect.stringContaining('Runtime connected; 1 profile connected'));
|
|
248
248
|
});
|
|
249
249
|
it('starts a new daemon when none was running', async () => {
|
|
@@ -261,11 +261,11 @@ describe('daemonRestart', () => {
|
|
|
261
261
|
runtimeName: 'fake',
|
|
262
262
|
pending: 0,
|
|
263
263
|
memoryMB: 51,
|
|
264
|
-
port:
|
|
264
|
+
port: 9777,
|
|
265
265
|
},
|
|
266
266
|
});
|
|
267
267
|
await daemonRestart();
|
|
268
|
-
expect(stderrSpy).toHaveBeenCalledWith(expect.stringContaining(`Daemon started on port
|
|
268
|
+
expect(stderrSpy).toHaveBeenCalledWith(expect.stringContaining(`Daemon started on port 9777 (v${PKG_VERSION})`));
|
|
269
269
|
expect(stderrSpy).toHaveBeenCalledWith(expect.stringContaining('Cloak runtime has not connected yet'));
|
|
270
270
|
});
|
|
271
271
|
it('reports failure when the daemon cannot stop', async () => {
|
|
@@ -278,7 +278,7 @@ describe('daemonRestart', () => {
|
|
|
278
278
|
runtimeName: 'fake',
|
|
279
279
|
pending: 0,
|
|
280
280
|
memoryMB: 50,
|
|
281
|
-
port:
|
|
281
|
+
port: 9777,
|
|
282
282
|
});
|
|
283
283
|
restartDaemonMock.mockResolvedValue({
|
|
284
284
|
previousStatus: { daemonVersion: '1.7.6' },
|
package/dist/src/constants.d.ts
CHANGED
|
@@ -2,9 +2,8 @@
|
|
|
2
2
|
* Shared constants used across explore, synthesize, and pipeline modules.
|
|
3
3
|
*/
|
|
4
4
|
import { DAEMON_HEADER_NAME } from './brand.js';
|
|
5
|
-
/** Default daemon port for HTTP
|
|
6
|
-
export declare const DEFAULT_DAEMON_PORT =
|
|
7
|
-
export declare function unsupportedDaemonPortEnvMessage(value?: string): string;
|
|
5
|
+
/** Default daemon port for HTTP communication with the browser runtime. */
|
|
6
|
+
export declare const DEFAULT_DAEMON_PORT = 9777;
|
|
8
7
|
export { DAEMON_HEADER_NAME };
|
|
9
8
|
/** URL query params that are volatile/ephemeral and should be stripped from patterns */
|
|
10
9
|
export declare const VOLATILE_PARAMS: Set<string>;
|