@link-assistant/web-capture 1.10.8 → 1.10.10
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 +12 -0
- package/README.md +16 -0
- package/bin/web-capture.js +50 -20
- package/docker-compose.yml +2 -0
- package/jest.config.mjs +1 -0
- package/package.json +1 -1
- package/src/fetch.js +2 -15
- package/src/gdocs.js +7 -5
- package/src/github.js +1 -0
- package/src/index.js +12 -0
- package/src/lib.js +11 -2
- package/src/proxy-headers.js +60 -0
- package/src/shared-dialog-cli.js +25 -0
- package/src/shared-dialog.js +950 -0
- package/src/stream.js +33 -26
- package/tests/ci/detect-code-changes.test.js +174 -0
- package/tests/ci/rust-dockerfile.test.js +54 -0
- package/tests/ci/workflow-policy.test.js +63 -0
- package/tests/e2e/docker.test.js +2 -3
- package/tests/e2e/process.test.js +95 -22
- package/tests/unit/cli.test.js +45 -1
- package/tests/unit/fetch-html.test.js +24 -0
- package/tests/unit/gdocs-export-headers.test.js +28 -0
- package/tests/unit/github.test.js +59 -0
- package/tests/unit/proxy-headers.test.js +100 -0
- package/tests/unit/shared-dialog.test.js +173 -0
- package/tests/unit/stream.test.js +85 -0
package/CHANGELOG.md
CHANGED
|
@@ -1,5 +1,17 @@
|
|
|
1
1
|
# @link-assistant/web-capture
|
|
2
2
|
|
|
3
|
+
## 1.10.10
|
|
4
|
+
|
|
5
|
+
### Patch Changes
|
|
6
|
+
|
|
7
|
+
- 65d7dcc: Add shared AI dialog capture for ChatGPT share links with structured transcript output and unsupported-provider diagnostics.
|
|
8
|
+
|
|
9
|
+
## 1.10.9
|
|
10
|
+
|
|
11
|
+
### Patch Changes
|
|
12
|
+
|
|
13
|
+
- Auto-release unreleased changes
|
|
14
|
+
|
|
3
15
|
## 1.10.8
|
|
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
|
package/bin/web-capture.js
CHANGED
|
@@ -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
|
-
|
|
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);
|
|
@@ -1370,14 +1382,8 @@ async function captureUrl(url, options) {
|
|
|
1370
1382
|
|
|
1371
1383
|
async function runSearch(query) {
|
|
1372
1384
|
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
1385
|
const format = (
|
|
1380
|
-
formatPassedExplicitly ? config.format : 'json'
|
|
1386
|
+
formatPassedExplicitly() ? config.format : 'json'
|
|
1381
1387
|
).toLowerCase();
|
|
1382
1388
|
const result = await search({
|
|
1383
1389
|
query,
|
|
@@ -1391,13 +1397,40 @@ async function runSearch(query) {
|
|
|
1391
1397
|
}
|
|
1392
1398
|
}
|
|
1393
1399
|
|
|
1400
|
+
async function runSharedDialog(sharedDialogUrl) {
|
|
1401
|
+
const { runSharedDialogCli } = await import('../src/shared-dialog-cli.js');
|
|
1402
|
+
await runSharedDialogCli(sharedDialogUrl, {
|
|
1403
|
+
capture: config.capture,
|
|
1404
|
+
engine: config.engine,
|
|
1405
|
+
format: formatPassedExplicitly() ? config.format : 'json',
|
|
1406
|
+
output: config.output,
|
|
1407
|
+
});
|
|
1408
|
+
}
|
|
1409
|
+
|
|
1410
|
+
function formatPassedExplicitly() {
|
|
1411
|
+
return process.argv.some(
|
|
1412
|
+
(arg) => arg === '-f' || arg === '--format' || arg.startsWith('--format=')
|
|
1413
|
+
);
|
|
1414
|
+
}
|
|
1415
|
+
|
|
1394
1416
|
async function main() {
|
|
1395
1417
|
const url = getUrlArgument();
|
|
1396
|
-
// The `query` key is only present when the `search <query>` command matched.
|
|
1397
1418
|
const isSearch = Object.prototype.hasOwnProperty.call(config, 'query');
|
|
1419
|
+
const isSharedDialog = Object.prototype.hasOwnProperty.call(
|
|
1420
|
+
config,
|
|
1421
|
+
'sharedDialogUrl'
|
|
1422
|
+
);
|
|
1398
1423
|
|
|
1399
|
-
if (
|
|
1400
|
-
|
|
1424
|
+
if (isSharedDialog) {
|
|
1425
|
+
const sharedDialogUrl =
|
|
1426
|
+
typeof config.sharedDialogUrl === 'string' ? config.sharedDialogUrl : '';
|
|
1427
|
+
if (!sharedDialogUrl.trim()) {
|
|
1428
|
+
console.error('Error: Missing shared-dialog URL');
|
|
1429
|
+
console.error('Usage: web-capture shared-dialog <url> [--format <fmt>]');
|
|
1430
|
+
process.exit(1);
|
|
1431
|
+
}
|
|
1432
|
+
await runSharedDialog(sharedDialogUrl);
|
|
1433
|
+
} else if (isSearch) {
|
|
1401
1434
|
const query = typeof config.query === 'string' ? config.query : '';
|
|
1402
1435
|
if (!query.trim()) {
|
|
1403
1436
|
console.error('Error: Missing search query');
|
|
@@ -1406,7 +1439,6 @@ async function main() {
|
|
|
1406
1439
|
}
|
|
1407
1440
|
await runSearch(query);
|
|
1408
1441
|
} else if (config.serve) {
|
|
1409
|
-
// Server mode
|
|
1410
1442
|
await startServer(config.port);
|
|
1411
1443
|
} else if (url) {
|
|
1412
1444
|
// --no-extract-images is an alias for --embed-images
|
|
@@ -1429,7 +1461,6 @@ async function main() {
|
|
|
1429
1461
|
config.archiveFormat = archiveFormat === 'gz' ? 'tar.gz' : archiveFormat;
|
|
1430
1462
|
config.format = 'archive';
|
|
1431
1463
|
}
|
|
1432
|
-
// Capture mode
|
|
1433
1464
|
await captureUrl(url, {
|
|
1434
1465
|
format: config.format,
|
|
1435
1466
|
output: config.output,
|
|
@@ -1457,7 +1488,6 @@ async function main() {
|
|
|
1457
1488
|
capture: config.capture,
|
|
1458
1489
|
});
|
|
1459
1490
|
} else {
|
|
1460
|
-
// No arguments - show error
|
|
1461
1491
|
console.error('Error: Missing URL or --serve flag');
|
|
1462
1492
|
console.error('Run with --help for usage information');
|
|
1463
1493
|
process.exit(1);
|
package/docker-compose.yml
CHANGED
package/jest.config.mjs
CHANGED
package/package.json
CHANGED
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
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
|
|
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
|
|
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
|
}
|
|
@@ -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
|
+
}
|