@agentrhq/webcmd 0.2.5 → 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 (200) hide show
  1. package/README.md +13 -2
  2. package/cli-manifest.json +217 -13
  3. package/clis/_shared/site-auth.js +3 -3
  4. package/clis/_shared/site-auth.test.js +4 -4
  5. package/clis/chatgpt/ask.js +1 -1
  6. package/clis/chatgpt/ask.test.js +11 -0
  7. package/clis/chatgpt/commands.test.js +102 -1
  8. package/clis/chatgpt/deep-research-result.js +45 -3
  9. package/clis/chatgpt/image.js +22 -2
  10. package/clis/chatgpt/project-file-add.js +24 -1
  11. package/clis/chatgpt/utils.js +258 -52
  12. package/clis/chatgpt/utils.test.js +259 -1
  13. package/clis/claude/ask.js +21 -1
  14. package/clis/facebook/feed.js +110 -24
  15. package/clis/facebook/feed.test.js +62 -0
  16. package/clis/facebook/search.js +185 -38
  17. package/clis/facebook/search.test.js +95 -50
  18. package/clis/instagram/explore.js +30 -12
  19. package/clis/instagram/explore.test.js +77 -0
  20. package/clis/instagram/post.js +14 -1
  21. package/clis/instagram/reel.js +13 -1
  22. package/clis/linkedin/company.js +153 -0
  23. package/clis/linkedin/company.test.js +111 -0
  24. package/clis/linkedin/connections.js +135 -0
  25. package/clis/linkedin/connections.test.js +141 -0
  26. package/clis/mercury/reimbursement-draft.js +12 -1
  27. package/clis/slock/whoami.test.js +2 -2
  28. package/clis/twitter/article-evaluate.test.js +27 -0
  29. package/clis/twitter/article.js +73 -16
  30. package/clis/twitter/article.test.js +209 -0
  31. package/clis/twitter/download.js +6 -1
  32. package/clis/twitter/post.js +14 -1
  33. package/clis/twitter/profile.js +1 -1
  34. package/clis/twitter/profile.test.js +8 -0
  35. package/clis/twitter/quote.js +11 -1
  36. package/clis/twitter/reply.js +11 -1
  37. package/dist/src/browser/base-page.d.ts +26 -0
  38. package/dist/src/browser/base-page.js +23 -8
  39. package/dist/src/browser/base-page.test.js +45 -12
  40. package/dist/src/browser/command-catalog.d.ts +6 -0
  41. package/dist/src/browser/command-catalog.js +259 -0
  42. package/dist/src/browser/command-catalog.test.d.ts +1 -0
  43. package/dist/src/browser/command-catalog.test.js +79 -0
  44. package/dist/src/browser/daemon-client.d.ts +2 -1
  45. package/dist/src/browser/runtime/local-cloak/actions.js +4 -1
  46. package/dist/src/browser/runtime/local-cloak/darwin-background-launch.d.ts +17 -0
  47. package/dist/src/browser/runtime/local-cloak/darwin-background-launch.js +106 -0
  48. package/dist/src/browser/runtime/local-cloak/darwin-background-launch.test.d.ts +1 -0
  49. package/dist/src/browser/runtime/local-cloak/darwin-background-launch.test.js +65 -0
  50. package/dist/src/browser/runtime/local-cloak/provider.test.js +36 -0
  51. package/dist/src/browser/runtime/local-cloak/session-manager.d.ts +10 -2
  52. package/dist/src/browser/runtime/local-cloak/session-manager.js +25 -9
  53. package/dist/src/browser/runtime/local-cloak/session-manager.test.js +36 -0
  54. package/dist/src/browser/target-resolver.d.ts +1 -0
  55. package/dist/src/browser/target-resolver.js +99 -4
  56. package/dist/src/browser/target-resolver.test.js +141 -1
  57. package/dist/src/build-manifest.d.ts +6 -0
  58. package/dist/src/build-manifest.js +18 -1
  59. package/dist/src/build-manifest.test.js +57 -1
  60. package/dist/src/builtin-command-surface.d.ts +9 -0
  61. package/dist/src/builtin-command-surface.js +16 -0
  62. package/dist/src/check-hosted-contract.test.d.ts +1 -0
  63. package/dist/src/check-hosted-contract.test.js +56 -0
  64. package/dist/src/cli.js +63 -110
  65. package/dist/src/cli.test.js +82 -6
  66. package/dist/src/command-presentation.d.ts +95 -0
  67. package/dist/src/command-presentation.js +486 -0
  68. package/dist/src/command-presentation.test.d.ts +1 -0
  69. package/dist/src/command-presentation.test.js +97 -0
  70. package/dist/src/command-surface.d.ts +43 -0
  71. package/dist/src/command-surface.js +205 -0
  72. package/dist/src/command-surface.test.d.ts +1 -0
  73. package/dist/src/command-surface.test.js +406 -0
  74. package/dist/src/commanderAdapter.d.ts +5 -1
  75. package/dist/src/commanderAdapter.js +12 -55
  76. package/dist/src/commanderAdapter.test.js +15 -3
  77. package/dist/src/commands/auth.js +3 -2
  78. package/dist/src/commands/auth.test.js +6 -6
  79. package/dist/src/community-plugin-sync.d.ts +11 -0
  80. package/dist/src/community-plugin-sync.js +138 -0
  81. package/dist/src/community-plugin-sync.test.d.ts +1 -0
  82. package/dist/src/community-plugin-sync.test.js +123 -0
  83. package/dist/src/completion-fast.d.ts +4 -4
  84. package/dist/src/completion-fast.js +15 -34
  85. package/dist/src/completion-shared.d.ts +4 -0
  86. package/dist/src/completion-shared.js +38 -0
  87. package/dist/src/completion.js +3 -27
  88. package/dist/src/completion.test.js +31 -2
  89. package/dist/src/docs-sync-review-cli.test.d.ts +1 -0
  90. package/dist/src/docs-sync-review-cli.test.js +322 -0
  91. package/dist/src/docs-sync-review.d.ts +117 -0
  92. package/dist/src/docs-sync-review.js +475 -0
  93. package/dist/src/docs-sync-review.test.d.ts +1 -0
  94. package/dist/src/docs-sync-review.test.js +324 -0
  95. package/dist/src/errors.d.ts +12 -3
  96. package/dist/src/errors.js +11 -7
  97. package/dist/src/errors.test.js +14 -1
  98. package/dist/src/execution.d.ts +1 -2
  99. package/dist/src/execution.js +4 -45
  100. package/dist/src/generate-release-notes-cli.test.js +11 -1
  101. package/dist/src/help.d.ts +4 -0
  102. package/dist/src/help.js +50 -255
  103. package/dist/src/help.test.js +27 -1
  104. package/dist/src/hosted/args.d.ts +4 -0
  105. package/dist/src/hosted/args.js +4 -0
  106. package/dist/src/hosted/args.test.d.ts +1 -0
  107. package/dist/src/hosted/args.test.js +49 -0
  108. package/dist/src/hosted/availability.d.ts +13 -0
  109. package/dist/src/hosted/availability.js +16 -0
  110. package/dist/src/hosted/availability.test.d.ts +1 -0
  111. package/dist/src/hosted/availability.test.js +180 -0
  112. package/dist/src/hosted/browser-args.d.ts +18 -0
  113. package/dist/src/hosted/browser-args.js +152 -0
  114. package/dist/src/hosted/browser-args.test.d.ts +1 -0
  115. package/dist/src/hosted/browser-args.test.js +182 -0
  116. package/dist/src/hosted/client.d.ts +59 -0
  117. package/dist/src/hosted/client.js +510 -0
  118. package/dist/src/hosted/client.test.d.ts +1 -0
  119. package/dist/src/hosted/client.test.js +738 -0
  120. package/dist/src/hosted/config.d.ts +55 -0
  121. package/dist/src/hosted/config.js +112 -0
  122. package/dist/src/hosted/config.test.d.ts +1 -0
  123. package/dist/src/hosted/config.test.js +81 -0
  124. package/dist/src/hosted/contract.d.ts +94 -0
  125. package/dist/src/hosted/contract.js +208 -0
  126. package/dist/src/hosted/contract.test.d.ts +1 -0
  127. package/dist/src/hosted/contract.test.js +361 -0
  128. package/dist/src/hosted/credentials.d.ts +38 -0
  129. package/dist/src/hosted/credentials.js +248 -0
  130. package/dist/src/hosted/credentials.test.d.ts +1 -0
  131. package/dist/src/hosted/credentials.test.js +93 -0
  132. package/dist/src/hosted/file-contract.test.d.ts +1 -0
  133. package/dist/src/hosted/file-contract.test.js +174 -0
  134. package/dist/src/hosted/files.d.ts +29 -0
  135. package/dist/src/hosted/files.js +296 -0
  136. package/dist/src/hosted/files.test.d.ts +1 -0
  137. package/dist/src/hosted/files.test.js +231 -0
  138. package/dist/src/hosted/main-lifecycle.test.d.ts +1 -0
  139. package/dist/src/hosted/main-lifecycle.test.js +213 -0
  140. package/dist/src/hosted/manifest.d.ts +14 -0
  141. package/dist/src/hosted/manifest.js +47 -0
  142. package/dist/src/hosted/manifest.test.d.ts +1 -0
  143. package/dist/src/hosted/manifest.test.js +164 -0
  144. package/dist/src/hosted/output-parity.test.d.ts +1 -0
  145. package/dist/src/hosted/output-parity.test.js +108 -0
  146. package/dist/src/hosted/root-command-surface.test.d.ts +1 -0
  147. package/dist/src/hosted/root-command-surface.test.js +646 -0
  148. package/dist/src/hosted/runner.d.ts +19 -0
  149. package/dist/src/hosted/runner.js +705 -0
  150. package/dist/src/hosted/runner.test.d.ts +1 -0
  151. package/dist/src/hosted/runner.test.js +1607 -0
  152. package/dist/src/hosted/setup.d.ts +10 -0
  153. package/dist/src/hosted/setup.js +79 -0
  154. package/dist/src/hosted/setup.test.d.ts +1 -0
  155. package/dist/src/hosted/setup.test.js +195 -0
  156. package/dist/src/hosted/types.d.ts +175 -0
  157. package/dist/src/hosted/types.js +1 -0
  158. package/dist/src/main.js +123 -97
  159. package/dist/src/manifest-types.d.ts +2 -0
  160. package/dist/src/output.d.ts +17 -2
  161. package/dist/src/output.js +88 -81
  162. package/dist/src/output.test.js +141 -51
  163. package/dist/src/pipeline/executor.test.js +1 -0
  164. package/dist/src/pipeline/steps/download.test.js +1 -0
  165. package/dist/src/plugin-create-cli.test.d.ts +1 -0
  166. package/dist/src/plugin-create-cli.test.js +37 -0
  167. package/dist/src/plugin-manifest.d.ts +12 -0
  168. package/dist/src/plugin-manifest.js +16 -0
  169. package/dist/src/plugin-manifest.test.js +19 -1
  170. package/dist/src/plugin-scaffold.d.ts +4 -1
  171. package/dist/src/plugin-scaffold.js +4 -1
  172. package/dist/src/plugin-scaffold.test.js +23 -8
  173. package/dist/src/plugin.js +4 -1
  174. package/dist/src/plugin.test.js +13 -0
  175. package/dist/src/registry.d.ts +2 -0
  176. package/dist/src/release-notes.d.ts +6 -1
  177. package/dist/src/release-notes.js +192 -4
  178. package/dist/src/release-notes.test.js +143 -4
  179. package/dist/src/root-command-surface.d.ts +31 -0
  180. package/dist/src/root-command-surface.js +106 -0
  181. package/dist/src/serialization.d.ts +1 -16
  182. package/dist/src/serialization.js +5 -55
  183. package/dist/src/stream-write.d.ts +12 -0
  184. package/dist/src/stream-write.js +91 -0
  185. package/dist/src/stream-write.test.d.ts +1 -0
  186. package/dist/src/stream-write.test.js +186 -0
  187. package/dist/src/types.d.ts +5 -0
  188. package/dist/src/utils.d.ts +1 -1
  189. package/dist/src/utils.js +2 -8
  190. package/dist/src/utils.test.js +50 -0
  191. package/hosted-contract.json +36748 -0
  192. package/package.json +6 -1
  193. package/scripts/check-hosted-contract.mjs +108 -0
  194. package/scripts/docs-sync-review.ts +332 -0
  195. package/scripts/generate-release-notes.ts +1 -1
  196. package/scripts/sync-community-plugins.ts +14 -0
  197. package/skills/smart-search/SKILL.md +14 -3
  198. package/skills/webcmd-autofix/SKILL.md +26 -37
  199. package/skills/webcmd-usage/SKILL.md +21 -7
  200. package/clis/antigravity/SKILL.md +0 -38
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
@@ -1022,11 +952,12 @@ Examples:
1022
952
  const profileSelection = getBrowserProfileSelection(command);
