@hraness/wordcell 0.20.0-bootstrap.1
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/LICENSE +21 -0
- package/README.md +749 -0
- package/dist/agent-context.js +35 -0
- package/dist/agent-guide-audit.js +18 -0
- package/dist/attachments.js +19 -0
- package/dist/authoring.js +33 -0
- package/dist/benchmark.js +19 -0
- package/dist/capture.js +49 -0
- package/dist/cli.js +57 -0
- package/dist/clip/acquire.js +57 -0
- package/dist/clip/args.js +21 -0
- package/dist/clip/bounded-byte-buffer.js +9 -0
- package/dist/clip/browser-profiles.js +17 -0
- package/dist/clip/bundle-reader.js +11 -0
- package/dist/clip/cli.js +27 -0
- package/dist/clip/cookies.js +21 -0
- package/dist/clip/defuddle-worker.js +84 -0
- package/dist/clip/doctor.js +32 -0
- package/dist/clip/jobs.js +784 -0
- package/dist/clip/network-proxy.js +9 -0
- package/dist/clip/network.js +28 -0
- package/dist/clip/persist.js +30 -0
- package/dist/clip/refresh.js +11 -0
- package/dist/clip/terminal.js +11 -0
- package/dist/evaluation-builder.js +12238 -0
- package/dist/evaluation-kb.js +26 -0
- package/dist/evaluation.js +35 -0
- package/dist/git.js +39 -0
- package/dist/graph.js +41 -0
- package/dist/index-054mb7d3.js +1974 -0
- package/dist/index-06c9ctr6.js +738 -0
- package/dist/index-180zbkkt.js +2480 -0
- package/dist/index-1gwbassd.js +743 -0
- package/dist/index-1xxnjn0d.js +171 -0
- package/dist/index-23z4zxgg.js +119 -0
- package/dist/index-2gv8y733.js +432 -0
- package/dist/index-3ehtt59c.js +199 -0
- package/dist/index-3rm7cz6h.js +373 -0
- package/dist/index-48pz4jpc.js +392 -0
- package/dist/index-4j3tt0c3.js +234 -0
- package/dist/index-5n05se68.js +307 -0
- package/dist/index-5vwpzb5a.js +764 -0
- package/dist/index-6jcz0m1c.js +712 -0
- package/dist/index-6sw24nvv.js +1189 -0
- package/dist/index-8a08v6x8.js +56 -0
- package/dist/index-adx6khj5.js +488 -0
- package/dist/index-b88v3vtm.js +805 -0
- package/dist/index-bcknqxrq.js +1943 -0
- package/dist/index-bs0yf4pp.js +711 -0
- package/dist/index-by0pbant.js +827 -0
- package/dist/index-d13v9ckt.js +150 -0
- package/dist/index-dfag79p7.js +370 -0
- package/dist/index-e4fagfjz.js +44 -0
- package/dist/index-e5fbsywq.js +658 -0
- package/dist/index-ekpwvbra.js +1070 -0
- package/dist/index-f984hw45.js +927 -0
- package/dist/index-gh719d91.js +63 -0
- package/dist/index-h170byqw.js +371 -0
- package/dist/index-hgve9rh2.js +2046 -0
- package/dist/index-hya40gb2.js +474 -0
- package/dist/index-j4zgmzjr.js +152 -0
- package/dist/index-m7et4rz4.js +966 -0
- package/dist/index-mxxxytys.js +1254 -0
- package/dist/index-n05s3wsb.js +2728 -0
- package/dist/index-nd6nynv2.js +1162 -0
- package/dist/index-npg9z1a4.js +431 -0
- package/dist/index-pj501bh1.js +140 -0
- package/dist/index-rdd5f7jj.js +70 -0
- package/dist/index-sy0zety7.js +3630 -0
- package/dist/index-tkpxp48t.js +1166 -0
- package/dist/index-w2zc0vwa.js +571 -0
- package/dist/index-why54q5p.js +462 -0
- package/dist/index-x3fthpsc.js +585 -0
- package/dist/index-x6m57nfc.js +1041 -0
- package/dist/index.js +784 -0
- package/dist/kb-alias.js +55 -0
- package/dist/navigation.js +13 -0
- package/dist/pdf.js +85 -0
- package/dist/percolate.js +44 -0
- package/dist/portfolio.js +77 -0
- package/dist/query.js +29 -0
- package/dist/repository-memory.js +60 -0
- package/dist/sdk.js +36 -0
- package/dist/search-rules.js +34 -0
- package/dist/search.js +22 -0
- package/dist/semantic.js +39 -0
- package/dist/source-inbox.js +17 -0
- package/dist/untrusted-content.js +32 -0
- package/dist/url-intelligence.js +74 -0
- package/dist/workflow.js +23 -0
- package/dist/workflows/decision-context.js +20 -0
- package/dist/workflows/explain-change.js +8 -0
- package/dist/workflows/index.js +28 -0
- package/dist/workflows/plan-radar.js +10 -0
- package/package.json +421 -0
- package/skills/wordcell/AGENTS.md +22 -0
- package/skills/wordcell/SKILL.md +91 -0
- package/skills/wordcell/agents/openai.yaml +4 -0
- package/skills/wordcell/references/companion-skills.md +96 -0
- package/skills/wordcell/references/customize.md +123 -0
- package/skills/wordcell/references/pdf-review.md +51 -0
- package/skills/wordcell/references/percolate.md +176 -0
- package/skills/wordcell/references/plan-structure.md +105 -0
- package/skills/wordcell/references/plan.md +115 -0
- package/skills/wordcell/references/query.md +252 -0
- package/skills/wordcell/references/refresh.md +133 -0
- package/skills/wordcell/references/save-pdf.md +125 -0
- package/skills/wordcell/references/save-url.md +178 -0
- package/skills/wordcell/references/url-authentication.md +75 -0
- package/skills/wordcell/references/url-platforms.md +25 -0
- package/skills/wordcell/templates/companion-skill.template.md +57 -0
- package/src/agent-context.ts +1241 -0
- package/src/agent-guide-audit.ts +750 -0
- package/src/attachments.ts +723 -0
- package/src/authoring-model.ts +798 -0
- package/src/authoring-platform.ts +549 -0
- package/src/authoring-program.ts +278 -0
- package/src/authoring-runtime.ts +54 -0
- package/src/authoring.ts +75 -0
- package/src/benchmark.ts +362 -0
- package/src/capture.ts +68 -0
- package/src/cli-program.ts +3662 -0
- package/src/cli.ts +6 -0
- package/src/clip/acquire.ts +1273 -0
- package/src/clip/archive-today.ts +402 -0
- package/src/clip/args.ts +423 -0
- package/src/clip/assets.ts +262 -0
- package/src/clip/bounded-byte-buffer.ts +75 -0
- package/src/clip/browser-profiles.ts +372 -0
- package/src/clip/bundle-reader.ts +608 -0
- package/src/clip/capture.ts +1054 -0
- package/src/clip/cli.ts +181 -0
- package/src/clip/cookies.ts +512 -0
- package/src/clip/doctor.ts +1110 -0
- package/src/clip/extract.ts +868 -0
- package/src/clip/jobs.ts +959 -0
- package/src/clip/lib.ts +861 -0
- package/src/clip/media.ts +1474 -0
- package/src/clip/metadata-search-tool/Cargo.lock +2498 -0
- package/src/clip/metadata-search-tool/Cargo.toml +18 -0
- package/src/clip/metadata-search-tool/runner.ts +685 -0
- package/src/clip/metadata-search-tool/src/main.rs +389 -0
- package/src/clip/metadata-search.ts +687 -0
- package/src/clip/network-proxy.ts +627 -0
- package/src/clip/network.ts +950 -0
- package/src/clip/package-root.ts +54 -0
- package/src/clip/persist.ts +1472 -0
- package/src/clip/platforms.ts +1824 -0
- package/src/clip/refresh.ts +197 -0
- package/src/clip/structured.ts +432 -0
- package/src/clip/terminal.ts +197 -0
- package/src/clip/url-intelligence.ts +581 -0
- package/src/clip/url-metadata-backfill.ts +616 -0
- package/src/clip/url-metadata-cli.ts +251 -0
- package/src/clip/url-metadata.ts +1397 -0
- package/src/evaluation-analysis-v2.ts +1440 -0
- package/src/evaluation-builder.ts +2539 -0
- package/src/evaluation-corpus-authoring.ts +2052 -0
- package/src/evaluation-evidence.ts +1147 -0
- package/src/evaluation-execution-v2.ts +426 -0
- package/src/evaluation-implementation.ts +131 -0
- package/src/evaluation-kb-closure.ts +1430 -0
- package/src/evaluation-kb-runner-v2.ts +735 -0
- package/src/evaluation-kb-v2.ts +2567 -0
- package/src/evaluation-kb.ts +1018 -0
- package/src/evaluation-measurement-v2.ts +1462 -0
- package/src/evaluation-packing-v2.ts +452 -0
- package/src/evaluation-redaction.ts +28 -0
- package/src/evaluation-v2.ts +3857 -0
- package/src/evaluation.ts +1220 -0
- package/src/git.ts +1276 -0
- package/src/graph.ts +1494 -0
- package/src/index.ts +21 -0
- package/src/init.ts +123 -0
- package/src/kb-alias.ts +14 -0
- package/src/navigation.ts +277 -0
- package/src/note-lock.ts +529 -0
- package/src/oh-adoption.ts +444 -0
- package/src/pdf/args.ts +202 -0
- package/src/pdf/capture.ts +298 -0
- package/src/pdf/cli.ts +159 -0
- package/src/pdf/extract.ts +303 -0
- package/src/pdf/layout.ts +618 -0
- package/src/pdf/markdown.ts +154 -0
- package/src/pdf/model.ts +234 -0
- package/src/pdf/ocr.ts +190 -0
- package/src/pdf/persist.ts +314 -0
- package/src/pdf/source.ts +135 -0
- package/src/pdf/tools.ts +146 -0
- package/src/pdf.ts +92 -0
- package/src/percolate.ts +1948 -0
- package/src/portfolio-audit.ts +574 -0
- package/src/portfolio-identity.ts +162 -0
- package/src/portfolio-registry.ts +473 -0
- package/src/portfolio.ts +765 -0
- package/src/query.ts +646 -0
- package/src/repository-memory.ts +1150 -0
- package/src/sdk.ts +1452 -0
- package/src/search-rules.ts +712 -0
- package/src/search.ts +710 -0
- package/src/semantic-runtime.ts +1139 -0
- package/src/semantic.ts +2480 -0
- package/src/source-inbox.ts +232 -0
- package/src/untrusted-content.ts +401 -0
- package/src/url-intelligence.ts +104 -0
- package/src/vault.ts +636 -0
- package/src/workflow-model.ts +444 -0
- package/src/workflow-platform.ts +74 -0
- package/src/workflow-program.ts +183 -0
- package/src/workflow-runtime.ts +51 -0
- package/src/workflow.ts +23 -0
- package/src/workflows/decision-context.ts +81 -0
- package/src/workflows/explain-change.ts +73 -0
- package/src/workflows/index.ts +3 -0
- package/src/workflows/plan-radar.ts +101 -0
|
@@ -0,0 +1,298 @@
|
|
|
1
|
+
import { createHash } from "node:crypto";
|
|
2
|
+
import { mkdtempSync, rmSync } from "node:fs";
|
|
3
|
+
import { tmpdir } from "node:os";
|
|
4
|
+
import { join } from "node:path";
|
|
5
|
+
|
|
6
|
+
import { countWords } from "../clip/extract.js";
|
|
7
|
+
import { slugify } from "../clip/lib.js";
|
|
8
|
+
import { sanitizeTerminalText } from "../clip/terminal.js";
|
|
9
|
+
import { inspectPdf } from "./extract.js";
|
|
10
|
+
import {
|
|
11
|
+
layoutBlocks,
|
|
12
|
+
parsePdfImageInterpretations,
|
|
13
|
+
} from "./layout.js";
|
|
14
|
+
import {
|
|
15
|
+
buildPdfMarkdown,
|
|
16
|
+
type ResolvedPdfImage,
|
|
17
|
+
} from "./markdown.js";
|
|
18
|
+
import type {
|
|
19
|
+
PdfCaptureDependencies,
|
|
20
|
+
PdfCaptureManifest,
|
|
21
|
+
PdfCaptureOptions,
|
|
22
|
+
PdfCaptureOutcome,
|
|
23
|
+
PdfImageCandidate,
|
|
24
|
+
PdfImageInterpretation,
|
|
25
|
+
PdfImageSemanticMetadata,
|
|
26
|
+
PdfManifestImage,
|
|
27
|
+
} from "./model.js";
|
|
28
|
+
import {
|
|
29
|
+
PDF_CAPTURE_MANIFEST_SCHEMA_VERSION,
|
|
30
|
+
PDF_CAPTURE_ANNOTATIONS_FILENAME,
|
|
31
|
+
PDF_CAPTURE_SOURCE_FILENAME,
|
|
32
|
+
} from "./model.js";
|
|
33
|
+
import { ocrPdfImage } from "./ocr.js";
|
|
34
|
+
import {
|
|
35
|
+
pdfImageAssetPath,
|
|
36
|
+
pdfMarkdownFilename,
|
|
37
|
+
persistPdfCapture,
|
|
38
|
+
} from "./persist.js";
|
|
39
|
+
import { resolvePdfTools, runPdfToolCommand } from "./tools.js";
|
|
40
|
+
|
|
41
|
+
function parsedInterpretations(
|
|
42
|
+
raw: readonly PdfImageInterpretation[] | undefined,
|
|
43
|
+
images: readonly PdfImageCandidate[],
|
|
44
|
+
): {
|
|
45
|
+
readonly byId: ReadonlyMap<string, PdfImageInterpretation>;
|
|
46
|
+
readonly values: readonly PdfImageInterpretation[];
|
|
47
|
+
} {
|
|
48
|
+
const parsed = raw === undefined ? [] : parsePdfImageInterpretations(raw);
|
|
49
|
+
const candidates = new Map(images.map((image) => [image.id, image]));
|
|
50
|
+
const output = new Map<string, PdfImageInterpretation>();
|
|
51
|
+
for (const interpretation of parsed) {
|
|
52
|
+
const image = candidates.get(interpretation.id);
|
|
53
|
+
if (image === undefined) {
|
|
54
|
+
throw new Error(`PDF image annotation does not match an extracted image: ${interpretation.id}`);
|
|
55
|
+
}
|
|
56
|
+
if (image.sha256 !== interpretation.sha256) {
|
|
57
|
+
throw new Error(`PDF image annotation hash does not match the extracted image: ${interpretation.id}`);
|
|
58
|
+
}
|
|
59
|
+
output.set(interpretation.id, interpretation);
|
|
60
|
+
}
|
|
61
|
+
return { byId: output, values: parsed };
|
|
62
|
+
}
|
|
63
|
+
|
|
64
|
+
function sortedImages(images: readonly PdfImageCandidate[]): readonly PdfImageCandidate[] {
|
|
65
|
+
return [...images].sort((left, right) =>
|
|
66
|
+
left.page - right.page
|
|
67
|
+
|| left.top - right.top
|
|
68
|
+
|| left.left - right.left
|
|
69
|
+
|| left.id.localeCompare(right.id));
|
|
70
|
+
}
|
|
71
|
+
|
|
72
|
+
function normalizedMetadata(
|
|
73
|
+
value: PdfImageSemanticMetadata | undefined,
|
|
74
|
+
): PdfImageSemanticMetadata | null {
|
|
75
|
+
return value ?? null;
|
|
76
|
+
}
|
|
77
|
+
|
|
78
|
+
function resolvedAnnotation(
|
|
79
|
+
image: PdfImageCandidate,
|
|
80
|
+
annotation: PdfImageInterpretation,
|
|
81
|
+
): ResolvedPdfImage {
|
|
82
|
+
const markdown = annotation.kind === "visual" ? "" : annotation.markdown;
|
|
83
|
+
return {
|
|
84
|
+
image,
|
|
85
|
+
kind: annotation.kind,
|
|
86
|
+
method: annotation.method ?? "manual",
|
|
87
|
+
markdown,
|
|
88
|
+
alt: annotation.kind === "visual" ? (annotation.alt ?? null) : null,
|
|
89
|
+
confidence: null,
|
|
90
|
+
wordCount: annotation.kind === "visual" ? 0 : countWords(markdown),
|
|
91
|
+
metadata: normalizedMetadata(annotation.metadata),
|
|
92
|
+
};
|
|
93
|
+
}
|
|
94
|
+
|
|
95
|
+
function embeddedPlatforms(images: readonly ResolvedPdfImage[]): readonly string[] {
|
|
96
|
+
const byIdentity = new Map<string, string>();
|
|
97
|
+
for (const image of images) {
|
|
98
|
+
const platform = image.metadata?.platform?.trim();
|
|
99
|
+
if (platform === undefined || platform === "") continue;
|
|
100
|
+
const key = platform.toLocaleLowerCase("en-US");
|
|
101
|
+
if (!byIdentity.has(key)) byIdentity.set(key, platform);
|
|
102
|
+
}
|
|
103
|
+
return [...byIdentity.entries()]
|
|
104
|
+
.sort(([left], [right]) => left.localeCompare(right))
|
|
105
|
+
.map(([, value]) => value);
|
|
106
|
+
}
|
|
107
|
+
|
|
108
|
+
function manifestImage(image: ResolvedPdfImage): PdfManifestImage {
|
|
109
|
+
return {
|
|
110
|
+
id: image.image.id,
|
|
111
|
+
page: image.image.page,
|
|
112
|
+
top: image.image.top,
|
|
113
|
+
left: image.image.left,
|
|
114
|
+
width: image.image.width,
|
|
115
|
+
height: image.image.height,
|
|
116
|
+
asset: {
|
|
117
|
+
path: pdfImageAssetPath(image.image),
|
|
118
|
+
mimeType: image.image.mimeType,
|
|
119
|
+
bytes: image.image.bytes,
|
|
120
|
+
sha256: image.image.sha256,
|
|
121
|
+
},
|
|
122
|
+
kind: image.kind,
|
|
123
|
+
method: image.method,
|
|
124
|
+
confidence: image.confidence,
|
|
125
|
+
wordCount: image.wordCount,
|
|
126
|
+
metadata: image.metadata,
|
|
127
|
+
};
|
|
128
|
+
}
|
|
129
|
+
|
|
130
|
+
/** Inspect, OCR/annotate, render, and atomically persist one prepared PDF source. */
|
|
131
|
+
export async function runPdfCapture(
|
|
132
|
+
options: PdfCaptureOptions,
|
|
133
|
+
dependencies: PdfCaptureDependencies = {},
|
|
134
|
+
): Promise<PdfCaptureOutcome> {
|
|
135
|
+
const callerOwnedWorkspace = options.workspaceDirectory !== undefined;
|
|
136
|
+
const workspaceDirectory = options.workspaceDirectory
|
|
137
|
+
?? mkdtempSync(join(tmpdir(), "hraness-wordcell-pdf-"));
|
|
138
|
+
try {
|
|
139
|
+
const inspection = await inspectPdf({
|
|
140
|
+
inputPath: options.inputPath,
|
|
141
|
+
workspaceDirectory,
|
|
142
|
+
...(options.timeoutMs === undefined ? {} : { timeoutMs: options.timeoutMs }),
|
|
143
|
+
...(options.maxPdfBytes === undefined ? {} : { maxPdfBytes: options.maxPdfBytes }),
|
|
144
|
+
...(options.maxPages === undefined ? {} : { maxPages: options.maxPages }),
|
|
145
|
+
...(options.maxImages === undefined ? {} : { maxImages: options.maxImages }),
|
|
146
|
+
...(options.maxAssetBytes === undefined ? {} : { maxAssetBytes: options.maxAssetBytes }),
|
|
147
|
+
...(options.maxTotalAssetBytes === undefined ? {} : { maxTotalAssetBytes: options.maxTotalAssetBytes }),
|
|
148
|
+
}, dependencies);
|
|
149
|
+
const tools = resolvePdfTools(dependencies);
|
|
150
|
+
const runTool = dependencies.runTool ?? runPdfToolCommand;
|
|
151
|
+
const images = sortedImages(inspection.pages.flatMap((page) => page.images));
|
|
152
|
+
const annotations = parsedInterpretations(options.interpretations, images);
|
|
153
|
+
const resolvedImages: ResolvedPdfImage[] = [];
|
|
154
|
+
const warnings = [...inspection.warnings];
|
|
155
|
+
let unclassifiedImages = 0;
|
|
156
|
+
let ocrWarningCount = 0;
|
|
157
|
+
|
|
158
|
+
for (const image of images) {
|
|
159
|
+
const annotation = annotations.byId.get(image.id);
|
|
160
|
+
if (annotation !== undefined) {
|
|
161
|
+
resolvedImages.push(resolvedAnnotation(image, annotation));
|
|
162
|
+
continue;
|
|
163
|
+
}
|
|
164
|
+
const ocr = await ocrPdfImage(image, {
|
|
165
|
+
tesseractPath: tools.tesseract,
|
|
166
|
+
timeoutMs: options.timeoutMs ?? 120_000,
|
|
167
|
+
runTool,
|
|
168
|
+
});
|
|
169
|
+
if (tools.tesseract === null) unclassifiedImages += 1;
|
|
170
|
+
for (const warning of ocr.warnings) {
|
|
171
|
+
warnings.push(`Page ${image.page} image ${image.id}: ${warning}`);
|
|
172
|
+
ocrWarningCount += 1;
|
|
173
|
+
}
|
|
174
|
+
resolvedImages.push({
|
|
175
|
+
image,
|
|
176
|
+
kind: ocr.kind,
|
|
177
|
+
method: tools.tesseract === null ? "unclassified" : "tesseract",
|
|
178
|
+
markdown: ocr.markdown,
|
|
179
|
+
alt: null,
|
|
180
|
+
confidence: ocr.confidence,
|
|
181
|
+
wordCount: ocr.wordCount,
|
|
182
|
+
metadata: null,
|
|
183
|
+
});
|
|
184
|
+
}
|
|
185
|
+
const platforms = embeddedPlatforms(resolvedImages);
|
|
186
|
+
const status = warnings.length > 0 || unclassifiedImages > 0 || ocrWarningCount > 0
|
|
187
|
+
? "partial"
|
|
188
|
+
: "complete";
|
|
189
|
+
const filenameTitle = inspection.originalFilename.replace(/\.pdf$/iu, "").trim();
|
|
190
|
+
const requestedSlug = options.slug ?? inspection.metadata.title ?? filenameTitle;
|
|
191
|
+
const slug = slugify(sanitizeTerminalText(requestedSlug));
|
|
192
|
+
if (slug === "") throw new Error("could not derive a safe PDF capture slug; pass an explicit slug");
|
|
193
|
+
const blocks = layoutBlocks(inspection.pages);
|
|
194
|
+
const byId = new Map(resolvedImages.map((image) => [image.image.id, image]));
|
|
195
|
+
const capturedAt = (dependencies.now ?? (() => new Date()))().toISOString();
|
|
196
|
+
const built = buildPdfMarkdown({
|
|
197
|
+
slug,
|
|
198
|
+
originalFilename: inspection.originalFilename,
|
|
199
|
+
sourceSha256: inspection.sourceSha256,
|
|
200
|
+
...(options.remoteSource === undefined ? {} : { sourceUrl: options.remoteSource.finalUrl }),
|
|
201
|
+
capturedDate: capturedAt.slice(0, 10),
|
|
202
|
+
status,
|
|
203
|
+
metadata: inspection.metadata,
|
|
204
|
+
blocks,
|
|
205
|
+
images: byId,
|
|
206
|
+
embeddedPlatforms: platforms,
|
|
207
|
+
});
|
|
208
|
+
const textImageCount = resolvedImages.filter((image) => image.kind === "text").length;
|
|
209
|
+
const mixedImageCount = resolvedImages.filter((image) => image.kind === "mixed").length;
|
|
210
|
+
const visualImageCount = resolvedImages.filter((image) => image.kind === "visual").length;
|
|
211
|
+
const annotationCount = resolvedImages.filter((image) =>
|
|
212
|
+
image.method === "agent" || image.method === "manual").length;
|
|
213
|
+
const tesseractCount = resolvedImages.filter((image) => image.method === "tesseract").length;
|
|
214
|
+
const ocr: PdfCaptureManifest["extraction"]["ocr"] = annotationCount > 0 && tesseractCount > 0
|
|
215
|
+
? "mixed"
|
|
216
|
+
: annotationCount > 0
|
|
217
|
+
? "annotations"
|
|
218
|
+
: tesseractCount > 0
|
|
219
|
+
? "tesseract"
|
|
220
|
+
: "unavailable";
|
|
221
|
+
const finalWarnings = [...new Set(warnings)];
|
|
222
|
+
const annotationsJson = annotations.values.length === 0
|
|
223
|
+
? null
|
|
224
|
+
: `${JSON.stringify(annotations.values, null, 2)}\n`;
|
|
225
|
+
const annotationsManifest: PdfCaptureManifest["annotations"] = annotationsJson === null
|
|
226
|
+
? null
|
|
227
|
+
: {
|
|
228
|
+
path: PDF_CAPTURE_ANNOTATIONS_FILENAME,
|
|
229
|
+
count: annotations.values.length,
|
|
230
|
+
bytes: Buffer.byteLength(annotationsJson),
|
|
231
|
+
sha256: createHash("sha256").update(annotationsJson).digest("hex"),
|
|
232
|
+
};
|
|
233
|
+
const manifest: PdfCaptureManifest = {
|
|
234
|
+
schemaVersion: PDF_CAPTURE_MANIFEST_SCHEMA_VERSION,
|
|
235
|
+
kind: "pdf",
|
|
236
|
+
capturedAt,
|
|
237
|
+
status,
|
|
238
|
+
source: {
|
|
239
|
+
originalFilename: sanitizeTerminalText(inspection.originalFilename).slice(0, 4_096),
|
|
240
|
+
path: PDF_CAPTURE_SOURCE_FILENAME,
|
|
241
|
+
mimeType: "application/pdf",
|
|
242
|
+
bytes: inspection.sourceBytes,
|
|
243
|
+
sha256: inspection.sourceSha256,
|
|
244
|
+
...(options.remoteSource === undefined
|
|
245
|
+
? {}
|
|
246
|
+
: {
|
|
247
|
+
requestedUrl: options.remoteSource.requestedUrl,
|
|
248
|
+
finalUrl: options.remoteSource.finalUrl,
|
|
249
|
+
}),
|
|
250
|
+
},
|
|
251
|
+
document: {
|
|
252
|
+
...inspection.metadata,
|
|
253
|
+
processedPages: inspection.processedPages,
|
|
254
|
+
},
|
|
255
|
+
extraction: {
|
|
256
|
+
layout: "pdftohtml-xml",
|
|
257
|
+
popplerVersion: inspection.popplerVersion,
|
|
258
|
+
ocr,
|
|
259
|
+
headingCount: built.headingCount,
|
|
260
|
+
textBlockCount: built.textBlockCount,
|
|
261
|
+
imageCount: resolvedImages.length,
|
|
262
|
+
textImageCount,
|
|
263
|
+
mixedImageCount,
|
|
264
|
+
visualImageCount,
|
|
265
|
+
},
|
|
266
|
+
images: resolvedImages.map(manifestImage),
|
|
267
|
+
annotations: annotationsManifest,
|
|
268
|
+
embeddedPlatforms: platforms,
|
|
269
|
+
warnings: finalWarnings,
|
|
270
|
+
};
|
|
271
|
+
const outputDirectory = persistPdfCapture({
|
|
272
|
+
outputBase: options.outputBase,
|
|
273
|
+
slug,
|
|
274
|
+
force: options.force ?? false,
|
|
275
|
+
sourcePath: inspection.inputPath,
|
|
276
|
+
markdown: built.markdown,
|
|
277
|
+
manifest,
|
|
278
|
+
images,
|
|
279
|
+
...(annotationsJson === null ? {} : { annotationsJson }),
|
|
280
|
+
});
|
|
281
|
+
return {
|
|
282
|
+
status,
|
|
283
|
+
slug,
|
|
284
|
+
outputDirectory,
|
|
285
|
+
markdownPath: join(outputDirectory, pdfMarkdownFilename(slug)),
|
|
286
|
+
sourcePath: join(outputDirectory, PDF_CAPTURE_SOURCE_FILENAME),
|
|
287
|
+
wordCount: countWords(built.markdown),
|
|
288
|
+
pageCount: inspection.metadata.pageCount,
|
|
289
|
+
processedPages: inspection.processedPages,
|
|
290
|
+
imageCount: images.length,
|
|
291
|
+
warnings: finalWarnings,
|
|
292
|
+
markdown: built.markdown,
|
|
293
|
+
manifest,
|
|
294
|
+
};
|
|
295
|
+
} finally {
|
|
296
|
+
if (!callerOwnedWorkspace) rmSync(workspaceDirectory, { recursive: true, force: true });
|
|
297
|
+
}
|
|
298
|
+
}
|
package/src/pdf/cli.ts
ADDED
|
@@ -0,0 +1,159 @@
|
|
|
1
|
+
import { lstatSync, readFileSync } from "node:fs";
|
|
2
|
+
import { resolve } from "node:path";
|
|
3
|
+
|
|
4
|
+
import { redactSensitiveText } from "../clip/persist.js";
|
|
5
|
+
import { sanitizeTerminalLine, sanitizeTerminalText } from "../clip/terminal.js";
|
|
6
|
+
import { parsePdfArguments, pdfUsage } from "./args.js";
|
|
7
|
+
import { runPdfCapture } from "./capture.js";
|
|
8
|
+
import { parsePdfImageInterpretations } from "./layout.js";
|
|
9
|
+
import { preparePdfSource, type PreparedPdfSource } from "./source.js";
|
|
10
|
+
import type {
|
|
11
|
+
PdfCaptureDependencies,
|
|
12
|
+
PdfCaptureOptions,
|
|
13
|
+
PdfCaptureOutcome,
|
|
14
|
+
PdfImageInterpretation,
|
|
15
|
+
} from "./model.js";
|
|
16
|
+
|
|
17
|
+
export type PdfCliOutput = {
|
|
18
|
+
readonly stdout: (value: string) => void;
|
|
19
|
+
readonly stderr: (value: string) => void;
|
|
20
|
+
};
|
|
21
|
+
|
|
22
|
+
const defaultOutput: PdfCliOutput = {
|
|
23
|
+
stdout: (value) => process.stdout.write(value),
|
|
24
|
+
stderr: (value) => process.stderr.write(value),
|
|
25
|
+
};
|
|
26
|
+
|
|
27
|
+
export type PdfCliDependencies = {
|
|
28
|
+
readonly runPdfCapture?: typeof runPdfCapture;
|
|
29
|
+
readonly captureDependencies?: PdfCaptureDependencies;
|
|
30
|
+
readonly readInterpretations?: (path: string) => readonly PdfImageInterpretation[];
|
|
31
|
+
readonly preparePdfSource?: typeof preparePdfSource;
|
|
32
|
+
};
|
|
33
|
+
|
|
34
|
+
function safe(value: string): string {
|
|
35
|
+
return sanitizeTerminalLine(redactSensitiveText(value));
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
function terminalSafeJson(value: unknown): string {
|
|
39
|
+
return `${JSON.stringify(
|
|
40
|
+
value,
|
|
41
|
+
(_key, candidate: unknown) => typeof candidate === "string"
|
|
42
|
+
? sanitizeTerminalText(redactSensitiveText(candidate))
|
|
43
|
+
: candidate,
|
|
44
|
+
2,
|
|
45
|
+
)}\n`;
|
|
46
|
+
}
|
|
47
|
+
|
|
48
|
+
function readInterpretations(path: string): readonly PdfImageInterpretation[] {
|
|
49
|
+
const absolute = resolve(path);
|
|
50
|
+
const stats = lstatSync(absolute);
|
|
51
|
+
if (!stats.isFile() || stats.isSymbolicLink()) {
|
|
52
|
+
throw new Error("PDF image annotations must be a regular JSON file");
|
|
53
|
+
}
|
|
54
|
+
if (stats.size > 16 * 1024 * 1024) {
|
|
55
|
+
throw new Error("PDF image annotations exceed the 16MB limit");
|
|
56
|
+
}
|
|
57
|
+
let value: unknown;
|
|
58
|
+
try {
|
|
59
|
+
value = JSON.parse(readFileSync(absolute, "utf8")) as unknown;
|
|
60
|
+
} catch {
|
|
61
|
+
throw new Error("PDF image annotations are not valid JSON");
|
|
62
|
+
}
|
|
63
|
+
return parsePdfImageInterpretations(value);
|
|
64
|
+
}
|
|
65
|
+
|
|
66
|
+
export function pdfCaptureSummary(outcome: PdfCaptureOutcome): Record<string, unknown> {
|
|
67
|
+
return {
|
|
68
|
+
ok: true,
|
|
69
|
+
status: outcome.status,
|
|
70
|
+
slug: outcome.slug,
|
|
71
|
+
outputDirectory: outcome.outputDirectory,
|
|
72
|
+
markdownPath: outcome.markdownPath,
|
|
73
|
+
sourcePath: outcome.sourcePath,
|
|
74
|
+
wordCount: outcome.wordCount,
|
|
75
|
+
pageCount: outcome.pageCount,
|
|
76
|
+
processedPages: outcome.processedPages,
|
|
77
|
+
imageCount: outcome.imageCount,
|
|
78
|
+
warnings: outcome.warnings,
|
|
79
|
+
manifest: outcome.manifest,
|
|
80
|
+
};
|
|
81
|
+
}
|
|
82
|
+
|
|
83
|
+
/** Delegated CLI entry point for the root `wordcell pdf` command. */
|
|
84
|
+
export async function main(
|
|
85
|
+
rawArguments: readonly string[] = process.argv.slice(2),
|
|
86
|
+
environment: Readonly<Record<string, string | undefined>> = process.env,
|
|
87
|
+
output: PdfCliOutput = defaultOutput,
|
|
88
|
+
dependencies: PdfCliDependencies = {},
|
|
89
|
+
): Promise<number> {
|
|
90
|
+
const parsed = parsePdfArguments(rawArguments, environment);
|
|
91
|
+
if (!parsed.ok) {
|
|
92
|
+
output.stderr(`error: ${safe(parsed.message)}\n\n${sanitizeTerminalText(pdfUsage)}`);
|
|
93
|
+
return 2;
|
|
94
|
+
}
|
|
95
|
+
const arguments_ = parsed.value;
|
|
96
|
+
if (arguments_.command === "help") {
|
|
97
|
+
output.stdout(sanitizeTerminalText(pdfUsage));
|
|
98
|
+
return 0;
|
|
99
|
+
}
|
|
100
|
+
if (!arguments_.quiet && !arguments_.json) {
|
|
101
|
+
output.stderr(`Saving PDF ${safe(arguments_.input)} ...\n`);
|
|
102
|
+
}
|
|
103
|
+
let preparedSource: PreparedPdfSource | null = null;
|
|
104
|
+
try {
|
|
105
|
+
const interpretations = arguments_.interpretationsPath === undefined
|
|
106
|
+
? undefined
|
|
107
|
+
: (dependencies.readInterpretations ?? readInterpretations)(arguments_.interpretationsPath);
|
|
108
|
+
preparedSource = await (dependencies.preparePdfSource ?? preparePdfSource)(
|
|
109
|
+
arguments_.input,
|
|
110
|
+
{
|
|
111
|
+
...(arguments_.timeoutMs === undefined ? {} : { timeoutMs: arguments_.timeoutMs }),
|
|
112
|
+
...(arguments_.maxPdfBytes === undefined ? {} : { maxPdfBytes: arguments_.maxPdfBytes }),
|
|
113
|
+
},
|
|
114
|
+
);
|
|
115
|
+
const options: PdfCaptureOptions = {
|
|
116
|
+
inputPath: preparedSource.inputPath,
|
|
117
|
+
outputBase: arguments_.outputBase,
|
|
118
|
+
...(preparedSource.remoteSource === undefined ? {} : { remoteSource: preparedSource.remoteSource }),
|
|
119
|
+
...(arguments_.slug === undefined ? {} : { slug: arguments_.slug }),
|
|
120
|
+
...(interpretations === undefined ? {} : { interpretations }),
|
|
121
|
+
force: arguments_.force,
|
|
122
|
+
...(arguments_.timeoutMs === undefined ? {} : { timeoutMs: arguments_.timeoutMs }),
|
|
123
|
+
...(arguments_.maxPdfBytes === undefined ? {} : { maxPdfBytes: arguments_.maxPdfBytes }),
|
|
124
|
+
...(arguments_.maxPages === undefined ? {} : { maxPages: arguments_.maxPages }),
|
|
125
|
+
...(arguments_.maxImages === undefined ? {} : { maxImages: arguments_.maxImages }),
|
|
126
|
+
...(arguments_.maxAssetBytes === undefined ? {} : { maxAssetBytes: arguments_.maxAssetBytes }),
|
|
127
|
+
...(arguments_.maxTotalAssetBytes === undefined
|
|
128
|
+
? {}
|
|
129
|
+
: { maxTotalAssetBytes: arguments_.maxTotalAssetBytes }),
|
|
130
|
+
};
|
|
131
|
+
const outcome = await (dependencies.runPdfCapture ?? runPdfCapture)(
|
|
132
|
+
options,
|
|
133
|
+
dependencies.captureDependencies,
|
|
134
|
+
);
|
|
135
|
+
if (arguments_.json) output.stdout(terminalSafeJson(pdfCaptureSummary(outcome)));
|
|
136
|
+
else {
|
|
137
|
+
output.stdout(`Done: ${safe(outcome.markdownPath)}\n`);
|
|
138
|
+
const pages = outcome.processedPages === outcome.pageCount
|
|
139
|
+
? `${outcome.pageCount} pages`
|
|
140
|
+
: `${outcome.processedPages} of ${outcome.pageCount} pages processed`;
|
|
141
|
+
output.stdout(
|
|
142
|
+
`Status: ${outcome.status}; ${pages}; ${outcome.wordCount} words; ${outcome.imageCount} images.\n`,
|
|
143
|
+
);
|
|
144
|
+
}
|
|
145
|
+
if (!arguments_.quiet && !arguments_.json) {
|
|
146
|
+
for (const warning of outcome.warnings) output.stderr(`warning: ${safe(warning)}\n`);
|
|
147
|
+
}
|
|
148
|
+
return 0;
|
|
149
|
+
} catch (error) {
|
|
150
|
+
const message = safe(error instanceof Error ? error.message : String(error));
|
|
151
|
+
if (arguments_.json) output.stdout(terminalSafeJson({ ok: false, error: message }));
|
|
152
|
+
else output.stderr(`error: ${message}\n`);
|
|
153
|
+
return 1;
|
|
154
|
+
} finally {
|
|
155
|
+
preparedSource?.dispose();
|
|
156
|
+
}
|
|
157
|
+
}
|
|
158
|
+
|
|
159
|
+
if (import.meta.main) process.exitCode = await main();
|