@arnilo/prism-tools 0.0.0-stage → 0.13.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/CHANGELOG.md +418 -0
- package/LICENSE +21 -0
- package/README.md +44 -2
- package/browser/README.md +98 -0
- package/browser/skills/browser-automation/SKILL.md +14 -0
- package/browser/src/actions/cdp.ts +131 -0
- package/browser/src/actions/dialog.ts +25 -0
- package/browser/src/actions/download_release.ts +22 -0
- package/browser/src/actions/index.ts +37 -0
- package/browser/src/actions/locator.ts +70 -0
- package/browser/src/actions/navigate.ts +23 -0
- package/browser/src/actions/screenshot.ts +27 -0
- package/browser/src/actions/scroll.ts +24 -0
- package/browser/src/actions/select_page.ts +21 -0
- package/browser/src/actions/types.ts +114 -0
- package/browser/src/actions/upload.ts +44 -0
- package/browser/src/actions/wait.ts +45 -0
- package/browser/src/browser.ts +197 -0
- package/browser/src/cdp.ts +178 -0
- package/browser/src/checkpoint.ts +189 -0
- package/browser/src/config.ts +72 -0
- package/browser/src/downloads.ts +253 -0
- package/browser/src/errors.ts +31 -0
- package/browser/src/evaluate.ts +101 -0
- package/browser/src/index.ts +129 -0
- package/browser/src/limits.ts +271 -0
- package/browser/src/manager.ts +730 -0
- package/browser/src/network.ts +263 -0
- package/browser/src/observe.ts +178 -0
- package/browser/src/policy.ts +133 -0
- package/browser/src/screenshot.ts +107 -0
- package/browser/src/shared-sandbox.ts +53 -0
- package/browser/src/snapshot.ts +99 -0
- package/browser/src/targets.ts +165 -0
- package/browser/src/tools.ts +480 -0
- package/browser/src/types.ts +316 -0
- package/browser/src/uploads.ts +174 -0
- package/coding/README.md +337 -0
- package/coding/src/agent/acp-operations.ts +146 -0
- package/coding/src/agent/artifacts.ts +38 -0
- package/coding/src/agent/ask-user-decision.ts +623 -0
- package/coding/src/agent/atomic-write.ts +23 -0
- package/coding/src/agent/bounded-file.ts +21 -0
- package/coding/src/agent/checks.ts +301 -0
- package/coding/src/agent/coding-checkpoint.ts +828 -0
- package/coding/src/agent/delete.ts +237 -0
- package/coding/src/agent/diagnostics-tool.ts +163 -0
- package/coding/src/agent/diagnostics.ts +262 -0
- package/coding/src/agent/edit-diff.ts +382 -0
- package/coding/src/agent/edit.ts +322 -0
- package/coding/src/agent/effects.ts +102 -0
- package/coding/src/agent/env.ts +28 -0
- package/coding/src/agent/execution-policy.ts +89 -0
- package/coding/src/agent/file-mutation-queue.ts +72 -0
- package/coding/src/agent/forge/github.ts +603 -0
- package/coding/src/agent/forge/index.ts +16 -0
- package/coding/src/agent/forge/types.ts +185 -0
- package/coding/src/agent/git-aware-repository.ts +305 -0
- package/coding/src/agent/git-exec.ts +326 -0
- package/coding/src/agent/git-status.ts +177 -0
- package/coding/src/agent/git-tools.ts +360 -0
- package/coding/src/agent/git.ts +1074 -0
- package/coding/src/agent/glob-match.ts +146 -0
- package/coding/src/agent/glob.ts +182 -0
- package/coding/src/agent/goal-verify.ts +385 -0
- package/coding/src/agent/grep-worker.ts +74 -0
- package/coding/src/agent/grep.ts +459 -0
- package/coding/src/agent/identity.ts +48 -0
- package/coding/src/agent/index.ts +676 -0
- package/coding/src/agent/language/client.ts +429 -0
- package/coding/src/agent/language/framing.ts +190 -0
- package/coding/src/agent/language/index.ts +23 -0
- package/coding/src/agent/language/intelligence.ts +682 -0
- package/coding/src/agent/language/types.ts +187 -0
- package/coding/src/agent/lifecycle-events.ts +121 -0
- package/coding/src/agent/lifecycle.ts +166 -0
- package/coding/src/agent/limits.ts +214 -0
- package/coding/src/agent/move.ts +178 -0
- package/coding/src/agent/mutation-path.ts +52 -0
- package/coding/src/agent/output-accumulator.ts +304 -0
- package/coding/src/agent/path-utils.ts +120 -0
- package/coding/src/agent/process/index.ts +58 -0
- package/coding/src/agent/process/recovery.ts +508 -0
- package/coding/src/agent/process/sessions-host.ts +222 -0
- package/coding/src/agent/process/sessions-monitor.ts +141 -0
- package/coding/src/agent/process/sessions-recovery.ts +241 -0
- package/coding/src/agent/process/sessions-spawn.ts +613 -0
- package/coding/src/agent/process/sessions-teardown.ts +281 -0
- package/coding/src/agent/process/sessions.ts +30 -0
- package/coding/src/agent/process/types.ts +358 -0
- package/coding/src/agent/read-path-set.ts +126 -0
- package/coding/src/agent/read.ts +713 -0
- package/coding/src/agent/repository/glob.ts +147 -0
- package/coding/src/agent/repository/indexed-search.ts +480 -0
- package/coding/src/agent/repository/list.ts +131 -0
- package/coding/src/agent/repository/operations.ts +22 -0
- package/coding/src/agent/repository/path.ts +98 -0
- package/coding/src/agent/repository/search.ts +277 -0
- package/coding/src/agent/repository/types.ts +243 -0
- package/coding/src/agent/repository/walk.ts +126 -0
- package/coding/src/agent/repository.ts +12 -0
- package/coding/src/agent/shell-background.ts +264 -0
- package/coding/src/agent/shell-output.ts +75 -0
- package/coding/src/agent/shell.ts +526 -0
- package/coding/src/agent/todo.ts +399 -0
- package/coding/src/agent/truncate.ts +251 -0
- package/coding/src/agent/write.ts +170 -0
- package/coding/src/ask-user-host.ts +48 -0
- package/coding/src/config.ts +278 -0
- package/coding/src/hard-deny.ts +97 -0
- package/coding/src/index.ts +318 -0
- package/coding/src/principal-guard.ts +84 -0
- package/coding/src/sandbox.ts +92 -0
- package/coding/src/security/approval.ts +144 -0
- package/coding/src/security/command-rules.ts +57 -0
- package/coding/src/security/docker-cli.ts +222 -0
- package/coding/src/security/docker-sandbox.ts +1205 -0
- package/coding/src/security/e2b-sandbox.ts +853 -0
- package/coding/src/security/egress/dns-pin.ts +88 -0
- package/coding/src/security/egress/index.ts +32 -0
- package/coding/src/security/egress/limits.ts +57 -0
- package/coding/src/security/egress/policy.ts +137 -0
- package/coding/src/security/egress/proxy.ts +599 -0
- package/coding/src/security/egress/types.ts +30 -0
- package/coding/src/security/index.ts +199 -0
- package/coding/src/security/native-sandbox.ts +639 -0
- package/coding/src/security/path-containment.ts +55 -0
- package/coding/src/security/sandbox-coding-operations.ts +392 -0
- package/coding/src/security/sandbox-fs-operations.ts +644 -0
- package/coding/src/security/sandbox-limits.ts +147 -0
- package/coding/src/security/sandbox-tar.ts +308 -0
- package/coding/src/security/sandbox.ts +179 -0
- package/coding/src/security/semaphore.ts +55 -0
- package/coding/src/targets.ts +110 -0
- package/coding/src/workspace/diff.ts +78 -0
- package/coding/src/workspace/errors.ts +29 -0
- package/coding/src/workspace/index.ts +5 -0
- package/coding/src/workspace/merge.ts +151 -0
- package/coding/src/workspace/routing.ts +76 -0
- package/coding/src/workspace/service.ts +584 -0
- package/computer-use/README.md +105 -0
- package/computer-use/skills/computer-use-linux/SKILL.md +17 -0
- package/computer-use/src/classify.ts +53 -0
- package/computer-use/src/command.ts +29 -0
- package/computer-use/src/config.ts +69 -0
- package/computer-use/src/create.ts +170 -0
- package/computer-use/src/devices.ts +247 -0
- package/computer-use/src/env.ts +28 -0
- package/computer-use/src/index.ts +157 -0
- package/computer-use/src/skill.ts +21 -0
- package/openapi/README.md +97 -0
- package/openapi/src/compile.ts +386 -0
- package/openapi/src/config.ts +73 -0
- package/openapi/src/errors.ts +16 -0
- package/openapi/src/execute.ts +241 -0
- package/openapi/src/index.ts +124 -0
- package/openapi/src/limits.ts +63 -0
- package/openapi/src/specs.ts +86 -0
- package/openapi/src/tools.ts +60 -0
- package/openapi/src/types.ts +61 -0
- package/package.json +171 -4
- package/research/README.md +72 -0
- package/research/skills/research-report/SKILL.md +12 -0
- package/research/skills/research-scoping/SKILL.md +10 -0
- package/research/skills/research-synthesis/SKILL.md +10 -0
- package/research/skills/source-triage/SKILL.md +10 -0
- package/research/src/check.ts +71 -0
- package/research/src/config.ts +41 -0
- package/research/src/index.ts +147 -0
- package/research/src/ledger.ts +157 -0
- package/research/src/urls.ts +21 -0
- package/web/README.md +287 -0
- package/web/skills/web-research/SKILL.md +17 -0
- package/web/src/brave.ts +66 -0
- package/web/src/cache.ts +151 -0
- package/web/src/config.ts +210 -0
- package/web/src/controls.ts +128 -0
- package/web/src/domains.ts +114 -0
- package/web/src/exa.ts +60 -0
- package/web/src/firecrawl.ts +160 -0
- package/web/src/index.ts +387 -0
- package/web/src/kagi.ts +73 -0
- package/web/src/limits.ts +55 -0
- package/web/src/native/convert.ts +84 -0
- package/web/src/native/fetch.ts +226 -0
- package/web/src/normalize.ts +131 -0
- package/web/src/obscura/cdp.ts +201 -0
- package/web/src/obscura/classify.ts +35 -0
- package/web/src/obscura/cli.ts +254 -0
- package/web/src/obscura/errors.ts +8 -0
- package/web/src/obscura/index.ts +59 -0
- package/web/src/obscura/limits.ts +92 -0
- package/web/src/obscura/mcp.ts +104 -0
- package/web/src/obscura/playwright.ts +133 -0
- package/web/src/obscura/process.ts +235 -0
- package/web/src/obscura/search-profile.ts +73 -0
- package/web/src/obscura/types.ts +48 -0
- package/web/src/obscura/web.ts +419 -0
- package/web/src/prompt-fetch.ts +88 -0
- package/web/src/render.ts +75 -0
- package/web/src/searxng.ts +78 -0
- package/web/src/slice.ts +139 -0
- package/web/src/tavily.ts +77 -0
- package/web/src/tools.ts +182 -0
- package/web/src/transport.ts +310 -0
- package/web/src/types.ts +199 -0
- package/work/README.md +77 -0
- package/work/THIRD_PARTY_NOTICES +24 -0
- package/work/docling/ocr.py +150 -0
- package/work/sandbox/Dockerfile +36 -0
- package/work/sandbox/soffice.sh +12 -0
- package/work/src/command.ts +29 -0
- package/work/src/config.ts +138 -0
- package/work/src/connectors/cli.ts +343 -0
- package/work/src/connectors/drafts.ts +647 -0
- package/work/src/connectors/errors.ts +8 -0
- package/work/src/connectors/file-bytes.ts +149 -0
- package/work/src/connectors/google-workspace-http.ts +399 -0
- package/work/src/connectors/google-workspace.ts +465 -0
- package/work/src/connectors/gws-tools.ts +378 -0
- package/work/src/connectors/http.ts +123 -0
- package/work/src/connectors/idempotency.ts +235 -0
- package/work/src/connectors/identity.ts +34 -0
- package/work/src/connectors/index.ts +93 -0
- package/work/src/connectors/limits.ts +50 -0
- package/work/src/connectors/m365-tools.ts +308 -0
- package/work/src/connectors/microsoft365-http.ts +428 -0
- package/work/src/connectors/microsoft365.ts +402 -0
- package/work/src/connectors/normalize.ts +120 -0
- package/work/src/connectors/tool-helpers.ts +398 -0
- package/work/src/connectors/tools.ts +42 -0
- package/work/src/connectors/types.ts +399 -0
- package/work/src/diagrams/canonicalize.ts +166 -0
- package/work/src/diagrams/caps.ts +52 -0
- package/work/src/diagrams/embed.ts +340 -0
- package/work/src/diagrams/errors.ts +76 -0
- package/work/src/diagrams/index.ts +81 -0
- package/work/src/diagrams/messages.ts +150 -0
- package/work/src/diagrams/origin.ts +52 -0
- package/work/src/diagrams/telemetry.ts +34 -0
- package/work/src/diagrams/xml.ts +267 -0
- package/work/src/document-extraction/index.ts +563 -0
- package/work/src/document-reader/ambient.d.ts +2 -0
- package/work/src/document-reader/errors.ts +7 -0
- package/work/src/document-reader/index.ts +181 -0
- package/work/src/document-reader/mistral-ocr.ts +318 -0
- package/work/src/document-reader/parsers.ts +171 -0
- package/work/src/documents/caps.ts +135 -0
- package/work/src/documents/diff.ts +269 -0
- package/work/src/documents/errors.ts +44 -0
- package/work/src/documents/fidelity.ts +98 -0
- package/work/src/documents/generate.ts +96 -0
- package/work/src/documents/hash.ts +8 -0
- package/work/src/documents/index.ts +14 -0
- package/work/src/documents/model-schema.ts +639 -0
- package/work/src/documents/parse.ts +252 -0
- package/work/src/documents/patch-history.ts +81 -0
- package/work/src/documents/patch.ts +476 -0
- package/work/src/documents/preview-html.ts +245 -0
- package/work/src/documents/preview.ts +227 -0
- package/work/src/documents/telemetry.ts +36 -0
- package/work/src/documents/translate/docx.ts +292 -0
- package/work/src/documents/translate/pptx.ts +304 -0
- package/work/src/documents/translate/xlsx.ts +137 -0
- package/work/src/documents/types.ts +188 -0
- package/work/src/index.ts +311 -0
- package/work/src/runtime/worker-pool-entry.ts +102 -0
- package/work/src/runtime/worker-pool.ts +267 -0
- package/work/src/sandbox/composition.ts +212 -0
- package/work/src/sandbox/image.ts +6 -0
- package/work/src/sandbox/index.ts +14 -0
- package/work/src/sheets/caps.ts +68 -0
- package/work/src/sheets/csv.ts +351 -0
- package/work/src/sheets/decimal.ts +388 -0
- package/work/src/sheets/errors.ts +52 -0
- package/work/src/sheets/index.ts +62 -0
- package/work/src/sheets/inference.ts +378 -0
- package/work/src/sheets/telemetry.ts +34 -0
- package/work/src/sheets/types.ts +162 -0
- package/work/src/sheets/xlsx.ts +388 -0
- package/work/src/skills/index.ts +8 -0
- package/work/src/skills/load.ts +43 -0
- package/work/src/tools/filesystem.ts +21 -0
- package/work/src/tools/index.ts +1 -0
- package/work/src/tools/local-filesystem.ts +33 -0
- package/work/src/tools/office.ts +311 -0
- package/work/vendor/hermes-agent/LICENSE +21 -0
- package/work/vendor/hermes-agent/skills/productivity/docx/LICENSE +21 -0
- package/work/vendor/hermes-agent/skills/productivity/docx/SKILL.md +196 -0
- package/work/vendor/hermes-agent/skills/productivity/docx/scripts/docx_comments.py +289 -0
- package/work/vendor/hermes-agent/skills/productivity/docx/scripts/docx_common.py +94 -0
- package/work/vendor/hermes-agent/skills/productivity/docx/scripts/docx_create.py +177 -0
- package/work/vendor/hermes-agent/skills/productivity/docx/scripts/docx_edit.py +250 -0
- package/work/vendor/hermes-agent/skills/productivity/docx/scripts/docx_read.py +149 -0
- package/work/vendor/hermes-agent/skills/productivity/docx/scripts/docx_revisions.py +147 -0
- package/work/vendor/hermes-agent/skills/productivity/docx/scripts/docx_template.py +70 -0
- package/work/vendor/hermes-agent/skills/productivity/docx/scripts/docx_validate.py +156 -0
- package/work/vendor/hermes-agent/skills/productivity/pdf/LICENSE +21 -0
- package/work/vendor/hermes-agent/skills/productivity/pdf/SKILL.md +125 -0
- package/work/vendor/hermes-agent/skills/productivity/pdf/scripts/_raster.py +76 -0
- package/work/vendor/hermes-agent/skills/productivity/pdf/scripts/extract_marker.py +88 -0
- package/work/vendor/hermes-agent/skills/productivity/pdf/scripts/extract_pymupdf.py +101 -0
- package/work/vendor/hermes-agent/skills/productivity/pdf/scripts/pdf_create.py +130 -0
- package/work/vendor/hermes-agent/skills/productivity/pdf/scripts/pdf_fill_form.py +97 -0
- package/work/vendor/hermes-agent/skills/productivity/pdf/scripts/pdf_form_layout.py +168 -0
- package/work/vendor/hermes-agent/skills/productivity/pdf/scripts/pdf_make_form.py +145 -0
- package/work/vendor/hermes-agent/skills/productivity/pdf/scripts/pdf_merge.py +50 -0
- package/work/vendor/hermes-agent/skills/productivity/pdf/scripts/pdf_meta.py +115 -0
- package/work/vendor/hermes-agent/skills/productivity/pdf/scripts/pdf_page_image.py +99 -0
- package/work/vendor/hermes-agent/skills/productivity/pdf/scripts/pdf_read.py +153 -0
- package/work/vendor/hermes-agent/skills/productivity/pdf/scripts/pdf_secure.py +71 -0
- package/work/vendor/hermes-agent/skills/productivity/pdf/scripts/pdf_split.py +84 -0
- package/work/vendor/hermes-agent/skills/productivity/pdf/scripts/pdf_stamp.py +143 -0
- package/work/vendor/hermes-agent/skills/productivity/pdf/scripts/pdf_watermark.py +51 -0
- package/work/vendor/hermes-agent/skills/productivity/powerpoint/LICENSE +21 -0
- package/work/vendor/hermes-agent/skills/productivity/powerpoint/SKILL.md +220 -0
- package/work/vendor/hermes-agent/skills/productivity/powerpoint/scripts/pptx_create.py +214 -0
- package/work/vendor/hermes-agent/skills/productivity/powerpoint/scripts/pptx_edit.py +436 -0
- package/work/vendor/hermes-agent/skills/productivity/powerpoint/scripts/pptx_from_template.py +88 -0
- package/work/vendor/hermes-agent/skills/productivity/powerpoint/scripts/pptx_read.py +131 -0
- package/work/vendor/hermes-agent/skills/productivity/powerpoint/scripts/pptx_render.py +93 -0
- package/work/vendor/hermes-agent/skills/productivity/xlsx/LICENSE +21 -0
- package/work/vendor/hermes-agent/skills/productivity/xlsx/SKILL.md +196 -0
- package/work/vendor/hermes-agent/skills/productivity/xlsx/scripts/csv_to_xlsx.py +104 -0
- package/work/vendor/hermes-agent/skills/productivity/xlsx/scripts/xlsx_create.py +259 -0
- package/work/vendor/hermes-agent/skills/productivity/xlsx/scripts/xlsx_edit.py +263 -0
- package/work/vendor/hermes-agent/skills/productivity/xlsx/scripts/xlsx_read.py +160 -0
- package/work/vendor/hermes-agent/skills/productivity/xlsx/scripts/xlsx_recalc.py +111 -0
- package/work/vendor/hermes-agent/skills/productivity/xlsx/scripts/xlsx_restructure.py +337 -0
- package/work/vendor/hermes-agent/skills/productivity/xlsx/scripts/xlsx_to_csv.py +69 -0
- package/work/vendor-lock.json +11 -0
|
@@ -0,0 +1,563 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Optional local Markdown extraction via `@firecrawl/anydoc`.
|
|
3
|
+
*
|
|
4
|
+
* The peer loads only when `createDocumentExtractor()` runs. This module never
|
|
5
|
+
* passes `ocr: 'hosted'`, never reads `FIRECRAWL_API_KEY`, and never spawns a
|
|
6
|
+
* process. Docling runs only when the host injects a runner, and only for an
|
|
7
|
+
* anydoc `needsOcr` PDF or an explicit image OCR call.
|
|
8
|
+
*/
|
|
9
|
+
import { fileURLToPath } from "node:url";
|
|
10
|
+
|
|
11
|
+
export const DEFAULT_MAX_EXTRACTION_BYTES = 32 * 1024 * 1024;
|
|
12
|
+
export const HARD_MAX_EXTRACTION_BYTES = 512 * 1024 * 1024;
|
|
13
|
+
export const DEFAULT_MAX_EXTRACTION_TEXT_BYTES = 2 * 1024 * 1024;
|
|
14
|
+
export const HARD_MAX_EXTRACTION_TEXT_BYTES = 64 * 1024 * 1024;
|
|
15
|
+
export const DEFAULT_MAX_OCR_PAGES = 32;
|
|
16
|
+
export const HARD_MAX_OCR_PAGES = 256;
|
|
17
|
+
export const DEFAULT_OCR_TIMEOUT_MS = 120_000;
|
|
18
|
+
export const HARD_OCR_TIMEOUT_MS = 600_000;
|
|
19
|
+
export const DEFAULT_MAX_OCR_STDERR_BYTES = 1024 * 1024;
|
|
20
|
+
export const HARD_MAX_OCR_STDERR_BYTES = 4 * 1024 * 1024;
|
|
21
|
+
|
|
22
|
+
/** Packaged helper. Host runner executes it; this module does not. */
|
|
23
|
+
export const doclingOcrHelperPath = fileURLToPath(new URL("../../docling/ocr.py", import.meta.url));
|
|
24
|
+
|
|
25
|
+
const IMAGE_FORMATS = ["png", "jpeg", "webp", "bmp", "tiff"] as const;
|
|
26
|
+
type ImageFormat = (typeof IMAGE_FORMATS)[number];
|
|
27
|
+
type OcrFormat = "pdf" | ImageFormat;
|
|
28
|
+
|
|
29
|
+
const MISSING_PEER =
|
|
30
|
+
'optional peer "@firecrawl/anydoc" is not installed. Install it (bun add @firecrawl/anydoc) to use @arnilo/prism-tools/work';
|
|
31
|
+
|
|
32
|
+
const REASON_MESSAGE = {
|
|
33
|
+
needsOcr: "document needs OCR",
|
|
34
|
+
unsupported: "document format is unsupported",
|
|
35
|
+
encrypted: "document is encrypted",
|
|
36
|
+
malformed: "document is malformed",
|
|
37
|
+
resourceLimit: "document exceeds an extractor resource limit",
|
|
38
|
+
missingPart: "document is missing a required part",
|
|
39
|
+
io: "document extraction failed",
|
|
40
|
+
hosted: "hosted document extraction is disabled",
|
|
41
|
+
missingPeer: MISSING_PEER,
|
|
42
|
+
inputLimit: "document exceeds maxBytes cap",
|
|
43
|
+
outputLimit: "extracted markdown exceeds maxTextBytes cap",
|
|
44
|
+
busy: "document extraction is busy",
|
|
45
|
+
} as const;
|
|
46
|
+
|
|
47
|
+
export type DocumentExtractionReason = keyof typeof REASON_MESSAGE;
|
|
48
|
+
|
|
49
|
+
const UPSTREAM = new Set<string>(["unsupported", "malformed", "encrypted", "resourceLimit", "missingPart", "io", "hosted"]);
|
|
50
|
+
|
|
51
|
+
export class DocumentExtractionError extends Error {
|
|
52
|
+
readonly code = "ERR_PRISM_DOCUMENT_EXTRACTION";
|
|
53
|
+
readonly reason: DocumentExtractionReason;
|
|
54
|
+
constructor(reason: DocumentExtractionReason, message?: string) {
|
|
55
|
+
super(message ?? REASON_MESSAGE[reason]);
|
|
56
|
+
this.name = "DocumentExtractionError";
|
|
57
|
+
this.reason = reason;
|
|
58
|
+
}
|
|
59
|
+
}
|
|
60
|
+
|
|
61
|
+
/** Scanned or image-only PDF. No markdown. Pages are 1-indexed when the peer reported them. */
|
|
62
|
+
export class NeedsOcrError extends DocumentExtractionError {
|
|
63
|
+
readonly pages: readonly number[];
|
|
64
|
+
readonly pageCount: number;
|
|
65
|
+
constructor(pages: readonly number[], pageCount: number) {
|
|
66
|
+
super("needsOcr");
|
|
67
|
+
this.name = "NeedsOcrError";
|
|
68
|
+
this.pages = pages;
|
|
69
|
+
this.pageCount = pageCount;
|
|
70
|
+
}
|
|
71
|
+
}
|
|
72
|
+
|
|
73
|
+
export interface DocumentExtractionResult {
|
|
74
|
+
readonly markdown: string;
|
|
75
|
+
readonly format: string;
|
|
76
|
+
/** True only after a host Docling runner returns ConversionStatus.SUCCESS. */
|
|
77
|
+
readonly ocrUsed: boolean;
|
|
78
|
+
/** Present only when Docling reported `document.num_pages()`. */
|
|
79
|
+
readonly pages?: number;
|
|
80
|
+
}
|
|
81
|
+
|
|
82
|
+
export interface DocumentExtractor {
|
|
83
|
+
readonly maxInputBytes: number;
|
|
84
|
+
readonly maxTextBytes: number;
|
|
85
|
+
extract(input: {
|
|
86
|
+
readonly bytes: Uint8Array;
|
|
87
|
+
readonly filename?: string;
|
|
88
|
+
/** Image OCR only. Ignored for formats anydoc converts. PDFs OCR on `needsOcr`, not this flag. */
|
|
89
|
+
readonly ocr?: boolean;
|
|
90
|
+
readonly signal?: AbortSignal;
|
|
91
|
+
}): Promise<DocumentExtractionResult>;
|
|
92
|
+
}
|
|
93
|
+
|
|
94
|
+
export interface OcrWorkerRequest {
|
|
95
|
+
readonly bytes: Uint8Array;
|
|
96
|
+
readonly format: OcrFormat;
|
|
97
|
+
readonly maxPages: number;
|
|
98
|
+
readonly maxBytes: number;
|
|
99
|
+
readonly maxTextBytes: number;
|
|
100
|
+
readonly timeoutMs: number;
|
|
101
|
+
readonly maxStderrBytes: number;
|
|
102
|
+
readonly signal?: AbortSignal;
|
|
103
|
+
readonly helperPath: string;
|
|
104
|
+
}
|
|
105
|
+
|
|
106
|
+
export interface OcrWorkerResult {
|
|
107
|
+
readonly exitCode: number | null;
|
|
108
|
+
readonly stdout: Uint8Array;
|
|
109
|
+
readonly stderr: Uint8Array;
|
|
110
|
+
readonly timedOut?: boolean;
|
|
111
|
+
readonly aborted?: boolean;
|
|
112
|
+
readonly stdoutTruncated?: boolean;
|
|
113
|
+
readonly stderrTruncated?: boolean;
|
|
114
|
+
}
|
|
115
|
+
|
|
116
|
+
/** Host-owned worker. Must kill the process on abort and when a byte cap trips. */
|
|
117
|
+
export type OcrRunner = (request: OcrWorkerRequest) => Promise<OcrWorkerResult>;
|
|
118
|
+
|
|
119
|
+
export interface CreateDocumentExtractorOptions {
|
|
120
|
+
readonly maxBytes?: number;
|
|
121
|
+
readonly maxTextBytes?: number;
|
|
122
|
+
readonly maxPages?: number;
|
|
123
|
+
readonly ocrTimeoutMs?: number;
|
|
124
|
+
readonly maxStderrBytes?: number;
|
|
125
|
+
/** Absent: scanned PDFs throw `NeedsOcrError` and images stay unsupported. */
|
|
126
|
+
readonly ocr?: OcrRunner;
|
|
127
|
+
}
|
|
128
|
+
|
|
129
|
+
interface AnydocApi {
|
|
130
|
+
formatFromBytes(bytes: Uint8Array): string | null;
|
|
131
|
+
formatFromExtension(extension: string): string | null;
|
|
132
|
+
toMarkdownBytes(bytes: Uint8Array, format?: string | null): Promise<string>;
|
|
133
|
+
}
|
|
134
|
+
|
|
135
|
+
function validateCap(name: string, value: number, hard: number): number {
|
|
136
|
+
if (!Number.isInteger(value) || value <= 0 || value > hard) {
|
|
137
|
+
throw new RangeError(`document-extraction ${name} must be an integer in (0, ${hard}], got ${value}`);
|
|
138
|
+
}
|
|
139
|
+
return value;
|
|
140
|
+
}
|
|
141
|
+
|
|
142
|
+
function isCsvName(filename: string | undefined): boolean {
|
|
143
|
+
if (!filename) return false;
|
|
144
|
+
const base = filename.split(/[/\\]/).pop() ?? "";
|
|
145
|
+
return base.length > ".csv".length && base.toLowerCase().endsWith(".csv") && !base.includes("\0");
|
|
146
|
+
}
|
|
147
|
+
|
|
148
|
+
function pagesOf(error: object): { pages: number[]; pageCount: number } {
|
|
149
|
+
const raw = "pages" in error ? error.pages : undefined;
|
|
150
|
+
const pages = Array.isArray(raw) ? raw.filter((n): n is number => Number.isInteger(n) && n > 0 && n <= 100_000).slice(0, 10_000) : [];
|
|
151
|
+
const count = "pageCount" in error ? error.pageCount : undefined;
|
|
152
|
+
const pageCount = Number.isInteger(count) && (count as number) >= 0 && (count as number) <= 100_000 ? (count as number) : pages.length;
|
|
153
|
+
return { pages, pageCount };
|
|
154
|
+
}
|
|
155
|
+
|
|
156
|
+
function mapError(error: unknown): DocumentExtractionError {
|
|
157
|
+
if (error instanceof DocumentExtractionError) return error;
|
|
158
|
+
const code = typeof error === "object" && error !== null && "code" in error ? error.code : undefined;
|
|
159
|
+
if (code === "needsOcr" && error !== null && typeof error === "object") {
|
|
160
|
+
const { pages, pageCount } = pagesOf(error);
|
|
161
|
+
return new NeedsOcrError(pages, pageCount);
|
|
162
|
+
}
|
|
163
|
+
const reason = typeof code === "string" && UPSTREAM.has(code) ? (code as DocumentExtractionReason) : "io";
|
|
164
|
+
return new DocumentExtractionError(reason);
|
|
165
|
+
}
|
|
166
|
+
|
|
167
|
+
async function loadAnydoc(): Promise<AnydocApi> {
|
|
168
|
+
try {
|
|
169
|
+
const mod = (await import("@firecrawl/anydoc")) as Partial<AnydocApi>;
|
|
170
|
+
if (
|
|
171
|
+
typeof mod.toMarkdownBytes !== "function" ||
|
|
172
|
+
typeof mod.formatFromBytes !== "function" ||
|
|
173
|
+
typeof mod.formatFromExtension !== "function"
|
|
174
|
+
) {
|
|
175
|
+
throw new DocumentExtractionError("missingPeer");
|
|
176
|
+
}
|
|
177
|
+
return mod as AnydocApi;
|
|
178
|
+
} catch (error) {
|
|
179
|
+
if (error instanceof DocumentExtractionError) throw error;
|
|
180
|
+
throw new DocumentExtractionError("missingPeer");
|
|
181
|
+
}
|
|
182
|
+
}
|
|
183
|
+
|
|
184
|
+
function createSlot() {
|
|
185
|
+
let running: Promise<void> | null = null;
|
|
186
|
+
let waiting = false;
|
|
187
|
+
return async function occupy(signal?: AbortSignal): Promise<() => void> {
|
|
188
|
+
for (;;) {
|
|
189
|
+
signal?.throwIfAborted();
|
|
190
|
+
if (!running) {
|
|
191
|
+
let release!: () => void;
|
|
192
|
+
running = new Promise((resolve) => {
|
|
193
|
+
release = () => {
|
|
194
|
+
running = null;
|
|
195
|
+
resolve();
|
|
196
|
+
};
|
|
197
|
+
});
|
|
198
|
+
return release;
|
|
199
|
+
}
|
|
200
|
+
if (waiting) throw new DocumentExtractionError("busy");
|
|
201
|
+
const current = running;
|
|
202
|
+
waiting = true;
|
|
203
|
+
try {
|
|
204
|
+
await waitTurn(current, signal);
|
|
205
|
+
} finally {
|
|
206
|
+
waiting = false;
|
|
207
|
+
}
|
|
208
|
+
}
|
|
209
|
+
};
|
|
210
|
+
}
|
|
211
|
+
|
|
212
|
+
function waitTurn(running: Promise<void>, signal?: AbortSignal): Promise<void> {
|
|
213
|
+
if (!signal) return running;
|
|
214
|
+
signal.throwIfAborted();
|
|
215
|
+
return new Promise((resolve, reject) => {
|
|
216
|
+
const onAbort = () => {
|
|
217
|
+
cleanup();
|
|
218
|
+
reject(signal.reason ?? new DOMException("The operation was aborted", "AbortError"));
|
|
219
|
+
};
|
|
220
|
+
const cleanup = () => signal.removeEventListener("abort", onAbort);
|
|
221
|
+
running.then(
|
|
222
|
+
() => {
|
|
223
|
+
cleanup();
|
|
224
|
+
resolve();
|
|
225
|
+
},
|
|
226
|
+
(error) => {
|
|
227
|
+
cleanup();
|
|
228
|
+
reject(error);
|
|
229
|
+
},
|
|
230
|
+
);
|
|
231
|
+
signal.addEventListener("abort", onAbort, { once: true });
|
|
232
|
+
});
|
|
233
|
+
}
|
|
234
|
+
|
|
235
|
+
function detectImage(bytes: Uint8Array): ImageFormat | null {
|
|
236
|
+
if (bytes.length >= 8 && bytes[0] === 0x89 && bytes[1] === 0x50 && bytes[2] === 0x4e && bytes[3] === 0x47) return "png";
|
|
237
|
+
if (bytes.length >= 3 && bytes[0] === 0xff && bytes[1] === 0xd8 && bytes[2] === 0xff) return "jpeg";
|
|
238
|
+
if (
|
|
239
|
+
bytes.length >= 12 &&
|
|
240
|
+
bytes[0] === 0x52 &&
|
|
241
|
+
bytes[1] === 0x49 &&
|
|
242
|
+
bytes[2] === 0x46 &&
|
|
243
|
+
bytes[3] === 0x46 &&
|
|
244
|
+
bytes[8] === 0x57 &&
|
|
245
|
+
bytes[9] === 0x45 &&
|
|
246
|
+
bytes[10] === 0x42 &&
|
|
247
|
+
bytes[11] === 0x50
|
|
248
|
+
) {
|
|
249
|
+
return "webp";
|
|
250
|
+
}
|
|
251
|
+
if (bytes.length >= 2 && bytes[0] === 0x42 && bytes[1] === 0x4d) return "bmp";
|
|
252
|
+
if (
|
|
253
|
+
bytes.length >= 4 &&
|
|
254
|
+
((bytes[0] === 0x49 && bytes[1] === 0x49 && bytes[2] === 0x2a && bytes[3] === 0) ||
|
|
255
|
+
(bytes[0] === 0x4d && bytes[1] === 0x4d && bytes[2] === 0 && bytes[3] === 0x2a))
|
|
256
|
+
) {
|
|
257
|
+
return "tiff";
|
|
258
|
+
}
|
|
259
|
+
return null;
|
|
260
|
+
}
|
|
261
|
+
|
|
262
|
+
function abortError(signal?: AbortSignal): Error {
|
|
263
|
+
const reason = signal?.reason;
|
|
264
|
+
return reason instanceof Error ? reason : new DOMException("The operation was aborted", "AbortError");
|
|
265
|
+
}
|
|
266
|
+
|
|
267
|
+
function isAbort(error: unknown): boolean {
|
|
268
|
+
return error instanceof Error && (error.name === "AbortError" || (error as { code?: unknown }).code === "ABORT_ERR");
|
|
269
|
+
}
|
|
270
|
+
|
|
271
|
+
const OCR_ERROR: Record<string, DocumentExtractionReason> = {
|
|
272
|
+
over_page: "resourceLimit",
|
|
273
|
+
input_limit: "resourceLimit",
|
|
274
|
+
output_limit: "outputLimit",
|
|
275
|
+
partial: "io",
|
|
276
|
+
missing_models: "io",
|
|
277
|
+
unsupported: "unsupported",
|
|
278
|
+
io: "io",
|
|
279
|
+
};
|
|
280
|
+
|
|
281
|
+
function stdoutCap(maxTextBytes: number): number {
|
|
282
|
+
return maxTextBytes * 6 + 64 * 1024;
|
|
283
|
+
}
|
|
284
|
+
|
|
285
|
+
function ocrFailure(stdout: Uint8Array): DocumentExtractionError {
|
|
286
|
+
let code: unknown;
|
|
287
|
+
try {
|
|
288
|
+
const parsed: unknown = JSON.parse(Buffer.from(stdout).toString("utf8"));
|
|
289
|
+
if (parsed !== null && typeof parsed === "object" && !Array.isArray(parsed) && "error" in parsed) {
|
|
290
|
+
code = parsed.error;
|
|
291
|
+
}
|
|
292
|
+
} catch {
|
|
293
|
+
code = undefined;
|
|
294
|
+
}
|
|
295
|
+
const reason = typeof code === "string" ? OCR_ERROR[code] : undefined;
|
|
296
|
+
return new DocumentExtractionError(reason ?? "io");
|
|
297
|
+
}
|
|
298
|
+
|
|
299
|
+
/** Argv for the packaged helper. Bytes stay on stdin. Rejects URL artifacts. */
|
|
300
|
+
export function doclingOcrArgs(request: OcrWorkerRequest, artifactsPath: string): string[] {
|
|
301
|
+
if (!artifactsPath || /[\0\r\n]/.test(artifactsPath) || /^[a-z][a-z0-9+.-]*:\/\//i.test(artifactsPath)) {
|
|
302
|
+
throw new DocumentExtractionError("unsupported");
|
|
303
|
+
}
|
|
304
|
+
return [
|
|
305
|
+
request.helperPath,
|
|
306
|
+
"--format",
|
|
307
|
+
request.format,
|
|
308
|
+
"--max-pages",
|
|
309
|
+
String(request.maxPages),
|
|
310
|
+
"--max-bytes",
|
|
311
|
+
String(request.maxBytes),
|
|
312
|
+
"--max-text-bytes",
|
|
313
|
+
String(request.maxTextBytes),
|
|
314
|
+
"--timeout-s",
|
|
315
|
+
String(Math.max(1, Math.ceil(request.timeoutMs / 1000))),
|
|
316
|
+
"--artifacts",
|
|
317
|
+
artifactsPath,
|
|
318
|
+
];
|
|
319
|
+
}
|
|
320
|
+
|
|
321
|
+
async function runOcr(
|
|
322
|
+
runner: OcrRunner,
|
|
323
|
+
bytes: Uint8Array,
|
|
324
|
+
format: OcrFormat,
|
|
325
|
+
limits: { maxPages: number; maxBytes: number; maxTextBytes: number; timeoutMs: number; maxStderrBytes: number },
|
|
326
|
+
signal?: AbortSignal,
|
|
327
|
+
): Promise<DocumentExtractionResult> {
|
|
328
|
+
signal?.throwIfAborted();
|
|
329
|
+
let result: OcrWorkerResult;
|
|
330
|
+
try {
|
|
331
|
+
result = await runner({
|
|
332
|
+
bytes,
|
|
333
|
+
format,
|
|
334
|
+
maxPages: limits.maxPages,
|
|
335
|
+
maxBytes: limits.maxBytes,
|
|
336
|
+
maxTextBytes: limits.maxTextBytes,
|
|
337
|
+
timeoutMs: limits.timeoutMs,
|
|
338
|
+
maxStderrBytes: limits.maxStderrBytes,
|
|
339
|
+
signal,
|
|
340
|
+
helperPath: doclingOcrHelperPath,
|
|
341
|
+
});
|
|
342
|
+
} catch (error) {
|
|
343
|
+
if (isAbort(error) || signal?.aborted) throw isAbort(error) ? error : abortError(signal);
|
|
344
|
+
throw new DocumentExtractionError("io");
|
|
345
|
+
}
|
|
346
|
+
if (result.aborted || signal?.aborted) throw abortError(signal);
|
|
347
|
+
if (result.timedOut) throw new DocumentExtractionError("resourceLimit");
|
|
348
|
+
if (!(result.stdout instanceof Uint8Array) || !(result.stderr instanceof Uint8Array)) {
|
|
349
|
+
throw new DocumentExtractionError("io");
|
|
350
|
+
}
|
|
351
|
+
if (result.stdoutTruncated || result.stderrTruncated || result.stderr.byteLength > limits.maxStderrBytes) {
|
|
352
|
+
throw new DocumentExtractionError("io");
|
|
353
|
+
}
|
|
354
|
+
if (result.stdout.byteLength > stdoutCap(limits.maxTextBytes)) {
|
|
355
|
+
throw new DocumentExtractionError("outputLimit");
|
|
356
|
+
}
|
|
357
|
+
if (result.exitCode !== 0) throw ocrFailure(result.stdout);
|
|
358
|
+
let parsed: unknown;
|
|
359
|
+
try {
|
|
360
|
+
parsed = JSON.parse(Buffer.from(result.stdout).toString("utf8"));
|
|
361
|
+
} catch {
|
|
362
|
+
throw new DocumentExtractionError("io");
|
|
363
|
+
}
|
|
364
|
+
if (parsed === null || typeof parsed !== "object" || Array.isArray(parsed) || !("ok" in parsed) || parsed.ok !== true) {
|
|
365
|
+
throw ocrFailure(result.stdout);
|
|
366
|
+
}
|
|
367
|
+
const markdown = "markdown" in parsed ? parsed.markdown : undefined;
|
|
368
|
+
const pages = "pages" in parsed ? parsed.pages : undefined;
|
|
369
|
+
if (typeof markdown !== "string" || !Number.isInteger(pages) || (pages as number) < 1 || (pages as number) > limits.maxPages) {
|
|
370
|
+
throw new DocumentExtractionError("io");
|
|
371
|
+
}
|
|
372
|
+
if (Buffer.byteLength(markdown, "utf8") > limits.maxTextBytes) {
|
|
373
|
+
throw new DocumentExtractionError("outputLimit");
|
|
374
|
+
}
|
|
375
|
+
return { markdown, format, ocrUsed: true, pages: pages as number };
|
|
376
|
+
}
|
|
377
|
+
|
|
378
|
+
export async function createDocumentExtractor(options: CreateDocumentExtractorOptions = {}): Promise<DocumentExtractor> {
|
|
379
|
+
const maxBytes = validateCap("maxBytes", options.maxBytes ?? DEFAULT_MAX_EXTRACTION_BYTES, HARD_MAX_EXTRACTION_BYTES);
|
|
380
|
+
const maxTextBytes = validateCap(
|
|
381
|
+
"maxTextBytes",
|
|
382
|
+
options.maxTextBytes ?? DEFAULT_MAX_EXTRACTION_TEXT_BYTES,
|
|
383
|
+
HARD_MAX_EXTRACTION_TEXT_BYTES,
|
|
384
|
+
);
|
|
385
|
+
const maxPages = validateCap("maxPages", options.maxPages ?? DEFAULT_MAX_OCR_PAGES, HARD_MAX_OCR_PAGES);
|
|
386
|
+
const ocrTimeoutMs = validateCap("ocrTimeoutMs", options.ocrTimeoutMs ?? DEFAULT_OCR_TIMEOUT_MS, HARD_OCR_TIMEOUT_MS);
|
|
387
|
+
const maxStderrBytes = validateCap("maxStderrBytes", options.maxStderrBytes ?? DEFAULT_MAX_OCR_STDERR_BYTES, HARD_MAX_OCR_STDERR_BYTES);
|
|
388
|
+
if (options.ocr !== undefined && typeof options.ocr !== "function") {
|
|
389
|
+
throw new TypeError("document extraction ocr runner must be a function");
|
|
390
|
+
}
|
|
391
|
+
const runner = options.ocr;
|
|
392
|
+
const limits = { maxPages, maxBytes, maxTextBytes, timeoutMs: ocrTimeoutMs, maxStderrBytes };
|
|
393
|
+
const anydoc = await loadAnydoc();
|
|
394
|
+
const occupy = createSlot();
|
|
395
|
+
|
|
396
|
+
return {
|
|
397
|
+
maxInputBytes: maxBytes,
|
|
398
|
+
maxTextBytes,
|
|
399
|
+
extract: async (input) => {
|
|
400
|
+
input.signal?.throwIfAborted();
|
|
401
|
+
if (!(input.bytes instanceof Uint8Array)) {
|
|
402
|
+
throw new TypeError("document extraction bytes must be a Uint8Array");
|
|
403
|
+
}
|
|
404
|
+
if (input.filename !== undefined && typeof input.filename !== "string") {
|
|
405
|
+
throw new TypeError("document extraction filename must be a string");
|
|
406
|
+
}
|
|
407
|
+
if (input.ocr !== undefined && typeof input.ocr !== "boolean") {
|
|
408
|
+
throw new TypeError("document extraction ocr must be a boolean");
|
|
409
|
+
}
|
|
410
|
+
if (input.bytes.byteLength > maxBytes) {
|
|
411
|
+
throw new DocumentExtractionError("inputLimit", `${REASON_MESSAGE.inputLimit} (${maxBytes})`);
|
|
412
|
+
}
|
|
413
|
+
const release = await occupy(input.signal);
|
|
414
|
+
try {
|
|
415
|
+
input.signal?.throwIfAborted();
|
|
416
|
+
// Copy so a subarray cannot expose a larger backing buffer to native code.
|
|
417
|
+
const bytes = new Uint8Array(input.bytes);
|
|
418
|
+
const image = input.ocr === true ? detectImage(bytes) : null;
|
|
419
|
+
if (image) {
|
|
420
|
+
if (!runner) throw new NeedsOcrError([], 1);
|
|
421
|
+
return await runOcr(runner, bytes, image, limits, input.signal);
|
|
422
|
+
}
|
|
423
|
+
const detected = anydoc.formatFromBytes(bytes);
|
|
424
|
+
let format = detected;
|
|
425
|
+
if (format === null) {
|
|
426
|
+
if (!isCsvName(input.filename) || anydoc.formatFromExtension(".csv") !== "csv") {
|
|
427
|
+
throw new DocumentExtractionError("unsupported");
|
|
428
|
+
}
|
|
429
|
+
format = "csv";
|
|
430
|
+
}
|
|
431
|
+
let markdown: string;
|
|
432
|
+
try {
|
|
433
|
+
// Two arguments only. A third argument is ConvertOptions and can set ocr:'hosted'.
|
|
434
|
+
markdown = await anydoc.toMarkdownBytes(bytes, format);
|
|
435
|
+
} catch (error) {
|
|
436
|
+
const mapped = mapError(error);
|
|
437
|
+
if (mapped instanceof NeedsOcrError && format === "pdf" && runner) {
|
|
438
|
+
if (mapped.pageCount > maxPages) throw new DocumentExtractionError("resourceLimit");
|
|
439
|
+
return await runOcr(runner, bytes, "pdf", limits, input.signal);
|
|
440
|
+
}
|
|
441
|
+
throw mapped;
|
|
442
|
+
}
|
|
443
|
+
input.signal?.throwIfAborted();
|
|
444
|
+
if (typeof markdown !== "string") throw new DocumentExtractionError("io");
|
|
445
|
+
if (Buffer.byteLength(markdown, "utf8") > maxTextBytes) {
|
|
446
|
+
throw new DocumentExtractionError("outputLimit", `${REASON_MESSAGE.outputLimit} (${maxTextBytes})`);
|
|
447
|
+
}
|
|
448
|
+
return { markdown, format, ocrUsed: false };
|
|
449
|
+
} finally {
|
|
450
|
+
release();
|
|
451
|
+
}
|
|
452
|
+
},
|
|
453
|
+
};
|
|
454
|
+
}
|
|
455
|
+
|
|
456
|
+
export interface DocumentIngestStats {
|
|
457
|
+
readonly extractions: number;
|
|
458
|
+
readonly ocrCount: number;
|
|
459
|
+
readonly durationMs: number;
|
|
460
|
+
}
|
|
461
|
+
|
|
462
|
+
export interface DocumentIngest {
|
|
463
|
+
/** Wiki `extractDocument` hook. Throws on extractor failure; does not return partial Markdown. */
|
|
464
|
+
extractDocument(input: {
|
|
465
|
+
readonly bytes: Uint8Array;
|
|
466
|
+
readonly filename?: string;
|
|
467
|
+
readonly mediaType?: string;
|
|
468
|
+
readonly title?: string;
|
|
469
|
+
readonly signal?: AbortSignal;
|
|
470
|
+
}): Promise<{ text: string; format: string }>;
|
|
471
|
+
/** RAG `Parser`. Metadata is trust plus provenance, never the Markdown body. */
|
|
472
|
+
readonly parser: {
|
|
473
|
+
parse(
|
|
474
|
+
document: { readonly uri: string; readonly data?: Uint8Array; readonly mediaType?: string },
|
|
475
|
+
options?: { readonly signal?: AbortSignal; readonly maxParseMs?: number },
|
|
476
|
+
): Promise<{ text: string; metadata: Record<string, string | number | boolean> }>;
|
|
477
|
+
};
|
|
478
|
+
/** True when image calls pass `ocr: true`. Hosts must set the same flag on wiki ingest. */
|
|
479
|
+
readonly ocrImages: boolean;
|
|
480
|
+
stats(): DocumentIngestStats;
|
|
481
|
+
}
|
|
482
|
+
|
|
483
|
+
const IMAGE_MEDIA = new Set(["image/png", "image/jpeg", "image/gif", "image/webp", "image/bmp", "image/tiff"]);
|
|
484
|
+
|
|
485
|
+
function uriName(uri: string): string | undefined {
|
|
486
|
+
const base = uri.split(/[/\\]/).pop();
|
|
487
|
+
if (!base || base.includes("\0") || base.length > 255) return undefined;
|
|
488
|
+
return base;
|
|
489
|
+
}
|
|
490
|
+
|
|
491
|
+
/** Host composition. Does not import the memory package. Counts and timings only — no document bodies. */
|
|
492
|
+
export function createDocumentIngest(
|
|
493
|
+
extractor: Pick<DocumentExtractor, "extract">,
|
|
494
|
+
options?: { readonly ocrImages?: boolean },
|
|
495
|
+
): DocumentIngest {
|
|
496
|
+
if (typeof extractor?.extract !== "function") {
|
|
497
|
+
throw new TypeError("document ingest requires an extractor");
|
|
498
|
+
}
|
|
499
|
+
const ocrImages = options?.ocrImages === true;
|
|
500
|
+
let extractions = 0;
|
|
501
|
+
let ocrCount = 0;
|
|
502
|
+
let durationMs = 0;
|
|
503
|
+
|
|
504
|
+
async function convert(input: {
|
|
505
|
+
readonly bytes: Uint8Array;
|
|
506
|
+
readonly filename?: string;
|
|
507
|
+
readonly mediaType?: string;
|
|
508
|
+
readonly signal?: AbortSignal;
|
|
509
|
+
}): Promise<DocumentExtractionResult> {
|
|
510
|
+
const started = Date.now();
|
|
511
|
+
try {
|
|
512
|
+
const image = ocrImages && (IMAGE_MEDIA.has(input.mediaType ?? "") || detectImage(input.bytes) !== null);
|
|
513
|
+
return await extractor.extract({
|
|
514
|
+
bytes: input.bytes,
|
|
515
|
+
filename: input.filename,
|
|
516
|
+
...(image ? { ocr: true } : {}),
|
|
517
|
+
signal: input.signal,
|
|
518
|
+
});
|
|
519
|
+
} finally {
|
|
520
|
+
durationMs += Date.now() - started;
|
|
521
|
+
}
|
|
522
|
+
}
|
|
523
|
+
|
|
524
|
+
return {
|
|
525
|
+
ocrImages,
|
|
526
|
+
stats: () => ({ extractions, ocrCount, durationMs }),
|
|
527
|
+
extractDocument: async (input) => {
|
|
528
|
+
const result = await convert(input);
|
|
529
|
+
extractions += 1;
|
|
530
|
+
if (result.ocrUsed) ocrCount += 1;
|
|
531
|
+
return { text: result.markdown, format: result.format };
|
|
532
|
+
},
|
|
533
|
+
parser: {
|
|
534
|
+
parse: async (document, parseOptions) => {
|
|
535
|
+
if (!(document.data instanceof Uint8Array)) throw new DocumentExtractionError("io");
|
|
536
|
+
const started = Date.now();
|
|
537
|
+
const result = await convert({
|
|
538
|
+
bytes: document.data,
|
|
539
|
+
filename: uriName(document.uri),
|
|
540
|
+
mediaType: document.mediaType,
|
|
541
|
+
signal: parseOptions?.signal,
|
|
542
|
+
});
|
|
543
|
+
if (parseOptions?.maxParseMs !== undefined && Date.now() - started > parseOptions.maxParseMs) {
|
|
544
|
+
throw new DocumentExtractionError("resourceLimit");
|
|
545
|
+
}
|
|
546
|
+
extractions += 1;
|
|
547
|
+
if (result.ocrUsed) ocrCount += 1;
|
|
548
|
+
return {
|
|
549
|
+
text: result.markdown,
|
|
550
|
+
metadata: {
|
|
551
|
+
untrusted: true,
|
|
552
|
+
inert: true,
|
|
553
|
+
injectionCapable: true,
|
|
554
|
+
format: result.format,
|
|
555
|
+
ocrUsed: result.ocrUsed,
|
|
556
|
+
uri: document.uri,
|
|
557
|
+
...(result.pages !== undefined ? { pages: result.pages } : {}),
|
|
558
|
+
},
|
|
559
|
+
};
|
|
560
|
+
},
|
|
561
|
+
},
|
|
562
|
+
};
|
|
563
|
+
}
|