@hraness/kb 0.17.1
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/DISCLOSURE +43 -0
- package/LICENSE +21 -0
- package/README.md +545 -0
- package/dist/agent-context.js +35 -0
- package/dist/agent-guide-audit.js +18 -0
- package/dist/attachments.js +19 -0
- package/dist/authoring.js +33 -0
- package/dist/benchmark.js +19 -0
- package/dist/capture.js +49 -0
- package/dist/cli.js +3646 -0
- package/dist/clip/acquire.js +57 -0
- package/dist/clip/args.js +21 -0
- package/dist/clip/bounded-byte-buffer.js +9 -0
- package/dist/clip/browser-profiles.js +17 -0
- package/dist/clip/bundle-reader.js +11 -0
- package/dist/clip/cli.js +27 -0
- package/dist/clip/cookies.js +21 -0
- package/dist/clip/defuddle-worker.js +84 -0
- package/dist/clip/doctor.js +32 -0
- package/dist/clip/jobs.js +784 -0
- package/dist/clip/network-proxy.js +9 -0
- package/dist/clip/network.js +28 -0
- package/dist/clip/persist.js +30 -0
- package/dist/clip/refresh.js +11 -0
- package/dist/clip/terminal.js +11 -0
- package/dist/evaluation-builder.js +12238 -0
- package/dist/evaluation-kb.js +26 -0
- package/dist/evaluation.js +35 -0
- package/dist/git.js +39 -0
- package/dist/graph.js +39 -0
- package/dist/index-01jj6rbv.js +1103 -0
- package/dist/index-06c9ctr6.js +738 -0
- package/dist/index-0kavxzqj.js +2480 -0
- package/dist/index-1gwbassd.js +743 -0
- package/dist/index-1n418kb9.js +827 -0
- package/dist/index-1vrd1rmn.js +56 -0
- package/dist/index-1xxnjn0d.js +171 -0
- package/dist/index-3rm7cz6h.js +373 -0
- package/dist/index-3v2z4f0q.js +328 -0
- package/dist/index-48pz4jpc.js +392 -0
- package/dist/index-4j3tt0c3.js +234 -0
- package/dist/index-5n05se68.js +307 -0
- package/dist/index-5vwpzb5a.js +764 -0
- package/dist/index-6g2pv9d2.js +370 -0
- package/dist/index-7fzc32gf.js +712 -0
- package/dist/index-84x0vjjp.js +432 -0
- package/dist/index-adx6khj5.js +488 -0
- package/dist/index-b88v3vtm.js +805 -0
- package/dist/index-cv6fh7z5.js +462 -0
- package/dist/index-cxfrakt7.js +1067 -0
- package/dist/index-d13v9ckt.js +150 -0
- package/dist/index-dycjbhw2.js +1943 -0
- package/dist/index-dyqwejk5.js +531 -0
- package/dist/index-e5fbsywq.js +658 -0
- package/dist/index-f984hw45.js +927 -0
- package/dist/index-gh719d91.js +63 -0
- package/dist/index-hgve9rh2.js +2046 -0
- package/dist/index-hya40gb2.js +474 -0
- package/dist/index-j4zgmzjr.js +152 -0
- package/dist/index-jsmvyyvf.js +1166 -0
- package/dist/index-kh0pwvdd.js +1974 -0
- package/dist/index-mqx4nd6v.js +119 -0
- package/dist/index-mxxxytys.js +1254 -0
- package/dist/index-n5dd7r0v.js +711 -0
- package/dist/index-npg9z1a4.js +431 -0
- package/dist/index-pj501bh1.js +140 -0
- package/dist/index-qry4vhxk.js +1041 -0
- package/dist/index-s2gw5aw9.js +199 -0
- package/dist/index-vvdj6kca.js +70 -0
- package/dist/index-w2zc0vwa.js +571 -0
- package/dist/index-x3fthpsc.js +585 -0
- package/dist/index-zr53sf63.js +44 -0
- package/dist/index-zxdy5pby.js +2728 -0
- package/dist/index-zzhgcwyt.js +966 -0
- package/dist/index.js +453 -0
- package/dist/navigation.js +13 -0
- package/dist/pdf.js +85 -0
- package/dist/percolate.js +24 -0
- package/dist/portfolio.js +77 -0
- package/dist/query.js +29 -0
- package/dist/repository-memory.js +60 -0
- package/dist/sdk.js +36 -0
- package/dist/search-rules.js +34 -0
- package/dist/search.js +22 -0
- package/dist/semantic.js +39 -0
- package/dist/source-inbox.js +17 -0
- package/dist/untrusted-content.js +32 -0
- package/dist/url-intelligence.js +74 -0
- package/dist/workflow.js +23 -0
- package/dist/workflows/decision-context.js +20 -0
- package/dist/workflows/explain-change.js +8 -0
- package/dist/workflows/index.js +28 -0
- package/dist/workflows/plan-radar.js +10 -0
- package/package.json +409 -0
- package/skills/kb/AGENTS.md +19 -0
- package/skills/kb/SKILL.md +82 -0
- package/skills/kb/agents/openai.yaml +4 -0
- package/skills/kb/references/pdf-review.md +51 -0
- package/skills/kb/references/percolate.md +144 -0
- package/skills/kb/references/plan-structure.md +105 -0
- package/skills/kb/references/plan.md +115 -0
- package/skills/kb/references/query.md +231 -0
- package/skills/kb/references/refresh.md +133 -0
- package/skills/kb/references/save-pdf.md +125 -0
- package/skills/kb/references/save-url.md +178 -0
- package/skills/kb/references/url-authentication.md +75 -0
- package/skills/kb/references/url-platforms.md +25 -0
- package/src/agent-context.ts +1241 -0
- package/src/agent-guide-audit.ts +750 -0
- package/src/attachments.ts +723 -0
- package/src/authoring.ts +1725 -0
- package/src/benchmark.ts +362 -0
- package/src/capture.ts +68 -0
- package/src/cli.ts +3660 -0
- package/src/clip/acquire.ts +1273 -0
- package/src/clip/archive-today.ts +402 -0
- package/src/clip/args.ts +423 -0
- package/src/clip/assets.ts +262 -0
- package/src/clip/bounded-byte-buffer.ts +75 -0
- package/src/clip/browser-profiles.ts +372 -0
- package/src/clip/bundle-reader.ts +608 -0
- package/src/clip/capture.ts +1054 -0
- package/src/clip/cli.ts +181 -0
- package/src/clip/cookies.ts +512 -0
- package/src/clip/doctor.ts +1110 -0
- package/src/clip/extract.ts +868 -0
- package/src/clip/jobs.ts +959 -0
- package/src/clip/lib.ts +861 -0
- package/src/clip/media.ts +1474 -0
- package/src/clip/metadata-search-tool/Cargo.lock +2498 -0
- package/src/clip/metadata-search-tool/Cargo.toml +18 -0
- package/src/clip/metadata-search-tool/runner.ts +685 -0
- package/src/clip/metadata-search-tool/src/main.rs +389 -0
- package/src/clip/metadata-search.ts +687 -0
- package/src/clip/network-proxy.ts +627 -0
- package/src/clip/network.ts +950 -0
- package/src/clip/package-root.ts +54 -0
- package/src/clip/persist.ts +1472 -0
- package/src/clip/platforms.ts +1824 -0
- package/src/clip/refresh.ts +197 -0
- package/src/clip/structured.ts +432 -0
- package/src/clip/terminal.ts +197 -0
- package/src/clip/url-intelligence.ts +581 -0
- package/src/clip/url-metadata-backfill.ts +616 -0
- package/src/clip/url-metadata-cli.ts +251 -0
- package/src/clip/url-metadata.ts +1397 -0
- package/src/evaluation-analysis-v2.ts +1440 -0
- package/src/evaluation-builder.ts +2539 -0
- package/src/evaluation-corpus-authoring.ts +2052 -0
- package/src/evaluation-evidence.ts +1147 -0
- package/src/evaluation-execution-v2.ts +426 -0
- package/src/evaluation-implementation.ts +131 -0
- package/src/evaluation-kb-closure.ts +1430 -0
- package/src/evaluation-kb-runner-v2.ts +735 -0
- package/src/evaluation-kb-v2.ts +2567 -0
- package/src/evaluation-kb.ts +1018 -0
- package/src/evaluation-measurement-v2.ts +1462 -0
- package/src/evaluation-packing-v2.ts +452 -0
- package/src/evaluation-redaction.ts +28 -0
- package/src/evaluation-v2.ts +3857 -0
- package/src/evaluation.ts +1220 -0
- package/src/git.ts +1276 -0
- package/src/graph.ts +1487 -0
- package/src/index.ts +20 -0
- package/src/init.ts +123 -0
- package/src/navigation.ts +277 -0
- package/src/note-lock.ts +529 -0
- package/src/pdf/args.ts +202 -0
- package/src/pdf/capture.ts +298 -0
- package/src/pdf/cli.ts +159 -0
- package/src/pdf/extract.ts +303 -0
- package/src/pdf/layout.ts +618 -0
- package/src/pdf/markdown.ts +154 -0
- package/src/pdf/model.ts +234 -0
- package/src/pdf/ocr.ts +190 -0
- package/src/pdf/persist.ts +314 -0
- package/src/pdf/source.ts +135 -0
- package/src/pdf/tools.ts +146 -0
- package/src/pdf.ts +92 -0
- package/src/percolate.ts +877 -0
- package/src/portfolio-audit.ts +574 -0
- package/src/portfolio-identity.ts +162 -0
- package/src/portfolio-registry.ts +473 -0
- package/src/portfolio.ts +765 -0
- package/src/query.ts +646 -0
- package/src/repository-memory.ts +1150 -0
- package/src/sdk.ts +1452 -0
- package/src/search-rules.ts +712 -0
- package/src/search.ts +710 -0
- package/src/semantic-runtime.ts +1139 -0
- package/src/semantic.ts +2480 -0
- package/src/source-inbox.ts +232 -0
- package/src/untrusted-content.ts +401 -0
- package/src/url-intelligence.ts +104 -0
- package/src/vault.ts +636 -0
- package/src/workflow.ts +653 -0
- package/src/workflows/decision-context.ts +81 -0
- package/src/workflows/explain-change.ts +73 -0
- package/src/workflows/index.ts +3 -0
- package/src/workflows/plan-radar.ts +101 -0
|
@@ -0,0 +1,1054 @@
|
|
|
1
|
+
import {
|
|
2
|
+
chmodSync,
|
|
3
|
+
copyFileSync,
|
|
4
|
+
existsSync,
|
|
5
|
+
mkdirSync,
|
|
6
|
+
mkdtempSync,
|
|
7
|
+
readFileSync,
|
|
8
|
+
rmSync,
|
|
9
|
+
statSync,
|
|
10
|
+
} from "node:fs";
|
|
11
|
+
import { tmpdir } from "node:os";
|
|
12
|
+
import { join } from "node:path";
|
|
13
|
+
|
|
14
|
+
import {
|
|
15
|
+
acquireBrowser,
|
|
16
|
+
acquireCookieHttp,
|
|
17
|
+
acquireCookieRecords,
|
|
18
|
+
acquireFile,
|
|
19
|
+
acquireHttp,
|
|
20
|
+
assertSafePersistentProfile,
|
|
21
|
+
type AcquiredPage,
|
|
22
|
+
type AcquisitionMethod,
|
|
23
|
+
} from "./acquire.js";
|
|
24
|
+
import { captureUrl, type CaptureArguments, type CaptureScope } from "./args.js";
|
|
25
|
+
import { localizeAssets, sniffImage, type AssetRecord } from "./assets.js";
|
|
26
|
+
import { cloneBrowserProfile } from "./browser-profiles.js";
|
|
27
|
+
import {
|
|
28
|
+
canonicalizeUrl,
|
|
29
|
+
chooseBestExtraction,
|
|
30
|
+
countWords,
|
|
31
|
+
extractPage,
|
|
32
|
+
extractionShowsAccessControl,
|
|
33
|
+
scoreExtraction,
|
|
34
|
+
type ExtractedPage,
|
|
35
|
+
type Platform,
|
|
36
|
+
} from "./extract.js";
|
|
37
|
+
import {
|
|
38
|
+
buildClipMarkdown,
|
|
39
|
+
CONTENT_REWRITE_TRUNCATION_WARNING,
|
|
40
|
+
rewriteContentWithStatus,
|
|
41
|
+
slugify,
|
|
42
|
+
type Article,
|
|
43
|
+
} from "./lib.js";
|
|
44
|
+
import { filterCookies, renderCookieHeader } from "./cookies.js";
|
|
45
|
+
import {
|
|
46
|
+
captureMedia,
|
|
47
|
+
captureVideoContext,
|
|
48
|
+
type MediaCaptureResult,
|
|
49
|
+
type MediaMetadata,
|
|
50
|
+
type MediaRecord,
|
|
51
|
+
type VideoContextCaptureResult,
|
|
52
|
+
} from "./media.js";
|
|
53
|
+
import {
|
|
54
|
+
abortCaptureBundle,
|
|
55
|
+
beginCaptureBundle,
|
|
56
|
+
commitCaptureBundle,
|
|
57
|
+
redactSensitiveText,
|
|
58
|
+
redactSensitiveTextWithCount,
|
|
59
|
+
sanitizeArtifactUrl,
|
|
60
|
+
writeCaptureBundle,
|
|
61
|
+
type CaptureBundleTransaction,
|
|
62
|
+
type CaptureManifest,
|
|
63
|
+
type CaptureManifestAsset,
|
|
64
|
+
type CaptureManifestInput,
|
|
65
|
+
} from "./persist.js";
|
|
66
|
+
import { classifyPlatformUrl } from "./platforms.js";
|
|
67
|
+
import { acquirePublicStructured, type PublicStructuredCapture } from "./structured.js";
|
|
68
|
+
import {
|
|
69
|
+
acquireArchiveTodaySnapshot,
|
|
70
|
+
discoverArchiveTodaySnapshot,
|
|
71
|
+
type ArchiveTodayDiscovery,
|
|
72
|
+
} from "./archive-today.js";
|
|
73
|
+
import { FetchFailure } from "./network.js";
|
|
74
|
+
|
|
75
|
+
export type AttemptOutcome = "succeeded" | "failed" | "skipped";
|
|
76
|
+
|
|
77
|
+
export type CaptureAttempt = {
|
|
78
|
+
readonly method: string;
|
|
79
|
+
readonly outcome: AttemptOutcome;
|
|
80
|
+
readonly message: string;
|
|
81
|
+
};
|
|
82
|
+
|
|
83
|
+
export type CaptureOutcome = {
|
|
84
|
+
readonly status: ExtractedPage["status"];
|
|
85
|
+
readonly sourceUrl: string;
|
|
86
|
+
readonly canonicalUrl: string;
|
|
87
|
+
readonly platform: Platform;
|
|
88
|
+
readonly scope: Exclude<CaptureScope, "auto">;
|
|
89
|
+
readonly slug: string;
|
|
90
|
+
readonly acquisitionMethod: AcquisitionMethod;
|
|
91
|
+
readonly extractor: string;
|
|
92
|
+
readonly wordCount: number;
|
|
93
|
+
readonly capturedItems: number;
|
|
94
|
+
readonly expectedItems: number | null;
|
|
95
|
+
readonly outputDirectory: string | null;
|
|
96
|
+
readonly markdownPath: string | null;
|
|
97
|
+
readonly assetCount: number;
|
|
98
|
+
readonly warnings: readonly string[];
|
|
99
|
+
readonly attempts: readonly CaptureAttempt[];
|
|
100
|
+
readonly markdown: string;
|
|
101
|
+
readonly manifest: CaptureManifest | null;
|
|
102
|
+
};
|
|
103
|
+
|
|
104
|
+
export type CaptureDependencies = {
|
|
105
|
+
readonly acquireFile?: typeof acquireFile;
|
|
106
|
+
readonly acquireHttp?: typeof acquireHttp;
|
|
107
|
+
readonly acquireCookieHttp?: typeof acquireCookieHttp;
|
|
108
|
+
readonly acquireCookieRecords?: typeof acquireCookieRecords;
|
|
109
|
+
readonly acquireBrowser?: typeof acquireBrowser;
|
|
110
|
+
readonly acquirePublicStructured?: typeof acquirePublicStructured;
|
|
111
|
+
readonly discoverArchiveTodaySnapshot?: typeof discoverArchiveTodaySnapshot;
|
|
112
|
+
readonly acquireArchiveTodaySnapshot?: typeof acquireArchiveTodaySnapshot;
|
|
113
|
+
readonly extractPage?: typeof extractPage;
|
|
114
|
+
readonly localizeAssets?: typeof localizeAssets;
|
|
115
|
+
readonly captureMedia?: typeof captureMedia;
|
|
116
|
+
readonly captureVideoContext?: typeof captureVideoContext;
|
|
117
|
+
readonly now?: () => Date;
|
|
118
|
+
};
|
|
119
|
+
|
|
120
|
+
const browserFirstPlatforms = new Set<Platform>([
|
|
121
|
+
"x",
|
|
122
|
+
"instagram",
|
|
123
|
+
"linkedin",
|
|
124
|
+
"reddit",
|
|
125
|
+
"facebook",
|
|
126
|
+
"tiktok",
|
|
127
|
+
"threads",
|
|
128
|
+
"whatsapp",
|
|
129
|
+
"youtube",
|
|
130
|
+
]);
|
|
131
|
+
|
|
132
|
+
/** Resolve `auto` to the useful default for each content family. */
|
|
133
|
+
export function effectiveScope(platform: Platform, scope: CaptureScope): Exclude<CaptureScope, "auto"> {
|
|
134
|
+
if (scope !== "auto") return scope;
|
|
135
|
+
if (platform === "hacker-news" || platform === "reddit" || platform === "github" || platform === "discourse") {
|
|
136
|
+
return "comments";
|
|
137
|
+
}
|
|
138
|
+
if (platform === "x" || platform === "bluesky") return "thread";
|
|
139
|
+
return "page";
|
|
140
|
+
}
|
|
141
|
+
|
|
142
|
+
function stableContentId(url: URL): string | null {
|
|
143
|
+
const classified = classifyPlatformUrl(url.href);
|
|
144
|
+
if (classified === null) return null;
|
|
145
|
+
switch (classified.platform) {
|
|
146
|
+
case "x":
|
|
147
|
+
case "bluesky":
|
|
148
|
+
return classified.postId;
|
|
149
|
+
case "hacker-news":
|
|
150
|
+
return classified.itemId;
|
|
151
|
+
case "reddit":
|
|
152
|
+
return classified.commentId ?? classified.postId;
|
|
153
|
+
case "github":
|
|
154
|
+
return classified.contentId;
|
|
155
|
+
case "discourse":
|
|
156
|
+
return classified.topicId;
|
|
157
|
+
case "instagram":
|
|
158
|
+
case "linkedin":
|
|
159
|
+
case "facebook":
|
|
160
|
+
case "tiktok":
|
|
161
|
+
case "threads":
|
|
162
|
+
case "whatsapp":
|
|
163
|
+
case "youtube":
|
|
164
|
+
return classified.contentId;
|
|
165
|
+
case "substack":
|
|
166
|
+
case "generic":
|
|
167
|
+
return null;
|
|
168
|
+
}
|
|
169
|
+
}
|
|
170
|
+
|
|
171
|
+
/** Prefer readable slugs while retaining stable social IDs to prevent collisions. */
|
|
172
|
+
export function captureSlug(options: CaptureArguments, extraction: ExtractedPage): string {
|
|
173
|
+
if (options.slug !== undefined) return slugify(options.slug);
|
|
174
|
+
const fallback = extraction.canonicalUrl.pathname.split("/").filter(Boolean).at(-1)
|
|
175
|
+
?? extraction.canonicalUrl.hostname;
|
|
176
|
+
// Page titles are foreign content and can reflect credential-bearing URLs.
|
|
177
|
+
// Redact them before they become persistent directory or file names.
|
|
178
|
+
const base = slugify(redactSensitiveText(extraction.article.title ?? fallback));
|
|
179
|
+
const id = stableContentId(extraction.canonicalUrl);
|
|
180
|
+
if (id === null) return base;
|
|
181
|
+
const idSlug = slugify(id);
|
|
182
|
+
if (idSlug === "" || base.endsWith(`-${idSlug}`) || base === idSlug) return base;
|
|
183
|
+
const available = Math.max(1, 80 - [...idSlug].length - 1);
|
|
184
|
+
const shortened = [...base].slice(0, available).join("").replace(/-+$/g, "") || "post";
|
|
185
|
+
return `${shortened}-${idSlug}`;
|
|
186
|
+
}
|
|
187
|
+
|
|
188
|
+
function shouldUseBrowser(
|
|
189
|
+
options: CaptureArguments,
|
|
190
|
+
platform: Platform,
|
|
191
|
+
scope: Exclude<CaptureScope, "auto">,
|
|
192
|
+
directCandidates: readonly ExtractedPage[],
|
|
193
|
+
): boolean {
|
|
194
|
+
if (options.mode === "browser") return true;
|
|
195
|
+
if (options.mode !== "auto") return false;
|
|
196
|
+
if (options.browserProfile !== undefined || options.browserLive || options.cdp !== undefined) return true;
|
|
197
|
+
if (options.evidence === "screenshot" || options.evidence === "all") return true;
|
|
198
|
+
if (browserFirstPlatforms.has(platform)) return true;
|
|
199
|
+
if (scope !== "page" && platform !== "hacker-news" && platform !== "bluesky") return true;
|
|
200
|
+
const boundedStructured = directCandidates.some((candidate) =>
|
|
201
|
+
(candidate.acquisition.method === "hacker-news-api" || candidate.acquisition.method === "bluesky-api")
|
|
202
|
+
&& candidate.warnings.some((warning) => (
|
|
203
|
+
/configured (?:item|depth)|item (?:or depth )?limit|depth limit|capture stopped at \d+ items?/i.test(warning)
|
|
204
|
+
)));
|
|
205
|
+
if (boundedStructured) return false;
|
|
206
|
+
const best = chooseBestExtraction(directCandidates);
|
|
207
|
+
return best === null || best.status !== "complete" || best.wordCount < 60;
|
|
208
|
+
}
|
|
209
|
+
|
|
210
|
+
function safeAttemptMessage(value: unknown): string {
|
|
211
|
+
const message = value instanceof Error ? value.message : String(value);
|
|
212
|
+
return redactSensitiveText(message).replace(/[\r\n]+/g, " ").slice(0, 1_000);
|
|
213
|
+
}
|
|
214
|
+
|
|
215
|
+
function finalizedWarnings(values: readonly string[], markdownRedactions = 0): readonly string[] {
|
|
216
|
+
const sanitized = values.map((value) => safeAttemptMessage(value));
|
|
217
|
+
if (markdownRedactions > 0) {
|
|
218
|
+
sanitized.push(
|
|
219
|
+
`Redacted ${markdownRedactions} credential-shaped occurrence${markdownRedactions === 1 ? "" : "s"} from captured Markdown.`,
|
|
220
|
+
);
|
|
221
|
+
}
|
|
222
|
+
return [...new Set(sanitized)];
|
|
223
|
+
}
|
|
224
|
+
|
|
225
|
+
function statusAfterContentRewrite(
|
|
226
|
+
status: ExtractedPage["status"],
|
|
227
|
+
truncated: boolean,
|
|
228
|
+
): ExtractedPage["status"] {
|
|
229
|
+
return truncated && status === "complete" ? "partial" : status;
|
|
230
|
+
}
|
|
231
|
+
|
|
232
|
+
function chooseCaptureExtraction(
|
|
233
|
+
candidates: readonly ExtractedPage[],
|
|
234
|
+
structuredCapture: PublicStructuredCapture | null,
|
|
235
|
+
): ExtractedPage | null {
|
|
236
|
+
const structured = structuredCapture?.extraction;
|
|
237
|
+
if (
|
|
238
|
+
structured !== undefined
|
|
239
|
+
&& (structured.acquisition.method === "hacker-news-api" || structured.acquisition.method === "bluesky-api")
|
|
240
|
+
&& (structured.status === "complete" || structured.status === "partial")
|
|
241
|
+
) {
|
|
242
|
+
// These official adapters preserve bounded, typed reply structure. A large
|
|
243
|
+
// rendered shell can contain more words while proving zero conversation
|
|
244
|
+
// items, so volume must not displace the authoritative bounded result.
|
|
245
|
+
return structured;
|
|
246
|
+
}
|
|
247
|
+
return chooseBestExtraction(candidates);
|
|
248
|
+
}
|
|
249
|
+
|
|
250
|
+
function authoritativeStructuredExtraction(structuredCapture: PublicStructuredCapture | null): boolean {
|
|
251
|
+
const extraction = structuredCapture?.extraction;
|
|
252
|
+
return extraction !== undefined
|
|
253
|
+
&& (extraction.acquisition.method === "hacker-news-api" || extraction.acquisition.method === "bluesky-api")
|
|
254
|
+
&& (extraction.status === "complete" || extraction.status === "partial");
|
|
255
|
+
}
|
|
256
|
+
|
|
257
|
+
function shouldTryArchiveFallback(
|
|
258
|
+
options: CaptureArguments,
|
|
259
|
+
candidates: readonly ExtractedPage[],
|
|
260
|
+
structuredCapture: PublicStructuredCapture | null,
|
|
261
|
+
): boolean {
|
|
262
|
+
if ((options.mode !== "auto" && options.mode !== "http") || options.currentTab) return false;
|
|
263
|
+
if (authoritativeStructuredExtraction(structuredCapture)) return false;
|
|
264
|
+
const best = chooseBestExtraction(candidates);
|
|
265
|
+
return best === null
|
|
266
|
+
|| (best.status !== "complete" && best.status !== "partial")
|
|
267
|
+
|| (best.status === "partial" && best.wordCount < 60 && best.capturedItems === 0);
|
|
268
|
+
}
|
|
269
|
+
|
|
270
|
+
const archiveBypassHttpStatuses = new Set([401, 402, 403, 407, 423, 429, 451]);
|
|
271
|
+
|
|
272
|
+
function recordArchiveFallbackDenial(denials: Set<string>, extraction: ExtractedPage): void {
|
|
273
|
+
if (extractionShowsAccessControl(extraction)) denials.add("access-control response");
|
|
274
|
+
}
|
|
275
|
+
|
|
276
|
+
function recordArchiveFallbackErrorDenial(denials: Set<string>, error: unknown): void {
|
|
277
|
+
if (error instanceof FetchFailure && error.status !== null && archiveBypassHttpStatuses.has(error.status)) {
|
|
278
|
+
denials.add("access-control or rate-limit response");
|
|
279
|
+
}
|
|
280
|
+
}
|
|
281
|
+
|
|
282
|
+
function archiveDiscoveryAttempt(discovery: ArchiveTodayDiscovery): CaptureAttempt {
|
|
283
|
+
switch (discovery.status) {
|
|
284
|
+
case "found":
|
|
285
|
+
return { method: "archive-is-discovery", outcome: "succeeded", message: "found one exact existing snapshot" };
|
|
286
|
+
case "not-found":
|
|
287
|
+
return { method: "archive-is-discovery", outcome: "skipped", message: "no exact existing snapshot was found" };
|
|
288
|
+
case "throttled":
|
|
289
|
+
return { method: "archive-is-discovery", outcome: "failed", message: "provider throttled the read-only lookup" };
|
|
290
|
+
case "unavailable":
|
|
291
|
+
return { method: "archive-is-discovery", outcome: "failed", message: `provider lookup unavailable (${discovery.reason})` };
|
|
292
|
+
}
|
|
293
|
+
}
|
|
294
|
+
|
|
295
|
+
async function tryArchiveFallback(options: {
|
|
296
|
+
readonly sourceUrl: string;
|
|
297
|
+
readonly platform: Platform;
|
|
298
|
+
readonly scope: Exclude<CaptureScope, "auto">;
|
|
299
|
+
readonly captureOptions: CaptureArguments;
|
|
300
|
+
readonly extractor: typeof extractPage;
|
|
301
|
+
readonly discover: typeof discoverArchiveTodaySnapshot;
|
|
302
|
+
readonly acquire: typeof acquireArchiveTodaySnapshot;
|
|
303
|
+
readonly now: () => Date;
|
|
304
|
+
readonly candidates: ExtractedPage[];
|
|
305
|
+
readonly attempts: CaptureAttempt[];
|
|
306
|
+
}): Promise<void> {
|
|
307
|
+
let discovery: ArchiveTodayDiscovery;
|
|
308
|
+
try {
|
|
309
|
+
discovery = await options.discover(options.sourceUrl, {
|
|
310
|
+
now: options.now,
|
|
311
|
+
timeoutMs: options.captureOptions.timeoutMs,
|
|
312
|
+
maxBytes: options.captureOptions.maxHtmlBytes,
|
|
313
|
+
userAgent: options.captureOptions.userAgent,
|
|
314
|
+
});
|
|
315
|
+
} catch (error) {
|
|
316
|
+
options.attempts.push({ method: "archive-is-discovery", outcome: "failed", message: safeAttemptMessage(error) });
|
|
317
|
+
return;
|
|
318
|
+
}
|
|
319
|
+
options.attempts.push(archiveDiscoveryAttempt(discovery));
|
|
320
|
+
if (discovery.status !== "found") return;
|
|
321
|
+
try {
|
|
322
|
+
const acquisition = await options.acquire(options.sourceUrl, discovery.snapshot.url, {
|
|
323
|
+
now: options.now,
|
|
324
|
+
timeoutMs: options.captureOptions.timeoutMs,
|
|
325
|
+
maxBytes: options.captureOptions.maxHtmlBytes,
|
|
326
|
+
userAgent: options.captureOptions.userAgent,
|
|
327
|
+
});
|
|
328
|
+
const extracted = await options.extractor(acquisition, options.scope, options.captureOptions.timeoutMs);
|
|
329
|
+
if (extracted === null) {
|
|
330
|
+
options.attempts.push({ method: "archive-is", outcome: "failed", message: "snapshot yielded no extractable content" });
|
|
331
|
+
return;
|
|
332
|
+
}
|
|
333
|
+
const warning = `Captured an Archive.today snapshot from ${discovery.snapshot.capturedAt}; it may differ from the current source.`;
|
|
334
|
+
const archivedStatus = extracted.status === "complete" ? "partial" : extracted.status;
|
|
335
|
+
const archived: ExtractedPage = {
|
|
336
|
+
...extracted,
|
|
337
|
+
canonicalUrl: new URL(options.sourceUrl),
|
|
338
|
+
platform: options.platform,
|
|
339
|
+
status: archivedStatus,
|
|
340
|
+
score: scoreExtraction(
|
|
341
|
+
extracted.article,
|
|
342
|
+
archivedStatus,
|
|
343
|
+
extracted.wordCount,
|
|
344
|
+
extracted.capturedItems,
|
|
345
|
+
acquisition,
|
|
346
|
+
),
|
|
347
|
+
warnings: Object.freeze([...extracted.warnings, warning]),
|
|
348
|
+
acquisition,
|
|
349
|
+
};
|
|
350
|
+
options.candidates.push(archived);
|
|
351
|
+
options.attempts.push({
|
|
352
|
+
method: "archive-is",
|
|
353
|
+
outcome: "succeeded",
|
|
354
|
+
message: `${archived.status}; ${archived.wordCount} words; ${archived.capturedItems} items`,
|
|
355
|
+
});
|
|
356
|
+
} catch (error) {
|
|
357
|
+
options.attempts.push({ method: "archive-is", outcome: "failed", message: safeAttemptMessage(error) });
|
|
358
|
+
}
|
|
359
|
+
}
|
|
360
|
+
|
|
361
|
+
async function tryAcquisition(
|
|
362
|
+
method: string,
|
|
363
|
+
acquire: () => Promise<AcquiredPage>,
|
|
364
|
+
scope: CaptureScope,
|
|
365
|
+
timeoutMs: number,
|
|
366
|
+
extractor: typeof extractPage,
|
|
367
|
+
candidates: ExtractedPage[],
|
|
368
|
+
attempts: CaptureAttempt[],
|
|
369
|
+
archiveFallbackDenials: Set<string>,
|
|
370
|
+
): Promise<AcquiredPage | null> {
|
|
371
|
+
try {
|
|
372
|
+
const acquisition = await acquire();
|
|
373
|
+
const extracted = await extractor(acquisition, scope, timeoutMs);
|
|
374
|
+
if (extracted === null) {
|
|
375
|
+
attempts.push({ method, outcome: "failed", message: "acquisition yielded no extractable content" });
|
|
376
|
+
return acquisition;
|
|
377
|
+
}
|
|
378
|
+
candidates.push(extracted);
|
|
379
|
+
recordArchiveFallbackDenial(archiveFallbackDenials, extracted);
|
|
380
|
+
attempts.push({
|
|
381
|
+
method: acquisition.method,
|
|
382
|
+
outcome: "succeeded",
|
|
383
|
+
message: `${extracted.status}; ${extracted.wordCount} words; ${extracted.capturedItems} items`,
|
|
384
|
+
});
|
|
385
|
+
return acquisition;
|
|
386
|
+
} catch (error) {
|
|
387
|
+
recordArchiveFallbackErrorDenial(archiveFallbackDenials, error);
|
|
388
|
+
attempts.push({ method, outcome: "failed", message: safeAttemptMessage(error) });
|
|
389
|
+
return null;
|
|
390
|
+
}
|
|
391
|
+
}
|
|
392
|
+
|
|
393
|
+
function screenshotIntoBundle(
|
|
394
|
+
screenshotPath: string | null,
|
|
395
|
+
transaction: CaptureBundleTransaction,
|
|
396
|
+
maxBytes: number,
|
|
397
|
+
): string | null {
|
|
398
|
+
if (screenshotPath === null || !existsSync(screenshotPath)) return null;
|
|
399
|
+
const stats = statSync(screenshotPath);
|
|
400
|
+
if (!stats.isFile() || stats.size > maxBytes) return null;
|
|
401
|
+
const bytes = readFileSync(screenshotPath);
|
|
402
|
+
if (sniffImage(bytes)?.mimeType !== "image/png") return null;
|
|
403
|
+
const evidenceDirectory = join(transaction.stagingDirectory, "evidence");
|
|
404
|
+
mkdirSync(evidenceDirectory, { recursive: true, mode: 0o700 });
|
|
405
|
+
const destination = join(evidenceDirectory, "page.png");
|
|
406
|
+
copyFileSync(screenshotPath, destination);
|
|
407
|
+
chmodSync(destination, 0o600);
|
|
408
|
+
return "evidence/page.png";
|
|
409
|
+
}
|
|
410
|
+
|
|
411
|
+
const mediaRecordManifestAsset = (
|
|
412
|
+
record: MediaRecord,
|
|
413
|
+
sourceUrl: string,
|
|
414
|
+
): CaptureManifestAsset => ({
|
|
415
|
+
source: sourceUrl,
|
|
416
|
+
url: sourceUrl,
|
|
417
|
+
path: record.path,
|
|
418
|
+
mimeType: record.mimeType,
|
|
419
|
+
bytes: record.bytes,
|
|
420
|
+
sha256: record.sha256,
|
|
421
|
+
});
|
|
422
|
+
|
|
423
|
+
const mediaManifestAssets = (
|
|
424
|
+
result: MediaCaptureResult,
|
|
425
|
+
sourceUrl: string,
|
|
426
|
+
): readonly CaptureManifestAsset[] => result.records.map((record) =>
|
|
427
|
+
mediaRecordManifestAsset(record, sourceUrl));
|
|
428
|
+
|
|
429
|
+
function safeMediaPath(path: string): string | null {
|
|
430
|
+
if (path === "" || path.startsWith("/") || /[\\\0\r\n?#]/.test(path)) return null;
|
|
431
|
+
const pieces = path.split("/");
|
|
432
|
+
if (pieces.some((piece) => piece === "" || piece === "." || piece === "..")) return null;
|
|
433
|
+
return pieces.map((piece) => encodeURIComponent(piece)).join("/");
|
|
434
|
+
}
|
|
435
|
+
|
|
436
|
+
/** Add playable local media to the note; image-only captures never call this path. */
|
|
437
|
+
export function appendCapturedMedia(content: string, records: readonly MediaRecord[]): string {
|
|
438
|
+
const lines: string[] = [];
|
|
439
|
+
for (const record of records) {
|
|
440
|
+
const path = safeMediaPath(record.path);
|
|
441
|
+
if (path === null) continue;
|
|
442
|
+
if (record.mimeType.startsWith("video/")) {
|
|
443
|
+
lines.push(`<video controls preload="metadata" src="${path}"></video>`, `[Download video](${path})`);
|
|
444
|
+
} else if (record.mimeType.startsWith("audio/")) {
|
|
445
|
+
lines.push(`<audio controls preload="metadata" src="${path}"></audio>`, `[Download audio](${path})`);
|
|
446
|
+
} else lines.push(`[Download media](${path})`);
|
|
447
|
+
}
|
|
448
|
+
if (lines.length === 0) return content;
|
|
449
|
+
return `${content.trimEnd()}\n\n## Media\n\n${lines.join("\n\n")}\n`;
|
|
450
|
+
}
|
|
451
|
+
|
|
452
|
+
function escapedVideoMetadata(value: string): string {
|
|
453
|
+
return value
|
|
454
|
+
.replace(/\\/gu, "\\\\")
|
|
455
|
+
.replace(/([`*_[\]<>#])/gu, "\\$1");
|
|
456
|
+
}
|
|
457
|
+
|
|
458
|
+
function videoDuration(value: number): string {
|
|
459
|
+
const totalSeconds = Math.max(0, Math.floor(value));
|
|
460
|
+
const hours = Math.floor(totalSeconds / 3_600);
|
|
461
|
+
const minutes = Math.floor((totalSeconds % 3_600) / 60);
|
|
462
|
+
const seconds = totalSeconds % 60;
|
|
463
|
+
return hours > 0
|
|
464
|
+
? `${String(hours).padStart(2, "0")}:${String(minutes).padStart(2, "0")}:${String(seconds).padStart(2, "0")}`
|
|
465
|
+
: `${String(minutes).padStart(2, "0")}:${String(seconds).padStart(2, "0")}`;
|
|
466
|
+
}
|
|
467
|
+
|
|
468
|
+
/** Add durable YouTube metadata, a local thumbnail, and a bounded transcript to readable Markdown. */
|
|
469
|
+
export function appendVideoContext(content: string, context: VideoContextCaptureResult): string {
|
|
470
|
+
const metadata = context.metadata;
|
|
471
|
+
const details: string[] = [];
|
|
472
|
+
if (metadata?.title !== undefined) details.push(`- **Title:** ${escapedVideoMetadata(metadata.title)}`);
|
|
473
|
+
const channel = metadata?.channel ?? metadata?.uploader;
|
|
474
|
+
if (channel !== undefined) details.push(`- **Channel:** ${escapedVideoMetadata(channel)}`);
|
|
475
|
+
if (metadata?.durationSeconds !== undefined) {
|
|
476
|
+
details.push(`- **Duration:** ${videoDuration(metadata.durationSeconds)}`);
|
|
477
|
+
}
|
|
478
|
+
if (metadata?.id !== undefined) details.push(`- **Video ID:** ${escapedVideoMetadata(metadata.id)}`);
|
|
479
|
+
|
|
480
|
+
const thumbnailPath = context.thumbnail === null ? null : safeMediaPath(context.thumbnail.path);
|
|
481
|
+
const sections: string[] = [];
|
|
482
|
+
if (details.length > 0 || thumbnailPath !== null || metadata?.description !== undefined) {
|
|
483
|
+
const video: string[] = ["## Video"];
|
|
484
|
+
if (details.length > 0) video.push("", details.join("\n"));
|
|
485
|
+
if (thumbnailPath !== null) video.push("", ``);
|
|
486
|
+
if (metadata?.description !== undefined && metadata.description.trim() !== "") {
|
|
487
|
+
const description = metadata.description
|
|
488
|
+
.split(/\r?\n/gu)
|
|
489
|
+
.map(escapedVideoMetadata)
|
|
490
|
+
.join("\n");
|
|
491
|
+
video.push("", "### Description", "", description);
|
|
492
|
+
}
|
|
493
|
+
sections.push(video.join("\n"));
|
|
494
|
+
}
|
|
495
|
+
if (context.transcript !== null && context.transcript.markdown.trim() !== "") {
|
|
496
|
+
sections.push(`## Transcript\n\n${context.transcript.markdown.trimEnd()}`);
|
|
497
|
+
}
|
|
498
|
+
return sections.length === 0
|
|
499
|
+
? content
|
|
500
|
+
: `${content.trimEnd()}\n\n${sections.join("\n\n")}\n`;
|
|
501
|
+
}
|
|
502
|
+
|
|
503
|
+
function articleWithVideoMetadata(article: Article, metadata: MediaMetadata | null): Article {
|
|
504
|
+
if (metadata === null) return article;
|
|
505
|
+
const published = metadata.timestamp === undefined
|
|
506
|
+
? article.published
|
|
507
|
+
: (() => {
|
|
508
|
+
const date = new Date(metadata.timestamp * 1_000);
|
|
509
|
+
return Number.isFinite(date.getTime()) ? date.toISOString() : article.published;
|
|
510
|
+
})();
|
|
511
|
+
return {
|
|
512
|
+
content: article.content,
|
|
513
|
+
title: metadata.title ?? article.title,
|
|
514
|
+
author: metadata.channel ?? metadata.uploader ?? article.author,
|
|
515
|
+
published,
|
|
516
|
+
description: metadata.description ?? article.description,
|
|
517
|
+
};
|
|
518
|
+
}
|
|
519
|
+
|
|
520
|
+
function cookieMediaOptions(options: CaptureArguments): {
|
|
521
|
+
readonly cookieBrowser?: { readonly source: "chrome" | "arc" | "brave" | "chromium" | "edge" | "firefox" | "safari"; readonly profile?: string };
|
|
522
|
+
readonly cookiesFile?: string;
|
|
523
|
+
} {
|
|
524
|
+
const source = options.cookieSources[0] ?? (options.browserProfile === undefined ? undefined : "chrome");
|
|
525
|
+
const profile = options.cookieSources.length > 0
|
|
526
|
+
? options.cookieProfile
|
|
527
|
+
: selectedBrowserCookieProfile(options);
|
|
528
|
+
return {
|
|
529
|
+
...(source === undefined
|
|
530
|
+
? {}
|
|
531
|
+
: { cookieBrowser: { source, ...(profile === undefined ? {} : { profile }) } }),
|
|
532
|
+
...(options.cookiesFile === undefined ? {} : { cookiesFile: options.cookiesFile }),
|
|
533
|
+
};
|
|
534
|
+
}
|
|
535
|
+
|
|
536
|
+
/** Address the selected profile directory, not only its Chromium user-data parent. */
|
|
537
|
+
function selectedBrowserCookieProfile(options: CaptureArguments): string | undefined {
|
|
538
|
+
if (options.browserProfile === undefined) return undefined;
|
|
539
|
+
return options.browserProfileDirectory === undefined
|
|
540
|
+
? options.browserProfile
|
|
541
|
+
: join(options.browserProfile, options.browserProfileDirectory);
|
|
542
|
+
}
|
|
543
|
+
|
|
544
|
+
function assetCookieProvider(
|
|
545
|
+
options: CaptureArguments,
|
|
546
|
+
reader: typeof acquireCookieRecords,
|
|
547
|
+
authorizedUrl: URL,
|
|
548
|
+
): ((url: URL) => Promise<string | null>) | undefined {
|
|
549
|
+
const explicit = options.cookieSources.length > 0 || options.cookiesFile !== undefined;
|
|
550
|
+
if (!explicit && options.browserProfile === undefined) return undefined;
|
|
551
|
+
const effective = explicit
|
|
552
|
+
? options
|
|
553
|
+
: {
|
|
554
|
+
...options,
|
|
555
|
+
cookieSources: ["chrome"] as const,
|
|
556
|
+
cookieProfile: selectedBrowserCookieProfile(options),
|
|
557
|
+
};
|
|
558
|
+
let records: ReturnType<typeof reader> | undefined;
|
|
559
|
+
return (url) => {
|
|
560
|
+
if (url.origin !== authorizedUrl.origin) return Promise.resolve(null);
|
|
561
|
+
// Read once against the captured page, then narrow the retained records again
|
|
562
|
+
// for each asset path. Attribute-less headers can never be reinterpreted as Path=/.
|
|
563
|
+
records ??= reader(effective, authorizedUrl);
|
|
564
|
+
return records.then((result) => {
|
|
565
|
+
const header = renderCookieHeader(filterCookies(result.cookies, url).cookies);
|
|
566
|
+
return header === "" ? null : header;
|
|
567
|
+
});
|
|
568
|
+
};
|
|
569
|
+
}
|
|
570
|
+
|
|
571
|
+
/** Run path-backed profiles from a temporary copy shared by every browser lane in one capture. */
|
|
572
|
+
export function withBrowserProfileSnapshot(
|
|
573
|
+
options: CaptureArguments,
|
|
574
|
+
temporaryDirectory: string,
|
|
575
|
+
): CaptureArguments {
|
|
576
|
+
if (options.browserProfile === undefined || options.browserProfileOwnership === "owned") return options;
|
|
577
|
+
const source = assertSafePersistentProfile(options);
|
|
578
|
+
if (source === null) return options;
|
|
579
|
+
const cloned = cloneBrowserProfile(source, temporaryDirectory);
|
|
580
|
+
return {
|
|
581
|
+
...options,
|
|
582
|
+
browserProfile: cloned.userDataPath,
|
|
583
|
+
browserProfileOwnership: "owned",
|
|
584
|
+
...(cloned.profileDirectory === undefined
|
|
585
|
+
? {}
|
|
586
|
+
: { browserProfileDirectory: cloned.profileDirectory }),
|
|
587
|
+
};
|
|
588
|
+
}
|
|
589
|
+
|
|
590
|
+
/** Run the layered acquisition pipeline and optionally persist one atomic capture bundle. */
|
|
591
|
+
export async function runCapture(
|
|
592
|
+
rawOptions: CaptureArguments,
|
|
593
|
+
dependencies: CaptureDependencies = {},
|
|
594
|
+
): Promise<CaptureOutcome> {
|
|
595
|
+
if (rawOptions.stdout && (rawOptions.media !== "none" || rawOptions.evidence !== "none")) {
|
|
596
|
+
throw new Error("stdout capture cannot request persisted media or evidence");
|
|
597
|
+
}
|
|
598
|
+
const deps = {
|
|
599
|
+
acquireFile: dependencies.acquireFile ?? acquireFile,
|
|
600
|
+
acquireHttp: dependencies.acquireHttp ?? acquireHttp,
|
|
601
|
+
acquireCookieHttp: dependencies.acquireCookieHttp ?? acquireCookieHttp,
|
|
602
|
+
acquireCookieRecords: dependencies.acquireCookieRecords ?? acquireCookieRecords,
|
|
603
|
+
acquireBrowser: dependencies.acquireBrowser ?? acquireBrowser,
|
|
604
|
+
acquirePublicStructured: dependencies.acquirePublicStructured ?? acquirePublicStructured,
|
|
605
|
+
discoverArchiveTodaySnapshot: dependencies.discoverArchiveTodaySnapshot ?? discoverArchiveTodaySnapshot,
|
|
606
|
+
acquireArchiveTodaySnapshot: dependencies.acquireArchiveTodaySnapshot ?? acquireArchiveTodaySnapshot,
|
|
607
|
+
extractPage: dependencies.extractPage ?? extractPage,
|
|
608
|
+
localizeAssets: dependencies.localizeAssets ?? localizeAssets,
|
|
609
|
+
captureMedia: dependencies.captureMedia ?? captureMedia,
|
|
610
|
+
captureVideoContext: dependencies.captureVideoContext ?? captureVideoContext,
|
|
611
|
+
now: dependencies.now ?? (() => new Date()),
|
|
612
|
+
};
|
|
613
|
+
const browserTemporaryDirectory = mkdtempSync(join(tmpdir(), "hraness-kb-browser-"));
|
|
614
|
+
chmodSync(browserTemporaryDirectory, 0o700);
|
|
615
|
+
|
|
616
|
+
try {
|
|
617
|
+
const preparedOptions = withBrowserProfileSnapshot(rawOptions, browserTemporaryDirectory);
|
|
618
|
+
let currentBrowser: AcquiredPage | null = null;
|
|
619
|
+
let resolvedOptions = preparedOptions;
|
|
620
|
+
if (preparedOptions.currentTab) {
|
|
621
|
+
try {
|
|
622
|
+
const acquired = await deps.acquireBrowser(preparedOptions, browserTemporaryDirectory, false);
|
|
623
|
+
const sanitizedUrl = new URL(sanitizeArtifactUrl(acquired.finalUrl.href));
|
|
624
|
+
currentBrowser = sanitizedUrl.href === acquired.finalUrl.href
|
|
625
|
+
? acquired
|
|
626
|
+
: { ...acquired, finalUrl: sanitizedUrl };
|
|
627
|
+
} catch (error) {
|
|
628
|
+
throw new Error(`current browser acquisition failed: ${safeAttemptMessage(error)}`, { cause: error });
|
|
629
|
+
}
|
|
630
|
+
resolvedOptions = { ...preparedOptions, url: currentBrowser.finalUrl, mode: "browser" };
|
|
631
|
+
}
|
|
632
|
+
const requestedUrl = captureUrl(resolvedOptions);
|
|
633
|
+
const platform: Platform = classifyPlatformUrl(requestedUrl.href)?.platform ?? "generic";
|
|
634
|
+
const sourceUrl = canonicalizeUrl(requestedUrl, platform).href;
|
|
635
|
+
const scope = effectiveScope(platform, resolvedOptions.scope);
|
|
636
|
+
const options: CaptureArguments = { ...resolvedOptions, scope };
|
|
637
|
+
const candidates: ExtractedPage[] = [];
|
|
638
|
+
const attempts: CaptureAttempt[] = [];
|
|
639
|
+
const archiveFallbackDenials = new Set<string>();
|
|
640
|
+
const browserOperationalWarnings: string[] = [];
|
|
641
|
+
let structuredCapture: PublicStructuredCapture | null = null;
|
|
642
|
+
const browserScreenshots = new Map<AcquiredPage, string>();
|
|
643
|
+
const eagerBrowserCandidates: ExtractedPage[] = [];
|
|
644
|
+
const eagerBrowserAttempts: CaptureAttempt[] = [];
|
|
645
|
+
const eagerBrowserRequested = options.mode === "auto" && browserFirstPlatforms.has(platform);
|
|
646
|
+
if (eagerBrowserRequested && (options.browserLive || options.cdp !== undefined)) {
|
|
647
|
+
browserOperationalWarnings.push(
|
|
648
|
+
"An attached browser attempt may have navigated and scrolled the active tab even if that candidate was not selected.",
|
|
649
|
+
);
|
|
650
|
+
} else if (
|
|
651
|
+
eagerBrowserRequested
|
|
652
|
+
&& options.browserProfile !== undefined
|
|
653
|
+
&& options.browserProfileOwnership !== "owned"
|
|
654
|
+
) {
|
|
655
|
+
browserOperationalWarnings.push(
|
|
656
|
+
"A selected browser profile was exercised even if that candidate was not selected; a path-backed persistent profile may have been updated by page activity.",
|
|
657
|
+
);
|
|
658
|
+
}
|
|
659
|
+
const eagerBrowser = eagerBrowserRequested
|
|
660
|
+
? tryAcquisition(
|
|
661
|
+
options.browserLive ? "browser-live" : options.cdp === undefined ? "browser" : "browser-cdp",
|
|
662
|
+
() => deps.acquireBrowser(options, browserTemporaryDirectory, false),
|
|
663
|
+
scope,
|
|
664
|
+
options.timeoutMs,
|
|
665
|
+
deps.extractPage,
|
|
666
|
+
eagerBrowserCandidates,
|
|
667
|
+
eagerBrowserAttempts,
|
|
668
|
+
archiveFallbackDenials,
|
|
669
|
+
)
|
|
670
|
+
: null;
|
|
671
|
+
if (options.currentTab) {
|
|
672
|
+
if (currentBrowser === null) throw new Error("current browser acquisition did not produce a page");
|
|
673
|
+
const browser = await tryAcquisition(
|
|
674
|
+
options.browserLive ? "browser-live-current" : "browser-cdp-current",
|
|
675
|
+
() => Promise.resolve(currentBrowser),
|
|
676
|
+
scope,
|
|
677
|
+
options.timeoutMs,
|
|
678
|
+
deps.extractPage,
|
|
679
|
+
candidates,
|
|
680
|
+
attempts,
|
|
681
|
+
archiveFallbackDenials,
|
|
682
|
+
);
|
|
683
|
+
if (browser !== null) browserOperationalWarnings.push(...browser.warnings);
|
|
684
|
+
if (browser?.screenshotPath !== undefined) browserScreenshots.set(browser, browser.screenshotPath);
|
|
685
|
+
} else if (options.mode === "file") {
|
|
686
|
+
await tryAcquisition(
|
|
687
|
+
"file",
|
|
688
|
+
() => deps.acquireFile(options),
|
|
689
|
+
scope,
|
|
690
|
+
options.timeoutMs,
|
|
691
|
+
deps.extractPage,
|
|
692
|
+
candidates,
|
|
693
|
+
attempts,
|
|
694
|
+
archiveFallbackDenials,
|
|
695
|
+
);
|
|
696
|
+
} else {
|
|
697
|
+
if (options.mode === "auto" || options.mode === "http") {
|
|
698
|
+
try {
|
|
699
|
+
structuredCapture = await deps.acquirePublicStructured(options);
|
|
700
|
+
if (structuredCapture !== null) {
|
|
701
|
+
candidates.push(structuredCapture.extraction);
|
|
702
|
+
recordArchiveFallbackDenial(archiveFallbackDenials, structuredCapture.extraction);
|
|
703
|
+
attempts.push({
|
|
704
|
+
method: structuredCapture.extraction.acquisition.method,
|
|
705
|
+
outcome: "succeeded",
|
|
706
|
+
message: `${structuredCapture.extraction.status}; ${structuredCapture.extraction.capturedItems} items`,
|
|
707
|
+
});
|
|
708
|
+
} else {
|
|
709
|
+
attempts.push({ method: "public-api", outcome: "skipped", message: "no stable public structured adapter" });
|
|
710
|
+
}
|
|
711
|
+
} catch (error) {
|
|
712
|
+
recordArchiveFallbackErrorDenial(archiveFallbackDenials, error);
|
|
713
|
+
attempts.push({ method: "public-api", outcome: "failed", message: safeAttemptMessage(error) });
|
|
714
|
+
}
|
|
715
|
+
await tryAcquisition(
|
|
716
|
+
"http",
|
|
717
|
+
() => deps.acquireHttp(options),
|
|
718
|
+
scope,
|
|
719
|
+
options.timeoutMs,
|
|
720
|
+
deps.extractPage,
|
|
721
|
+
candidates,
|
|
722
|
+
attempts,
|
|
723
|
+
archiveFallbackDenials,
|
|
724
|
+
);
|
|
725
|
+
if (options.cookieSources.length > 0 || options.cookiesFile !== undefined) {
|
|
726
|
+
await tryAcquisition(
|
|
727
|
+
"cookie-http",
|
|
728
|
+
() => deps.acquireCookieHttp(options),
|
|
729
|
+
scope,
|
|
730
|
+
options.timeoutMs,
|
|
731
|
+
deps.extractPage,
|
|
732
|
+
candidates,
|
|
733
|
+
attempts,
|
|
734
|
+
archiveFallbackDenials,
|
|
735
|
+
);
|
|
736
|
+
}
|
|
737
|
+
}
|
|
738
|
+
|
|
739
|
+
if (eagerBrowser !== null) {
|
|
740
|
+
const browser = await eagerBrowser;
|
|
741
|
+
candidates.push(...eagerBrowserCandidates);
|
|
742
|
+
attempts.push(...eagerBrowserAttempts);
|
|
743
|
+
if (browser !== null) browserOperationalWarnings.push(...browser.warnings);
|
|
744
|
+
if (browser?.screenshotPath !== undefined) browserScreenshots.set(browser, browser.screenshotPath);
|
|
745
|
+
} else if (shouldUseBrowser(options, platform, scope, candidates)) {
|
|
746
|
+
if (options.browserLive || options.cdp !== undefined) {
|
|
747
|
+
browserOperationalWarnings.push(
|
|
748
|
+
"An attached browser attempt may have navigated and scrolled the active tab even if that candidate was not selected.",
|
|
749
|
+
);
|
|
750
|
+
} else if (
|
|
751
|
+
options.browserProfile !== undefined
|
|
752
|
+
&& options.browserProfileOwnership !== "owned"
|
|
753
|
+
) {
|
|
754
|
+
browserOperationalWarnings.push(
|
|
755
|
+
"A selected browser profile was exercised even if that candidate was not selected; a path-backed persistent profile may have been updated by page activity.",
|
|
756
|
+
);
|
|
757
|
+
}
|
|
758
|
+
const browser = await tryAcquisition(
|
|
759
|
+
options.browserLive ? "browser-live" : options.cdp === undefined ? "browser" : "browser-cdp",
|
|
760
|
+
() => deps.acquireBrowser(options, browserTemporaryDirectory, false),
|
|
761
|
+
scope,
|
|
762
|
+
options.timeoutMs,
|
|
763
|
+
deps.extractPage,
|
|
764
|
+
candidates,
|
|
765
|
+
attempts,
|
|
766
|
+
archiveFallbackDenials,
|
|
767
|
+
);
|
|
768
|
+
if (browser !== null) browserOperationalWarnings.push(...browser.warnings);
|
|
769
|
+
if (browser?.screenshotPath !== undefined) browserScreenshots.set(browser, browser.screenshotPath);
|
|
770
|
+
}
|
|
771
|
+
}
|
|
772
|
+
|
|
773
|
+
if (shouldTryArchiveFallback(options, candidates, structuredCapture)) {
|
|
774
|
+
if (archiveFallbackDenials.size > 0) {
|
|
775
|
+
attempts.push({
|
|
776
|
+
method: "archive-is-discovery",
|
|
777
|
+
outcome: "skipped",
|
|
778
|
+
message: `archive fallback disabled after ${[...archiveFallbackDenials].sort().join(" and ")}`,
|
|
779
|
+
});
|
|
780
|
+
} else {
|
|
781
|
+
await tryArchiveFallback({
|
|
782
|
+
sourceUrl,
|
|
783
|
+
platform,
|
|
784
|
+
scope,
|
|
785
|
+
captureOptions: options,
|
|
786
|
+
extractor: deps.extractPage,
|
|
787
|
+
discover: deps.discoverArchiveTodaySnapshot,
|
|
788
|
+
acquire: deps.acquireArchiveTodaySnapshot,
|
|
789
|
+
now: deps.now,
|
|
790
|
+
candidates,
|
|
791
|
+
attempts,
|
|
792
|
+
});
|
|
793
|
+
}
|
|
794
|
+
}
|
|
795
|
+
|
|
796
|
+
const best = chooseCaptureExtraction(candidates, structuredCapture);
|
|
797
|
+
if (best === null) {
|
|
798
|
+
const details = attempts.filter(({ outcome }) => outcome === "failed").map(({ method, message }) => `${method}: ${message}`);
|
|
799
|
+
throw new Error(`no acquisition produced usable content${details.length === 0 ? "" : ` (${details.join("; ")})`}`);
|
|
800
|
+
}
|
|
801
|
+
const slug = captureSlug(options, best);
|
|
802
|
+
if (slug === "") {
|
|
803
|
+
throw new Error(options.slug === undefined
|
|
804
|
+
? "could not derive a safe slug; pass one after the URL"
|
|
805
|
+
: `slug ${JSON.stringify(options.slug)} contains no letters or digits`);
|
|
806
|
+
}
|
|
807
|
+
|
|
808
|
+
const capturedAt = deps.now().toISOString();
|
|
809
|
+
const attemptWarnings = attempts
|
|
810
|
+
.filter(({ outcome }) => outcome === "failed")
|
|
811
|
+
.map(({ method, message }) => `${method} attempt failed: ${message}`);
|
|
812
|
+
const warnings = [...new Set([...best.warnings, ...browserOperationalWarnings, ...attemptWarnings])];
|
|
813
|
+
|
|
814
|
+
if (options.stdout) {
|
|
815
|
+
const rewritten = rewriteContentWithStatus(best.article.content, best.canonicalUrl, new Map());
|
|
816
|
+
const status = statusAfterContentRewrite(best.status, rewritten.truncated);
|
|
817
|
+
const redactedMarkdown = redactSensitiveTextWithCount(buildClipMarkdown(best.article, {
|
|
818
|
+
slug,
|
|
819
|
+
sourceHref: best.canonicalUrl.href,
|
|
820
|
+
clipped: capturedAt.slice(0, 10),
|
|
821
|
+
content: rewritten.content,
|
|
822
|
+
platform: best.platform,
|
|
823
|
+
captureStatus: status,
|
|
824
|
+
captureMethod: best.acquisition.method,
|
|
825
|
+
captureScope: scope,
|
|
826
|
+
}));
|
|
827
|
+
const wordCount = countWords(redactedMarkdown.text);
|
|
828
|
+
return {
|
|
829
|
+
status,
|
|
830
|
+
sourceUrl,
|
|
831
|
+
canonicalUrl: best.canonicalUrl.href,
|
|
832
|
+
platform: best.platform,
|
|
833
|
+
scope,
|
|
834
|
+
slug,
|
|
835
|
+
acquisitionMethod: best.acquisition.method,
|
|
836
|
+
extractor: best.extractor,
|
|
837
|
+
wordCount,
|
|
838
|
+
capturedItems: best.capturedItems,
|
|
839
|
+
expectedItems: best.expectedItems,
|
|
840
|
+
outputDirectory: null,
|
|
841
|
+
markdownPath: null,
|
|
842
|
+
assetCount: 0,
|
|
843
|
+
warnings: finalizedWarnings([
|
|
844
|
+
...warnings,
|
|
845
|
+
...(rewritten.truncated ? [CONTENT_REWRITE_TRUNCATION_WARNING] : []),
|
|
846
|
+
], redactedMarkdown.count),
|
|
847
|
+
attempts,
|
|
848
|
+
markdown: redactedMarkdown.text,
|
|
849
|
+
manifest: null,
|
|
850
|
+
};
|
|
851
|
+
}
|
|
852
|
+
|
|
853
|
+
const transaction = beginCaptureBundle({ outputRoot: options.outputBase, slug, force: options.force });
|
|
854
|
+
try {
|
|
855
|
+
const imageCookieProvider = assetCookieProvider(options, deps.acquireCookieRecords, best.canonicalUrl);
|
|
856
|
+
const localized = options.media === "none"
|
|
857
|
+
? {
|
|
858
|
+
...rewriteContentWithStatus(best.article.content, best.canonicalUrl, new Map()),
|
|
859
|
+
assets: [] as readonly AssetRecord[],
|
|
860
|
+
warnings: [] as readonly string[],
|
|
861
|
+
}
|
|
862
|
+
: await deps.localizeAssets(best.article.content, {
|
|
863
|
+
assetsDirectory: transaction.assetsDirectory,
|
|
864
|
+
baseUrl: best.canonicalUrl,
|
|
865
|
+
userAgent: options.userAgent,
|
|
866
|
+
timeoutMs: options.timeoutMs,
|
|
867
|
+
maxAssetBytes: options.maxAssetBytes,
|
|
868
|
+
maxTotalAssetBytes: options.maxTotalAssetBytes,
|
|
869
|
+
allowPrivateNetwork: options.allowPrivateNetwork,
|
|
870
|
+
...(imageCookieProvider === undefined ? {} : { cookieHeaderProvider: imageCookieProvider }),
|
|
871
|
+
});
|
|
872
|
+
const combinedWarnings = [
|
|
873
|
+
...warnings,
|
|
874
|
+
...localized.warnings,
|
|
875
|
+
...(localized.truncated ? [CONTENT_REWRITE_TRUNCATION_WARNING] : []),
|
|
876
|
+
];
|
|
877
|
+
const status = statusAfterContentRewrite(best.status, localized.truncated);
|
|
878
|
+
const manifestAssets: CaptureManifestAsset[] = [...localized.assets];
|
|
879
|
+
let videoContext: VideoContextCaptureResult | null = null;
|
|
880
|
+
let videoContextStatus: CaptureManifestInput["artifacts"]["videoContext"]["status"] = "not-requested";
|
|
881
|
+
const videoContextRequested = best.platform === "youtube" && options.media !== "none";
|
|
882
|
+
if (videoContextRequested) {
|
|
883
|
+
const usedBytes = manifestAssets.reduce((sum, asset) => sum + asset.bytes, 0);
|
|
884
|
+
const remainingBytes = options.maxTotalAssetBytes - usedBytes;
|
|
885
|
+
if (remainingBytes < 1) {
|
|
886
|
+
videoContextStatus = "partial";
|
|
887
|
+
combinedWarnings.push("Skipped YouTube thumbnail and transcript because the configured asset byte budget was exhausted.");
|
|
888
|
+
} else {
|
|
889
|
+
videoContext = await deps.captureVideoContext({
|
|
890
|
+
url: best.canonicalUrl,
|
|
891
|
+
outputDirectory: join(transaction.assetsDirectory, "video"),
|
|
892
|
+
relativePrefix: "assets/video",
|
|
893
|
+
timeoutMs: options.timeoutMs,
|
|
894
|
+
maxFileBytes: Math.min(options.maxAssetBytes, remainingBytes),
|
|
895
|
+
maxTotalBytes: remainingBytes,
|
|
896
|
+
allowPrivateNetwork: options.allowPrivateNetwork,
|
|
897
|
+
maxFiles: Math.max(3, Math.min(options.maxItems, 12)),
|
|
898
|
+
userAgent: options.userAgent,
|
|
899
|
+
...cookieMediaOptions(options),
|
|
900
|
+
});
|
|
901
|
+
videoContextStatus = videoContext.status === "captured" && videoContext.warnings.length > 0
|
|
902
|
+
? "partial"
|
|
903
|
+
: videoContext.status;
|
|
904
|
+
if (videoContext.thumbnail !== null) {
|
|
905
|
+
manifestAssets.push(mediaRecordManifestAsset(videoContext.thumbnail, best.canonicalUrl.href));
|
|
906
|
+
}
|
|
907
|
+
combinedWarnings.push(...videoContext.warnings);
|
|
908
|
+
}
|
|
909
|
+
}
|
|
910
|
+
let mediaRecords: readonly MediaRecord[] = [];
|
|
911
|
+
let mediaStatus: CaptureManifestInput["artifacts"]["media"]["status"] = "not-requested";
|
|
912
|
+
if (options.media === "all") {
|
|
913
|
+
const usedBytes = manifestAssets.reduce((sum, asset) => sum + asset.bytes, 0);
|
|
914
|
+
const remainingBytes = options.maxTotalAssetBytes - usedBytes;
|
|
915
|
+
if (remainingBytes < 1) {
|
|
916
|
+
mediaStatus = "partial";
|
|
917
|
+
combinedWarnings.push("Skipped full media because the configured asset byte budget was exhausted.");
|
|
918
|
+
} else {
|
|
919
|
+
const media = await deps.captureMedia({
|
|
920
|
+
url: best.canonicalUrl,
|
|
921
|
+
outputDirectory: join(transaction.assetsDirectory, "media"),
|
|
922
|
+
relativePrefix: "assets/media",
|
|
923
|
+
timeoutMs: options.timeoutMs,
|
|
924
|
+
maxFileBytes: Math.min(options.maxAssetBytes, remainingBytes),
|
|
925
|
+
maxTotalBytes: remainingBytes,
|
|
926
|
+
allowPrivateNetwork: options.allowPrivateNetwork,
|
|
927
|
+
maxFiles: Math.min(options.maxItems, 100),
|
|
928
|
+
userAgent: options.userAgent,
|
|
929
|
+
...cookieMediaOptions(options),
|
|
930
|
+
});
|
|
931
|
+
mediaStatus = media.status === "captured" && media.warnings.length > 0 ? "partial" : media.status;
|
|
932
|
+
mediaRecords = media.records;
|
|
933
|
+
manifestAssets.push(...mediaManifestAssets(media, best.canonicalUrl.href));
|
|
934
|
+
combinedWarnings.push(...media.warnings);
|
|
935
|
+
}
|
|
936
|
+
}
|
|
937
|
+
|
|
938
|
+
const requestedScreenshot = options.evidence === "screenshot" || options.evidence === "all";
|
|
939
|
+
const selectedScreenshot = browserScreenshots.get(best.acquisition) ?? null;
|
|
940
|
+
const screenshotPath = requestedScreenshot
|
|
941
|
+
? screenshotIntoBundle(selectedScreenshot, transaction, options.maxAssetBytes)
|
|
942
|
+
: null;
|
|
943
|
+
if (requestedScreenshot && screenshotPath === null) {
|
|
944
|
+
combinedWarnings.push(
|
|
945
|
+
browserScreenshots.size > 0
|
|
946
|
+
? "A screenshot was captured for a different acquisition candidate, so it was not attached to the selected content."
|
|
947
|
+
: "A screenshot was requested but no valid bounded PNG was captured.",
|
|
948
|
+
);
|
|
949
|
+
}
|
|
950
|
+
const article = articleWithVideoMetadata(best.article, videoContext?.metadata ?? null);
|
|
951
|
+
const contentWithVideo = videoContext === null
|
|
952
|
+
? localized.content
|
|
953
|
+
: appendVideoContext(localized.content, videoContext);
|
|
954
|
+
const redactedMarkdown = redactSensitiveTextWithCount(buildClipMarkdown(article, {
|
|
955
|
+
slug,
|
|
956
|
+
sourceHref: best.canonicalUrl.href,
|
|
957
|
+
clipped: capturedAt.slice(0, 10),
|
|
958
|
+
content: appendCapturedMedia(contentWithVideo, mediaRecords),
|
|
959
|
+
platform: best.platform,
|
|
960
|
+
captureStatus: status,
|
|
961
|
+
captureMethod: best.acquisition.method,
|
|
962
|
+
captureScope: scope,
|
|
963
|
+
}));
|
|
964
|
+
const wordCount = countWords(redactedMarkdown.text);
|
|
965
|
+
const finalWarnings = finalizedWarnings(combinedWarnings, redactedMarkdown.count);
|
|
966
|
+
const includeSource = options.evidence === "source" || options.evidence === "all";
|
|
967
|
+
const manifestInput: CaptureManifestInput = {
|
|
968
|
+
sourceUrl,
|
|
969
|
+
canonicalUrl: best.canonicalUrl.href,
|
|
970
|
+
capturedAt,
|
|
971
|
+
platform: best.platform,
|
|
972
|
+
status,
|
|
973
|
+
scope,
|
|
974
|
+
acquisition: {
|
|
975
|
+
method: best.acquisition.method,
|
|
976
|
+
finalUrl: best.acquisition.method === "archive-is"
|
|
977
|
+
? sanitizeArtifactUrl(best.acquisition.finalUrl.href)
|
|
978
|
+
: canonicalizeUrl(best.acquisition.finalUrl, best.platform).href,
|
|
979
|
+
contentType: best.acquisition.contentType,
|
|
980
|
+
},
|
|
981
|
+
extraction: {
|
|
982
|
+
extractor: best.extractor,
|
|
983
|
+
score: best.score,
|
|
984
|
+
wordCount,
|
|
985
|
+
capturedItems: best.capturedItems,
|
|
986
|
+
expectedItems: best.expectedItems,
|
|
987
|
+
},
|
|
988
|
+
attempts,
|
|
989
|
+
assets: manifestAssets,
|
|
990
|
+
artifacts: {
|
|
991
|
+
images: {
|
|
992
|
+
requested: options.media !== "none",
|
|
993
|
+
status: options.media === "none"
|
|
994
|
+
? "not-requested"
|
|
995
|
+
: localized.truncated || localized.warnings.length > 0 ? "partial" : "captured",
|
|
996
|
+
files: localized.assets.length,
|
|
997
|
+
},
|
|
998
|
+
media: {
|
|
999
|
+
requested: options.media === "all",
|
|
1000
|
+
status: mediaStatus,
|
|
1001
|
+
files: mediaRecords.length,
|
|
1002
|
+
},
|
|
1003
|
+
videoContext: {
|
|
1004
|
+
requested: videoContextRequested,
|
|
1005
|
+
status: videoContextStatus,
|
|
1006
|
+
thumbnailPath: videoContext?.thumbnail?.path ?? null,
|
|
1007
|
+
transcriptLanguage: videoContext?.transcript?.language ?? null,
|
|
1008
|
+
transcriptCueCount: videoContext?.transcript?.cueCount ?? 0,
|
|
1009
|
+
transcriptTruncated: videoContext?.transcript?.truncated ?? false,
|
|
1010
|
+
metadata: videoContext?.metadata ?? null,
|
|
1011
|
+
},
|
|
1012
|
+
},
|
|
1013
|
+
evidence: {
|
|
1014
|
+
requested: options.evidence,
|
|
1015
|
+
screenshotPath,
|
|
1016
|
+
screenshotStatus: requestedScreenshot ? screenshotPath === null ? "unavailable" : "captured" : "not-requested",
|
|
1017
|
+
sourceHtmlStatus: includeSource ? "captured" : "not-requested",
|
|
1018
|
+
},
|
|
1019
|
+
warnings: finalWarnings,
|
|
1020
|
+
};
|
|
1021
|
+
const manifest = writeCaptureBundle(transaction, {
|
|
1022
|
+
markdown: redactedMarkdown.text,
|
|
1023
|
+
manifest: manifestInput,
|
|
1024
|
+
...(includeSource ? { sourceHtml: best.acquisition.sourceEvidence ?? best.acquisition.body } : {}),
|
|
1025
|
+
});
|
|
1026
|
+
const outputDirectory = commitCaptureBundle(transaction);
|
|
1027
|
+
return {
|
|
1028
|
+
status,
|
|
1029
|
+
sourceUrl,
|
|
1030
|
+
canonicalUrl: best.canonicalUrl.href,
|
|
1031
|
+
platform: best.platform,
|
|
1032
|
+
scope,
|
|
1033
|
+
slug,
|
|
1034
|
+
acquisitionMethod: best.acquisition.method,
|
|
1035
|
+
extractor: best.extractor,
|
|
1036
|
+
wordCount,
|
|
1037
|
+
capturedItems: best.capturedItems,
|
|
1038
|
+
expectedItems: best.expectedItems,
|
|
1039
|
+
outputDirectory,
|
|
1040
|
+
markdownPath: join(outputDirectory, `${slug}.md`),
|
|
1041
|
+
assetCount: manifestAssets.length,
|
|
1042
|
+
warnings: finalWarnings,
|
|
1043
|
+
attempts,
|
|
1044
|
+
markdown: redactedMarkdown.text,
|
|
1045
|
+
manifest,
|
|
1046
|
+
};
|
|
1047
|
+
} catch (error) {
|
|
1048
|
+
abortCaptureBundle(transaction);
|
|
1049
|
+
throw error;
|
|
1050
|
+
}
|
|
1051
|
+
} finally {
|
|
1052
|
+
rmSync(browserTemporaryDirectory, { recursive: true, force: true });
|
|
1053
|
+
}
|
|
1054
|
+
}
|