@agentrhq/webcmd 0.2.1 → 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 (52) hide show
  1. package/README.md +39 -24
  2. package/cli-manifest.json +504 -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/dist/src/browser/bridge.d.ts +1 -0
  16. package/dist/src/browser/bridge.js +1 -1
  17. package/dist/src/browser/page.d.ts +4 -1
  18. package/dist/src/browser/page.js +12 -1
  19. package/dist/src/browser/protocol.d.ts +2 -0
  20. package/dist/src/browser/runtime/local-cloak/actions.js +1 -0
  21. package/dist/src/browser/runtime/local-cloak/session-manager.d.ts +2 -0
  22. package/dist/src/browser/runtime/local-cloak/session-manager.js +12 -2
  23. package/dist/src/browser/runtime/local-cloak/session-manager.test.js +58 -0
  24. package/dist/src/build-manifest.js +1 -0
  25. package/dist/src/build-manifest.test.js +34 -0
  26. package/dist/src/cli.js +111 -28
  27. package/dist/src/discovery.js +1 -0
  28. package/dist/src/engine.test.js +62 -0
  29. package/dist/src/execution.js +1 -1
  30. package/dist/src/manifest-types.d.ts +2 -0
  31. package/dist/src/registry.d.ts +10 -0
  32. package/dist/src/registry.js +5 -3
  33. package/dist/src/registry.test.js +25 -0
  34. package/dist/src/runtime.d.ts +2 -0
  35. package/dist/src/runtime.js +1 -0
  36. package/dist/src/skills.d.ts +23 -5
  37. package/dist/src/skills.js +87 -45
  38. package/dist/src/skills.test.js +80 -23
  39. package/package.json +2 -2
  40. package/skills/smart-search/SKILL.md +156 -0
  41. package/skills/smart-search/references/sources-ai.md +74 -0
  42. package/skills/smart-search/references/sources-info.md +43 -0
  43. package/skills/smart-search/references/sources-media.md +40 -0
  44. package/skills/smart-search/references/sources-other.md +32 -0
  45. package/skills/smart-search/references/sources-shopping.md +21 -0
  46. package/skills/smart-search/references/sources-social.md +36 -0
  47. package/skills/smart-search/references/sources-tech.md +38 -0
  48. package/skills/smart-search/references/sources-travel.md +26 -0
  49. package/skills/webcmd-adapter-author/SKILL.md +1 -0
  50. package/skills/webcmd-adapter-author/references/adapter-template.md +2 -0
  51. package/skills/webcmd-autofix/SKILL.md +8 -0
  52. package/skills/webcmd-sitemap-author/SKILL.md +1 -1
@@ -42,6 +42,64 @@ describe('CloakSessionManager', () => {
42
42
  expect(launchPersistentContext).toHaveBeenCalledTimes(1);
43
43
  expect(launchPersistentContext.mock.calls[0][0]).toMatchObject({ headless: false });
44
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
+ });
45
103
  it('closes ephemeral adapter sessions when released', async () => {
46
104
  const launched = fakeContext();
47
105
  const manager = new CloakSessionManager({
@@ -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';
@@ -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;
@@ -711,10 +776,20 @@ export function createProgram(BUILTIN_CLIS, USER_CLIS) {
711
776
  });
712
777
  const skillsCmd = program
713
778
  .command('skills')
714
- .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
+ });
715
790
  skillsCmd
716
791
  .command('list')
717
- .description('List bundled webcmd-* skills')
792
+ .description('List bundled Webcmd skills')
718
793
  .option('-f, --format <fmt>', 'Output format: table, json, yaml, md, csv', 'table')
719
794
  .action((opts) => {
720
795
  const rows = listWebcmdSkills();
@@ -727,30 +802,38 @@ export function createProgram(BUILTIN_CLIS, USER_CLIS) {
727
802
  });
728
803
  });
729
804
  skillsCmd
