npm package discovery and stats viewer.

Discover Tips

  • General search

    [free text search, go nuts!]

  • Package details

    pkg:[package-name]

  • User packages

    @[username]

Sponsor

Optimize Toolset

I’ve always been into building performant and accessible sites, but lately I’ve been taking it extremely seriously. So much so that I’ve been building a tool to help me optimize and monitor the sites that I build to make sure that I’m making an attempt to offer the best experience to those who visit them. If you’re into performant, accessible and SEO friendly sites, you might like it too! You can check it out at Optimize Toolset.

About

Hi, 👋, I’m Ryan Hefner  and I built this site for me, and you! The goal of this site was to provide an easy way for me to check the stats on my npm packages, both for prioritizing issues and updates, and to give me a little kick in the pants to keep up on stuff.

As I was building it, I realized that I was actually using the tool to build the tool, and figured I might as well put this out there and hopefully others will find it to be a fast and useful way to search and browse npm packages as I have.

If you’re interested in other things I’m working on, follow me on Twitter or check out the open source projects I’ve been publishing on GitHub.

I am also working on a Twitter bot for this site to tweet the most popular, newest, random packages from npm. Please follow that account now and it will start sending out packages soon–ish.

Open Software & Tools

This site wouldn’t be possible without the immense generosity and tireless efforts from the people who make contributions to the world and share their work via open source initiatives. Thank you 🙏

© 2026 – Pkg Stats / Ryan Hefner

@okrapdf/sdk

v0.17.0

Published

okraPDF — upload a PDF, get an API. Runtime client, React hooks, and CLI.

Readme

@okrapdf/sdk

Upload a PDF, get an OpenAI-compatible endpoint.

npm install @okrapdf/sdk

Just want the okra command? npm install -g @okrapdf/cli (this package is the library it wraps; the legacy unscoped okrapdf package is deprecated).

Get your API key at app.okrapdf.com/settings.

Quick Start

import { OkraClient } from '@okrapdf/sdk';

const okra = new OkraClient({ apiKey: process.env.OKRA_API_KEY });
const session = await okra.sessions.create('./invoice.pdf');

// Every document gets its own chat/completions URL
console.log(session.modelEndpoint);

That prints a URL like:

https://api.okrapdf.com/v1/documents/doc-441a8a0be0e94914b982

This is a full OpenAI-compatible base URL. Plug it into any client.

Choose Your Entry Point

| I want to... | Start here | | -------------------------------- | --------------------------------------------------------------------------- | | Use TypeScript/JS | okra.sessions.create() + session.prompt() — SDK Methods | | Use the CLI | okra auth login + okra upload — CLI | | Use Claude Code or AI agents | MCP server — Setup guide | | Use any LLM client | OpenAI-compatible endpoint — OpenAI SDK | | Build a React app | @okrapdf/sdk/react hooks — Sub-path exports | | Query across documents | Collections fan-out — Collections |

What You Get

Upload a PDF and okraPDF gives you predictable URLs for everything:

Document:   doc-441a8a0be0e94914b982

Completion: https://api.okrapdf.com/document/doc-441a8a0be0e94914b982/chat/completions
Status:     https://api.okrapdf.com/document/doc-441a8a0be0e94914b982/status
Pages:      https://api.okrapdf.com/document/doc-441a8a0be0e94914b982/pages
Entities:   https://api.okrapdf.com/document/doc-441a8a0be0e94914b982/nodes
Download:   https://api.okrapdf.com/document/doc-441a8a0be0e94914b982/download

Page images:
  pg 1:     https://api.okrapdf.com/v1/documents/doc-441a8a0be0e94914b982/pg_1.png
  resized:  https://api.okrapdf.com/v1/documents/doc-441a8a0be0e94914b982/w_200,h_300/pg_1.png
  shimmer:  https://api.okrapdf.com/v1/documents/doc-441a8a0be0e94914b982/d_shimmer/pg_1.png

All URLs are deterministic. Build them from the document ID without calling the API first.

