@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.
Files changed (200) hide show
  1. package/DISCLOSURE +43 -0
  2. package/LICENSE +21 -0
  3. package/README.md +545 -0
  4. package/dist/agent-context.js +35 -0
  5. package/dist/agent-guide-audit.js +18 -0
  6. package/dist/attachments.js +19 -0
  7. package/dist/authoring.js +33 -0
  8. package/dist/benchmark.js +19 -0
  9. package/dist/capture.js +49 -0
  10. package/dist/cli.js +3646 -0
  11. package/dist/clip/acquire.js +57 -0
  12. package/dist/clip/args.js +21 -0
  13. package/dist/clip/bounded-byte-buffer.js +9 -0
  14. package/dist/clip/browser-profiles.js +17 -0
  15. package/dist/clip/bundle-reader.js +11 -0
  16. package/dist/clip/cli.js +27 -0
  17. package/dist/clip/cookies.js +21 -0
  18. package/dist/clip/defuddle-worker.js +84 -0
  19. package/dist/clip/doctor.js +32 -0
  20. package/dist/clip/jobs.js +784 -0
  21. package/dist/clip/network-proxy.js +9 -0
  22. package/dist/clip/network.js +28 -0
  23. package/dist/clip/persist.js +30 -0
  24. package/dist/clip/refresh.js +11 -0
  25. package/dist/clip/terminal.js +11 -0
  26. package/dist/evaluation-builder.js +12238 -0
  27. package/dist/evaluation-kb.js +26 -0
  28. package/dist/evaluation.js +35 -0
  29. package/dist/git.js +39 -0
  30. package/dist/graph.js +39 -0
  31. package/dist/index-01jj6rbv.js +1103 -0
  32. package/dist/index-06c9ctr6.js +738 -0
  33. package/dist/index-0kavxzqj.js +2480 -0
  34. package/dist/index-1gwbassd.js +743 -0
  35. package/dist/index-1n418kb9.js +827 -0
  36. package/dist/index-1vrd1rmn.js +56 -0
  37. package/dist/index-1xxnjn0d.js +171 -0
  38. package/dist/index-3rm7cz6h.js +373 -0
  39. package/dist/index-3v2z4f0q.js +328 -0
  40. package/dist/index-48pz4jpc.js +392 -0
  41. package/dist/index-4j3tt0c3.js +234 -0
  42. package/dist/index-5n05se68.js +307 -0
  43. package/dist/index-5vwpzb5a.js +764 -0
  44. package/dist/index-6g2pv9d2.js +370 -0
  45. package/dist/index-7fzc32gf.js +712 -0
  46. package/dist/index-84x0vjjp.js +432 -0
  47. package/dist/index-adx6khj5.js +488 -0
  48. package/dist/index-b88v3vtm.js +805 -0
  49. package/dist/index-cv6fh7z5.js +462 -0
  50. package/dist/index-cxfrakt7.js +1067 -0
  51. package/dist/index-d13v9ckt.js +150 -0
  52. package/dist/index-dycjbhw2.js +1943 -0
  53. package/dist/index-dyqwejk5.js +531 -0
  54. package/dist/index-e5fbsywq.js +658 -0
  55. package/dist/index-f984hw45.js +927 -0
  56. package/dist/index-gh719d91.js +63 -0
  57. package/dist/index-hgve9rh2.js +2046 -0
  58. package/dist/index-hya40gb2.js +474 -0
  59. package/dist/index-j4zgmzjr.js +152 -0
  60. package/dist/index-jsmvyyvf.js +1166 -0
  61. package/dist/index-kh0pwvdd.js +1974 -0
  62. package/dist/index-mqx4nd6v.js +119 -0
  63. package/dist/index-mxxxytys.js +1254 -0
  64. package/dist/index-n5dd7r0v.js +711 -0
  65. package/dist/index-npg9z1a4.js +431 -0
  66. package/dist/index-pj501bh1.js +140 -0
  67. package/dist/index-qry4vhxk.js +1041 -0
  68. package/dist/index-s2gw5aw9.js +199 -0
  69. package/dist/index-vvdj6kca.js +70 -0
  70. package/dist/index-w2zc0vwa.js +571 -0
  71. package/dist/index-x3fthpsc.js +585 -0
  72. package/dist/index-zr53sf63.js +44 -0
  73. package/dist/index-zxdy5pby.js +2728 -0
  74. package/dist/index-zzhgcwyt.js +966 -0
  75. package/dist/index.js +453 -0
  76. package/dist/navigation.js +13 -0
  77. package/dist/pdf.js +85 -0
  78. package/dist/percolate.js +24 -0
  79. package/dist/portfolio.js +77 -0
  80. package/dist/query.js +29 -0
  81. package/dist/repository-memory.js +60 -0
  82. package/dist/sdk.js +36 -0
  83. package/dist/search-rules.js +34 -0
  84. package/dist/search.js +22 -0
  85. package/dist/semantic.js +39 -0
  86. package/dist/source-inbox.js +17 -0
  87. package/dist/untrusted-content.js +32 -0
  88. package/dist/url-intelligence.js +74 -0
  89. package/dist/workflow.js +23 -0
  90. package/dist/workflows/decision-context.js +20 -0
  91. package/dist/workflows/explain-change.js +8 -0
  92. package/dist/workflows/index.js +28 -0
  93. package/dist/workflows/plan-radar.js +10 -0
  94. package/package.json +409 -0
  95. package/skills/kb/AGENTS.md +19 -0
  96. package/skills/kb/SKILL.md +82 -0
  97. package/skills/kb/agents/openai.yaml +4 -0
  98. package/skills/kb/references/pdf-review.md +51 -0
  99. package/skills/kb/references/percolate.md +144 -0
  100. package/skills/kb/references/plan-structure.md +105 -0
  101. package/skills/kb/references/plan.md +115 -0
  102. package/skills/kb/references/query.md +231 -0
  103. package/skills/kb/references/refresh.md +133 -0
  104. package/skills/kb/references/save-pdf.md +125 -0
  105. package/skills/kb/references/save-url.md +178 -0
  106. package/skills/kb/references/url-authentication.md +75 -0
  107. package/skills/kb/references/url-platforms.md +25 -0
  108. package/src/agent-context.ts +1241 -0
  109. package/src/agent-guide-audit.ts +750 -0
  110. package/src/attachments.ts +723 -0
  111. package/src/authoring.ts +1725 -0
  112. package/src/benchmark.ts +362 -0
  113. package/src/capture.ts +68 -0
  114. package/src/cli.ts +3660 -0
  115. package/src/clip/acquire.ts +1273 -0
  116. package/src/clip/archive-today.ts +402 -0
  117. package/src/clip/args.ts +423 -0
  118. package/src/clip/assets.ts +262 -0
  119. package/src/clip/bounded-byte-buffer.ts +75 -0
  120. package/src/clip/browser-profiles.ts +372 -0
  121. package/src/clip/bundle-reader.ts +608 -0
  122. package/src/clip/capture.ts +1054 -0
  123. package/src/clip/cli.ts +181 -0
  124. package/src/clip/cookies.ts +512 -0
  125. package/src/clip/doctor.ts +1110 -0
  126. package/src/clip/extract.ts +868 -0
  127. package/src/clip/jobs.ts +959 -0
  128. package/src/clip/lib.ts +861 -0
  129. package/src/clip/media.ts +1474 -0
  130. package/src/clip/metadata-search-tool/Cargo.lock +2498 -0
  131. package/src/clip/metadata-search-tool/Cargo.toml +18 -0
  132. package/src/clip/metadata-search-tool/runner.ts +685 -0
  133. package/src/clip/metadata-search-tool/src/main.rs +389 -0
  134. package/src/clip/metadata-search.ts +687 -0
  135. package/src/clip/network-proxy.ts +627 -0
  136. package/src/clip/network.ts +950 -0
  137. package/src/clip/package-root.ts +54 -0
  138. package/src/clip/persist.ts +1472 -0
  139. package/src/clip/platforms.ts +1824 -0
  140. package/src/clip/refresh.ts +197 -0
  141. package/src/clip/structured.ts +432 -0
  142. package/src/clip/terminal.ts +197 -0
  143. package/src/clip/url-intelligence.ts +581 -0
  144. package/src/clip/url-metadata-backfill.ts +616 -0
  145. package/src/clip/url-metadata-cli.ts +251 -0
  146. package/src/clip/url-metadata.ts +1397 -0
  147. package/src/evaluation-analysis-v2.ts +1440 -0
  148. package/src/evaluation-builder.ts +2539 -0
  149. package/src/evaluation-corpus-authoring.ts +2052 -0
  150. package/src/evaluation-evidence.ts +1147 -0
  151. package/src/evaluation-execution-v2.ts +426 -0
  152. package/src/evaluation-implementation.ts +131 -0
  153. package/src/evaluation-kb-closure.ts +1430 -0
  154. package/src/evaluation-kb-runner-v2.ts +735 -0
  155. package/src/evaluation-kb-v2.ts +2567 -0
  156. package/src/evaluation-kb.ts +1018 -0
  157. package/src/evaluation-measurement-v2.ts +1462 -0
  158. package/src/evaluation-packing-v2.ts +452 -0
  159. package/src/evaluation-redaction.ts +28 -0
  160. package/src/evaluation-v2.ts +3857 -0
  161. package/src/evaluation.ts +1220 -0
  162. package/src/git.ts +1276 -0
  163. package/src/graph.ts +1487 -0
  164. package/src/index.ts +20 -0
  165. package/src/init.ts +123 -0
  166. package/src/navigation.ts +277 -0
  167. package/src/note-lock.ts +529 -0
  168. package/src/pdf/args.ts +202 -0
  169. package/src/pdf/capture.ts +298 -0
  170. package/src/pdf/cli.ts +159 -0
  171. package/src/pdf/extract.ts +303 -0
  172. package/src/pdf/layout.ts +618 -0
  173. package/src/pdf/markdown.ts +154 -0
  174. package/src/pdf/model.ts +234 -0
  175. package/src/pdf/ocr.ts +190 -0
  176. package/src/pdf/persist.ts +314 -0
  177. package/src/pdf/source.ts +135 -0
  178. package/src/pdf/tools.ts +146 -0
  179. package/src/pdf.ts +92 -0
  180. package/src/percolate.ts +877 -0
  181. package/src/portfolio-audit.ts +574 -0
  182. package/src/portfolio-identity.ts +162 -0
  183. package/src/portfolio-registry.ts +473 -0
  184. package/src/portfolio.ts +765 -0
  185. package/src/query.ts +646 -0
  186. package/src/repository-memory.ts +1150 -0
  187. package/src/sdk.ts +1452 -0
  188. package/src/search-rules.ts +712 -0
  189. package/src/search.ts +710 -0
  190. package/src/semantic-runtime.ts +1139 -0
  191. package/src/semantic.ts +2480 -0
  192. package/src/source-inbox.ts +232 -0
  193. package/src/untrusted-content.ts +401 -0
  194. package/src/url-intelligence.ts +104 -0
  195. package/src/vault.ts +636 -0
  196. package/src/workflow.ts +653 -0
  197. package/src/workflows/decision-context.ts +81 -0
  198. package/src/workflows/explain-change.ts +73 -0
  199. package/src/workflows/index.ts +3 -0
  200. package/src/workflows/plan-radar.ts +101 -0
