@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.
Files changed (78) hide show
  1. package/README.md +58 -31
  2. package/cli-manifest.json +506 -0
  3. package/clis/_shared/site-auth.js +6 -1
  4. package/clis/district/_lib.js +566 -0
  5. package/clis/district/auth.js +49 -0
  6. package/clis/district/checkout.js +278 -0
  7. package/clis/district/listings.js +158 -0
  8. package/clis/district/locations.js +211 -0
  9. package/clis/district/search.js +218 -0
  10. package/clis/district/seats.js +233 -0
  11. package/clis/district/set-location.js +82 -0
  12. package/clis/district/showtimes.js +433 -0
  13. package/clis/reddit/popular.js +12 -1
  14. package/clis/reddit/popular.test.js +12 -3
  15. package/clis/twitter/delete.js +14 -6
  16. package/clis/twitter/delete.test.js +74 -1
  17. package/clis/twitter/timeline.js +3 -1
  18. package/clis/twitter/timeline.test.js +4 -0
  19. package/dist/src/browser/bridge-readiness.test.js +1 -1
  20. package/dist/src/browser/bridge.d.ts +2 -0
  21. package/dist/src/browser/bridge.js +6 -4
  22. package/dist/src/browser/cdp.d.ts +1 -0
  23. package/dist/src/browser/daemon-client.d.ts +1 -0
  24. package/dist/src/browser/daemon-client.js +7 -2
  25. package/dist/src/browser/daemon-client.test.js +33 -30
  26. package/dist/src/browser/daemon-transport.js +3 -19
  27. package/dist/src/browser/page.d.ts +5 -1
  28. package/dist/src/browser/page.js +16 -1
  29. package/dist/src/browser/profile.d.ts +9 -0
  30. package/dist/src/browser/profile.js +18 -7
  31. package/dist/src/browser/profile.test.d.ts +1 -0
  32. package/dist/src/browser/profile.test.js +44 -0
  33. package/dist/src/browser/protocol.d.ts +3 -0
  34. package/dist/src/browser/runtime/local-cloak/actions.js +25 -10
  35. package/dist/src/browser/runtime/local-cloak/session-manager.d.ts +3 -0
  36. package/dist/src/browser/runtime/local-cloak/session-manager.js +15 -2
  37. package/dist/src/browser/runtime/local-cloak/session-manager.test.js +145 -0
  38. package/dist/src/build-manifest.js +1 -0
  39. package/dist/src/build-manifest.test.js +34 -0
  40. package/dist/src/cli.js +133 -45
  41. package/dist/src/cli.test.js +2 -2
  42. package/dist/src/commands/daemon.test.js +13 -13
  43. package/dist/src/constants.d.ts +2 -3
  44. package/dist/src/constants.js +3 -10
  45. package/dist/src/daemon.js +1 -5
  46. package/dist/src/discovery.js +1 -0
  47. package/dist/src/doctor.js +12 -0
  48. package/dist/src/doctor.test.js +30 -3
  49. package/dist/src/engine.test.js +62 -0
  50. package/dist/src/execution.js +5 -3
  51. package/dist/src/external.js +19 -1
  52. package/dist/src/external.test.js +37 -1
  53. package/dist/src/main.js +0 -5
  54. package/dist/src/manifest-types.d.ts +2 -0
  55. package/dist/src/node-network.test.js +3 -3
  56. package/dist/src/registry.d.ts +10 -0
  57. package/dist/src/registry.js +5 -3
  58. package/dist/src/registry.test.js +25 -0
  59. package/dist/src/runtime-identity.test.js +0 -8
  60. package/dist/src/runtime.d.ts +4 -0
  61. package/dist/src/runtime.js +2 -0
  62. package/dist/src/skills.d.ts +23 -5
  63. package/dist/src/skills.js +87 -45
  64. package/dist/src/skills.test.js +80 -23
  65. package/package.json +3 -3
  66. package/skills/smart-search/SKILL.md +156 -0
  67. package/skills/smart-search/references/sources-ai.md +74 -0
  68. package/skills/smart-search/references/sources-info.md +43 -0
  69. package/skills/smart-search/references/sources-media.md +40 -0
  70. package/skills/smart-search/references/sources-other.md +32 -0
  71. package/skills/smart-search/references/sources-shopping.md +21 -0
  72. package/skills/smart-search/references/sources-social.md +36 -0
  73. package/skills/smart-search/references/sources-tech.md +38 -0
  74. package/skills/smart-search/references/sources-travel.md +26 -0
  75. package/skills/webcmd-adapter-author/SKILL.md +1 -0
  76. package/skills/webcmd-adapter-author/references/adapter-template.md +2 -0
  77. package/skills/webcmd-autofix/SKILL.md +8 -0
  78. 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
  });