Use with OpenAI SDK

import OpenAI from 'openai';

const openai = new OpenAI({
  apiKey: process.env.OKRA_API_KEY,
  baseURL: session.modelEndpoint, // https://api.okrapdf.com/v1/documents/doc-...
});

const res = await openai.chat.completions.create({
  model: 'okra',
  messages: [{ role: 'user', content: 'What form is this?' }],
});

console.log(res.choices[0].message.content);
// → "This is Form W-9 (Request for Taxpayer Identification Number and
//    Certification), used by entities to collect a taxpayer's TIN..."

Use with AI SDK

import { createOpenAICompatible } from '@ai-sdk/openai-compatible';
import { generateText } from 'ai';

const provider = createOpenAICompatible({
  name: 'okra',
  apiKey: process.env.OKRA_API_KEY,
  baseURL: session.modelEndpoint,
});

const { text } = await generateText({
  model: provider('okra'),
  prompt: 'Summarize this document in 3 bullet points',
});

Use with curl

# Upload
curl -X POST https://api.okrapdf.com/document/doc-my-w9/upload-url \
  -H "Authorization: Bearer $OKRA_API_KEY" \
  -H "Content-Type: application/json" \
  -d '{"url": "https://www.irs.gov/pub/irs-pdf/fw9.pdf"}'

# Ask a question
curl https://api.okrapdf.com/document/doc-my-w9/chat/completions \
  -H "Authorization: Bearer $OKRA_API_KEY" \
  -H "Content-Type: application/json" \
  -d '{"messages": [{"role": "user", "content": "List all parts of this form."}]}'

Response:

{
  "id": "chatcmpl-18g5qhmmrm",
  "object": "chat.completion",
  "model": "accounts/fireworks/models/kimi-k2p5",
  "choices": [
    {
      "message": {
        "role": "assistant",
        "content": "Based on the Form W-9 document, there are two numbered parts:\n\n| Part | Title |\n|------|-------|\n| Part I | Taxpayer Identification Number (TIN) |\n| Part II | Certification |"
      },
      "finish_reason": "stop"
    }
  ],
  "usage": { "prompt_tokens": 227, "completion_tokens": 404, "total_tokens": 631 }
}

SDK Methods

The SDK wraps all of this so you don't need a separate client:

// Ask a question (non-streaming)
const { answer } = await session.prompt('What is the total amount due?');

// Stream
for await (const event of session.stream('Summarize this document')) {
  if (event.type === 'text_delta') process.stdout.write(event.text);
}

// Structured output with Zod
import { z } from 'zod';

const Invoice = z.object({
  vendor: z.string(),
  total: z.number(),
  lineItems: z.array(
    z.object({
      description: z.string(),
      amount: z.number(),
    }),
  ),
});

const { data } = await session.prompt('Extract the invoice', { schema: Invoice });
// data: { vendor: "Acme Corp", total: 1250.00, lineItems: [...] }

Declarative Components

A Component declares the desired structured result. Reusable field Components keep their own schema, grader, assertion, and instructions; the containing document Component only composes them.

/** @jsxImportSource @okrapdf/component */

import { z } from 'zod';
import { Assertion, Component, Field, Grader, StructuredOutput } from '@okrapdf/component';

function TotalField() {
  return (
    <Field name="total" schema={z.string().nullable()}>
      <Grader name="Total Amount Accuracy" type="string_check" operation="eq" />
      <Assertion evaluator="receipt.total-valid" />
    </Field>
  );
}

export default (
  <Component id="receipt" name="Ideal receipt" dataset={{ provider: 'local', id: 'receipts' }}>
    <StructuredOutput name="receipt_details">
      <TotalField />
    </StructuredOutput>
  </Component>
);

The compiler resolves the local requirements to $.total, emits a strict JSON Schema and canonical workflow definition, and leaves evaluator implementation to Effect Layers in the runtime. The same declaration works through the SDK or CLI against hosted and self-hosted servers:

