browser-debugger-cli 0.10.0 → 0.12.0

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 (103) hide show
  1. package/.claude/skills/bdg/SKILL.md +268 -0
  2. package/README.md +148 -74
  3. package/dist/commands/css.d.ts +13 -0
  4. package/dist/commands/css.js +53 -0
  5. package/dist/commands/dom/audit.d.ts +14 -0
  6. package/dist/commands/dom/audit.js +87 -0
  7. package/dist/commands/dom/formInteraction.js +36 -6
  8. package/dist/commands/dom/helpers/keyAttributes.d.ts +3 -2
  9. package/dist/commands/dom/helpers/keyAttributes.js +6 -4
  10. package/dist/commands/dom/helpers/screenshot.d.ts +1 -0
  11. package/dist/commands/dom/helpers/screenshot.js +158 -38
  12. package/dist/commands/dom/index.js +4 -1
  13. package/dist/commands/dom/screenshot.js +10 -6
  14. package/dist/commands/dom/wait.js +5 -3
  15. package/dist/commands/helpJson.js +1 -1
  16. package/dist/commands/installSkill.d.ts +20 -0
  17. package/dist/commands/installSkill.js +87 -0
  18. package/dist/commands/optionBehaviors.js +21 -6
  19. package/dist/commands/page.js +7 -4
  20. package/dist/commands/peek.d.ts +7 -0
  21. package/dist/commands/peek.js +65 -23
  22. package/dist/commands/shared/optionTypes.d.ts +5 -1
  23. package/dist/commands/start.d.ts +13 -0
  24. package/dist/commands/start.js +19 -2
  25. package/dist/commands/tail.d.ts +7 -1
  26. package/dist/commands/tail.js +13 -62
  27. package/dist/commands.js +5 -0
  28. package/dist/daemon/session/commandRegistry.js +7 -1
  29. package/dist/daemon/session/plugins.js +4 -52
  30. package/dist/daemon.js +7986 -6848
  31. package/dist/errors/messages.d.ts +50 -4
  32. package/dist/errors/messages.js +94 -5
  33. package/dist/index.js +709 -190
  34. package/dist/ipc/client.d.ts +4 -0
  35. package/dist/ipc/client.js +8 -0
  36. package/dist/ipc/protocol/auditTypes.d.ts +129 -0
  37. package/dist/ipc/protocol/auditTypes.js +6 -0
  38. package/dist/ipc/protocol/commands.d.ts +23 -0
  39. package/dist/ipc/protocol/commands.js +2 -0
  40. package/dist/ipc/protocol/domTypes.d.ts +4 -0
  41. package/dist/ipc/protocol/inspectTypes.d.ts +71 -8
  42. package/dist/runtime/css/search.d.ts +39 -0
  43. package/dist/runtime/css/search.js +122 -0
  44. package/dist/runtime/dom/actionEffects.d.ts +4 -1
  45. package/dist/runtime/dom/actionEffects.js +8 -4
  46. package/dist/runtime/dom/audit.d.ts +19 -0
  47. package/dist/runtime/dom/audit.js +36 -0
  48. package/dist/runtime/dom/auditModel.d.ts +45 -0
  49. package/dist/runtime/dom/auditModel.js +215 -0
  50. package/dist/runtime/dom/auditScripts.d.ts +107 -0
  51. package/dist/runtime/dom/auditScripts.js +112 -0
  52. package/dist/runtime/dom/elementGeometry.d.ts +8 -2
  53. package/dist/runtime/dom/elementGeometry.js +24 -8
  54. package/dist/runtime/dom/elementInfo.d.ts +3 -2
  55. package/dist/runtime/dom/elementInfo.js +8 -2
  56. package/dist/runtime/dom/formFillHelpers/fill.js +2 -2
  57. package/dist/runtime/dom/inspect.d.ts +7 -0
  58. package/dist/runtime/dom/inspect.js +88 -23
  59. package/dist/runtime/dom/inspectAllStyles.d.ts +16 -4
  60. package/dist/runtime/dom/inspectAllStyles.js +89 -7
  61. package/dist/runtime/dom/inspectCascade.d.ts +19 -2
  62. package/dist/runtime/dom/inspectCascade.js +214 -44
  63. package/dist/runtime/dom/inspectCascadeModel.d.ts +8 -0
  64. package/dist/runtime/dom/inspectCascadeModel.js +108 -34
  65. package/dist/runtime/dom/inspectHints.d.ts +26 -3
  66. package/dist/runtime/dom/inspectHints.js +125 -9
  67. package/dist/runtime/dom/inspectModel.d.ts +3 -0
  68. package/dist/runtime/dom/inspectModel.js +30 -7
  69. package/dist/runtime/dom/inspectPaintModel.d.ts +48 -22
  70. package/dist/runtime/dom/inspectPaintModel.js +180 -68
  71. package/dist/runtime/dom/inspectRules.d.ts +19 -0
  72. package/dist/runtime/dom/inspectRules.js +21 -5
  73. package/dist/runtime/dom/inspectScripts.d.ts +85 -12
  74. package/dist/runtime/dom/inspectScripts.js +314 -28
  75. package/dist/runtime/dom/inspectTree.js +10 -2
  76. package/dist/runtime/dom/inspectWhyModel.d.ts +2 -1
  77. package/dist/runtime/dom/inspectWhyModel.js +52 -10
  78. package/dist/runtime/dom/layout.js +31 -9
  79. package/dist/runtime/dom/reactEventHelpers.d.ts +7 -0
  80. package/dist/runtime/dom/reactEventHelpers.js +27 -9
  81. package/dist/runtime/page/emulation.d.ts +13 -4
  82. package/dist/runtime/page/emulation.js +69 -4
  83. package/dist/runtime/page/userAgent.d.ts +17 -0
  84. package/dist/runtime/page/userAgent.js +57 -0
  85. package/dist/types.d.ts +12 -0
  86. package/dist/ui/formatters/audit.d.ts +19 -0
  87. package/dist/ui/formatters/audit.js +106 -0
  88. package/dist/ui/formatters/dom.d.ts +1 -1
  89. package/dist/ui/formatters/dom.js +6 -3
  90. package/dist/ui/formatters/inspect.js +42 -15
  91. package/dist/ui/formatters/installSkill.d.ts +11 -0
  92. package/dist/ui/formatters/installSkill.js +31 -0
  93. package/dist/ui/formatters/status.js +1 -1
  94. package/dist/ui/messages/commands.d.ts +44 -7
  95. package/dist/ui/messages/commands.js +83 -11
  96. package/dist/ui/messages/preview.d.ts +6 -0
  97. package/dist/ui/messages/preview.js +9 -1
  98. package/dist/utils/cssValues.js +36 -4
  99. package/dist/utils/decisionTrees.js +0 -5
  100. package/dist/utils/suggestions.d.ts +4 -2
  101. package/dist/utils/suggestions.js +7 -5
  102. package/dist/utils/taskMappings.js +1 -1
  103. package/package.json +4 -2
