cbrowser 19.1.5 → 19.1.6

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 (97) hide show
  1. package/dist/analysis/accessibility-empathy.d.ts.map +1 -1
  2. package/dist/analysis/accessibility-empathy.js +17 -2
  3. package/dist/analysis/accessibility-empathy.js.map +1 -1
  4. package/dist/analysis/agent-ready-audit.d.ts.map +1 -1
  5. package/dist/analysis/agent-ready-audit.js +318 -68
  6. package/dist/analysis/agent-ready-audit.js.map +1 -1
  7. package/dist/browser.d.ts +72 -8
  8. package/dist/browser.d.ts.map +1 -1
  9. package/dist/browser.js +146 -120
  10. package/dist/browser.js.map +1 -1
  11. package/dist/charge-scope.d.ts +20 -0
  12. package/dist/charge-scope.d.ts.map +1 -0
  13. package/dist/charge-scope.js +46 -0
  14. package/dist/charge-scope.js.map +1 -0
  15. package/dist/mcp-server-remote.d.ts.map +1 -1
  16. package/dist/mcp-server-remote.js +34 -3
  17. package/dist/mcp-server-remote.js.map +1 -1
  18. package/dist/mcp-server.d.ts.map +1 -1
  19. package/dist/mcp-server.js +33 -44
  20. package/dist/mcp-server.js.map +1 -1
  21. package/dist/mcp-tools/base/audit-tools.d.ts.map +1 -1
  22. package/dist/mcp-tools/base/audit-tools.js +6 -1
  23. package/dist/mcp-tools/base/audit-tools.js.map +1 -1
  24. package/dist/mcp-tools/base/browser-management-tools.d.ts +11 -0
  25. package/dist/mcp-tools/base/browser-management-tools.d.ts.map +1 -1
  26. package/dist/mcp-tools/base/browser-management-tools.js +27 -6
  27. package/dist/mcp-tools/base/browser-management-tools.js.map +1 -1
  28. package/dist/mcp-tools/base/browser-state-tools.js +3 -3
  29. package/dist/mcp-tools/base/browser-state-tools.js.map +1 -1
  30. package/dist/mcp-tools/base/capture-tools.d.ts.map +1 -1
  31. package/dist/mcp-tools/base/capture-tools.js +3 -1
  32. package/dist/mcp-tools/base/capture-tools.js.map +1 -1
  33. package/dist/mcp-tools/base/cognitive-tools.d.ts.map +1 -1
  34. package/dist/mcp-tools/base/cognitive-tools.js +6 -37
  35. package/dist/mcp-tools/base/cognitive-tools.js.map +1 -1
  36. package/dist/mcp-tools/base/extraction-tools.d.ts.map +1 -1
  37. package/dist/mcp-tools/base/extraction-tools.js +6 -5
  38. package/dist/mcp-tools/base/extraction-tools.js.map +1 -1
  39. package/dist/mcp-tools/base/gif-tools.d.ts.map +1 -1
  40. package/dist/mcp-tools/base/gif-tools.js +2 -0
  41. package/dist/mcp-tools/base/gif-tools.js.map +1 -1
  42. package/dist/mcp-tools/base/interaction-tools.d.ts.map +1 -1
  43. package/dist/mcp-tools/base/interaction-tools.js +1 -0
  44. package/dist/mcp-tools/base/interaction-tools.js.map +1 -1
  45. package/dist/mcp-tools/base/llms-txt-tools.d.ts.map +1 -1
  46. package/dist/mcp-tools/base/llms-txt-tools.js +1 -0
  47. package/dist/mcp-tools/base/llms-txt-tools.js.map +1 -1
  48. package/dist/mcp-tools/base/marketing-tools.d.ts.map +1 -1
  49. package/dist/mcp-tools/base/marketing-tools.js +4 -0
  50. package/dist/mcp-tools/base/marketing-tools.js.map +1 -1
  51. package/dist/mcp-tools/base/navigation-tools.d.ts.map +1 -1
  52. package/dist/mcp-tools/base/navigation-tools.js +2 -1
  53. package/dist/mcp-tools/base/navigation-tools.js.map +1 -1
  54. package/dist/mcp-tools/base/persona-comparison-tools.d.ts.map +1 -1
  55. package/dist/mcp-tools/base/persona-comparison-tools.js +2 -0
  56. package/dist/mcp-tools/base/persona-comparison-tools.js.map +1 -1
  57. package/dist/mcp-tools/base/security-tools.d.ts.map +1 -1
  58. package/dist/mcp-tools/base/security-tools.js +2 -0
  59. package/dist/mcp-tools/base/security-tools.js.map +1 -1
  60. package/dist/mcp-tools/base/site-knowledge-tools.d.ts.map +1 -1
  61. package/dist/mcp-tools/base/site-knowledge-tools.js +9 -1
  62. package/dist/mcp-tools/base/site-knowledge-tools.js.map +1 -1
  63. package/dist/mcp-tools/base/testing-tools.js +1 -1
  64. package/dist/mcp-tools/base/testing-tools.js.map +1 -1
  65. package/dist/mcp-tools/base/values-tools.d.ts.map +1 -1
  66. package/dist/mcp-tools/base/values-tools.js +2 -0
  67. package/dist/mcp-tools/base/values-tools.js.map +1 -1
  68. package/dist/mcp-tools/credit-refund.d.ts +20 -0
  69. package/dist/mcp-tools/credit-refund.d.ts.map +1 -0
  70. package/dist/mcp-tools/credit-refund.js +37 -0
  71. package/dist/mcp-tools/credit-refund.js.map +1 -0
  72. package/dist/mcp-tools/persona-creation-tools.d.ts.map +1 -1
  73. package/dist/mcp-tools/persona-creation-tools.js +2 -0
  74. package/dist/mcp-tools/persona-creation-tools.js.map +1 -1
  75. package/dist/mcp-tools/persona-lifecycle-tools.js +6 -6
  76. package/dist/mcp-tools/persona-lifecycle-tools.js.map +1 -1
  77. package/dist/mcp-tools/screenshot-utils.d.ts +15 -0
  78. package/dist/mcp-tools/screenshot-utils.d.ts.map +1 -1
  79. package/dist/mcp-tools/screenshot-utils.js +18 -0
  80. package/dist/mcp-tools/screenshot-utils.js.map +1 -1
  81. package/dist/mcp-tools/tier-gate.d.ts.map +1 -1
  82. package/dist/mcp-tools/tier-gate.js +25 -2
  83. package/dist/mcp-tools/tier-gate.js.map +1 -1
  84. package/dist/personas.d.ts +19 -0
  85. package/dist/personas.d.ts.map +1 -1
  86. package/dist/personas.js +79 -0
  87. package/dist/personas.js.map +1 -1
  88. package/dist/remediation/patch-generator.d.ts.map +1 -1
  89. package/dist/remediation/patch-generator.js +15 -18
  90. package/dist/remediation/patch-generator.js.map +1 -1
  91. package/dist/types.d.ts +12 -0
  92. package/dist/types.d.ts.map +1 -1
  93. package/dist/types.js.map +1 -1
  94. package/dist/visual/perceptual-transport.d.ts.map +1 -1
  95. package/dist/visual/perceptual-transport.js +10 -2
  96. package/dist/visual/perceptual-transport.js.map +1 -1
  97. package/package.json +1 -1