await okra.components.run(receipt, { inputs: { document_id: 'doc-123' } });
okra components build ./receipt.tsx
okra components run ./receipt.tsx --document doc-123

Pages & Entities

const pages = await session.pages(); // { pageCount: 6, pages: [...] }
const { nodes } = await session.entities(); // extracted text, tables, etc.
const { nodes } = await session.entities({ type: 'table' });

Upload

Accepts file paths, URLs, Blob, ArrayBuffer, or Uint8Array:

// URL
const session = await okra.sessions.create('https://example.com/report.pdf');

// Bytes
const session = await okra.sessions.create(pdfBytes, {
  upload: { fileName: 'report.pdf' },
});

// Attach to existing document (no upload, no wait)
const session = okra.sessions.from('doc-441a8a0be0e94914b982');

If you want a passive PDF asset upload instead of an immediate document workflow, use files.upload():

const file = await okra.files.upload('./report.pdf');

console.log(file.id); // doc-...
console.log(file.urls.bytes); // direct PDF bytes URL
console.log(file.workflow_bound); // false

okra.sessions.create() / okra.upload() still mean "upload and start document processing." okra.files.upload() means "store this PDF as a first-class file asset and let me decide later what workflow to run."

An upload can report live processing progress after the upload request has completed:

const controller = new AbortController();
const session = await okra.upload(pdfBytes, {
  signal: controller.signal,
  onProgress: (event) => console.log(event.type, event.data),
});

The progress subscription closes on terminal progress, upload failure, or signal abort. sessions.create() also releases its upload progress subscription when its wait finishes. Calling controller.abort() stops this client's upload and progress subscription; server work that has already been accepted may continue.

For explicit document-event iteration, subscribe before opening the connection:

const events = okra.events(session.id, { bufferSize: 1024 });
const iterator = events[Symbol.asyncIterator]();
events.connect();
try {
  for await (const event of iterator) {
    console.log(event.type, event.data);
    if (event.type === 'document.ready') break;
  }
} finally {
  events.close();
}

Callbacks receive live events without retaining an iterator backlog. Iteration starts at iterator creation and shares one cursor. Its unread buffer defaults to 1024 events; overflow closes the connection and rejects the iterator explicitly. A remote close drains accepted events. Explicit close, signal abort, or iterator return discards unread events and releases pending reads. Calling connect() repeatedly keeps the same connection. Create a new event stream after it closes to subscribe again.

session.wait({ timeoutMs, pollIntervalMs }) applies its deadline to status requests, response-body reads and polling sleeps. With realtime: true, a timed-out event wait closes its subscription before starting a fresh polling window. After a ready event, the final status request uses the same deadline; if it fails or times out, the ready event supplies the result. Caller cancellation releases local resources and rejects with TIMEOUT / status 499.

Parse Jobs & Polling

Use okra.parse() for a passive-file parse job, or create a document reparse job with okra.createJob({ type: 'document.parse', document_id: 'doc-...' }). The same job envelope is returned from POST /v1/parse, POST /v1/jobs, GET /v1/jobs/{id}, and okra jobs wait:

const job = await okra.parse({ fileId: file.id, parser: 'textlayer' });

console.log(job.terminal); // false until completed/failed/cancelled
console.log(job.progress?.percent); // best-known 0-100 progress
console.log(job.next_poll_after_ms); // n8n-friendly poll delay
console.log(job.retryable, job.error_code, job.user_message);
console.log(job.cost_usd); // real vendor usage at completion when available

Pass signal to okra.parse() to cancel input preparation or the client request. Cancellation stops later submission after a pending Blob or local-file read, releases response readers, and discards responses that arrive after cancellation. A job already accepted by the server continues; its job ID remains the handle for observing or cancelling server processing.

Parse jobs expose live page/chunk counters while work is running: pages_completed, pages_failed, pages_running, pages_pending, pages_total, chunks_completed, and chunks_total. Empty parser output now fails with error_code: "empty_result" instead of reporting a completed job with no page coverage.

Document Wait-For Subscriptions

