@link-assistant/web-capture 1.7.5 → 1.7.7

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.
package/CHANGELOG.md CHANGED
@@ -1,5 +1,17 @@
1
1
  # @link-assistant/web-capture
2
2
 
3
+ ## 1.7.7
4
+
5
+ ### Patch Changes
6
+
7
+ - 9034d01: Add Habr archive-compatible markdown controls for figure-numbered image localization and preserved code block whitespace.
8
+
9
+ ## 1.7.6
10
+
11
+ ### Patch Changes
12
+
13
+ - Support Google Docs capture selection through `--capture browser` editor-model parsing, public export API capture, and authenticated Docs REST API capture.
14
+
3
15
  ## 1.7.5
4
16
 
5
17
  ### Patch Changes
package/README.md CHANGED
@@ -92,11 +92,14 @@ web-capture --serve --port 8080
92
92
  ### Google Docs
93
93
 
94
94
  ```bash
95
- # Capture public Google Doc as Markdown
96
- web-capture https://docs.google.com/document/d/DOC_ID/edit
95
+ # Capture the live editor model as Markdown (default --capture browser)
96
+ web-capture https://docs.google.com/document/d/DOC_ID/edit --capture browser
97
97
 
98
- # Capture private Google Doc with API token
99
- web-capture https://docs.google.com/document/d/DOC_ID/edit --apiToken YOUR_TOKEN
98
+ # Capture via the public export endpoint
99
+ web-capture https://docs.google.com/document/d/DOC_ID/edit --capture api
100
+
101
+ # Capture via the Google Docs REST API
102
+ web-capture https://docs.google.com/document/d/DOC_ID/edit --capture api --apiToken YOUR_TOKEN
100
103
  ```
101
104
 
102
105
  ## API Endpoints (Server Mode)
@@ -8,6 +8,23 @@ import fs from 'fs';
8
8
  import path from 'path';
9
9
  import { URL } from 'node:url';
10
10
  import { makeConfig } from 'lino-arguments';
11
+ import makeLog from 'log-lazy';
12
+
13
+ function makeVerboseLog(enabled) {
14
+ return makeLog({
15
+ level: enabled ? 'all' : 'none',
16
+ log: {
17
+ fatal: console.error,
18
+ error: console.error,
19
+ warn: console.error,
20
+ info: console.error,
21
+ debug: console.error,
22
+ verbose: console.error,
23
+ trace: console.error,
24
+ silly: console.error,
25
+ },
26
+ });
27
+ }
11
28
 
12
29
  // Create configuration using lino-arguments pattern
