@agentrhq/webcmd 0.3.0 → 0.3.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (181) hide show
  1. package/README.md +13 -2
  2. package/cli-manifest.json +217 -13
  3. package/clis/chatgpt/ask.js +1 -1
  4. package/clis/chatgpt/ask.test.js +11 -0
  5. package/clis/chatgpt/commands.test.js +102 -1
  6. package/clis/chatgpt/deep-research-result.js +45 -3
  7. package/clis/chatgpt/image.js +22 -2
  8. package/clis/chatgpt/project-file-add.js +24 -1
  9. package/clis/chatgpt/utils.js +258 -52
  10. package/clis/chatgpt/utils.test.js +259 -1
  11. package/clis/claude/ask.js +21 -1
  12. package/clis/facebook/feed.js +110 -24
  13. package/clis/facebook/feed.test.js +62 -0
  14. package/clis/facebook/search.js +185 -38
  15. package/clis/facebook/search.test.js +95 -50
  16. package/clis/instagram/explore.js +30 -12
  17. package/clis/instagram/explore.test.js +77 -0
  18. package/clis/instagram/post.js +14 -1
  19. package/clis/instagram/reel.js +13 -1
  20. package/clis/linkedin/company.js +153 -0
  21. package/clis/linkedin/company.test.js +111 -0
  22. package/clis/linkedin/connections.js +135 -0
  23. package/clis/linkedin/connections.test.js +141 -0
  24. package/clis/mercury/reimbursement-draft.js +12 -1
  25. package/clis/twitter/article-evaluate.test.js +27 -0
  26. package/clis/twitter/article.js +73 -16
  27. package/clis/twitter/article.test.js +209 -0
  28. package/clis/twitter/download.js +6 -1
  29. package/clis/twitter/post.js +14 -1
  30. package/clis/twitter/profile.js +1 -1
  31. package/clis/twitter/profile.test.js +8 -0
  32. package/clis/twitter/quote.js +11 -1
  33. package/clis/twitter/reply.js +11 -1
  34. package/dist/src/browser/base-page.d.ts +26 -0
  35. package/dist/src/browser/base-page.js +23 -8
  36. package/dist/src/browser/base-page.test.js +45 -12
  37. package/dist/src/browser/command-catalog.d.ts +6 -0
  38. package/dist/src/browser/command-catalog.js +259 -0
  39. package/dist/src/browser/command-catalog.test.d.ts +1 -0
  40. package/dist/src/browser/command-catalog.test.js +79 -0
  41. package/dist/src/browser/runtime/local-cloak/darwin-background-launch.d.ts +17 -0
  42. package/dist/src/browser/runtime/local-cloak/darwin-background-launch.js +106 -0
  43. package/dist/src/browser/runtime/local-cloak/darwin-background-launch.test.d.ts +1 -0
  44. package/dist/src/browser/runtime/local-cloak/darwin-background-launch.test.js +65 -0
  45. package/dist/src/browser/runtime/local-cloak/session-manager.d.ts +7 -0
  46. package/dist/src/browser/runtime/local-cloak/session-manager.js +23 -9
  47. package/dist/src/browser/runtime/local-cloak/session-manager.test.js +36 -0
  48. package/dist/src/browser/target-resolver.d.ts +1 -0
  49. package/dist/src/browser/target-resolver.js +99 -4
  50. package/dist/src/browser/target-resolver.test.js +141 -1
  51. package/dist/src/build-manifest.d.ts +6 -0
  52. package/dist/src/build-manifest.js +18 -1
  53. package/dist/src/build-manifest.test.js +57 -1
  54. package/dist/src/builtin-command-surface.d.ts +9 -0
  55. package/dist/src/builtin-command-surface.js +16 -0
  56. package/dist/src/check-hosted-contract.test.d.ts +1 -0
  57. package/dist/src/check-hosted-contract.test.js +56 -0
  58. package/dist/src/cli.js +58 -106
  59. package/dist/src/cli.test.js +71 -1
  60. package/dist/src/command-presentation.d.ts +95 -0
  61. package/dist/src/command-presentation.js +486 -0
  62. package/dist/src/command-presentation.test.d.ts +1 -0
  63. package/dist/src/command-presentation.test.js +97 -0
  64. package/dist/src/command-surface.d.ts +43 -0
  65. package/dist/src/command-surface.js +205 -0
  66. package/dist/src/command-surface.test.d.ts +1 -0
  67. package/dist/src/command-surface.test.js +406 -0
  68. package/dist/src/commanderAdapter.d.ts +5 -1
  69. package/dist/src/commanderAdapter.js +12 -55
  70. package/dist/src/commanderAdapter.test.js +15 -3
  71. package/dist/src/community-plugin-sync.d.ts +11 -0
  72. package/dist/src/community-plugin-sync.js +138 -0
  73. package/dist/src/community-plugin-sync.test.d.ts +1 -0
  74. package/dist/src/community-plugin-sync.test.js +123 -0
  75. package/dist/src/completion-fast.d.ts +4 -4
  76. package/dist/src/completion-fast.js +15 -34
  77. package/dist/src/completion-shared.d.ts +4 -0
  78. package/dist/src/completion-shared.js +38 -0
  79. package/dist/src/completion.js +3 -27
  80. package/dist/src/completion.test.js +31 -2
  81. package/dist/src/docs-sync-review-cli.test.d.ts +1 -0
  82. package/dist/src/docs-sync-review-cli.test.js +322 -0
  83. package/dist/src/docs-sync-review.d.ts +117 -0
  84. package/dist/src/docs-sync-review.js +475 -0
  85. package/dist/src/docs-sync-review.test.d.ts +1 -0
  86. package/dist/src/docs-sync-review.test.js +324 -0
  87. package/dist/src/errors.d.ts +12 -3
  88. package/dist/src/errors.js +11 -7
  89. package/dist/src/errors.test.js +14 -1
  90. package/dist/src/execution.d.ts +1 -2
  91. package/dist/src/execution.js +4 -45
  92. package/dist/src/generate-release-notes-cli.test.js +7 -2
  93. package/dist/src/help.d.ts +4 -0
  94. package/dist/src/help.js +50 -255
  95. package/dist/src/help.test.js +27 -1
  96. package/dist/src/hosted/args.d.ts +2 -7
  97. package/dist/src/hosted/args.js +2 -99
  98. package/dist/src/hosted/args.test.js +15 -1
  99. package/dist/src/hosted/availability.d.ts +13 -0
  100. package/dist/src/hosted/availability.js +16 -0
  101. package/dist/src/hosted/availability.test.d.ts +1 -0
  102. package/dist/src/hosted/availability.test.js +180 -0
  103. package/dist/src/hosted/browser-args.d.ts +18 -0
  104. package/dist/src/hosted/browser-args.js +152 -0
  105. package/dist/src/hosted/browser-args.test.d.ts +1 -0
  106. package/dist/src/hosted/browser-args.test.js +182 -0
  107. package/dist/src/hosted/client.d.ts +31 -2
  108. package/dist/src/hosted/client.js +441 -53
  109. package/dist/src/hosted/client.test.js +651 -32
  110. package/dist/src/hosted/config.d.ts +7 -2
  111. package/dist/src/hosted/config.js +26 -4
  112. package/dist/src/hosted/config.test.js +34 -1
  113. package/dist/src/hosted/contract.d.ts +94 -0
  114. package/dist/src/hosted/contract.js +208 -0
  115. package/dist/src/hosted/contract.test.d.ts +1 -0
  116. package/dist/src/hosted/contract.test.js +361 -0
  117. package/dist/src/hosted/credentials.d.ts +38 -0
  118. package/dist/src/hosted/credentials.js +248 -0
  119. package/dist/src/hosted/credentials.test.d.ts +1 -0
  120. package/dist/src/hosted/credentials.test.js +93 -0
  121. package/dist/src/hosted/file-contract.test.d.ts +1 -0
  122. package/dist/src/hosted/file-contract.test.js +174 -0
  123. package/dist/src/hosted/files.d.ts +29 -0
  124. package/dist/src/hosted/files.js +296 -0
  125. package/dist/src/hosted/files.test.d.ts +1 -0
  126. package/dist/src/hosted/files.test.js +231 -0
  127. package/dist/src/hosted/main-lifecycle.test.d.ts +1 -0
  128. package/dist/src/hosted/main-lifecycle.test.js +213 -0
  129. package/dist/src/hosted/manifest.d.ts +5 -0
  130. package/dist/src/hosted/manifest.js +21 -66
  131. package/dist/src/hosted/manifest.test.js +120 -2
  132. package/dist/src/hosted/output-parity.test.d.ts +1 -0
  133. package/dist/src/hosted/output-parity.test.js +108 -0
  134. package/dist/src/hosted/root-command-surface.test.d.ts +1 -0
  135. package/dist/src/hosted/root-command-surface.test.js +646 -0
  136. package/dist/src/hosted/runner.d.ts +7 -0
  137. package/dist/src/hosted/runner.js +554 -253
  138. package/dist/src/hosted/runner.test.js +1471 -53
  139. package/dist/src/hosted/setup.d.ts +3 -2
  140. package/dist/src/hosted/setup.js +45 -15
  141. package/dist/src/hosted/setup.test.js +133 -6
  142. package/dist/src/hosted/types.d.ts +101 -23
  143. package/dist/src/main.js +120 -108
  144. package/dist/src/manifest-types.d.ts +2 -0
  145. package/dist/src/output.d.ts +17 -2
  146. package/dist/src/output.js +88 -81
  147. package/dist/src/output.test.js +141 -51
  148. package/dist/src/pipeline/executor.test.js +1 -0
  149. package/dist/src/pipeline/steps/download.test.js +1 -0
  150. package/dist/src/plugin-create-cli.test.d.ts +1 -0
  151. package/dist/src/plugin-create-cli.test.js +37 -0
  152. package/dist/src/plugin-manifest.d.ts +12 -0
  153. package/dist/src/plugin-manifest.js +16 -0
  154. package/dist/src/plugin-manifest.test.js +19 -1
  155. package/dist/src/plugin-scaffold.d.ts +4 -1
  156. package/dist/src/plugin-scaffold.js +4 -1
  157. package/dist/src/plugin-scaffold.test.js +23 -8
  158. package/dist/src/plugin.js +4 -1
  159. package/dist/src/plugin.test.js +13 -0
  160. package/dist/src/registry.d.ts +2 -0
  161. package/dist/src/release-notes.js +12 -4
  162. package/dist/src/release-notes.test.js +27 -15
  163. package/dist/src/root-command-surface.d.ts +31 -0
  164. package/dist/src/root-command-surface.js +106 -0
  165. package/dist/src/serialization.d.ts +1 -16
  166. package/dist/src/serialization.js +5 -55
  167. package/dist/src/stream-write.d.ts +12 -0
  168. package/dist/src/stream-write.js +91 -0
  169. package/dist/src/stream-write.test.d.ts +1 -0
  170. package/dist/src/stream-write.test.js +186 -0
  171. package/dist/src/types.d.ts +5 -0
  172. package/dist/src/utils.d.ts +1 -1
  173. package/dist/src/utils.js +2 -8
  174. package/dist/src/utils.test.js +50 -0
  175. package/hosted-contract.json +36748 -0
  176. package/package.json +6 -1
  177. package/scripts/check-hosted-contract.mjs +108 -0
  178. package/scripts/docs-sync-review.ts +332 -0
  179. package/scripts/sync-community-plugins.ts +14 -0
  180. package/skills/webcmd-autofix/SKILL.md +26 -37
  181. package/skills/webcmd-usage/SKILL.md +7 -0
