> ## Documentation Index
> Fetch the complete documentation index at: https://docs.ndi.nace.ai/llms.txt
> Use this file to discover all available pages before exploring further.

# NDI CLI

> Parse, extract, search, and test documents from your terminal

The NDI CLI (`ndi`) gives you terminal access to the same `/v1` methods as the
SDKs: parse, extract, split, classify, ground, plus workspaces, ingestion, and
search. Use it for batch processing, scripting, CI/CD, and quick operations
without writing application code.

## Fast path for coding agents

Use the CLI when the document is already on disk or when an agent needs a
terminal-first workflow. It avoids writing upload code and prints Markdown
that agents can read directly.

```bash theme={"dark"}
pip install ndi-cli
ndi login
ndi parse ./document.pdf -o md --save document.parse.md
```

A directory is scanned recursively for supported files, one job each:

```bash theme={"dark"}
ndi parse ./documents --out-dir ./out
```

For agent tool calling instead of terminal commands, use the [MCP server](/sdks/mcp).

## Installation

```bash theme={"dark"}
pip install ndi-cli
```

Requires Python 3.11+. The executable is `ndi`. `ndi version` prints the CLI
and the `ndi-sdk` it is talking through.

## Authentication

```bash theme={"dark"}
ndi login
```

This opens the NDI console in the browser and writes the minted key to
`~/.ndi/config.toml`. `$NDI_API_KEY` still wins over the file.
`$NDI_CONFIG_PATH` overrides the config location.

To paste a key instead of opening a browser:

```bash theme={"dark"}
ndi login --api-key
# KEY=- reads stdin; omit KEY to type it hidden
```

Workspace-scoped verbs also need a workspace:

```bash theme={"dark"}
ndi workspace create fy25-audit
ndi workspace use <workspace-id>
# or: export NDI_WORKSPACE_ID="<workspace uuid>"
# or: ndi parse … --workspace ID
```

The default host is `https://ndi-api.nace.ai`. Override with `$NDI_BASE_URL`.

## Quick examples

```bash theme={"dark"}
# Parse a single file (markdown to stdout, status to stderr)
ndi parse path/to/document.pdf

# Parse an entire folder
ndi parse ./docs --out-dir ./out

# Extract with a schema (path or inline JSON)
ndi extract ./docs/invoice.pdf -s schemas/invoice.json

# Reuse a parse as the extract source
ndi parse a.pdf -o id | ndi extract - -s schema.json

# Split into sections you name
ndi split packet.pdf --class invoice:Invoice --class backup:Backup

# Search an ingested workspace
ndi hybrid-search "total liabilities" -k 10

# Count or list across the whole corpus
ndi filtered-search "all invoices over $500"

# Test a ledger package
ndi je-testing "test whether any posted journal line was approved by its own preparer" \
  --path ledger/journal.xlsx

# Measure the ingested journal package
ndi ledger-understanding start
ndi ledger-understanding get

# Upload a nested folder, skip unsupported types, then ingest
ndi files upload ./corpus --ingest
```

Parsed markdown goes to **stdout** by default. Status lines (`job <id> queued`,
`saved …`) go to **stderr**. `-o auto|md|json|payload|id` picks the shape.
`--json` is an alias for `-o json`. `--save PATH` / `--out-dir DIR` write
files. `--async` submits and prints the job id.

When a Parse result has external complete content, `-o auto` and `-o md` print
the inline preview. Use `-o json` or `-o payload` to read its disclosure and
authenticated full-content URL. A final-size preview carries
`content_truncated`; reduced-layout Office parsing carries `parse_fidelity`.
Documents use `document.content_url`; workbooks use the affected
`document.spreadsheet.sheets[].content_url`. The CLI does not download these
URLs automatically.

API failures exit 1. Usage and config errors exit 2. An unsupported single
file is reported on stderr as `error: 422 [unsupported_file_type] ...`; the
command does not print an upload handle or job ID, `ndi files upload` leaves
no workspace file, and `--ingest` does not queue ingestion. A directory
upload skips those files with a warning instead of failing the run.

## Supported file types

Directory scans for document operations pick up these extensions (other files
are skipped with a note on stderr). `ndi files upload DIR` also accepts the
workspace-only types in [Format support](/concepts/format-support) (HTML,
plaintext, markdown, email, extra media). Per-operation gates are stricter and
are enforced by the API for each file. Do not copy this table into automation
as a permanent allowlist.

| Format             | Extensions                                                |
| ------------------ | --------------------------------------------------------- |
| PDF                | `.pdf`                                                    |
| Images             | `.png`, `.jpg`, `.jpeg`, `.tif`, `.tiff`, `.webp`         |
| Office documents   | `.doc`, `.docx`, `.ppt`, `.pptx`                          |
| Spreadsheets       | `.xls`, `.xlsx`, `.xlsm`, `.xlsb`, `.ods`, `.csv`, `.tsv` |
| Columnar / records | `.parquet`, `.jsonl`, `.ndjson`, `.json`                  |
| Audio / video      | `.wav`, `.mp3`, `.m4a`, `.mp4`, `.mov`                    |