@@ -0,0 +1,87 @@
1
+ import { existsSync, mkdirSync, readFileSync, writeFileSync } from 'fs';
2
+ import { homedir } from 'os';
3
+ import { dirname, join } from 'path';
4
+ import { runCommand } from './shared/CommandRunner.js';
5
+ import { jsonOption } from './shared/commonOptions.js';
6
+ import { CommandError } from '../errors/index.js';
7
+ import { skillSourceMissingError, skillWriteFailedError } from '../errors/messages.js';
8
+ import { formatInstalledSkills } from '../ui/formatters/installSkill.js';
9
+ import { getErrorMessage } from '../utils/errors.js';
10
+ import { EXIT_CODES } from '../utils/exitCodes.js';
11
+ import { PACKAGE_ROOT } from '../utils/packageRoot.js';
12
+ /** The skill shipped with the package (also used by agents working in this repo). */
13
+ const SKILL_SOURCE_PATH = join(PACKAGE_ROOT, '.claude', 'skills', 'bdg', 'SKILL.md');
14
+ /** Skill roots, relative to the home directory, of the agents the skill is installed for. */
15
+ const SKILL_ROOTS = {
16
+ claude: join('.claude', 'skills'),
17
+ agents: join('.agents', 'skills'),
18
+ };
19
+ /**
20
+ * Copy the bdg skill into each target's skill directory, overwriting an
21
+ * older copy.
22
+ *
23
+ * @param targets - Agents to install for
24
+ * @param home - Home directory the skill roots are relative to
25
+ * @param source - SKILL.md to copy
26
+ * @returns One entry per target, in the given order
27
+ * @throws CommandError when the source is missing (83) or a write fails (82)
28
+ */
29
+ export function installSkill(targets, home = homedir(), source = SKILL_SOURCE_PATH) {
30
+ if (!existsSync(source)) {
31
+ const err = skillSourceMissingError(source);
32
+ throw new CommandError(err.message, { suggestion: err.suggestion }, EXIT_CODES.RESOURCE_NOT_FOUND);
33
+ }
34
+ const content = readFileSync(source, 'utf-8');
35
+ return targets.map((target) => writeSkill(target, join(home, SKILL_ROOTS[target], 'bdg', 'SKILL.md'), content));
36
+ }
37
+ /**
38
+ * Write the skill to one path unless it already holds the same content.
39
+ *
40
+ * @param target - Agent the path belongs to
41
+ * @param path - Destination SKILL.md
42
+ * @param content - Skill text
43
+ * @returns What happened to the file
44
+ * @throws CommandError (82) when the directory or file cannot be written
45
+ */
46
+ function writeSkill(target, path, content) {
47
+ const existing = existsSync(path) ? readFileSync(path, 'utf-8') : undefined;
48
+ if (existing === content) {
49
+ return { target, path, status: 'unchanged' };
50
+ }
51
+ try {
52
+ mkdirSync(dirname(path), { recursive: true });
53
+ writeFileSync(path, content);
54
+ }
55
+ catch (caught) {
56
+ const err = skillWriteFailedError(path, getErrorMessage(caught));
57
+ throw new CommandError(err.message, { suggestion: err.suggestion }, EXIT_CODES.PERMISSION_DENIED);
58
+ }
59
+ return { target, path, status: existing === undefined ? 'installed' : 'updated' };
60
+ }
61
+ /**
62
+ * Targets picked by the flags; no flag means every agent.
63
+ *
64
+ * @param options - Parsed command options
65
+ * @returns Targets to install for
66
+ */
67
+ function selectedTargets(options) {
68
+ const picked = Object.keys(SKILL_ROOTS).filter((target) => options[target]);
69
+ return picked.length > 0 ? picked : Object.keys(SKILL_ROOTS);
70
+ }
71
+ /**
72
+ * Register the install-skill command.
73
+ *
74
+ * @param program - Commander.js Command instance to register commands on
75
+ */
76
+ export function registerInstallSkillCommand(program) {
77
+ program
78
+ .command('install-skill')
79
+ .description('Install the bdg agent skill for Claude Code (~/.claude/skills) and agents reading ~/.agents/skills (Codex, Gemini CLI, ...); re-run after upgrading bdg')
80
+ .option('--claude', 'Only ~/.claude/skills (Claude Code)')
81
+ .option('--agents', 'Only ~/.agents/skills (Codex, Gemini CLI and other agents)')
82
+ .addOption(jsonOption())
83
+ .action(async (options) => {
84
+ await runCommand((opts) => Promise.resolve({ success: true, data: { skills: installSkill(selectedTargets(opts)) } }), options, formatInstalledSkills);
85
+ });
86
+ }
87
+ //# sourceMappingURL=installSkill.js.map
@@ -10,7 +10,7 @@ import { MAX_EDGE_PX, PIXELS_PER_TOKEN, TALL_PAGE_THRESHOLD, } from './dom/scree
10
10
  /** What DOM actions report about the network requests they triggered */