package/dist/browser.js CHANGED
@@ -11,7 +11,7 @@
11
11
  import { chromium, firefox, webkit } from "playwright";
12
12
  import { existsSync, readFileSync, writeFileSync, readdirSync, statSync, unlinkSync, mkdirSync, readlinkSync } from "fs";
13
13
  import { isStaleSingletonLock } from "./utils.js";
14
- import { join } from "path";
14
+ import { join, dirname } from "path";
15
15
  import { execSync } from "child_process";
16
16
  import { mergeConfig, getPaths, ensureDirectories } from "./config.js";
17
17
  import { BUILTIN_PERSONAS, getPersona } from "./personas.js";
@@ -199,6 +199,16 @@ export class CBrowser {
199
199
  overlayHandler;
200
200
  // Track last filled input for Enter key fallback when submit buttons are hidden
201
201
  lastFilledInputSelector = null;
202
+ /** Set by every compressed screenshot; cleared by every screenshot(). */
203
+ _lastScreenshotInfo;
204
+ /**
205
+ * Size, quality and source size of the last compressed screenshot, or
206
+ * undefined when the last screenshot was not compressed or could not be
207
+ * measured. Compare `path` with the file you hold before trusting it.
208
+ */
209
+ get lastScreenshotInfo() {
210
+ return this._lastScreenshotInfo;
211
+ }
202
212
  constructor(userConfig = {}) {
203
213
  this.config = mergeConfig(userConfig);
204
214
  this.skipSessionRestore = userConfig.skipSessionRestore ?? false;
@@ -4773,6 +4783,8 @@ For more help: https://playwright.dev/docs/browsers
4773
4783
  async screenshot(path, options) {
4774
4784
  const page = await this.getPage();
4775
4785
  const opts = options || {};
4786
+ // A stale record must never describe a newer file.
4787
+ this._lastScreenshotInfo = undefined;
4776
4788
  // In remote mode, auto-enable compression to stay under Claude.ai's 200KB limit
4777
4789
  // unless explicitly disabled
4778
4790
  const useCompression = opts.compress ??
@@ -4823,8 +4835,11 @@ For more help: https://playwright.dev/docs/browsers
4823
4835
  return rect;
4824
4836
  }
4825
4837
  /**
4826
- * Take a compressed JPEG screenshot.
4827
- * Automatically adjusts quality and scale to stay under maxSize.
4838
+ * Take a compressed JPEG screenshot that fits `maxSize` bytes.
4839
+ *
4840
+ * The page is captured once, at its current viewport, and the budget is met
4841
+ * in memory -- lower JPEG quality first, then a smaller image. The viewport
4842
+ * is never resized. `lastScreenshotInfo` says what came out.
4828
4843
  *
4829
4844
  * @param path - Output path (will use .jpg extension)
4830
4845
  * @param options - Compression options
@@ -4834,133 +4849,144 @@ For more help: https://playwright.dev/docs/browsers
4834
4849
  const page = await this.getPage();
4835
4850
  const maxSize = options?.maxSize || 110000; // ~150KB after base64 (110KB raw = ~150KB base64)
4836
4851
  const fullPage = options?.fullPage || false;
4837
- // Quality and scale steps to try
4838
- const qualitySteps = [85, 70, 55, 45, 35, 25];
4839
- // Viewport scaling is skipped for two cases.
4840
- //
4841
- // noResize: the caller owns the viewport (a capture is running) and a
4842
- // resize would reflow the page into the recording.
4843
- //
4844
- // fullPage: shrinking the viewport does not shrink a full-page capture,
4845
- // it just makes the page taller and narrower. Measured on a marketing
4846
- // page: stepping the viewport down to 0.3 moved the file from 645KB to
4847
- // 580KB against an 87KB target, and left a 384px-wide picture of a whole
4848
- // page that no one could read. The image downscale below does the work
4849
- // instead, from a full-width capture.
4850
- const scaleSteps = (options?.noResize || fullPage) ? [1.0] : [1.0, 0.75, 0.5, 0.4, 0.3];
4851
- let quality = options?.quality || 85;
4852
- let scale = options?.scale || 1.0;
4852
+ const startQuality = options?.quality || 85;
4853
4853
  // Generate filename with .jpg extension
4854
4854
  const baseFilename = path || join(this.paths.screenshotsDir, `screenshot-${Date.now()}`);
4855
4855
  const filename = baseFilename.endsWith('.jpg') || baseFilename.endsWith('.jpeg')
4856
4856
  ? baseFilename
4857
4857
  : baseFilename.replace(/\.png$/, '') + '.jpg';
4858
- // Get viewport size for scaling
4859
- const viewport = page.viewportSize() || { width: 1280, height: 800 };
4860
- // Clip is expressed in unscaled CSS pixels, so it has to scale with the viewport
4861
- const baseClip = options?.clip ? await this.resolveClip(options) : undefined;
4862
- // Try different quality/scale combinations
4863
- for (const tryScale of scaleSteps) {
4864
- for (const tryQuality of qualitySteps) {
4865
- // Skip if we'd make quality worse than needed
4866
- if (tryQuality < quality && tryScale >= scale)
4867
- continue;
4868
- const scaledWidth = Math.round(viewport.width * tryScale);
4869
- const scaledHeight = Math.round(viewport.height * tryScale);
4870
- // Set viewport to scaled size temporarily if scaling down
4871
- if (tryScale < 1.0) {
4872
- await page.setViewportSize({ width: scaledWidth, height: scaledHeight });
4873
- }
4874
- try {
4875
- const scaledClip = baseClip && {
4876
- x: Math.round(baseClip.x * tryScale),
4877
- y: Math.round(baseClip.y * tryScale),
4878
- width: Math.max(1, Math.round(baseClip.width * tryScale)),
4879
- height: Math.max(1, Math.round(baseClip.height * tryScale)),
4880
- };
4881
- await page.screenshot({
4882
- path: filename,
4883
- fullPage,
4884
- type: 'jpeg',
4885
- quality: tryQuality,
4886
- ...(scaledClip ? { clip: scaledClip } : {})
4887
- });
4888
- // Check file size
4889
- const stats = await import('node:fs').then(fs => fs.statSync(filename));
4890
- const fileSize = stats.size;
4891
- if (this.config.verbose) {
4892
- console.log(` Screenshot: ${fileSize} bytes (quality=${tryQuality}, scale=${tryScale})`);
4893
- }
4894
- // Restore viewport if we scaled
4895
- if (tryScale < 1.0) {
4896
- await page.setViewportSize(viewport);
4897
- }
4898
- // If under limit, we're done
4899
- if (fileSize <= maxSize) {
4900
- return filename;
4901
- }
4902
- // Continue to next quality level
4903
- quality = tryQuality;
4904
- scale = tryScale;
4905
- }
4906
- catch (err) {
4907
- // Restore viewport on error
4908
- if (tryScale < 1.0) {
4909
- await page.setViewportSize(viewport);
4910
- }
4911
- throw err;
4912
- }
4913
- }
4914
- }
4915
- // Viewport scaling cannot shrink a full-page capture: making the viewport
4916
- // narrower just makes the page taller, so the loop above exhausts every
4917
- // step and still returns something far over budget. Measured: a full-page
4918
- // shot of a marketing page came back at 567KB against an ~89KB target,
4919
- // which then could not be delivered inline at all.
4858
+ const clip = options?.clip ? await this.resolveClip(options) : undefined;
4859
+ // BUG-02 (2026-10-07): the budget is met by the IMAGE, never the viewport.
4920
4860
  //
4921
- // So the last resort resizes the IMAGE rather than the viewport. A smaller
4922
- // picture of the whole page is still readable and still shows the layout;
4923
- // no picture at all is what the caller was getting.
4861
+ // This used to call page.setViewportSize on the caller's live page at
4862
+ // 0.75 / 0.5 / 0.4 / 0.3 of its size and back. That reflowed the page for
4863
+ // real: navigate on cbrowser.ai fired resizes ["960x600","1280x800",
4864
+ // "640x400","1280x800"] and returned a 640px picture of the hamburger nav,
4865
+ // a layout the DOM tools never saw, and anything that reacts to resize
4866
+ // (open menus, carousels) reacted. The quality ladder meant to run first
4867
+ // was dead behind an inverted skip guard, so a page over budget at q85 went
4868
+ // straight to 960px even when q55 fitted at full size.
4869
+ //
4870
+ // So: one capture, at the caller's viewport, then fit it in memory.
4871
+ const source = await page.screenshot({
4872
+ fullPage,
4873
+ type: 'jpeg',
4874
+ quality: startQuality,
4875
+ ...(clip ? { clip } : {}),
4876
+ });
4877
+ let fitted;
4924
4878
  try {
4925
- const sharp = (await import("sharp")).default;
4926
- let current = statSync(filename).size;
4927
- if (current > maxSize) {
4928
- const meta = await sharp(filename).metadata();
4929
- if (meta.width) {
4930
- // Width is computed from the size ratio rather than stepped through
4931
- // fixed factors. Stepping overshot badly -- it landed on a 211px-wide
4932
- // picture of a whole page, which meets the byte target and cannot be
4933
- // read, and it also stalled when re-encoding a quality-25 input at a
4934
- // higher quality made the file grow instead of shrink.
4935
- //
4936
- // File size scales roughly with area, so width scales with the square
4937
- // root of the ratio. MIN_READABLE_WIDTH is the floor: past it the
4938
- // screenshot stops being something a reader can use, and an honest
4939
- // omission beats an unreadable thumbnail.
4940
- const MIN_READABLE_WIDTH = 700;
4941
- const ratio = Math.sqrt(maxSize / current) * 0.92;
4942
- const target = Math.max(MIN_READABLE_WIDTH, Math.round(meta.width * ratio));
4943
- if (target < meta.width) {
4944
- const buf = await sharp(filename)
4945
- .resize({ width: target })
4946
- .jpeg({ quality: 62 })
4947
- .toBuffer();
4948
- // Only keep it if it actually helped; a re-encode that grew the file
4949
- // is worse than the original on every axis.
4950
- if (buf.length < current) {
4951
- writeFileSync(filename, buf);
4952
- current = buf.length;
4953
- }
4954
- }
4955
- }
4956
- }
4957
- if (this.config.verbose) {
4958
- console.log(` Screenshot final size ${(current / 1024).toFixed(0)}KB (target ${(maxSize / 1024).toFixed(0)}KB)`);
4959
- }
4879
+ fitted = await this.fitJpegToBudget(source, maxSize, startQuality);
4880
+ }
4881
+ catch { /* sharp failed: an oversized screenshot still beats no screenshot */ }
4882
+ const out = fitted?.buffer ?? source;
4883
+ // page.screenshot({ path }) created missing directories; keep that for
4884
+ // callers that pass a path of their own.
4885
+ mkdirSync(dirname(filename), { recursive: true });
4886
+ writeFileSync(filename, out);
4887
+ if (fitted) {
4888
+ this._lastScreenshotInfo = {
4889
+ path: filename,
4890
+ width: fitted.width,
4891
+ height: fitted.height,
4892
+ sourceWidth: fitted.sourceWidth,
4893
+ sourceHeight: fitted.sourceHeight,
4894
+ downscaled: fitted.width < fitted.sourceWidth,
4895
+ quality: fitted.quality,
4896
+ bytes: out.length,
4897
+ maxSize,
4898
+ };
4899
+ }
4900
+ if (this.config.verbose) {
4901
+ const dims = fitted ? ` ${fitted.width}x${fitted.height} of ${fitted.sourceWidth}x${fitted.sourceHeight}, quality=${fitted.quality},` : '';
4902
+ console.log(` Screenshot:${dims} ${out.length} bytes (target ${maxSize})`);
4960
4903
  }
4961
- catch { /* an oversized screenshot still beats no screenshot */ }
4962
4904
  return filename;
4963
4905
  }
