Pagecraft

The AI layer

Bring your own key, or your own machine. There is no Pagecraft-operated inference.

@pagecraft/ai can summarise a document, answer a question about it with the page the answer came off, find personal data in it, describe its images, translate it, propose an outline, or suggest a filename.

It holds no credentials and knows no endpoint. Every request goes through a fetch you inject, to a provider the user configured, with a key handed over one call at a time out of a vault encrypted on their device. There is no Pagecraft-operated inference, proxy or relay, and nothing in the package could reach one.

Not on npm yet. Until it is, build it from source and depend on packages/ai by path:

git clone https://github.com/amanawasthi2025/pagecraft
cd pagecraft
pnpm install && pnpm build

The path that costs nothing

A model on the user’s own machine needs no key, costs nothing, and keeps the grounding text on the device with the document it came from. It is the default answer to “do I have to pay for any of this?”, and it is not a degraded mode — it is the same code path as every other provider.

import { ask } from '@pagecraft/ai';

const answer = await ask(
  { kind: 'ollama', baseUrl: 'http://localhost:11434', model: 'llama3.2' },
  { turns: [{ role: 'user', text: 'Summarise this.' }] },
  { fetch: window.fetch.bind(window) },
);

Ollama has to be told the page may call it — start it with OLLAMA_ORIGINS set to the site’s address. LM Studio has the same setting in its developer tab, and speaks the OpenAI protocol at http://localhost:1234/v1.

A provider you pay for

Six kinds: anthropic, openai, google, openai-compatible, ollama, lm-studio. providerKinds describes each one — whether it needs a key, what address to prefill, what has to be allowed at the other end.

import { checkProvider, openVault, indexedDbStore } from '@pagecraft/ai';

const provider = {
  kind: 'anthropic',
  baseUrl: 'https://api.anthropic.com',
  model: 'claude-opus-5',
} as const;

// Test a key before saving it. It answers rather than throws: half the time the
// answer is no, and that is the point of asking.
const check = await checkProvider(provider, { fetch, key });
if (!check.ok) console.error(check.error.message, check.error.remedy);

const vault = await openVault(indexedDbStore());
await vault.create(passphrase); // or vault.unlock(passphrase) after the first time
await vault.put('anthropic', key);

The vault writes ciphertext and nothing else. Its store is injectable for the same reason fetch is, and “only ciphertext is written down” is asserted in the test suite by reading that store — because what is written down is the claim.

Grounding: what will be sent, before it is sent

An AI tool works on passages taken out of the document. Those passages are the one thing that leaves the device, so the rule is that the user sees them first — in full, byte for byte, as the string that will actually be sent.

import { ground, openSession } from '@pagecraft/ai';

const session = openSession({
  provider,
  fetch,
  // A function, not a key: the session holds no credential of its own, so
  // locking the vault stops a sitting that is already under way.
  key: () => vault.get('anthropic'),
  grounding: ground(passages),
});

show(session.grounding.text); // in full
show(session.estimate(question)); // tokens, and money where a price is known
session.disclose(); // once it is on the screen, not before

const answer = await session.ask(question, { onText: (piece) => append(piece) });
console.log(session.spent); // tokens and cost, and whether either was counted or estimated

session.ask before session.disclose throws. That is the whole mechanism: a surface cannot send a document’s contents to a third party without having put those contents in front of the person whose document it is.

Prices carry the date they were read, so an estimate can say how old its figures are rather than quoting a number as though it were current.

The tools

Each AI tool is two halves rather than one function, and the split is where the consent lives (ADR-0034). prepare reads the documents on the device and works out the grounding — the exact string, and the exact pictures, a request would carry. The host shows that and calls disclose(). run asks, and cannot ask before the host has, because the session it is given refuses.

import { detectPii } from '@pagecraft/ai';

const prepared = await detectPii.prepare(documents, {}, { engine });
show(prepared.grounding.text);
session.disclose();

const found = await detectPii.run(prepared, session, { engine });
found.findings; // [{ text: 'Priya Nair', kind: 'name', page: 2 }, …]
found.unverified; // findings whose text is not in the document as written

Each returns something structured rather than prose to parse:

Tool What it gives back
summarize A summary, with citations naming the pages it came off
chat An answer, with the same citations
extractData The fields you named as JSON, and the ones the document did not carry
detectPii Personal data by kind and page, separated into what was verified and what was not
describeImages Alt text per picture, ready for alt-text
translate The text in another language, page by page
generateOutline Headings and pages, ready for bookmarks
autoName A filename the document deserves

Two are worth noticing. detectPii reports what it found and which of it it could verify is in the document as written, so a name the model invented is set aside rather than passed on. What survives feeds straight into redact, and describeImages feeds alt-text, both in @pagecraft/core and both on the device. The model suggests; the engine acts.

What this does not do

  • It does not run a model. It speaks to one you chose, at an address you gave it.
  • It does not keep a key. Keys live in the vault, encrypted with the user’s passphrase, and are lent per call.
  • It does not send a document. It sends passages, after showing them. Pictures go only to the tool that is about pictures.
  • It cannot reach an origin the build does not admit. In the browser the content security policy names the providers this build was configured with, and nothing else — so a compromised page could not exfiltrate to an address the policy has never heard of.
  • It does not promise the model is right. A summary is a summary. Anything that changes a document goes through an operation, on the device, where you can see what it did.

Where to go next