Commands accept a file, a directory, an `https://` URL, a staged
`ndi://upload/<uuid>`, a prior parse `jobid://<uuid>` (not classify), a
workspace file `ws://<file_id>`, or `-` (a handle line, or raw bytes with
`--file-name`).

## Parse

Converts a document into markdown, text, and layout blocks.

### Flags

| Flag                 | Description                                                                      |
| -------------------- | -------------------------------------------------------------------------------- |
| `-p`, `--pages SPEC` | 1-based pages, e.g. `3` or `1-5,8`                                               |
| `--mode`             | Parsing effort: `low` (default), `medium`, `high`. `high` adds page verification |
| `--format`           | Comma list: `markdown`, `text`, `blocks` (default `markdown,blocks`)             |
| `--table-format`     | `html` (default) or `markdown`                                                   |
| `--chunk`            | `none` (default), `page`, or `section`                                           |
| `--figures`          | `omit`, `include`, or `describe` (default)                                       |
| `--sheets`           | Comma-separated workbook sheet names                                             |
| `--password`         | PDF password. Never stored                                                       |
| `-j N`               | Parallel jobs for a directory (default 4)                                        |

### Examples

```bash theme={"dark"}
ndi parse document.pdf
ndi parse document.pdf --mode high
ndi parse document.pdf -p 1-5,8
ndi parse workbook.xlsx --sheets "P&L,Balance sheet"
ndi parse contract.pdf -o md --save contract.parse.md
```

See [Parse](/guides/parse) for what the result contains.

## Extract

Pulls structured data against a JSON Schema you provide. The top-level schema
must be an object. Pass exactly one of `-s`/`--schema` (file path or inline
JSON) or `--schema-id` (a saved schema).

```bash theme={"dark"}
ndi extract ./docs/invoice.pdf -s schemas/invoice.json
ndi extract ./docs/invoice.pdf -s '{"type":"object","properties":{"total":{"type":"number"}}}'
ndi extract --validate -s schemas/invoice.json
```

`--validate` checks the schema without creating a job. `--instructions` adds
guidance. `--no-citations` drops cell/page citations. `-p` limits pages.

<Info>
  Extraction can reuse a prior parse: `ndi parse a.pdf -o id | ndi extract - -s schema.json`
  sends the job id on stdin instead of uploading the file again.
</Info>

See [Extract](/guides/extract) and [Extraction schemas](/guides/extraction-schemas).

## Split

Finds logical sections against classes you name. Each `--class` is
`id:label[:description]`; `--classes FILE.json` takes an array of the same
shape.

```bash theme={"dark"}
ndi split packet.pdf --class invoice:Invoice --class backup:Supporting
ndi split packet.pdf --classes classes.json --unknown include --include-content
```

`--unknown` is `include` (default), `force`, or `error`.

## Classify

Labels a document or its pages against your classes. Same `--class` /
`--classes` shape as split.

```bash theme={"dark"}
ndi classify scan.pdf --class invoice:Invoice --class receipt:Receipt
ndi classify scan.pdf --classes classes.json --granularity page
```

`--granularity` is `document` (default) or `page`. `--unknown` is `allow`
(default) or `force_best`. Classify cannot reuse a parse job — pass a file,
URL, upload, or workspace file.

## Ground

Locates quoted text back in the source.

```bash theme={"dark"}
ndi ground report.pdf --target t1="total liabilities of $4.2 million"
ndi ground report.pdf --targets targets.json --previews --max-matches 5
```

Each `--target` is `id=TEXT`. `--targets` takes a JSON array of the same
objects the API accepts.

## Workspaces and files

A workspace is a persistent corpus. Upload, ingest, then search.

```bash theme={"dark"}
ndi workspace create fy25-audit
ndi workspace use <id>
ndi files upload ./ledger.xlsx --path ledgers/journal.xlsx --ingest
ndi files upload ./corpus --ingest
ndi ingest --prefix ledgers/          # or --file-id, or --stale-only
ndi workspace stats
```

A directory walks nested files, keeps relative paths under `--path` (default:
the directory name), and uploads in parallel (`-j`, default 8). Unsupported
files and `.DS_Store` are skipped; a warning on stderr lists them when the
run finishes. `--ingest` then queues ingestion in batches of 1000 file ids.
A single file still requires `--path`. `--on-conflict` is `reject` (default)
or `new_version`. Delete a workspace with `--confirm-name` equal to the
workspace name.

See [Workspaces](/guides/workspace) and [Ingest](/guides/ingest-reconcile).

## Search and journal-entry testing

Files must be ingested first. These verbs return jobs.