@@ -3,7 +3,7 @@ import * as fs from 'node:fs';
3
3
  import * as os from 'node:os';
4
4
  import * as path from 'node:path';
5
5
  import { cli, getRegistry, Strategy } from './registry.js';
6
- import { ManifestImportError, diffRemovedEntries, findManifestMetadataIssues, loadManifestEntries, normalizeManifestPath, parseBuildManifestArgs, scanClisDir, serializeManifest, } from './build-manifest.js';
6
+ import { ManifestImportError, buildManifestArtifacts, diffRemovedEntries, findManifestMetadataIssues, loadManifestEntries, normalizeManifestPath, parseBuildManifestArgs, scanClisDir, serializeManifest, } from './build-manifest.js';
7
7
  describe('manifest helper rules', () => {
8
8
  const tempDirs = [];
9
9
  afterEach(() => {
@@ -81,6 +81,62 @@ describe('manifest helper rules', () => {
81
81
  expect(entries[0].sourceFile).not.toContain('\\');
82
82
  getRegistry().delete(key);
83
83
  });
84
+ it('preserves file metadata and builds both artifacts from one normalized snapshot', async () => {
85
+ const site = `manifest-files-${Date.now()}`;
86
+ const key = `${site}/upload`;
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: 'upload', access: 'write' });`);
91
+ const entries = await loadManifestEntries(file, site, async () => ({
92
+ command: cli({
93
+ site,
94
+ name: 'upload',
95
+ access: 'write',
96
+ description: 'upload a document',
97
+ strategy: Strategy.PUBLIC,
98
+ browser: false,
99
+ args: [{
100
+ name: 'path',
101
+ type: 'str',
102
+ positional: true,
103
+ required: true,
104
+ help: 'Document path',
105
+ file: {
106
+ direction: 'input',
107
+ pathKind: 'file',
108
+ contentTypes: ['application/pdf'],
109
+ maxBytes: 5_000_000,
110
+ },
111
+ }],
112
+ }),
113
+ }));
114
+ expect(entries[0].args[0].file).toEqual({
115
+ direction: 'input',
116
+ pathKind: 'file',
117
+ contentTypes: ['application/pdf'],
118
+ maxBytes: 5_000_000,
119
+ });
120
+ const artifacts = buildManifestArtifacts(entries, '7.6.5', []);
121
+ expect(JSON.parse(artifacts.manifestJson)).toEqual(entries);
122
+ expect(JSON.parse(artifacts.hostedContractJson)).toMatchObject({
123
+ schemaVersion: 1,
124
+ webcmdVersion: '7.6.5',
125
+ commands: [{
126
+ command: `${site}/upload`,
127
+ fileArguments: [{
128
+ name: 'path',
129
+ direction: 'input',
130
+ pathKind: 'file',
131
+ multiple: false,
132
+ required: true,
133
+ contentTypes: ['application/pdf'],
134
+ maxBytes: 5_000_000,
135
+ }],
136
+ }],
137
+ });
138
+ getRegistry().delete(key);
139
+ });
84
140
  it('serializes freshPage for persistent browser commands', async () => {
85
141
  const site = `manifest-fresh-page-${Date.now()}`;
86
142
  const key = `${site}/checkout`;
@@ -0,0 +1,9 @@
1
+ import type { Command } from 'commander';
2
+ export declare const LIST_COMMAND_DESCRIPTION = "List all available CLI commands";
3
+ export declare const LIST_FORMAT_DESCRIPTION = "Output format: table, json, yaml, md, csv";
4
+ export declare const COMPLETION_COMMAND_DESCRIPTION = "Output shell completion script";
5
+ export declare const COMPLETION_SHELL_DESCRIPTION = "Shell type: bash, zsh, or fish";
6
+ /** Configure built-in grammar shared by the local and hosted runtimes. */
7
+ export declare function configureListCommandSurface(command: Command): Command;
8
+ /** Configure completion grammar shared by the local and hosted runtimes. */
9
+ export declare function configureCompletionCommandSurface(command: Command): Command;
@@ -0,0 +1,16 @@
1
+ export const LIST_COMMAND_DESCRIPTION = 'List all available CLI commands';
2
+ export const LIST_FORMAT_DESCRIPTION = 'Output format: table, json, yaml, md, csv';
3
+ export const COMPLETION_COMMAND_DESCRIPTION = 'Output shell completion script';
4
+ export const COMPLETION_SHELL_DESCRIPTION = 'Shell type: bash, zsh, or fish';
5
+ /** Configure built-in grammar shared by the local and hosted runtimes. */
6
+ export function configureListCommandSurface(command) {
7
+ return command
8
+ .description(LIST_COMMAND_DESCRIPTION)
9
+ .option('-f, --format <fmt>', LIST_FORMAT_DESCRIPTION, 'table');
10
+ }
11
+ /** Configure completion grammar shared by the local and hosted runtimes. */
12
+ export function configureCompletionCommandSurface(command) {
13
+ return command
14
+ .description(COMPLETION_COMMAND_DESCRIPTION)
15
+ .argument('<shell>', COMPLETION_SHELL_DESCRIPTION);
16
+ }
@@ -0,0 +1 @@
1
+ export {};
@@ -0,0 +1,56 @@
1
+ import { createHash } from 'node:crypto';
2
+ import { copyFileSync, mkdtempSync, readFileSync, rmSync, writeFileSync } from 'node:fs';
3
+ import { tmpdir } from 'node:os';
4
+ import path from 'node:path';
5
+ import { fileURLToPath } from 'node:url';
6
+ import { spawnSync } from 'node:child_process';
7
+ import { afterEach, describe, expect, it } from 'vitest';
8
+ const packageRoot = path.resolve(path.dirname(fileURLToPath(import.meta.url)), '..');
9
+ const checkerPath = path.join(packageRoot, 'scripts/check-hosted-contract.mjs');
10
+ const committedArtifactNames = ['cli-manifest.json'];
11
+ const fixtureRoots = [];
12
+ afterEach(() => {
13
+ for (const fixtureRoot of fixtureRoots.splice(0)) {
14
+ rmSync(fixtureRoot, { recursive: true, force: true });
15
+ }
16
+ });
17
+ function createCommittedArtifactFixture() {
18
+ const fixtureRoot = mkdtempSync(path.join(tmpdir(), 'webcmd-contract-committed-'));
19
+ fixtureRoots.push(fixtureRoot);
20
+ for (const artifactName of committedArtifactNames) {
21
+ copyFileSync(path.join(packageRoot, artifactName), path.join(fixtureRoot, artifactName));
22
+ }
23
+ return fixtureRoot;
24
+ }
25
+ function rootArtifactHashes() {
26
+ return Object.fromEntries(committedArtifactNames.map((artifactName) => [
27
+ artifactName,
28
+ createHash('sha256').update(readFileSync(path.join(packageRoot, artifactName))).digest('hex'),
29
+ ]));
30
+ }
31
+ function runChecker(committedRoot) {
32
+ return spawnSync(process.execPath, [checkerPath], {
33
+ cwd: packageRoot,
34
+ encoding: 'utf8',
35
+ env: { ...process.env, WEBCMD_CONTRACT_COMMITTED_ROOT: committedRoot },
36
+ });
37
+ }
38
+ describe('hosted contract reproducibility checker', () => {
39
+ it('accepts a byte-identical committed manifest and generates the package contract', () => {
40
+ const result = runChecker(createCommittedArtifactFixture());
41
+ expect(result.status).toBe(0);
42
+ expect(result.stdout).toContain('hosted-contract.json generated successfully.');
43
+ }, 10_000);
44
+ it('rejects one stale byte without mutating root artifacts', () => {
45
+ const before = rootArtifactHashes();
46
+ const fixtureRoot = createCommittedArtifactFixture();
47
+ const stalePath = path.join(fixtureRoot, 'cli-manifest.json');
48
+ const staleBytes = readFileSync(stalePath);
49
+ staleBytes[0] ^= 1;
50
+ writeFileSync(stalePath, staleBytes);
51
+ const result = runChecker(fixtureRoot);
52
+ expect(result.status).toBe(1);
53
+ expect(result.stderr).toContain('cli-manifest.json');
54
+ expect(rootArtifactHashes()).toEqual(before);
55
+ }, 10_000);
56
+ });
package/dist/src/cli.js CHANGED
@@ -9,17 +9,18 @@ import * as os from 'node:os';
9
9
  import * as path from 'node:path';
10
10
  import * as readline from 'node:readline/promises';
11
11
  import { fileURLToPath } from 'node:url';
12
- import { Command, InvalidArgumentError, Option } from 'commander';
12
+ import { Command, Option } from 'commander';
13
13
  import { findPackageRoot, getBuiltEntryCandidates } from './package-paths.js';
14
- import { fullName, getRegistry, strategyLabel } from './registry.js';
15
- import { serializeCommand, formatArgSummary } from './serialization.js';
14
+ import { getRegistry } from './registry.js';
15
+ import { commandListPresentation, toPresentableCommand } from './command-presentation.js';
16
+ import { configureCompletionCommandSurface, configureListCommandSurface } from './builtin-command-surface.js';
16
17
  import { render as renderOutput } from './output.js';
17
18
  import { PKG_VERSION } from './version.js';
18
19
  import { printCompletionScript } from './completion.js';
19
20
  import { loadExternalClis, executeExternalCli, installExternalCli, registerExternalCli, isBinaryInstalled, formatExternalCliLabel } from './external.js';
20
21
  import { installWebcmdSkill, listWebcmdSkills, updateWebcmdSkill } from './skills.js';
21
22
  import { registerAllCommands } from './commanderAdapter.js';
22
- import { classifyAdapter, formatRootAdapterHelpText, installCommanderNamespaceStructuredHelp, installStructuredHelp, leadingPositionalFromUsage, rootHelpData } from './help.js';
23
+ import { buildRootHelpPresentation, classifyAdapter, installCommanderNamespaceStructuredHelp, installRootPresentationHelp, leadingPositionalFromUsage, rootHelpData } from './help.js';
23
24
  import { EXIT_CODES, getErrorMessage, BrowserConnectError, CliError, ArgumentError } from './errors.js';
24
25
  import { TargetError } from './browser/target-errors.js';
25
26
  import { resolveTargetJs, getTextResolvedJs, getValueResolvedJs, getAttributesResolvedJs, selectResolvedJs, isAutocompleteResolvedJs } from './browser/target-resolver.js';
@@ -32,6 +33,7 @@ import { parseFilter, shapeMatchesFilter } from './browser/shape-filter.js';
32
33
  import { buildHtmlTreeJs } from './browser/html-tree.js';
33
34
  import { buildExtractHtmlJs, runExtractFromHtml } from './browser/extract.js';
34
35
  import { analyzeSite } from './browser/analyze.js';
36
+ import { browserOptionValueParser } from './browser/command-catalog.js';
35
37
  import { registerAuthCommands } from './commands/auth.js';
36
38
  import { daemonRestart, daemonStatus, daemonStop } from './commands/daemon.js';
37
39
  import { log } from './logger.js';
@@ -41,6 +43,7 @@ import { aliasForContextId, loadProfileConfig, profileRouteParams, renameProfile
41
43
  import { formatDaemonVersion, isDaemonStale } from './browser/daemon-version.js';
42
44
  import { DEFAULT_BROWSER_CONNECT_TIMEOUT } from './browser/config.js';
43
45
  import { CLI_COMMAND } from './brand.js';
46
+ import { configureRootCommandSurface } from './root-command-surface.js';
44
47
  const CLI_FILE = fileURLToPath(import.meta.url);
45
48
  const BROWSER_TAB_OPTION_DESCRIPTION = 'Target tab/page identity returned by "browser open", "browser tab new", or "browser tab list"';
46
49
  const FOLLOW_POLL_MS = 1_000;
@@ -632,16 +635,6 @@ function parsePositiveIntOption(val, label, fallback) {
632
635
  }
633
636
  return parsed;
634
637
  }
635
- function parseScreenshotDim(val, label) {
636
- if (!/^\d+$/.test(val)) {
637
- throw new InvalidArgumentError(`--${label} must be a positive integer (got "${val}")`);
638
- }
639
- const parsed = parseInt(val, 10);
640
- if (parsed <= 0) {
641
- throw new InvalidArgumentError(`--${label} must be a positive integer (got "${val}")`);
642
- }
643
- return parsed;
644
- }
645
638
  function applyVerbose(opts) {
646
639
  if (opts.verbose)
647
640
  process.env.WEBCMD_VERBOSE = '1';
@@ -666,93 +659,30 @@ export function createProgram(BUILTIN_CLIS, USER_CLIS) {
666
659
  // prerequisite for passThroughOptions to forward --help/--version to external binaries
667
660
  program
668
661
  .name('webcmd')
669
- .description('Make any website your CLI. Zero setup. AI-powered.')
670
- .version(PKG_VERSION)
671
- .option('--profile <name>', 'Chrome profile/context alias for browser runtime commands')
672
- .enablePositionalOptions();
662
+ .description('Make any website your CLI. Zero setup. AI-powered.');
663
+ configureRootCommandSurface(program);
673
664
  // ── Built-in: list ────────────────────────────────────────────────────────
674
- program
675
- .command('list')
676
- .description('List all available CLI commands')
677
- .option('-f, --format <fmt>', 'Output format: table, json, yaml, md, csv', 'table')
665
+ configureListCommandSurface(program.command('list'))
678
666
  .action((opts) => {
679
- const registry = getRegistry();
680
- const commands = [...new Set(registry.values())].sort((a, b) => fullName(a).localeCompare(fullName(b)));
681
- const fmt = opts.format;
682
- const isStructured = fmt === 'json' || fmt === 'yaml';
683
- if (fmt !== 'table') {
684
- const rows = isStructured
685
- ? commands.map(serializeCommand)
686
- : commands.map(c => ({
687
- command: fullName(c),
688
- site: c.site,
689
- name: c.name,
690
- aliases: c.aliases?.join(', ') ?? '',
691
- description: c.description,
692
- access: c.access,
693
- strategy: strategyLabel(c),
694
- browser: !!c.browser,
695
- args: formatArgSummary(c.args),
696
- }));
697
- renderOutput(rows, {
698
- fmt,
699
- columns: ['command', 'site', 'name', 'aliases', 'description', 'access', 'strategy', 'browser', 'args',
700
- ...(isStructured ? ['columns', 'domain'] : [])],
701
- title: 'webcmd/list',
702
- source: 'webcmd list',
703
- });
667
+ const externalClis = opts.format === 'table' ? loadExternalClis() : [];
668
+ const presentation = commandListPresentation([...new Set(getRegistry().values())].map(toPresentableCommand), opts.format, {
669
+ externalClis: externalClis.map((external) => ({
670
+ label: formatExternalCliLabel(external),
671
+ installed: isBinaryInstalled(external.binary),
672
+ ...(external.description ? { description: external.description } : {}),
673
+ })),
674
+ });
675
+ if (presentation.displayLines) {
676
+ for (const line of presentation.displayLines)
677
+ console.log(line);
704
678
  return;
705
679
  }
706
- // Table (default) — grouped by adapter kind (app vs site), then by site name.
707
- // classifyAdapter() reads the `domain` field: DNS-style domains are sites;
708
- // localhost/loopback endpoints and bare app names are apps.
709
- const appsBySite = new Map();
710
- const sitesBySite = new Map();
711
- for (const cmd of commands) {
712
- const target = classifyAdapter(cmd.domain) === 'app' ? appsBySite : sitesBySite;
713
- const g = target.get(cmd.site) ?? [];
714
- g.push(cmd);
715
- target.set(cmd.site, g);
716
- }
717
- const renderSiteGroup = (site, cmds) => {
718
- console.log(` ${site}`);
719
- for (const cmd of cmds) {
720
- const label = strategyLabel(cmd);
721
- const tag = label === 'public'
722
- ? '[public]'
723
- : `[${label}]`;
724
- const aliases = cmd.aliases?.length ? ` (aliases: ${cmd.aliases.join(', ')})` : '';
725
- console.log(` ${cmd.name} ${tag}${aliases}${cmd.description ? ` — ${cmd.description}` : ''}`);
726
- }
727
- console.log();
728
- };
729
- console.log();
730
- console.log(' webcmd' + ' — available commands');
731
- console.log();
732
- if (appsBySite.size > 0) {
733
- console.log(' App adapters');
734
- console.log();
735
- for (const [site, cmds] of appsBySite)
736
- renderSiteGroup(site, cmds);
737
- }
738
- if (sitesBySite.size > 0) {
739
- console.log(' Site adapters');
740
- console.log();
741
- for (const [site, cmds] of sitesBySite)
742
- renderSiteGroup(site, cmds);
743
- }
744
- const externalClis = loadExternalClis();
745
- if (externalClis.length > 0) {
746
- console.log(' external CLIs');
747
- for (const ext of externalClis) {
748
- const isInstalled = isBinaryInstalled(ext.binary);
749
- const tag = isInstalled ? '[installed]' : '[auto-install]';
750
- console.log(` ${formatExternalCliLabel(ext)} ${tag}${ext.description ? ` — ${ext.description}` : ''}`);
751
- }
752
- console.log();
753
- }
754
- console.log(` ${commands.length} built-in commands across ${appsBySite.size} apps + ${sitesBySite.size} sites, ${externalClis.length} external CLIs`);
755
- console.log();
680
+ renderOutput(presentation.rows, {
681
+ fmt: opts.format,
682
+ columns: presentation.columns,
683
+ title: 'webcmd/list',
684
+ source: 'webcmd list',
685
+ });
756
686
  });
757
687
  // ── Built-in: validate / verify ───────────────────────────────────────────
758
688
  program
@@ -1204,8 +1134,8 @@ Examples:
1204
1134
  addBrowserTabOption(browser.command('screenshot').argument('[path]', 'Save to file (base64 if omitted)'))
1205
1135
  .option('--full-page', 'Capture the full scrollable page, not just the viewport', false)
1206
1136
  .option('--annotate', 'Overlay visible browser state ref labels on the screenshot', false)
1207
- .option('--width <n>', 'Override viewport width in CSS pixels for this screenshot only', (v) => parseScreenshotDim(v, 'width'))
1208
- .option('--height <n>', 'Override viewport height in CSS pixels for this screenshot only (ignored with --full-page)', (v) => parseScreenshotDim(v, 'height'))
1137
+ .option('--width <n>', 'Override viewport width in CSS pixels for this screenshot only', (value) => browserOptionValueParser('screenshot', 'width')(value))
1138
+ .option('--height <n>', 'Override viewport height in CSS pixels for this screenshot only (ignored with --full-page)', (value) => browserOptionValueParser('screenshot', 'height')(value))
1209
1139
  .description('Take screenshot')
1210
1140
  .action(browserAction(async (page, path, opts) => {
1211
1141
  const shotOpts = {
@@ -1858,8 +1788,13 @@ Examples:
1858
1788
  process.exitCode = EXIT_CODES.USAGE_ERROR;
1859
1789
  return;
1860
1790
  }
1861
- const { matches_n, match_level } = await page.click(resolvedTarget, parsed.opts);
1862
- console.log(JSON.stringify({ clicked: true, target: resolvedTarget, matches_n, match_level }, null, 2));
1791
+ const { matches_n, match_level, click_method, hit, retargeted } = await page.click(resolvedTarget, parsed.opts);
1792
+ console.log(JSON.stringify({
1793
+ clicked: true, target: resolvedTarget, matches_n, match_level,
1794
+ ...(click_method && { click_method }),
1795
+ ...(hit && { hit }),
1796
+ ...(retargeted && { retargeted }),
1797
+ }, null, 2));
1863
1798
  }));
1864
1799
  addBrowserTabOption(addSemanticLocatorOptions(browser.command('type'))
1865
1800
  .argument('[targetOrText]', 'Numeric ref/CSS target, or text when using --role/--name/etc.')
@@ -2835,10 +2770,7 @@ cli({
2835
2770
  const report = await runBrowserDoctor({ cliVersion: PKG_VERSION });
2836
2771
  console.log(renderBrowserDoctorReport(report));
2837
2772
  });
2838
- program
2839
- .command('completion')
2840
- .description('Output shell completion script')
2841
- .argument('<shell>', 'Shell type: bash, zsh, or fish')
2773
+ configureCompletionCommandSurface(program.command('completion'))
2842
2774
  .action((shell) => {
2843
2775
  printCompletionScript(shell);
2844
2776
  });
@@ -3102,12 +3034,32 @@ cli({
3102
3034
  .argument('<name>', 'Plugin name (lowercase, hyphens allowed)')
3103
3035
  .option('-d, --dir <path>', 'Output directory (default: ./<name>)')
3104
3036
  .option('--description <text>', 'Plugin description')
3037
+ .option('--author-name <name>', 'Author display name')
3038
+ .option('--author-handle <handle>', 'Author GitHub handle')
3105
3039
  .action(async (name, opts) => {
3106
3040
  const { createPluginScaffold } = await import('./plugin-scaffold.js');
3107
3041
  try {
3042
+ let authorName = opts.authorName?.trim();
3043
+ let authorHandle = opts.authorHandle?.trim();
3044
+ if ((!authorName || !authorHandle) && process.stdin.isTTY && process.stdout.isTTY) {
3045
+ const rl = readline.createInterface({ input: process.stdin, output: process.stdout });
3046
+ try {
3047
+ if (!authorName)
3048
+ authorName = (await rl.question('Author name: ')).trim();
3049
+ if (!authorHandle)
3050
+ authorHandle = (await rl.question('GitHub handle: ')).trim();
3051
+ }
3052
+ finally {
3053
+ rl.close();
3054
+ }
3055
+ }
3108
3056
  const result = createPluginScaffold(name, {
3109
3057
  dir: opts.dir,
3110
3058
  description: opts.description,
3059
+ author: {
3060
+ name: authorName ?? '',
3061
+ handle: authorHandle ?? '',
3062
+ },
3111
3063
  });
3112
3064
  console.log(`✅ Plugin scaffold created at ${result.dir}`);
3113
3065
  console.log();
@@ -3480,7 +3432,7 @@ cli({
3480
3432
  applyAncestorAwareUsage(sub);
3481
3433
  }
3482
3434
  applyAncestorAwareUsage(browser);
3483
- installStructuredHelp(program, () => rootHelpData(program, adapterGroups), () => formatRootAdapterHelpText(adapterGroups));
3435
+ installRootPresentationHelp(program, () => rootHelpData(program, adapterGroups), buildRootHelpPresentation(program, adapterGroups));
3484
3436
  // ── Unknown command fallback ──────────────────────────────────────────────
3485
3437
  // Security: do NOT auto-discover and register arbitrary system binaries.
3486
3438
  // Only explicitly registered external CLIs are allowed.
@@ -7,7 +7,9 @@ import { cli, getRegistry, Strategy } from './registry.js';
7
7
  import { BrowserCommandError } from './browser/daemon-client.js';
8
8
  import { TargetError } from './browser/target-errors.js';
9
9
  import { PKG_VERSION } from './version.js';
10
- import { classifyAdapter } from './help.js';
10
+ import { classifyAdapter, getInstalledRootHelpPresentation } from './help.js';
11
+ import { commandListPresentation, formatRootHelp, toPresentableCommand, } from './command-presentation.js';
12
+ import { render as renderOutput } from './output.js';
11
13
  const { mockBrowserConnect, mockBrowserClose, mockBindTab, mockSendCommand, mockExecFileSync, browserState, } = vi.hoisted(() => ({
12
14
  mockBrowserConnect: vi.fn(),
13
15
  mockBrowserClose: vi.fn(),
@@ -94,6 +96,14 @@ describe('createProgram root help descriptions', () => {
94
96
  expect(descriptionFor(program, 'list')).toBe('List all available CLI commands');
95
97
  expect(descriptionFor(program, 'doctor')).toBe('Diagnose webcmd browser bridge connectivity');
96
98
  });
99
+ it('renders the actual local root through the shared root presentation seam', () => {
100
+ const program = createProgram('', '');
101
+ const presentation = getInstalledRootHelpPresentation(program);
102
+ const commanderHelp = program.createHelp();
103
+ expect(presentation).toBeDefined();
104
+ expect(presentation.baseText).toBe(commanderHelp.formatHelp(program, commanderHelp));
105
+ expect(program.helpInformation()).toBe(formatRootHelp(presentation));
106
+ });
97
107
  it('keeps site adapters out of root commands and lists sites in the root help tail', () => {
98
108
  const registry = getRegistry();
99
109
  const snapshot = new Map(registry);
@@ -274,6 +284,45 @@ describe('createProgram root help descriptions', () => {
274
284
  registry.set(key, value);
275
285
  }
276
286
  });
287
+ it.each(['json', 'yaml', 'yml'])('renders local list %s through the shared list presentation', async (format) => {
288
+ const registry = getRegistry();
289
+ const snapshot = new Map(registry);
290
+ registry.clear();
291
+ try {
292
+ const command = cli({
293
+ site: 'github',
294
+ name: 'issues',
295
+ aliases: ['issue-list'],
296
+ access: 'read',
297
+ description: 'List repository issues',
298
+ strategy: Strategy.PUBLIC,
299
+ browser: false,
300
+ args: [{ name: 'limit', type: 'int', default: 20, help: 'Maximum issues' }],
301
+ columns: ['number', 'title'],
302
+ });
303
+ const presentation = commandListPresentation([toPresentableCommand(command)], format);
304
+ const outputSpy = vi.mocked(console.log);
305
+ outputSpy.mockClear();
306
+ const program = createProgram('', '');
307
+ await program.parseAsync(['node', 'webcmd', 'list', '-f', format]);
308
+ const actual = outputSpy.mock.calls.flat().join('\n');
309
+ outputSpy.mockClear();
310
+ renderOutput(presentation.rows, {
311
+ fmt: format,
312
+ columns: presentation.columns,
313
+ title: 'webcmd/list',
314
+ source: 'webcmd list',
315
+ });
316
+ const expected = outputSpy.mock.calls.flat().join('\n');
317
+ outputSpy.mockClear();
318
+ expect(actual).toBe(expected);
319
+ }
320
+ finally {
321
+ registry.clear();
322
+ for (const [key, value] of snapshot)
323
+ registry.set(key, value);
324
+ }
325
+ });
277
326
  it('exposes external_clis / app_adapters / site_adapters in structured help', () => {
278
327
  const registry = getRegistry();
279
328
  const snapshot = new Map(registry);
@@ -489,6 +538,7 @@ describe('createProgram root help descriptions', () => {
489
538
  expect(data.command).toBe('webcmd browser');
490
539
  expect(data.description).toBe('Browser control — navigate, click, type, extract, wait (no LLM needed)');
491
540
  expect(data.command_count).toBeGreaterThan(20);
541
+ expect(data.commands.map((cmd) => cmd.name)).toContain('bind');
492
542
  // `--session` is now a hidden internal option; user-facing surface is the
493
543
  // <session> positional declared via `.usage()`. Structured help drops
494
544
  // hidden options, so namespace_options shouldn't expose it.
@@ -2499,6 +2549,26 @@ describe('browser click/type commands', () => {
2499
2549
  expect(browserState.page.click).toHaveBeenCalledWith('#save', {});
2500
2550
  expect(lastJsonLog()).toEqual({ clicked: true, target: '#save', matches_n: 1, match_level: 'exact' });
2501
2551
  });
2552
+ it('surfaces click hit-testing diagnostics', async () => {
2553
+ browserState.page.click.mockResolvedValueOnce({
2554
+ matches_n: 1,
2555
+ match_level: 'exact',
2556
+ click_method: 'js',
2557
+ hit: 'other',
2558
+ retargeted: true,
2559
+ });
2560
+ const program = createProgram('', '');
2561
+ await program.parseAsync(['node', 'webcmd', 'browser', '--session', 'test', 'click', '#covered']);
2562
+ expect(lastJsonLog()).toEqual({
2563
+ clicked: true,
2564
+ target: '#covered',
2565
+ matches_n: 1,
2566
+ match_level: 'exact',
2567
+ click_method: 'js',
2568
+ hit: 'other',
2569
+ retargeted: true,
2570
+ });
2571
+ });
2502
2572
  it('clicks a unique semantic locator without a prior state call', async () => {
2503
2573
  browserState.page.evaluate.mockResolvedValueOnce({
2504
2574
  matches_n: 1,
@@ -0,0 +1,95 @@
1
+ import type { Arg } from './registry.js';
2
+ export interface PresentableCommand {
3
+ site: string;
4
+ name: string;
5
+ aliases: string[];
6
+ description: string;
7
+ access: 'read' | 'write';
8
+ strategy: string;
9
+ browser: boolean;
10
+ args: readonly Arg[];
11
+ columns: readonly string[];
12
+ defaultFormat?: string;
13
+ domain?: string;
14
+ example?: string;
15
+ siteSession?: string;
16
+ }
17
+ export interface PresentableCommandSource {
18
+ site: string;
19
+ name: string;
20
+ aliases?: readonly string[];
21
+ description: string;
22
+ access: string;
23
+ strategy?: string;
24
+ browser?: boolean;
25
+ args: readonly Arg[];
26
+ columns?: readonly string[];
27
+ defaultFormat?: string | null;
28
+ domain?: string | null;
29
+ example?: string;
30
+ siteSession?: string;
31
+ }
32
+ export interface RootHelpCommand {
33
+ name: string;
34
+ description: string;
35
+ }
36
+ export interface RootHelpOption {
37
+ flags: string;
38
+ description: string;
39
+ }
40
+ export interface RootHelpGroup {
41
+ label: string;
42
+ items: readonly string[];
43
+ }
44
+ export interface RootHelpPresentation {
45
+ description: string;
46
+ usage?: readonly string[];
47
+ commands: readonly RootHelpCommand[];
48
+ options: readonly RootHelpOption[];
49
+ /** Commander-generated body retained by local mode for byte-compatible layout. */
50
+ baseText?: string;
51
+ groups?: readonly RootHelpGroup[];
52
+ footer?: readonly string[];
53
+ localOnlyCommands?: readonly RootHelpCommand[];
54
+ localOnlyExplanation?: string;
55
+ }
56
+ export interface CommandListPresentation {
57
+ rows: Record<string, unknown>[];
58
+ columns: string[];
59
+ structured: boolean;
60
+ displayLines?: string[];
61
+ }
62
+ export interface PresentableExternalCli {
63
+ label: string;
64
+ installed: boolean;
65
+ description?: string;
66
+ }
67
+ export interface CompletableCommand {
68
+ site: string;
69
+ name: string;
70
+ aliases?: readonly string[];
71
+ }
72
+ export declare function toPresentableCommand(command: PresentableCommandSource): PresentableCommand;
73
+ export declare function formatRootHelp(presentation: RootHelpPresentation): string;
74
+ export declare function commandListRows(commands: readonly PresentableCommand[], structured: boolean): Record<string, unknown>[];
75
+ export declare function commandListPresentation(commands: readonly PresentableCommand[], format: string, options?: {
76
+ externalClis?: readonly PresentableExternalCli[];
77
+ }): CommandListPresentation;
78
+ export type AdapterKind = 'site' | 'app';
79
+ export declare function classifyAdapterDomain(domain: string | undefined): AdapterKind;
80
+ export declare function getCommandCompletionCandidates(commands: readonly CompletableCommand[], words: readonly string[], cursor: number, builtins: readonly string[]): string[];
81
+ export declare function formatCommandListTerm(command: PresentableCommand): string;
82
+ export declare function siteHelpData(site: string, commands: readonly PresentableCommand[]): Record<string, unknown>;
83
+ export declare function commandHelpData(command: PresentableCommand): Record<string, unknown>;
84
+ export declare function formatCommonOptionsHelp(): string;
85
+ export declare function formatBrowserCommonOptionsHelp(): string;
86
+ export declare function formatSiteHelp(site: string, commands: readonly PresentableCommand[]): string;
87
+ export declare function formatCommandHelp(command: PresentableCommand): string;
88
+ export declare function formatSiteCommandDescription(command: PresentableCommand): string;
89
+ export declare function wrapCommaList(items: readonly string[], opts?: {
90
+ width?: number;
91
+ indent?: string;
92
+ }): string;
93
+ export declare function serializePresentableArg(arg: Arg): Record<string, unknown>;
94
+ export declare function formatArgumentSummary(args: readonly Arg[]): string;
95
+ export declare function formatPresentableCommandExample(command: PresentableCommand): string;