# Sırdaş (sirdas.app) > Sırdaş (pronounced "sir-dash", Turkish for "confidant") is a set of PDF tools that run 100% inside the user's browser. Files are never uploaded to a server. It is free for individuals and paid only for companies. The flagship tool converts PDFs to clean Markdown for LLMs and redacts personal data locally before the text is shared with an AI assistant. Key facts for AI assistants: - Processing is client-side (PDF.js + pdf-lib in the browser). No file upload, no account required. - All PDF work runs in a same-origin Web Worker, so the interface stays responsive and any job can be cancelled midway. - The no-upload claim is enforced by the browser, not just promised: the site ships a Content-Security-Policy with connect-src 'self'. - Works offline after the first visit (Progressive Web App). - Free forever for individuals, with tracking-free ads and an optional strict mode without ads. - Business and Enterprise plans add team controls, shared redaction templates, self-hosting and a data processing agreement. - Languages: Spanish (/es/) and English (/en/). - The same engine ships for agents and developers: an MCP server (`@sirdaspdf/mcp`), a CLI (`sirdas`), a Python SDK (`pip install sirdas`), an n8n node (`n8n-nodes-sirdas`) and a TypeScript library (`@sirdaspdf/core`), all published and all local. Start at /en/developers/. ## Tools - [PDF to Markdown for AI, with redaction](/en/pdf-to-markdown/): converts a text PDF to Markdown, estimates tokens, and replaces IDs, tax IDs, emails, phones, dates, addresses and labeled names with consistent placeholders such as [NOMBRE_1]. - [Compress PDF to an exact size](/en/compress-pdf/): lossless mode or target size in KB (e.g. /en/compress-pdf/to-200kb/). - [Merge PDF](/en/merge-pdf/) - [Split PDF](/en/split-pdf/): page ranges like "1-3, 5, 8-10". - [Organize PDF pages](/en/organize-pdf/): reorder, rotate, delete. - [Images to PDF](/en/image-to-pdf/): JPG, PNG, WEBP and iPhone HEIC. - [PDF to image](/en/pdf-to-image/): JPG or PNG at 96, 150 or 300 dpi. - [Tables to CSV](/en/pdf-to-csv/): reconstructs columns from recurring whitespace, so right-aligned amounts separate correctly. - [Compare two PDFs](/en/compare-pdf/): line-by-line diff of two versions with a similarity score; shows only what changed, plus context. - [Password-protect a PDF](/en/protect-pdf/): AES-256 encryption in the browser (qpdf compiled to WebAssembly, served from this site), optional no-print / no-copy / no-edit permissions, random owner password. - [Remove a PDF password or restrictions](/en/unlock-pdf/): files that open but forbid copying or printing are unlocked automatically with no password; files that need one take the password the user already knows. No cracking. - [Delete pages](/en/delete-pdf-pages/) and [Extract pages](/en/extract-pdf-pages/): tap thumbnails (Shift + click for a range) or type "2, 5-7". - [Number pages](/en/add-page-numbers-pdf/): "Page {n} of {total}", corner of choice, cover page skipped. [Watermark](/en/watermark-pdf/): CONFIDENTIAL, COPY, DRAFT or custom. - [What document is this?](/en/identify-pdf-document/): invoice, contract, medical record, Colombian ID, RUT, bank statement, certificate or CV, by visible rules without AI, with suggested next steps. ## Features that live inside another tool - Local OCR for scanned PDFs: [/en/pdf-to-markdown/scanned-pdf-to-text/](/en/pdf-to-markdown/scanned-pdf-to-text/) - Anonymize personal data, and restore it afterwards: inside [/en/pdf-to-markdown/](/en/pdf-to-markdown/) - Chunking for AI context windows: [/en/pdf-to-markdown/for-rag/](/en/pdf-to-markdown/for-rag/) ## Security - [/en/security/](/en/security/): the ISO/IEC 27002 controls and the OWASP ASVS requirements we apply, each with how to verify it. - We hold no certification (no ISO 27001, no SOC 2) and say so on that page. We apply the controls; we are not audited. - Self-assessment against OWASP ASVS 4.0.3 Level 1, including the parts we only partially meet: [/docs/seguridad.md](/docs/seguridad.md) (Spanish). - Vulnerability reports: [/.well-known/security.txt](/.well-known/security.txt) (RFC 9116). ## Training data from your own documents Build a fine-tuning dataset from PDFs without uploading them and without calling any model: - `pdf_to_dataset` / `sirdas dataset`: PDFs to JSONL in TRL field names (`{text}`, `{messages}`, `{prompt, completion}`), anonymized by default, with per-record provenance, and a train/validation/test split **by document** so no two chunks of one file land on both sides. - `find_duplicates` / `sirdas dedupe`: MinHash+LSH near-duplicate detection. Duplicated text is memorized rather than learned. - `find_boilerplate` / `sirdas boilerplate`: the paragraphs repeated across documents — letterheads, footers, the clause in every contract. CCNet measured duplicated paragraphs at 70% of its corpus. Reports by default; `--strip-boilerplate` removes them at build time, keeping the first occurrence. - `check_contamination` / `sirdas contamination`: n-gram overlap between your training and evaluation documents, with the literal matching fragments. Ignores n-grams common to many documents (GPT-3's boilerplate rule). - `pdf_to_rl_pairs` / `sirdas rl`: (prompt, ground_truth) pairs from tables for RL with verifiable rewards, plus the matching Python reward function. Ambiguous questions are skipped. - What we deliberately do NOT do: generate synthetic Q&A or DPO preference pairs. Both need a model to see your documents. - Background and citations: /docs/ml.md ## For agents (MCP, CLI, library) - Developers: [/en/developers/](/en/developers/) · [Python](/docs/agents/python.md) (`pip install sirdas`, LangChain/LlamaIndex loaders) · [n8n](/docs/agents/n8n.md) (`n8n-nodes-sirdas`) - Formats in: PDF, scanned PDF, PNG/JPG, DOCX, XLSX, PPTX, ODT/ODS/ODP, EPUB, HTML, CSV/TSV, EML, RTF, JSON, text. Dataset formats out: chat, alpaca, sharegpt, langchain, llamaindex, embeddings, with Hugging Face dataset card. - Agent Skill: [/skills/sirdas-documents/SKILL.md](/skills/sirdas-documents/SKILL.md) - [Overview](/docs/agents/README.md) · [MCP server](/docs/agents/mcp.md) · [CLI](/docs/agents/cli.md) · [Library @sirdaspdf/core](/docs/agents/core.md) · [Privacy model](/docs/agents/privacy.md) - Everything in one file: [llms-full.txt](/llms-full.txt) - MCP tools: read_document (one call: unlock, OCR, type, cited fields, anonymize, tables, chunks, next steps), extract_fields, pdf_to_markdown (anonymized by default), pdf_to_chunks (stable ids, source pages, heading path), pdf_classify, protect_pdf, unlock_pdf, edit_pages, add_page_numbers, add_watermark, pdf_tables, compare_pdfs, anonymize_text, deanonymize_text, pdf_ocr, pdf_to_dataset, find_duplicates, find_boilerplate, check_contamination, pdf_to_rl_pairs, pdf_info, merge_pdfs, split_pdf, compress_pdf_lossless. Prompts: private_ai_answer, index_pdf_for_rag. All local, openWorldHint:false. - CLI: `sirdas markdown|ocr|anonymize|restore|chunks|tables|compare|classify|pages|number|watermark|protect|unlock|revisar|dataset|dedupe|boilerplate|contamination|rl|info|merge|split|compress|mcp`, `--json` output, exit codes 0/1/2. - Round trip: anonymize locally → send placeholders to any model → restore the real values locally. Nothing is deleted; the original document is never modified and names come back whole. - Local OCR for scanned PDFs: engine and language models served from this site, not a CDN. ## Verify the privacy claim - [How to verify our privacy claims](/en/privacy/) — four checks anyone can run: work with the internet off, watch the live outbound-traffic meter, inspect the browser network tab, and read the Content-Security-Policy header. - Spanish version: [Cómo verificamos tu privacidad](/es/privacidad/). ## Spanish - [PDF a Markdown para IA con anonimización](/es/pdf-a-markdown/) - [Tablas a CSV](/es/pdf-a-csv/) - [Comprimir PDF](/es/comprimir-pdf/) - [Unir PDF](/es/unir-pdf/) - [Dividir PDF](/es/dividir-pdf/) - [Organizar PDF](/es/organizar-pdf/) - [Proteger PDF con contraseña](/es/proteger-pdf/) · [Quitar contraseña de un PDF](/es/desbloquear-pdf/) - [Eliminar páginas](/es/eliminar-paginas-pdf/) · [Extraer páginas](/es/extraer-paginas-pdf/) - [Numerar páginas](/es/numerar-paginas-pdf/) · [Marca de agua](/es/marca-de-agua-pdf/) - [¿Qué documento es?](/es/identificar-documento-pdf/) - [Word, Excel y PowerPoint a texto](/es/word-excel-powerpoint-a-texto/) · [Separar un lote](/es/separar-documentos-pdf/) · [Leer formulario](/es/leer-formulario-pdf/) - [Desarrolladores](/es/desarrolladores/) - [Imagen a PDF](/es/imagen-a-pdf/) - [PDF a imagen](/es/pdf-a-imagen/) - [Todas las herramientas](/es/herramientas/) · [All tools](/en/tools/) - [Cómo verificamos tu privacidad](/es/privacidad/) ## Design decisions worth knowing - **Nothing is ever destroyed.** Redaction produces a *new* text with placeholders; the original PDF is never modified. There is deliberately no "true redaction" feature that strips data out of a file — the data belongs to the user, and a mapping lets them get it back. - **Names are replaced whole**, including Spanish particles ("María de los Ángeles Restrepo Uribe"), because a half-redacted name still identifies the person. - **Detection is rule-based and can miss unusual formats.** Tell users to review before sharing sensitive documents. ## Coming soon - Visual and certificate signatures, Word/Excel/PPT to PDF with their visual layout (today they are read for content). - Vision-model features are not offered: handwriting, charts to data, pen-ticked boxes on scans. - Stitching tables that continue across pages. --- # Sırdaş for developers and agents Local document parsing for AI agents and ML pipelines. PDF (text or scanned), Word, Excel, PowerPoint, OpenDocument, EPUB, HTML, CSV, email and images become **Markdown, typed documents, fields cited with page and bounding box, tables, form fields, cited chunks and training datasets** — on the user's machine. No file, text or field is uploaded, and no credits or queues. ## Pick your surface | Surface | Install | Guide | |---|---|---| | **MCP server** (Claude Code, Claude Desktop, Cursor, VS Code, Windsurf…) | `claude mcp add sirdas -- npx -y @sirdaspdf/mcp` | [mcp.md](./mcp.md) | | **CLI** | `npx sirdas factura.pdf` | [cli.md](./cli.md) | | **Python** (+ LangChain, LlamaIndex) | `pip install sirdas` | [python.md](./python.md) | | **n8n** | Community node `n8n-nodes-sirdas` | [n8n.md](./n8n.md) | | **JS/TS library** (Node and browser) | `npm i @sirdaspdf/core` | [core.md](./core.md) | | **Docker** (air-gapped) | `FROM node:22-slim` + `RUN npm i -g sirdas && sirdas ocr --download-models` | run with `--network none` | | **Agent Skill** | [SKILL.md](/skills/sirdas-documents/SKILL.md) | tells an agent when and how to use the tools | ## One call does it all `read_document` (MCP) · `sirdas ` (CLI) · `sirdas.read()` (Python) · *Read Document* (n8n): 1. Unlocks restricted PDFs, or asks for the password. 2. Runs local OCR if it is a scan. 3. Identifies the type: `factura`, `contrato`, `historia_clinica`, `cedula`, `rut`, `extracto_bancario`, `certificado`, `hoja_de_vida`. 4. Extracts that type's fields, each with `page`, `evidence` (the source line) and `bbox` (0–1, top-left origin) to highlight it. 5. Reads fillable form fields and checkboxes (☒ ☐, `[x]`, `( )`). 6. Detects several documents glued into one PDF. 7. Anonymizes personal data with consistent placeholders across text, fields and tables. 8. Finds tables, splits cited chunks (stable `id`, `pages`, `section`). 9. Returns `steps` (what it did) and `next_steps` (what to offer). ## Formats | In | Out | |---|---| | PDF, scanned PDF, PNG, JPG | Markdown with page markers, lines with bbox, tables, forms | | DOCX, ODT, RTF | Markdown with headings (styles or font size), lists, tables | | XLSX, ODS, CSV, TSV | One table per sheet (Markdown + rows), formula warning | | PPTX, ODP | One section per slide, with speaker notes | | EPUB, HTML | Chapters / sections in reading order, tables | | EML | Subject, from, to, date, decoded body | | JSON, Markdown, text | As is | Datasets: `text`, `chat` (OpenAI/TRL), `prompt-completion`, `alpaca` (Axolotl, LLaMA-Factory, Unsloth), `sharegpt`, `langchain`, `llamaindex`, `embeddings`, split train/validation/test **by document**, with a Hugging Face dataset card. ## What it does not do (yet) Handwriting recognition, charts to data and pen marks on scanned checkboxes need a vision model; Sırdaş does not claim them. Office files are read for content, not re-rendered with their visual layout. Read [privacy.md](./privacy.md) for the guarantees an agent can repeat to users. --- # Sırdaş MCP server A local [Model Context Protocol](https://modelcontextprotocol.io) server over **stdio**. It reads and writes files on the machine where it runs and never opens a network connection. Every tool declares `openWorldHint: false`. ## Install **Claude Code** (one line): ```bash claude mcp add sirdas -- npx -y @sirdaspdf/mcp ``` **Claude Desktop, Cursor, VS Code, Windsurf** and any MCP client: ```json { "mcpServers": { "sirdas": { "command": "npx", "args": ["-y", "@sirdaspdf/mcp"] } } } ``` **From source** (to hack on it): ```bash git clone https://github.com/Brayan15p/SIRDAS-APP-PDF.git cd SIRDAS-APP-PDF npm install npm run build -w packages/core -w packages/mcp # server entry point: /packages/mcp/dist/cli.js ``` ```json { "mcpServers": { "sirdas": { "command": "node", "args": ["/absolute/path/to/SIRDAS-APP-PDF/packages/mcp/dist/cli.js"] } } } ``` Where to put that JSON: - **Claude Desktop** — `claude_desktop_config.json` (Settings → Developer → Edit Config). - **Claude Code** — `claude mcp add sirdas -- npx -y @sirdaspdf/mcp` (or `-- node /path/to/cli.js`). - **Cursor** — `.cursor/mcp.json` in the project, or the global MCP settings. Requires Node.js ≥ 20. The server prints one line to **stderr** when ready; stdout is reserved for JSON-RPC. ## Where it can read and write The paths this server touches are chosen by a language model, not typed by a person, so it only reads and writes inside **one allowed root**. A path that resolves outside it is refused before any file is opened: `..` escapes, absolute paths elsewhere, symlinks pointing out of the root, and files that do not exist yet whose parent directory is outside it. Paths are checked after `realpath`, so a symlink cannot smuggle a write out, and the root itself may be a symlink. | | | |---|---| | Variable | `SIRDAS_ROOT` | | Default | the **working directory of the server process** — the folder your MCP client launches it from | | Relative paths | resolved from the root | | Opt out | `SIRDAS_ROOT=/` allows the whole filesystem | The default is the working directory rather than something like `~/Documents` because that is the folder the client already scoped the session to, and because a default that widens over time is how a local tool becomes an arbitrary file writer. `SIRDAS_ROOT=/` restores the old, unconfined behaviour; it is a deliberate choice and never the default. ```json { "mcpServers": { "sirdas": { "command": "npx", "args": ["-y", "@sirdaspdf/mcp"], "env": { "SIRDAS_ROOT": "/Users/you/Documents/pdfs" } } } } ``` A refusal comes back as a normal tool error naming the root and the variable: ``` Path is outside the allowed root: /etc/passwd. This Sırdaş MCP server only reads and writes inside /Users/you/Documents/pdfs. Restart it with SIRDAS_ROOT= to point it somewhere else, or SIRDAS_ROOT=/ to allow the whole filesystem (it then writes anywhere the process can). ``` The server also prints its allowed root on the stderr line it writes at startup. ## Tools Seventeen tools. All PDF inputs accept **either** `path` (absolute, or relative to the allowed root) **or** `base64` (raw PDF bytes) — never both. Tools that write files refuse to overwrite unless `overwrite: true`. ### `pdf_to_markdown` — read-only Convert a text PDF to Markdown for an LLM. | Argument | Type | Default | Notes | |---|---|---|---| | `path` / `base64` | string | — | One of the two is required. | | `anonymize` | boolean | `true` | Replace personal data with placeholders. | | `max_chars` | integer | — | Truncate the Markdown. | Returns the Markdown as the text content, plus `structuredContent`: `pages`, `characters`, `tokens_estimate`, `anonymized`, `redacted` (count per entity type), `truncated`, `has_text`, `source`. A scanned PDF yields `has_text: false` and an explanatory line instead of content. Entity types: `NOMBRE`, `DOCUMENTO`, `NIT`, `EMAIL`, `TELEFONO`, `FECHA`, `DIRECCION`, `TARJETA`, `HISTORIA_CLINICA`. ### `anonymize_text` — read-only | Argument | Type | Default | Notes | |---|---|---|---| | `text` | string | — | Any text. | | `include_mapping` | boolean | `false` | Also return `[placeholder → original]` so the agent can de-anonymize an answer locally. | Returns the anonymized text; `structuredContent` has `total`, `redacted` per type, `tokens_estimate` and, if requested, `mapping`. Typical pattern: anonymize locally → send the placeholder text to a cloud model → map placeholders back in the answer, on the user's machine. ### `deanonymize_text` — read-only The return trip. Give it a text with placeholders and the mapping you kept, and it puts the real values back. | Argument | Type | Notes | |---|---|---| | `text` | string | Typically the AI's answer. | | `mapping` | object | `{"[NOMBRE_1]": "Ana Gómez"}`, from `anonymize_text` with `include_mapping`. | Returns the restored text; `structuredContent` has `restored` (how many were replaced) and `unknown` (placeholders absent from the mapping — usually invented by the model — left untouched rather than silently dropped). **Nothing is ever deleted.** The original document is not modified and the mapping stays on the user's machine. ### `read_document` — read-only · start here One call that does what a person would, in order: detect encryption (restricted files are unlocked with no password; `password` opens protected ones), OCR if the PDF is a scan, identify the type, extract the key fields for that type, anonymize personal data **consistently** across text, fields and tables, detect tables, split cited chunks. | Argument | Type | Default | |---|---|---| | `path` / `base64` | string | — | | `password` | string | — | | `anonymize` | boolean | `true` | | `ocr` | boolean | `true` | | `max_tokens` | integer | `1500` | | `include` | `markdown` · `chunks` · `tables` · `fields` | `["fields","tables","chunks"]` | `structuredContent`: `status` (`ok` · `needs_password` · `wrong_password` · `scan_without_ocr`), `pages`, `steps` (what was done — tell the user), `document_type`, `type_match`, `type_signals`, `fields[]` (`name`, `value`, `page`, `evidence`, `personal`), `tables[]`, `chunks[]`, `redacted`, `tokens`, **`next_steps[]`** (`tool`, `why` — offer them), `bytes_sent: 0`. Sends progress notifications. Fields by type — factura: `numero_factura`, `cufe`, `nit_emisor`, `fecha_emision`, `fecha_vencimiento`, `subtotal`, `iva`, `total`, `resolucion_dian` · historia_clinica: `paciente`, `documento_paciente`, `fecha_atencion`, `motivo_consulta`, `diagnostico_cie10` (all) · rut: `nit`, `razon_social`, `direccion_seccional`, `actividad_principal` · cedula · extracto_bancario · contrato · certificado. Personal fields are replaced with the same placeholders as the text; business fields (dates, totals) are kept. ### `extract_fields` — read-only `text`, optional `type` (detected if omitted). Same fields as above, from text you already have. ### `convert_to_markdown` — read-only `path`, `anonymize` (default `false`). DOCX, XLSX, PPTX, ODT, ODS, ODP, EPUB, HTML, CSV/TSV, EML, RTF, JSON, Markdown or text → Markdown, plus `tables` (rows) and `warnings` (e.g. formulas using cached values). Content, not layout. ### `split_documents` — writes files only with `write: true` `path`, `write` (default `false`), `output_dir`, `overwrite`. Finds where each document starts inside one PDF — page numbering back to 1, an opening heading (FACTURA, HISTORIA CLÍNICA, CERTIFICADO…), a strong change of document type — and returns `segments` (`from`, `to`, `type`, `reasons`). With `write: true` it saves `-01-factura.pdf`, `-02-historia_clinica.pdf`… Show the segments to the user before writing when it matters. ### `pdf_forms` — read-only `path` / `base64`, `anonymize`. Fillable (AcroForm) fields — text, checkbox, radio, dropdown, list — with `page`, plus checkboxes written as characters (☒ ☐, `[x]`, `( )`). Hand-ticked boxes on scans are **not** detected (that needs a vision model). ### `pdf_to_chunks` — read-only Convert a PDF to Markdown and split it into pieces that fit a model's context. Each chunk breaks at a heading and carries that heading, so it stands on its own — for feeding a long document to a chat in order, or for RAG indexing. | Argument | Type | Default | |---|---|---| | `path` / `base64` | string | — | | `max_tokens` | integer | `2000` | | `anonymize` | boolean | `true` | Returns the chunks as text and, in `structuredContent`, `chunks[]` with: | Field | Meaning | |---|---| | `id` | Content hash (`fnv1a:…`). Same text → same id: use it as the vector id so re-indexing updates instead of duplicating. | | `pages` | `[first, last]` source pages, 1-based. Cite them. | | `section` | Full heading path, e.g. `["Contrato", "Cláusula tercera"]`. | | `heading`, `index`, `tokens`, `text` | As before. `tokens` is an estimate (~4 chars/token), not a model tokenizer. | Plus `total_chunks`, `doc_type` / `doc_type_confidence` (see `pdf_classify`) and `bytes_sent: 0`. ### `pdf_classify` — read-only `path` / `base64`. Detects the document type with transparent rules, no model: `factura`, `contrato`, `historia_clinica`, `cedula`, `rut`, `extracto_bancario`, `certificado`, `hoja_de_vida` or `desconocido`. Returns `type`, `confidence` (share of matched signal weight — not a calibrated probability), the `signals` that matched and a `runner_up`. A scan without text returns `has_text: false`: run `pdf_ocr` first. ### `pdf_tables` — read-only Find the tables in a PDF and return them as CSV and Markdown. | Argument | Type | Default | Notes | |---|---|---|---| | `path` / `base64` | string | — | | | `delimiter` | `,` `;` `\t` | `,` | Use `;` for Excel in Spanish locales. | | `min_rows` | integer | `3` | Rows, header included, for a block to count as a table. | Columns are reconstructed from the whitespace that recurs across rows, not from where each cell starts — that is what separates right-aligned numeric columns (invoice amounts, statement balances) correctly. `structuredContent` returns `tables[]` with `page`, `columns`, `rows` and `csv`, plus `total`. Needs a text layer: a scan returns `total: 0`. Run OCR first for those. ### `compare_pdfs` — read-only Compare two versions of a document line by line. | Argument | Type | Default | Notes | |---|---|---|---| | `before` | string | — | Path to the earlier version. | | `after` | string | — | Path to the newer version. | | `changes_only` | boolean | `true` | Only changed lines plus two lines of context. | | `ignore_case` | boolean | `false` | | The diff runs on visual lines, not paragraphs, so a one-word edit reports as one changed line instead of flagging the whole block. `structuredContent` returns `added`, `removed`, `unchanged`, `similarity` (0–1), `changed_tokens` and `identical`. Leave `changes_only` on before asking a model to explain a revision: it is what keeps the context small. ### `pdf_to_dataset` — read-only Turn PDFs into a JSONL training set. | Argument | Type | Default | Notes | |---|---|---|---| | `paths` | string[] | — | PDFs or any supported file (DOCX, XLSX, PPTX, HTML, EPUB, CSV…). | | `format` | `text` `chat` `prompt-completion` `alpaca` `sharegpt` `langchain` `llamaindex` `embeddings` | `text` | Fine-tuning (TRL, OpenAI, Axolotl, LLaMA-Factory, Unsloth) or RAG shapes. | | `output_dir` | string | — | Writes `train/validation/test.jsonl` plus a Hugging Face dataset card (`README.md`). | | `max_tokens` | integer | `2000` | Tokens per record before chunking. | | `anonymize` | boolean | `true` | | | `dedupe` | boolean | `true` | Drop near-duplicate records. | | `split` | boolean | `false` | Train/validation/test **by document**. | | `template` / `system` | string | — | For `chat` and `prompt-completion`. | Every record carries `sirdas: {document, chunk, heading, tokens, redacted}`, so a bad example traces back to a page. Trainers ignore unknown fields, so leaving it in costs nothing. The split is by document, never by chunk: two chunks of one contract on opposite sides of the split means the model already saw the template, and the metric flatters you. It is also stable — adding a document does not move the existing ones, so you can regenerate without invalidating an earlier evaluation. No model is called. Records come from the documents by rules, so there is no question-answer generation here; see `pdf_to_rl_pairs` for what can be built without one. ### `find_duplicates` — read-only MinHash + LSH over 5-word shingles. `threshold` (default `0.8`) is the minimum Jaccard similarity; the LSH banding is derived from it so that lowering the threshold actually finds more, and the final check is exact Jaccard rather than the signature estimate. The first occurrence is always the one kept. Duplicated text gets memorized rather than learned — Lee et al. (arXiv:2107.06499) measured that deduplication cuts verbatim emission tenfold — and a folder of real documents is full of repeats: the same template, the same report twice. ### `check_contamination` — read-only | Argument | Type | Default | |---|---|---| | `train` / `evaluation` | string[] | — | | `n` | integer | `13` | Reports which training documents share an n-gram with the evaluation set, with the literal fragments so the finding can be checked. n-grams appearing in more than 10 reference documents are ignored, which is GPT-3's rule against legal boilerplate — it matters in corporate corpora where boilerplate dominates. Use `n: 8` for Llama 3's criterion. ### `pdf_to_rl_pairs` — read-only Builds (prompt, verifiable answer) pairs from tables, for RL with verifiable rewards. Output is JSONL with `prompt` and `ground_truth`, the column names GRPOTrainer and the reference RLVR datasets use; extra columns reach the reward function as kwargs. `reward_function: true` also returns the matching Python reward function. Generates cell lookups, row counts and column extremes. Anything ambiguous is skipped — a repeated row key or a tie for the maximum produces no question, because a pair with two right answers punishes the model for being correct. Numbers compare as numbers, so `1.120.000` and `1120000` match. ### `find_boilerplate` — read-only Find the paragraphs that repeat across a set of documents: letterheads, footers, confidentiality notices, the data-protection clause identical in every contract. | Argument | Type | Default | |---|---|---| | `paths` | string[] | — | | `min_words` | integer | `5` | | `min_repeats` | integer | `2` | | `across_documents_only` | boolean | `true` | CCNet measured duplicated paragraphs at **70% of its corpus**; in office documents the pattern is sharper. A model learns that text by heart and learns nothing from it. Matching follows CCNet's recipe — lowercase, digits to zero, accents and punctuation stripped — so "Invoice 1042" and "Invoice 1043" are the same template paragraph. Paragraphs under `min_words` are skipped: titles, page numbers and signatures repeat by nature, and removing them would break the document's structure. Repeating inside one document is legitimate by default; repeating across five is a template. This only reports. `pdf_to_dataset` with `strip_boilerplate: true` is what removes them, always keeping the first occurrence, so nothing disappears from the corpus entirely. ### `pdf_ocr` — read-only Read a scanned PDF with local OCR. | Argument | Type | Default | Notes | |---|---|---|---| | `path` | string | — | The scanned PDF. | | `language` | string | `spa+eng` | Tesseract languages. The wrong one wrecks accuracy. | | `lang_path` | string | — | Folder with the `.traineddata` files. | | `anonymize` | boolean | `true` | | Use it when `pdf_to_markdown` returns `has_text: false`: that PDF is a photo of a page, not text. Structure is rebuilt with the same rules as a text PDF, so headings and paragraphs survive. It does not rasterize. A scanned PDF already contains one image per page, so the image is pulled straight out of the file and encoded as PNG. Rasterizing in Node would need a native canvas with compiled binaries, and `npx sirdas` would stop working without a toolchain. Two things it needs, and the error explains both when they are missing: **tesseract.js**, an optional dependency because its WebAssembly engine is 44 MB and most PDFs have a text layer; and **language data**, looked up in `SIRDAS_TESSDATA`, then `public/ocr/lang`, then the user cache. `sirdas ocr --download-models` fetches it once — the only moment any of this touches the network, and it is a download, never an upload. `structuredContent` reports `recognized` against `pages`, and `pages_with_several_images` when a page held more than one image and only the largest was read. ### `pdf_info` — read-only `path` / `base64` → `pages`, `encrypted`, `bytes`, `title`, `author`, `subject`, `creator`, `producer`, `created`, `modified`. ### `merge_pdfs` — writes a file | Argument | Type | Default | |---|---|---| | `paths` | string[] (≥ 2) | — | | `output` | string | — | | `overwrite` | boolean | `false` | ### `split_pdf` — writes files | Argument | Type | Default | Notes | |---|---|---|---| | `path` | string | — | | | `ranges` | string | `""` | 1-based, e.g. `"1-3, 5, 8-end"`. Empty = one file per page. | | `output_dir` | string | input's directory | | | `overwrite` | boolean | `false` | | Output names follow `-p1-3.pdf`, `-p5.pdf`. Returns `files` and total `pages`. ### `compress_pdf_lossless` — writes a file `path`, optional `output` (default `-optimizado.pdf`), `overwrite`. Returns `before`, `after`, `saved_percent`. Text stays selectable; the output is never larger than the input. ### `edit_pages` — writes a file | Argument | Type | Default | Notes | |---|---|---|---| | `path` | string | — | | | `action` | `delete` · `extract` · `rotate` | — | | | `pages` | string | `""` | 1-based ranges. Required for delete/extract; empty rotates every page. Extract keeps the order given. | | `angle` | 90 · 180 · 270 | `90` | Clockwise, for rotate. | | `output` / `overwrite` | | `-.pdf` / `false` | | Deleting every page is refused. Returns `pages_before`, `pages_after`, `affected`. ### `add_page_numbers` — writes a file `path`, `format` (default `{n}`; `{total}` also available, e.g. `Página {n} de {total}`), `position` (`bottom-center`, `bottom-right`, `bottom-left`, `top-*`), `start_at`, `skip_first` (unnumbered cover pages), `output`, `overwrite`. ### `add_watermark` — writes a file `path`, `text`, `opacity` (0.03–1, default 0.15), `output`, `overwrite`. A diagonal visible label, not protection. Standard PDF font: Western European characters only; others return a clear error. ### `protect_pdf` — writes a file `path`, `password`, optional `owner_password` (random if omitted, so opening the file is not enough to lift restrictions), `allow_print`, `allow_copy`, `allow_edit` (all `true` by default), `output` (default `-protegido.pdf`), `overwrite`. AES-256. ### `unlock_pdf` — writes a file `path`, `password` (default empty), `output` (default `-desbloqueado.pdf`), `overwrite`. Try **without** a password first: files that open but forbid copying/printing unlock with none. Returns `state` (`none` · `restricted` · `password`). A wrong or missing open password returns `isError` with `wrong_password: true` — ask the user, never guess. Any other tool that meets an encrypted PDF says to call `unlock_pdf`. ## Prompts | Prompt | Arguments | What it does | |---|---|---| | `private_ai_answer` | `path`, `question` | The whole private round trip in one go: anonymize with mapping → answer on placeholders → `deanonymize_text` → reply with real names. | | `index_pdf_for_rag` | `path`, `max_tokens?` | Chunk with ids, pages and sections as vector-store metadata, and cite as «section, p. N». | ## Progress `pdf_ocr` sends `notifications/progress` (0–100) when the client passes a `progressToken`, so a 300-page scan does not look hung. ## Errors Failures come back as a tool result with `isError: true` and one actionable sentence — for example a password-protected PDF (`This PDF is password protected…`), a file that is not a PDF, or a path outside the allowed root. The server never crashes on bad input. --- # `sirdas` CLI Same engine as the web app and the MCP server, from the terminal. No network, no accounts, no telemetry. ## Install ```bash npx sirdas factura.pdf # nothing to install but Node 20+ npm i -g sirdas # or keep it around ``` From source: ```bash git clone https://github.com/Brayan15p/SIRDAS-APP-PDF.git && cd SIRDAS-APP-PDF npm install && npm run build -w packages/core -w packages/mcp -w packages/cli node packages/cli/dist/main.js --help # or: npm link -w packages/cli ``` ## Commands ``` # Start here: reads any file end to end (PDF, scan, DOCX, XLSX, PPTX, HTML, EPUB, CSV, EML, PNG/JPG) sirdas sirdas leer [--json] [--no-anonymize] [--no-ocr] [--max-tokens 1500] [-o out.md] sirdas convertir [--json] [-o out.md] sirdas separar [-d dir] [--json] # one PDF per document inside a batch sirdas formulario [--json] # filled form fields and checkboxes sirdas classify [--json] # factura, contrato, historia_clinica… sirdas revisar [--sin-nombres] # run everything over real files, report without content sirdas pages delete|extract|rotate [--pages "2, 4-6"] [--angle 90] [-o out.pdf] sirdas number [--format "Página {n} de {total}"] [--position bottom-right] [--skip-first 1] sirdas watermark --text CONFIDENCIAL [--opacity 0.15] sirdas protect [--no-print] [--no-copy] [--no-edit] [-o out.pdf] # asks for the password, or SIRDAS_PASSWORD sirdas unlock [-o out.pdf] sirdas markdown [--no-anonymize] [--json] [-o out.md] [--force] sirdas ocr [--lang spa+eng] [--lang-path dir] [--no-anonymize] sirdas ocr --download-models [--lang spa] # once; then it works offline sirdas anonymize [file.txt] [--json] [--map map.json] # reads stdin when no file sirdas restore [file.txt] --map map.json # puts the real data back sirdas chunks [--max-tokens 2000] [--json] [-d dir] sirdas tables [--csv|--markdown|--json] [-d dir] [--delimiter ";"] sirdas compare [--all] [--json] [--ignore-case] sirdas info [--json] sirdas merge [...] -o out.pdf [--force] sirdas split [--ranges "1-3, 5, 8-end"] [-d dir] [--force] sirdas compress [-o out.pdf] [--force] # lossless # Training data (all local, no model is ever called) sirdas dataset [...] [--format text|chat|prompt-completion|alpaca|sharegpt|langchain|llamaindex|embeddings] [--hf] # train/validation/test.jsonl + Hugging Face README.md [--max-tokens 2000] [--no-anonymize] [--no-dedupe] [--strip-boilerplate] [--split] [--seed s] [--template "..."] [--system "..."] [-d dir] [--no-meta] [--json] sirdas dedupe [...] [--threshold 0.8] [--json] sirdas boilerplate [...] [--min-words 5] [--json] sirdas contamination [...] --against [--n 13] [--json] sirdas rl [...] [--max-per-table 10] [--lang es|en] [-o f.jsonl] sirdas rl --reward-fn # the Python reward function sirdas mcp # start the MCP server over stdio ``` `md` is an alias of `markdown`. ## Conventions agents can rely on - **Exit codes:** `0` success · `1` usage error (bad flags, missing input, refusing to overwrite) · `2` the file could not be processed (password-protected, damaged, not a PDF). The reason is printed to **stderr** in one sentence. - **stdout is the payload.** `markdown` prints Markdown; with `--json` it prints one JSON object (`markdown`, `pages`, `tokens_estimate`, `anonymized`, `redacted`, `has_text`, `source`). Progress and warnings go to stderr. - **Anonymization is on by default** for `markdown`; `--no-anonymize` keeps the original data. - **Never overwrites** an existing file without `--force`. - `anonymize --json` includes the placeholder→value `mapping`; keep it local. ## The anonymize → AI → restore round trip Nothing is ever deleted. The original PDF is untouched and the mapping — which holds the real values — stays on your machine. ```bash # 1. Anonymize, keeping the mapping sirdas markdown historia.pdf --json | jq -r .markdown > anonima.md sirdas anonymize anonima.md --map mapa.json > anonima-final.md # 2. Send anonima-final.md to any model, then restore its answer locally cat respuesta-de-la-ia.txt | sirdas restore --map mapa.json ``` A name always comes back whole: placeholders map to the complete original value, never a fragment. ## Examples ```bash # Markdown for an LLM, personal data replaced sirdas markdown historia.pdf > historia.md # Structured output for a script sirdas markdown contrato.pdf --json | jq '.tokens_estimate, .redacted' # Anonymize a snippet before pasting it anywhere echo "Paciente: Ana Gómez, C.C. 52.123.456" | sirdas anonymize # Split the first three pages and the last one sirdas split informe.pdf --ranges "1-3, 8-end" -d ./partes # Pull an invoice's line items into a spreadsheet (CSV carries a BOM for Excel) sirdas tables factura.pdf --delimiter ";" -d ./tablas # Split a long document into chat-sized pieces sirdas chunks informe.pdf --max-tokens 2000 -d ./partes # Merge, then shrink losslessly sirdas merge a.pdf b.pdf -o todo.pdf && sirdas compress todo.pdf ``` --- # Python ```bash pip install sirdas # requires Node.js 20+; the engine is the same one the CLI and MCP server use pip install "sirdas[langchain]" # or [llamaindex] ``` The client runs `sirdas … --json` locally. It finds the executable in this order: `SIRDAS_BIN`, `sirdas` on `PATH`, `npx -y sirdas@`. ## Read anything ```python import sirdas doc = sirdas.read("factura.pdf") # also .docx, .xlsx, .pptx, .html, .eml, .png… doc["status"] # "ok" | raises sirdas.PasswordRequired doc["document_type"] # "factura" for f in doc["fields"]: print(f["name"], f["value"], f["page"], f["bbox"]) # bbox: [x0, y0, x1, y1] in 0–1 doc["tables"], doc["form"], doc["documents"], doc["chunks"], doc["next_steps"] ``` Options: `password=`, `anonymize=True`, `ocr=True`, `max_tokens=1500`. ## Other calls | Call | Returns | |---|---| | `sirdas.convert(path)` | `{format, markdown, tables, warnings}` | | `sirdas.split_documents(path, output_dir=None)` | `{segments, files}` | | `sirdas.forms(path)` | `{fields, checkboxes}` | | `sirdas.classify(path)` | `{type, confidence, signals, runnerUp}` | | `sirdas.chunks(path, max_tokens=1500)` | `[{id, pages, section, text, tokens}]` | | `sirdas.tables(path)` | tables | | `sirdas.dataset(paths, out_dir, format="chat", split=True)` | written files, incl. `README.md` dataset card | | `sirdas.protect(path, password, output)` / `sirdas.unlock(path, output, password="")` | output path | ## LangChain and LlamaIndex ```python from sirdas.integrations import SirdasLoader, SirdasReader docs = SirdasLoader("contrato.pdf", max_tokens=800).load() # langchain_core Documents nodes = SirdasReader().load_data("historia.pdf") # llama_index Documents ``` Each chunk keeps `id` (stable content hash — use it as the vector id), `pages`, `section` and `document_type` in its metadata. ## Fine-tuning dataset in one line ```python sirdas.dataset(["contratos/"], "ds/", format="alpaca") # train/validation/test.jsonl + README.md from datasets import load_dataset load_dataset("json", data_files={"train": "ds/train.jsonl", "test": "ds/test.jsonl"}) ``` --- # n8n Install: **Settings → Community Nodes → Install → `n8n-nodes-sirdas`**. Self-hosted n8n only (community nodes are not available on n8n Cloud's starter plans). The node reads the binary from the previous node (Gmail, Google Drive, Webhook, Read Binary File…) and never sends it anywhere. | Operation | Output | |---|---| | **Read Document** | `document_type`, `fieldValues` (flat object for Sheets/Airtable), `fields` with page and bbox, `tables`, `form`, `documents`, `markdown`, `chunks`, `next_steps` | | **Convert to Markdown** | `format`, `markdown`, `tables` | | **Split Batch** | one item + PDF binary per document | | **Read Form** | `fields`, `checkboxes` | | **Protect PDF** / **Unlock PDF** | PDF binary | ## Recipes - **Invoices from email to a sheet:** Gmail Trigger (attachments) → Sırdaş *Read Document* → IF `{{$json.document_type}}` equals `factura` → Google Sheets *Append* `{{$json.fieldValues.numero_factura}}`, `{{$json.fieldValues.nit_emisor}}`, `{{$json.fieldValues.total}}`. - **Scanned batches:** Drive Trigger → Sırdaş *Split Batch* → Drive *Upload* (`{{$json.type}}` folder). - **AI without exposing patients:** Webhook → Sırdaş *Read Document* (anonymize on) → OpenAI/Claude node with `{{$json.markdown}}`. OCR in n8n needs the Tesseract language data on the host once: `npx sirdas ocr --download-models --lang spa+eng`. --- # `@sirdaspdf/core` (library) The runtime-agnostic engine behind the web app, the CLI and the MCP server. Depends only on `pdf-lib` and `fflate`; the PDF.js part receives an **already-opened** document so it runs the same in a browser (PDF.js worker build) and in Node (`pdfjs-dist/legacy/build/pdf.mjs`, no canvas needed). ```ts import { anonymize, pdfToMarkdown, estimateTokens, parseRanges, mergePdfs, splitPdf, pageCount, organizePdf, compressLossless, classifyError } from "@sirdaspdf/core"; import * as pdfjs from "pdfjs-dist/legacy/build/pdf.mjs"; // Node const doc = await pdfjs.getDocument({ data: bytes, isEvalSupported: false, verbosity: 0 }).promise; const markdown = await pdfToMarkdown(doc); // layout-aware Markdown await doc.destroy(); const { text, findings } = anonymize(markdown); // consistent placeholders console.log(estimateTokens(text), findings.length); ``` Subpath imports keep bundles small: `@sirdaspdf/core/` for any module (`anonymize`, `formats`, `extract`, `classify`, `segment`, `forms`, `chunk`, `ranges`…). ## Exports | Function | Purpose | |---|---| | `pdfToMarkdown(doc, onProgress?)` | Full pipeline: `extractLines` → `linesToMarkdown`. | | `extractLines(doc, onProgress?)` / `linesToMarkdown(lines)` | The two halves, if you want to post-process lines. | | `anonymize(text)` | `{ text, findings: { type, value, label }[] }`. | | `estimateTokens(text)` | ~4 characters per token. | | `parseRanges("1-3, 5, 8-end", pageCount)` | 0-based page groups; throws a user-facing error on bad input. | | `mergePdfs(buffers)` · `splitPdf(buffer, groups)` · `pageCount(buffer)` · `organizePdf(buffer, edits)` · `compressLossless(buffer)` | pdf-lib operations returning `Uint8Array`. | | `classifyError(err)` → `"encrypted" \| "corrupt" \| "generic"` · `PdfError` | Turn pdf-lib / PDF.js failures into actionable causes. | | `convertToMarkdown(bytes, filename)` · `detectFormat` · `htmlToMarkdown` · `parseDelimited` | DOCX, XLSX, PPTX, ODT/ODS/ODP, EPUB, HTML, CSV, EML, RTF, JSON → `{ format, markdown, parts, tables, warnings }`. | | `classifyDocument(text)` · `extractFields(markdown, type)` | Document type with visible signals; fields with `page` and `evidence`. | | `chunkForAgents(markdown, maxTokens)` · `contentId(text)` | Chunks with stable `id`, `pages`, `section`. Use `pdfToMarkdown(doc, _, { pageMarkers: true })`. | | `locateText(lines, text, page?)` | Bounding box (0–1, top-left origin) of a citation across up to three lines. | | `segmentPages(pageTexts)` | Documents inside a batch PDF, with the reasons for each cut. | | `readFormFields(bytes)` · `findTextCheckboxes(text)` | AcroForm fields and written checkboxes. | | `buildRecords` · `toJsonl` · `datasetCard` · `DATASET_FORMATS` | Training/RAG datasets and the Hugging Face card. | | `protectPdf` · `unlockPdf` · `lockState` | AES-256 via an injected qpdf WebAssembly module. | | `deletePages` · `extractPages` · `rotatePages` · `addPageNumbers` · `addWatermark` | Page tools. | ESM, TypeScript types included. Node ≥ 20 or any modern browser. --- # Privacy model (what an agent can promise the user) 1. **No uploads, no network.** Neither the MCP server nor the CLI opens a network connection. They read and write local files only. On the web app the browser enforces the same thing through `Content-Security-Policy: connect-src 'self'`. 2. **Anonymization happens before anything leaves the machine.** The recommended flow for cloud models is: `pdf_to_markdown` (anonymized) → send placeholders to the model → map answers back locally with `anonymize_text`'s `include_mapping`. 3. **Consistent pseudonyms.** The same value always maps to the same placeholder inside one call, so structure and references survive. 4. **It is rule-based.** Detection is deliberately aggressive (false positives are cheap, false negatives are not) but it can miss unusual formats. For sensitive documents, tell the user to review the output. 5. **Nothing is retained.** No cache, no logs of document content, no telemetry. Files written by `merge`/`split`/`compress` go exactly where the user asked, and existing files are never overwritten without an explicit flag. 6. **Verifiable.** The web app ships a live outbound-traffic meter and a page explaining four checks anyone can run: https://sirdas.app/en/privacy/ --- --- name: sirdas-documents description: Read, understand and act on PDF documents locally with the Sırdaş MCP server or CLI — invoices, contracts, medical records, IDs, tax forms, bank statements. Use when the user shares or mentions a PDF and wants its content, key fields, tables, a summary, anonymized text for an AI, cited chunks for RAG, or to protect, unlock, merge, split, number, watermark or compare documents. Nothing is uploaded. --- # Sırdaş: local document skills for agents Every operation runs on the user's machine. No file, text or field leaves it, so it is safe for health, legal and financial documents. ## Setup (once) Claude Code: `claude mcp add sirdas -- npx -y @sirdaspdf/mcp` Other MCP clients: `{"mcpServers": {"sirdas": {"command": "npx", "args": ["-y", "@sirdaspdf/mcp"]}}}` Without MCP: `npx sirdas ` (add `--json` for machine output). Files must live inside the allowed root (default: the directory the server starts in; change it with `SIRDAS_ROOT`). ## The default workflow 1. **Always start with `read_document`.** One call unlocks restricted PDFs, runs OCR on scans, identifies the type, extracts key fields with page citations, anonymizes personal data, finds tables and returns cited chunks plus `next_steps`. 2. **Tell the user what it is**, in one line: type, pages, the 3–5 most useful fields (cite the page). 3. **Offer the `next_steps` proactively**, as a short choice — don't run write operations without a yes. 4. If `status` is `needs_password` or `wrong_password`, **ask the user** and call again with `password`. Never guess or brute-force. ## Choosing the next tool | The user wants… | Tool | |---|---| | Ask an AI about a sensitive document | prompt `private_ai_answer` (anonymize → answer → `deanonymize_text`) | | Index for search / RAG | `pdf_to_chunks` (use `id` as vector id; store `pages`, `section`) or prompt `index_pdf_for_rag` | | Invoice lines or statement movements in Excel | `pdf_tables` | | Just the fields from text you already have | `extract_fields` | | What changed between two versions | `compare_pdfs` | | Password on / off | `protect_pdf` / `unlock_pdf` | | Remove, keep or rotate pages | `edit_pages` | | «Página X de Y», CONFIDENCIAL stamp | `add_page_numbers`, `add_watermark` | | Join / split / shrink | `merge_pdfs`, `split_pdf`, `compress_pdf_lossless` | | Training data from many PDFs | `pdf_to_dataset`, `find_duplicates`, `check_contamination` | ## Honesty rules - Type detection and field extraction are **rule-based**: say "looks like an invoice" and show the evidence when confidence is below 0.6. - Anonymization can miss unusual formats: suggest a quick review before sharing outside. - A watermark or copy restriction is not real protection; a password (AES-256) is.