730
- .command('read')
731
- .description("Print a bundled webcmd-* skill's SKILL.md or reference file")
732
- .argument('<skill>', 'Skill name, or skill/path like webcmd-browser/references/foo.md')
733
- .argument('[path]', 'Path under the skill directory')
734
- .option('--json', 'Output a JSON envelope instead of raw markdown', false)
735
- .action((skill, skillPath, opts) => {
736
- let result;
737
- try {
738
- result = readWebcmdSkill(skill, skillPath ?? '');
739
- }
740
- catch (err) {
741
- console.error(`Error: ${getErrorMessage(err)}`);
742
- if (err instanceof CliError && err.hint)
743
- console.error(`Hint: ${err.hint}`);
744
- process.exitCode = err instanceof CliError ? err.exitCode : EXIT_CODES.GENERIC_ERROR;
745
- return;
746
- }
747
- if (opts.json) {
748
- console.log(JSON.stringify(result, null, 2));
749
- return;
750
- }
751
- process.stdout.write(result.content);
752
- if (!result.content.endsWith('\n'))
753
- 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');
754
837
  });
755
838
  const authCmd = registerAuthCommands(program);
756
839
  program
@@ -211,6 +211,7 @@ async function loadFromManifest(manifestPath, clisDir) {
211
211
  source: entry.sourceFile ? path.resolve(clisDir, entry.sourceFile) : modulePath,
212
212
  navigateBefore: entry.navigateBefore,
213
213
  siteSession: entry.siteSession,
214
+ freshPage: entry.freshPage,
214
215
  defaultWindowMode: entry.defaultWindowMode,
215
216
  _lazy: true,
216
217
  _modulePath: modulePath,
@@ -3,6 +3,7 @@ import { discoverClis, discoverPlugins, ensureUserCliCompatShims, ensureUserAdap
3
3
  import { executeCommand } from './execution.js';
4
4
  import { getRegistry, cli, Strategy } from './registry.js';
5
5
  import { clearAllHooks, onAfterExecute } from './hooks.js';
6
+ import * as runtime from './runtime.js';
6
7
  import * as fs from 'node:fs';
7
8
  import * as os from 'node:os';
8
9
  import * as path from 'node:path';
@@ -71,6 +72,67 @@ cli({
71
72
  await fs.promises.rm(tempBuildRoot, { recursive: true, force: true });
72
73
  }
73
74
  });
75
+ it('preserves freshPage from manifest before lazy command import', async () => {
76
+ const site = `manifest-fresh-${Date.now()}`;
77
+ const tempBuildRoot = await fs.promises.mkdtemp(path.join(os.tmpdir(), 'webcmd-manifest-fresh-'));
78
+ const distDir = path.join(tempBuildRoot, 'dist');
79
+ const siteDir = path.join(distDir, site);
80
+ const commandPath = path.join(siteDir, 'checkout.js');
81
+ const manifestPath = path.join(tempBuildRoot, 'cli-manifest.json');
82
+ const sessionOpts = [];
83
+ const mockPage = { closeWindow: vi.fn().mockResolvedValue(undefined) };
84
+ try {
85
+ await fs.promises.mkdir(siteDir, { recursive: true });
86
+ await fs.promises.writeFile(manifestPath, `${JSON.stringify([
87
+ {
88
+ site,
89
+ name: 'checkout',
90
+ description: 'fresh checkout',
91
+ access: 'write',
92
+ strategy: 'public',
93
+ browser: true,
94
+ args: [],
95
+ type: 'js',
96
+ modulePath: `${site}/checkout.js`,
97
+ sourceFile: `${site}/checkout.js`,
98
+ siteSession: 'persistent',
99
+ freshPage: true,
100
+ },
101
+ ], null, 2)}\n`);
102
+ await fs.promises.writeFile(commandPath, `
103
+ import { cli, Strategy } from '${pathToFileURL(path.join(process.cwd(), 'src', 'registry.ts')).href}';
104
+ cli({
105
+ site: '${site}',
106
+ name: 'checkout', access: 'write',
107
+ description: 'fresh checkout',
108
+ browser: true,
109
+ strategy: Strategy.PUBLIC,
110
+ siteSession: 'persistent',
111
+ freshPage: true,
112
+ func: async () => [{ ok: true }],
113
+ });
114
+ `);
115
+ await discoverClis(distDir);
116
+ const cmd = getRegistry().get(`${site}/checkout`);
117
+ expect(cmd?.freshPage).toBe(true);
118
+ vi.spyOn(runtime, 'browserSession').mockImplementation(async (_Factory, fn, opts) => {
119
+ sessionOpts.push(opts ?? {});
120
+ return fn(mockPage);
121
+ });
122
+ await expect(executeCommand(cmd, {})).resolves.toEqual([{ ok: true }]);
123
+ expect(sessionOpts).toHaveLength(1);
124
+ expect(sessionOpts[0]).toMatchObject({
125
+ session: `site:${site}`,
126
+ siteSession: 'persistent',
127
+ freshPage: true,
128
+ });
129
+ }
130
+ finally {
131
+ vi.restoreAllMocks();
132
+ getRegistry().delete(`${site}/checkout`);
133
+ await fs.promises.rm(tempBuildRoot, { recursive: true, force: true });
134
+ }
135
+ });
74
136
  it('loads user CLI modules via package exports symlink', async () => {
75
137
  const tempWebcmdRoot = await fs.promises.mkdtemp(path.join(os.tmpdir(), 'webcmd-user-clis-'));
76
138
  const userClisDir = path.join(tempWebcmdRoot, 'clis');
@@ -334,7 +334,7 @@ export async function executeCommand(cmd, rawKwargs, debug = false, opts = {}) {
334
334
  await page.closeWindow?.().catch(() => { });
335
335
  throw err;
336
336
  }
337
- }, { session, cdpEndpoint, ...profileRouting, windowMode, surface: 'adapter', siteSession });
337
+ }, { session, cdpEndpoint, ...profileRouting, windowMode, surface: 'adapter', siteSession, freshPage: cmd.freshPage === true && siteSession === 'persistent' });
338
338
  }