13
30
  const config = makeConfig({
@@ -307,6 +324,82 @@ function deriveOutputPath(absoluteUrl, ext, dataDir) {
307
324
  return path.join(dir, `document.${ext}`);
308
325
  }
309
326
 
327
+ function archiveExtension(archiveFormat) {
328
+ if (archiveFormat === 'tar.gz' || archiveFormat === 'gz') {
329
+ return 'tar.gz';
330
+ }
331
+ if (archiveFormat === '7z') {
332
+ return '7z';
333
+ }
334
+ if (archiveFormat === 'tar') {
335
+ return 'tar';
336
+ }
337
+ return 'zip';
338
+ }
339
+
340
+ function writeGoogleDocsTextOutput({
341
+ content,
342
+ absoluteUrl,
343
+ ext,
344
+ explicitOutput,
345
+ dataDir,
346
+ label,
347
+ }) {
348
+ const output =
349
+ explicitOutput === '-'
350
+ ? null
351
+ : explicitOutput || deriveOutputPath(absoluteUrl, ext, dataDir);
352
+ if (output) {
353
+ fs.mkdirSync(path.dirname(output), { recursive: true });
354
+ fs.writeFileSync(output, content, 'utf-8');
355
+ console.error(`${label} saved to: ${output}`);
356
+ } else {
357
+ process.stdout.write(content);
358
+ }
359
+ }
360
+
361
+ async function writeGoogleDocsArchive({
362
+ archiveResult,
363
+ absoluteUrl,
364
+ explicitOutput,
365
+ dataDir,
366
+ archiveFormat,
367
+ prettyHtml,
368
+ }) {
369
+ const { default: archiver } = await import('archiver');
370
+ const { prettyPrintHtml } = await import('../src/lib.js');
371
+ const output =
372
+ explicitOutput === '-'
373
+ ? null
374
+ : explicitOutput ||
375
+ deriveOutputPath(absoluteUrl, archiveExtension(archiveFormat), dataDir);
376
+ const archiverFn = archiver.default ? archiver.default : archiver;
377
+ const archive = archiverFn('zip', { zlib: { level: 9 } });
378
+ archive.append(archiveResult.markdown, { name: 'document.md' });
379
+ archive.append(
380
+ prettyHtml ? prettyPrintHtml(archiveResult.html) : archiveResult.html,
381
+ { name: 'document.html' }
382
+ );
383
+ for (const img of archiveResult.images || []) {
384
+ archive.append(img.data, { name: `images/${img.filename}` });
385
+ }
386
+
387
+ if (output) {
388
+ fs.mkdirSync(path.dirname(output), { recursive: true });
389
+ const outStream = fs.createWriteStream(output);
390
+ archive.pipe(outStream);
391
+ await archive.finalize();
392
+ await new Promise((resolve) => outStream.on('close', resolve));
393
+ console.error(`Google Doc (archive) saved to: ${output}`);
394
+ } else {
395
+ const { PassThrough } = await import('stream');
396
+ const passthrough = new PassThrough();
397
+ archive.pipe(passthrough);
398
+ passthrough.pipe(process.stdout);
399
+ await archive.finalize();
400
+ }
401
+ }
402
+
310
403
  async function captureUrl(url, options) {
311
404
  const {
312
405
  format,
@@ -325,7 +418,10 @@ async function captureUrl(url, options) {
325
418
  imagesDir,
326
419
  dataDir,
327
420
  keepOriginalLinks,
421
+ capture,
422
+ verbose,
328
423
  } = options;
424
+ const log = makeVerboseLog(verbose);
329
425
 
330
426
  // Ensure URL is absolute
331
427
  let absoluteUrl = url;
@@ -345,142 +441,299 @@ async function captureUrl(url, options) {
345
441
  const { fetchHtml, convertToUtf8, convertRelativeUrls } =
346
442
  await import('../src/lib.js');
347
443
  const { createBrowser } = await import('../src/browser.js');
348
- const { isGoogleDocsUrl, fetchGoogleDoc, fetchGoogleDocAsMarkdown } =
349
- await import('../src/gdocs.js');
444
+ const {
445
+ isGoogleDocsUrl,
446
+ fetchGoogleDoc,
447
+ fetchGoogleDocAsMarkdown,
448
+ fetchGoogleDocFromDocsApi,
449
+ captureGoogleDocWithBrowser,
450
+ selectGoogleDocsCaptureMethod,
451
+ } = await import('../src/gdocs.js');
350
452
  const { stripBase64Images } = await import('../src/extract-images.js');
351
453
 
352
454
  const normalizedFormat = format.toLowerCase();
455
+ log.debug(() => ({
456
+ event: 'capture.start',
457
+ url: absoluteUrl,
458
+ format: normalizedFormat,
459
+ capture,
460
+ engine,
461
+ hasApiToken: Boolean(options.apiToken),
462
+ }));
353
463
 
354
- // Auto-detect Google Docs URLs and use API-based capture
464
+ // Google Docs capture honors --capture:
465
+ // - browser: load /edit and extract DOCS_modelChunk
466
+ // - api without token: public export endpoint
467
+ // - api with token: docs.googleapis.com REST API
355
468
  if (isGoogleDocsUrl(absoluteUrl)) {
356
- try {
357
- const apiToken = options.apiToken;
358
- const { extractAndSaveImages } = await import('../src/extract-images.js');
359
- if (normalizedFormat === 'archive' || normalizedFormat === 'zip') {
360
- const { fetchGoogleDocAsArchive } = await import('../src/gdocs.js');
361
- const { default: archiver } = await import('archiver');
362
- const archiveFormat = options.archiveFormat || 'zip';
363
- const ext =
364
- archiveFormat === 'tar.gz' || archiveFormat === 'gz'
365
- ? 'tar.gz'
366
- : archiveFormat === '7z'
367
- ? '7z'
368
- : archiveFormat === 'tar'
369
- ? 'tar'
370
- : 'zip';
371
- const archiveResult = await fetchGoogleDocAsArchive(absoluteUrl, {
469
+ const apiToken = options.apiToken;
470
+ const gdocsMethod = selectGoogleDocsCaptureMethod(capture, apiToken);
471
+ const modelFormats = new Set([
472
+ 'archive',
473
+ 'zip',
474
+ 'markdown',
475
+ 'md',
476
+ 'html',
477
+ 'txt',
478
+ 'text',
479
+ ]);
480
+ log.debug(() => ({
481
+ event: 'gdocs.capture.selected',
482
+ url: absoluteUrl,
483
+ method: gdocsMethod,
484
+ format: normalizedFormat,
485
+ modelFormat: modelFormats.has(normalizedFormat),
486
+ hasApiToken: Boolean(apiToken),
487
+ }));
488
+
489
+ if (
490
+ gdocsMethod === 'browser-model' &&
491
+ !modelFormats.has(normalizedFormat)
492
+ ) {
493
+ log.debug(() => ({
494
+ event: 'gdocs.capture.fallback-browser-pipeline',
495
+ reason: 'requested format is not supported by editor model renderer',
496
+ format: normalizedFormat,
497
+ }));
498
+ // Screenshot/PDF/DOCX formats should use the regular browser pipeline below.
499
+ } else if (gdocsMethod === 'browser-model') {
500
+ try {
501
+ const result = await captureGoogleDocWithBrowser(absoluteUrl, {
502
+ engine,
372
503
  apiToken,
504
+ log,
373
505
  });
374
- const output =
375
- explicitOutput === '-'
376
- ? null
377
- : explicitOutput || deriveOutputPath(absoluteUrl, ext, dataDir);
378
- if (output) {
379
- const outStream = fs.createWriteStream(output);
380
- const archive = archiver.default
381
- ? archiver.default('zip', { zlib: { level: 9 } })
382
- : archiver('zip', { zlib: { level: 9 } });
383
- archive.pipe(outStream);
384
- const { prettyPrintHtml: ppHtml } = await import('../src/lib.js');
385
- archive.append(archiveResult.markdown, { name: 'document.md' });
386
- const htmlContent = options.prettyHtml
387
- ? ppHtml(archiveResult.html)
388
- : archiveResult.html;
389
- archive.append(htmlContent, {
390
- name: 'document.html',
506
+ log.debug(() => ({
507
+ event: 'gdocs.capture.browser-model.rendered',
508
+ documentId: result.documentId,
509
+ blocks: result.capture?.blocks?.length || 0,
510
+ tables: result.capture?.tables?.length || 0,
511
+ images: result.capture?.images?.length || 0,
512
+ markdownBytes: Buffer.byteLength(result.markdown || ''),
513
+ htmlBytes: Buffer.byteLength(result.html || ''),
514
+ textBytes: Buffer.byteLength(result.text || ''),
515
+ }));
516
+ if (normalizedFormat === 'archive' || normalizedFormat === 'zip') {
517
+ await writeGoogleDocsArchive({
518
+ archiveResult: { ...result, images: [] },
519
+ absoluteUrl,
520
+ explicitOutput,
521
+ dataDir,
522
+ archiveFormat: options.archiveFormat || 'zip',
523
+ prettyHtml: options.prettyHtml,
391
524
  });
392
- for (const img of archiveResult.images) {
393
- archive.append(img.data, { name: `images/${img.filename}` });
394
- }
395
- await archive.finalize();
396
- await new Promise((resolve) => outStream.on('close', resolve));
397
- console.error(`Google Doc (archive) saved to: ${output}`);
398
525
  } else {
399
- const { PassThrough } = await import('stream');
400
- const passthrough = new PassThrough();
401
- const archiverMod = await import('archiver');
402
- const archiverFn =
403
- archiverMod.default?.default || archiverMod.default;
404
- const archive = archiverFn('zip', { zlib: { level: 9 } });
405
- archive.pipe(passthrough);
406
- passthrough.pipe(process.stdout);
407
- const { prettyPrintHtml: ppHtml2 } = await import('../src/lib.js');
408
- archive.append(archiveResult.markdown, { name: 'document.md' });
409
- const htmlContent2 = options.prettyHtml
410
- ? ppHtml2(archiveResult.html)
411
- : archiveResult.html;
412
- archive.append(htmlContent2, {
413
- name: 'document.html',
526
+ const rendered =
527
+ normalizedFormat === 'html'
528
+ ? result.html
529
+ : normalizedFormat === 'txt' || normalizedFormat === 'text'
530
+ ? result.text
531
+ : result.markdown;
532
+ const ext =
533
+ normalizedFormat === 'html'
534
+ ? 'html'
535
+ : normalizedFormat === 'txt' || normalizedFormat === 'text'
536
+ ? 'txt'
537
+ : 'md';
538
+ writeGoogleDocsTextOutput({
539
+ content: rendered,
540
+ absoluteUrl,
541
+ ext,
542
+ explicitOutput,
543
+ dataDir,
544
+ label: `Google Doc (${gdocsMethod})`,
414
545
  });
415
- for (const img of archiveResult.images) {
416
- archive.append(img.data, { name: `images/${img.filename}` });
417
- }
418
- await archive.finalize();
419
546
  }
420
- } else if (normalizedFormat === 'markdown' || normalizedFormat === 'md') {
421
- const result = await fetchGoogleDocAsMarkdown(absoluteUrl, {
547
+ return;
548
+ } catch (err) {
549
+ console.error(`Error capturing Google Doc: ${err.message}`);
550
+ process.exit(1);
551
+ }
552
+ } else if (gdocsMethod === 'docs-api') {
553
+ try {
554
+ const result = await fetchGoogleDocFromDocsApi(absoluteUrl, {
422
555
  apiToken,
556
+ log,
423
557
  });
424
- let { markdown } = result;
425
- const output =
426
- explicitOutput === '-'
427
- ? null
428
- : explicitOutput || deriveOutputPath(absoluteUrl, 'md', dataDir);
429
- if (output) {
430
- if (embedImages) {
431
- // Keep base64 data URIs inline
432
- } else if (keepOriginalLinks) {
433
- const strip = stripBase64Images(markdown);
434
- if (strip.stripped > 0) {
435
- markdown = strip.markdown;
436
- console.error(
437
- `Stripped ${strip.stripped} base64 images (keeping original links)`
438
- );
558
+ log.debug(() => ({
559
+ event: 'gdocs.capture.docs-api.rendered',
560
+ documentId: result.documentId,
561
+ markdownBytes: Buffer.byteLength(result.markdown || ''),
562
+ htmlBytes: Buffer.byteLength(result.html || ''),
563
+ textBytes: Buffer.byteLength(result.text || ''),
564
+ }));
565
+ if (normalizedFormat === 'archive' || normalizedFormat === 'zip') {
566
+ await writeGoogleDocsArchive({
567
+ archiveResult: { ...result, images: [] },
568
+ absoluteUrl,
569
+ explicitOutput,
570
+ dataDir,
571
+ archiveFormat: options.archiveFormat || 'zip',
572
+ prettyHtml: options.prettyHtml,
573
+ });
574
+ } else {
575
+ const rendered =
576
+ normalizedFormat === 'html'
577
+ ? result.html
578
+ : normalizedFormat === 'txt' || normalizedFormat === 'text'
579
+ ? result.text
580
+ : result.markdown;
581
+ const ext =
582
+ normalizedFormat === 'html'
583
+ ? 'html'
584
+ : normalizedFormat === 'txt' || normalizedFormat === 'text'
585
+ ? 'txt'
586
+ : 'md';
587
+ writeGoogleDocsTextOutput({
588
+ content: rendered,
589
+ absoluteUrl,
590
+ ext,
591
+ explicitOutput,
592
+ dataDir,
593
+ label: `Google Doc (${gdocsMethod})`,
594
+ });
595
+ }
596
+ return;
597
+ } catch (err) {
598
+ console.error(`Error capturing Google Doc: ${err.message}`);
599
+ process.exit(1);
600
+ }
601
+ } else {
602
+ try {
603
+ const { extractAndSaveImages } =
604
+ await import('../src/extract-images.js');
605
+ if (normalizedFormat === 'archive' || normalizedFormat === 'zip') {
606
+ const { fetchGoogleDocAsArchive } = await import('../src/gdocs.js');
607
+ const { default: archiver } = await import('archiver');
608
+ const archiveFormat = options.archiveFormat || 'zip';
609
+ const ext =
610
+ archiveFormat === 'tar.gz' || archiveFormat === 'gz'
611
+ ? 'tar.gz'
612
+ : archiveFormat === '7z'
613
+ ? '7z'
614
+ : archiveFormat === 'tar'
615
+ ? 'tar'
616
+ : 'zip';
617
+ const archiveResult = await fetchGoogleDocAsArchive(absoluteUrl, {
618
+ apiToken,
619
+ log,
620
+ });
621
+ const output =
622
+ explicitOutput === '-'
623
+ ? null
624
+ : explicitOutput || deriveOutputPath(absoluteUrl, ext, dataDir);
625
+ if (output) {
626
+ const outStream = fs.createWriteStream(output);
627
+ const archive = archiver.default
628
+ ? archiver.default('zip', { zlib: { level: 9 } })
629
+ : archiver('zip', { zlib: { level: 9 } });
630
+ archive.pipe(outStream);
631
+ const { prettyPrintHtml: ppHtml } = await import('../src/lib.js');
632
+ archive.append(archiveResult.markdown, { name: 'document.md' });
633
+ const htmlContent = options.prettyHtml
634
+ ? ppHtml(archiveResult.html)
635
+ : archiveResult.html;
636
+ archive.append(htmlContent, {
637
+ name: 'document.html',
638
+ });
639
+ for (const img of archiveResult.images) {
640
+ archive.append(img.data, { name: `images/${img.filename}` });
439
641
  }
642
+ await archive.finalize();
643
+ await new Promise((resolve) => outStream.on('close', resolve));
644
+ console.error(`Google Doc (archive) saved to: ${output}`);
440
645
  } else {
441
- const outputDir = path.dirname(output);
442
- const extracted = extractAndSaveImages(markdown, outputDir, {
443
- imagesDir: options.imagesDir || 'images',
646
+ const { PassThrough } = await import('stream');
647
+ const passthrough = new PassThrough();
648
+ const archiverMod = await import('archiver');
649
+ const archiverFn =
650
+ archiverMod.default?.default || archiverMod.default;
651
+ const archive = archiverFn('zip', { zlib: { level: 9 } });
652
+ archive.pipe(passthrough);
653
+ passthrough.pipe(process.stdout);
654
+ const { prettyPrintHtml: ppHtml2 } = await import('../src/lib.js');
655
+ archive.append(archiveResult.markdown, { name: 'document.md' });
656
+ const htmlContent2 = options.prettyHtml
657
+ ? ppHtml2(archiveResult.html)
658
+ : archiveResult.html;
659
+ archive.append(htmlContent2, {
660
+ name: 'document.html',
444
661
  });
445
- if (extracted.extracted > 0) {
446
- markdown = extracted.markdown;
447
- console.error(
448
- `Extracted ${extracted.extracted} images to ${options.imagesDir || 'images'}/`
449
- );
662
+ for (const img of archiveResult.images) {
663
+ archive.append(img.data, { name: `images/${img.filename}` });
450
664
  }
665
+ await archive.finalize();
666
+ }
667
+ } else if (
668
+ normalizedFormat === 'markdown' ||
669
+ normalizedFormat === 'md'
670
+ ) {
671
+ const result = await fetchGoogleDocAsMarkdown(absoluteUrl, {
672
+ apiToken,
673
+ log,
674
+ });
675
+ let { markdown } = result;
676
+ const output =
677
+ explicitOutput === '-'
678
+ ? null
679
+ : explicitOutput || deriveOutputPath(absoluteUrl, 'md', dataDir);
680
+ if (output) {
681
+ if (embedImages) {
682
+ // Keep base64 data URIs inline
683
+ } else if (keepOriginalLinks) {
684
+ const strip = stripBase64Images(markdown);
685
+ if (strip.stripped > 0) {
686
+ markdown = strip.markdown;
687
+ console.error(
688
+ `Stripped ${strip.stripped} base64 images (keeping original links)`
689
+ );
690
+ }
691
+ } else {
692
+ const outputDir = path.dirname(output);
693
+ const extracted = extractAndSaveImages(markdown, outputDir, {
694
+ imagesDir: options.imagesDir || 'images',
695
+ });
696
+ if (extracted.extracted > 0) {
697
+ markdown = extracted.markdown;
698
+ console.error(
699
+ `Extracted ${extracted.extracted} images to ${options.imagesDir || 'images'}/`
700
+ );
701
+ }
702
+ }
703
+ fs.mkdirSync(path.dirname(output), { recursive: true });
704
+ fs.writeFileSync(output, markdown, 'utf-8');
705
+ console.error(`Google Doc Markdown saved to: ${output}`);
706
+ } else {
707
+ process.stdout.write(markdown);
451
708
  }
452
- fs.mkdirSync(path.dirname(output), { recursive: true });
453
- fs.writeFileSync(output, markdown, 'utf-8');
454
- console.error(`Google Doc Markdown saved to: ${output}`);
455
- } else {
456
- process.stdout.write(markdown);
457
- }
458
- } else {
459
- const gdocsFormat =
460
- normalizedFormat === 'png' || normalizedFormat === 'image'
461
- ? 'html'
462
- : normalizedFormat;
463
- const result = await fetchGoogleDoc(absoluteUrl, {
464
- format: gdocsFormat,
465
- apiToken,
466
- });
467
- const output =
468
- explicitOutput === '-'
469
- ? null
470
- : explicitOutput ||
471
- deriveOutputPath(absoluteUrl, gdocsFormat, dataDir);
472
- if (output) {
473
- fs.mkdirSync(path.dirname(output), { recursive: true });
474
- fs.writeFileSync(output, result.content, 'utf-8');
475
- console.error(`Google Doc (${gdocsFormat}) saved to: ${output}`);
476
709
  } else {
477
- process.stdout.write(result.content);
710
+ const gdocsFormat =
711
+ normalizedFormat === 'png' || normalizedFormat === 'image'
712
+ ? 'html'
713
+ : normalizedFormat;
714
+ const result = await fetchGoogleDoc(absoluteUrl, {
715
+ format: gdocsFormat,
716
+ apiToken,
717
+ log,
718
+ });
719
+ const output =
720
+ explicitOutput === '-'
721
+ ? null
722
+ : explicitOutput ||
723
+ deriveOutputPath(absoluteUrl, gdocsFormat, dataDir);
724
+ if (output) {
725
+ fs.mkdirSync(path.dirname(output), { recursive: true });
726
+ fs.writeFileSync(output, result.content, 'utf-8');
727
+ console.error(`Google Doc (${gdocsFormat}) saved to: ${output}`);
728
+ } else {
729
+ process.stdout.write(result.content);
730
+ }
478
731
  }
732
+ return;
733
+ } catch (err) {
734
+ console.error(`Error capturing Google Doc: ${err.message}`);
735
+ process.exit(1);
479
736
  }
480
- return;
481
- } catch (err) {
482
- console.error(`Error capturing Google Doc: ${err.message}`);
483
- process.exit(1);
484
737
  }
485
738
  }
486
739
 
@@ -893,6 +1146,7 @@ async function main() {
893
1146
  archiveFormat: config.archiveFormat,
894
1147
  keepOriginalLinks: config.keepOriginalLinks,
895
1148
  prettyHtml: !config.noPrettyHtml,
1149
+ capture: config.capture,
896
1150
  });
897
1151
  } else {
898
1152
  // No arguments - show error
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@link-assistant/web-capture",
3
- "version": "1.7.5",
3
+ "version": "1.7.7",
4
4
  "description": "CLI and microservice to render web pages as HTML, Markdown, or PNG",
5
5
  "main": "src/index.js",
6
6
  "bin": {
@@ -40,6 +40,7 @@
40
40
  "he": "^1.2.0",
41
41
  "iconv-lite": "^0.6.3",
42
42
  "lino-arguments": "^0.3.0",
43
+ "log-lazy": "^1.0.4",
43
44
  "node-fetch": "^2.7.0",
44
45
  "playwright": "^1.49.0",
45
46
  "puppeteer": "^24.8.2",
package/src/browser.js CHANGED
@@ -172,6 +172,20 @@ function createPageAdapter(rawPage, commander, engineType) {
172
172
  await commander.destroy();
173
173
  await rawPage.close();
174
174
  },
175
+ async addInitScript(fn) {
176
+ if (engineType === 'playwright' && rawPage.addInitScript) {
177
+ await rawPage.addInitScript(fn);
178
+ } else if (rawPage.evaluateOnNewDocument) {
179
+ await rawPage.evaluateOnNewDocument(fn);
180
+ }
181
+ },
182
+ async waitForTimeout(ms) {
183
+ if (rawPage.waitForTimeout) {
184
+ await rawPage.waitForTimeout(ms);
185
+ } else {
186
+ await new Promise((resolve) => setTimeout(resolve, ms));
187
+ }
188
+ },
175
189
 
176
190
  // --- browser-commander v0.8.0+: PDF generation ---
177
191
  // Usage: await page.pdf({ pdfOptions: { format: 'A4', printBackground: true } })
@@ -211,6 +225,7 @@ function createPageAdapter(rawPage, commander, engineType) {
211
225
 
212
226
  // Engine type identifier
213
227
  type: engineType,
228
+ rawPage,
214
229
  };
215
230
  }
216
231