mirror of
https://github.com/magnus919/agent-skills.git
synced 2026-09-11 19:47:12 +03:00
feat(anydoc): support explicit hosted OCR (#420)
* feat(anydoc): support explicit hosted OCR Closes #419 Signed-off-by: Magnus Hedemark <magnus919@pm.me> * test(anydoc): update release contract expectations Signed-off-by: Magnus Hedemark <magnus919@pm.me> * test(anydoc): align hosted OCR hint contract Signed-off-by: Magnus Hedemark <magnus919@pm.me> * chore: refresh generated marketplace Signed-off-by: Magnus Hedemark <magnus919@pm.me> * chore: refresh generated llms catalog Signed-off-by: Magnus Hedemark <magnus919@pm.me> --------- Signed-off-by: Magnus Hedemark <magnus919@pm.me>
This commit is contained in:
@@ -84,7 +84,7 @@
|
||||
"./anydoc"
|
||||
],
|
||||
"strict": false,
|
||||
"description": "Convert Word (.doc/.docx/.docm), PowerPoint (.ppt/.pps/.pot/.pptx/.pptm/.ppsx/.ppsm), Excel (.xls/.xlsx/.xlsm/.xlsb), OpenDocument (.odt/.ods/.odp), RTF, EPUB, CSV, and PDF documents to clean GitHub-Flavored Markdown locally with the Any Doc CLI (npx -y @firecrawl/anydoc@0.1.6): headings, GFM tables, slide structure, and footnotes in one pass. Use when a task needs the contents of an office document, spreadsheet, presentation, ebook, or PDF you cannot read directly. Do not use for generating, editing, or validating documents (use documents), for ebook packaging (use epub), or for OCR of scanned or image-only PDFs (anydoc does not OCR; route to OCR tooling)."
|
||||
"description": "Convert Word (.doc/.docx/.docm), PowerPoint (.ppt/.pps/.pot/.pptx/.pptm/.ppsx/.ppsm), Excel (.xls/.xlsx/.xlsm/.xlsb), OpenDocument (.odt/.ods/.odp), RTF, EPUB, CSV, and PDF documents to clean GitHub-Flavored Markdown locally with the Any Doc CLI (npx -y @firecrawl/anydoc@0.2.4): headings, GFM tables, slide structure, and footnotes in one pass. Use when a task needs the contents of an office document, spreadsheet, presentation, ebook, or PDF you cannot read directly. Do not use for generating, editing, or validating documents (use documents), for ebook packaging (use epub). For scanned or image-only PDFs, use hosted OCR only when the user explicitly authorizes whole-document upload; otherwise route to local OCR tooling."
|
||||
},
|
||||
{
|
||||
"name": "api-design-and-evolution",
|
||||
|
||||
+6
-5
@@ -1,12 +1,12 @@
|
||||
# anydoc — office documents to GitHub-Flavored Markdown
|
||||
|
||||
Convert Word, PowerPoint, Excel, OpenDocument, RTF, EPUB, CSV, and PDF files into clean, LLM-friendly GitHub-Flavored Markdown — entirely on your own machine, with no API keys and no file uploads. One command turns a report, spreadsheet, or slide deck into markdown you (or an agent) can read, summarize, quote, and feed into a knowledge base.
|
||||
Convert Word, PowerPoint, Excel, OpenDocument, RTF, EPUB, CSV, and PDF files into clean, LLM-friendly GitHub-Flavored Markdown. Local conversion stays on your machine; an explicitly authorized hosted OCR mode handles scanned PDFs through Firecrawl Parse when whole-document upload is acceptable.
|
||||
|
||||
## Why Install This Skill
|
||||
|
||||
Office documents are opaque to agents. A `.docx` or `.pptx` is a binary zip; a `.xls` is an OLE container; a PDF can be anything. Reading them directly means parsing formats, handling encodings, and reconstructing structure by hand — exactly the work anydoc automates. This skill gives your agent a single, verified command that converts all 8 format families (21 extensions) into GitHub-Flavored Markdown with headings, GFM tables, slide structure, and footnotes preserved, plus the knowledge of exactly where fidelity is lost (Excel number formats, legacy PowerPoint tables, PDF tables).
|
||||
|
||||
The skill wraps the pinned `@firecrawl/anydoc` v0.1.6 CLI with a small helper script that adds input checks, friendly error hints for the known failure classes (scanned PDFs, encrypted files, malformed archives), batch conversion, dry-run planning, and JSON output — so an agent gets predictable exit codes and messages instead of guessing. It also documents the exact error vocabulary of the real CLI, so failures like "PDF has no extractable text ... OCR is required" are recognized and routed correctly (to OCR tooling) rather than retried blindly.
|
||||
The skill wraps the pinned `@firecrawl/anydoc` v0.2.4 CLI with a small helper script that adds input checks, friendly error hints for the known failure classes (scanned PDFs, encrypted files, malformed archives), batch conversion, dry-run planning, JSON output, and an explicit `--allow-hosted-upload` acknowledgement for hosted OCR.
|
||||
|
||||
## What You Get
|
||||
|
||||
@@ -25,13 +25,13 @@ You need Node.js 20+ and `npx` (no other install — the CLI and its native bina
|
||||
|
||||
```bash
|
||||
cd anydoc
|
||||
npx -y @firecrawl/anydoc@0.1.6 fixtures/fixture-handmade-outline.docx
|
||||
npx -y @firecrawl/anydoc@0.2.4 fixtures/fixture-handmade-outline.docx
|
||||
```
|
||||
|
||||
This converts the sample Word document and prints GitHub-Flavored Markdown to stdout (note the `#`/`##`/`###` heading lines). To write to a file instead:
|
||||
|
||||
```bash
|
||||
npx -y @firecrawl/anydoc@0.1.6 fixtures/fixture-handmade-outline.docx -o outline.md
|
||||
npx -y @firecrawl/anydoc@0.2.4 fixtures/fixture-handmade-outline.docx -o outline.md
|
||||
```
|
||||
|
||||
Or use the wrapper for the same job:
|
||||
@@ -50,6 +50,7 @@ Load this skill when the task involves any of these:
|
||||
- "Turn this CSV into a markdown table"
|
||||
- "Read this document into markdown for a knowledge base or vault"
|
||||
- "Convert this PDF to markdown" — but only for text-based PDFs; scanned or image-only PDFs fail (anydoc does not OCR)
|
||||
- "OCR this scanned PDF" — use local OCR by default, or explicitly authorize `--ocr hosted --allow-hosted-upload` when sending the whole document to Firecrawl Parse is acceptable
|
||||
|
||||
Do **not** load this skill for document generation or editing ("create a docx report", "build a PDF proposal", "validate this document") — that is the `documents` skill's job — or for EPUB authoring (`epub` skill).
|
||||
|
||||
@@ -58,4 +59,4 @@ Do **not** load this skill for document generation or editing ("create a docx re
|
||||
- **Node.js >= 20** and `npx` (the CLI is distributed via npm; the native binary ships as a platform-specific npm `optionalDependency`, so there is no manual install or compilation).
|
||||
- **Network once** — the first `npx` run downloads the package and binary; later runs use the npm cache. For permanent or fully offline use, run `npm install -g @firecrawl/anydoc` once.
|
||||
- **Python 3** (standard library only) if you use the `scripts/anydoc` wrapper.
|
||||
- **No API keys, no services** — conversion happens locally; files never leave your machine.
|
||||
- **Local mode needs no API key or service**. Hosted OCR uses Firecrawl Parse and may use `FIRECRAWL_API_KEY`; it sends the whole OCR-required PDF and has no page selection.
|
||||
|
||||
+41
-19
@@ -4,19 +4,19 @@ description: >-
|
||||
Convert Word (.doc/.docx/.docm), PowerPoint (.ppt/.pps/.pot/.pptx/.pptm/.ppsx/.ppsm),
|
||||
Excel (.xls/.xlsx/.xlsm/.xlsb), OpenDocument (.odt/.ods/.odp), RTF, EPUB, CSV, and
|
||||
PDF documents to clean GitHub-Flavored Markdown locally with the Any Doc CLI
|
||||
(npx -y @firecrawl/anydoc@0.1.6): headings, GFM tables, slide structure, and
|
||||
(npx -y @firecrawl/anydoc@0.2.4): headings, GFM tables, slide structure, and
|
||||
footnotes in one pass. Use when a task needs the contents of an office document,
|
||||
spreadsheet, presentation, ebook, or PDF you cannot read directly. Do not use for
|
||||
generating, editing, or validating documents (use documents), for ebook packaging
|
||||
(use epub), or for OCR of scanned or image-only PDFs (anydoc does not OCR; route
|
||||
to OCR tooling).
|
||||
(use epub). For scanned or image-only PDFs, use hosted OCR only when the user
|
||||
explicitly authorizes whole-document upload; otherwise route to local OCR tooling.
|
||||
license: MIT
|
||||
compatibility: >-
|
||||
Node.js >= 20 and npx. The pinned CLI is @firecrawl/anydoc@0.1.6; the native
|
||||
Node.js >= 20 and npx. The pinned CLI is @firecrawl/anydoc@0.2.4; the native
|
||||
binary ships via npm optionalDependencies (no install step, no postinstall, no
|
||||
compilation). Conversion runs entirely on your machine — no services, no API
|
||||
keys, no uploads. The first npx run downloads the package once (network
|
||||
required); later runs use the npm cache.
|
||||
compilation). Local conversion needs no service or API key. Hosted OCR sends the
|
||||
whole PDF to Firecrawl Parse and may use FIRECRAWL_API_KEY. The first npx run
|
||||
downloads the package once (network required); later runs use the npm cache.
|
||||
metadata:
|
||||
skills: anydoc, markdown, conversion, docx, xlsx, pptx, pdf, odt, ods, odp, rtf, epub, csv, office, documents, firecrawl
|
||||
tags: conversion, markdown, office, documents
|
||||
@@ -28,9 +28,9 @@ allowed-tools: Bash Read
|
||||
|
||||
The `anydoc` skill converts office documents, spreadsheets, presentations,
|
||||
ebooks, CSV, and text-based PDFs into GitHub-Flavored Markdown using the pinned
|
||||
Any Doc CLI (`@firecrawl/anydoc` v0.1.6). One shared document model and one GFM
|
||||
serializer produce the same logical output across formats, and conversion runs
|
||||
locally in milliseconds — no service, no API key, no file upload.
|
||||
Any Doc CLI (`@firecrawl/anydoc` v0.2.4). One shared document model and one GFM
|
||||
serializer produce the same logical output across formats. Local conversion runs
|
||||
without a service, API key, or file upload; hosted OCR is a separate explicit route.
|
||||
|
||||
## Overview
|
||||
|
||||
@@ -51,11 +51,11 @@ Before invoking anydoc, classify the request:
|
||||
| If the user needs... | Do this |
|
||||
| --- | --- |
|
||||
| The contents of an existing supported document | Continue to [Command Map](#command-map). |
|
||||
| Generation, editing, validation, EPUB packaging, HTML scraping, OCR, or password decryption | Stop and use the route in [When not to use](#when-not-to-use). |
|
||||
| Generation, editing, validation, EPUB packaging, HTML scraping, or password decryption | Stop and use the route in [When not to use](#when-not-to-use). |
|
||||
| A format-fidelity or failure decision | Load the matching row in [Reference Routing](#reference-routing) before choosing a command. |
|
||||
| A conversion result | Choose stdout, `-o`, or batch; run it; then follow [Verification](#verification). |
|
||||
|
||||
> **Hard boundary:** anydoc reads existing supported documents to Markdown. It does not create, edit, validate, package, OCR, decrypt, or scrape them.
|
||||
> **Hard boundary:** local anydoc conversion reads existing supported documents to Markdown without uploading them. Hosted OCR is opt-in only: it sends the whole OCR-required PDF to the configured Parse service. AnyDoc does not create, edit, validate, package, decrypt, or scrape documents.
|
||||
|
||||
## When to use
|
||||
|
||||
@@ -85,7 +85,7 @@ epub, xlsx, ods, odp, csv`; extension aliases map through them (`.docm`→docx,
|
||||
| Rich Text Format | `.rtf` | same document shape as DOCX/ODT | Use for text extraction, not layout preservation. |
|
||||
| EPUB | `.epub` | `#` chapter headings, GFM tables, internal anchor links | Use to read an existing EPUB; use `epub` to author or package one. |
|
||||
| CSV | `.csv` | one GFM table; label-like first row promoted to header; delimiter sniffing; UTF-16 with BOM | Use for delimited tabular content; inspect delimiter and encoding when output looks wrong. |
|
||||
| PDF | `.pdf` | headings + inline emphasis, but a lower-fidelity pipeline: tables flatten to text, footnotes and links degrade. **Scanned or image-only PDFs fail** — anydoc does not OCR | Use only for text-based PDFs; route scanned PDFs to OCR and treat tables as lower fidelity. |
|
||||
| PDF | `.pdf` | headings + inline emphasis, but a lower-fidelity pipeline: tables flatten to text, footnotes and links degrade. Text-based PDFs stay local; scanned/image-only PDFs require explicit hosted OCR or another OCR tool | Use local mode by default; hosted mode uploads the whole PDF and has no page selection. |
|
||||
|
||||
See [references/formats.md](references/formats.md) for the full per-format
|
||||
expectations and fidelity caveats, and [references/errors.md](references/errors.md)
|
||||
@@ -103,22 +103,44 @@ the document from stdin.
|
||||
| Convert one file to a markdown file | `anydoc/scripts/anydoc convert <file> -o out.md` | The output is large, must be reviewed later, or should be preserved as an artifact. |
|
||||
| Convert many files to a directory | `anydoc/scripts/anydoc batch <file1> <file2> ... --out-dir out/` | The request is a bounded batch and per-file output/status is useful. |
|
||||
| Show the tool and pinned CLI version | `anydoc/scripts/anydoc info` | You need to confirm the executable and version before troubleshooting or reporting an environment issue. |
|
||||
| Raw pinned CLI, one document | `npx -y @firecrawl/anydoc@0.1.6 <file> [-o out.md]` | The wrapper is unavailable; preserve the pinned CLI and its documented semantics. |
|
||||
| Raw pinned CLI, read stdin | `cat data.csv \| npx -y @firecrawl/anydoc@0.1.6 - --format csv` | Bytes already arrive on stdin and the format is known; keep the producer pipeline separate from the converter. |
|
||||
| Raw pinned CLI, one document | `npx -y @firecrawl/anydoc@0.2.4 <file> [-o out.md]` | The wrapper is unavailable; preserve the pinned CLI and its documented semantics. |
|
||||
| Raw pinned CLI, read stdin | `cat data.csv \| npx -y @firecrawl/anydoc@0.2.4 - --format csv` | Bytes already arrive on stdin and the format is known; keep the producer pipeline separate from the converter. |
|
||||
|
||||
For an OCR-required PDF, first use the local default so the failure is visible:
|
||||
|
||||
```bash
|
||||
anydoc/scripts/anydoc convert scan.pdf --ocr reject
|
||||
```
|
||||
|
||||
If the user explicitly authorizes sending the complete PDF to Firecrawl Parse,
|
||||
use the wrapper acknowledgement and a trusted `FIRECRAWL_API_KEY` environment
|
||||
variable when needed:
|
||||
|
||||
```bash
|
||||
anydoc/scripts/anydoc convert scan.pdf --ocr hosted --allow-hosted-upload
|
||||
```
|
||||
|
||||
The wrapper never places the key on the command line. Hosted OCR has no page
|
||||
selection, and a hosted failure is not permission to silently switch endpoints.
|
||||
|
||||
Notes:
|
||||
|
||||
- `scripts/anydoc` is an executable Python 3 script (shebang `#!/usr/bin/env
|
||||
python3`); `python3 anydoc/scripts/anydoc ...` is equivalent when the
|
||||
executable bit is unavailable.
|
||||
- The raw `npx -y @firecrawl/anydoc@0.1.6` rows are the ground truth for
|
||||
- The raw `npx -y @firecrawl/anydoc@0.2.4` rows are the ground truth for
|
||||
conversion behavior; the wrapper delegates to exactly that command.
|
||||
- Always pin `@0.1.6` for reproducible conversions. `-y` answers npx's
|
||||
- Always pin `@0.2.4` for reproducible conversions. `-y` answers npx's
|
||||
"Ok to proceed?" prompt non-interactively — the CLI itself never prompts.
|
||||
- Both forms share the same contract: one document per invocation, exit code
|
||||
`0` success / `1` conversion or IO failure / `2` usage error, diagnostics as
|
||||
exactly one `anydoc: <message>` line on stderr, and no prompts.
|
||||
|
||||
Hosted OCR is supported by the 0.2.4 library and CLI, but the wrapper requires
|
||||
both `--ocr hosted` and `--allow-hosted-upload` so an upload cannot be selected
|
||||
implicitly. The hosted route sends the complete PDF to Firecrawl Parse because
|
||||
page selection is unavailable. Do not place API keys on the command line.
|
||||
|
||||
## Reference Routing
|
||||
|
||||
Load only the row that answers the immediate question; the command examples and verification contract remain in this file.
|
||||
@@ -140,7 +162,7 @@ Use this routing table before reaching for a conversion command:
|
||||
| --- | --- | --- |
|
||||
| Generate, edit, inspect rendered layout, or validate a PDF/Word/Excel/PowerPoint artifact | `documents` skill | anydoc extracts existing document contents to Markdown; it does not author, preserve rendered layout, or validate artifacts. |
|
||||
| Package or author an EPUB | `epub` skill | anydoc reads an existing EPUB to Markdown but never writes or validates an EPUB container. |
|
||||
| OCR a scanned or image-only PDF | OCR tooling or the hosted Firecrawl Parse API | anydoc has no OCR path; report the documented unsupported error and do not retry locally. |
|
||||
| OCR a scanned or image-only PDF | Local OCR tooling, or AnyDoc hosted OCR after explicit authorization | Local mode reports the OCR-required error without uploading; hosted mode sends the whole PDF to Firecrawl Parse. |
|
||||
| Scrape HTML or other web content | A web-scraping skill | HTML is not a supported anydoc input. |
|
||||
| Transcribe binary media such as images, video, or audio | A media or transcription tool | Embedded images become alt text; anydoc cannot transcribe media. |
|
||||
| Preserve pagination, fonts, templates, or rendered layout | A document/layout tool | The only output contract is GitHub-Flavored Markdown. |
|
||||
@@ -164,7 +186,7 @@ Caveat/route: <documented limitation or next action>
|
||||
|
||||
| Condition | Do not | Next |
|
||||
| --- | --- | --- |
|
||||
| Scanned or image-only PDF / OCR-required error | Retry the same file locally | Route to OCR tooling or the hosted Firecrawl Parse API. |
|
||||
| Scanned or image-only PDF / OCR-required error | Retry unchanged or upload implicitly | Use local OCR, or explicitly authorize and run `--ocr hosted --allow-hosted-upload`; page selection is unavailable. |
|
||||
| Encrypted or password-protected document | Guess a password or retry unchanged | Request an unencrypted copy or owner-authorized re-export. |
|
||||
| Unsupported, malformed, or resource-limit error | Guess a parser or claim partial success | Match the exact error in [references/errors.md](references/errors.md) and follow its bounded route. |
|
||||
| Exit 0 but expected structural markers are absent | Report success from the exit code alone | Inspect the output shape and source fidelity before reporting completion. |
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
# CLI reference: the Any Doc CLI (pinned @firecrawl/anydoc@0.1.6)
|
||||
# CLI reference: the Any Doc CLI (pinned @firecrawl/anydoc@0.2.4)
|
||||
|
||||
Everything here was captured by running the pinned CLI on this machine
|
||||
(`npx -y @firecrawl/anydoc@0.1.6`, version 0.1.6, Node v22). The CLI is a 4.7 KB
|
||||
(`npx -y @firecrawl/anydoc@0.2.4`, version 0.2.4, Node v22). The CLI is a 4.7 KB
|
||||
Node wrapper (`bin.anydoc = cli.js`) around a native NAPI binding that ships as
|
||||
an npm `optionalDependency` per platform.
|
||||
|
||||
@@ -24,6 +24,8 @@ Options:
|
||||
doc, docx, odt, pdf, ppt, pptx, rtf, epub, xlsx, ods, odp, csv
|
||||
(extension aliases like xls, docm, ppsx resolve
|
||||
to these)
|
||||
--ocr <mode> `reject` (default) or `hosted` for OCR-required PDFs
|
||||
--allow-hosted-upload Acknowledges whole-document upload for hosted OCR
|
||||
-h, --help Print this help and exit
|
||||
-V, --version Print the version and exit
|
||||
|
||||
@@ -44,7 +46,7 @@ Examples:
|
||||
curl -s https://example.com/paper.pdf | anydoc -
|
||||
```
|
||||
|
||||
`anydoc --version` prints exactly `0.1.6` (verified; both `--help` and
|
||||
`anydoc --version` prints exactly `0.2.4` (verified; both `--help` and
|
||||
`--version` exit 0 and write to stdout).
|
||||
|
||||
## Invocation forms
|
||||
@@ -70,7 +72,7 @@ anydoc - [options] < file # read the document from stdin
|
||||
| `-o <path>`, `--output <path>` | Write the Markdown to `<path>` instead of stdout. **Silently overwrites** an existing file (verified). Writing to a directory fails with exit 1: `anydoc: EISDIR: illegal operation on a directory, open '<path>'`. With `-o`, stdout stays silent. |
|
||||
| `-f <fmt>`, `--format <fmt>` | Force the input format instead of detecting it. Values: `doc, docx, odt, pdf, ppt, pptx, rtf, epub, xlsx, ods, odp, csv`. Extension aliases resolve through the parser mapping (verified: `--format xls`, `--format docm` accepted). Invalid value → exit 2: `anydoc: invalid format 'bogus'; expected one of: doc, docx, odt, pdf, ppt, pptx, rtf, epub, xlsx, ods, odp, csv`. |
|
||||
| `-h`, `--help` | Print help to stdout, exit 0. Works even when the native binding is unavailable. |
|
||||
| `-V`, `--version` | Print the version (`0.1.6`) to stdout, exit 0. Binding-independent like `--help`. |
|
||||
| `-V`, `--version` | Print the version (`0.2.4`) to stdout, exit 0. Binding-independent like `--help`. |
|
||||
| `--format=x` | Inline `=` value syntax is supported for long options (verified: `--format=rtf` works). |
|
||||
| `--` | End of options: everything after `--` is treated as a positional input (a filename starting with `-`). |
|
||||
| Missing option value | `anydoc: <option> requires a value` → exit 2 (e.g. `anydoc: -o requires a value`). |
|
||||
@@ -86,7 +88,7 @@ anydoc - [options] < file # read the document from stdin
|
||||
Verified success pattern:
|
||||
|
||||
```bash
|
||||
printf 'name,role\nAlice,Engineer\n' | npx -y @firecrawl/anydoc@0.1.6 - --format csv
|
||||
printf 'name,role\nAlice,Engineer\n' | npx -y @firecrawl/anydoc@0.2.4 - --format csv
|
||||
```
|
||||
|
||||
- **Markdown goes to stdout only.** With `-o`, stdout stays silent.
|
||||
@@ -103,18 +105,18 @@ anydoc - [options] < file # read the document from stdin
|
||||
## Running it: npx invocation
|
||||
|
||||
```bash
|
||||
npx -y @firecrawl/anydoc@0.1.6 report.docx # markdown to stdout
|
||||
npx -y @firecrawl/anydoc@0.1.6 slides.pptx -o slides.md # to a file
|
||||
npx -y @firecrawl/anydoc@0.1.6 - --format csv < data.csv # stdin (CSV needs --format)
|
||||
curl -s https://example.com/paper.pdf | npx -y @firecrawl/anydoc@0.1.6 - # URL → stdin
|
||||
npx -y @firecrawl/anydoc@0.2.4 report.docx # markdown to stdout
|
||||
npx -y @firecrawl/anydoc@0.2.4 slides.pptx -o slides.md # to a file
|
||||
npx -y @firecrawl/anydoc@0.2.4 - --format csv < data.csv # stdin (CSV needs --format)
|
||||
curl -s https://example.com/paper.pdf | npx -y @firecrawl/anydoc@0.2.4 - # URL → stdin
|
||||
```
|
||||
|
||||
### Version pinning
|
||||
|
||||
Always pin the version: `npx -y @firecrawl/anydoc@0.1.6`. An unpinned
|
||||
Always pin the version: `npx -y @firecrawl/anydoc@0.2.4`. An unpinned
|
||||
invocation (`npx -y @firecrawl/anydoc` with no `@version` suffix) floats to the
|
||||
latest published tag, so conversions are not reproducible across time. All
|
||||
behavior in this skill is documented against **0.1.6**. The `-y` flag answers
|
||||
behavior in this skill is documented against **0.2.4**. The `-y` flag answers
|
||||
npx's "Ok to proceed?" install prompt non-interactively — omitting it means npx
|
||||
asks for confirmation before installing a cold-cache package.
|
||||
|
||||
@@ -122,8 +124,8 @@ asks for confirmation before installing a cold-cache package.
|
||||
|
||||
- The **first** `npx` invocation downloads the npm package plus the native
|
||||
platform binary (network required once). Verified with a fresh empty npm
|
||||
cache: `env npm_config_cache=$(mktemp -d) npx -y @firecrawl/anydoc@0.1.6 --version`
|
||||
prints `0.1.6` and exits 0.
|
||||
cache: `env npm_config_cache=$(mktemp -d) npx -y @firecrawl/anydoc@0.2.4 --version`
|
||||
prints `0.2.4` and exits 0.
|
||||
- Later runs reuse the npm cache; measured warm startup is ~0.33–0.55 s per
|
||||
invocation (see [workflows.md](workflows.md)).
|
||||
- **Cold-cache offline**: if the package is not cached and there is no network,
|
||||
@@ -141,7 +143,7 @@ asks for confirmation before installing a cold-cache package.
|
||||
package per platform (`darwin-x64`, `darwin-arm64`, `linux-x64-gnu`,
|
||||
`linux-arm64-gnu`, `linux-x64-musl`, `linux-arm64-musl`, `win32-x64-msvc`),
|
||||
with **no postinstall script and no compilation**.
|
||||
- The npm package `@firecrawl/anydoc` 0.1.6 is ~48 KB unpacked (the binding
|
||||
- The npm package `@firecrawl/anydoc` 0.2.4 is ~48 KB unpacked (the binding
|
||||
package is a few MB per platform); published 2026-08-05T18:29:40Z.
|
||||
- The Rust crate `anydoc` (crates.io) and Python wheels `firecrawl-anydoc`
|
||||
(PyPI, imports as `anydoc`, Python >= 3.10) ship in the same release train.
|
||||
@@ -173,8 +175,8 @@ delegates to the pinned CLI. It adds value beyond a thin npx alias:
|
||||
inputs from different directories collide on the same `<stem>.md` and the
|
||||
last one wins.
|
||||
- **`info [--version]`** — reports the tool name and the pinned CLI version
|
||||
(`anydoc 0.1.6 (wraps @firecrawl/anydoc@0.1.6)`) without invoking the
|
||||
converter; `info --version` prints exactly `0.1.6`.
|
||||
(`anydoc 0.2.4 (wraps @firecrawl/anydoc@0.2.4)`) without invoking the
|
||||
converter; `info --version` prints exactly `0.2.4`.
|
||||
- Global **`--json`** (exactly one JSON document on stdout; diagnostics stay
|
||||
on stderr) and **`--dry-run`** (print what would run — the exact `npx`
|
||||
command line and output paths — and execute nothing: no CLI spawn, no
|
||||
@@ -183,7 +185,7 @@ delegates to the pinned CLI. It adds value beyond a thin npx alias:
|
||||
- Checks for Node >= 20 (missing `node`, or a version below 20, exits 1 with
|
||||
a clear message naming Node.js and the required version) and for `npx`
|
||||
(missing `npx` exits 1 naming `npx` and the pinned package
|
||||
`@firecrawl/anydoc@0.1.6`); always invokes npx with `-y`; never prompts;
|
||||
`@firecrawl/anydoc@0.2.4`); always invokes npx with `-y`; never prompts;
|
||||
exit codes 0/1/2 mirror the CLI.
|
||||
|
||||
Run it as `anydoc/scripts/anydoc <subcommand> ...` from the repository root,
|
||||
|
||||
+11
-13
@@ -1,7 +1,7 @@
|
||||
# Errors, exit codes, and troubleshooting
|
||||
|
||||
Every message below is a **verbatim real stderr capture** from the pinned CLI
|
||||
(`@firecrawl/anydoc@0.1.6`) run against the committed fixtures in `fixtures/`
|
||||
(`@firecrawl/anydoc@0.2.4`) run against the committed fixtures in `fixtures/`
|
||||
(and, for resource limits, generated oversized archives). The CLI prints
|
||||
exactly one line to stderr, prefixed `anydoc: `, and never prompts.
|
||||
|
||||
@@ -53,7 +53,7 @@ without `--format csv` fails:
|
||||
anydoc: unsupported input: unrecognized file content: name the format explicitly
|
||||
```
|
||||
|
||||
Fix: add `--format csv` (e.g. `cat data.csv | npx -y @firecrawl/anydoc@0.1.6 - --format csv`).
|
||||
Fix: add `--format csv` (e.g. `cat data.csv | npx -y @firecrawl/anydoc@0.2.4 - --format csv`).
|
||||
|
||||
### malformed — structurally unusable archive
|
||||
|
||||
@@ -110,7 +110,7 @@ conversion is **not streaming**, and the whole entry is checked before use.
|
||||
anydoc: EISDIR: illegal operation on a directory, open '<path>'
|
||||
```
|
||||
|
||||
Verified: `npx -y @firecrawl/anydoc@0.1.6 report.rtf -o /tmp` prints
|
||||
Verified: `npx -y @firecrawl/anydoc@0.2.4 report.rtf -o /tmp` prints
|
||||
`anydoc: EISDIR: illegal operation on a directory, open '/tmp'` and exits 1.
|
||||
Fix: pass a file path (or a path in a directory that exists); anydoc **does
|
||||
not create directories**.
|
||||
@@ -137,17 +137,15 @@ Notes:
|
||||
|
||||
## The no-OCR caveat (read before converting PDFs)
|
||||
|
||||
- anydoc converts **text-based PDFs locally** via `pdf-inspector`; there is no
|
||||
OCR service anywhere in the pipeline.
|
||||
- anydoc converts **text-based PDFs locally** via `pdf-inspector`; hosted OCR is
|
||||
a separate opt-in path for OCR-required PDFs.
|
||||
- **Scanned / image-only PDFs fail as `unsupported`** with the exact message
|
||||
above (`... OCR is required`). The library's stance: "Scanned and image-only
|
||||
PDFs need OCR, which anydoc does not do."
|
||||
- **Route, don't retry.** When this message fires: report the exact error,
|
||||
state that OCR is required, and direct the user to OCR tooling or the hosted
|
||||
Firecrawl Parse API. Do **not** retry the same file locally, do **not**
|
||||
claim anydoc can OCR, and do **not** fabricate the document's content.
|
||||
- There is no password option, no OCR option, and no retry-until-success
|
||||
behavior to enable.
|
||||
- Route, don't retry unchanged. Report the exact error and choose local OCR or,
|
||||
after explicit authorization, `--ocr hosted --allow-hosted-upload`. Hosted
|
||||
mode sends the whole document to Firecrawl Parse and has no page selection.
|
||||
Do not fabricate the document's content or silently upload it.
|
||||
|
||||
## Troubleshooting recipes
|
||||
|
||||
@@ -204,8 +202,8 @@ The wrapper mirrors the CLI's contract and adds pre-validation and hints:
|
||||
but `node` was not found on PATH ...` / `anydoc: Node.js version v18.20.0 is
|
||||
too old; anydoc requires Node.js >= 20 ...`), before any CLI invocation.
|
||||
- **npx missing (exit 1)**: stderr names `npx` and the pinned package
|
||||
(`@firecrawl/anydoc@0.1.6`): `anydoc: `npx` was not found on PATH —
|
||||
conversion runs via `npx -y @firecrawl/anydoc@0.1.6`. Install Node.js >= 20
|
||||
(`@firecrawl/anydoc@0.2.4`): `anydoc: `npx` was not found on PATH —
|
||||
conversion runs via `npx -y @firecrawl/anydoc@0.2.4`. Install Node.js >= 20
|
||||
(which ships npx), or install the CLI permanently with `npm install -g
|
||||
@firecrawl/anydoc`.`.
|
||||
- **Batch exit policy**: `batch` exits 1 when any input failed; per-file
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
# Formats: what anydoc converts and what GFM you get
|
||||
|
||||
This reference documents every input format the pinned CLI (`@firecrawl/anydoc`
|
||||
v0.1.6) accepts, the GitHub-Flavored Markdown each one produces, and the
|
||||
v0.2.4) accepts, the GitHub-Flavored Markdown each one produces, and the
|
||||
fidelity caveats you must know before trusting the output. Every claim below
|
||||
was verified by running the real CLI against the committed fixtures in
|
||||
`fixtures/` (see [sources.md](sources.md) for provenance and the verification
|
||||
@@ -291,7 +291,7 @@ as normal markdown links.
|
||||
|
||||
Expected output: the file renders as **one GFM table**. The first row is
|
||||
**promoted to the header row** when it looks like labels (≥ 2 columns,
|
||||
non-empty, non-numeric, distinct fields) — this behavior ships in 0.1.6.
|
||||
non-empty, non-numeric, distinct fields) — this behavior ships in 0.2.4.
|
||||
Quoted fields with embedded commas and newlines are preserved.
|
||||
|
||||
Real conversion of `fixtures/fixture-handmade-quoted.csv`:
|
||||
@@ -366,7 +366,7 @@ Wide head End Tall B2 C2 B3 C3
|
||||
- Numbered/bulleted list structure compresses (markers inline), and some
|
||||
Unicode degrades (e.g. emoji without ZWJ).
|
||||
|
||||
### Scanned or image-only PDFs — no OCR
|
||||
### Scanned or image-only PDFs — explicit hosted OCR
|
||||
|
||||
A PDF with **no extractable text layer** fails as `unsupported` with this exact
|
||||
message (exit code 1):
|
||||
@@ -375,11 +375,12 @@ message (exit code 1):
|
||||
anydoc: unsupported input: PDF has no extractable text (Scanned, 1 pages): OCR is required
|
||||
```
|
||||
|
||||
anydoc **does not perform OCR** — the library's stance is explicit, and there
|
||||
is no password, retry, or OCR option. When this message fires: report the exact
|
||||
error, state that OCR is required, and route the file to OCR tooling or the
|
||||
hosted Firecrawl Parse API. Do not retry the same file locally and do not claim
|
||||
anydoc can OCR it. See [errors.md](errors.md) for the full routing guidance.
|
||||
The local default does not perform OCR. When this message fires, report the
|
||||
exact error and either route the file to local OCR tooling or, only after explicit
|
||||
authorization, rerun with `--ocr hosted --allow-hosted-upload`. Hosted mode sends
|
||||
the whole document to Firecrawl Parse because page selection is unavailable. Do
|
||||
not select hosted mode implicitly or claim hosted accuracy from the upstream
|
||||
announcement. See [errors.md](errors.md) for routing guidance.
|
||||
|
||||
## Formats anydoc does NOT support
|
||||
|
||||
|
||||
@@ -15,8 +15,8 @@
|
||||
|
||||
- Research and empirical verification performed **2026-08-05** and **2026-08-06**
|
||||
on macOS (arm64) with Node v22.22.3, network access, and the pinned CLI
|
||||
`npx -y @firecrawl/anydoc@0.1.6`.
|
||||
- The pinned release **0.1.6** was published to npm at
|
||||
`npx -y @firecrawl/anydoc@0.2.4`.
|
||||
- The pinned release **0.2.4** was published to npm at
|
||||
**2026-08-05T18:29:40Z**; PyPI wheels for the same version were uploaded
|
||||
**2026-08-05T18:29Z**. First release was 0.1.1 (2026-08-04).
|
||||
|
||||
@@ -58,10 +58,10 @@ samples carry no upstream copyright.
|
||||
## Verification procedure
|
||||
|
||||
Every factual claim in this skill was confirmed against the **real pinned CLI**
|
||||
(v0.1.6), not inferred from documentation:
|
||||
(v0.2.4), not inferred from documentation:
|
||||
|
||||
1. **Environment warm-up**: `node --version` (v22.22.3 ≥ 20), then
|
||||
`npx -y @firecrawl/anydoc@0.1.6 --version` → prints `0.1.6`; `--help` →
|
||||
`npx -y @firecrawl/anydoc@0.2.4 --version` → prints `0.2.4`; `--help` →
|
||||
the verbatim help block reproduced in
|
||||
[cli-reference.md](cli-reference.md).
|
||||
2. **Positive conversions**: the pinned CLI was run on every committed fixture
|
||||
@@ -85,9 +85,9 @@ Every factual claim in this skill was confirmed against the **real pinned CLI**
|
||||
documented `max_entry_bytes` prefix).
|
||||
5. **First-run/offline**: a fresh empty npm cache was used to verify the
|
||||
first-run download path (`env npm_config_cache=$(mktemp -d) npx -y
|
||||
@firecrawl/anydoc@0.1.6 --version` → `0.1.6`, exit 0).
|
||||
@firecrawl/anydoc@0.2.4 --version` → `0.2.4`, exit 0).
|
||||
6. **Startup timing**: repeated warm invocations were timed
|
||||
(`/usr/bin/time -p npx -y @firecrawl/anydoc@0.1.6 ...`) — ~0.32–0.35 s each,
|
||||
(`/usr/bin/time -p npx -y @firecrawl/anydoc@0.2.4 ...`) — ~0.32–0.35 s each,
|
||||
consistent with the documented ~0.33–0.55 s warm-cache startup range.
|
||||
|
||||
Repository checks applied after authoring: frontmatter and structure
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
# Workflows: recipes for converting documents to markdown
|
||||
|
||||
All recipes use the pinned CLI `npx -y @firecrawl/anydoc@0.1.6` (ground truth)
|
||||
All recipes use the pinned CLI `npx -y @firecrawl/anydoc@0.2.4` (ground truth)
|
||||
and the skill's wrapper `scripts/anydoc` where it adds value. Commands are
|
||||
shown relative to the repository root; `anydoc/fixtures/...` paths can be
|
||||
replaced with any document path. The vault-ingestion recipe (section 5) is
|
||||
@@ -12,10 +12,10 @@ is no batch mode.
|
||||
|
||||
```bash
|
||||
# Markdown to stdout
|
||||
npx -y @firecrawl/anydoc@0.1.6 anydoc/fixtures/fixture-handmade-outline.docx
|
||||
npx -y @firecrawl/anydoc@0.2.4 anydoc/fixtures/fixture-handmade-outline.docx
|
||||
|
||||
# Markdown to a file (stdout stays silent; existing file is overwritten)
|
||||
npx -y @firecrawl/anydoc@0.1.6 anydoc/fixtures/fixture-handmade-outline.docx -o outline.md
|
||||
npx -y @firecrawl/anydoc@0.2.4 anydoc/fixtures/fixture-handmade-outline.docx -o outline.md
|
||||
|
||||
# Same jobs through the wrapper
|
||||
python3 anydoc/scripts/anydoc convert anydoc/fixtures/fixture-handmade-outline.docx
|
||||
@@ -30,8 +30,8 @@ lines.
|
||||
|
||||
```bash
|
||||
# Extensionless or mislabeled file: name the format explicitly
|
||||
npx -y @firecrawl/anydoc@0.1.6 ./data --format csv
|
||||
npx -y @firecrawl/anydoc@0.1.6 ./report --format docx
|
||||
npx -y @firecrawl/anydoc@0.2.4 ./data --format csv
|
||||
npx -y @firecrawl/anydoc@0.2.4 ./report --format docx
|
||||
```
|
||||
|
||||
Use `--format <name>` only when detection cannot work (CSV from stdin, or a
|
||||
@@ -43,10 +43,10 @@ missing/wrong extension). Aliases resolve: `--format xls`, `--format docm`,
|
||||
|
||||
```bash
|
||||
# CSV from stdin requires --format csv (no signature, no extension)
|
||||
printf 'name,role\nAlice,Engineer\n' | npx -y @firecrawl/anydoc@0.1.6 - --format csv
|
||||
printf 'name,role\nAlice,Engineer\n' | npx -y @firecrawl/anydoc@0.2.4 - --format csv
|
||||
|
||||
# Any document type can come from stdin; detection reads the bytes
|
||||
curl -s https://example.com/paper.pdf | npx -y @firecrawl/anydoc@0.1.6 -
|
||||
curl -s https://example.com/paper.pdf | npx -y @firecrawl/anydoc@0.2.4 -
|
||||
```
|
||||
|
||||
The wrapper supports the same: `cat data.csv | python3 anydoc/scripts/anydoc convert - -f csv`.
|
||||
@@ -56,7 +56,7 @@ Piping notes:
|
||||
- Markdown goes to **stdout only**; diagnostics are the single
|
||||
`anydoc: <message>` stderr line.
|
||||
- **EPIPE is handled**: if the downstream pipe closes early
|
||||
(`... anydoc@0.1.6 big.xlsx | head -n 1`), the CLI exits 0 with no stderr
|
||||
(`... anydoc@0.2.4 big.xlsx | head -n 1`), the CLI exits 0 with no stderr
|
||||
noise — piping into `head` is safe and is not a failure.
|
||||
|
||||
## 4. Batch conversion (raw CLI)
|
||||
@@ -66,7 +66,7 @@ The raw CLI takes one document per invocation, so batch with a shell loop:
|
||||
```bash
|
||||
mkdir -p out
|
||||
for f in anydoc/fixtures/*.docx; do
|
||||
npx -y @firecrawl/anydoc@0.1.6 "$f" -o "out/$(basename "${f%.docx}").md"
|
||||
npx -y @firecrawl/anydoc@0.2.4 "$f" -o "out/$(basename "${f%.docx}").md"
|
||||
done
|
||||
```
|
||||
|
||||
@@ -148,9 +148,9 @@ Before treating a conversion as done:
|
||||
Use the committed fixtures to sanity-check an environment once:
|
||||
|
||||
```bash
|
||||
npx -y @firecrawl/anydoc@0.1.6 anydoc/fixtures/fixture-handmade-outline.docx # headings
|
||||
npx -y @firecrawl/anydoc@0.1.6 anydoc/fixtures/sheet.xlsx # ## Values + table
|
||||
npx -y @firecrawl/anydoc@0.1.6 anydoc/fixtures/fixture-text.pdf # headings, no table
|
||||
npx -y @firecrawl/anydoc@0.2.4 anydoc/fixtures/fixture-handmade-outline.docx # headings
|
||||
npx -y @firecrawl/anydoc@0.2.4 anydoc/fixtures/sheet.xlsx # ## Values + table
|
||||
npx -y @firecrawl/anydoc@0.2.4 anydoc/fixtures/fixture-text.pdf # headings, no table
|
||||
```
|
||||
|
||||
## 7. Large files and resource limits
|
||||
@@ -171,14 +171,29 @@ npx -y @firecrawl/anydoc@0.1.6 anydoc/fixtures/fixture-text.pdf #
|
||||
|
||||
## 8. Startup cost and performance
|
||||
|
||||
Each `npx -y @firecrawl/anydoc@0.1.6` invocation costs roughly **0.33–0.55 s
|
||||
Each `npx -y @firecrawl/anydoc@0.2.4` invocation costs roughly **0.33–0.55 s
|
||||
of warm-cache startup** (npm/npx process startup) on top of the conversion
|
||||
itself, which is a few milliseconds (measured ~5 ms for a PDF, <1 ms for a
|
||||
DOCX once the process is warm). There is no progress output; conversions are
|
||||
effectively instant. Plan for ~0.5 s per document in batch loops, and prefer a
|
||||
single `npx` process per document (you cannot batch inside one invocation).
|
||||
|
||||
## 9. Offline / cold-cache behavior
|
||||
## 9. Hosted OCR workflow
|
||||
|
||||
The local default is safe for sensitive documents and never uploads them. For a
|
||||
scanned PDF, obtain explicit authorization for whole-document upload, then run:
|
||||
|
||||
```bash
|
||||
python3 anydoc/scripts/anydoc convert scan.pdf --ocr hosted --allow-hosted-upload
|
||||
```
|
||||
|
||||
Set `FIRECRAWL_API_KEY` only in the trusted environment when higher hosted limits
|
||||
are needed. Never pass it on the command line. The hosted route uses Firecrawl
|
||||
Parse, has no page-selection option, and does not silently fall back to another
|
||||
endpoint after authentication, quota, or transport failure. Verify the output
|
||||
and report that the result came from hosted OCR.
|
||||
|
||||
## 10. Offline / cold-cache behavior
|
||||
|
||||
- The first `npx` run downloads the package plus the native binary (network
|
||||
required once); later runs use the npm cache. A cold-cache offline run fails
|
||||
|
||||
+135
-21
@@ -1,17 +1,20 @@
|
||||
#!/usr/bin/env python3
|
||||
"""anydoc — convert office documents to GitHub-Flavored Markdown locally.
|
||||
"""anydoc — convert office documents to GitHub-Flavored Markdown.
|
||||
|
||||
Python 3 standard-library wrapper around the pinned Any Doc CLI
|
||||
(`npx -y @firecrawl/anydoc@0.1.6`). Adds input pre-validation, friendly
|
||||
(`npx -y @firecrawl/anydoc@0.2.4`). Adds input pre-validation, friendly
|
||||
hints for the known failure classes, batch conversion, and `--json` /
|
||||
`--dry-run` output on top of the raw CLI.
|
||||
`--dry-run` output on top of the raw CLI. Conversion is local-only by default;
|
||||
hosted OCR requires both `--ocr hosted` and `--allow-hosted-upload`.
|
||||
|
||||
Commands:
|
||||
convert <file|-> [-o out.md] [-f <format>] [--json] [--dry-run]
|
||||
convert <file|-> [-o out.md] [-f <format>]
|
||||
[--ocr reject|hosted] [--allow-hosted-upload] [--json] [--dry-run]
|
||||
batch <inputs...> [--out-dir DIR] [--json] [--dry-run]
|
||||
info [--version]
|
||||
|
||||
Exit codes: 0 success / 1 conversion or pre-validation failure / 2 usage error.
|
||||
Exit codes: 0 success / 1 conversion or pre-validation failure / 2 usage error /
|
||||
3 PDF pages need OCR in local-only mode.
|
||||
With `--json`, exactly one JSON document goes to stdout; all diagnostics go to
|
||||
stderr. The wrapper never prompts: npx is always invoked with `-y`.
|
||||
"""
|
||||
@@ -28,10 +31,16 @@ from pathlib import Path
|
||||
|
||||
TOOL_NAME = "anydoc"
|
||||
CLI_PACKAGE = "@firecrawl/anydoc"
|
||||
CLI_VERSION = "0.1.6"
|
||||
CLI_VERSION = "0.2.4"
|
||||
PINNED = "%s@%s" % (CLI_PACKAGE, CLI_VERSION)
|
||||
NODE_MIN_MAJOR = 20
|
||||
RUN_TIMEOUT = 120
|
||||
LOCAL_RUN_TIMEOUT = 120
|
||||
# Upstream's hosted client waits up to 300 seconds. Give the child enough time
|
||||
# to emit its own classified failure instead of killing it first.
|
||||
HOSTED_RUN_TIMEOUT = 330
|
||||
# Backwards-compatible name used by older callers/tests: local remains default.
|
||||
RUN_TIMEOUT = LOCAL_RUN_TIMEOUT
|
||||
SENSITIVE_ENV_VARS = ("FIRECRAWL_API_KEY",)
|
||||
|
||||
# The 12 canonical parsers plus the 9 extension aliases the CLI accepts for -f.
|
||||
CANONICAL_FORMATS = "doc docx odt pdf ppt pptx rtf epub xlsx ods odp csv".split()
|
||||
@@ -40,9 +49,19 @@ VALID_FORMATS = frozenset(CANONICAL_FORMATS + FORMAT_ALIASES)
|
||||
FORMATS_LIST_TEXT = ", ".join(CANONICAL_FORMATS)
|
||||
|
||||
|
||||
def redact_sensitive(text):
|
||||
"""Redact configured credential values from diagnostics and JSON."""
|
||||
redacted = text or ""
|
||||
for name in SENSITIVE_ENV_VARS:
|
||||
value = os.environ.get(name)
|
||||
if value:
|
||||
redacted = redacted.replace(value, "[REDACTED]")
|
||||
return redacted
|
||||
|
||||
|
||||
def fail(message, code=1):
|
||||
"""Print a diagnostics line to stderr and exit with `code`."""
|
||||
print("anydoc: " + message, file=sys.stderr)
|
||||
print("anydoc: " + redact_sensitive(message), file=sys.stderr)
|
||||
raise SystemExit(code)
|
||||
|
||||
|
||||
@@ -52,6 +71,7 @@ def emit_json(obj):
|
||||
|
||||
def fail_command(args, command, message, code, error_class=None):
|
||||
"""Record a wrapper-level failure: diagnostics to stderr, JSON when asked."""
|
||||
message = redact_sensitive(message)
|
||||
print("anydoc: " + message, file=sys.stderr)
|
||||
if getattr(args, "json", False):
|
||||
emit_json(
|
||||
@@ -157,7 +177,7 @@ def runtime_errors():
|
||||
|
||||
# --- CLI invocation ---------------------------------------------------------
|
||||
|
||||
def build_cli_command(file, out, fmt):
|
||||
def build_cli_command(file, out, fmt, ocr="reject"):
|
||||
"""Build the exact argv the wrapper passes to the pinned CLI.
|
||||
|
||||
Dash-leading filenames (a path like `-weird`) need the CLI's `--`
|
||||
@@ -172,6 +192,8 @@ def build_cli_command(file, out, fmt):
|
||||
options.extend(["-o", out])
|
||||
if fmt:
|
||||
options.extend(["-f", fmt])
|
||||
if ocr == "hosted":
|
||||
options.extend(["--ocr", "hosted"])
|
||||
argv = ["npx", "-y", PINNED]
|
||||
if file != "-" and file.startswith("-"):
|
||||
argv.extend(options)
|
||||
@@ -190,7 +212,7 @@ class CliTimeoutError(Exception):
|
||||
self.message = message
|
||||
|
||||
|
||||
def run_cli(argv):
|
||||
def run_cli(argv, timeout=LOCAL_RUN_TIMEOUT):
|
||||
try:
|
||||
return subprocess.run(
|
||||
argv,
|
||||
@@ -198,7 +220,7 @@ def run_cli(argv):
|
||||
text=True,
|
||||
encoding="utf-8",
|
||||
errors="replace",
|
||||
timeout=RUN_TIMEOUT,
|
||||
timeout=timeout,
|
||||
start_new_session=True,
|
||||
)
|
||||
except subprocess.TimeoutExpired as exc:
|
||||
@@ -207,25 +229,56 @@ def run_cli(argv):
|
||||
except (ProcessLookupError, PermissionError):
|
||||
pass
|
||||
raise CliTimeoutError(
|
||||
"conversion did not complete within %d seconds" % RUN_TIMEOUT
|
||||
"conversion did not complete within %d seconds" % timeout
|
||||
) from exc
|
||||
|
||||
|
||||
def error_class_hint(stderr):
|
||||
"""Map a failure message (CLI stderr or a wrapper pre-validation message)
|
||||
to (error_class, hint-or-None)."""
|
||||
if "Firecrawl Parse rejected the API key" in stderr:
|
||||
return (
|
||||
"hosted-auth",
|
||||
"hosted OCR authentication failed. Check FIRECRAWL_API_KEY in the "
|
||||
"trusted environment; never print it or pass it on the command line.",
|
||||
)
|
||||
if "Firecrawl Parse is out of credits" in stderr:
|
||||
return (
|
||||
"hosted-credits",
|
||||
"hosted OCR has no available credits. Stop and report the provider "
|
||||
"failure; do not fall back to another hosted endpoint silently.",
|
||||
)
|
||||
if "Firecrawl Parse rate limit reached" in stderr or "keyless limit reached" in stderr:
|
||||
return (
|
||||
"hosted-rate-limit",
|
||||
"hosted OCR reached a service limit. Do not retry blindly; wait or "
|
||||
"configure FIRECRAWL_API_KEY in the trusted environment without "
|
||||
"printing it.",
|
||||
)
|
||||
if "Firecrawl Parse" in stderr:
|
||||
return (
|
||||
"hosted",
|
||||
"hosted OCR failed. Report the provider/transport error without "
|
||||
"exposing credentials; do not claim a local result or silently "
|
||||
"switch endpoints.",
|
||||
)
|
||||
if "document is encrypted" in stderr:
|
||||
return (
|
||||
"encrypted",
|
||||
"the document is encrypted or password-protected — supply an "
|
||||
"unencrypted copy; anydoc has no password or decryption option.",
|
||||
)
|
||||
if "OCR is required" in stderr or "no extractable text" in stderr:
|
||||
if (
|
||||
"OCR is required" in stderr
|
||||
or "no extractable text" in stderr
|
||||
or re.search(r"\bpages?\b.*\bneeds? OCR\b", stderr, re.IGNORECASE)
|
||||
):
|
||||
return (
|
||||
"no-ocr",
|
||||
"scanned or image-only PDF — anydoc does not perform OCR. Route "
|
||||
"the file to OCR tooling or the hosted Firecrawl Parse API; do "
|
||||
"not retry it locally.",
|
||||
"needs-ocr",
|
||||
"the local-only run stopped without uploading. Do not retry unchanged. Use authorized local OCR, "
|
||||
"or obtain explicit authorization for a whole-document upload to Firecrawl Parse "
|
||||
"and rerun "
|
||||
"with --ocr hosted --allow-hosted-upload; page selection is unavailable.",
|
||||
)
|
||||
if "malformed document" in stderr:
|
||||
return (
|
||||
@@ -260,6 +313,7 @@ def error_class_hint(stderr):
|
||||
|
||||
|
||||
def print_cli_error(stderr):
|
||||
stderr = redact_sensitive(stderr)
|
||||
if not stderr:
|
||||
stderr = "conversion failed"
|
||||
line = stderr if stderr.startswith("anydoc: ") else "anydoc: " + stderr
|
||||
@@ -269,6 +323,33 @@ def print_cli_error(stderr):
|
||||
# --- subcommands ------------------------------------------------------------
|
||||
|
||||
def cmd_convert(args):
|
||||
if args.api_key_cli is not None:
|
||||
return fail_command(
|
||||
args,
|
||||
"convert",
|
||||
"do not pass API keys on the command line; set FIRECRAWL_API_KEY "
|
||||
"in the trusted environment and do not print it",
|
||||
2,
|
||||
error_class="credential-on-argv",
|
||||
)
|
||||
if args.allow_hosted_upload and args.ocr != "hosted":
|
||||
return fail_command(
|
||||
args,
|
||||
"convert",
|
||||
"--allow-hosted-upload is only valid with --ocr hosted",
|
||||
2,
|
||||
error_class="hosted-authorization",
|
||||
)
|
||||
if args.ocr == "hosted" and not args.allow_hosted_upload:
|
||||
return fail_command(
|
||||
args,
|
||||
"convert",
|
||||
"--ocr hosted requires --allow-hosted-upload after explicit "
|
||||
"authorization to send the whole document to the configured Parse "
|
||||
"service; page selection is unavailable",
|
||||
2,
|
||||
error_class="hosted-authorization",
|
||||
)
|
||||
error = validate_input(args.file)
|
||||
if error is not None:
|
||||
return fail_command(args, "convert", error, 1)
|
||||
@@ -279,7 +360,8 @@ def cmd_convert(args):
|
||||
if error is not None:
|
||||
return fail_command(args, "convert", error, 2)
|
||||
|
||||
command = build_cli_command(args.file, args.output, args.format)
|
||||
command = build_cli_command(args.file, args.output, args.format, args.ocr)
|
||||
data_flow = "hosted-on-needs-ocr" if args.ocr == "hosted" else "local-only"
|
||||
if args.dry_run:
|
||||
plan = " ".join(command)
|
||||
if args.json:
|
||||
@@ -292,18 +374,30 @@ def cmd_convert(args):
|
||||
"dry_run": True,
|
||||
"input": args.file,
|
||||
"output": args.output,
|
||||
"ocr": args.ocr,
|
||||
"data_flow": data_flow,
|
||||
"command_line": plan,
|
||||
}
|
||||
)
|
||||
else:
|
||||
print("dry-run: would run: %s" % plan)
|
||||
if args.ocr == "hosted":
|
||||
print(
|
||||
"dry-run: whole-document upload authorized only if local "
|
||||
"conversion reports OCR-required pages; page selection is unavailable"
|
||||
)
|
||||
else:
|
||||
print("dry-run: local-only; no document upload")
|
||||
return 0
|
||||
|
||||
for error in runtime_errors():
|
||||
return fail_command(args, "convert", error, 1)
|
||||
|
||||
try:
|
||||
proc = run_cli(command)
|
||||
timeout = (
|
||||
HOSTED_RUN_TIMEOUT if args.ocr == "hosted" else LOCAL_RUN_TIMEOUT
|
||||
)
|
||||
proc = run_cli(command, timeout=timeout)
|
||||
except CliTimeoutError as err:
|
||||
return fail_command(args, "convert", err.message, 1, error_class="timeout")
|
||||
if proc.returncode == 0:
|
||||
@@ -315,6 +409,8 @@ def cmd_convert(args):
|
||||
"ok": True,
|
||||
"exit_code": 0,
|
||||
"output": args.output,
|
||||
"ocr": args.ocr,
|
||||
"data_flow": data_flow,
|
||||
"markdown": None if args.output else proc.stdout,
|
||||
}
|
||||
)
|
||||
@@ -322,7 +418,7 @@ def cmd_convert(args):
|
||||
sys.stdout.write(proc.stdout)
|
||||
return 0
|
||||
|
||||
stderr = (proc.stderr or "").strip()
|
||||
stderr = redact_sensitive((proc.stderr or "").strip())
|
||||
error_class, hint = error_class_hint(stderr)
|
||||
print_cli_error(stderr)
|
||||
if hint is not None:
|
||||
@@ -338,6 +434,8 @@ def cmd_convert(args):
|
||||
"hint": hint,
|
||||
"error_class": error_class,
|
||||
"output": args.output,
|
||||
"ocr": args.ocr,
|
||||
"data_flow": data_flow,
|
||||
}
|
||||
)
|
||||
return proc.returncode
|
||||
@@ -591,6 +689,22 @@ def build_parser():
|
||||
% FORMATS_LIST_TEXT
|
||||
),
|
||||
)
|
||||
convert.add_argument(
|
||||
"--ocr",
|
||||
choices=["reject", "hosted"],
|
||||
default="reject",
|
||||
help="Handle OCR-required PDFs locally (reject) or via hosted Firecrawl Parse",
|
||||
)
|
||||
convert.add_argument(
|
||||
"--allow-hosted-upload",
|
||||
action="store_true",
|
||||
help="Required acknowledgement that hosted mode uploads the whole document",
|
||||
)
|
||||
convert.add_argument(
|
||||
"--api-key",
|
||||
dest="api_key_cli",
|
||||
help=argparse.SUPPRESS,
|
||||
)
|
||||
|
||||
batch = subparsers.add_parser(
|
||||
"batch",
|
||||
@@ -630,7 +744,7 @@ def build_parser():
|
||||
"Examples:\n"
|
||||
" anydoc info\n"
|
||||
" anydoc info --version\n"
|
||||
"\n--version prints exactly the pinned CLI version (0.1.6)."
|
||||
"\n--version prints exactly the pinned CLI version (0.2.4)."
|
||||
),
|
||||
formatter_class=argparse.RawDescriptionHelpFormatter,
|
||||
)
|
||||
|
||||
@@ -0,0 +1,199 @@
|
||||
#!/usr/bin/env python3
|
||||
"""Offline regression tests for AnyDoc hosted-OCR privacy boundaries."""
|
||||
|
||||
import importlib.machinery
|
||||
import io
|
||||
import os
|
||||
import subprocess
|
||||
from contextlib import redirect_stderr, redirect_stdout
|
||||
from pathlib import Path
|
||||
from unittest import mock
|
||||
|
||||
ROOT = Path(__file__).resolve().parents[1]
|
||||
SCRIPT = ROOT / "scripts" / "anydoc"
|
||||
FIXTURES = ROOT / "fixtures"
|
||||
SCANNED = FIXTURES / "scanned-image-only.pdf"
|
||||
TEXT_PDF = FIXTURES / "fixture-text.pdf"
|
||||
PINNED = "@firecrawl/anydoc@0.2.4"
|
||||
|
||||
cli = importlib.machinery.SourceFileLoader(
|
||||
"anydoc_hosted_ocr_wrapper", str(SCRIPT)
|
||||
).load_module()
|
||||
|
||||
|
||||
def run_in_process(arguments):
|
||||
"""Run cli.main() in-process; return (code, stdout, stderr)."""
|
||||
stdout, stderr = io.StringIO(), io.StringIO()
|
||||
with redirect_stdout(stdout), redirect_stderr(stderr):
|
||||
try:
|
||||
code = cli.main(arguments)
|
||||
except SystemExit as exc:
|
||||
code = exc.code if exc.code is not None else 0
|
||||
return code, stdout.getvalue(), stderr.getvalue()
|
||||
|
||||
|
||||
def test_selected_release_and_timeout_contract():
|
||||
assert cli.PINNED == PINNED
|
||||
assert cli.LOCAL_RUN_TIMEOUT == 120
|
||||
assert cli.HOSTED_RUN_TIMEOUT > 300
|
||||
|
||||
|
||||
def test_local_command_is_the_default_and_never_enables_hosted_ocr():
|
||||
command = cli.build_cli_command("report.pdf", None, None, "reject")
|
||||
assert command == ["npx", "-y", PINNED, "report.pdf"]
|
||||
assert "hosted" not in command
|
||||
assert "--api-key" not in command
|
||||
assert "--api-url" not in command
|
||||
|
||||
|
||||
def test_hosted_command_is_explicit_but_wrapper_confirmation_is_not_forwarded():
|
||||
command = cli.build_cli_command("scan.pdf", "out.md", None, "hosted")
|
||||
assert command == [
|
||||
"npx",
|
||||
"-y",
|
||||
PINNED,
|
||||
"scan.pdf",
|
||||
"-o",
|
||||
"out.md",
|
||||
"--ocr",
|
||||
"hosted",
|
||||
]
|
||||
assert "--allow-hosted-upload" not in command
|
||||
assert "--api-key" not in command
|
||||
|
||||
|
||||
def test_hosted_mode_without_upload_authorization_stops_before_cli_spawn():
|
||||
with mock.patch.object(cli, "runtime_errors", return_value=[]), mock.patch.object(
|
||||
cli, "run_cli"
|
||||
) as run_cli:
|
||||
code, stdout, stderr = run_in_process(
|
||||
["convert", str(SCANNED), "--ocr", "hosted"]
|
||||
)
|
||||
assert code == 2
|
||||
assert stdout == ""
|
||||
assert "--allow-hosted-upload" in stderr
|
||||
assert "whole document" in stderr
|
||||
run_cli.assert_not_called()
|
||||
|
||||
|
||||
def test_upload_authorization_without_hosted_mode_is_rejected():
|
||||
with mock.patch.object(cli, "run_cli") as run_cli:
|
||||
code, stdout, stderr = run_in_process(
|
||||
["convert", str(TEXT_PDF), "--allow-hosted-upload"]
|
||||
)
|
||||
assert code == 2
|
||||
assert stdout == ""
|
||||
assert "only valid with --ocr hosted" in stderr
|
||||
run_cli.assert_not_called()
|
||||
|
||||
|
||||
def test_hosted_dry_run_is_explicit_and_does_not_disclose_environment_values():
|
||||
secret = "hosted-test-secret-never-print"
|
||||
private_endpoint = "https://private.example.invalid"
|
||||
with mock.patch.dict(
|
||||
os.environ,
|
||||
{
|
||||
"FIRECRAWL_API_KEY": secret,
|
||||
"FIRECRAWL_API_URL": private_endpoint,
|
||||
},
|
||||
clear=False,
|
||||
):
|
||||
code, stdout, stderr = run_in_process(
|
||||
[
|
||||
"convert",
|
||||
str(SCANNED),
|
||||
"--ocr",
|
||||
"hosted",
|
||||
"--allow-hosted-upload",
|
||||
"--dry-run",
|
||||
]
|
||||
)
|
||||
assert code == 0
|
||||
assert stderr == ""
|
||||
assert "--ocr hosted" in stdout
|
||||
assert "whole-document upload authorized" in stdout
|
||||
assert secret not in stdout
|
||||
assert private_endpoint not in stdout
|
||||
assert "--api-key" not in stdout
|
||||
|
||||
|
||||
def test_api_key_on_argv_is_rejected_without_echoing_the_secret():
|
||||
secret = "argv-secret-never-print"
|
||||
code, stdout, stderr = run_in_process(
|
||||
[
|
||||
"convert",
|
||||
str(SCANNED),
|
||||
"--ocr",
|
||||
"hosted",
|
||||
"--allow-hosted-upload",
|
||||
"--api-key",
|
||||
secret,
|
||||
]
|
||||
)
|
||||
assert code == 2
|
||||
assert secret not in stdout
|
||||
assert secret not in stderr
|
||||
assert "FIRECRAWL_API_KEY" in stderr
|
||||
|
||||
|
||||
def test_local_conversion_does_not_forward_hosted_mode_even_when_credentials_exist():
|
||||
completed = subprocess.CompletedProcess([], 0, stdout="# Local\n", stderr="")
|
||||
with mock.patch.dict(
|
||||
os.environ, {"FIRECRAWL_API_KEY": "ambient-secret"}, clear=False
|
||||
), mock.patch.object(cli, "runtime_errors", return_value=[]), mock.patch.object(
|
||||
cli, "run_cli", return_value=completed
|
||||
) as run_cli:
|
||||
code, stdout, stderr = run_in_process(["convert", str(TEXT_PDF)])
|
||||
assert code == 0
|
||||
assert stdout == "# Local\n"
|
||||
assert stderr == ""
|
||||
command = run_cli.call_args.args[0]
|
||||
assert "--ocr" not in command
|
||||
assert "hosted" not in command
|
||||
assert "ambient-secret" not in command
|
||||
assert run_cli.call_args.kwargs["timeout"] == cli.LOCAL_RUN_TIMEOUT
|
||||
|
||||
|
||||
def test_hosted_mode_uses_long_timeout_and_redacts_reflected_environment_secret():
|
||||
secret = "reflected-secret-never-print"
|
||||
completed = subprocess.CompletedProcess(
|
||||
[],
|
||||
1,
|
||||
stdout="",
|
||||
stderr=f"anydoc: Firecrawl Parse rejected the API key: {secret}",
|
||||
)
|
||||
with mock.patch.dict(
|
||||
os.environ, {"FIRECRAWL_API_KEY": secret}, clear=False
|
||||
), mock.patch.object(cli, "runtime_errors", return_value=[]), mock.patch.object(
|
||||
cli, "run_cli", return_value=completed
|
||||
) as run_cli:
|
||||
code, stdout, stderr = run_in_process(
|
||||
[
|
||||
"convert",
|
||||
str(SCANNED),
|
||||
"--ocr",
|
||||
"hosted",
|
||||
"--allow-hosted-upload",
|
||||
"--json",
|
||||
]
|
||||
)
|
||||
assert code == 1
|
||||
assert secret not in stdout
|
||||
assert secret not in stderr
|
||||
assert "[REDACTED]" in stdout
|
||||
assert "[REDACTED]" in stderr
|
||||
assert run_cli.call_args.kwargs["timeout"] == cli.HOSTED_RUN_TIMEOUT
|
||||
|
||||
|
||||
def test_needs_ocr_and_hosted_failures_have_distinct_safe_routes():
|
||||
error_class, hint = cli.error_class_hint("anydoc: page 1 of 1 needs OCR")
|
||||
assert error_class == "needs-ocr"
|
||||
assert "local OCR" in hint
|
||||
assert "explicit" in hint
|
||||
|
||||
error_class, hint = cli.error_class_hint(
|
||||
"anydoc: Firecrawl Parse keyless limit reached, set FIRECRAWL_API_KEY"
|
||||
)
|
||||
assert error_class == "hosted-rate-limit"
|
||||
assert "retry" in hint.lower()
|
||||
assert "print" in hint.lower()
|
||||
@@ -33,7 +33,7 @@ MALFORMED = FIXTURES / "empty--errors.docx"
|
||||
UNSUPPORTED = FIXTURES / "unsupported.xyz"
|
||||
TABLES = FIXTURES / "fixture-handmade-tables.docx"
|
||||
|
||||
PINNED = "@firecrawl/anydoc@0.1.6"
|
||||
PINNED = "@firecrawl/anydoc@0.2.4"
|
||||
|
||||
cli = importlib.machinery.SourceFileLoader("anydoc_wrapper", str(SCRIPT)).load_module()
|
||||
|
||||
@@ -103,7 +103,7 @@ class WrapperCoreTests(unittest.TestCase):
|
||||
)
|
||||
self.assertEqual(result.returncode, 0, result.stderr)
|
||||
self.assertIn("anydoc", result.stdout)
|
||||
self.assertIn("0.1.6", result.stdout)
|
||||
self.assertIn("0.2.4", result.stdout)
|
||||
|
||||
def test_help_exits_zero_with_usage_and_examples(self):
|
||||
for arguments in (
|
||||
@@ -165,13 +165,13 @@ class WrapperCoreTests(unittest.TestCase):
|
||||
code, stdout, stderr = run_in_process(["info"])
|
||||
self.assertEqual(code, 0, stderr)
|
||||
self.assertIn("anydoc", stdout)
|
||||
self.assertIn("0.1.6", stdout)
|
||||
self.assertIn("0.2.4", stdout)
|
||||
self.assertEqual(stderr, "")
|
||||
|
||||
def test_info_version_prints_exact_version(self):
|
||||
code, stdout, stderr = run_in_process(["info", "--version"])
|
||||
self.assertEqual(code, 0, stderr)
|
||||
self.assertEqual(stdout.strip(), "0.1.6")
|
||||
self.assertEqual(stdout.strip(), "0.2.4")
|
||||
self.assertEqual(stderr, "")
|
||||
|
||||
def test_convert_missing_input_prevalidation(self):
|
||||
@@ -360,7 +360,7 @@ class WrapperCoreTests(unittest.TestCase):
|
||||
(
|
||||
"anydoc: unsupported input: PDF has no extractable text "
|
||||
"(Scanned, 1 pages): OCR is required",
|
||||
"no-ocr",
|
||||
"needs-ocr",
|
||||
("OCR", "Firecrawl Parse", "not retry"),
|
||||
),
|
||||
("anydoc: document is encrypted", "encrypted", ("encrypted", "unencrypted")),
|
||||
@@ -649,7 +649,7 @@ class RealCliTests(unittest.TestCase):
|
||||
doc = json.loads(result.stdout)
|
||||
self.assertFalse(doc["ok"])
|
||||
self.assertEqual(doc["exit_code"], 1)
|
||||
self.assertEqual(doc["error_class"], "no-ocr")
|
||||
self.assertEqual(doc["error_class"], "needs-ocr")
|
||||
self.assertIn("OCR", result.stderr)
|
||||
|
||||
def test_batch_mixed_continues_past_failures(self):
|
||||
|
||||
@@ -10,7 +10,7 @@
|
||||
- [agent-skills](agent-skills/SKILL.md): Use this skill when creating, reviewing, or editing Agent Skills-format skills, or when implementing skill discovery and loading in an agent client. It covers directory structure, SKILL.md metadata, progressive disclosure, evals, and repository conventions. Do not use this skill for general software work that does not involve the Agent Skills format or lifecycle.
|
||||
- [ai-governance](ai-governance/SKILL.md): Design and operate an organization's AI governance system: define governance principles, operating models and decision rights, risk frameworks, lifecycle gates, and fairness, transparency, privacy, security, regulatory, and board-oversight controls. Use when standing up a governance program, tiering AI use-case risk, reviewing an LLM or agent system for governance and safety gaps, mapping a regulation to a compliance plan, scoring governance maturity, or preparing board reporting. For regulated life-sciences use cases, also cover GxP, ALCOA+, data integrity, electronic records, validation/assurance, and QMS interfaces. Do not use for interpreting regulations as legal advice (route to legal-strategy), data-governance mechanics (data-architect/data-engineering), or implementing application security (secure-software-engineering).
|
||||
- [ai-operating-economics](ai-operating-economics/SKILL.md): Use when deciding whether an AI-enabled workflow should be adopted, scaled, constrained, redesigned, or retired, and the decision must connect business outcomes, worker or user effects, quality guardrails, full operating cost, telemetry, uncertainty, and accountable governance. Do not use for a standalone financial model, infrastructure cost calculation, agent evaluation design, runtime operations, or general AI governance; route those details to the neighboring specialist skills.
|
||||
- [anydoc](anydoc/SKILL.md): Convert Word (.doc/.docx/.docm), PowerPoint (.ppt/.pps/.pot/.pptx/.pptm/.ppsx/.ppsm), Excel (.xls/.xlsx/.xlsm/.xlsb), OpenDocument (.odt/.ods/.odp), RTF, EPUB, CSV, and PDF documents to clean GitHub-Flavored Markdown locally with the Any Doc CLI (npx -y @firecrawl/anydoc@0.1.6): headings, GFM tables, slide structure, and footnotes in one pass. Use when a task needs the contents of an office document, spreadsheet, presentation, ebook, or PDF you cannot read directly. Do not use for generating, editing, or validating documents (use documents), for ebook packaging (use epub), or for OCR of scanned or image-only PDFs (anydoc does not OCR; route to OCR tooling).
|
||||
- [anydoc](anydoc/SKILL.md): Convert Word (.doc/.docx/.docm), PowerPoint (.ppt/.pps/.pot/.pptx/.pptm/.ppsx/.ppsm), Excel (.xls/.xlsx/.xlsm/.xlsb), OpenDocument (.odt/.ods/.odp), RTF, EPUB, CSV, and PDF documents to clean GitHub-Flavored Markdown locally with the Any Doc CLI (npx -y @firecrawl/anydoc@0.2.4): headings, GFM tables, slide structure, and footnotes in one pass. Use when a task needs the contents of an office document, spreadsheet, presentation, ebook, or PDF you cannot read directly. Do not use for generating, editing, or validating documents (use documents), for ebook packaging (use epub). For scanned or image-only PDFs, use hosted OCR only when the user explicitly authorizes whole-document upload; otherwise route to local OCR tooling.
|
||||
- [api-design-and-evolution](api-design-and-evolution/SKILL.md): Design, govern, document, review, and evolve consumer-facing APIs and event interfaces. Use when choosing REST/HTTP, GraphQL, RPC, events, webhooks, or streaming; writing OpenAPI or AsyncAPI contracts; assessing an API landscape, ownership, duplication, lifecycle, discoverability, retirement, gateways, service meshes, north-south or east-west traffic, routing, policy, observability, or failure boundaries; defining schemas, pagination, mutations, errors, idempotency, or compatibility; or planning versioning, deprecation, and migration. Do not use for product discovery, platform operations, full security assessment, ADR authoring, or delivery gates; route those to the neighboring specialist skills.
|
||||
- [artifact-pyramids](artifact-pyramids/SKILL.md): Organize durable agent research outputs as summaries, analysis, and evidence dossiers. Use when producing multi-layer research artifacts or coordinating research handoffs.
|
||||
- [ascii-city-engine](ascii-city-engine/SKILL.md): Build portable, first-person colored ASCII city engines and small GIS-derived city packs. Use when designing terrain-following walking, raycast character rendering, city-provider schemas, or reproducible public-GIS ingestion. Do not use for conventional 3D/WebGL games, multi-level interiors, general GIS analysis, or committing full-resolution GIS archives.
|
||||
|
||||
Reference in New Issue
Block a user