339
339
  else {
340
340
  // Non-browser commands: enforce a timeout only when the command exposes
@@ -38,6 +38,8 @@ export interface ManifestEntry {
38
38
  navigateBefore?: boolean | string;
39
39
  /** Site session lifecycle defaults — see CliCommand.siteSession */
40
40
  siteSession?: 'ephemeral' | 'persistent';
41
+ /** Fresh page behavior for persistent site sessions — see CliCommand.freshPage */
42
+ freshPage?: boolean;
41
43
  /** Default browser window visibility — see CliCommand.defaultWindowMode */
42
44
  defaultWindowMode?: 'foreground' | 'background';
43
45
  }
@@ -63,6 +63,16 @@ interface BaseCliCommand {
63
63
  navigateBefore?: boolean | string;
64
64
  /** Site session lifecycle for adapter commands. */
65
65
  siteSession?: SiteSessionMode;
66
+ /**
67
+ * Start this command on a freshly created page even when the persistent
68
+ * site session already has a leased tab. The old tab is closed and a new
69
+ * one opened under the same lease, so profile state (cookies, localStorage,
70
+ * login, location) survives while stale DOM (leftover modals, drawers,
71
+ * half-finished flows from earlier commands) is discarded. Requires
72
+ * `siteSession: 'persistent'` — ephemeral sessions always start fresh, so
73
+ * combining them is a contradiction and fails at registration.
74
+ */
75
+ freshPage?: boolean;
66
76
  /** Default browser window mode for commands whose UX requires visibility. */
67
77
  defaultWindowMode?: 'foreground' | 'background';
68
78
  /** Override the default CLI output format when the user does not pass -f/--format. */
@@ -29,6 +29,7 @@ export function cli(opts) {
29
29
  validateArgs: opts.validateArgs,
30
30
  navigateBefore: opts.navigateBefore,
31
31
  siteSession: opts.siteSession,
32
+ freshPage: opts.freshPage,
32
33
  defaultWindowMode: opts.defaultWindowMode,
33
34
  defaultFormat: opts.defaultFormat,
34
35
  authStatus: opts.authStatus,
@@ -86,12 +87,13 @@ function assertCommandAccess(cmd) {
86
87
  throw new Error(`Command ${key} must declare access: 'read' | 'write'`);
87
88
  }
88
89
  function assertSiteSession(cmd) {
89
- if (cmd.siteSession === undefined)
90
- return;
91
90
  const key = `${cmd.site}/${cmd.name}`;
92
- if (cmd.siteSession !== 'ephemeral' && cmd.siteSession !== 'persistent') {
91
+ if (cmd.siteSession !== undefined && cmd.siteSession !== 'ephemeral' && cmd.siteSession !== 'persistent') {
93
92
  throw new Error(`Command ${key} siteSession must be one of: ephemeral, persistent`);
94
93
  }
94
+ if (cmd.freshPage === true && cmd.siteSession !== 'persistent') {
95
+ throw new Error(`Command ${key} freshPage requires siteSession: 'persistent' — ephemeral sessions already start on a fresh page`);
96
+ }
95
97
  }
96
98
  export function registerCommand(cmd) {
97
99
  const normalized = normalizeCommand(cmd);
@@ -19,6 +19,31 @@ describe('cli() registration', () => {
19
19
  expect(cmd.browser).toBe(false);
20
20
  expect(cmd.args).toEqual([]);
21
21
  });
22
+ it('accepts freshPage with a persistent site session', () => {
23
+ const cmd = cli({
24
+ site: 'test-registry',
25
+ name: 'fresh-ok', access: 'write',
26
+ description: 'test',
27
+ siteSession: 'persistent',
28
+ freshPage: true,
29
+ });
30
+ expect(cmd.freshPage).toBe(true);
31
+ });
32
+ it('rejects freshPage without a persistent site session', () => {
33
+ expect(() => cli({
34
+ site: 'test-registry',
35
+ name: 'fresh-bad', access: 'write',
36
+ description: 'test',
37
+ freshPage: true,
38
+ })).toThrow(/freshPage requires siteSession: 'persistent'/);
39
+ expect(() => cli({
40
+ site: 'test-registry',
41
+ name: 'fresh-bad-ephemeral', access: 'write',
42
+ description: 'test',
43
+ siteSession: 'ephemeral',
44
+ freshPage: true,
45
+ })).toThrow(/freshPage requires siteSession: 'persistent'/);
46
+ });
22
47
  it('puts registered command in the registry', () => {
23
48
  cli({
24
49
  site: 'test-registry',
@@ -34,6 +34,7 @@ export interface IBrowserFactory {
34
34
  windowMode?: BrowserWindowMode;
35
35
  surface?: BrowserSurface;
36
36
  siteSession?: 'ephemeral' | 'persistent';
37
+ freshPage?: boolean;
37
38
  }): Promise<IPage>;
38
39
  close(): Promise<void>;
39
40
  }
@@ -46,4 +47,5 @@ export declare function browserSession<T>(BrowserFactory: new () => IBrowserFact
46
47
  windowMode?: BrowserWindowMode;
47
48
  surface?: BrowserSurface;
48
49
  siteSession?: 'ephemeral' | 'persistent';
50
+ freshPage?: boolean;
49
51
  }): Promise<T>;
@@ -46,6 +46,7 @@ export async function browserSession(BrowserFactory, fn, opts = {}) {
46
46
  windowMode: opts.windowMode,
47
47
  surface: opts.surface,
48
48
  siteSession: opts.siteSession,
49
+ freshPage: opts.freshPage,
49
50
  });
50
51
  return await fn(page);
51
52
  }
@@ -4,11 +4,29 @@ export interface WebcmdSkillInfo {
4
4
  version: string;
5
5
  path: string;
6
6
  }
7
- export interface WebcmdSkillReadResult {
8
- skill: string;
9
- path: string;
10
- content: string;
7
+ export interface WebcmdSkillInstallOptions {
8
+ provider?: string;
9
+ scope?: string;
10
+ customPath?: string;
11
+ packageRoot?: string;
12
+ homeDir?: string;
13
+ cwd?: string;
14
+ }
15
+ export interface WebcmdSkillLink {
16
+ name: string;
17
+ source: string;
18
+ stableLink: string;
19
+ destination?: string;
20
+ }
21
+ export interface WebcmdSkillInstallResult {
22
+ provider?: SkillProvider;
23
+ scope?: SkillScope;
24
+ skills: WebcmdSkillLink[];
11
25
  }
26
+ type SkillProvider = 'agents' | 'codex' | 'claude';
27
+ type SkillScope = 'user' | 'project';
12
28
  export declare function getSkillsRoot(packageRoot?: string): string;
13
29
  export declare function listWebcmdSkills(packageRoot?: string): WebcmdSkillInfo[];
14
- export declare function readWebcmdSkill(target: string, relpath?: string, packageRoot?: string): WebcmdSkillReadResult;
30
+ export declare function installWebcmdSkill(options?: WebcmdSkillInstallOptions): WebcmdSkillInstallResult;
31
+ export declare function updateWebcmdSkill(options?: WebcmdSkillInstallOptions): WebcmdSkillInstallResult;
32
+ export {};