Skip to content

Commit 63c52a8

Browse files
ReadycodeAIclaude
andcommitted
ReadyCode Reader 0.1.0: PDF, Word and Excel via MCP and in the browser
Local MCP server (load_document, ask_document, list_documents) and a browser demo sharing one engine (src/core.cjs). Files are read locally; only the passages a question needs go to TypeSafe Jev on the user's own OpenRouter key. Benchmarks with code-checked answers in benchmark/. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
1 parent 1366300 commit 63c52a8

19 files changed

Lines changed: 2546 additions & 0 deletions

‎.gitignore‎

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,4 @@
1+
node_modules/
2+
web/dist/
3+
*.log
4+
.DS_Store

‎NOTICE‎

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,5 @@
1+
ReadyCode Reader
2+
Copyright 2026 ReadyCode (https://readycode.ai)
3+
4+
This product includes software developed by ReadyCode.
5+
It uses pdf.js (Mozilla Foundation, Apache-2.0).

‎README.md‎

Lines changed: 123 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,123 @@
1+
# ReadyCode Reader
2+
3+
**Ask huge PDFs, Word documents and spreadsheets specific questions. Your AI gets only the passages that answer, with citations, instead of reading the whole file.**
4+
5+
ReadyCode Reader is a free, open-source [MCP](https://modelcontextprotocol.io) server for Claude Code, Cursor, Codex and any other MCP client, and it also runs in a browser. It reads a file once on your computer. Then, for each question, it returns **only the passages that answer it**, with where they came from, plus checks on that evidence:
6+
7+
- **Not in the document:** if the file can't answer, your AI is told `not_in_document` instead of getting passages to guess from.
8+
- **Passages disagree:** if two passages give different values, your AI is told to report both.
9+
- **Hidden instructions:** passages that try to instruct the AI reading them are flagged and treated as data only.
10+
- **Needs a calculation:** spreadsheet questions like "how many…" or "list every…" are flagged instead of being estimated from a few rows.
11+
12+
## Standard AI vs Reader
13+
14+
Same questions, same AI, fresh sessions: once the normal way (the AI reads the file with its own tools), once with Reader.
15+
16+
| Test | Standard | With Reader |
17+
|---|---|---|
18+
| **Codex**, 200-page NASA PDF, 8 questions | 8/8 correct · **913,468 tokens** · about 3 min | 8/8 correct · **41,997 tokens** · about 20 s |
19+
| **Grok 4.7 in Cursor**, same PDF | 8/8 correct · read the whole book (about 45,000 tokens) · about 4 min | 8/8 correct · **4,693 tokens** of evidence · about 2 min |
20+
21+
Token counts for Codex come from Codex's own session log. That's **95% fewer tokens and about 9× faster** for the same answers. Most of the saving comes from steps: an agent re-sends its whole conversation at every step, and with Reader it needed 2 steps instead of 17.
22+
23+
## Measured on large files
24+
25+
Every expected answer below was checked by code against the file, and every "not in the document" question was checked to be truly absent. Scripts and question files are in [`benchmark/`](benchmark/).
26+
27+
| File | Size as text | Correct | Tokens returned | Time for all questions | Check cost |
28+
|---|---|---|---|---|---|
29+
| NASA *Earth at Night* (PDF, 200 pages) | 42,282 tokens | 8/8 | 3,487 | 2.3 s | $0.0027 |
30+
| Australian Universities Accord Final Report (Word) | 263,487 tokens | 11/11 | 15,790 | 1.7 s | $0.0042 |
31+
| Sample contacts workbook (Excel, 100 MB, 1.1 million rows) | 271,424,159 tokens | 11/11 | 2,323 | 0.9 s | $0.0038 |
32+
33+
The Word report is larger than many AI context windows, and the spreadsheet is more than a hundred times larger than any. Each set includes questions the file cannot answer; Reader said `not_in_document` every time. On the spreadsheet it also declined 3 counting and ranking questions rather than guess.
34+
35+
Honest limits of these results: they are our own runs on three files, not an independent study, and head-to-head runs against a standard AI exist so far only for the PDF.
36+
37+
## Install (MCP)
38+
39+
You need [Node.js](https://nodejs.org) 20 or newer and an [OpenRouter](https://openrouter.ai) API key, set once as the environment variable `OPENROUTER_API_KEY`.
40+
41+
**Claude Code**
42+
43+
```bash
44+
claude mcp add readycode-reader -- npx -y @readycode/reader
45+
```
46+
47+
**Cursor**: add to `~/.cursor/mcp.json`:
48+
49+
```json
50+
{
51+
"mcpServers": {
52+
"readycode-reader": { "command": "npx", "args": ["-y", "@readycode/reader"] }
53+
}
54+
}
55+
```
56+
57+
**Codex**: add to `~/.codex/config.toml`:
58+
59+
```toml
60+
[mcp_servers.readycode-reader]
61+
command = "npx"
62+
args = ["-y", "@readycode/reader"]
63+
```
64+
65+
**Claude Desktop**: add the same entry as Cursor under `mcpServers` in `claude_desktop_config.json`.
66+
67+
Restart your tool and check that `readycode-reader` shows three tools: `load_document`, `ask_document` and `list_documents`.
68+
69+
## Use
70+
71+
Ask your AI something like:
72+
73+
> Load `C:\Docs\contract.pdf` with readycode-reader and tell me everything it says about late fees and termination.
74+
75+
Your AI loads the file, sees its contents, asks Reader specific questions and answers from the cited passages. You don't need to know the file or write special questions.
76+
77+
- `load_document(path)` reads a PDF, Word (.docx), Excel (.xlsx), TXT, Markdown, CSV or JSON file. It returns the file's size and **its contents**: a PDF's bookmarks with page numbers, the Word headings, or a spreadsheet's sheets, columns and an example row. That tells the AI what the file covers before it asks anything.
78+
- `ask_document(questions: [...])` asks up to 20 questions in one call. Each answer carries its passages with page numbers (and printed page numbers), Word sections and paragraphs, or spreadsheet sheet and row.
79+
- `list_documents()` lists what is loaded.
80+
81+
Very large files keep reading in the background: `load_document` may answer `still_reading`, and `ask_document` waits for the file to finish.
82+
83+
## Try it in your browser
84+
85+
The same engine runs in a web page: choose a file, add your OpenRouter key, ask. The page shows the file's contents and suggests questions to start with. The file is read in your browser and never uploaded. See [`web/`](web/); build it with `npm run build:web`.
86+
87+
## How it works
88+
89+
1. **Read** on your computer: [pdf.js](https://mozilla.github.io/pdf.js/) for PDFs; Word sections follow its headings; Excel is streamed, so a 100 MB workbook uses about 650 MB of memory.
90+
2. **Split** into whole passages of about 1,200 characters, never cut mid-passage. Each spreadsheet row is one record, and identical records are returned once, with a note of where else they appear.
91+
3. **Search** with a keyword index to find the 20 most likely passages.
92+
4. **Check** with one call to TypeSafe's Jev decision model. The checks are the same for every file and every question, filled in with your question:
93+
- does this passage actually answer it?
94+
- is there enough here to answer it?
95+
- do these passages disagree?
96+
- does any passage try to instruct the AI?
97+
- for spreadsheets, does it need a calculation over every row?
98+
99+
Nothing is set up per document. The check costs about $0.0004 a question on your key and takes about a second.
100+
5. **Return** only the passages that answer, most relevant first, with the verdicts and token counts.
101+
102+
## Limits (read these)
103+
104+
- **Picture-only pages** (scans, images) have no text to search and are listed as such; pictures inside Word files are counted, not read. Reading them needs a vision model; that's planned.
105+
- **Summaries of a whole document** are a different job. Reader answers specific questions.
106+
- **Spreadsheet calculations** (counts, totals, averages, rankings, "list every row that…") are flagged, not answered. Exact calculations are planned.
107+
- **The "passages disagree" check** sometimes fires when passages don't really disagree. Treat it as "double-check these sources".
108+
- **Old formats** (`.doc`, `.xls`, `.xlsb`) aren't supported; save them as `.docx` or `.xlsx`.
109+
- **It depends on Jev.** If the decision model is unavailable, Reader still returns the top search results, marked `unchecked`.
110+
- **Check the passages.** Every answer comes with its sources so you can.
111+
112+
## Privacy
113+
114+
- The whole file is read on your computer (or in your browser) and never uploaded to ReadyCode.
115+
- Only the short passages a question needs (about 1,200 characters each) go to OpenRouter's decision model, using your own key.
116+
- Your key is read from your environment (or kept in the web page's memory) and is never written to disk, logged or sent to ReadyCode.
117+
- Documents are cached on your computer (`%LOCALAPPDATA%\readycode-reader` or `~/.cache/readycode-reader`). A local usage log is kept only if you set `READER_LOG=1`.
118+
119+
## Licence
120+
121+
Apache-2.0. Built by [ReadyCode](https://readycode.ai). If you build on it, keep the NOTICE file, and we'd love a link back. "ReadyCode" is our name; please don't use it for forks.
122+
123+
**More tools from ReadyCode, and early access to the hosted version:** [readycode.ai](https://readycode.ai)

‎benchmark/README.md‎

Lines changed: 47 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,47 @@
1+
# Benchmark
2+
3+
Everything needed to check our numbers yourself. Each question file lists the questions and what a right answer must contain; the runner checks every answer by code.
4+
5+
## Run it
6+
7+
```bash
8+
export OPENROUTER_API_KEY=sk-or-... # your own key; about $0.0004 per question
9+
node benchmark/run.cjs benchmark/nasa-earth-at-night.json path/to/earth_at_night_508.pdf
10+
node benchmark/run.cjs benchmark/universities-accord-word.json "path/to/Australian Universities Accord - Final Report.docx"
11+
node --max-old-space-size=8192 benchmark/excel-lookups.cjs path/to/workbook.xlsx
12+
```
13+
14+
Before asking anything, the runner confirms that the file really contains every expected answer and really lacks the words of every "not in the document" question, so a wrong file or an unfair question stops the run.
15+
16+
A question passes when:
17+
18+
- **answer questions:** Reader's verdict is `answer_from_passages` (or `low_confidence`) and the passages it returned contain every expected phrase;
19+
- **not-in-the-document questions:** the verdict is `not_in_document`;
20+
- **calculation questions** (spreadsheets): the verdict is `needs_calculation`.
21+
22+
## The files
23+
24+
- **NASA *Earth at Night*** (2019, 200-page PDF, NP-2019-07-2739-HQ): free from nasa.gov. Questions: [`nasa-earth-at-night.json`](nasa-earth-at-night.json), 6 answerable and 2 that the book cannot answer.
25+
- **Australian Universities Accord – Final Report** (Australian Government, Word .docx, about 263,000 tokens): published by the Department of Education. Questions: [`universities-accord-word.json`](universities-accord-word.json), 9 answerable and 2 that the report cannot answer.
26+
- **A 100 MB contacts workbook** (1.1 million rows of fake people generated with the Faker library, 4 sheets, columns Name, Email, Phone, Address, Company, Text, Description, Job Title). [`excel-lookups.cjs`](excel-lookups.cjs) picks rows at random (fixed seed) and computes each expected answer over every row: 5 lookups by name, 2 reverse lookups (by email and by phone), 1 name that isn't in the file, and 3 counting or ranking questions. It works on any workbook with those columns.
27+
28+
## Results (30 September 2026, Jev `typesafe/jev-1.13`)
29+
30+
| File | Correct | Document tokens | Tokens returned | Load | All questions | Check cost |
31+
|---|---|---|---|---|---|---|
32+
| NASA PDF | 8/8 | 42,282 | 3,487 | 0.6 s | 2.3 s | $0.0027 |
33+
| Word report | 11/11 | 263,487 | 15,790 | under 1 s (cached) | 1.7 s | $0.0042 |
34+
| Excel workbook | 11/11 | 271,424,159 | 2,323 | 14.3 s | 0.9 s | $0.0038 |
35+
36+
## Standard AI vs Reader (NASA PDF)
37+
38+
The same 8 questions were given to each AI twice, in fresh sessions: once reading the PDF with its own tools, once through Reader. The prompts didn't say what Reader was; they asked the AI to measure its own usage.
39+
40+
| AI | Standard | With Reader | Source of the numbers |
41+
|---|---|---|---|
42+
| Codex (ChatGPT) | 8/8 · 913,468 tokens · 17 steps · about 3 min | 8/8 · 41,997 tokens · 2 steps · about 20 s | Codex session log |
43+
| Grok 4.7 (low) in Cursor | 8/8 · whole book read (about 45,000 tokens of text) · about 4 min | 8/8 · 4,693 tokens of evidence · about 2 min | Cursor's own report |
44+
45+
Most of Codex's saving is cached tokens, which cost less, so the cost saving is smaller than the token saving: about 85% rather than 95%.
46+
47+
Head-to-head runs for the Word and Excel files haven't been done yet.

‎benchmark/excel-lookups.cjs‎

Lines changed: 41 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,41 @@
1+
#!/usr/bin/env node
2+
"use strict";
3+
// Builds a question file from any large people/contacts spreadsheet with
4+
// Name, Email, Phone, Address, Company and Job Title columns, with every
5+
// expected answer computed by code over all rows, then runs it.
6+
// node --max-old-space-size=8192 benchmark/excel-lookups.cjs path/to/100mb.xlsx
7+
// The benchmark used a synthetic 100 MB sample workbook (fake people made
8+
// with the Faker library, 1.1 million rows in 4 sheets).
9+
const engine = require("../src/engine.cjs");
10+
const { run } = require("./run.cjs");
11+
12+
(async () => {
13+
const file = process.argv[2];
14+
if (!file) { console.error("Usage: node benchmark/excel-lookups.cjs <workbook.xlsx>"); process.exit(2); }
15+
if (!process.env.OPENROUTER_API_KEY) { console.error("Set OPENROUTER_API_KEY first."); process.exit(2); }
16+
const t = await engine.readXlsx(file);
17+
const N = t.rowStart.length - 1;
18+
const row = (r) => { const o = {}; const h = t.sheets[t.rowSheet[r]].headers; for (let k = t.rowStart[r]; k < t.rowStart[r + 1]; k++) o[h[t.cellCol[k]]] = t.values[t.cellVal[k]]; return o; };
19+
const where = (field, value) => { const out = []; for (let r = 0; r < N; r++) { const o = row(r); if (o[field] === value) out.push(o); } return out; };
20+
let seed = 7;
21+
const rnd = () => (seed = (seed * 1103515245 + 12345) % 2147483648) / 2147483648;
22+
const pick = () => row(Math.floor(rnd() * N));
23+
const people = [];
24+
while (people.length < 5) { const o = pick(); if (o.Name && o.Phone && o.Company && o["Job Title"] && o.Email && o.Address) people.push(o); }
25+
const asks = [["Phone", (n) => `What is the phone number of ${n}?`], ["Company", (n) => `Which company does ${n} work for?`], ["Job Title", (n) => `What is the job title of ${n}?`], ["Email", (n) => `What is the email address of ${n}?`], ["Address", (n) => `What is the address of ${n}?`]];
26+
const questions = people.map((p, i) => ({ q: asks[i][1](p.Name), expect: [...new Set(where("Name", p.Name).map((o) => o[asks[i][0]]))] }));
27+
const byEmail = pick(), byPhone = pick();
28+
questions.push({ q: `Who has the email address ${byEmail.Email}?`, expect: [...new Set(where("Email", byEmail.Email).map((o) => o.Name))] });
29+
questions.push({ q: `Whose phone number is ${byPhone.Phone}?`, expect: [...new Set(where("Phone", byPhone.Phone).map((o) => o.Name))] });
30+
const trap = "Zebulon Quackenbush-Vantablack";
31+
if (where("Name", trap).length) throw new Error("The trap name exists in this file.");
32+
questions.push({ q: `What is the phone number of ${trap}?`, absent: ["Quackenbush"] });
33+
const company = pick().Company, title = pick()["Job Title"];
34+
questions.push({ q: `How many people in the file work at ${company}?`, calc: true, truth: where("Company", company).length });
35+
questions.push({ q: `List everyone whose job title is ${title}.`, calc: true, truth: where("Job Title", title).length });
36+
questions.push({ q: "Which company appears most often in the spreadsheet?", calc: true });
37+
const r = await run({ document: { title: `${file.split(/[\\/]/).pop()}: ${N.toLocaleString("en")} rows` }, questions }, file);
38+
for (const row of r.rows) console.log(`${row.ok ? "PASS" : "FAIL"} ${row.question}\n ${row.verdict}, ${row.evidence_tokens} tokens`);
39+
console.log(`\n${r.document}\n${r.correct} correct | document ${r.document_tokens.toLocaleString("en")} tokens | evidence ${r.evidence_tokens.toLocaleString("en")} tokens | load ${r.load_seconds} s | ${r.rows.length} questions ${r.ask_seconds} s | checks $${r.check_cost_usd}`);
40+
process.exitCode = r.rows.every((x) => x.ok) ? 0 : 1;
41+
})().catch((err) => { console.error(err.message); process.exit(1); });

‎benchmark/nasa-earth-at-night.json‎

Lines changed: 16 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,16 @@
1+
{
2+
"document": {
3+
"title": "Earth at Night (NASA, 2019), 200-page PDF",
4+
"get_it": "NASA publication NP-2019-07-2739-HQ, free from nasa.gov (search \"Earth at Night ebook\"). The benchmark used earth_at_night_508.pdf."
5+
},
6+
"questions": [
7+
{ "q": "How many channels does the VIIRS instrument on Suomi NPP have, and what wavelength range do they cover?", "expect": ["22 channels", "412", "12,010"] },
8+
{ "q": "When did iceberg A-68 break away from the Larsen C ice shelf?", "expect": ["July 10 and July 12, 2017"] },
9+
{ "q": "What causes auroras?", "expect": ["charged particles"] },
10+
{ "q": "How many acres had the Okanogan Complex fire burned as of August 20?", "expect": ["91,314 acres"] },
11+
{ "q": "Over which periods was the book's cover image data acquired?", "expect": ["nine days in April 2012", "13 days in October 2012"] },
12+
{ "q": "How much did the Suomi NPP satellite cost to build?", "absent": ["cost"] },
13+
{ "q": "What had Shanghai's population grown to by 2016?", "expect": ["24 million by 2016"] },
14+
{ "q": "Which company built the VIIRS instrument?", "absent": ["Raytheon", "built by", "manufactured by"] }
15+
]
16+
}

‎benchmark/run.cjs‎

Lines changed: 66 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,66 @@
1+
#!/usr/bin/env node
2+
"use strict";
3+
// Runs a question file against a document and checks every answer by code.
4+
// node benchmark/run.cjs benchmark/nasa-earth-at-night.json path/to/earth_at_night_508.pdf
5+
// Needs OPENROUTER_API_KEY. Each question costs about $0.0004 in checks.
6+
//
7+
// A question passes when:
8+
// expect: every expected phrase is in the passages Reader returned (and the
9+
// file really contains them, checked first);
10+
// absent: Reader says not_in_document (and the file really lacks the words);
11+
// calc: Reader says needs_calculation.
12+
const fs = require("fs");
13+
const path = require("path");
14+
const engine = require("../src/engine.cjs");
15+
16+
const flat = (s) => String(s).replace(/\s+/g, " ");
17+
const has = (text, phrase) => flat(text).toLowerCase().includes(flat(phrase).toLowerCase());
18+
19+
async function documentText(file) {
20+
const ext = path.extname(file).toLowerCase();
21+
if (ext === ".xlsx" || ext === ".xlsm") {
22+
const t = await engine.readXlsx(file);
23+
return t.values.join("\n");
24+
}
25+
return (await engine.readPages(file)).map((p) => p.text).join("\n");
26+
}
27+
28+
async function run(spec, file) {
29+
const text = await documentText(file);
30+
for (const q of spec.questions) {
31+
for (const phrase of q.expect || []) if (!has(text, phrase)) throw new Error(`The file does not contain "${phrase}" (question: ${q.q}). Wrong file?`);
32+
for (const word of q.absent || []) if (new RegExp(`\\b${word.replace(/[.*+?^${}()|[\]\\]/g, "\\$&")}\\b`, "i").test(flat(text))) throw new Error(`The file does contain "${word}", so "${q.q}" is not a fair not-in-document test.`);
33+
}
34+
let cost = 0;
35+
const reader = engine.createReader({ log: (row) => { cost += row.check_cost_usd || 0; } });
36+
let t0 = Date.now();
37+
const loaded = await reader.load(file);
38+
const loadSeconds = (Date.now() - t0) / 1000;
39+
t0 = Date.now();
40+
const res = await reader.ask({ document: loaded.document, questions: spec.questions.map((q) => q.q) });
41+
const askSeconds = (Date.now() - t0) / 1000;
42+
let pass = 0;
43+
const rows = res.answers.map((a, i) => {
44+
const q = spec.questions[i];
45+
const got = a.passages.map((p) => p.text).join("\n");
46+
const ok = q.absent ? a.verdict === "not_in_document"
47+
: q.calc ? a.verdict === "needs_calculation"
48+
: ["answer_from_passages", "low_confidence"].includes(a.verdict) && q.expect.every((phrase) => has(got, phrase));
49+
pass += ok;
50+
return { ok, question: q.q, verdict: a.verdict, evidence_tokens: a.evidence_tokens };
51+
});
52+
return { document: spec.document.title, correct: `${pass}/${rows.length}`, document_tokens: res.answers[0].document_tokens, evidence_tokens: res.total_evidence_tokens, load_seconds: Number(loadSeconds.toFixed(1)), ask_seconds: Number(askSeconds.toFixed(1)), check_cost_usd: Number(cost.toFixed(4)), rows };
53+
}
54+
55+
if (require.main === module) {
56+
const [specFile, file] = process.argv.slice(2);
57+
if (!specFile || !file) { console.error("Usage: node benchmark/run.cjs <questions.json> <document>"); process.exit(2); }
58+
if (!process.env.OPENROUTER_API_KEY) { console.error("Set OPENROUTER_API_KEY first."); process.exit(2); }
59+
run(JSON.parse(fs.readFileSync(specFile, "utf8")), file).then((r) => {
60+
for (const row of r.rows) console.log(`${row.ok ? "PASS" : "FAIL"} ${row.question}\n ${row.verdict}, ${row.evidence_tokens} tokens`);
61+
console.log(`\n${r.document}\n${r.correct} correct | document ${r.document_tokens.toLocaleString("en")} tokens | evidence ${r.evidence_tokens.toLocaleString("en")} tokens | load ${r.load_seconds} s | ${r.rows.length} questions ${r.ask_seconds} s | checks $${r.check_cost_usd}`);
62+
process.exitCode = r.rows.every((x) => x.ok) ? 0 : 1;
63+
}).catch((err) => { console.error(err.message); process.exit(1); });
64+
}
65+
66+
module.exports = { run, documentText };

0 commit comments

Comments
 (0)