okra documents wait-for <docId> --match "<term>" creates a document subscription, then long-polls until matching nodes are ingested:

okra documents wait-for doc-abc123 --match "table of contents" --timeout 120
okra documents wait-for doc-abc123 --match "Revenue" --literal

The CLI prints a structured envelope with result.matched_nodes[] entries: node_id, page, value_excerpt, optional bbox, and optional bbox_source. It also includes next_actions such as reading the matched page. The underlying API is POST /v1/documents/{id}/subscriptions followed by GET /v1/documents/{id}/subscriptions/{subscription_id}/wait?timeout=...; wait responses return status: "active" | "matched" | "timeout", matched, and matches[].

Deterministic URLs

Build page image and export URLs from a document ID — no API call needed:

import { doc } from 'okrapdf/doc';

const d = doc('doc-441a8a0be0e94914b982');

d.pages(1).image(); // .../pg_1.png
d.pages(1).image({ w: 200, h: 300 }); // .../w_200,h_300/pg_1.png
d.export('markdown'); // .../export.md

Collections — Fan-Out Query to CSV

Ask the same question across every document in a collection. Each doc answers independently in parallel, results stream back as NDJSON.

import { OkraClient } from '@okrapdf/sdk';
import { z } from 'zod';
import { writeFileSync } from 'fs';

const okra = new OkraClient({ apiKey: process.env.OKRA_API_KEY });

// Fan-out: ask every doc in the collection the same question
const stream = okra.collections.query(
  'col-40da068481cf4f248853507cba6be611',
  'Who are the top 3 people mentioned in this document?',
);

// Gather all results
const result = await stream.gather();

// Write CSV
const header = 'doc_id,doc_name,answer,cost_usd';
const rows = [...result.answers.values()].map(
  (a) => `"${a.docId}","${a.answer.slice(0, 200)}",${a.costUsd}`,
);
writeFileSync('results.csv', [header, ...rows].join('\n'));

console.log(`${result.completed} docs, $${result.totalCostUsd.toFixed(4)} total`);

Real output from a 10-K earnings collection:

doc_id,file_name,answer,cost_usd
"doc-9a3f21...","NVDA-10K-2025.pdf","Revenue: $130.5B, Net Income: $72.9B, YoY Growth: 114%",0.0048
"doc-b7e810...","AAPL-10K-2025.pdf","Revenue: $391.0B, Net Income: $101.2B, YoY Growth: 5%",0.0039
"doc-c4d562...","MSFT-10K-2025.pdf","Revenue: $254.2B, Net Income: $97.1B, YoY Growth: 16%",0.0051

Experimental: structured collection fan-out is still available via a schema, but it is not part of the stable v0.14 surface:

const FinancialReport = z.object({
  company: z.string(),
  revenue: z.number(),
  netIncome: z.number(),
  quarter: z.string(),
});

const stream = okra.collections.query('col-financials', 'Extract the financial summary', {
  schema: FinancialReport,
});

const result = await stream.gather();
for (const [docId, answer] of result.answers) {
  console.log(answer.data); // { company: "NVIDIA", revenue: 35082, ... }
}

Or stream per-doc events in real time:

for await (const event of stream) {
  if (event.type === 'result') {
    console.log(`${event.doc_id}: ${event.answer.slice(0, 80)}...`);
  }
}

Sub-path Exports

| Import | Use | | ---------------------------- | --------------------------------------------------------------------------------------------------------------------- | | @okrapdf/sdk | OkraClient, types, errors | | @okrapdf/sdk/doc | doc() URL builder | | @okrapdf/sdk/browser | Browser-safe client (no Node.js deps) | | @okrapdf/sdk/worker | Cloudflare Worker adapter | | @okrapdf/sdk/react | React hooks (useSession, usePages) | | @okrapdf/sdk/content-types | Typed extraction contracts — getContentType, listContentTypes, manifests (dependency-light: no client/CLI bundle) |

CLI

npm install -g @okrapdf/cli   # the `okra` binary lives in @okrapdf/cli, a thin wrapper over this SDK
okra auth login

