@link-assistant/web-capture 1.10.9 → 1.10.11

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.10.11
4
+
5
+ ### Patch Changes
6
+
7
+ - 611e6e7: Upgrade Archiver to version 8 so downstream installs no longer include the deprecated glob package.
8
+
9
+ ## 1.10.10
10
+
11
+ ### Patch Changes
12
+
13
+ - 65d7dcc: Add shared AI dialog capture for ChatGPT share links with structured transcript output and unsupported-provider diagnostics.
14
+
3
15
  ## 1.10.9
4
16
 
5
17
  ### Patch Changes
package/README.md CHANGED
@@ -15,6 +15,7 @@ A CLI and microservice to fetch URLs and render them as:
15
15
  - **ZIP archive**: Markdown + locally downloaded images
16
16
  - **PDF**: Print-quality document with embedded images
17
17
  - **DOCX**: Word document with embedded images
18
+ - **Shared AI dialog transcript**: ChatGPT share captures with structured diagnostics for unsupported providers
18
19
 
19
20
  ## Installation
20
21
 
@@ -99,6 +100,10 @@ web-capture https://example.com --no-extract-latex --no-post-process -o page.md
99
100
  # Structured search-provider capture (JSON by default)
100
101
  web-capture search "formal methods" --provider wikipedia
101
102
 
103
+ # Shared AI dialog capture (JSON by default)
104
+ web-capture shared-dialog https://chatgpt.com/share/SHARE_ID
105
+ web-capture shared-dialog https://chatgpt.com/share/SHARE_ID --format demo-memory -o -
106
+
102
107
  # Start as API server
103
108
  web-capture --serve
104
109
 
@@ -321,6 +326,17 @@ Example response (`format=json`):
321
326
  }