1023
953
  const contextId = profileSelection?.contextId;
1024
954
  const routing = profileRouteParams(profileSelection);
955
+ const windowMode = getBrowserWindowMode(command, 'foreground');
1025
956
  try {
1026
957
  const { BrowserBridge } = await import('./browser/index.js');
1027
958
  const bridge = new BrowserBridge();
1028
- await bridge.connect({ timeout: DEFAULT_BROWSER_CONNECT_TIMEOUT, session, surface: 'browser', ...routing });
1029
- await fn({ session, contextId, routing }, opts);
959
+ await bridge.connect({ timeout: DEFAULT_BROWSER_CONNECT_TIMEOUT, session, surface: 'browser', ...routing, windowMode });
960
+ await fn({ session, contextId, routing, windowMode }, opts);
1030
961
  }
1031
962
  catch (err) {
1032
963
  if (err instanceof BrowserCommandError) {
@@ -1044,7 +975,7 @@ Examples:
1044
975
  .description('Bind an existing Cloak runtime tab to the browser session named by <session>')
1045
976
  .option('--page <id>', 'Cloak tab page id from `webcmd browser <session> tab list`')
1046
977
  .option('--index <n>', 'Cloak tab index from `webcmd browser <session> tab list`')
1047
- .action(browserSessionCommandAction(async ({ session, contextId, routing }, opts) => {
978
+ .action(browserSessionCommandAction(async ({ session, contextId, routing, windowMode }, opts) => {
1048
979
  const page = typeof opts.page === 'string' && opts.page.trim() ? opts.page.trim() : undefined;
1049
980
  const rawIndex = typeof opts.index === 'string' && opts.index.trim() ? opts.index.trim() : undefined;
1050
981
  if ((page && rawIndex) || (!page && !rawIndex)) {
@@ -1054,7 +985,7 @@ Examples:
1054
985
  throw new BrowserCommandError('--index must be a non-negative integer.', 'invalid_request');
1055
986
  }
1056
987
  const index = rawIndex === undefined ? undefined : Number.parseInt(rawIndex, 10);
1057
- const data = await bindTab(session, { ...routing, ...(page && { page }), ...(index !== undefined && { index }) });
988
+ const data = await bindTab(session, { ...routing, ...(page && { page }), ...(index !== undefined && { index }), windowMode });
1058
989
  saveBrowserTargetState(undefined, getBrowserScope(session, contextId));
1059
990
  console.log(JSON.stringify({ session, ...((data && typeof data === 'object') ? data : { data }) }, null, 2));
1060
991
  }));
@@ -1203,8 +1134,8 @@ Examples:
1203
1134
  addBrowserTabOption(browser.command('screenshot').argument('[path]', 'Save to file (base64 if omitted)'))
1204
1135
  .option('--full-page', 'Capture the full scrollable page, not just the viewport', false)
1205
1136
  .option('--annotate', 'Overlay visible browser state ref labels on the screenshot', false)
1206
- .option('--width <n>', 'Override viewport width in CSS pixels for this screenshot only', (v) => parseScreenshotDim(v, 'width'))
1207
- .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))
1208
1139
  .description('Take screenshot')
1209
1140
  .action(browserAction(async (page, path, opts) => {
1210
1141
  const shotOpts = {
@@ -1857,8 +1788,13 @@ Examples:
1857
1788
  process.exitCode = EXIT_CODES.USAGE_ERROR;
1858
1789
  return;
1859
1790
  }
1860
- const { matches_n, match_level } = await page.click(resolvedTarget, parsed.opts);
1861
- 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));
1862
1798
  }));
1863
1799
  addBrowserTabOption(addSemanticLocatorOptions(browser.command('type'))
1864
1800
  .argument('[targetOrText]', 'Numeric ref/CSS target, or text when using --role/--name/etc.')
@@ -2834,10 +2770,7 @@ cli({
2834
2770
  const report = await runBrowserDoctor({ cliVersion: PKG_VERSION });
2835
2771
  console.log(renderBrowserDoctorReport(report));
2836
2772
  });
2837
- program
2838
- .command('completion')
2839
- .description('Output shell completion script')
2840
- .argument('<shell>', 'Shell type: bash, zsh, or fish')
2773
+ configureCompletionCommandSurface(program.command('completion'))
2841
2774
  .action((shell) => {
2842
2775
  printCompletionScript(shell);
2843
2776
  });
@@ -3101,12 +3034,32 @@ cli({
3101
3034
  .argument('<name>', 'Plugin name (lowercase, hyphens allowed)')
3102
3035
  .option('-d, --dir <path>', 'Output directory (default: ./<name>)')
3103
3036
  .option('--description <text>', 'Plugin description')
3037
+ .option('--author-name <name>', 'Author display name')
3038
+ .option('--author-handle <handle>', 'Author GitHub handle')
3104
3039
  .action(async (name, opts) => {
3105
3040
  const { createPluginScaffold } = await import('./plugin-scaffold.js');
3106
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
+ }
3107
3056
  const result = createPluginScaffold(name, {
3108
3057
  dir: opts.dir,
3109
3058
  description: opts.description,
3059
+ author: {
3060
+ name: authorName ?? '',
3061
+ handle: authorHandle ?? '',
3062
+ },
3110
3063
  });
3111
3064
  console.log(`✅ Plugin scaffold created at ${result.dir}`);
3112
3065
  console.log();
@@ -3479,7 +3432,7 @@ cli({
3479
3432
  applyAncestorAwareUsage(sub);
3480
3433
  }
3481
3434
  applyAncestorAwareUsage(browser);
3482
- installStructuredHelp(program, () => rootHelpData(program, adapterGroups), () => formatRootAdapterHelpText(adapterGroups));
3435
+ installRootPresentationHelp(program, () => rootHelpData(program, adapterGroups), buildRootHelpPresentation(program, adapterGroups));
3483
3436
  // ── Unknown command fallback ──────────────────────────────────────────────
3484
3437
  // Security: do NOT auto-discover and register arbitrary system binaries.
3485
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.
@@ -1109,8 +1159,8 @@ describe('browser tab targeting commands', () => {
1109
1159
  it('binds an existing Cloak tab by page id into a browser session', async () => {
1110
1160
  const program = createProgram('', '');
1111
1161
  await program.parseAsync(['node', 'webcmd', 'browser', '--session', 'test', 'bind', '--page', 'tab-2']);
1112
- expect(mockBrowserConnect).toHaveBeenCalledWith({ timeout: 45, session: 'test', surface: 'browser' });
1113
- expect(mockBindTab).toHaveBeenCalledWith('test', { page: 'tab-2' });
1162
+ expect(mockBrowserConnect).toHaveBeenCalledWith({ timeout: 45, session: 'test', surface: 'browser', windowMode: 'foreground' });
1163
+ expect(mockBindTab).toHaveBeenCalledWith('test', { page: 'tab-2', windowMode: 'foreground' });
1114
1164
  const out = lastJsonLog();
1115
1165
  expect(out.session).toBe('test');
1116
1166
  expect(out.url).toBe('https://user.example/inbox');
@@ -1118,11 +1168,17 @@ describe('browser tab targeting commands', () => {
1118
1168
  it('binds an existing Cloak tab by index into a browser session', async () => {
1119
1169
  const program = createProgram('', '');
1120
1170
  await program.parseAsync(['node', 'webcmd', 'browser', '--session', 'test', 'bind', '--index', '1']);
1121
- expect(mockBrowserConnect).toHaveBeenCalledWith({ timeout: 45, session: 'test', surface: 'browser' });
1122
- expect(mockBindTab).toHaveBeenCalledWith('test', { index: 1 });
1171
+ expect(mockBrowserConnect).toHaveBeenCalledWith({ timeout: 45, session: 'test', surface: 'browser', windowMode: 'foreground' });
1172
+ expect(mockBindTab).toHaveBeenCalledWith('test', { index: 1, windowMode: 'foreground' });
1123
1173
  const out = lastJsonLog();
1124
1174
  expect(out.session).toBe('test');
1125
1175
  });
1176
+ it('passes background window mode when binding a Cloak tab', async () => {
1177
+ const program = createProgram('', '');
1178
+ await program.parseAsync(['node', 'webcmd', 'browser', '--session', 'test', '--window', 'background', 'bind', '--page', 'tab-2']);
1179
+ expect(mockBrowserConnect).toHaveBeenCalledWith({ timeout: 45, session: 'test', surface: 'browser', windowMode: 'background' });
1180
+ expect(mockBindTab).toHaveBeenCalledWith('test', { page: 'tab-2', windowMode: 'background' });
1181
+ });
1126
1182
  it('requires an explicit Cloak tab target for bind', async () => {
1127
1183
  const program = createProgram('', '');
1128
1184
  await program.parseAsync(['node', 'webcmd', 'browser', '--session', 'test', 'bind']);
@@ -1237,7 +1293,7 @@ describe('browser tab targeting commands', () => {
1237
1293
  it('unbinds a session through the daemon close-window command', async () => {
1238
1294
  const program = createProgram('', '');
1239
1295
  await program.parseAsync(['node', 'webcmd', 'browser', '--session', 'test', 'unbind']);
1240
- expect(mockBrowserConnect).toHaveBeenCalledWith({ timeout: 45, session: 'test', surface: 'browser' });
1296
+ expect(mockBrowserConnect).toHaveBeenCalledWith({ timeout: 45, session: 'test', surface: 'browser', windowMode: 'foreground' });
1241
1297
  expect(mockSendCommand).toHaveBeenCalledWith('close-window', { session: 'test', surface: 'browser' });
1242
1298
  const out = lastJsonLog();
1243
1299
  expect(out).toEqual({ unbound: true, session: 'test' });
@@ -2493,6 +2549,26 @@ describe('browser click/type commands', () => {
2493
2549
  expect(browserState.page.click).toHaveBeenCalledWith('#save', {});
2494
2550
  expect(lastJsonLog()).toEqual({ clicked: true, target: '#save', matches_n: 1, match_level: 'exact' });
2495
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
+ });
2496
2572
  it('clicks a unique semantic locator without a prior state call', async () => {
2497
2573
  browserState.page.evaluate.mockResolvedValueOnce({
2498
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;