```bash theme={"dark"}
ndi hybrid-search "revenue by segment" -k 10 --path-prefix reports/
ndi automatic-search "What were total liabilities at year end?"
ndi fact-search "What were total liabilities at year end?"
ndi deep-search "Which subsidiaries missed covenant tests?" --effort high
ndi filtered-search "all invoices over $500" --prefix invoices/
ndi je-testing "test whether any posted journal line was approved by its own preparer" \
  --path ledger/journal.xlsx --reasoning-effort medium
ndi ledger-understanding start
ndi ledger-understanding get
```

`hybrid-search` is inline (no job). `automatic-search`, `fact-search`,
`deep-search`, `filtered-search`, and `je-testing` wait for the job unless you
pass `--async`. `automatic-search` prints the chosen route and the output job
id; receipts and a filtered cursor belong to that job. A filtered route's
`next:` cursor pages with `ndi filtered-search --cursor`.
`ndi ledger-understanding start` waits the same way; `get` is a read and
prints the stored report.

`--effort` on `deep-search` is `low`, `medium`, or `high`. JET uses
`--reasoning-effort` to change its thinking budget without switching engines;
omit it for the service default. `--path` on `je-testing` is
repeatable and intersects with `--prefix`. There is no `--session-id`: a
shell has nowhere to keep a thread.

`filtered-search` answers filter/rank/count questions over the metadata
catalog rather than by reading files. Its result prints a `next:` cursor when
more rows remain; page it with `ndi filtered-search --cursor <cursor>`. A page
is a new job that re-executes the original query against the **current**
catalog, so `--prefix` and `--context` are refused with a cursor — they belong
to the initial query — and the page's own flags supersede the first run's.
There is no `--result-unit` (the server deprecates the grouping override) and
no `--allow-clarification` (a clarification is answered by a new initial
query, and a shell has nowhere to keep the thread).

Read `not exhaustive` before treating a filtered-search count as a census: the
matches are valid, but the total is a floor. An `unreadable` or `partly
readable` line names the field behind a thin result. Neither exits non-zero —
both are successful partial answers.

A degraded journal-entry testing run exits non-zero even though the job
succeeded — the procedure broke before finalizing an answer.

`--force` on `ledger-understanding start` re-measures even when a report
for this package already exists. Without it, an unchanged package
republishes the stored report. A workspace that has never produced a
report exits on `ledger_understanding_not_found`.

See [Search](/guides/search), [Automatic search](/api-reference/automatic-search),
[Filtered search](/api-reference/filtered-search),
[Journal-entry testing](/api-reference/je-testing), and
[Ledger understanding](/api-reference/ledger-understanding).

## Workspace tools

Read-only inspection of an ingested corpus. These are the six verbs an agent
sandbox is allowed to run; every other `ndi` command is refused there.

| Command                     | Use                                                                   |
| --------------------------- | --------------------------------------------------------------------- |
| `ndi folder-metadata [DIR]` | Counts, categories, one page of files. `--tables` adds a table census |
| `ndi file-metadata PATH`    | Components, schemas, which tool applies                               |
| `ndi read-file PATH`        | Text to stdout; truncation footer to stderr                           |
| `ndi ask-file PATH QUERY`   | Cited question of one document, image, or recording                   |
| `ndi run-sql "SELECT …"`    | One read-only SELECT over ingested tables                             |
| `ndi hybrid-search QUERY`   | BM25 + dense vectors, fused                                           |

See [File tools](/guides/file-tools) and [Query tables](/guides/query-tables).

## Jobs

```bash theme={"dark"}
ndi job <id>                 # read one job
ndi jobs --kind parse --status succeeded
ndi cancel <id> --reason "no longer needed"
```

`-o id` prints only the job id. A wait that times out still prints the id so
you can `ndi job` it later. Default HTTP timeout is 900 seconds
(`--timeout`).

See [Jobs](/guides/jobs).

## Output

| Flag            | Effect                                                                  |
| --------------- | ----------------------------------------------------------------------- |
| `-o auto`       | Default text rendering (markdown for a parse that produced it)          |
| `-o md`         | Parse markdown only                                                     |
| `-o json`       | The whole job record (`--json` is an alias)                             |
| `-o payload`    | `job.result` only                                                       |
| `-o id`         | Job id only                                                             |
| `--save PATH`   | Write one result to this file                                           |
| `--out-dir DIR` | One file per input (required for a directory; `--save` is for one file) |
| `--async`       | Submit and print the job id without waiting                             |

## Next steps

<CardGroup cols={2}>
  <Card title="Parse guide" icon="file-text" href="/guides/parse">
    Modes, pages, spreadsheets, and what the result contains.
  </Card>

  <Card title="Extract guide" icon="braces" href="/guides/extract">
    JSON Schema extraction and citations.
  </Card>

  <Card title="Search a workspace" icon="search" href="/guides/search">
    Hybrid, fact, deep search, and journal-entry testing.
  </Card>

  <Card title="MCP server" icon="bot" href="/sdks/mcp">
    The same catalog as tools inside Claude Code, Cursor, or opencode.
  </Card>
</CardGroup>