322
327
  ```
323
328
 
329
+ ### Shared Dialog Endpoint
330
+
331
+ ```
332
+ GET /shared-dialog?url=<URL>&format=json|meta-language|demo-memory|markdown|txt
333
+ ```
334
+
335
+ Captures replayable shared AI dialog transcripts. ChatGPT share links are
336
+ decoded from their embedded `linear_conversation` data. Providers that cannot
337
+ currently expose transcript data, such as Google AI Mode interstitial pages,
338
+ return a structured unsupported diagnostic.
339
+
324
340
  ## CLI Reference
325
341
 
326
342
  ### Server Mode
@@ -1,8 +1,4 @@
1
1
  #!/usr/bin/env node
2
- // CLI entry point for web-capture
3
- // Supports two modes:
4
- // 1. Server mode: web-capture --serve [--port 3000]
5
- // 2. Capture mode: web-capture <url> [options]
6
2
 
7
3
  import fs from 'fs';
8
4
  import path from 'path';
@@ -27,7 +23,11 @@ function makeVerboseLog(enabled) {
27
23
  });
28
24
  }
29
25
 
30
- // Create configuration using lino-arguments pattern
26
+ const originalConsoleLog = console.log;
27
+ console.log = (...args) =>
28
+ String(args[0]).includes(' variables from ')
29
+ ? console.error(...args)
30
+ : originalConsoleLog(...args);
31
31
  const config = makeConfig({
32
32
  yargs: ({ yargs, getenv }) =>
33
33
  yargs
@@ -49,6 +49,15 @@ const config = makeConfig({
49
49
  description: 'Search query',
50
50
  })
51
51
  )
52
+ .command(
53
+ 'shared-dialog <sharedDialogUrl>',
54
+ 'Capture replayable shared AI dialog transcripts',
55
+ (yargs) =>
56
+ yargs.positional('sharedDialogUrl', {
57
+ type: 'string',
58
+ description: 'Shared AI dialog URL',
59
+ })
60
+ )
52
61
  .option('provider', {
53
62
  type: 'string',
54
63
  description:
@@ -78,7 +87,7 @@ const config = makeConfig({
78
87
  alias: 'f',
79
88
  type: 'string',
80
89
  description:
81
- 'Output format: markdown, md, html, txt, text, image, png, jpeg, pdf, docx, archive',
90
+ 'Output format: markdown, md, html, txt, text, image, png, jpeg, pdf, docx, archive, json, meta-language, demo-memory',
82
91
  default: 'markdown',
83
92
  })
84
93
  .option('theme', {
@@ -289,7 +298,8 @@ const config = makeConfig({
289
298
  'Capture private Google Doc with API token'
290
299
  )
291
300
  .epilogue(
292
- '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 /txt?url=<URL> Get text content\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'
301
+ '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 /txt?url=<URL> Get text content\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' +
302
+ '\n GET /shared-dialog?url=<URL>&format=json|meta-language|demo-memory|markdown|txt Shared AI dialog capture'
293
303
  )
294
304
  .strict(),
295
305
  lenv: {
@@ -297,6 +307,7 @@ const config = makeConfig({
297
307
  path: '.lenv',
298
308
  },
299
309
  });
310
+ console.log = originalConsoleLog;
300
311
 
301
312
  function getUrlArgument() {
302
313
  if (typeof config.url === 'string' && config.url.length > 0) {
@@ -331,6 +342,7 @@ async function startServer(port) {
331
342
  console.log(` GET /docx?url=<URL> - DOCX with embedded images`);
332
343
  console.log(` GET /fetch?url=<URL> - Proxy fetch content`);
333
344
  console.log(` GET /stream?url=<URL> - Stream content`);
345
+ console.log(` GET /shared-dialog?url=<URL> - Shared AI dialog capture`);
334
346
  console.log('');
335
347
  console.log('Press Ctrl+C to stop the server');
336
348
  resolve(server);
@@ -537,15 +549,14 @@ async function writeGoogleDocsArchive({
537
549
  archiveFormat,
538
550
  prettyHtml,
539
551
  }) {
540
- const { default: archiver } = await import('archiver');
552
+ const { ZipArchive } = await import('archiver');
541
553
  const { prettyPrintHtml } = await import('../src/lib.js');
542
554
  const output =
543
555
  explicitOutput === '-'
544
556
  ? null
545
557
  : explicitOutput ||
546
558
  deriveOutputPath(absoluteUrl, archiveExtension(archiveFormat), dataDir);
547
- const archiverFn = archiver.default ? archiver.default : archiver;
548
- const archive = archiverFn('zip', { zlib: { level: 9 } });
559
+ const archive = new ZipArchive({ zlib: { level: 9 } });
549
560
  archive.append(archiveResult.markdown, { name: 'document.md' });
550
561
  archive.append(
551
562
  prettyHtml ? prettyPrintHtml(archiveResult.html) : archiveResult.html,
@@ -823,7 +834,7 @@ async function captureUrl(url, options) {
823
834
  try {
824
835
  if (normalizedFormat === 'archive' || normalizedFormat === 'zip') {
825
836
  const { fetchGoogleDocAsArchive } = await import('../src/gdocs.js');
826
- const { default: archiver } = await import('archiver');
837
+ const { ZipArchive } = await import('archiver');
827
838
  const archiveFormat = options.archiveFormat || 'zip';
828
839
  const ext =
829
840
  archiveFormat === 'tar.gz' || archiveFormat === 'gz'
@@ -843,9 +854,7 @@ async function captureUrl(url, options) {
843
854
  : explicitOutput || deriveOutputPath(absoluteUrl, ext, dataDir);
844
855
  if (output) {
845
856
  const outStream = fs.createWriteStream(output);
846
- const archive = archiver.default
847
- ? archiver.default('zip', { zlib: { level: 9 } })
848
- : archiver('zip', { zlib: { level: 9 } });
857
+ const archive = new ZipArchive({ zlib: { level: 9 } });
849
858
  archive.pipe(outStream);
850
859
  const { prettyPrintHtml: ppHtml } = await import('../src/lib.js');
851
860
  archive.append(archiveResult.markdown, { name: 'document.md' });
@@ -864,10 +873,7 @@ async function captureUrl(url, options) {
864
873
  } else {
865
874
  const { PassThrough } = await import('stream');
866
875
  const passthrough = new PassThrough();
867
- const archiverMod = await import('archiver');
868
- const archiverFn =
869
- archiverMod.default?.default || archiverMod.default;
870
- const archive = archiverFn('zip', { zlib: { level: 9 } });
876
+ const archive = new ZipArchive({ zlib: { level: 9 } });
871
877
  archive.pipe(passthrough);
872
878
  passthrough.pipe(process.stdout);
873
879
  const { prettyPrintHtml: ppHtml2 } = await import('../src/lib.js');
@@ -1099,7 +1105,7 @@ async function captureUrl(url, options) {
1099
1105
  console.error(`DOCX saved to: ${outPath}`);
1100
1106
  } else if (normalizedFormat === 'archive' || normalizedFormat === 'zip') {
1101
1107
  // ZIP archive
1102
- const { default: archiver } = await import('archiver');
1108
+ const { ZipArchive } = await import('archiver');
1103
1109
  const { fetchHtml, convertHtmlToMarkdown, convertRelativeUrls } =
1104
1110
  await import('../src/lib.js');
1105
1111
  const { retry } = await import('../src/retry.js');
@@ -1119,9 +1125,7 @@ async function captureUrl(url, options) {
1119
1125
  explicitOutput ||
1120
1126
  `${new URL(absoluteUrl).hostname.replace(/\./g, '-')}-archive.zip`;
1121
1127
  const outStream = fs.createWriteStream(outPath);
1122
- const archive = archiver.default
1123
- ? archiver.default('zip', { zlib: { level: 9 } })
1124
- : archiver('zip', { zlib: { level: 9 } });
1128
+ const archive = new ZipArchive({ zlib: { level: 9 } });
1125
1129
  archive.pipe(outStream);
1126
1130
 
1127
1131
  // Collect images
@@ -1370,14 +1374,8 @@ async function captureUrl(url, options) {
1370
1374
 
1371
1375
  async function runSearch(query) {
1372
1376
  const { search, formatSearchAsMarkdown } = await import('../src/search.js');
1373
- // `--format` defaults to `markdown` for capture mode, but search results are
1374
- // primarily structured JSON. Only honor the format flag when the user passed
1375
- // it explicitly; otherwise default search output to JSON.
1376
- const formatPassedExplicitly = process.argv.some(
1377
- (arg) => arg === '-f' || arg === '--format' || arg.startsWith('--format=')
1378
- );
1379
1377
  const format = (
1380
- formatPassedExplicitly ? config.format : 'json'
1378
+ formatPassedExplicitly() ? config.format : 'json'
1381
1379
  ).toLowerCase();
1382
1380
  const result = await search({
1383
1381
  query,
@@ -1391,13 +1389,40 @@ async function runSearch(query) {
1391
1389
  }
1392
1390
  }
1393
1391
 
1392
+ async function runSharedDialog(sharedDialogUrl) {
1393
+ const { runSharedDialogCli } = await import('../src/shared-dialog-cli.js');
1394
+ await runSharedDialogCli(sharedDialogUrl, {
1395
+ capture: config.capture,
1396
+ engine: config.engine,
1397
+ format: formatPassedExplicitly() ? config.format : 'json',
1398
+ output: config.output,
1399
+ });
1400
+ }
1401
+
1402
+ function formatPassedExplicitly() {
1403
+ return process.argv.some(
1404
+ (arg) => arg === '-f' || arg === '--format' || arg.startsWith('--format=')
1405
+ );
1406
+ }
1407
+
1394
1408
  async function main() {
1395
1409
  const url = getUrlArgument();
1396
- // The `query` key is only present when the `search <query>` command matched.
1397
1410
  const isSearch = Object.prototype.hasOwnProperty.call(config, 'query');
1411
+ const isSharedDialog = Object.prototype.hasOwnProperty.call(
1412
+ config,
1413
+ 'sharedDialogUrl'
1414
+ );
1398
1415
 
1399
- if (isSearch) {
1400
- // Search mode
1416
+ if (isSharedDialog) {
1417
+ const sharedDialogUrl =
1418
+ typeof config.sharedDialogUrl === 'string' ? config.sharedDialogUrl : '';
1419
+ if (!sharedDialogUrl.trim()) {
1420
+ console.error('Error: Missing shared-dialog URL');
1421
+ console.error('Usage: web-capture shared-dialog <url> [--format <fmt>]');
1422
+ process.exit(1);
1423
+ }
1424
+ await runSharedDialog(sharedDialogUrl);
1425
+ } else if (isSearch) {
1401
1426
  const query = typeof config.query === 'string' ? config.query : '';
1402
1427
  if (!query.trim()) {
1403
1428
  console.error('Error: Missing search query');
@@ -1406,7 +1431,6 @@ async function main() {
1406
1431
  }
1407
1432
  await runSearch(query);
1408
1433
  } else if (config.serve) {
1409
- // Server mode
1410
1434
  await startServer(config.port);
1411
1435
  } else if (url) {
1412
1436
  // --no-extract-images is an alias for --embed-images
@@ -1429,7 +1453,6 @@ async function main() {
1429
1453
  config.archiveFormat = archiveFormat === 'gz' ? 'tar.gz' : archiveFormat;
1430
1454
  config.format = 'archive';
1431
1455
  }
1432
- // Capture mode
1433
1456
  await captureUrl(url, {
1434
1457
  format: config.format,
1435
1458
  output: config.output,
@@ -1457,7 +1480,6 @@ async function main() {
1457
1480
  capture: config.capture,
1458
1481
  });
1459
1482
  } else {
1460
- // No arguments - show error
1461
1483
  console.error('Error: Missing URL or --serve flag');
1462
1484
  console.error('Run with --help for usage information');
1463
1485
  process.exit(1);
@@ -5,5 +5,7 @@ services:
5
5
  dockerfile: Dockerfile
6
6
  ports:
7
7
  - '3000:3000'
8
+ extra_hosts:
9
+ - 'host.docker.internal:host-gateway'
8
10
  environment:
9
11
  - PORT=3000
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@link-assistant/web-capture",
3
- "version": "1.10.9",
3
+ "version": "1.10.11",
4
4
  "description": "CLI and microservice to render web pages as HTML, Markdown, or PNG",
5
5
  "main": "src/index.js",
6
6
  "bin": {
@@ -33,7 +33,7 @@
33
33
  },
34
34
  "dependencies": {
35
35
  "@kreuzberg/html-to-markdown-node": "^3.5.5",
36
- "archiver": "^7.0.1",
36
+ "archiver": "^8.0.0",
37
37
  "browser-commander": "^0.8.0",
38
38
  "cheerio": "^1.0.0",
39
39
  "docx": "^9.6.1",
package/src/archive.js CHANGED
@@ -15,7 +15,7 @@
15
15
  * documentFormat - 'markdown' (default) or 'html' - format of the main document
16
16
  */
17
17
 
18
- import archiver from 'archiver';
18
+ import { ZipArchive } from 'archiver';
19
19
  import fetch from 'node-fetch';
20
20
  import * as cheerio from 'cheerio';
21
21
  import { URL } from 'url';
@@ -88,7 +88,7 @@ export async function archiveHandler(req, res) {
88
88
  `attachment; filename="${hostname}-archive.zip"`
89
89
  );
90
90
 
91
- const archive = archiver('zip', { zlib: { level: 9 } });
91
+ const archive = new ZipArchive({ zlib: { level: 9 } });
92
92
  archive.pipe(res);
93
93
 
94
94
  // Build the image map for local downloads
@@ -251,7 +251,7 @@ function appendMarkdownAndImages(archive, markdown) {
251
251
  * @returns {Promise<Buffer>} the finalized ZIP archive bytes
252
252
  */
253
253
  export async function buildArchiveFromHtml(html, baseUrl) {
254
- const archive = archiver('zip', { zlib: { level: 9 } });
254
+ const archive = new ZipArchive({ zlib: { level: 9 } });
255
255
  const chunks = [];
256
256
  archive.on('data', (chunk) => chunks.push(chunk));
257
257
  const finished = new Promise((resolve, reject) => {
package/src/fetch.js CHANGED
@@ -1,5 +1,6 @@
1
1
  import fetch from 'node-fetch';
2
2
  import { convertGoogleDriveUrl } from './lib.js';
3
+ import { copyProxyResponseHeaders } from './proxy-headers.js';
3
4
 
4
5
  export async function fetchHandler(req, res) {
5
6
  const url = req.query.url;
@@ -10,21 +11,7 @@ export async function fetchHandler(req, res) {
10
11
  const response = await fetch(convertGoogleDriveUrl(url));
11
12
  // Copy status and headers
12
13
  res.status(response.status);
13
-
14
- // Set default content type if not present
15
- const contentType = response.headers.get('content-type') || 'text/plain';
16
- res.setHeader('Content-Type', contentType);
17
-
18
- // Copy other headers
19
- for (const [key, value] of response.headers.entries()) {
20
- if (
21
- key.toLowerCase() !== 'transfer-encoding' &&
22
- key.toLowerCase() !== 'content-encoding' &&
23
- key.toLowerCase() !== 'content-length'
24
- ) {
25
- res.setHeader(key, value);
26
- }
27
- }
14
+ copyProxyResponseHeaders(response.headers, res);
28
15
 
29
16
  // Get the response body as buffer and send it
30
17
  const buffer = await response.buffer();
package/src/gdocs.js CHANGED
@@ -42,6 +42,12 @@ const GDOCS_EXPORT_BASE = 'https://docs.google.com/document/d';
42
42
  const GDOCS_API_BASE = 'https://docs.googleapis.com/v1/documents';
43
43
  const DEFAULT_USER_AGENT =
44
44
  'Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/120.0.0.0 Safari/537.36';
45
+ const PUBLIC_EXPORT_HEADERS = {
46
+ 'User-Agent': DEFAULT_USER_AGENT,
47
+ 'Accept-Charset': 'utf-8',
48
+ 'Accept-Language': 'en-US,en;q=0.9',
49
+ 'Accept-Encoding': 'identity',
50
+ };
45
51
 
46
52
  /**
47
53
  * Supported Google Docs export formats.
@@ -165,11 +171,7 @@ export async function fetchGoogleDoc(url, options = {}) {
165
171
  hasApiToken: Boolean(apiToken),
166
172
  }));
167
173
 
168
- const headers = {
169
- 'User-Agent': DEFAULT_USER_AGENT,
170
- 'Accept-Charset': 'utf-8',
171
- 'Accept-Language': 'en-US,en;q=0.9',
172
- };
174
+ const headers = { ...PUBLIC_EXPORT_HEADERS };
173
175
 
174
176
  if (apiToken) {
175
177
  headers.Authorization = `Bearer ${apiToken}`;
package/src/github.js CHANGED
@@ -244,6 +244,7 @@ async function fetchGithubText(url, options = {}) {
244
244
  function githubHeaders(accept) {
245
245
  const headers = {
246
246
  Accept: accept,
247
+ 'Accept-Encoding': 'identity',
247
248
  'User-Agent': GITHUB_USER_AGENT,
248
249
  'X-GitHub-Api-Version': '2022-11-28',
249
250
  };
package/src/index.js CHANGED
@@ -13,6 +13,7 @@ import { animationHandler } from './animation.js';
13
13
  import { figuresHandler } from './figures.js';
14
14
  import { themedImageHandler } from './themed-image.js';
15
15
  import { searchHandler } from './search.js';
16
+ import { sharedDialogHandler } from './shared-dialog.js';
16
17
 
17
18
  const app = express();
18
19
  const port = process.env.PORT || 3000;
@@ -31,6 +32,7 @@ app.get('/animation', animationHandler);
31
32
  app.get('/figures', figuresHandler);
32
33
  app.get('/themed-image', themedImageHandler);
33
34
  app.get('/search', searchHandler);
35
+ app.get('/shared-dialog', sharedDialogHandler);
34
36
 
35
37
  // Start the server if this is the main module
36
38
  const isMainModule =
@@ -78,3 +80,13 @@ export {
78
80
  formatSearchAsMarkdown,
79
81
  SEARCH_PROVIDERS,
80
82
  } from './search.js';
83
+ export {
84
+ captureSharedDialog,
85
+ formatSharedDialogAsDemoMemory,
86
+ formatSharedDialogAsMarkdown,
87
+ formatSharedDialogAsMetaLanguage,
88
+ formatSharedDialogAsText,
89
+ formatSharedDialogResult,
90
+ parseSharedDialog,
91
+ sharedDialogHandler,
92
+ } from './shared-dialog.js';
package/src/lib.js CHANGED
@@ -15,6 +15,9 @@ import { retry } from './retry.js';
15
15
  const STACKPRINTER_RETRIES = 3;
16
16
  const STACKPRINTER_RETRY_BASE_DELAY_MS = 1000;
17
17
  const GOOGLE_DRIVE_FILE_ID_RE = /^[A-Za-z0-9_-]+$/;
18
+ const HTML_FETCH_HEADERS = {
19
+ 'accept-encoding': 'identity',
20
+ };
18
21
 
19
22
  export async function fetchHtml(url) {
20
23
  if (!url) {
@@ -30,7 +33,7 @@ export async function fetchHtml(url) {
30
33
  return await fetchStackPrinterHtml(stackPrinterUrl);
31
34
  }
32
35
 
33
- const response = await fetch(url);
36
+ const response = await fetchHtmlResponse(url);
34
37
  return response.text();
35
38
  }
36
39
 
@@ -194,7 +197,7 @@ function imageExtensionForContentType(contentType) {
194
197
  async function fetchStackPrinterHtml(url) {
195
198
  return await retry(
196
199
  async () => {
197
- const response = await fetch(url);
200
+ const response = await fetchHtmlResponse(url);
198
201
  if (!response.ok) {
199
202
  throw new Error(`StackPrinter returned HTTP ${response.status}`);
200
203
  }
@@ -212,6 +215,12 @@ async function fetchStackPrinterHtml(url) {
212
215
  );
213
216
  }
214
217
 
218
+ function fetchHtmlResponse(url) {
219
+ return fetch(url, {
220
+ headers: HTML_FETCH_HEADERS,
221
+ });
222
+ }
223
+
215
224
  function isStackPrinterTransientError(html) {
216
225
  return /Ooooops|Please try again later/i.test(html);
217
226
  }
package/src/markdown.js CHANGED
@@ -17,7 +17,7 @@ import { convertWithKreuzberg, isKreuzbergAvailable } from './kreuzberg.js';
17
17
  import { applyImageMode } from './extract-images.js';
18
18
  import { createBrowser, getBrowserEngine } from './browser.js';
19
19
  import { retry } from './retry.js';
20
- import archiver from 'archiver';
20
+ import { ZipArchive } from 'archiver';
21
21
  import fetch from 'node-fetch';
22
22
  import * as cheerio from 'cheerio';
23
23
 
@@ -300,7 +300,7 @@ async function sendTextPasteMarkdownArchive(res, url, markdown, rawText) {
300
300
  res.setHeader('Content-Type', 'application/zip');
301
301
  res.setHeader('Content-Disposition', `attachment; filename="${pasteId}.zip"`);
302
302
 
303
- const archive = archiver('zip', { zlib: { level: 9 } });
303
+ const archive = new ZipArchive({ zlib: { level: 9 } });
304
304
  archive.pipe(res);
305
305
  archive.append(
306
306
  [
@@ -0,0 +1,60 @@
1
+ const HOP_BY_HOP_HEADERS = new Set([
2
+ 'connection',
3
+ 'keep-alive',
4
+ 'proxy-authenticate',
5
+ 'proxy-authorization',
6
+ 'te',
7
+ 'trailer',
8
+ 'transfer-encoding',
9
+ 'upgrade',
10
+ ]);
11
+
12
+ const TRANSFORMED_BODY_HEADERS = new Set(['content-encoding']);
13
+
14
+ export function getProxyHeaderEntries(sourceHeaders, options = {}) {
15
+ const { preserveContentLength = false } = options;
16
+ const connectionTokens = getConnectionTokens(sourceHeaders);
17
+ const isEncoded = Boolean(sourceHeaders.get('content-encoding'));
18
+ const entries = [];
19
+
20
+ for (const [key, value] of sourceHeaders.entries()) {
21
+ const lowerKey = key.toLowerCase();
22
+ if (
23
+ HOP_BY_HOP_HEADERS.has(lowerKey) ||
24
+ TRANSFORMED_BODY_HEADERS.has(lowerKey) ||
25
+ connectionTokens.has(lowerKey)
26
+ ) {
27
+ continue;
28
+ }
29
+ if (
30
+ lowerKey === 'content-length' &&
31
+ (!preserveContentLength || isEncoded)
32
+ ) {
33
+ continue;
34
+ }
35
+ entries.push([key, value]);
36
+ }
37
+
38
+ return entries;
39
+ }
40
+
41
+ export function copyProxyResponseHeaders(sourceHeaders, res, options = {}) {
42
+ const contentType = sourceHeaders.get('content-type') || 'text/plain';
43
+ res.setHeader('Content-Type', contentType);
44
+
45
+ for (const [key, value] of getProxyHeaderEntries(sourceHeaders, options)) {
46
+ if (key.toLowerCase() !== 'content-type') {
47
+ res.setHeader(key, value);
48
+ }
49
+ }
50
+ }
51
+
52
+ function getConnectionTokens(sourceHeaders) {
53
+ const connection = sourceHeaders.get('connection') || '';
54
+ return new Set(
55
+ connection
56
+ .split(',')
57
+ .map((token) => token.trim().toLowerCase())
58
+ .filter(Boolean)
59
+ );
60
+ }
@@ -0,0 +1,25 @@
1
+ import fs from 'fs';
2
+ import path from 'path';
3
+
4
+ import {
5
+ captureSharedDialog,
6
+ formatSharedDialogResult,
7
+ } from './shared-dialog.js';
8
+
9
+ export async function runSharedDialogCli(sharedDialogUrl, options = {}) {
10
+ const format = (options.format || 'json').toLowerCase();
11
+ const capture = await captureSharedDialog({
12
+ url: sharedDialogUrl,
13
+ capture: options.capture,
14
+ engine: options.engine,
15
+ });
16
+ const body = formatSharedDialogResult(capture, format);
17
+
18
+ if (options.output && options.output !== '-') {
19
+ fs.mkdirSync(path.dirname(options.output), { recursive: true });
20
+ fs.writeFileSync(options.output, body, 'utf-8');
21
+ console.error(`Shared dialog (${format}) saved to: ${options.output}`);
22
+ } else {
23
+ process.stdout.write(body);
24
+ }
25
+ }