@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 +12 -0
- package/README.md +16 -0
- package/bin/web-capture.js +57 -35
- package/docker-compose.yml +2 -0
- package/package.json +2 -2
- package/src/archive.js +3 -3
- 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/markdown.js +2 -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/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/package-runtime-dependencies.test.js +26 -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.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
|
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);
|
|
@@ -537,15 +549,14 @@ async function writeGoogleDocsArchive({
|
|
|
537
549
|
archiveFormat,
|
|
538
550
|
prettyHtml,
|
|
539
551
|
}) {
|
|
540
|
-
const {
|
|
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
|
|
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 {
|
|
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 =
|
|
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
|
|
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 {
|
|
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 =
|
|
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 (
|
|
1400
|
-
|
|
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);
|
package/docker-compose.yml
CHANGED
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@link-assistant/web-capture",
|
|
3
|
-
"version": "1.10.
|
|
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": "^
|
|
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
|
|
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 =
|
|
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 =
|
|
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
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
|
}
|
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
|
|
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 =
|
|
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
|
+
}
|