@@ -0,0 +1,2480 @@
1
+ // @bun
2
+ import {
3
+ acquireArchiveTodaySnapshot,
4
+ discoverArchiveTodaySnapshot
5
+ } from "./index-7fzc32gf.js";
6
+ import {
7
+ canonicalizeUrl,
8
+ chooseBestExtraction,
9
+ countWords,
10
+ extractPage,
11
+ extractionShowsAccessControl,
12
+ localizeAssets,
13
+ scoreExtraction,
14
+ sniffImage
15
+ } from "./index-f984hw45.js";
16
+ import {
17
+ adapterCapabilities,
18
+ inspectClipEnvironment,
19
+ renderAdapterCapabilities,
20
+ renderDoctorReport
21
+ } from "./index-1n418kb9.js";
22
+ import {
23
+ acquireBrowser,
24
+ acquireCookieHttp,
25
+ acquireCookieRecords,
26
+ acquireFile,
27
+ acquireHttp,
28
+ assertSafePersistentProfile
29
+ } from "./index-qry4vhxk.js";
30
+ import {
31
+ CONTENT_REWRITE_TRUNCATION_WARNING,
32
+ buildClipMarkdown,
33
+ classifyPlatformUrl,
34
+ parseBlueskyCapture,
35
+ parseHackerNewsCapture,
36
+ parseRedditCapture,
37
+ renderCapturedDocument,
38
+ rewriteContentWithStatus,
39
+ slugify
40
+ } from "./index-hgve9rh2.js";
41
+ import {
42
+ startNetworkProxy
43
+ } from "./index-w2zc0vwa.js";
44
+ import {
45
+ FetchFailure,
46
+ decodeBytes,
47
+ safeFetch
48
+ } from "./index-e5fbsywq.js";
49
+ import {
50
+ captureUrl,
51
+ parseArguments,
52
+ usage
53
+ } from "./index-6g2pv9d2.js";
54
+ import {
55
+ BoundedByteBuffer
56
+ } from "./index-gh719d91.js";
57
+ import {
58
+ abortCaptureBundle,
59
+ beginCaptureBundle,
60
+ commitCaptureBundle,
61
+ redactSensitiveText,
62
+ redactSensitiveTextWithCount,
63
+ sanitizeArtifactUrl,
64
+ writeCaptureBundle
65
+ } from "./index-mxxxytys.js";
66
+ import {
67
+ MAX_COOKIE_BYTES,
68
+ filterCookieProviderResult,
69
+ filterCookies,
70
+ readCookieFile,
71
+ renderCookieHeader,
72
+ renderNetscapeCookieJar
73
+ } from "./index-84x0vjjp.js";
74
+ import {
75
+ sanitizeTerminalLine,
76
+ sanitizeTerminalText
77
+ } from "./index-1xxnjn0d.js";
78
+ import {
79
+ cloneBrowserProfile
80
+ } from "./index-5n05se68.js";
81
+
82
+ // src/clip/capture.ts
83
+ import {
84
+ chmodSync as chmodSync2,
85
+ copyFileSync,
86
+ existsSync as existsSync2,
87
+ mkdirSync as mkdirSync2,
88
+ mkdtempSync as mkdtempSync2,
89
+ readFileSync as readFileSync2,
90
+ rmSync as rmSync2,
91
+ statSync as statSync2
92
+ } from "fs";
93
+ import { tmpdir as tmpdir2 } from "os";
94
+ import { join as join2 } from "path";
95
+
96
+ // src/clip/media.ts
97
+ import { createHash } from "crypto";
98
+ import { spawn, spawnSync } from "child_process";
99
+ import {
100
+ chmodSync,
101
+ existsSync,
102
+ lstatSync,
103
+ mkdirSync,
104
+ mkdtempSync,
105
+ readFileSync,
106
+ readdirSync,
107
+ realpathSync,
108
+ renameSync,
109
+ rmSync,
110
+ statSync,
111
+ unlinkSync,
112
+ writeFileSync
113
+ } from "fs";
114
+ import { homedir, tmpdir } from "os";
115
+ import { basename, extname, join, resolve } from "path";
116
+ import { getCookies } from "@steipete/sweet-cookie";
117
+ var metadataPrefix = "CLIP_MEDIA_JSON\t";
118
+ var isRecord = (value) => typeof value === "object" && value !== null && !Array.isArray(value);
119
+ function startsWith(bytes, signature) {
120
+ return signature.every((byte, index) => bytes[index] === byte);
121
+ }
122
+ function ascii(bytes, start, length) {
123
+ let result = "";
124
+ for (let index = start;index < start + length && index < bytes.length; index += 1) {
125
+ result += String.fromCharCode(bytes[index] ?? 0);
126
+ }
127
+ return result;
128
+ }
129
+ function sniffMediaMimeType(bytes, extension) {
130
+ const normalized = extension.toLowerCase();
131
+ if (ascii(bytes, 4, 4) === "ftyp") {
132
+ if (normalized === ".mov")
133
+ return "video/quicktime";
134
+ if (normalized === ".m4a")
135
+ return "audio/mp4";
136
+ if (normalized === ".mp4")
137
+ return "video/mp4";
138
+ if (normalized === ".m4v")
139
+ return "video/x-m4v";
140
+ return null;
141
+ }
142
+ if (startsWith(bytes, [26, 69, 223, 163])) {
143
+ if (normalized === ".webm")
144
+ return "video/webm";
145
+ if (normalized === ".mkv")
146
+ return "video/x-matroska";
147
+ return null;
148
+ }
149
+ if (ascii(bytes, 0, 3) === "ID3" || bytes[0] === 255 && ((bytes[1] ?? 0) & 224) === 224) {
150
+ return normalized === ".mp3" ? "audio/mpeg" : null;
151
+ }
152
+ if (ascii(bytes, 0, 4) === "OggS") {
153
+ if (normalized === ".opus")
154
+ return "audio/opus";
155
+ if (normalized === ".ogg")
156
+ return "audio/ogg";
157
+ return null;
158
+ }
159
+ if (ascii(bytes, 0, 4) === "RIFF" && ascii(bytes, 8, 4) === "WAVE") {
160
+ return normalized === ".wav" ? "audio/wav" : null;
161
+ }
162
+ if (ascii(bytes, 0, 4) === "fLaC")
163
+ return normalized === ".flac" ? "audio/flac" : null;
164
+ if (bytes[0] === 255 && ((bytes[1] ?? 0) & 246) === 240) {
165
+ return normalized === ".aac" ? "audio/aac" : null;
166
+ }
167
+ return null;
168
+ }
169
+ async function readBoundedStream(stream, maxBytes) {
170
+ const iterable = stream;
171
+ const bytes = new BoundedByteBuffer(maxBytes);
172
+ for await (const value of iterable) {
173
+ let chunk;
174
+ if (Buffer.isBuffer(value))
175
+ chunk = value;
176
+ else if (typeof value === "string")
177
+ chunk = Buffer.from(value);
178
+ else if (value instanceof Uint8Array)
179
+ chunk = Buffer.from(value);
180
+ else
181
+ throw new Error("media command returned an unsupported output chunk");
182
+ if (!bytes.append(chunk))
183
+ throw new Error(`media command output exceeded ${maxBytes} bytes`);
184
+ }
185
+ return new TextDecoder().decode(bytes.toUint8Array());
186
+ }
187
+ function inspectMonitoredDirectory(directory, maxFiles, maxFileBytes, maxTotalBytes) {
188
+ let entries;
189
+ try {
190
+ entries = readdirSync(directory, { withFileTypes: true });
191
+ } catch (error) {
192
+ return `could not inspect media staging directory: ${error instanceof Error ? error.message : String(error)}`;
193
+ }
194
+ if (entries.length > maxFiles)
195
+ return `media capture created more than ${maxFiles} files`;
196
+ let totalBytes = 0;
197
+ for (const entry of entries) {
198
+ const path = join(directory, entry.name);
199
+ let stats;
200
+ try {
201
+ stats = lstatSync(path);
202
+ } catch {
203
+ continue;
204
+ }
205
+ if (!entry.isFile() || stats.isSymbolicLink())
206
+ return "media capture created an unexpected non-file output";
207
+ if (stats.size > maxFileBytes)
208
+ return `media capture created a file larger than ${maxFileBytes} bytes`;
209
+ totalBytes += stats.size;
210
+ if (totalBytes > maxTotalBytes)
211
+ return `media capture exceeded the ${maxTotalBytes}-byte total limit`;
212
+ }
213
+ return null;
214
+ }
215
+ var runMediaCommand = async (specification) => {
216
+ const executable = specification.command[0];
217
+ if (executable === undefined)
218
+ throw new Error("media command is empty");
219
+ const useProcessGroup = process.platform !== "win32";
220
+ const child = spawn(executable, specification.command.slice(1), {
221
+ ...specification.cwd === undefined ? {} : { cwd: specification.cwd },
222
+ detached: useProcessGroup,
223
+ stdio: ["pipe", "pipe", "pipe"],
224
+ windowsHide: true
225
+ });
226
+ child.stdin.once("error", () => {});
227
+ child.stdin.end(specification.stdin ?? "");
228
+ const exited = new Promise((resolveExit, rejectExit) => {
229
+ child.once("error", rejectExit);
230
+ child.once("close", (code) => resolveExit(code ?? 1));
231
+ });
232
+ const signalProcessTree = (signal) => {
233
+ if (useProcessGroup && child.pid !== undefined) {
234
+ try {
235
+ process.kill(-child.pid, signal);
236
+ return;
237
+ } catch {}
238
+ }
239
+ try {
240
+ child.kill(signal);
241
+ } catch {}
242
+ };
243
+ let failure = null;
244
+ let forceKillTimer = null;
245
+ const requestStop = (reason) => {
246
+ if (failure === null)
247
+ failure = reason;
248
+ if (forceKillTimer !== null)
249
+ return;
250
+ signalProcessTree("SIGTERM");
251
+ forceKillTimer = setTimeout(() => signalProcessTree("SIGKILL"), 1000);
252
+ };
253
+ const timeout = setTimeout(() => {
254
+ requestStop(`media command timed out after ${specification.timeoutMs}ms`);
255
+ }, specification.timeoutMs);
256
+ const monitor = setInterval(() => {
257
+ const violation = inspectMonitoredDirectory(specification.monitoredDirectory, specification.maxFiles, specification.maxFileBytes, specification.maxTotalBytes);
258
+ if (violation !== null)
259
+ requestStop(violation);
260
+ }, 100);
261
+ try {
262
+ const [stdout, stderr, exitCode] = await Promise.all([
263
+ readBoundedStream(child.stdout, specification.maxOutputBytes),
264
+ readBoundedStream(child.stderr, specification.maxOutputBytes),
265
+ exited
266
+ ]);
267
+ const finalViolation = inspectMonitoredDirectory(specification.monitoredDirectory, specification.maxFiles, specification.maxFileBytes, specification.maxTotalBytes);
268
+ if (failure !== null)
269
+ throw new Error(failure);
270
+ if (finalViolation !== null)
271
+ throw new Error(finalViolation);
272
+ return { stdout, stderr, exitCode };
273
+ } catch (error) {
274
+ if (failure === null)
275
+ requestStop(error instanceof Error ? error.message : "media command failed");
276
+ await exited.catch(() => 1);
277
+ throw error;
278
+ } finally {
279
+ clearTimeout(timeout);
280
+ clearInterval(monitor);
281
+ if (forceKillTimer !== null)
282
+ clearTimeout(forceKillTimer);
283
+ }
284
+ };
285
+ function discoverYtDlp(options = {}) {
286
+ const exists = options.exists ?? existsSync;
287
+ const which = options.which ?? ((name) => Bun.which(name));
288
+ const fromPath = which("yt-dlp");
289
+ if (fromPath !== null && exists(fromPath))
290
+ return fromPath;
291
+ const homeDirectory = options.homeDirectory ?? homedir();
292
+ return [
293
+ join(homeDirectory, ".local", "bin", "yt-dlp"),
294
+ "/opt/homebrew/bin/yt-dlp",
295
+ "/usr/local/bin/yt-dlp"
296
+ ].find((path) => exists(path)) ?? null;
297
+ }
298
+ function readInstalledNodeVersion(executable) {
299
+ try {
300
+ const result = spawnSync(executable, ["--version"], {
301
+ encoding: "utf8",
302
+ timeout: 1000,
303
+ maxBuffer: 4096,
304
+ windowsHide: true
305
+ });
306
+ if (result.status !== 0 || typeof result.stdout !== "string")
307
+ return null;
308
+ return result.stdout.trim();
309
+ } catch {
310
+ return null;
311
+ }
312
+ }
313
+ function supportsYtDlpNodeRuntime(version) {
314
+ if (version === null)
315
+ return false;
316
+ const match = /^v?(\d+)(?:\.|$)/.exec(version.trim());
317
+ if (match?.[1] === undefined)
318
+ return false;
319
+ const major = Number(match[1]);
320
+ return Number.isSafeInteger(major) && major >= 22;
321
+ }
322
+ function discoverNodeRuntime(options = {}) {
323
+ const exists = options.exists ?? existsSync;
324
+ const which = options.which ?? ((name) => Bun.which(name));
325
+ const readVersion = options.readVersion ?? readInstalledNodeVersion;
326
+ const homeDirectory = options.homeDirectory ?? homedir();
327
+ const candidates = [
328
+ which("node"),
329
+ join(homeDirectory, ".local", "bin", "node"),
330
+ "/opt/homebrew/bin/node",
331
+ "/usr/local/bin/node",
332
+ "/usr/bin/node"
333
+ ];
334
+ const seen = new Set;
335
+ for (const candidate of candidates) {
336
+ if (candidate === null || seen.has(candidate))
337
+ continue;
338
+ seen.add(candidate);
339
+ if (exists(candidate) && supportsYtDlpNodeRuntime(readVersion(candidate)))
340
+ return candidate;
341
+ }
342
+ return null;
343
+ }
344
+ function cleanString(value, maximumLength) {
345
+ if (typeof value !== "string")
346
+ return;
347
+ const cleaned = value.replace(/\0/g, "").trim();
348
+ if (cleaned === "" || cleaned.length > maximumLength)
349
+ return;
350
+ return cleaned;
351
+ }
352
+ function finiteNonNegative(value) {
353
+ return typeof value === "number" && Number.isFinite(value) && value >= 0 ? value : undefined;
354
+ }
355
+ function safeWebUrl(value) {
356
+ const candidate = cleanString(value, 8192);
357
+ if (candidate === undefined)
358
+ return;
359
+ try {
360
+ const url = new URL(candidate);
361
+ if (url.protocol !== "http:" && url.protocol !== "https:" || url.username !== "" || url.password !== "") {
362
+ return;
363
+ }
364
+ return url.href;
365
+ } catch {
366
+ return;
367
+ }
368
+ }
369
+ function parseMediaMetadata(stdout) {
370
+ const scanStart = Math.max(0, stdout.length - 2 * 1024 * 1024);
371
+ let lineEnd = stdout.length;
372
+ while (lineEnd >= scanStart) {
373
+ const newline = stdout.lastIndexOf(`
374
+ `, lineEnd - 1);
375
+ const lineStart = Math.max(scanStart, newline + 1);
376
+ const rawLine = stdout.slice(lineStart, lineEnd);
377
+ const line = rawLine.endsWith("\r") ? rawLine.slice(0, -1) : rawLine;
378
+ lineEnd = newline < scanStart ? scanStart - 1 : newline;
379
+ if (!line.startsWith(metadataPrefix))
380
+ continue;
381
+ try {
382
+ const parsed = JSON.parse(line.slice(metadataPrefix.length));
383
+ if (!isRecord(parsed))
384
+ continue;
385
+ const id = cleanString(parsed.id, 512);
386
+ const title = cleanString(parsed.title, 8192);
387
+ const description = cleanString(parsed.description, 500000);
388
+ const uploader = cleanString(parsed.uploader, 8192);
389
+ const uploaderId = cleanString(parsed.uploader_id, 8192);
390
+ const channel = cleanString(parsed.channel, 8192);
391
+ const channelId = cleanString(parsed.channel_id, 8192);
392
+ const webpageUrl = safeWebUrl(parsed.webpage_url);
393
+ const extractor = cleanString(parsed.extractor, 1024);
394
+ const durationSeconds = finiteNonNegative(parsed.duration);
395
+ const timestamp = finiteNonNegative(parsed.timestamp);
396
+ return {
397
+ ...id === undefined ? {} : { id },
398
+ ...title === undefined ? {} : { title },
399
+ ...description === undefined ? {} : { description },
400
+ ...uploader === undefined ? {} : { uploader },
401
+ ...uploaderId === undefined ? {} : { uploaderId },
402
+ ...channel === undefined ? {} : { channel },
403
+ ...channelId === undefined ? {} : { channelId },
404
+ ...webpageUrl === undefined ? {} : { webpageUrl },
405
+ ...extractor === undefined ? {} : { extractor },
406
+ ...durationSeconds === undefined ? {} : { durationSeconds },
407
+ ...timestamp === undefined ? {} : { timestamp }
408
+ };
409
+ } catch {}
410
+ }
411
+ return null;
412
+ }
413
+ function validProfile(profile) {
414
+ return profile === undefined || profile.trim() !== "" && profile.length <= 4096 && !/\p{Cc}/u.test(profile);
415
+ }
416
+ function buildMediaCookieOptions(request) {
417
+ const common = {
418
+ url: request.url.href,
419
+ timeoutMs: request.timeoutMs,
420
+ mode: "first",
421
+ debug: false
422
+ };
423
+ const profile = request.profile?.trim();
424
+ if (request.source === "edge") {
425
+ return {
426
+ ...common,
427
+ browsers: ["edge"],
428
+ edgeProfile: profile ?? ""
429
+ };
430
+ }
431
+ if (request.source === "firefox") {
432
+ return {
433
+ ...common,
434
+ browsers: ["firefox"],
435
+ firefoxProfile: profile ?? ""
436
+ };
437
+ }
438
+ if (request.source === "safari") {
439
+ return {
440
+ ...common,
441
+ browsers: ["safari"],
442
+ ...profile === undefined ? {} : { safariCookiesFile: profile }
443
+ };
444
+ }
445
+ return {
446
+ ...common,
447
+ browsers: ["chrome"],
448
+ chromiumBrowser: request.source,
449
+ chromeProfile: profile ?? ""
450
+ };
451
+ }
452
+ function createMediaCookieProvider(reader) {
453
+ return (request) => {
454
+ if (request.source !== "file")
455
+ return reader(buildMediaCookieOptions(request));
456
+ const parsed = readCookieFile(request.file, request.url);
457
+ return Promise.resolve(parsed.ok ? {
458
+ cookies: parsed.cookies,
459
+ warnings: parsed.rejected === 0 ? [] : [`Ignored ${parsed.rejected} malformed, expired, or out-of-scope cookie record(s).`]
460
+ } : { cookies: [], warnings: [] });
461
+ };
462
+ }
463
+ var readMediaCookies = createMediaCookieProvider((options) => getCookies(options));
464
+ async function prepareCookieJar(request, directory, provider) {
465
+ let provided;
466
+ try {
467
+ provided = await provider(request);
468
+ } catch {
469
+ return { ok: false, warning: "Could not read cookies from the explicitly selected browser." };
470
+ }
471
+ const filtered = filterCookieProviderResult(provided, request.url);
472
+ if (!filtered.validShape) {
473
+ return { ok: false, warning: "The selected browser cookie provider returned malformed data." };
474
+ }
475
+ if (filtered.cookies.length === 0) {
476
+ return {
477
+ ok: false,
478
+ warning: filtered.rejected === 0 ? "No origin-scoped cookies were found in the explicitly selected browser." : `No usable origin-scoped cookies were found; rejected ${filtered.rejected} malformed, expired, or out-of-scope record(s).`
479
+ };
480
+ }
481
+ const body = renderNetscapeCookieJar(filtered.cookies, request.url);
482
+ if (Buffer.byteLength(body, "utf8") > MAX_COOKIE_BYTES) {
483
+ return { ok: false, warning: "Origin-scoped browser cookies exceeded the private jar size limit." };
484
+ }
485
+ const path = join(directory, "cookies.txt");
486
+ try {
487
+ writeFileSync(path, body, { encoding: "utf8", flag: "wx", mode: 384 });
488
+ chmodSync(path, 384);
489
+ } catch {
490
+ return { ok: false, warning: "Could not create the private temporary cookie jar." };
491
+ }
492
+ const warnings = [];
493
+ if (filtered.rejected > 0) {
494
+ warnings.push(`Ignored ${filtered.rejected} malformed, expired, or out-of-scope browser cookie record(s).`);
495
+ }
496
+ if (filtered.providerWarningCount > 0) {
497
+ warnings.push(`The browser cookie provider reported ${filtered.providerWarningCount} non-fatal warning(s).`);
498
+ }
499
+ return { ok: true, path, warnings };
500
+ }
501
+ function metadataTemplate() {
502
+ return `${metadataPrefix}{"id":%(id)j,"title":%(title)j,"description":%(description)j,` + `"uploader":%(uploader)j,"uploader_id":%(uploader_id)j,"webpage_url":%(webpage_url)j,` + `"channel":%(channel)j,"channel_id":%(channel_id)j,"extractor":%(extractor)j,` + `"duration":%(duration)j,"timestamp":%(timestamp)j}`;
503
+ }
504
+ function commandArguments(executable, options, cookieFile, proxyUrl) {
505
+ const arguments_ = [
506
+ executable,
507
+ "--ignore-config",
508
+ "--no-playlist",
509
+ "--max-downloads",
510
+ "1",
511
+ "--max-filesize",
512
+ String(options.maxFileBytes),
513
+ "--restrict-filenames",
514
+ "--no-overwrites",
515
+ "--no-progress",
516
+ "--newline",
517
+ "--no-colors",
518
+ "--socket-timeout",
519
+ String(Math.max(1, Math.ceil(options.timeoutMs / 1000))),
520
+ "--retries",
521
+ "2",
522
+ "--fragment-retries",
523
+ "2",
524
+ "--proxy",
525
+ proxyUrl,
526
+ "--batch-file",
527
+ "-",
528
+ "--downloader",
529
+ "native",
530
+ "--output",
531
+ "media-%(id).80B.%(ext)s",
532
+ "--print",
533
+ `after_move:${metadataTemplate()}`
534
+ ];
535
+ if (options.userAgent !== undefined)
536
+ arguments_.push("--user-agent", options.userAgent);
537
+ if (cookieFile !== undefined)
538
+ arguments_.push("--cookies", cookieFile);
539
+ return arguments_;
540
+ }
541
+ function videoContextCommandArguments(executable, options, language, cookieFile, proxyUrl, nodeRuntime) {
542
+ const arguments_ = [
543
+ executable,
544
+ "--ignore-config",
545
+ "--no-playlist",
546
+ "--max-downloads",
547
+ "1",
548
+ "--max-filesize",
549
+ String(options.maxFileBytes),
550
+ "--restrict-filenames",
551
+ "--no-overwrites",
552
+ "--no-progress",
553
+ "--newline",
554
+ "--no-colors",
555
+ "--socket-timeout",
556
+ String(Math.max(1, Math.ceil(options.timeoutMs / 1000))),
557
+ "--retries",
558
+ "2",
559
+ "--fragment-retries",
560
+ "2",
561
+ "--proxy",
562
+ proxyUrl,
563
+ "--batch-file",
564
+ "-",
565
+ "--skip-download",
566
+ "--no-simulate",
567
+ "--ignore-no-formats-error",
568
+ "--write-thumbnail",
569
+ "--write-subs",
570
+ "--write-auto-subs",
571
+ "--sub-langs",
572
+ language,
573
+ "--sub-format",
574
+ "vtt",
575
+ "--output",
576
+ "unused-%(id).80B.%(ext)s",
577
+ "--output",
578
+ "thumbnail:thumbnail-%(id).80B.%(ext)s",
579
+ "--output",
580
+ "subtitle:transcript-%(id).80B.%(language).16B.%(ext)s",
581
+ "--print",
582
+ metadataTemplate()
583
+ ];
584
+ if (nodeRuntime !== null)
585
+ arguments_.push("--js-runtimes", `node:${nodeRuntime}`);
586
+ if (options.userAgent !== undefined)
587
+ arguments_.push("--user-agent", options.userAgent);
588
+ if (cookieFile !== undefined)
589
+ arguments_.push("--cookies", cookieFile);
590
+ return arguments_;
591
+ }
592
+ function privateMediaUrlInput(url) {
593
+ const value = url.href;
594
+ if (/[\0\r\n]/.test(value))
595
+ throw new Error("media URL contains an invalid batch-file control character");
596
+ return `${value}
597
+ `;
598
+ }
599
+ function errorClassification(stderr) {
600
+ const normalized = stderr.toLowerCase();
601
+ if (normalized.includes("unsupported url") || normalized.includes("no suitable extractor")) {
602
+ return { status: "unsupported", warning: "yt-dlp does not support media capture for this URL." };
603
+ }
604
+ if (normalized.includes("drm")) {
605
+ return { status: "unsupported", warning: "The media is DRM-protected; clip does not bypass DRM or access controls." };
606
+ }
607
+ if (normalized.includes("login") || normalized.includes("sign in") || normalized.includes("cookies")) {
608
+ return { status: "failed", warning: "The site requires an authorized session; explicitly select a cookie source or cookie file." };
609
+ }
610
+ if (normalized.includes("requested format is not available") || normalized.includes("no video formats found")) {
611
+ return { status: "unsupported", warning: "No downloadable, non-DRM media format was exposed for this page." };
612
+ }
613
+ return { status: "failed", warning: "yt-dlp could not capture media for this page; page text and images can still be clipped." };
614
+ }
615
+ function safeRunnerFailure(message) {
616
+ const safePatterns = [
617
+ /^media command timed out after \d+ms$/,
618
+ /^media command output exceeded \d+ bytes$/,
619
+ /^media capture created more than \d+ files$/,
620
+ /^media capture created a file larger than \d+ bytes$/,
621
+ /^media capture exceeded the \d+-byte total limit$/
622
+ ];
623
+ return safePatterns.some((pattern) => pattern.test(message)) ? message : "yt-dlp media capture failed; page text and images can still be clipped.";
624
+ }
625
+ function positiveBound(value, fallback, maximum) {
626
+ return value === undefined || !Number.isSafeInteger(value) || value < 1 ? fallback : Math.min(value, maximum);
627
+ }
628
+ function utf8Prefix(value, maxBytes) {
629
+ const bytes = Buffer.from(value);
630
+ if (bytes.byteLength <= maxBytes)
631
+ return value;
632
+ return bytes.subarray(0, maxBytes).toString("utf8").replace(/\ufffd$/u, "");
633
+ }
634
+ function decodeVttEntities(value) {
635
+ return value.replace(/&nbsp;/gi, " ").replace(/&amp;/gi, "&").replace(/&lt;/gi, "<").replace(/&gt;/gi, ">").replace(/&quot;/gi, '"').replace(/&#39;|&apos;/gi, "'").replace(/&#(?:x([0-9a-f]{1,6})|(\d{1,7}));/gi, (_match, hexadecimal, decimal) => {
636
+ const codePoint = Number.parseInt(hexadecimal ?? decimal ?? "", hexadecimal === undefined ? 10 : 16);
637
+ return Number.isSafeInteger(codePoint) && codePoint > 0 && codePoint <= 1114111 && !(codePoint >= 55296 && codePoint <= 57343) ? String.fromCodePoint(codePoint) : "";
638
+ });
639
+ }
640
+ function cleanVttCue(value) {
641
+ return decodeVttEntities(value.replace(/<[^>\n]{0,512}>/g, "")).replace(/\s+/g, " ").trim().slice(0, 16384);
642
+ }
643
+ function escapeTranscriptMarkdown(value) {
644
+ return value.replace(/\\/g, "\\\\").replace(/([`*_~[\]])/g, "\\$1").replace(/</g, "&lt;").replace(/>/g, "&gt;");
645
+ }
646
+ function vttTimestamp(value) {
647
+ const timestamp = value.trim().split(/\s+/u)[0];
648
+ if (timestamp === undefined)
649
+ return null;
650
+ const clock = timestamp.replace(",", ".").split(".");
651
+ if (clock.length !== 2 || clock[1] === undefined || !/^\d{3}$/.test(clock[1]))
652
+ return null;
653
+ const pieces = clock[0]?.split(":") ?? [];
654
+ if (pieces.length !== 2 && pieces.length !== 3)
655
+ return null;
656
+ if (pieces.some((piece) => !/^\d{2}$/.test(piece)))
657
+ return null;
658
+ const seconds = Number(pieces.at(-1));
659
+ const minutes = Number(pieces.at(-2));
660
+ const hours = pieces.length === 3 ? Number(pieces[0]) : 0;
661
+ if (!Number.isSafeInteger(hours) || !Number.isSafeInteger(minutes) || !Number.isSafeInteger(seconds) || minutes > 59 || seconds > 59)
662
+ return null;
663
+ return hours * 3600 + minutes * 60 + seconds;
664
+ }
665
+ function formatTranscriptTimestamp(seconds) {
666
+ const hours = Math.floor(seconds / 3600);
667
+ const minutes = Math.floor(seconds % 3600 / 60);
668
+ const remainingSeconds = seconds % 60;
669
+ return hours > 0 ? `${String(hours).padStart(2, "0")}:${String(minutes).padStart(2, "0")}:${String(remainingSeconds).padStart(2, "0")}` : `${String(minutes).padStart(2, "0")}:${String(remainingSeconds).padStart(2, "0")}`;
670
+ }
671
+ function rollingCaptionDelta(previous, current) {
672
+ if (previous === null)
673
+ return current;
674
+ if (previous === current || previous.includes(current))
675
+ return null;
676
+ const previousWords = previous.split(" ");
677
+ const currentWords = current.split(" ");
678
+ const maximumOverlap = Math.min(previousWords.length, currentWords.length, 256);
679
+ for (let overlap = maximumOverlap;overlap > 0; overlap -= 1) {
680
+ let matches = true;
681
+ for (let index = 0;index < overlap; index += 1) {
682
+ if (previousWords[previousWords.length - overlap + index] !== currentWords[index]) {
683
+ matches = false;
684
+ break;
685
+ }
686
+ }
687
+ if (matches) {
688
+ const delta = currentWords.slice(overlap).join(" ").trim();
689
+ return delta === "" ? null : delta;
690
+ }
691
+ }
692
+ return current;
693
+ }
694
+ function parseWebVtt(input, options = {}) {
695
+ const maxInputBytes = positiveBound(options.maxInputBytes, 8 * 1024 * 1024, 32 * 1024 * 1024);
696
+ const maxCues = positiveBound(options.maxCues, 20000, 1e5);
697
+ const maxOutputBytes = positiveBound(options.maxOutputBytes, 8 * 1024 * 1024, 32 * 1024 * 1024);
698
+ const inputWasTruncated = Buffer.byteLength(input) > maxInputBytes;
699
+ const boundedInput = inputWasTruncated ? utf8Prefix(input, maxInputBytes) : input;
700
+ const lines = boundedInput.replace(/\r\n?/g, `
701
+ `).split(`
702
+ `);
703
+ const markdown = [];
704
+ let outputBytes = 0;
705
+ let previousCue = null;
706
+ let truncated = inputWasTruncated;
707
+ for (let index = 0;index < lines.length; ) {
708
+ const timing = lines[index] ?? "";
709
+ const arrow = timing.indexOf("-->");
710
+ if (arrow < 0) {
711
+ index += 1;
712
+ continue;
713
+ }
714
+ const seconds = vttTimestamp(timing.slice(0, arrow));
715
+ index += 1;
716
+ const cueLines = [];
717
+ while (index < lines.length && (lines[index] ?? "").trim() !== "") {
718
+ if ((lines[index] ?? "").includes("-->"))
719
+ break;
720
+ cueLines.push(lines[index] ?? "");
721
+ index += 1;
722
+ }
723
+ if (seconds === null)
724
+ continue;
725
+ const current = cleanVttCue(cueLines.join(" "));
726
+ if (current === "")
727
+ continue;
728
+ const delta = rollingCaptionDelta(previousCue, current);
729
+ previousCue = current;
730
+ if (delta === null)
731
+ continue;
732
+ if (markdown.length >= maxCues) {
733
+ truncated = true;
734
+ break;
735
+ }
736
+ const line = `- [${formatTranscriptTimestamp(seconds)}] ${escapeTranscriptMarkdown(delta)}`;
737
+ const lineBytes = Buffer.byteLength(line) + (markdown.length === 0 ? 0 : 1);
738
+ if (outputBytes + lineBytes > maxOutputBytes) {
739
+ truncated = true;
740
+ break;
741
+ }
742
+ markdown.push(line);
743
+ outputBytes += lineBytes;
744
+ }
745
+ return {
746
+ markdown: markdown.length === 0 ? "" : `${markdown.join(`
747
+ `)}
748
+ `,
749
+ cueCount: markdown.length,
750
+ truncated
751
+ };
752
+ }
753
+ function sha256(bytes) {
754
+ return createHash("sha256").update(bytes).digest("hex");
755
+ }
756
+ function normalizePrefix(value) {
757
+ if (value === undefined || value === "")
758
+ return "media";
759
+ const pieces = value.split("/").filter((piece) => piece !== "" && piece !== ".");
760
+ if (pieces.length === 0 || pieces.some((piece) => piece === ".." || /[\\\0]/.test(piece)))
761
+ return "media";
762
+ return pieces.join("/");
763
+ }
764
+ function promoteMediaFiles(runDirectory, outputDirectory, relativePrefix, maxFiles, maxFileBytes, maxTotalBytes) {
765
+ const violation = inspectMonitoredDirectory(runDirectory, maxFiles, maxFileBytes, maxTotalBytes);
766
+ if (violation !== null)
767
+ return { records: [], warnings: [violation] };
768
+ const recordsByHash = new Map;
769
+ const warnings = [];
770
+ for (const entry of readdirSync(runDirectory, { withFileTypes: true })) {
771
+ if (!entry.isFile())
772
+ continue;
773
+ const extension = extname(entry.name).toLowerCase();
774
+ const source = join(runDirectory, entry.name);
775
+ const stats = statSync(source);
776
+ if (!stats.isFile() || stats.size > maxFileBytes) {
777
+ warnings.push(`Ignored invalid or oversized yt-dlp output ${basename(entry.name)}.`);
778
+ continue;
779
+ }
780
+ const bytes = readFileSync(source);
781
+ const mimeType = sniffMediaMimeType(bytes, extension);
782
+ if (mimeType === null) {
783
+ warnings.push(`Ignored unrecognized or mislabeled yt-dlp output ${basename(entry.name)}.`);
784
+ continue;
785
+ }
786
+ const digest = sha256(bytes);
787
+ const filename = `${digest}${extension}`;
788
+ const destination = join(outputDirectory, filename);
789
+ if (existsSync(destination)) {
790
+ const destinationStats = lstatSync(destination);
791
+ if (!destinationStats.isFile() || destinationStats.isSymbolicLink()) {
792
+ warnings.push(`Refused unsafe existing media destination ${filename}.`);
793
+ continue;
794
+ }
795
+ const destinationDigest = sha256(readFileSync(destination));
796
+ if (destinationDigest !== digest) {
797
+ warnings.push(`Refused conflicting existing media destination ${filename}.`);
798
+ continue;
799
+ }
800
+ unlinkSync(source);
801
+ } else
802
+ renameSync(source, destination);
803
+ if (!recordsByHash.has(digest)) {
804
+ recordsByHash.set(digest, {
805
+ path: `${relativePrefix}/${filename}`,
806
+ mimeType,
807
+ bytes: stats.size,
808
+ sha256: digest
809
+ });
810
+ }
811
+ }
812
+ return {
813
+ records: [...recordsByHash.values()].sort((left, right) => left.path.localeCompare(right.path)),
814
+ warnings
815
+ };
816
+ }
817
+ function promoteVideoContextFiles(runDirectory, outputDirectory, relativePrefix, language, maxFiles, maxFileBytes, maxTotalBytes) {
818
+ const violation = inspectMonitoredDirectory(runDirectory, maxFiles, maxFileBytes, maxTotalBytes);
819
+ if (violation !== null)
820
+ return { thumbnail: null, transcript: null, warnings: [violation] };
821
+ const warnings = [];
822
+ let thumbnailCandidate = null;
823
+ let transcriptCandidate = null;
824
+ const entries = readdirSync(runDirectory, { withFileTypes: true }).filter((entry) => entry.isFile()).sort((left, right) => left.name.localeCompare(right.name));
825
+ for (const entry of entries) {
826
+ const source = join(runDirectory, entry.name);
827
+ const stats = statSync(source);
828
+ if (!stats.isFile() || stats.size > maxFileBytes) {
829
+ warnings.push("Ignored an invalid or oversized yt-dlp sidecar.");
830
+ continue;
831
+ }
832
+ const extension = extname(entry.name).toLowerCase();
833
+ if (extension === ".vtt") {
834
+ const contents = readFileSync(source, "utf8");
835
+ unlinkSync(source);
836
+ const parsed = parseWebVtt(contents, {
837
+ maxInputBytes: Math.min(maxFileBytes, 8 * 1024 * 1024),
838
+ maxOutputBytes: Math.min(maxFileBytes, 8 * 1024 * 1024)
839
+ });
840
+ if (parsed.cueCount > 0 && (transcriptCandidate === null || parsed.cueCount > transcriptCandidate.cueCount)) {
841
+ transcriptCandidate = parsed;
842
+ }
843
+ continue;
844
+ }
845
+ const bytes = readFileSync(source);
846
+ const image = sniffImage(bytes);
847
+ if (image === null) {
848
+ warnings.push("Ignored an unrecognized yt-dlp sidecar.");
849
+ continue;
850
+ }
851
+ if (thumbnailCandidate === null || bytes.byteLength > thumbnailCandidate.bytes.byteLength) {
852
+ thumbnailCandidate = {
853
+ source,
854
+ bytes,
855
+ mimeType: image.mimeType,
856
+ extension: `.${image.extension}`
857
+ };
858
+ }
859
+ }
860
+ let thumbnail = null;
861
+ if (thumbnailCandidate !== null) {
862
+ const digest = sha256(thumbnailCandidate.bytes);
863
+ const filename = `${digest}${thumbnailCandidate.extension}`;
864
+ const destination = join(outputDirectory, filename);
865
+ let promoted = false;
866
+ if (existsSync(destination)) {
867
+ const destinationStats = lstatSync(destination);
868
+ if (!destinationStats.isFile() || destinationStats.isSymbolicLink()) {
869
+ warnings.push("Refused an unsafe existing video-thumbnail destination.");
870
+ } else if (sha256(readFileSync(destination)) !== digest) {
871
+ warnings.push("Refused a conflicting existing video-thumbnail destination.");
872
+ } else {
873
+ unlinkSync(thumbnailCandidate.source);
874
+ promoted = true;
875
+ }
876
+ } else {
877
+ renameSync(thumbnailCandidate.source, destination);
878
+ promoted = true;
879
+ }
880
+ if (promoted) {
881
+ thumbnail = {
882
+ path: `${relativePrefix}/${filename}`,
883
+ mimeType: thumbnailCandidate.mimeType,
884
+ bytes: thumbnailCandidate.bytes.byteLength,
885
+ sha256: digest
886
+ };
887
+ }
888
+ }
889
+ if (transcriptCandidate?.truncated === true) {
890
+ warnings.push("The video transcript reached its bounded parse limit and was truncated.");
891
+ }
892
+ return {
893
+ thumbnail,
894
+ transcript: transcriptCandidate === null ? null : {
895
+ language,
896
+ markdown: transcriptCandidate.markdown,
897
+ cueCount: transcriptCandidate.cueCount,
898
+ truncated: transcriptCandidate.truncated
899
+ },
900
+ warnings
901
+ };
902
+ }
903
+ function validateOptions(options) {
904
+ if (options.url.protocol !== "http:" && options.url.protocol !== "https:")
905
+ return "Media URL must use HTTP or HTTPS.";
906
+ if (options.url.username !== "" || options.url.password !== "")
907
+ return "Media URL must not contain credentials.";
908
+ if (!Number.isSafeInteger(options.timeoutMs) || options.timeoutMs < 1)
909
+ return "Media timeout must be a positive integer.";
910
+ if (!Number.isSafeInteger(options.maxFileBytes) || options.maxFileBytes < 1)
911
+ return "Per-file media limit must be positive.";
912
+ if (!Number.isSafeInteger(options.maxTotalBytes) || options.maxTotalBytes < options.maxFileBytes) {
913
+ return "Total media limit must be at least the per-file limit.";
914
+ }
915
+ if (options.cookiesFile !== undefined) {
916
+ try {
917
+ const cookieFileStats = lstatSync(resolve(options.cookiesFile));
918
+ if (!cookieFileStats.isFile())
919
+ return "The explicitly selected cookie file is not a regular file.";
920
+ if (cookieFileStats.size > MAX_COOKIE_BYTES)
921
+ return "The explicitly selected cookie file exceeds the 2mb input limit.";
922
+ } catch {
923
+ return "The explicitly selected cookie file is unavailable.";
924
+ }
925
+ }
926
+ if (options.cookieBrowser !== undefined && !validProfile(options.cookieBrowser.profile)) {
927
+ return "The explicitly selected browser cookie profile is invalid.";
928
+ }
929
+ return null;
930
+ }
931
+ async function captureMedia(options) {
932
+ const validation = validateOptions(options);
933
+ if (validation !== null)
934
+ return { status: "failed", records: [], metadata: null, warnings: [validation] };
935
+ const exists = options.exists ?? existsSync;
936
+ const executable = options.executable ?? discoverYtDlp({
937
+ ...options.homeDirectory === undefined ? {} : { homeDirectory: options.homeDirectory },
938
+ exists,
939
+ ...options.which === undefined ? {} : { which: options.which }
940
+ });
941
+ if (executable === null || !exists(executable)) {
942
+ return {
943
+ status: "unavailable",
944
+ records: [],
945
+ metadata: null,
946
+ warnings: ["yt-dlp is not installed; skipped optional audio/video capture."]
947
+ };
948
+ }
949
+ const maxFiles = Math.max(1, Math.min(options.maxFiles ?? 12, 100));
950
+ const maxOutputBytes = Math.max(4096, Math.min(options.maxOutputBytes ?? 2 * 1024 * 1024, 16 * 1024 * 1024));
951
+ const outputDirectory = resolve(options.outputDirectory);
952
+ try {
953
+ mkdirSync(outputDirectory, { recursive: true, mode: 493 });
954
+ const outputStats = lstatSync(outputDirectory);
955
+ if (!outputStats.isDirectory() || outputStats.isSymbolicLink()) {
956
+ return { status: "failed", records: [], metadata: null, warnings: ["Media destination must be a real directory, not a symlink."] };
957
+ }
958
+ } catch {
959
+ return { status: "failed", records: [], metadata: null, warnings: ["Could not create the media destination directory."] };
960
+ }
961
+ const realOutputDirectory = realpathSync(outputDirectory);
962
+ const runDirectory = mkdtempSync(join(realOutputDirectory, ".clip-media-"));
963
+ let authDirectory = null;
964
+ let networkProxy = null;
965
+ const run = options.run ?? runMediaCommand;
966
+ const authenticationWarnings = [];
967
+ try {
968
+ let cookieFile;
969
+ let cookieRequest;
970
+ if (options.cookiesFile !== undefined) {
971
+ cookieRequest = {
972
+ url: options.url,
973
+ source: "file",
974
+ file: resolve(options.cookiesFile),
975
+ timeoutMs: options.timeoutMs
976
+ };
977
+ if (options.cookieBrowser !== undefined) {
978
+ authenticationWarnings.push("The explicit cookie file took precedence over the selected browser cookie source.");
979
+ }
980
+ } else if (options.cookieBrowser !== undefined) {
981
+ cookieRequest = {
982
+ url: options.url,
983
+ source: options.cookieBrowser.source,
984
+ timeoutMs: options.timeoutMs,
985
+ ...options.cookieBrowser.profile === undefined ? {} : { profile: options.cookieBrowser.profile }
986
+ };
987
+ }
988
+ if (cookieRequest !== undefined) {
989
+ authDirectory = mkdtempSync(join(tmpdir(), "hraness-kb-auth-"));
990
+ chmodSync(authDirectory, 448);
991
+ const prepared = await prepareCookieJar(cookieRequest, authDirectory, options.cookieProvider ?? readMediaCookies);
992
+ if (!prepared.ok) {
993
+ return { status: "failed", records: [], metadata: null, warnings: [prepared.warning] };
994
+ }
995
+ cookieFile = prepared.path;
996
+ authenticationWarnings.push(...prepared.warnings);
997
+ }
998
+ networkProxy = await (options.startProxy ?? startNetworkProxy)({
999
+ allowPrivateNetwork: options.allowPrivateNetwork ?? false,
1000
+ timeoutMs: options.timeoutMs,
1001
+ maxTransferredBytes: Math.max(64 * 1024 * 1024, Math.min(Number.MAX_SAFE_INTEGER, options.maxTotalBytes * 3))
1002
+ });
1003
+ const result = await run({
1004
+ command: commandArguments(executable, options, cookieFile, networkProxy.url),
1005
+ stdin: privateMediaUrlInput(options.url),
1006
+ cwd: runDirectory,
1007
+ timeoutMs: options.timeoutMs,
1008
+ maxOutputBytes,
1009
+ monitoredDirectory: runDirectory,
1010
+ maxFiles,
1011
+ maxFileBytes: options.maxFileBytes,
1012
+ maxTotalBytes: options.maxTotalBytes
1013
+ });
1014
+ const metadata = parseMediaMetadata(result.stdout);
1015
+ if (result.exitCode !== 0 && result.exitCode !== 101) {
1016
+ const classification = errorClassification(result.stderr);
1017
+ return {
1018
+ status: classification.status,
1019
+ records: [],
1020
+ metadata,
1021
+ warnings: [...authenticationWarnings, classification.warning]
1022
+ };
1023
+ }
1024
+ const promoted = promoteMediaFiles(runDirectory, realOutputDirectory, normalizePrefix(options.relativePrefix), maxFiles, options.maxFileBytes, options.maxTotalBytes);
1025
+ if (result.exitCode === 101 && promoted.records.length === 0) {
1026
+ const classification = errorClassification(result.stderr);
1027
+ return {
1028
+ status: classification.status,
1029
+ records: [],
1030
+ metadata,
1031
+ warnings: [...authenticationWarnings, ...promoted.warnings, classification.warning]
1032
+ };
1033
+ }
1034
+ if (promoted.records.length === 0) {
1035
+ return {
1036
+ status: "unsupported",
1037
+ records: [],
1038
+ metadata,
1039
+ warnings: [
1040
+ ...authenticationWarnings,
1041
+ ...promoted.warnings,
1042
+ "yt-dlp completed without a supported audio/video file."
1043
+ ]
1044
+ };
1045
+ }
1046
+ return {
1047
+ status: "captured",
1048
+ records: promoted.records,
1049
+ metadata,
1050
+ warnings: [...authenticationWarnings, ...promoted.warnings]
1051
+ };
1052
+ } catch (error) {
1053
+ const message = error instanceof Error ? error.message : String(error);
1054
+ return {
1055
+ status: "failed",
1056
+ records: [],
1057
+ metadata: null,
1058
+ warnings: [...authenticationWarnings, safeRunnerFailure(message)]
1059
+ };
1060
+ } finally {
1061
+ try {
1062
+ await networkProxy?.close();
1063
+ } finally {
1064
+ if (authDirectory !== null)
1065
+ rmSync(authDirectory, { recursive: true, force: true });
1066
+ rmSync(runDirectory, { recursive: true, force: true });
1067
+ }
1068
+ }
1069
+ }
1070
+ function transcriptLanguage(value) {
1071
+ const language = (value ?? "en").trim();
1072
+ return language.length <= 64 && /^[A-Za-z0-9]{1,16}(?:[-_][A-Za-z0-9]{1,16})*$/.test(language) ? language : null;
1073
+ }
1074
+ function nodeRuntimeFor(options, exists) {
1075
+ if (options.nodeExecutable === null)
1076
+ return { path: null, warnings: [] };
1077
+ const readVersion = options.readNodeVersion ?? readInstalledNodeVersion;
1078
+ if (options.nodeExecutable !== undefined) {
1079
+ if (exists(options.nodeExecutable) && supportsYtDlpNodeRuntime(readVersion(options.nodeExecutable)))
1080
+ return { path: options.nodeExecutable, warnings: [] };
1081
+ return {
1082
+ path: null,
1083
+ warnings: ["The selected Node.js runtime was unavailable or older than Node.js 22; yt-dlp used its default runtime discovery."]
1084
+ };
1085
+ }
1086
+ return {
1087
+ path: discoverNodeRuntime({
1088
+ ...options.homeDirectory === undefined ? {} : { homeDirectory: options.homeDirectory },
1089
+ exists,
1090
+ ...options.which === undefined ? {} : { which: options.which },
1091
+ readVersion
1092
+ }),
1093
+ warnings: []
1094
+ };
1095
+ }
1096
+ async function captureVideoContext(options) {
1097
+ const validation = validateOptions(options);
1098
+ if (validation !== null) {
1099
+ return { status: "failed", thumbnail: null, transcript: null, metadata: null, warnings: [validation] };
1100
+ }
1101
+ const language = transcriptLanguage(options.transcriptLanguage);
1102
+ if (language === null) {
1103
+ return {
1104
+ status: "failed",
1105
+ thumbnail: null,
1106
+ transcript: null,
1107
+ metadata: null,
1108
+ warnings: ["Transcript language must be one exact language identifier such as en or en-US."]
1109
+ };
1110
+ }
1111
+ const exists = options.exists ?? existsSync;
1112
+ const executable = options.executable ?? discoverYtDlp({
1113
+ ...options.homeDirectory === undefined ? {} : { homeDirectory: options.homeDirectory },
1114
+ exists,
1115
+ ...options.which === undefined ? {} : { which: options.which }
1116
+ });
1117
+ if (executable === null || !exists(executable)) {
1118
+ return {
1119
+ status: "unavailable",
1120
+ thumbnail: null,
1121
+ transcript: null,
1122
+ metadata: null,
1123
+ warnings: ["yt-dlp is not installed; skipped optional video context capture."]
1124
+ };
1125
+ }
1126
+ const maxFiles = Math.max(1, Math.min(options.maxFiles ?? 12, 100));
1127
+ const maxOutputBytes = Math.max(4096, Math.min(options.maxOutputBytes ?? 2 * 1024 * 1024, 16 * 1024 * 1024));
1128
+ const outputDirectory = resolve(options.outputDirectory);
1129
+ try {
1130
+ mkdirSync(outputDirectory, { recursive: true, mode: 493 });
1131
+ const outputStats = lstatSync(outputDirectory);
1132
+ if (!outputStats.isDirectory() || outputStats.isSymbolicLink()) {
1133
+ return {
1134
+ status: "failed",
1135
+ thumbnail: null,
1136
+ transcript: null,
1137
+ metadata: null,
1138
+ warnings: ["Video-context destination must be a real directory, not a symlink."]
1139
+ };
1140
+ }
1141
+ } catch {
1142
+ return {
1143
+ status: "failed",
1144
+ thumbnail: null,
1145
+ transcript: null,
1146
+ metadata: null,
1147
+ warnings: ["Could not create the video-context destination directory."]
1148
+ };
1149
+ }
1150
+ const realOutputDirectory = realpathSync(outputDirectory);
1151
+ const runDirectory = mkdtempSync(join(realOutputDirectory, ".clip-video-context-"));
1152
+ let authDirectory = null;
1153
+ let networkProxy = null;
1154
+ const run = options.run ?? runMediaCommand;
1155
+ const authenticationWarnings = [];
1156
+ try {
1157
+ let cookieFile;
1158
+ let cookieRequest;
1159
+ if (options.cookiesFile !== undefined) {
1160
+ cookieRequest = {
1161
+ url: options.url,
1162
+ source: "file",
1163
+ file: resolve(options.cookiesFile),
1164
+ timeoutMs: options.timeoutMs
1165
+ };
1166
+ if (options.cookieBrowser !== undefined) {
1167
+ authenticationWarnings.push("The explicit cookie file took precedence over the selected browser cookie source.");
1168
+ }
1169
+ } else if (options.cookieBrowser !== undefined) {
1170
+ cookieRequest = {
1171
+ url: options.url,
1172
+ source: options.cookieBrowser.source,
1173
+ timeoutMs: options.timeoutMs,
1174
+ ...options.cookieBrowser.profile === undefined ? {} : { profile: options.cookieBrowser.profile }
1175
+ };
1176
+ }
1177
+ if (cookieRequest !== undefined) {
1178
+ authDirectory = mkdtempSync(join(tmpdir(), "hraness-kb-auth-"));
1179
+ chmodSync(authDirectory, 448);
1180
+ const prepared = await prepareCookieJar(cookieRequest, authDirectory, options.cookieProvider ?? readMediaCookies);
1181
+ if (!prepared.ok) {
1182
+ return {
1183
+ status: "failed",
1184
+ thumbnail: null,
1185
+ transcript: null,
1186
+ metadata: null,
1187
+ warnings: [prepared.warning]
1188
+ };
1189
+ }
1190
+ cookieFile = prepared.path;
1191
+ authenticationWarnings.push(...prepared.warnings);
1192
+ }
1193
+ networkProxy = await (options.startProxy ?? startNetworkProxy)({
1194
+ allowPrivateNetwork: options.allowPrivateNetwork ?? false,
1195
+ timeoutMs: options.timeoutMs,
1196
+ maxTransferredBytes: Math.max(64 * 1024 * 1024, Math.min(Number.MAX_SAFE_INTEGER, options.maxTotalBytes * 3))
1197
+ });
1198
+ const nodeRuntime = nodeRuntimeFor(options, exists);
1199
+ const result = await run({
1200
+ command: videoContextCommandArguments(executable, options, language, cookieFile, networkProxy.url, nodeRuntime.path),
1201
+ stdin: privateMediaUrlInput(options.url),
1202
+ cwd: runDirectory,
1203
+ timeoutMs: options.timeoutMs,
1204
+ maxOutputBytes,
1205
+ monitoredDirectory: runDirectory,
1206
+ maxFiles,
1207
+ maxFileBytes: options.maxFileBytes,
1208
+ maxTotalBytes: options.maxTotalBytes
1209
+ });
1210
+ const metadata = parseMediaMetadata(result.stdout);
1211
+ const promoted = promoteVideoContextFiles(runDirectory, realOutputDirectory, normalizePrefix(options.relativePrefix), language, maxFiles, options.maxFileBytes, options.maxTotalBytes);
1212
+ const capturedSomething = metadata !== null || promoted.thumbnail !== null || promoted.transcript !== null;
1213
+ const warnings = [...authenticationWarnings, ...nodeRuntime.warnings, ...promoted.warnings];
1214
+ if (result.exitCode !== 0 && !(result.exitCode === 101 && capturedSomething)) {
1215
+ const classification = errorClassification(result.stderr);
1216
+ return {
1217
+ status: capturedSomething ? "partial" : classification.status,
1218
+ thumbnail: promoted.thumbnail,
1219
+ transcript: promoted.transcript,
1220
+ metadata,
1221
+ warnings: [...warnings, classification.warning]
1222
+ };
1223
+ }
1224
+ if (metadata === null)
1225
+ warnings.push("yt-dlp returned no allowlisted video metadata.");
1226
+ if (promoted.thumbnail === null)
1227
+ warnings.push("yt-dlp returned no supported video thumbnail.");
1228
+ if (promoted.transcript === null) {
1229
+ warnings.push(`yt-dlp returned no ${language} transcript for this video.`);
1230
+ }
1231
+ return {
1232
+ status: metadata !== null && promoted.thumbnail !== null && promoted.transcript !== null ? "captured" : "partial",
1233
+ thumbnail: promoted.thumbnail,
1234
+ transcript: promoted.transcript,
1235
+ metadata,
1236
+ warnings
1237
+ };
1238
+ } catch (error) {
1239
+ const message = error instanceof Error ? error.message : String(error);
1240
+ return {
1241
+ status: "failed",
1242
+ thumbnail: null,
1243
+ transcript: null,
1244
+ metadata: null,
1245
+ warnings: [...authenticationWarnings, safeRunnerFailure(message)]
1246
+ };
1247
+ } finally {
1248
+ try {
1249
+ await networkProxy?.close();
1250
+ } finally {
1251
+ if (authDirectory !== null)
1252
+ rmSync(authDirectory, { recursive: true, force: true });
1253
+ rmSync(runDirectory, { recursive: true, force: true });
1254
+ }
1255
+ }
1256
+ }
1257
+
1258
+ // src/clip/structured.ts
1259
+ var isRecord2 = (value) => typeof value === "object" && value !== null && !Array.isArray(value);
1260
+ var itemId = (value) => {
1261
+ if (typeof value === "string" && /^\d+$/.test(value))
1262
+ return value;
1263
+ if (typeof value === "number" && Number.isSafeInteger(value) && value >= 0)
1264
+ return String(value);
1265
+ return null;
1266
+ };
1267
+ var enqueueHackerNewsChildren = (value, depth, maximumQueueSize, queue, scheduled) => {
1268
+ if (!isRecord2(value) || !Array.isArray(value.kids))
1269
+ return { duplicate: false, truncated: false };
1270
+ let duplicate = false;
1271
+ for (const child of value.kids) {
1272
+ const id = itemId(child);
1273
+ if (id === null)
1274
+ continue;
1275
+ if (scheduled.has(id)) {
1276
+ duplicate = true;
1277
+ continue;
1278
+ }
1279
+ if (queue.length >= maximumQueueSize)
1280
+ return { duplicate, truncated: true };
1281
+ scheduled.add(id);
1282
+ queue.push({ id, depth });
1283
+ }
1284
+ return { duplicate, truncated: false };
1285
+ };
1286
+ async function defaultJsonFetcher(options, url, maxBytes, timeoutMs = options.timeoutMs) {
1287
+ const response = await safeFetch(url, {
1288
+ timeoutMs,
1289
+ maxBytes,
1290
+ allowPrivateNetwork: options.allowPrivateNetwork,
1291
+ userAgent: options.userAgent,
1292
+ accept: "application/json",
1293
+ retries: 2
1294
+ });
1295
+ const text = decodeBytes(response.bytes, response.contentType);
1296
+ try {
1297
+ return JSON.parse(text);
1298
+ } catch (error) {
1299
+ throw new Error(`invalid JSON from ${url.origin}`, { cause: error });
1300
+ }
1301
+ }
1302
+ function serializedBytes(value) {
1303
+ try {
1304
+ return new TextEncoder().encode(JSON.stringify(value)).byteLength;
1305
+ } catch {
1306
+ return Number.POSITIVE_INFINITY;
1307
+ }
1308
+ }
1309
+ function walkDocument(document) {
1310
+ let pageItems = 0;
1311
+ let scopedItems = 0;
1312
+ let incomplete = false;
1313
+ let rootIncomplete = false;
1314
+ let blockedRoot = false;
1315
+ const active = new WeakSet;
1316
+ const visit = (entry, location) => {
1317
+ if (active.has(entry)) {
1318
+ incomplete = true;
1319
+ return;
1320
+ }
1321
+ active.add(entry);
1322
+ if (entry.kind === "boundary" || entry.kind === "more") {
1323
+ incomplete = true;
1324
+ if (location === "root")
1325
+ rootIncomplete = true;
1326
+ active.delete(entry);
1327
+ return;
1328
+ }
1329
+ const unavailable = entry.kind === "unavailable";
1330
+ const unavailableButRepresented = unavailable && (entry.reason === "deleted" || entry.reason === "dead" || entry.reason === "removed");
1331
+ const captured = entry.kind === "content" || unavailableButRepresented;
1332
+ if (location === "root" && captured)
1333
+ pageItems += 1;
1334
+ if (location === "reply" && captured)
1335
+ scopedItems += 1;
1336
+ if (unavailable && (entry.reason === "not-found" || entry.reason === "blocked"))
1337
+ incomplete = true;
1338
+ if (location === "root" && unavailable && (entry.reason === "not-found" || entry.reason === "blocked")) {
1339
+ rootIncomplete = true;
1340
+ }
1341
+ if (location === "root" && unavailable && entry.reason === "blocked")
1342
+ blockedRoot = true;
1343
+ if (entry.kind === "content") {
1344
+ for (const quote of entry.quotes)
1345
+ visit(quote, "quote");
1346
+ for (const reply of entry.replies)
1347
+ visit(reply, "reply");
1348
+ } else if (entry.kind === "unavailable") {
1349
+ for (const reply of entry.replies)
1350
+ visit(reply, "reply");
1351
+ }
1352
+ active.delete(entry);
1353
+ };
1354
+ for (const entry of document.ancestors)
1355
+ visit(entry, "ancestor");
1356
+ for (const entry of document.roots)
1357
+ visit(entry, "root");
1358
+ return { pageItems, scopedItems, incomplete, rootIncomplete, blockedRoot };
1359
+ }
1360
+ var rootContent = (document) => {
1361
+ const root = document.roots[0];
1362
+ return root?.kind === "content" ? root : null;
1363
+ };
1364
+ function structuredStatus(document, scope, adapterWarnings) {
1365
+ const walked = walkDocument(document);
1366
+ const root = rootContent(document);
1367
+ if (scope === "page") {
1368
+ return {
1369
+ status: walked.blockedRoot ? "blocked" : walked.rootIncomplete || walked.pageItems === 0 || adapterWarnings.length > 0 ? "partial" : "complete",
1370
+ capturedItems: walked.pageItems,
1371
+ expectedItems: null,
1372
+ declaredItems: null
1373
+ };
1374
+ }
1375
+ const declaredItems = root?.metrics.replies ?? null;
1376
+ const expectedItems = declaredItems === null ? null : Math.max(declaredItems, walked.scopedItems);
1377
+ if (walked.blockedRoot) {
1378
+ return { status: "blocked", capturedItems: walked.scopedItems, expectedItems, declaredItems };
1379
+ }
1380
+ const shortOfDeclared = declaredItems !== null && walked.scopedItems < declaredItems;
1381
+ return {
1382
+ status: walked.incomplete || shortOfDeclared || adapterWarnings.length > 0 ? "partial" : "complete",
1383
+ capturedItems: walked.scopedItems,
1384
+ expectedItems,
1385
+ declaredItems
1386
+ };
1387
+ }
1388
+ function structuredCaptureFromDocument(options, document, evidence, method, adapterWarnings, extractor = `${document.platform}-public-api`) {
1389
+ const rendered = renderCapturedDocument(document);
1390
+ const content = rendered.replace(/^# [^\n]+\n\n/, "").trim();
1391
+ const root = rootContent(document);
1392
+ const article = {
1393
+ content,
1394
+ title: document.title,
1395
+ author: root?.author?.name ?? null,
1396
+ published: root?.createdAt ?? null,
1397
+ description: null
1398
+ };
1399
+ const completeness = structuredStatus(document, options.scope, [...adapterWarnings, ...document.warnings]);
1400
+ const warnings = [...adapterWarnings, ...document.warnings];
1401
+ if (completeness.declaredItems !== null && completeness.capturedItems > completeness.declaredItems) {
1402
+ warnings.push(`The source declared ${completeness.declaredItems} scoped items, but ${completeness.capturedItems} distinct items were captured; the expected count was normalized to the observed count.`);
1403
+ }
1404
+ if (completeness.status !== "complete") {
1405
+ warnings.push(`Structured ${document.platform} capture is ${completeness.status}; limits or unavailable branches remain.`);
1406
+ }
1407
+ const acquisition = {
1408
+ body: JSON.stringify(evidence),
1409
+ contentType: "application/json",
1410
+ finalUrl: captureUrl(options),
1411
+ method,
1412
+ warnings
1413
+ };
1414
+ const wordCount = countWords(content);
1415
+ const statusWeight = {
1416
+ complete: 1e5,
1417
+ partial: 60000,
1418
+ "auth-required": 0,
1419
+ blocked: -1e4,
1420
+ unsupported: -20000
1421
+ };
1422
+ return {
1423
+ extraction: {
1424
+ article,
1425
+ canonicalUrl: new URL(document.sourceUrl),
1426
+ platform: document.platform,
1427
+ status: completeness.status,
1428
+ score: statusWeight[completeness.status] + Math.min(content.length, 50000) + completeness.capturedItems * 50,
1429
+ wordCount,
1430
+ expectedItems: completeness.expectedItems,
1431
+ capturedItems: completeness.capturedItems,
1432
+ extractor,
1433
+ warnings,
1434
+ acquisition
1435
+ },
1436
+ evidence: `${JSON.stringify(evidence, null, 2)}
1437
+ `
1438
+ };
1439
+ }
1440
+ async function captureHackerNews(options, classified, fetchJson) {
1441
+ const endpoint = (id) => new URL(`https://hacker-news.firebaseio.com/v0/item/${encodeURIComponent(id)}.json`);
1442
+ const deadline = Date.now() + options.timeoutMs;
1443
+ let remainingBytes = options.maxHtmlBytes;
1444
+ const remainingTime = () => {
1445
+ const value = deadline - Date.now();
1446
+ if (value <= 0)
1447
+ throw new Error(`Hacker News capture exceeded the ${options.timeoutMs}ms total deadline`);
1448
+ return value;
1449
+ };
1450
+ const rootAllocation = Math.min(remainingBytes, 1024 * 1024);
1451
+ if (rootAllocation < 1)
1452
+ throw new Error("Hacker News capture has no remaining response-byte budget");
1453
+ remainingBytes -= rootAllocation;
1454
+ const root = await fetchJson(endpoint(classified.itemId), rootAllocation, remainingTime());
1455
+ const rootBytes = serializedBytes(root);
1456
+ if (!Number.isFinite(rootBytes) || rootBytes > rootAllocation) {
1457
+ throw new Error("Hacker News root item exceeded its bounded JSON allocation");
1458
+ }
1459
+ remainingBytes += rootAllocation - rootBytes;
1460
+ if (!isRecord2(root) || itemId(root.id) === null)
1461
+ throw new Error("Hacker News API returned no root item");
1462
+ if (options.scope === "page") {
1463
+ const evidence2 = { root, descendants: [] };
1464
+ const parsed2 = parseHackerNewsCapture(evidence2, classified.href, {
1465
+ limits: { maxItems: options.maxItems, maxDepth: options.maxDepth }
1466
+ });
1467
+ if (!parsed2.ok)
1468
+ throw new Error(parsed2.error.message);
1469
+ return structuredCaptureFromDocument(options, parsed2.document, evidence2, "hacker-news-api", []);
1470
+ }
1471
+ const descendants = [];
1472
+ const warnings = [];
1473
+ const scheduled = new Set([classified.itemId]);
1474
+ const queue = [];
1475
+ const initialChildren = enqueueHackerNewsChildren(root, 1, Math.max(0, options.maxItems - 1), queue, scheduled);
1476
+ let duplicateChildren = initialChildren.duplicate;
1477
+ let limited = initialChildren.truncated;
1478
+ while (queue.length > 0 && descendants.length + 1 < options.maxItems) {
1479
+ const remaining = options.maxItems - descendants.length - 1;
1480
+ const batchSize = Math.min(8, remaining, queue.length, remainingBytes);
1481
+ if (batchSize < 1) {
1482
+ limited = true;
1483
+ break;
1484
+ }
1485
+ const batch = queue.splice(0, batchSize);
1486
+ const allocation = Math.min(64 * 1024, Math.floor(remainingBytes / batch.length));
1487
+ remainingBytes -= allocation * batch.length;
1488
+ const fetched = await Promise.all(batch.map(async ({ id, depth }) => {
1489
+ try {
1490
+ const value = await fetchJson(endpoint(id), allocation, remainingTime());
1491
+ const bytes = serializedBytes(value);
1492
+ if (!Number.isFinite(bytes) || bytes > allocation) {
1493
+ return { id, depth, value: null, warning: `Hacker News item ${id} exceeded its bounded JSON allocation.` };
1494
+ }
1495
+ remainingBytes += allocation - bytes;
1496
+ return { id, depth, value, warning: null };
1497
+ } catch (error) {
1498
+ const message = error instanceof Error ? error.message : String(error);
1499
+ return { id, depth, value: null, warning: `Could not fetch Hacker News item ${id}: ${message}` };
1500
+ }
1501
+ }));
1502
+ const childrenToSchedule = [];
1503
+ for (const result of fetched) {
1504
+ if (result.warning !== null)
1505
+ warnings.push(result.warning);
1506
+ if (result.value === null)
1507
+ continue;
1508
+ descendants.push(result.value);
1509
+ childrenToSchedule.push({ value: result.value, depth: result.depth });
1510
+ }
1511
+ const maximumQueueSize = Math.max(0, options.maxItems - descendants.length - 1);
1512
+ for (const result of childrenToSchedule) {
1513
+ const atDepthLimit = result.depth >= options.maxDepth - 1;
1514
+ const enqueued = enqueueHackerNewsChildren(result.value, result.depth + 1, atDepthLimit ? queue.length : maximumQueueSize, queue, scheduled);
1515
+ duplicateChildren = duplicateChildren || enqueued.duplicate;
1516
+ limited = limited || enqueued.truncated;
1517
+ }
1518
+ }
1519
+ if (duplicateChildren)
1520
+ warnings.push("Hacker News duplicate or cyclic child IDs were skipped.");
1521
+ if (queue.length > 0 || limited) {
1522
+ warnings.push("Hacker News descendants exceeded the configured item, depth, byte, or total-deadline limit.");
1523
+ }
1524
+ const evidence = { root, descendants };
1525
+ const parsed = parseHackerNewsCapture(evidence, classified.href, {
1526
+ limits: { maxItems: options.maxItems, maxDepth: options.maxDepth }
1527
+ });
1528
+ if (!parsed.ok)
1529
+ throw new Error(parsed.error.message);
1530
+ const document = {
1531
+ ...parsed.document,
1532
+ warnings: [...parsed.document.warnings, ...warnings]
1533
+ };
1534
+ return structuredCaptureFromDocument(options, document, evidence, "hacker-news-api", warnings);
1535
+ }
1536
+ async function captureBluesky(options, classified, fetchJson) {
1537
+ let did = classified.actor;
1538
+ const evidence = {};
1539
+ if (!did.startsWith("did:")) {
1540
+ const resolveUrl = new URL("https://public.api.bsky.app/xrpc/com.atproto.identity.resolveHandle");
1541
+ resolveUrl.searchParams.set("handle", did);
1542
+ const resolution = await fetchJson(resolveUrl, Math.min(options.maxHtmlBytes, 1024 * 1024));
1543
+ evidence.resolution = resolution;
1544
+ if (!isRecord2(resolution) || typeof resolution.did !== "string" || !resolution.did.startsWith("did:")) {
1545
+ throw new Error(`Bluesky could not resolve ${classified.actor}`);
1546
+ }
1547
+ did = resolution.did;
1548
+ }
1549
+ const threadUrl = new URL("https://public.api.bsky.app/xrpc/app.bsky.feed.getPostThread");
1550
+ threadUrl.searchParams.set("uri", `at://${did}/app.bsky.feed.post/${classified.postId}`);
1551
+ threadUrl.searchParams.set("depth", options.scope === "page" ? "0" : String(Math.min(options.maxDepth, 1000)));
1552
+ threadUrl.searchParams.set("parentHeight", String(Math.min(options.maxDepth, 1000)));
1553
+ const thread = await fetchJson(threadUrl, options.maxHtmlBytes);
1554
+ evidence.thread = thread;
1555
+ const parsed = parseBlueskyCapture(thread, classified.href, {
1556
+ limits: { maxItems: options.maxItems, maxDepth: options.maxDepth }
1557
+ });
1558
+ if (!parsed.ok)
1559
+ throw new Error(parsed.error.message);
1560
+ return structuredCaptureFromDocument(options, parsed.document, evidence, "bluesky-api", []);
1561
+ }
1562
+ function rootOnlyRedditInput(input) {
1563
+ if (!Array.isArray(input))
1564
+ return input;
1565
+ const values = input;
1566
+ const post = values[0];
1567
+ return post === undefined ? values : [post];
1568
+ }
1569
+ function redditHasPagination(input) {
1570
+ if (!Array.isArray(input))
1571
+ return false;
1572
+ const comments = input[1];
1573
+ if (!isRecord2(comments))
1574
+ return false;
1575
+ const data = isRecord2(comments.data) ? comments.data : null;
1576
+ return data !== null && data.after !== undefined && data.after !== null;
1577
+ }
1578
+ async function captureReddit(options, classified, fetchJson) {
1579
+ const endpoint = new URL(`https://www.reddit.com/comments/${encodeURIComponent(classified.postId)}.json`);
1580
+ endpoint.searchParams.set("raw_json", "1");
1581
+ endpoint.searchParams.set("limit", String(Math.max(1, options.maxItems - 1)));
1582
+ endpoint.searchParams.set("depth", String(options.scope === "page" ? 0 : options.maxDepth));
1583
+ if (classified.commentId !== null)
1584
+ endpoint.searchParams.set("comment", classified.commentId);
1585
+ const evidence = await fetchJson(endpoint, options.maxHtmlBytes, options.timeoutMs);
1586
+ const parserInput = options.scope === "page" ? rootOnlyRedditInput(evidence) : evidence;
1587
+ const parsed = parseRedditCapture(parserInput, classified.href, {
1588
+ limits: { maxItems: options.maxItems, maxDepth: options.maxDepth }
1589
+ });
1590
+ if (!parsed.ok)
1591
+ throw new Error(parsed.error.message);
1592
+ const warnings = options.scope !== "page" && redditHasPagination(evidence) ? ["Reddit JSON returned a pagination cursor; additional comments remain uncaptured."] : [];
1593
+ const storedEvidence = options.scope === "page" ? parserInput : evidence;
1594
+ return structuredCaptureFromDocument(options, parsed.document, storedEvidence, "reddit-json", warnings, "reddit-json");
1595
+ }
1596
+ async function acquirePublicStructured(options, dependencies = {}) {
1597
+ const classified = classifyPlatformUrl(captureUrl(options).href);
1598
+ if (classified === null)
1599
+ return null;
1600
+ const fetchJson = dependencies.fetchJson ?? ((url, maxBytes, timeoutMs) => defaultJsonFetcher(options, url, maxBytes, timeoutMs));
1601
+ if (classified.platform === "hacker-news")
1602
+ return captureHackerNews(options, classified, fetchJson);
1603
+ if (classified.platform === "bluesky")
1604
+ return captureBluesky(options, classified, fetchJson);
1605
+ if (classified.platform === "reddit")
1606
+ return captureReddit(options, classified, fetchJson);
1607
+ return null;
1608
+ }
1609
+
1610
+ // src/clip/capture.ts
1611
+ var browserFirstPlatforms = new Set([
1612
+ "x",
1613
+ "instagram",
1614
+ "linkedin",
1615
+ "reddit",
1616
+ "facebook",
1617
+ "tiktok",
1618
+ "threads",
1619
+ "whatsapp",
1620
+ "youtube"
1621
+ ]);
1622
+ function effectiveScope(platform, scope) {
1623
+ if (scope !== "auto")
1624
+ return scope;
1625
+ if (platform === "hacker-news" || platform === "reddit" || platform === "github" || platform === "discourse") {
1626
+ return "comments";
1627
+ }
1628
+ if (platform === "x" || platform === "bluesky")
1629
+ return "thread";
1630
+ return "page";
1631
+ }
1632
+ function stableContentId(url) {
1633
+ const classified = classifyPlatformUrl(url.href);
1634
+ if (classified === null)
1635
+ return null;
1636
+ switch (classified.platform) {
1637
+ case "x":
1638
+ case "bluesky":
1639
+ return classified.postId;
1640
+ case "hacker-news":
1641
+ return classified.itemId;
1642
+ case "reddit":
1643
+ return classified.commentId ?? classified.postId;
1644
+ case "github":
1645
+ return classified.contentId;
1646
+ case "discourse":
1647
+ return classified.topicId;
1648
+ case "instagram":
1649
+ case "linkedin":
1650
+ case "facebook":
1651
+ case "tiktok":
1652
+ case "threads":
1653
+ case "whatsapp":
1654
+ case "youtube":
1655
+ return classified.contentId;
1656
+ case "substack":
1657
+ case "generic":
1658
+ return null;
1659
+ }
1660
+ }
1661
+ function captureSlug(options, extraction) {
1662
+ if (options.slug !== undefined)
1663
+ return slugify(options.slug);
1664
+ const fallback = extraction.canonicalUrl.pathname.split("/").filter(Boolean).at(-1) ?? extraction.canonicalUrl.hostname;
1665
+ const base = slugify(redactSensitiveText(extraction.article.title ?? fallback));
1666
+ const id = stableContentId(extraction.canonicalUrl);
1667
+ if (id === null)
1668
+ return base;
1669
+ const idSlug = slugify(id);
1670
+ if (idSlug === "" || base.endsWith(`-${idSlug}`) || base === idSlug)
1671
+ return base;
1672
+ const available = Math.max(1, 80 - [...idSlug].length - 1);
1673
+ const shortened = [...base].slice(0, available).join("").replace(/-+$/g, "") || "post";
1674
+ return `${shortened}-${idSlug}`;
1675
+ }
1676
+ function shouldUseBrowser(options, platform, scope, directCandidates) {
1677
+ if (options.mode === "browser")
1678
+ return true;
1679
+ if (options.mode !== "auto")
1680
+ return false;
1681
+ if (options.browserProfile !== undefined || options.browserLive || options.cdp !== undefined)
1682
+ return true;
1683
+ if (options.evidence === "screenshot" || options.evidence === "all")
1684
+ return true;
1685
+ if (browserFirstPlatforms.has(platform))
1686
+ return true;
1687
+ if (scope !== "page" && platform !== "hacker-news" && platform !== "bluesky")
1688
+ return true;
1689
+ const boundedStructured = directCandidates.some((candidate) => (candidate.acquisition.method === "hacker-news-api" || candidate.acquisition.method === "bluesky-api") && candidate.warnings.some((warning) => /configured (?:item|depth)|item (?:or depth )?limit|depth limit|capture stopped at \d+ items?/i.test(warning)));
1690
+ if (boundedStructured)
1691
+ return false;
1692
+ const best = chooseBestExtraction(directCandidates);
1693
+ return best === null || best.status !== "complete" || best.wordCount < 60;
1694
+ }
1695
+ function safeAttemptMessage(value) {
1696
+ const message = value instanceof Error ? value.message : String(value);
1697
+ return redactSensitiveText(message).replace(/[\r\n]+/g, " ").slice(0, 1000);
1698
+ }
1699
+ function finalizedWarnings(values, markdownRedactions = 0) {
1700
+ const sanitized = values.map((value) => safeAttemptMessage(value));
1701
+ if (markdownRedactions > 0) {
1702
+ sanitized.push(`Redacted ${markdownRedactions} credential-shaped occurrence${markdownRedactions === 1 ? "" : "s"} from captured Markdown.`);
1703
+ }
1704
+ return [...new Set(sanitized)];
1705
+ }
1706
+ function statusAfterContentRewrite(status, truncated) {
1707
+ return truncated && status === "complete" ? "partial" : status;
1708
+ }
1709
+ function chooseCaptureExtraction(candidates, structuredCapture) {
1710
+ const structured = structuredCapture?.extraction;
1711
+ if (structured !== undefined && (structured.acquisition.method === "hacker-news-api" || structured.acquisition.method === "bluesky-api") && (structured.status === "complete" || structured.status === "partial")) {
1712
+ return structured;
1713
+ }
1714
+ return chooseBestExtraction(candidates);
1715
+ }
1716
+ function authoritativeStructuredExtraction(structuredCapture) {
1717
+ const extraction = structuredCapture?.extraction;
1718
+ return extraction !== undefined && (extraction.acquisition.method === "hacker-news-api" || extraction.acquisition.method === "bluesky-api") && (extraction.status === "complete" || extraction.status === "partial");
1719
+ }
1720
+ function shouldTryArchiveFallback(options, candidates, structuredCapture) {
1721
+ if (options.mode !== "auto" && options.mode !== "http" || options.currentTab)
1722
+ return false;
1723
+ if (authoritativeStructuredExtraction(structuredCapture))
1724
+ return false;
1725
+ const best = chooseBestExtraction(candidates);
1726
+ return best === null || best.status !== "complete" && best.status !== "partial" || best.status === "partial" && best.wordCount < 60 && best.capturedItems === 0;
1727
+ }
1728
+ var archiveBypassHttpStatuses = new Set([401, 402, 403, 407, 423, 429, 451]);
1729
+ function recordArchiveFallbackDenial(denials, extraction) {
1730
+ if (extractionShowsAccessControl(extraction))
1731
+ denials.add("access-control response");
1732
+ }
1733
+ function recordArchiveFallbackErrorDenial(denials, error) {
1734
+ if (error instanceof FetchFailure && error.status !== null && archiveBypassHttpStatuses.has(error.status)) {
1735
+ denials.add("access-control or rate-limit response");
1736
+ }
1737
+ }
1738
+ function archiveDiscoveryAttempt(discovery) {
1739
+ switch (discovery.status) {
1740
+ case "found":
1741
+ return { method: "archive-is-discovery", outcome: "succeeded", message: "found one exact existing snapshot" };
1742
+ case "not-found":
1743
+ return { method: "archive-is-discovery", outcome: "skipped", message: "no exact existing snapshot was found" };
1744
+ case "throttled":
1745
+ return { method: "archive-is-discovery", outcome: "failed", message: "provider throttled the read-only lookup" };
1746
+ case "unavailable":
1747
+ return { method: "archive-is-discovery", outcome: "failed", message: `provider lookup unavailable (${discovery.reason})` };
1748
+ }
1749
+ }
1750
+ async function tryArchiveFallback(options) {
1751
+ let discovery;
1752
+ try {
1753
+ discovery = await options.discover(options.sourceUrl, {
1754
+ now: options.now,
1755
+ timeoutMs: options.captureOptions.timeoutMs,
1756
+ maxBytes: options.captureOptions.maxHtmlBytes,
1757
+ userAgent: options.captureOptions.userAgent
1758
+ });
1759
+ } catch (error) {
1760
+ options.attempts.push({ method: "archive-is-discovery", outcome: "failed", message: safeAttemptMessage(error) });
1761
+ return;
1762
+ }
1763
+ options.attempts.push(archiveDiscoveryAttempt(discovery));
1764
+ if (discovery.status !== "found")
1765
+ return;
1766
+ try {
1767
+ const acquisition = await options.acquire(options.sourceUrl, discovery.snapshot.url, {
1768
+ now: options.now,
1769
+ timeoutMs: options.captureOptions.timeoutMs,
1770
+ maxBytes: options.captureOptions.maxHtmlBytes,
1771
+ userAgent: options.captureOptions.userAgent
1772
+ });
1773
+ const extracted = await options.extractor(acquisition, options.scope, options.captureOptions.timeoutMs);
1774
+ if (extracted === null) {
1775
+ options.attempts.push({ method: "archive-is", outcome: "failed", message: "snapshot yielded no extractable content" });
1776
+ return;
1777
+ }
1778
+ const warning = `Captured an Archive.today snapshot from ${discovery.snapshot.capturedAt}; it may differ from the current source.`;
1779
+ const archivedStatus = extracted.status === "complete" ? "partial" : extracted.status;
1780
+ const archived = {
1781
+ ...extracted,
1782
+ canonicalUrl: new URL(options.sourceUrl),
1783
+ platform: options.platform,
1784
+ status: archivedStatus,
1785
+ score: scoreExtraction(extracted.article, archivedStatus, extracted.wordCount, extracted.capturedItems, acquisition),
1786
+ warnings: Object.freeze([...extracted.warnings, warning]),
1787
+ acquisition
1788
+ };
1789
+ options.candidates.push(archived);
1790
+ options.attempts.push({
1791
+ method: "archive-is",
1792
+ outcome: "succeeded",
1793
+ message: `${archived.status}; ${archived.wordCount} words; ${archived.capturedItems} items`
1794
+ });
1795
+ } catch (error) {
1796
+ options.attempts.push({ method: "archive-is", outcome: "failed", message: safeAttemptMessage(error) });
1797
+ }
1798
+ }
1799
+ async function tryAcquisition(method, acquire, scope, timeoutMs, extractor, candidates, attempts, archiveFallbackDenials) {
1800
+ try {
1801
+ const acquisition = await acquire();
1802
+ const extracted = await extractor(acquisition, scope, timeoutMs);
1803
+ if (extracted === null) {
1804
+ attempts.push({ method, outcome: "failed", message: "acquisition yielded no extractable content" });
1805
+ return acquisition;
1806
+ }
1807
+ candidates.push(extracted);
1808
+ recordArchiveFallbackDenial(archiveFallbackDenials, extracted);
1809
+ attempts.push({
1810
+ method: acquisition.method,
1811
+ outcome: "succeeded",
1812
+ message: `${extracted.status}; ${extracted.wordCount} words; ${extracted.capturedItems} items`
1813
+ });
1814
+ return acquisition;
1815
+ } catch (error) {
1816
+ recordArchiveFallbackErrorDenial(archiveFallbackDenials, error);
1817
+ attempts.push({ method, outcome: "failed", message: safeAttemptMessage(error) });
1818
+ return null;
1819
+ }
1820
+ }
1821
+ function screenshotIntoBundle(screenshotPath, transaction, maxBytes) {
1822
+ if (screenshotPath === null || !existsSync2(screenshotPath))
1823
+ return null;
1824
+ const stats = statSync2(screenshotPath);
1825
+ if (!stats.isFile() || stats.size > maxBytes)
1826
+ return null;
1827
+ const bytes = readFileSync2(screenshotPath);
1828
+ if (sniffImage(bytes)?.mimeType !== "image/png")
1829
+ return null;
1830
+ const evidenceDirectory = join2(transaction.stagingDirectory, "evidence");
1831
+ mkdirSync2(evidenceDirectory, { recursive: true, mode: 448 });
1832
+ const destination = join2(evidenceDirectory, "page.png");
1833
+ copyFileSync(screenshotPath, destination);
1834
+ chmodSync2(destination, 384);
1835
+ return "evidence/page.png";
1836
+ }
1837
+ var mediaRecordManifestAsset = (record, sourceUrl) => ({
1838
+ source: sourceUrl,
1839
+ url: sourceUrl,
1840
+ path: record.path,
1841
+ mimeType: record.mimeType,
1842
+ bytes: record.bytes,
1843
+ sha256: record.sha256
1844
+ });
1845
+ var mediaManifestAssets = (result, sourceUrl) => result.records.map((record) => mediaRecordManifestAsset(record, sourceUrl));
1846
+ function safeMediaPath(path) {
1847
+ if (path === "" || path.startsWith("/") || /[\\\0\r\n?#]/.test(path))
1848
+ return null;
1849
+ const pieces = path.split("/");
1850
+ if (pieces.some((piece) => piece === "" || piece === "." || piece === ".."))
1851
+ return null;
1852
+ return pieces.map((piece) => encodeURIComponent(piece)).join("/");
1853
+ }
1854
+ function appendCapturedMedia(content, records) {
1855
+ const lines = [];
1856
+ for (const record of records) {
1857
+ const path = safeMediaPath(record.path);
1858
+ if (path === null)
1859
+ continue;
1860
+ if (record.mimeType.startsWith("video/")) {
1861
+ lines.push(`<video controls preload="metadata" src="${path}"></video>`, `[Download video](${path})`);
1862
+ } else if (record.mimeType.startsWith("audio/")) {
1863
+ lines.push(`<audio controls preload="metadata" src="${path}"></audio>`, `[Download audio](${path})`);
1864
+ } else
1865
+ lines.push(`[Download media](${path})`);
1866
+ }
1867
+ if (lines.length === 0)
1868
+ return content;
1869
+ return `${content.trimEnd()}
1870
+
1871
+ ## Media
1872
+
1873
+ ${lines.join(`
1874
+
1875
+ `)}
1876
+ `;
1877
+ }
1878
+ function escapedVideoMetadata(value) {
1879
+ return value.replace(/\\/gu, "\\\\").replace(/([`*_[\]<>#])/gu, "\\$1");
1880
+ }
1881
+ function videoDuration(value) {
1882
+ const totalSeconds = Math.max(0, Math.floor(value));
1883
+ const hours = Math.floor(totalSeconds / 3600);
1884
+ const minutes = Math.floor(totalSeconds % 3600 / 60);
1885
+ const seconds = totalSeconds % 60;
1886
+ return hours > 0 ? `${String(hours).padStart(2, "0")}:${String(minutes).padStart(2, "0")}:${String(seconds).padStart(2, "0")}` : `${String(minutes).padStart(2, "0")}:${String(seconds).padStart(2, "0")}`;
1887
+ }
1888
+ function appendVideoContext(content, context) {
1889
+ const metadata = context.metadata;
1890
+ const details = [];
1891
+ if (metadata?.title !== undefined)
1892
+ details.push(`- **Title:** ${escapedVideoMetadata(metadata.title)}`);
1893
+ const channel = metadata?.channel ?? metadata?.uploader;
1894
+ if (channel !== undefined)
1895
+ details.push(`- **Channel:** ${escapedVideoMetadata(channel)}`);
1896
+ if (metadata?.durationSeconds !== undefined) {
1897
+ details.push(`- **Duration:** ${videoDuration(metadata.durationSeconds)}`);
1898
+ }
1899
+ if (metadata?.id !== undefined)
1900
+ details.push(`- **Video ID:** ${escapedVideoMetadata(metadata.id)}`);
1901
+ const thumbnailPath = context.thumbnail === null ? null : safeMediaPath(context.thumbnail.path);
1902
+ const sections = [];
1903
+ if (details.length > 0 || thumbnailPath !== null || metadata?.description !== undefined) {
1904
+ const video = ["## Video"];
1905
+ if (details.length > 0)
1906
+ video.push("", details.join(`
1907
+ `));
1908
+ if (thumbnailPath !== null)
1909
+ video.push("", `![Video thumbnail](${thumbnailPath})`);
1910
+ if (metadata?.description !== undefined && metadata.description.trim() !== "") {
1911
+ const description = metadata.description.split(/\r?\n/gu).map(escapedVideoMetadata).join(`
1912
+ `);
1913
+ video.push("", "### Description", "", description);
1914
+ }
1915
+ sections.push(video.join(`
1916
+ `));
1917
+ }
1918
+ if (context.transcript !== null && context.transcript.markdown.trim() !== "") {
1919
+ sections.push(`## Transcript
1920
+
1921
+ ${context.transcript.markdown.trimEnd()}`);
1922
+ }
1923
+ return sections.length === 0 ? content : `${content.trimEnd()}
1924
+
1925
+ ${sections.join(`
1926
+
1927
+ `)}
1928
+ `;
1929
+ }
1930
+ function articleWithVideoMetadata(article, metadata) {
1931
+ if (metadata === null)
1932
+ return article;
1933
+ const published = metadata.timestamp === undefined ? article.published : (() => {
1934
+ const date = new Date(metadata.timestamp * 1000);
1935
+ return Number.isFinite(date.getTime()) ? date.toISOString() : article.published;
1936
+ })();
1937
+ return {
1938
+ content: article.content,
1939
+ title: metadata.title ?? article.title,
1940
+ author: metadata.channel ?? metadata.uploader ?? article.author,
1941
+ published,
1942
+ description: metadata.description ?? article.description
1943
+ };
1944
+ }
1945
+ function cookieMediaOptions(options) {
1946
+ const source = options.cookieSources[0] ?? (options.browserProfile === undefined ? undefined : "chrome");
1947
+ const profile = options.cookieSources.length > 0 ? options.cookieProfile : selectedBrowserCookieProfile(options);
1948
+ return {
1949
+ ...source === undefined ? {} : { cookieBrowser: { source, ...profile === undefined ? {} : { profile } } },
1950
+ ...options.cookiesFile === undefined ? {} : { cookiesFile: options.cookiesFile }
1951
+ };
1952
+ }
1953
+ function selectedBrowserCookieProfile(options) {
1954
+ if (options.browserProfile === undefined)
1955
+ return;
1956
+ return options.browserProfileDirectory === undefined ? options.browserProfile : join2(options.browserProfile, options.browserProfileDirectory);
1957
+ }
1958
+ function assetCookieProvider(options, reader, authorizedUrl) {
1959
+ const explicit = options.cookieSources.length > 0 || options.cookiesFile !== undefined;
1960
+ if (!explicit && options.browserProfile === undefined)
1961
+ return;
1962
+ const effective = explicit ? options : {
1963
+ ...options,
1964
+ cookieSources: ["chrome"],
1965
+ cookieProfile: selectedBrowserCookieProfile(options)
1966
+ };
1967
+ let records;
1968
+ return (url) => {
1969
+ if (url.origin !== authorizedUrl.origin)
1970
+ return Promise.resolve(null);
1971
+ records ??= reader(effective, authorizedUrl);
1972
+ return records.then((result) => {
1973
+ const header = renderCookieHeader(filterCookies(result.cookies, url).cookies);
1974
+ return header === "" ? null : header;
1975
+ });
1976
+ };
1977
+ }
1978
+ function withBrowserProfileSnapshot(options, temporaryDirectory) {
1979
+ if (options.browserProfile === undefined || options.browserProfileOwnership === "owned")
1980
+ return options;
1981
+ const source = assertSafePersistentProfile(options);
1982
+ if (source === null)
1983
+ return options;
1984
+ const cloned = cloneBrowserProfile(source, temporaryDirectory);
1985
+ return {
1986
+ ...options,
1987
+ browserProfile: cloned.userDataPath,
1988
+ browserProfileOwnership: "owned",
1989
+ ...cloned.profileDirectory === undefined ? {} : { browserProfileDirectory: cloned.profileDirectory }
1990
+ };
1991
+ }
1992
+ async function runCapture(rawOptions, dependencies = {}) {
1993
+ if (rawOptions.stdout && (rawOptions.media !== "none" || rawOptions.evidence !== "none")) {
1994
+ throw new Error("stdout capture cannot request persisted media or evidence");
1995
+ }
1996
+ const deps = {
1997
+ acquireFile: dependencies.acquireFile ?? acquireFile,
1998
+ acquireHttp: dependencies.acquireHttp ?? acquireHttp,
1999
+ acquireCookieHttp: dependencies.acquireCookieHttp ?? acquireCookieHttp,
2000
+ acquireCookieRecords: dependencies.acquireCookieRecords ?? acquireCookieRecords,
2001
+ acquireBrowser: dependencies.acquireBrowser ?? acquireBrowser,
2002
+ acquirePublicStructured: dependencies.acquirePublicStructured ?? acquirePublicStructured,
2003
+ discoverArchiveTodaySnapshot: dependencies.discoverArchiveTodaySnapshot ?? discoverArchiveTodaySnapshot,
2004
+ acquireArchiveTodaySnapshot: dependencies.acquireArchiveTodaySnapshot ?? acquireArchiveTodaySnapshot,
2005
+ extractPage: dependencies.extractPage ?? extractPage,
2006
+ localizeAssets: dependencies.localizeAssets ?? localizeAssets,
2007
+ captureMedia: dependencies.captureMedia ?? captureMedia,
2008
+ captureVideoContext: dependencies.captureVideoContext ?? captureVideoContext,
2009
+ now: dependencies.now ?? (() => new Date)
2010
+ };
2011
+ const browserTemporaryDirectory = mkdtempSync2(join2(tmpdir2(), "hraness-kb-browser-"));
2012
+ chmodSync2(browserTemporaryDirectory, 448);
2013
+ try {
2014
+ const preparedOptions = withBrowserProfileSnapshot(rawOptions, browserTemporaryDirectory);
2015
+ let currentBrowser = null;
2016
+ let resolvedOptions = preparedOptions;
2017
+ if (preparedOptions.currentTab) {
2018
+ try {
2019
+ const acquired = await deps.acquireBrowser(preparedOptions, browserTemporaryDirectory, false);
2020
+ const sanitizedUrl = new URL(sanitizeArtifactUrl(acquired.finalUrl.href));
2021
+ currentBrowser = sanitizedUrl.href === acquired.finalUrl.href ? acquired : { ...acquired, finalUrl: sanitizedUrl };
2022
+ } catch (error) {
2023
+ throw new Error(`current browser acquisition failed: ${safeAttemptMessage(error)}`, { cause: error });
2024
+ }
2025
+ resolvedOptions = { ...preparedOptions, url: currentBrowser.finalUrl, mode: "browser" };
2026
+ }
2027
+ const requestedUrl = captureUrl(resolvedOptions);
2028
+ const platform = classifyPlatformUrl(requestedUrl.href)?.platform ?? "generic";
2029
+ const sourceUrl = canonicalizeUrl(requestedUrl, platform).href;
2030
+ const scope = effectiveScope(platform, resolvedOptions.scope);
2031
+ const options = { ...resolvedOptions, scope };
2032
+ const candidates = [];
2033
+ const attempts = [];
2034
+ const archiveFallbackDenials = new Set;
2035
+ const browserOperationalWarnings = [];
2036
+ let structuredCapture = null;
2037
+ const browserScreenshots = new Map;
2038
+ const eagerBrowserCandidates = [];
2039
+ const eagerBrowserAttempts = [];
2040
+ const eagerBrowserRequested = options.mode === "auto" && browserFirstPlatforms.has(platform);
2041
+ if (eagerBrowserRequested && (options.browserLive || options.cdp !== undefined)) {
2042
+ browserOperationalWarnings.push("An attached browser attempt may have navigated and scrolled the active tab even if that candidate was not selected.");
2043
+ } else if (eagerBrowserRequested && options.browserProfile !== undefined && options.browserProfileOwnership !== "owned") {
2044
+ browserOperationalWarnings.push("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.");
2045
+ }
2046
+ const eagerBrowser = eagerBrowserRequested ? tryAcquisition(options.browserLive ? "browser-live" : options.cdp === undefined ? "browser" : "browser-cdp", () => deps.acquireBrowser(options, browserTemporaryDirectory, false), scope, options.timeoutMs, deps.extractPage, eagerBrowserCandidates, eagerBrowserAttempts, archiveFallbackDenials) : null;
2047
+ if (options.currentTab) {
2048
+ if (currentBrowser === null)
2049
+ throw new Error("current browser acquisition did not produce a page");
2050
+ const browser = await tryAcquisition(options.browserLive ? "browser-live-current" : "browser-cdp-current", () => Promise.resolve(currentBrowser), scope, options.timeoutMs, deps.extractPage, candidates, attempts, archiveFallbackDenials);
2051
+ if (browser !== null)
2052
+ browserOperationalWarnings.push(...browser.warnings);
2053
+ if (browser?.screenshotPath !== undefined)
2054
+ browserScreenshots.set(browser, browser.screenshotPath);
2055
+ } else if (options.mode === "file") {
2056
+ await tryAcquisition("file", () => deps.acquireFile(options), scope, options.timeoutMs, deps.extractPage, candidates, attempts, archiveFallbackDenials);
2057
+ } else {
2058
+ if (options.mode === "auto" || options.mode === "http") {
2059
+ try {
2060
+ structuredCapture = await deps.acquirePublicStructured(options);
2061
+ if (structuredCapture !== null) {
2062
+ candidates.push(structuredCapture.extraction);
2063
+ recordArchiveFallbackDenial(archiveFallbackDenials, structuredCapture.extraction);
2064
+ attempts.push({
2065
+ method: structuredCapture.extraction.acquisition.method,
2066
+ outcome: "succeeded",
2067
+ message: `${structuredCapture.extraction.status}; ${structuredCapture.extraction.capturedItems} items`
2068
+ });
2069
+ } else {
2070
+ attempts.push({ method: "public-api", outcome: "skipped", message: "no stable public structured adapter" });
2071
+ }
2072
+ } catch (error) {
2073
+ recordArchiveFallbackErrorDenial(archiveFallbackDenials, error);
2074
+ attempts.push({ method: "public-api", outcome: "failed", message: safeAttemptMessage(error) });
2075
+ }
2076
+ await tryAcquisition("http", () => deps.acquireHttp(options), scope, options.timeoutMs, deps.extractPage, candidates, attempts, archiveFallbackDenials);
2077
+ if (options.cookieSources.length > 0 || options.cookiesFile !== undefined) {
2078
+ await tryAcquisition("cookie-http", () => deps.acquireCookieHttp(options), scope, options.timeoutMs, deps.extractPage, candidates, attempts, archiveFallbackDenials);
2079
+ }
2080
+ }
2081
+ if (eagerBrowser !== null) {
2082
+ const browser = await eagerBrowser;
2083
+ candidates.push(...eagerBrowserCandidates);
2084
+ attempts.push(...eagerBrowserAttempts);
2085
+ if (browser !== null)
2086
+ browserOperationalWarnings.push(...browser.warnings);
2087
+ if (browser?.screenshotPath !== undefined)
2088
+ browserScreenshots.set(browser, browser.screenshotPath);
2089
+ } else if (shouldUseBrowser(options, platform, scope, candidates)) {
2090
+ if (options.browserLive || options.cdp !== undefined) {
2091
+ browserOperationalWarnings.push("An attached browser attempt may have navigated and scrolled the active tab even if that candidate was not selected.");
2092
+ } else if (options.browserProfile !== undefined && options.browserProfileOwnership !== "owned") {
2093
+ browserOperationalWarnings.push("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.");
2094
+ }
2095
+ const browser = await tryAcquisition(options.browserLive ? "browser-live" : options.cdp === undefined ? "browser" : "browser-cdp", () => deps.acquireBrowser(options, browserTemporaryDirectory, false), scope, options.timeoutMs, deps.extractPage, candidates, attempts, archiveFallbackDenials);
2096
+ if (browser !== null)
2097
+ browserOperationalWarnings.push(...browser.warnings);
2098
+ if (browser?.screenshotPath !== undefined)
2099
+ browserScreenshots.set(browser, browser.screenshotPath);
2100
+ }
2101
+ }
2102
+ if (shouldTryArchiveFallback(options, candidates, structuredCapture)) {
2103
+ if (archiveFallbackDenials.size > 0) {
2104
+ attempts.push({
2105
+ method: "archive-is-discovery",
2106
+ outcome: "skipped",
2107
+ message: `archive fallback disabled after ${[...archiveFallbackDenials].sort().join(" and ")}`
2108
+ });
2109
+ } else {
2110
+ await tryArchiveFallback({
2111
+ sourceUrl,
2112
+ platform,
2113
+ scope,
2114
+ captureOptions: options,
2115
+ extractor: deps.extractPage,
2116
+ discover: deps.discoverArchiveTodaySnapshot,
2117
+ acquire: deps.acquireArchiveTodaySnapshot,
2118
+ now: deps.now,
2119
+ candidates,
2120
+ attempts
2121
+ });
2122
+ }
2123
+ }
2124
+ const best = chooseCaptureExtraction(candidates, structuredCapture);
2125
+ if (best === null) {
2126
+ const details = attempts.filter(({ outcome }) => outcome === "failed").map(({ method, message }) => `${method}: ${message}`);
2127
+ throw new Error(`no acquisition produced usable content${details.length === 0 ? "" : ` (${details.join("; ")})`}`);
2128
+ }
2129
+ const slug = captureSlug(options, best);
2130
+ if (slug === "") {
2131
+ throw new Error(options.slug === undefined ? "could not derive a safe slug; pass one after the URL" : `slug ${JSON.stringify(options.slug)} contains no letters or digits`);
2132
+ }
2133
+ const capturedAt = deps.now().toISOString();
2134
+ const attemptWarnings = attempts.filter(({ outcome }) => outcome === "failed").map(({ method, message }) => `${method} attempt failed: ${message}`);
2135
+ const warnings = [...new Set([...best.warnings, ...browserOperationalWarnings, ...attemptWarnings])];
2136
+ if (options.stdout) {
2137
+ const rewritten = rewriteContentWithStatus(best.article.content, best.canonicalUrl, new Map);
2138
+ const status = statusAfterContentRewrite(best.status, rewritten.truncated);
2139
+ const redactedMarkdown = redactSensitiveTextWithCount(buildClipMarkdown(best.article, {
2140
+ slug,
2141
+ sourceHref: best.canonicalUrl.href,
2142
+ clipped: capturedAt.slice(0, 10),
2143
+ content: rewritten.content,
2144
+ platform: best.platform,
2145
+ captureStatus: status,
2146
+ captureMethod: best.acquisition.method,
2147
+ captureScope: scope
2148
+ }));
2149
+ const wordCount = countWords(redactedMarkdown.text);
2150
+ return {
2151
+ status,
2152
+ sourceUrl,
2153
+ canonicalUrl: best.canonicalUrl.href,
2154
+ platform: best.platform,
2155
+ scope,
2156
+ slug,
2157
+ acquisitionMethod: best.acquisition.method,
2158
+ extractor: best.extractor,
2159
+ wordCount,
2160
+ capturedItems: best.capturedItems,
2161
+ expectedItems: best.expectedItems,
2162
+ outputDirectory: null,
2163
+ markdownPath: null,
2164
+ assetCount: 0,
2165
+ warnings: finalizedWarnings([
2166
+ ...warnings,
2167
+ ...rewritten.truncated ? [CONTENT_REWRITE_TRUNCATION_WARNING] : []
2168
+ ], redactedMarkdown.count),
2169
+ attempts,
2170
+ markdown: redactedMarkdown.text,
2171
+ manifest: null
2172
+ };
2173
+ }
2174
+ const transaction = beginCaptureBundle({ outputRoot: options.outputBase, slug, force: options.force });
2175
+ try {
2176
+ const imageCookieProvider = assetCookieProvider(options, deps.acquireCookieRecords, best.canonicalUrl);
2177
+ const localized = options.media === "none" ? {
2178
+ ...rewriteContentWithStatus(best.article.content, best.canonicalUrl, new Map),
2179
+ assets: [],
2180
+ warnings: []
2181
+ } : await deps.localizeAssets(best.article.content, {
2182
+ assetsDirectory: transaction.assetsDirectory,
2183
+ baseUrl: best.canonicalUrl,
2184
+ userAgent: options.userAgent,
2185
+ timeoutMs: options.timeoutMs,
2186
+ maxAssetBytes: options.maxAssetBytes,
2187
+ maxTotalAssetBytes: options.maxTotalAssetBytes,
2188
+ allowPrivateNetwork: options.allowPrivateNetwork,
2189
+ ...imageCookieProvider === undefined ? {} : { cookieHeaderProvider: imageCookieProvider }
2190
+ });
2191
+ const combinedWarnings = [
2192
+ ...warnings,
2193
+ ...localized.warnings,
2194
+ ...localized.truncated ? [CONTENT_REWRITE_TRUNCATION_WARNING] : []
2195
+ ];
2196
+ const status = statusAfterContentRewrite(best.status, localized.truncated);
2197
+ const manifestAssets = [...localized.assets];
2198
+ let videoContext = null;
2199
+ let videoContextStatus = "not-requested";
2200
+ const videoContextRequested = best.platform === "youtube" && options.media !== "none";
2201
+ if (videoContextRequested) {
2202
+ const usedBytes = manifestAssets.reduce((sum, asset) => sum + asset.bytes, 0);
2203
+ const remainingBytes = options.maxTotalAssetBytes - usedBytes;
2204
+ if (remainingBytes < 1) {
2205
+ videoContextStatus = "partial";
2206
+ combinedWarnings.push("Skipped YouTube thumbnail and transcript because the configured asset byte budget was exhausted.");
2207
+ } else {
2208
+ videoContext = await deps.captureVideoContext({
2209
+ url: best.canonicalUrl,
2210
+ outputDirectory: join2(transaction.assetsDirectory, "video"),
2211
+ relativePrefix: "assets/video",
2212
+ timeoutMs: options.timeoutMs,
2213
+ maxFileBytes: Math.min(options.maxAssetBytes, remainingBytes),
2214
+ maxTotalBytes: remainingBytes,
2215
+ allowPrivateNetwork: options.allowPrivateNetwork,
2216
+ maxFiles: Math.max(3, Math.min(options.maxItems, 12)),
2217
+ userAgent: options.userAgent,
2218
+ ...cookieMediaOptions(options)
2219
+ });
2220
+ videoContextStatus = videoContext.status === "captured" && videoContext.warnings.length > 0 ? "partial" : videoContext.status;
2221
+ if (videoContext.thumbnail !== null) {
2222
+ manifestAssets.push(mediaRecordManifestAsset(videoContext.thumbnail, best.canonicalUrl.href));
2223
+ }
2224
+ combinedWarnings.push(...videoContext.warnings);
2225
+ }
2226
+ }
2227
+ let mediaRecords = [];
2228
+ let mediaStatus = "not-requested";
2229
+ if (options.media === "all") {
2230
+ const usedBytes = manifestAssets.reduce((sum, asset) => sum + asset.bytes, 0);
2231
+ const remainingBytes = options.maxTotalAssetBytes - usedBytes;
2232
+ if (remainingBytes < 1) {
2233
+ mediaStatus = "partial";
2234
+ combinedWarnings.push("Skipped full media because the configured asset byte budget was exhausted.");
2235
+ } else {
2236
+ const media = await deps.captureMedia({
2237
+ url: best.canonicalUrl,
2238
+ outputDirectory: join2(transaction.assetsDirectory, "media"),
2239
+ relativePrefix: "assets/media",
2240
+ timeoutMs: options.timeoutMs,
2241
+ maxFileBytes: Math.min(options.maxAssetBytes, remainingBytes),
2242
+ maxTotalBytes: remainingBytes,
2243
+ allowPrivateNetwork: options.allowPrivateNetwork,
2244
+ maxFiles: Math.min(options.maxItems, 100),
2245
+ userAgent: options.userAgent,
2246
+ ...cookieMediaOptions(options)
2247
+ });
2248
+ mediaStatus = media.status === "captured" && media.warnings.length > 0 ? "partial" : media.status;
2249
+ mediaRecords = media.records;
2250
+ manifestAssets.push(...mediaManifestAssets(media, best.canonicalUrl.href));
2251
+ combinedWarnings.push(...media.warnings);
2252
+ }
2253
+ }
2254
+ const requestedScreenshot = options.evidence === "screenshot" || options.evidence === "all";
2255
+ const selectedScreenshot = browserScreenshots.get(best.acquisition) ?? null;
2256
+ const screenshotPath = requestedScreenshot ? screenshotIntoBundle(selectedScreenshot, transaction, options.maxAssetBytes) : null;
2257
+ if (requestedScreenshot && screenshotPath === null) {
2258
+ combinedWarnings.push(browserScreenshots.size > 0 ? "A screenshot was captured for a different acquisition candidate, so it was not attached to the selected content." : "A screenshot was requested but no valid bounded PNG was captured.");
2259
+ }
2260
+ const article = articleWithVideoMetadata(best.article, videoContext?.metadata ?? null);
2261
+ const contentWithVideo = videoContext === null ? localized.content : appendVideoContext(localized.content, videoContext);
2262
+ const redactedMarkdown = redactSensitiveTextWithCount(buildClipMarkdown(article, {
2263
+ slug,
2264
+ sourceHref: best.canonicalUrl.href,
2265
+ clipped: capturedAt.slice(0, 10),
2266
+ content: appendCapturedMedia(contentWithVideo, mediaRecords),
2267
+ platform: best.platform,
2268
+ captureStatus: status,
2269
+ captureMethod: best.acquisition.method,
2270
+ captureScope: scope
2271
+ }));
2272
+ const wordCount = countWords(redactedMarkdown.text);
2273
+ const finalWarnings = finalizedWarnings(combinedWarnings, redactedMarkdown.count);
2274
+ const includeSource = options.evidence === "source" || options.evidence === "all";
2275
+ const manifestInput = {
2276
+ sourceUrl,
2277
+ canonicalUrl: best.canonicalUrl.href,
2278
+ capturedAt,
2279
+ platform: best.platform,
2280
+ status,
2281
+ scope,
2282
+ acquisition: {
2283
+ method: best.acquisition.method,
2284
+ finalUrl: best.acquisition.method === "archive-is" ? sanitizeArtifactUrl(best.acquisition.finalUrl.href) : canonicalizeUrl(best.acquisition.finalUrl, best.platform).href,
2285
+ contentType: best.acquisition.contentType
2286
+ },
2287
+ extraction: {
2288
+ extractor: best.extractor,
2289
+ score: best.score,
2290
+ wordCount,
2291
+ capturedItems: best.capturedItems,
2292
+ expectedItems: best.expectedItems
2293
+ },
2294
+ attempts,
2295
+ assets: manifestAssets,
2296
+ artifacts: {
2297
+ images: {
2298
+ requested: options.media !== "none",
2299
+ status: options.media === "none" ? "not-requested" : localized.truncated || localized.warnings.length > 0 ? "partial" : "captured",
2300
+ files: localized.assets.length
2301
+ },
2302
+ media: {
2303
+ requested: options.media === "all",
2304
+ status: mediaStatus,
2305
+ files: mediaRecords.length
2306
+ },
2307
+ videoContext: {
2308
+ requested: videoContextRequested,
2309
+ status: videoContextStatus,
2310
+ thumbnailPath: videoContext?.thumbnail?.path ?? null,
2311
+ transcriptLanguage: videoContext?.transcript?.language ?? null,
2312
+ transcriptCueCount: videoContext?.transcript?.cueCount ?? 0,
2313
+ transcriptTruncated: videoContext?.transcript?.truncated ?? false,
2314
+ metadata: videoContext?.metadata ?? null
2315
+ }
2316
+ },
2317
+ evidence: {
2318
+ requested: options.evidence,
2319
+ screenshotPath,
2320
+ screenshotStatus: requestedScreenshot ? screenshotPath === null ? "unavailable" : "captured" : "not-requested",
2321
+ sourceHtmlStatus: includeSource ? "captured" : "not-requested"
2322
+ },
2323
+ warnings: finalWarnings
2324
+ };
2325
+ const manifest = writeCaptureBundle(transaction, {
2326
+ markdown: redactedMarkdown.text,
2327
+ manifest: manifestInput,
2328
+ ...includeSource ? { sourceHtml: best.acquisition.sourceEvidence ?? best.acquisition.body } : {}
2329
+ });
2330
+ const outputDirectory = commitCaptureBundle(transaction);
2331
+ return {
2332
+ status,
2333
+ sourceUrl,
2334
+ canonicalUrl: best.canonicalUrl.href,
2335
+ platform: best.platform,
2336
+ scope,
2337
+ slug,
2338
+ acquisitionMethod: best.acquisition.method,
2339
+ extractor: best.extractor,
2340
+ wordCount,
2341
+ capturedItems: best.capturedItems,
2342
+ expectedItems: best.expectedItems,
2343
+ outputDirectory,
2344
+ markdownPath: join2(outputDirectory, `${slug}.md`),
2345
+ assetCount: manifestAssets.length,
2346
+ warnings: finalWarnings,
2347
+ attempts,
2348
+ markdown: redactedMarkdown.text,
2349
+ manifest
2350
+ };
2351
+ } catch (error) {
2352
+ abortCaptureBundle(transaction);
2353
+ throw error;
2354
+ }
2355
+ } finally {
2356
+ rmSync2(browserTemporaryDirectory, { recursive: true, force: true });
2357
+ }
2358
+ }
2359
+
2360
+ // src/clip/cli.ts
2361
+ var defaultOutput = {
2362
+ stdout: (value) => process.stdout.write(value),
2363
+ stderr: (value) => process.stderr.write(value)
2364
+ };
2365
+ function line(value) {
2366
+ return value.endsWith(`
2367
+ `) ? value : `${value}
2368
+ `;
2369
+ }
2370
+ function safe(value) {
2371
+ return sanitizeTerminalLine(redactSensitiveText(value));
2372
+ }
2373
+ function redacted(value) {
2374
+ return redactSensitiveText(value);
2375
+ }
2376
+ function terminalSafeJson(value) {
2377
+ return `${JSON.stringify(value, (_key, candidate) => typeof candidate === "string" ? sanitizeTerminalText(candidate) : candidate, 2)}
2378
+ `;
2379
+ }
2380
+ function captureSummary(outcome) {
2381
+ return {
2382
+ ok: captureSucceeded(outcome),
2383
+ status: outcome.status,
2384
+ sourceUrl: redacted(outcome.sourceUrl),
2385
+ canonicalUrl: redacted(outcome.canonicalUrl),
2386
+ platform: outcome.platform,
2387
+ scope: outcome.scope,
2388
+ slug: outcome.slug,
2389
+ acquisitionMethod: outcome.acquisitionMethod,
2390
+ extractor: outcome.extractor,
2391
+ wordCount: outcome.wordCount,
2392
+ capturedItems: outcome.capturedItems,
2393
+ expectedItems: outcome.expectedItems,
2394
+ outputDirectory: outcome.outputDirectory,
2395
+ markdownPath: outcome.markdownPath,
2396
+ assetCount: outcome.assetCount,
2397
+ warnings: outcome.warnings.map((warning) => redacted(warning)),
2398
+ attempts: outcome.attempts.map((attempt) => ({ ...attempt, message: redacted(attempt.message) })),
2399
+ manifest: outcome.manifest
2400
+ };
2401
+ }
2402
+ function captureSucceeded(outcome) {
2403
+ return outcome.status === "complete" || outcome.status === "partial";
2404
+ }
2405
+ function captureExitCode(outcome) {
2406
+ return captureSucceeded(outcome) ? 0 : 3;
2407
+ }
2408
+ async function diagnosticCommand(arguments_, output, inspectEnvironment) {
2409
+ const report = await inspectEnvironment();
2410
+ output.stdout(arguments_.json ? terminalSafeJson(report) : sanitizeTerminalText(renderDoctorReport(report)));
2411
+ const requiredReady = report.bun.status === "ready" && report.dependencies.every(({ status }) => status === "ready");
2412
+ return requiredReady ? 0 : 4;
2413
+ }
2414
+ async function main(rawArguments = process.argv.slice(2), environment = process.env, output = defaultOutput, dependencies = {}, runtimeOptions = {}) {
2415
+ const parsed = parseArguments(rawArguments, environment);
2416
+ if (!parsed.ok) {
2417
+ output.stderr(`error: ${safe(parsed.message)}
2418
+
2419
+ ${sanitizeTerminalText(usage)}`);
2420
+ return 2;
2421
+ }
2422
+ const arguments_ = parsed.value;
2423
+ if (arguments_.command === "help") {
2424
+ output.stdout(sanitizeTerminalText(usage));
2425
+ return 0;
2426
+ }
2427
+ if (arguments_.command === "doctor") {
2428
+ return diagnosticCommand(arguments_, output, dependencies.inspectClipEnvironment ?? inspectClipEnvironment);
2429
+ }
2430
+ if (arguments_.command === "adapters") {
2431
+ output.stdout(arguments_.json ? terminalSafeJson({ schemaVersion: 1, adapters: adapterCapabilities }) : sanitizeTerminalText(renderAdapterCapabilities()));
2432
+ return 0;
2433
+ }
2434
+ if (!arguments_.quiet && !arguments_.json) {
2435
+ const target = arguments_.currentTab ? "the current browser tab" : safe(arguments_.url?.href ?? "current");
2436
+ output.stderr(`Capturing ${target} (${arguments_.mode}, ${arguments_.scope}) ...
2437
+ `);
2438
+ }
2439
+ try {
2440
+ if (runtimeOptions.ownedBrowserProfile !== undefined && arguments_.browserProfile !== runtimeOptions.ownedBrowserProfile.path) {
2441
+ throw new Error("owned browser-profile execution does not match the selected private profile path");
2442
+ }
2443
+ const captureArguments = runtimeOptions.ownedBrowserProfile === undefined ? {
2444
+ ...arguments_,
2445
+ ...runtimeOptions.browserExecutable === undefined ? {} : { browserExecutable: runtimeOptions.browserExecutable }
2446
+ } : {
2447
+ ...arguments_,
2448
+ browserProfileOwnership: "owned",
2449
+ ...runtimeOptions.browserExecutable === undefined ? {} : { browserExecutable: runtimeOptions.browserExecutable },
2450
+ ...runtimeOptions.ownedBrowserProfile.profileDirectory === undefined ? {} : { browserProfileDirectory: runtimeOptions.ownedBrowserProfile.profileDirectory }
2451
+ };
2452
+ const outcome = await (dependencies.runCapture ?? runCapture)(captureArguments);
2453
+ if (arguments_.json) {
2454
+ output.stdout(terminalSafeJson(captureSummary(outcome)));
2455
+ } else if (arguments_.stdout) {
2456
+ output.stdout(sanitizeTerminalText(outcome.markdown));
2457
+ } else {
2458
+ output.stdout(line(safe(`Done: ${outcome.markdownPath ?? outcome.outputDirectory ?? outcome.slug}`)));
2459
+ output.stdout(line(safe(`Status: ${outcome.status}; ${outcome.wordCount} words; ${outcome.capturedItems}${outcome.expectedItems === null ? "" : `/${outcome.expectedItems}`} items; ${outcome.assetCount} assets.`)));
2460
+ }
2461
+ if (!arguments_.quiet && outcome.warnings.length > 0) {
2462
+ for (const warning of outcome.warnings)
2463
+ output.stderr(`warning: ${safe(warning)}
2464
+ `);
2465
+ }
2466
+ return captureExitCode(outcome);
2467
+ } catch (error) {
2468
+ const message = safe(error instanceof Error ? error.message : String(error));
2469
+ if (arguments_.json)
2470
+ output.stdout(terminalSafeJson({ ok: false, error: message }));
2471
+ else
2472
+ output.stderr(`error: ${message}
2473
+ `);
2474
+ return 1;
2475
+ }
2476
+ }
2477
+ if (import.meta.main)
2478
+ process.exitCode = await main();
2479
+
2480
+ export { runCapture, captureSummary, captureSucceeded, captureExitCode, main };