11
11
  const TRIGGERED_REQUESTS_BEHAVIOR = 'Requests (and WebSocket connections) that start after the action begins are returned as triggeredRequests (method, url, status, durationMs; pending when still running at return, loading when the response arrived but its body is still streaming; with resourceType; human output lists documents, XHR/fetch and WebSockets first (up to 10) and counts static assets on one line; absent when network telemetry is off). Attribution is by time: requests a page timer or poller starts meanwhile are listed too, whether or not the action caused them';
12
12
  /** What every DOM action reports about the page besides its requests */
13
- const ACTION_EFFECTS_BEHAVIOR = 'The result also says what changed on the page: a navigation (Page: navigated to <url> (status), or URL changed to <url> (same document); JSON navigation { url, sameDocument, status }), and messages that appeared or changed in alert/status/aria-live elements or flash/error/toast-like classes (New text: "…" (element); JSON messages [{ text, element }], at most 3; after a navigation every message on the new page counts; texts of only digits and time units, such as clocks and counters, are left out, but other text that changes on its own, such as a rotating banner, can show up). Both are absent when nothing changed. Cost: one page script sent before the action without waiting for it and one read after it, a few ms; when the page does not answer (a pending navigation) bdg waits at most 200 ms for the snapshot and 250 ms per read, and the navigation is still reported from CDP events';
13
+ const ACTION_EFFECTS_BEHAVIOR = 'The result also says what changed on the page: a navigation (Page: navigated to <url> (status), or URL changed to <url> (same document); JSON navigation { url, sameDocument, status }), and messages that appeared or changed in alert/status/aria-live elements or flash/error/toast-like classes (New text: "…" (element); JSON messages [{ text, element }], at most 3, with "(+N more)" and moreMessages for the rest; after a navigation every message on the new page counts; texts of only digits and time units, such as clocks and counters, are left out, but other text that changes on its own, such as a rotating banner, can show up). Both are absent when nothing changed. Cost: one page script sent before the action without waiting for it and one read after it, a few ms; when the page does not answer (a pending navigation) bdg waits at most 200 ms for the snapshot and 250 ms per read, and the navigation is still reported from CDP events';
14
14
  /** What click and pressKey report when the page was still changing as they returned */
15
15
  const STILL_CHANGING_BEHAVIOR = 'When the page was still changing as the action returned, the status line says (page still changing), a note below it says what was pending and suggests bdg dom wait <selector>, and JSON has settled: false with pending { requests (document, fetch/XHR and script requests still running), navigation (a new page still loading), loading (a loading indicator that appeared, e.g. "div#loading"), domChanging (DOM changes kept coming in bursts over a second look 250 ms later; a single render, ticking text and style animations do not count), busy (the page did not answer within 250 ms: a long script) }; absent when the page looked settled (exit code stays 0). A result a timer renders later, with no DOM change, request or loading indicator before it, is not detected. Cost: nothing extra, except 250 ms plus one read when the DOM looked busy. Not checked with --no-wait';
16
16
  /** What hover and pressKey report about elements they showed */