okra upload ./invoice.pdf
okra context ask "What is the total?" --source-id doc-abc123   # cited answer
okra extract ./invoice.pdf --content-type invoice              # typed + grounded; or --schema ./schema.json --cite
okra invoice extract ./invoice.pdf                             # the same, as a first-class noun
okra collections query earnings "What changed quarter over quarter?" -o earnings.csv

Use npx @okrapdf/sdk ... for one-off runs if you do not want a global install.

Agent Skill Install

For skill-aware coding agents, the intended distribution form is:

npx skills add okrapdf/okra-agent

Publishing the public okrapdf/okra-agent skill repo is a follow-up release step. The form that works today from an installed CLI package is:

npm install @okrapdf/sdk
npx skills add ./node_modules/@okrapdf/sdk/skills/okra-agent

If your install path differs, use ./node_modules/<pkg>/skills/okra-agent, where <pkg> is the installed okra package directory.

To point the same CLI commands at a self-hosted or Railway instance, create and activate a profile:

okra profile add local --base-url https://my-okra.up.railway.app
okra profile use local
okra upload ./invoice.pdf
okra parse doc-abc123 --model gemini-3-flash --prompt layout-bbox-gemini-multipage@1
okra audit doc-abc123 --standard wcag
okra redact doc-abc123 --model local
okra open doc-abc123 --view review

Self-host runtime bundles can be checked locally without starting a server:

okra self-host validate ./examples/self-host-runtime
okra self-host plan ./examples/self-host-runtime --target railway
okra self-host railway ./examples/self-host-runtime > railway.json
okra self-host env ./examples/self-host-runtime
okra self-host compose ./examples/self-host-runtime --mode stack > docker-compose.yml
okra self-host compose ./examples/self-host-runtime --mode networks > docker-compose.networks.yml
okra self-host smoke https://my-okra.up.railway.app --workflow both

Start the zero-config local server and its attached browser workbench through the CLI package:

npx @okrapdf/cli@latest web
npx @okrapdf/cli@latest serve                 # server-first; do not open a browser
OKRA_SERVER_PASSWORD=secret npx @okrapdf/cli@latest web --host 0.0.0.0

Both commands expose okra.client.v1 over HTTP+SSE, persist local state under ~/.okra/serve, and serve the same document-first workbench at /. Opening a PDF never starts parsing. A validated runtime bundle remains optional:

okra serve ./examples/self-host-runtime --port 8787

CLI diagnostics, completions, and exits

okra doctor
okra doctor --json
okra completions zsh > ~/.zfunc/_okra
okra completions bash > ~/.okra-completions.bash

okra doctor checks the local Node runtime, auth/config, API reachability, installed CLI version, and self-host environment variables without printing secrets. In headless mode or with --json, failures use a stable envelope: ok, command, error, message, hint, retryable, numeric HTTP-style code, process exit_code, and next_actions.

Exit codes: 0 success, 1 client/auth/usage error, 2 server or network error.

For Docker/Railway, the example image runs the same command against /app/self-host-runtime and exposes /health, /v1/self-host/status, /v1/workflows/catalog, /v1/capabilities, SDK-compatible document routes under /document/:id/*, shared graph routes under /v1/documents/:id/*, /v1/sources/n8n, and /v1/workflows. Starter uploads and workflow runs write OkraDocumentGraph artifacts under OKRA_DATA_DIR; workflow runs also persist one inspectable capability_run per declared recipe step. Set OKRA_API_KEY on public self-host deployments to require bearer auth on uploads, URL ingestion, n8n source intake, and workflow execution.

The CLI focuses on the core workflows: auth, upload, extract, chat/ask, list/delete, render, collection query, endpoint profiles, and self-host bundle validation/planning/runtime launch. Advanced inspection commands (find, search, status) and the facet/lens deployment commands remain available but are hidden from default help. Experimental structured collection extraction is available via okra collection extract ... and okra collection query ... --schema ....

License

MIT