@@ -103,6 +103,7 @@ function toManifestEntry(cmd, modulePath, sourceFile) {
103
103
  sourceFile,
104
104
  navigateBefore: cmd.navigateBefore,
105
105
  siteSession: cmd.siteSession,
106
+ freshPage: cmd.freshPage,
106
107
  defaultWindowMode: cmd.defaultWindowMode,
107
108
  };
108
109
  }
@@ -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, readWebcmdSkill } from './skills.js';
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, resolveProfileContextId, setDefaultProfile } from './browser/profile.js';
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, contextId, opts = {}) {
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
- ...(contextId && { contextId }),
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 getBrowserContextId(command) {
565
+ function getBrowserProfileSelection(command) {
501
566
  const raw = getCommandOption(command, 'profile');
502
- return resolveProfileContextId(typeof raw === 'string' && raw.trim() ? raw.trim() : undefined);
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.contextId;
512
- return getBrowserScope(getPageSession(page), typeof contextId === 'string' && contextId.trim() ? contextId.trim() : undefined);
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('Read bundled Webcmd skills');
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 webcmd-* skills')
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('read')
728
- .description("Print a bundled webcmd-* skill's SKILL.md or reference file")
729
- .argument('<skill>', 'Skill name, or skill/path like webcmd-browser/references/foo.md')
730
- .argument('[path]', 'Path under the skill directory')
731
- .option('--json', 'Output a JSON envelope instead of raw markdown', false)
732
- .action((skill, skillPath, opts) => {
733
- let result;
734
- try {
735
- result = readWebcmdSkill(skill, skillPath ?? '');
736
- }
737
- catch (err) {
738
- console.error(`Error: ${getErrorMessage(err)}`);
739
- if (err instanceof CliError && err.hint)
740
- console.error(`Hint: ${err.hint}`);
741
- process.exitCode = err instanceof CliError ? err.exitCode : EXIT_CODES.GENERIC_ERROR;
742
- return;
743
- }
744
- if (opts.json) {
745
- console.log(JSON.stringify(result, null, 2));
746
- return;
747
- }
748
- process.stdout.write(result.content);
749
- if (!result.content.endsWith('\n'))
750
- process.stdout.write('\n');
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 contextId = getBrowserContextId(command);
971
+ const profileSelection = getBrowserProfileSelection(command);
886
972
  const windowMode = getBrowserWindowMode(command, 'foreground');
887
- page = await getBrowserPage(session, targetPage, contextId, { windowMode });
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 contextId = getBrowserContextId(command);
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', ...(contextId && { contextId }) });
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, { ...(contextId && { contextId }), ...(page && { page }), ...(index !== undefined && { index }) });
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', ...(contextId && { contextId }) });
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
  }));
@@ -1004,7 +1004,7 @@ describe('profile list', () => {
1004
1004
  runtimeVersion: '1.0.3',
1005
1005
  pending: 0,
1006
1006
  memoryMB: 20,
1007
- port: 19825,
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: 19825,
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: 19825,
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('19825'));
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: 19825,
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: 19825,
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: 19825,
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: 19825,
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: 19825,
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: 19825,
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: 19825,
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 19825 (v${PKG_VERSION})`));
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: 19825,
264
+ port: 9777,
265
265
  },
266
266
  });
267
267
  await daemonRestart();
268
- expect(stderrSpy).toHaveBeenCalledWith(expect.stringContaining(`Daemon started on port 19825 (v${PKG_VERSION})`));
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: 19825,
281
+ port: 9777,
282
282
  });
283
283
  restartDaemonMock.mockResolvedValue({
284
284
  previousStatus: { daemonVersion: '1.7.6' },
@@ -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/WebSocket communication with browser extension */
6
- export declare const DEFAULT_DAEMON_PORT = 19825;
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>;