@@ -25,8 +25,12 @@ const NO_WAIT_TRIGGERED_REQUESTS = 'Returns immediately without waiting for netw
25
25
  const OPTION_BEHAVIORS = {
26
26
  'screenshot:--selector': {
27
27
  default: 'Captures the page (full page unless --no-full-page)',
28
- whenEnabled: 'Captures one element; the selector (or a query index) can also be given as the second argument: bdg dom screenshot out.png "#sel". Both given and naming different elements exits 81',
29
- automaticBehavior: 'The capture covers the border box plus content overflowing it (uncleared floats, positioned children; not what an overflow: hidden ancestor cuts off, nor fixed descendants); JSON element.bounds is the border box and element.captured the larger area when it grew, which human output notes',
28
+ whenEnabled: 'Captures one element; the selector (or a query index) can also be given as the second argument: bdg dom screenshot out.png "#sel". With --index it picks that match of the selector (--selector ".item" --index 2). A positional and an option naming different elements exits 81',
29
+ automaticBehavior: "The capture covers the border box plus what overflows it: uncleared floats, positioned children, text past a tight line height, and the element's own box shadows and outline (a focus ring); not what an overflow: hidden ancestor cuts off, nor fixed descendants. JSON element.bounds is the border box and element.captured the larger area when it grew, which human output notes. An element smaller than the viewport is scrolled into view for the capture and the page scroll put back afterwards; a larger one is captured with the page laid out at its width without scrollbars, so it does not shift",
30
+ },
31
+ 'screenshot:--padding': {
32
+ default: 'The element capture is its painted area (border box, overflowing content, shadows, outline)',
33
+ whenEnabled: 'Adds that many CSS px of the page around the element capture on every side (0-500); without an element it exits 81',
30
34
  },
31
35
  'screenshot:--no-resize': {
32
36
  default: `Images auto-resized to max ${MAX_EDGE_PX}px longest edge for Claude Vision optimization (~1,600 tokens)`,
@@ -208,10 +212,16 @@ const OPTION_BEHAVIORS = {
208
212
  whenEnabled: 'Lists every listener of framework roots individually',
209
213
  tokenImpact: 'On React pages --all adds a row per event type and phase (about 140 rows, 60 KB of JSON)',
210
214
  },
215
+ 'audit:--level': {
216
+ default: 'Text must reach WCAG AA: 4.5, or 3 for large text (24px, or 18.66px bold); every text-drawing element is checked, composited like dom inspect',
217
+ whenEnabled: '--level AAA asks 7, or 4.5 for large text',
218
+ automaticBehavior: 'One walk over the rendered elements (open shadow roots included, at most 20000; capped says when it stopped). Findings are sorted weakest first; --limit (default 20) lists that many per check and the rest are counted. Overflow leaves out content inside horizontal scrollers and visually-hidden 1px text; identical findings are grouped (×N)',
219
+ tokenImpact: 'About one line per finding; --limit bounds it',
220
+ },
211
221
  'layout:--index': {
212
222
  default: 'Reports every match of the selector (human output lists the first 20, JSON up to 100 plus an omitted count); a numeric argument reports that cached query element',
213
223
  whenEnabled: 'Reports only the nth match (0-based); out of range exits 81',
214
- automaticBehavior: 'Coordinates are CSS px: bounds relative to the top-level page (iframe offsets and page scroll included), viewport relative to the visible area. Iframes and overflow containers (scroll lists, overflow: hidden) clip what counts as visible (clippedBy names the one cutting it off). scrollBy brings the whole element into view and is limited to how far the page can scroll: for an element out of view it centres it (aligns its start when it is larger than the viewport; human output says "to centre it"), for a partly visible one it is the smallest scroll that shows all of it (the part cut off at the top or bottom; the start of one larger than the viewport; "partly visible (87%); scroll up 5px to see all of it"). Elements a page script moves on scroll (floating menus) may move again after it; fixed and sticky elements (page scroll does not move them, or only until they stick) and ones beyond that range get offScreenReason instead, which says "page scrolling is locked (…)" when the page cannot scroll because body/html is position: fixed or overflow: hidden, so in-flow content is not called fixed; a visible dialog (dialog[open], [aria-modal=true], [role=dialog|alertdialog]) is named as the likely cause ("likely by dialog div#consent"). page.viewport is the layout viewport without scrollbars, as dom scroll reports it; page.colorScheme is the prefers-color-scheme media feature the page sees (not the theme it renders). Content in a closed <details> or under content-visibility: hidden is hidden. coveredBy is the topmost element at the center of the largest visible box (none for pointer-events: none, nor for an element of the same click target: an overlay inside the link, button or label the element is in, a link to the same URL, or the textless absolutely positioned overlay link spanning the card that holds plain content); inert elements are flagged, not hidden',
224
+ automaticBehavior: 'Coordinates are CSS px: bounds relative to the top-level page (iframe offsets and page scroll included), viewport relative to the visible area. Iframes and overflow containers (scroll lists, overflow: hidden) clip what counts as visible (clippedBy names the one cutting it off). scrollBy brings the whole element into view and is limited to how far the page can scroll: for an element out of view it centres it (aligns its start when it is larger than the viewport; human output says "to centre it"), for a partly visible one it is the smallest scroll that shows all of it (the part cut off at the top or bottom; the start of one larger than the viewport; "partly visible (87%); scroll up 5px to see all of it"). Elements a page script moves on scroll (floating menus) may move again after it; fixed and sticky elements (page scroll does not move them, or only until they stick) and ones beyond that range get offScreenReason instead, which says "page scrolling is locked (…)" when the page cannot scroll because body/html is position: fixed or overflow: hidden, so in-flow content is not called fixed; a visible dialog (dialog[open], [aria-modal=true], [role=dialog|alertdialog]) is named as the likely cause ("likely by dialog div#consent"). page.viewport is the layout viewport without scrollbars, as dom scroll reports it; page.colorScheme is the prefers-color-scheme media feature the page sees (not the theme it renders). Content in a closed <details> or under content-visibility: hidden is hidden. coveredBy is the first element painted above it at the center of the largest visible box that paints there (the background of a sticky header rather than the transparent logo on it; inside a shadow host, what its shadow root paints), else the topmost one with coverTransparent (none for pointer-events: none, nor for an element of the same click target: an overlay inside the link, button or label the element is in, a link to the same URL, or the textless absolutely positioned overlay link spanning the card that holds plain content); inert elements are flagged, not hidden',
215
225
  tokenImpact: 'About one line per element; a cheap alternative to screenshots for "where is it?"',
216
226
  },
217
227
  'inspect:--index': {
@@ -286,11 +296,11 @@ const OPTION_BEHAVIORS = {
286
296
  },
287
297
  'peek:-f': {
288
298
  default: 'Shows snapshot of current data',
289
- whenEnabled: 'Continuous monitoring - refreshes every second (like tail -f)',
299
+ whenEnabled: 'Continuous monitoring (like tail -f): refreshes every second, or every --interval ms (100-60000). Replaces the deprecated bdg tail',
290
300
  },
291
301
  'peek:--follow': {
292
302
  default: 'Shows snapshot of current data',
293
- whenEnabled: 'Continuous monitoring - refreshes every second (like tail -f)',
303
+ whenEnabled: 'Continuous monitoring (like tail -f): refreshes every second, or every --interval ms (100-60000). Replaces the deprecated bdg tail',
294
304
  },
295
305
  'peek:-v': {
296
306
  default: 'Compact output (truncated URLs, no resource types)',
@@ -326,6 +336,11 @@ const OPTION_BEHAVIORS = {
326
336
  whenEnabled: 'The page gets exactly that viewport (CSS px, e.g. 1280x800) for the whole session, through navigations and reloads (Emulation.setDeviceMetricsOverride at the display pixel ratio); a launched Chrome also opens its window at that size, so tabs the page opens get it too',
327
337
  automaticBehavior: 'Works with --chrome-ws-url: the override belongs to the session, and Chrome drops it when the session ends, so the attached browser gets its own size back. bdg status shows the resulting layout viewport without the scrollbar (Viewport: 1265×800 (emulated 1280x800)). Invalid sizes (not WxH, a side outside 1-10000) exit 81',
328
338
  },
339
+ 'bdg:--mobile': {
340
+ default: 'A desktop viewport: classic scrollbars take ~15px of the width, no touch, a desktop user agent',
341
+ whenEnabled: 'Emulates a phone for the whole session: a mobile viewport (390x844 unless --viewport) at pixel ratio 3 with mobile layout (meta viewport, overlay scrollbars, so 100vw fits), touch (pointer: coarse, maxTouchPoints 5) and an Android Chrome user agent with mobile client hints; bdg page emulate --mobile turns it on mid-session, --viewport WxH without --mobile or --reset turns it off',
342
+ automaticBehavior: 'Screenshots keep the mobile layout and are taken at pixel ratio 1 (CSS px = image px); bdg status shows "(emulated 390x844, phone)"',
343
+ },
329
344
  'bdg:--color-scheme': {
330
345
  default: 'The page sees the system setting for prefers-color-scheme (headless Chrome follows the OS, so a dark OS renders dark pages); bdg status and dom layout show which one',
331
346
  whenEnabled: 'Emulates prefers-color-scheme: light or dark for the whole session (Emulation.setEmulatedMedia); other values exit 81 with a suggestion',
@@ -5,7 +5,7 @@
5
5
  import { Option } from 'commander';
6
6
  import { noActiveSessionError, runCommand } from './shared/CommandRunner.js';
7
7
  import { jsonOption } from './shared/commonOptions.js';
8
- import { parseColorScheme, parseViewport } from './start.js';
8
+ import { parseColorScheme, requestedViewport } from './start.js';
9
9
  import { CommandError } from '../errors/index.js';
10
10
  import { javascriptNavigationError } from '../errors/messages.js';
11
11
  import { getStatus, pageEmulate, pageNavigate } from '../ipc/client.js';
@@ -127,12 +127,13 @@ async function showPageInfo(options) {
127
127
  function emulationRequest(options) {
128
128
  if (options.reset)
129
129
  return { reset: true };
130
- if (options.viewport === undefined && options.colorScheme === undefined) {
130
+ if (options.viewport === undefined && options.colorScheme === undefined && !options.mobile) {
131
131
  const err = pageEmulateNothingError();
132
132
  throw new CommandError(err.message, { suggestion: err.suggestion }, EXIT_CODES.INVALID_ARGUMENTS);
133
133
  }
134
+ const viewport = requestedViewport(options.viewport, options.mobile);
134
135
  return {
135
- ...(options.viewport !== undefined && { viewport: parseViewport(options.viewport) }),
136
+ ...(viewport && { viewport }),
136
137
  ...(options.colorScheme !== undefined && {
137
138
  colorScheme: parseColorScheme(options.colorScheme),
138
139
  }),
@@ -188,11 +189,13 @@ export function registerPageCommands(program) {
188
189
  page
189
190
  .command('emulate')
190
191
  .description(PAGE_EMULATE_DESCRIPTION)
191
- .option('--viewport <WxH>', 'Viewport size in CSS px, e.g. 900x700')
192
+ .option('--viewport <WxH>', 'Viewport size in CSS px, e.g. 900x700 (a desktop one unless --mobile)')
192
193
  .option('--color-scheme <scheme>', 'Emulate prefers-color-scheme: light or dark')
194
+ .option('--mobile', 'Emulate a phone: mobile viewport (390x844 unless --viewport), touch, mobile user agent')
193
195
  .addOption(new Option('--reset', 'Back to the browser window size and the system setting').conflicts([
194
196
  'viewport',
195
197
  'colorScheme',
198
+ 'mobile',
196
199
  ]))
197
200
  .addOption(jsonOption())
198
201
  .action(async (options) => {
@@ -2,5 +2,12 @@
2
2
  * Peek command for previewing collected session data.
3
3
  */
4
4
  import type { Command } from 'commander';
5
+ import type { PeekCommandOptions } from './shared/optionTypes.js';
6
+ /**
7
+ * Watch the session data (`peek --follow`, and the deprecated `tail`).
8
+ *
9
+ * @param options - Peek options (follow implied)
10
+ */
11
+ export declare function followPreview(options: PeekCommandOptions): Promise<void>;
5
12
  export declare function registerPeekCommand(program: Command): void;
6
13
  //# sourceMappingURL=peek.d.ts.map
@@ -8,9 +8,12 @@ import { fetchPreviewOutput, createErrorResult, } from './shared/dataFetcher.js'
8
8
  import { followFetchFailure, setupFollowMode, } from './shared/followMode.js';
9
9
  import { handleValidationError } from './shared/handleValidationError.js';
10
10
  import { MAX_LAST_ITEMS, positiveIntRule, resourceTypeRule } from './shared/validation.js';
11
+ import { CommandError } from '../errors/index.js';
12
+ import { intervalWithoutFollowError } from '../errors/messages.js';
11
13
  import { filterByResourceType } from '../telemetry/filters.js';
12
14
  import { buildPreviewJsonData, formatPreview, } from '../ui/formatters/preview.js';
13
15
  import { followingPreviewMessage, stoppedFollowingPreviewMessage } from '../ui/messages/preview.js';
16
+ import { EXIT_CODES } from '../utils/exitCodes.js';
14
17
  function parseOptions(options) {
15
18
  const lastN = positiveIntRule({
16
19
  name: '--last',
@@ -20,7 +23,17 @@ function parseOptions(options) {
20
23
  allowZeroForAll: true,
21
24
  }).validate(options.last);
22
25
  const resourceTypes = resourceTypeRule().validate(options.type);
23
- return { lastN, resourceTypes };
26
+ if (options.interval !== undefined && !options.follow) {
27
+ const err = intervalWithoutFollowError();
28
+ throw new CommandError(err.message, { suggestion: err.suggestion }, EXIT_CODES.INVALID_ARGUMENTS);
29
+ }
30
+ const interval = positiveIntRule({
31
+ name: '--interval',
32
+ min: 100,
33
+ max: 60000,
34
+ default: 1000,
35
+ }).validate(options.interval);
36
+ return { lastN, resourceTypes, interval };
24
37
  }
25
38
  /**
26
39
  * Data section selected by `--network` / `--console`, if any.
@@ -75,11 +88,12 @@ function createPreviewOptions(base, resourceTypes, unfilteredCount) {
75
88
  }
76
89
  return options;
77
90
  }
78
- async function runFollowMode(options, lastN, resourceTypes, baseOptions) {
91
+ async function runFollowMode(options, parsed, baseOptions) {
92
+ const { lastN, resourceTypes, interval } = parsed;
79
93
  const showPreview = async () => {
80
94
  const result = await fetchAndFilterPreview(lastN, resourceTypes, peekSection(options));
81
95
  if (!result.success) {
82
- return followFetchFailure(result, { json: options.json, retryIntervalMs: 1000 });
96
+ return followFetchFailure(result, { json: options.json, retryIntervalMs: interval });
83
97
  }
84
98
  noteFollowConnected();
85
99
  if (!options.json)
@@ -91,9 +105,51 @@ async function runFollowMode(options, lastN, resourceTypes, baseOptions) {
91
105
  await setupFollowMode(showPreview, {
92
106
  startMessage: followingPreviewMessage,
93
107
  stopMessage: stoppedFollowingPreviewMessage,
94
- intervalMs: 1000,
108
+ intervalMs: interval,
95
109
  });
96
110
  }
111
+ /**
112
+ * Parse the options, reporting an invalid one and exiting.
113
+ *
114
+ * @param options - Peek options
115
+ * @returns Parsed options
116
+ */
117
+ function parsePeekOptions(options) {
118
+ try {
119
+ return parseOptions(options);
120
+ }
121
+ catch (error) {
122
+ handleValidationError(error, options.json ?? false);
123
+ }
124
+ }
125
+ /**
126
+ * How the preview is shown.
127
+ *
128
+ * @param options - Peek options
129
+ * @param lastN - Items to show
130
+ * @returns Preview options
131
+ */
132
+ function previewDisplayOptions(options, lastN) {
133
+ return {
134
+ json: options.json,
135
+ network: options.network,
136
+ console: options.console,
137
+ last: lastN,
138
+ verbose: options.verbose,
139
+ follow: options.follow,
140
+ };
141
+ }
142
+ /**
143
+ * Watch the session data (`peek --follow`, and the deprecated `tail`).
144
+ *
145
+ * @param options - Peek options (follow implied)
146
+ */
147
+ export async function followPreview(options) {
148
+ const following = { ...options, follow: true };
149
+ showBothSectionsWhenBothRequested(following);
150
+ const parsed = parsePeekOptions(following);
151
+ await runFollowMode(following, parsed, previewDisplayOptions(following, parsed.lastN));
152
+ }
97
153
  export function registerPeekCommand(program) {
98
154
  program
99
155
  .command('peek')
@@ -103,6 +159,7 @@ export function registerPeekCommand(program) {
103
159
  .option('-n, --network', 'Show only network requests', false)
104
160
  .option('-c, --console', 'Show only console messages', false)
105
161
  .option('-f, --follow', 'Watch for updates (like tail -f)', false)
162
+ .option('--interval <ms>', 'Refresh interval of --follow in ms, 100-60000 (default: 1000)')
106
163
  .option('--last <count>', 'Show last N items, 0 for all', '10')
107
164
  .option('--type <types>', 'Filter network requests by resource type (comma-separated: Document,XHR,Fetch,etc.)')
108
165
  .action(async (options) => {
@@ -110,26 +167,11 @@ export function registerPeekCommand(program) {
110
167
  if (options.network && !options.json) {
111
168
  console.error('Note: "bdg peek --network" is deprecated. Use "bdg network list" for enhanced filtering.');
112
169
  }
113
- let lastN;
114
- let resourceTypes;
115
- try {
116
- const parsed = parseOptions(options);
117
- lastN = parsed.lastN;
118
- resourceTypes = parsed.resourceTypes;
119
- }
120
- catch (error) {
121
- handleValidationError(error, options.json ?? false);
122
- }
123
- const baseOptions = {
124
- json: options.json,
125
- network: options.network,
126
- console: options.console,
127
- last: lastN,
128
- verbose: options.verbose,
129
- follow: options.follow,
130
- };
170
+ const parsed = parsePeekOptions(options);
171
+ const { lastN, resourceTypes } = parsed;
172
+ const baseOptions = previewDisplayOptions(options, lastN);
131
173
  if (options.follow) {
132
- await runFollowMode(options, lastN, resourceTypes, baseOptions);
174
+ await runFollowMode(options, parsed, baseOptions);
133
175
  return;
134
176
  }
135
177
  await runCommand(async () => {
@@ -93,8 +93,10 @@ export interface ScreenshotOptions {
93
93
  fullPage?: boolean;
94
94
  /** CSS selector for element capture */
95
95
  selector?: string;
96
- /** Cached element index (0-based) from previous query */
96
+ /** Cached element index (0-based) from previous query, or with `selector`, which of its matches */
97
97
  index?: number;
98
+ /** Extra space (CSS px) around an element capture */
99
+ padding?: number;
98
100
  /** Continuous capture mode to directory */
99
101
  follow?: boolean;
100
102
  /** Capture interval in ms for follow mode (string from CLI) */
@@ -373,6 +375,8 @@ export interface PeekCommandOptions extends BaseOptions, PreviewDisplayOptions {
373
375
  last?: string;
374
376
  /** Filter network requests by resource type (comma-separated) */
375
377
  type?: string;
378
+ /** Refresh interval of --follow in ms (string from CLI, default: 1000) */
379
+ interval?: string;
376
380
  }
377
381
  /**
378
382
  * Options for tail command.
@@ -26,6 +26,8 @@ export interface CollectorOptions {
26
26
  chromeFlags?: string;
27
27
  /** Viewport size, e.g. `1280x800`. */
28
28
  viewport?: string;
29
+ /** Emulate a phone (`--mobile`) */
30
+ mobile?: boolean;
29
31
  /** `prefers-color-scheme` to emulate: light or dark. */
30
32
  colorScheme?: string;
31
33
  }
@@ -53,6 +55,17 @@ export declare function extractUserDataDirFromFlags(flags: string[]): {
53
55
  * @returns The modified Command instance with all telemetry options applied
54
56
  */
55
57
  export declare function applyCollectorOptions(command: Command): Command;
58
+ /** Viewport of `--mobile` without `--viewport` (a common phone, CSS px) */
59
+ export declare const MOBILE_VIEWPORT: ViewportSize;
60
+ /**
61
+ * The viewport to emulate from `--viewport` and `--mobile`.
62
+ *
63
+ * @param viewport - `--viewport` value
64
+ * @param mobile - `--mobile` was given
65
+ * @returns Viewport (a phone's with `--mobile`), or undefined for neither
66
+ * @throws CommandError (81) for an invalid size
67
+ */
68
+ export declare function requestedViewport(viewport: string | undefined, mobile: boolean | undefined): ViewportSize | undefined;
56
69
  /**
57
70
  * Parse a `--viewport` value: width and height in CSS px joined by `x`
58
71
  * (`1280x800`; `X`, `×` and `,` work too).
@@ -81,7 +81,24 @@ export function applyCollectorOptions(command) {
81
81
  .addOption(jsonOption())
82
82
  .option('--chrome-flags <flags>', 'Custom Chrome flags (space-separated, e.g., --chrome-flags="--ignore-certificate-errors --disable-web-security")')
83
83
  .option('--viewport <WxH>', 'Viewport size in CSS px for the whole session, e.g. 1280x800 (default: 1920x1080 window)')
84
- .option('--color-scheme <scheme>', 'Emulate prefers-color-scheme for the session: light or dark (default: the system setting)');
84
+ .option('--color-scheme <scheme>', 'Emulate prefers-color-scheme for the session: light or dark (default: the system setting)')
85
+ .option('--mobile', `Emulate a phone for the session: mobile viewport (${MOBILE_VIEWPORT.width}x${MOBILE_VIEWPORT.height} unless --viewport), touch, mobile user agent`);
86
+ }
87
+ /** Viewport of `--mobile` without `--viewport` (a common phone, CSS px) */
88
+ export const MOBILE_VIEWPORT = { width: 390, height: 844 };
89
+ /**
90
+ * The viewport to emulate from `--viewport` and `--mobile`.
91
+ *
92
+ * @param viewport - `--viewport` value
93
+ * @param mobile - `--mobile` was given
94
+ * @returns Viewport (a phone's with `--mobile`), or undefined for neither
95
+ * @throws CommandError (81) for an invalid size
96
+ */
97
+ export function requestedViewport(viewport, mobile) {
98
+ const size = viewport !== undefined ? parseViewport(viewport) : undefined;
99
+ if (!mobile)
100
+ return size;
101
+ return { ...(size ?? MOBILE_VIEWPORT), mobile: true };
85
102
  }
86
103
  /** Largest viewport side accepted by `--viewport` (CSS px) */
87
104
  const MAX_VIEWPORT_SIDE = 10000;
@@ -157,7 +174,7 @@ function buildSessionOptions(options) {
157
174
  quiet: options.quiet ?? false,
158
175
  json: options.json ?? false,
159
176
  chromeFlags,
160
- viewport: options.viewport !== undefined ? parseViewport(options.viewport) : undefined,
177
+ viewport: requestedViewport(options.viewport, options.mobile),
161
178
  colorScheme: options.colorScheme !== undefined ? parseColorScheme(options.colorScheme) : undefined,
162
179
  };
163
180
  }
@@ -1,6 +1,12 @@
1
1
  /**
2
- * Tail command for continuous session monitoring.
2
+ * `bdg tail`: deprecated alias of `bdg peek --follow` (#115). It keeps its
3
+ * options and says to use `peek --follow` instead.
3
4
  */
4
5
  import type { Command } from 'commander';
6
+ /**
7
+ * Register the deprecated `tail` command.
8
+ *
9
+ * @param program - Root command
10
+ */
5
11
  export declare function registerTailCommand(program: Command): void;
6
12
  //# sourceMappingURL=tail.d.ts.map
@@ -1,43 +1,19 @@
1
1
  /**
2
- * Tail command for continuous session monitoring.
2
+ * `bdg tail`: deprecated alias of `bdg peek --follow` (#115). It keeps its
3
+ * options and says to use `peek --follow` instead.
4
+ */
5
+ import { followPreview } from './peek.js';
6
+ import { jsonOption } from './shared/commonOptions.js';
7
+ import { tailDeprecatedNotice } from '../ui/messages/preview.js';
8
+ /**
9
+ * Register the deprecated `tail` command.
10
+ *
11
+ * @param program - Root command
3
12
  */
4
- import { jsonOption, showBothSectionsWhenBothRequested } from './shared/commonOptions.js';
5
- import { noteFollowConnected } from './shared/daemonErrorHandler.js';
6
- import { fetchPreviewOutput } from './shared/dataFetcher.js';
7
- import { followFetchFailure, setupFollowMode, } from './shared/followMode.js';
8
- import { handleValidationError } from './shared/handleValidationError.js';
9
- import { MAX_LAST_ITEMS, positiveIntRule } from './shared/validation.js';
10
- import { formatPreview } from '../ui/formatters/preview.js';
11
- import { followingPreviewMessage, stoppedFollowingPreviewMessage } from '../ui/messages/preview.js';
12
- function parseOptions(options) {
13
- const lastRule = positiveIntRule({
14
- name: '--last',
15
- min: 1,
16
- max: MAX_LAST_ITEMS,
17
- default: 10,
18
- allowZeroForAll: true,
19
- });
20
- const intervalRule = positiveIntRule({ name: '--interval', min: 100, max: 60000, default: 1000 });
21
- return {
22
- lastN: lastRule.validate(options.last),
23
- interval: intervalRule.validate(options.interval),
24
- };
25
- }
26
- function createPreviewOptions(options, lastN) {
27
- return {
28
- json: options.json,
29
- network: options.network,
30
- console: options.console,
31
- last: lastN,
32
- verbose: options.verbose,
33
- follow: true,
34
- viewedAt: new Date(),
35
- };
36
- }
37
13
  export function registerTailCommand(program) {
38
14
  program
39
15
  .command('tail')
40
- .description('Continuously monitor session data (like tail -f)')
16
+ .description('Deprecated: use "bdg peek --follow" (same options)')
41
17
  .addOption(jsonOption())
42
18
  .option('-v, --verbose', 'Use verbose output with full URLs and formatting', false)
43
19
  .option('-n, --network', 'Show only network requests', false)
@@ -45,33 +21,8 @@ export function registerTailCommand(program) {
45
21
  .option('--last <count>', 'Show last N items (network requests + console messages), 0 for all', '10')
46
22
  .option('--interval <ms>', 'Update interval in milliseconds', '1000')
47
23
  .action(async (options) => {
48
- showBothSectionsWhenBothRequested(options);
49
- let lastN;
50
- let interval;
51
- try {
52
- const parsed = parseOptions(options);
53
- lastN = parsed.lastN;
54
- interval = parsed.interval;
55
- }
56
- catch (error) {
57
- handleValidationError(error, options.json ?? false);
58
- }
59
- const showPreview = async () => {
60
- const result = await fetchPreviewOutput({ lastN });
61
- if (!result.success) {
62
- return followFetchFailure(result, { json: options.json, retryIntervalMs: interval });
63
- }
64
- noteFollowConnected();
65
- if (!options.json)
66
- console.clear();
67
- console.log(formatPreview(result.data, createPreviewOptions(options, lastN)));
68
- return undefined;
69
- };
70
- await setupFollowMode(showPreview, {
71
- startMessage: followingPreviewMessage,
72
- stopMessage: stoppedFollowingPreviewMessage,
73
- intervalMs: interval,
74
- });
24
+ console.error(tailDeprecatedNotice());
25
+ await followPreview({ ...options, follow: true });
75
26
  });
76
27
  }
77
28
  //# sourceMappingURL=tail.js.map
package/dist/commands.js CHANGED
@@ -4,7 +4,9 @@ import { registerConsoleCommand } from './commands/console.js';
4
4
  import { registerDetailsCommand } from './commands/details.js';
5
5
  import { registerFormInteractionCommands } from './commands/dom/formInteraction.js';
6
6
  import { registerDomCommands } from './commands/dom/index.js';
7
+ import { registerInstallSkillCommand } from './commands/installSkill.js';
7
8
  import { registerNetworkCommands } from './commands/network/index.js';
9
+ import { registerCssCommands } from './commands/css.js';
8
10
  import { registerPageCommands } from './commands/page.js';
9
11
  import { registerPeekCommand } from './commands/peek.js';
10
12
  import { registerSessionsCommand } from './commands/sessions.js';
@@ -38,11 +40,14 @@ export const commandRegistry = [
38
40
  registerDomCommands,
39
41
  registerFormInteractionCommands,
40
42
  registerPageCommands,
43
+ registerCssCommands,
41
44
  addCommandGroup('CDP Commands:'),
42
45
  registerCdpCommand,
43
46
  addCommandGroup('Network Commands:'),
44
47
  registerNetworkCommands,
45
48
  addCommandGroup('Console Commands:'),
46
49
  registerConsoleCommand,
50
+ addCommandGroup('Agent Setup:'),
51
+ registerInstallSkillCommand,
47
52
  ];
48
53
  //# sourceMappingURL=commands.js.map
@@ -4,6 +4,8 @@ import { createInteractionRunner } from './interactions.js';
4
4
  import { withTriggeredRequestCount } from './triggeredRequests.js';
5
5
  import { CommandError } from '../../errors/index.js';
6
6
  import { cdpCallError, formDiscoveryFailedError } from '../../errors/messages.js';
7
+ import { searchStyleSheets } from '../../runtime/css/search.js';
8
+ import { auditPage } from '../../runtime/dom/audit.js';
7
9
  import { evaluateScript, withBusyPageRecovery } from '../../runtime/dom/evalHelpers.js';
8
10
  import { inspectEventListeners } from '../../runtime/dom/eventListeners.js';
9
11
  import { FORM_DISCOVERY_SCRIPT, isRawFormData } from '../../runtime/dom/formDiscovery.js';
@@ -458,7 +460,11 @@ export function createCommandRegistry(store, emulation) {
458
460
  })), params.wait !== false))),
459
461
  dom_listeners: async (cdp, params) => withBusyPageRecovery(cdp, inspectEventListeners(cdp, params)),
460
462
  dom_layout: async (cdp, params) => withBusyPageRecovery(cdp, inspectLayout(cdp, params)),
461
- dom_inspect: async (cdp, params) => withBusyPageRecovery(cdp, inspectElement(cdp, params)),
463
+ dom_audit: async (cdp, params) => withBusyPageRecovery(cdp, auditPage(cdp, params)),
464
+ css_search: async (cdp, params) => searchStyleSheets(cdp, params),
465
+ dom_inspect: async (cdp, params) => withBusyPageRecovery(cdp, inspectElement(cdp, params).then((result) => result.theme === 'dark' && emulation.get().colorScheme === 'dark'
466
+ ? { ...result, themeFrom: 'emulation' }
467
+ : result)),
462
468
  dom_wait: async (cdp, params) => waitForCondition(cdp, params),
463
469
  page_navigate: async (cdp, params) => interact(cdp, () => navigatePage(cdp, params.action, {
464
470
  ...filterDefined({ url: params.url, wait: params.wait }),