@link-assistant/web-capture 1.7.4 → 1.7.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.
package/CHANGELOG.md CHANGED
@@ -1,5 +1,17 @@
1
1
  # @link-assistant/web-capture
2
2
 
3
+ ## 1.7.6
4
+
5
+ ### Patch Changes
6
+
7
+ - Support Google Docs capture selection through `--capture browser` editor-model parsing, public export API capture, and authenticated Docs REST API capture.
8
+
9
+ ## 1.7.5
10
+
11
+ ### Patch Changes
12
+
13
+ - dfaf007: Remove the unsupported `/gdocs` HTTP API route from the JavaScript and Rust server surfaces while keeping Google Docs URL capture through normal output formats.
14
+
3
15
  ## 1.7.4
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({
@@ -230,7 +247,7 @@ const config = makeConfig({
230
247
  'Capture private Google Doc with API token'
231
248
  )
232
249
  .epilogue(
233
- 'API Endpoints (in server mode):\n GET /html?url=<URL>&engine=<ENGINE> Get rendered HTML\n GET /markdown?url=<URL> Get Markdown conversion\n GET /image?url=<URL>&format=png|jpeg&theme=light|dark Screenshot\n GET /archive?url=<URL>&localImages=true&documentFormat=markdown|html ZIP archive\n GET /pdf?url=<URL>&theme=light|dark PDF with embedded images\n GET /docx?url=<URL> DOCX with embedded images\n GET /fetch?url=<URL> Proxy fetch\n GET /stream?url=<URL> Streaming proxy\n GET /gdocs?url=<URL>&format=markdown|html|txt Google Docs capture'
250
+ 'API Endpoints (in server mode):\n GET /html?url=<URL>&engine=<ENGINE> Get rendered HTML\n GET /markdown?url=<URL> Get Markdown conversion\n GET /image?url=<URL>&format=png|jpeg&theme=light|dark Screenshot\n GET /archive?url=<URL>&localImages=true&documentFormat=markdown|html ZIP archive\n GET /pdf?url=<URL>&theme=light|dark PDF with embedded images\n GET /docx?url=<URL> DOCX with embedded images\n GET /fetch?url=<URL> Proxy fetch\n GET /stream?url=<URL> Streaming proxy'
234
251
  )
235
252
  .strict(),
236
253
  lenv: {
@@ -271,7 +288,6 @@ async function startServer(port) {
271
288
  console.log(` GET /docx?url=<URL> - DOCX with embedded images`);
272
289
  console.log(` GET /fetch?url=<URL> - Proxy fetch content`);
273
290
  console.log(` GET /stream?url=<URL> - Stream content`);
274
- console.log(` GET /gdocs?url=<URL> - Capture Google Docs document`);
275
291
  console.log('');
276
292
  console.log('Press Ctrl+C to stop the server');
277
293
  resolve(server);
@@ -308,6 +324,82 @@ function deriveOutputPath(absoluteUrl, ext, dataDir) {
308
324
  return path.join(dir, `document.${ext}`);
309
325
  }
310
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
+
311
403
  async function captureUrl(url, options) {
312
404
  const {
313
405
  format,
@@ -326,7 +418,10 @@ async function captureUrl(url, options) {
326
418
  imagesDir,
327
419
  dataDir,
328
420
  keepOriginalLinks,
421
+ capture,
422
+ verbose,
329
423
  } = options;
424
+ const log = makeVerboseLog(verbose);
330
425
 
331
426
  // Ensure URL is absolute
332
427
  let absoluteUrl = url;
@@ -346,142 +441,299 @@ async function captureUrl(url, options) {
346
441
  const { fetchHtml, convertToUtf8, convertRelativeUrls } =
347
442
  await import('../src/lib.js');
348
443
  const { createBrowser } = await import('../src/browser.js');
349
- const { isGoogleDocsUrl, fetchGoogleDoc, fetchGoogleDocAsMarkdown } =
350
- 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');
351
452
  const { stripBase64Images } = await import('../src/extract-images.js');
352
453
 
353
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
+ }));
354
463
 
355
- // 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
356
468
  if (isGoogleDocsUrl(absoluteUrl)) {
357
- try {
358
- const apiToken = options.apiToken;
359
- const { extractAndSaveImages } = await import('../src/extract-images.js');
360
- if (normalizedFormat === 'archive' || normalizedFormat === 'zip') {
361
- const { fetchGoogleDocAsArchive } = await import('../src/gdocs.js');
362
- const { default: archiver } = await import('archiver');
363
- const archiveFormat = options.archiveFormat || 'zip';
364
- const ext =
365
- archiveFormat === 'tar.gz' || archiveFormat === 'gz'
366
- ? 'tar.gz'
367
- : archiveFormat === '7z'
368
- ? '7z'
369
- : archiveFormat === 'tar'
370
- ? 'tar'
371
- : 'zip';
372
- 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,
373
503
  apiToken,
504
+ log,
374
505
  });
375
- const output =
376
- explicitOutput === '-'
377
- ? null
378
- : explicitOutput || deriveOutputPath(absoluteUrl, ext, dataDir);
379
- if (output) {
380
- const outStream = fs.createWriteStream(output);
381
- const archive = archiver.default
382
- ? archiver.default('zip', { zlib: { level: 9 } })
383
- : archiver('zip', { zlib: { level: 9 } });
384
- archive.pipe(outStream);
385
- const { prettyPrintHtml: ppHtml } = await import('../src/lib.js');
386
- archive.append(archiveResult.markdown, { name: 'document.md' });
387
- const htmlContent = options.prettyHtml
388
- ? ppHtml(archiveResult.html)
389
- : archiveResult.html;
390
- archive.append(htmlContent, {
391
- 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,
392
524
  });
393
- for (const img of archiveResult.images) {
394
- archive.append(img.data, { name: `images/${img.filename}` });
395
- }
396
- await archive.finalize();
397
- await new Promise((resolve) => outStream.on('close', resolve));
398
- console.error(`Google Doc (archive) saved to: ${output}`);
399
525
  } else {
400
- const { PassThrough } = await import('stream');
401
- const passthrough = new PassThrough();
402
- const archiverMod = await import('archiver');
403
- const archiverFn =
404
- archiverMod.default?.default || archiverMod.default;
405
- const archive = archiverFn('zip', { zlib: { level: 9 } });
406
- archive.pipe(passthrough);
407
- passthrough.pipe(process.stdout);
408
- const { prettyPrintHtml: ppHtml2 } = await import('../src/lib.js');
409
- archive.append(archiveResult.markdown, { name: 'document.md' });
410
- const htmlContent2 = options.prettyHtml
411
- ? ppHtml2(archiveResult.html)
412
- : archiveResult.html;
413
- archive.append(htmlContent2, {
414
- 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})`,
415
545
  });
416
- for (const img of archiveResult.images) {
417
- archive.append(img.data, { name: `images/${img.filename}` });
418
- }
419
- await archive.finalize();
420
546
  }
421
- } else if (normalizedFormat === 'markdown' || normalizedFormat === 'md') {
422
- 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, {
423
555
  apiToken,
556
+ log,
424
557
  });
425
- let { markdown } = result;
426
- const output =
427
- explicitOutput === '-'
428
- ? null
429
- : explicitOutput || deriveOutputPath(absoluteUrl, 'md', dataDir);
430
- if (output) {
431
- if (embedImages) {
432
- // Keep base64 data URIs inline
433
- } else if (keepOriginalLinks) {
434
- const strip = stripBase64Images(markdown);
435
- if (strip.stripped > 0) {
436
- markdown = strip.markdown;
437
- console.error(
438
- `Stripped ${strip.stripped} base64 images (keeping original links)`
439
- );
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}` });
440
641
  }
642
+ await archive.finalize();
643
+ await new Promise((resolve) => outStream.on('close', resolve));
644
+ console.error(`Google Doc (archive) saved to: ${output}`);
441
645
  } else {
442
- const outputDir = path.dirname(output);
443
- const extracted = extractAndSaveImages(markdown, outputDir, {
444
- 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',
445
661
  });
446
- if (extracted.extracted > 0) {
447
- markdown = extracted.markdown;
448
- console.error(
449
- `Extracted ${extracted.extracted} images to ${options.imagesDir || 'images'}/`
450
- );
662
+ for (const img of archiveResult.images) {
663
+ archive.append(img.data, { name: `images/${img.filename}` });
451
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);
452
708
  }
453
- fs.mkdirSync(path.dirname(output), { recursive: true });
454
- fs.writeFileSync(output, markdown, 'utf-8');
455
- console.error(`Google Doc Markdown saved to: ${output}`);
456
- } else {
457
- process.stdout.write(markdown);
458
- }
459
- } else {
460
- const gdocsFormat =
461
- normalizedFormat === 'png' || normalizedFormat === 'image'
462
- ? 'html'
463
- : normalizedFormat;
464
- const result = await fetchGoogleDoc(absoluteUrl, {
465
- format: gdocsFormat,
466
- apiToken,
467
- });
468
- const output =
469
- explicitOutput === '-'
470
- ? null
471
- : explicitOutput ||
472
- deriveOutputPath(absoluteUrl, gdocsFormat, dataDir);
473
- if (output) {
474
- fs.mkdirSync(path.dirname(output), { recursive: true });
475
- fs.writeFileSync(output, result.content, 'utf-8');
476
- console.error(`Google Doc (${gdocsFormat}) saved to: ${output}`);
477
709
  } else {
478
- 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
+ }
479
731
  }
732
+ return;
733
+ } catch (err) {
734
+ console.error(`Error capturing Google Doc: ${err.message}`);
735
+ process.exit(1);
480
736
  }
481
- return;
482
- } catch (err) {
483
- console.error(`Error capturing Google Doc: ${err.message}`);
484
- process.exit(1);
485
737
  }
486
738
  }
487
739
 
@@ -894,6 +1146,7 @@ async function main() {
894
1146
  archiveFormat: config.archiveFormat,
895
1147
  keepOriginalLinks: config.keepOriginalLinks,
896
1148
  prettyHtml: !config.noPrettyHtml,
1149
+ capture: config.capture,
897
1150
  });
898
1151
  } else {
899
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.4",
3
+ "version": "1.7.6",
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