4906
+ /**
4907
+ * Fit a JPEG into `maxSize` bytes without going back to the page.
4908
+ *
4909
+ * Every candidate is encoded from the pixels of `source`, the one
4910
+ * high-quality capture. The old last resort re-encoded an already-degraded
4911
+ * q25 file at q62; the result came out larger, was discarded, and the
4912
+ * over-budget file shipped (measured: 2560x1440 at 91.9KB and a 1280x800
4913
+ * noise canvas at 96KB, both against 89KB). A single pass with a fixed
4914
+ * floor cannot promise the budget either, so this loops until it fits.
4915
+ *
4916
+ * Order, each step tried only when the one before did not fit:
4917
+ * 1. full size, q70 then q55 -- the floor where text stays clean;
4918
+ * 2. narrower, q70 then q55, down to READABLE_WIDTH;
4919
+ * 3. at that width, q45 then q35 -- below it text stops being readable,
4920
+ * so quality is the cheaper thing to give up first;
4921
+ * 4. narrower still at the floor quality, down to MIN_WIDTH;
4922
+ * 5. at MIN_WIDTH, q45 / q35 / q25.
4923
+ * If nothing fits, the smallest candidate comes back over budget, and the
4924
+ * inline budget check in buildContentWithScreenshots reports it omitted.
4925
+ */
4926
+ async fitJpegToBudget(source, maxSize, startQuality) {
4927
+ const sharp = (await import("sharp")).default;
4928
+ const meta = await sharp(source).metadata();
4929
+ const sourceWidth = meta.width ?? 0;
4930
+ const sourceHeight = meta.height ?? 0;
4931
+ let best = { buffer: source, width: sourceWidth, height: sourceHeight, sourceWidth, sourceHeight, quality: startQuality };
4932
+ if (source.length <= maxSize || !sourceWidth)
4933
+ return best;
4934
+ const QUALITY_FLOOR = 55;
4935
+ const READABLE_WIDTH = 700;
4936
+ const MIN_WIDTH = 200;
4937
+ // Decoded once; no attempt starts from another attempt's output.
4938
+ const { data, info } = await sharp(source).raw().toBuffer({ resolveWithObject: true });
4939
+ const raw = { width: info.width, height: info.height, channels: info.channels };
4940
+ const attempt = async (width, quality) => {
4941
+ let img = sharp(data, { raw });
4942
+ if (width < sourceWidth)
4943
+ img = img.resize({ width });
4944
+ const enc = await img.jpeg({ quality }).toBuffer({ resolveWithObject: true });
4945
+ const result = { buffer: enc.data, width: enc.info.width, height: enc.info.height, sourceWidth, sourceHeight, quality };
4946
+ if (result.buffer.length < best.buffer.length)
4947
+ best = result;
4948
+ return result;
4949
+ };
4950
+ let width = sourceWidth;
4951
+ let lastBytes = source.length;
4952
+ const atThisWidth = async (qualities) => {
4953
+ for (const q of qualities) {
4954
+ const r = await attempt(width, q);
4955
+ if (r.buffer.length <= maxSize)
4956
+ return r;
4957
+ }
4958
+ return undefined;
4959
+ };
4960
+ const narrower = async (floor, qualities) => {
4961
+ while (width > floor) {
4962
+ // Bytes scale roughly with area, so width with the square root of the
4963
+ // overshoot. At least 10% narrower each pass, so the loop always ends.
4964
+ const target = Math.floor(width * Math.sqrt(maxSize / lastBytes) * 0.95);
4965
+ width = Math.max(floor, Math.min(Math.floor(width * 0.9), target));
4966
+ for (const q of qualities) {
4967
+ const r = await attempt(width, q);
4968
+ if (r.buffer.length <= maxSize)
4969
+ return r;
4970
+ lastBytes = r.buffer.length;
4971
+ }
4972
+ }
4973
+ return undefined;
4974
+ };
4975
+ const fullSize = [70, QUALITY_FLOOR].filter((q) => q < startQuality);
4976
+ const ladder = fullSize.length > 0 ? fullSize : [startQuality];
4977
+ const floorQuality = ladder[ladder.length - 1];
4978
+ if (fullSize.length > 0) {
4979
+ const r = await atThisWidth(fullSize);
4980
+ if (r)
4981
+ return r;
4982
+ lastBytes = best.buffer.length;
4983
+ }
4984
+ return (await narrower(Math.min(READABLE_WIDTH, width), ladder))
4985
+ ?? (await atThisWidth([45, 35].filter((q) => q < floorQuality)))
4986
+ ?? (await narrower(Math.min(MIN_WIDTH, width), [floorQuality]))
4987
+ ?? (await atThisWidth([45, 35, 25].filter((q) => q < floorQuality)))
4988
+ ?? best;
4989
+ }
4964
4990
  // =========================================================================
4965
4991
  // Sessions
4966
4992
  // =========================================================================