paratext

paratext

Everything around your terminal output that is not the output: hyperlinks, images, window title, clipboard, notifications and the bell — each with a static fallback for terminals that cannot do it. Covers the OSC half of ansi-escapes, terminal-link and term-img; a drop-in path is not claimed until the compat oracle grades one. Zero dependencies.

Paratext is the literary term for everything around a text that is not the text — the title, the cover, the margins, the notes. This package owns the terminal equivalent: OSC, the escape class (ESC ]) that addresses the terminal program rather than the character grid. Hyperlinks, inline images, the window title, the clipboard, desktop notifications, the working directory, and the bell.

It covers the OSC half of ansi-escapes, terminal-link and term-img; a drop-in path is not claimed until the compat oracle grades one. Every capability has a static projection, so a pipe or an agent gets Docs (https://x.dev), never raw escape bytes.

Zero dependencies. The intent and design live at .sdlc/intents/paratext/.

Nothing in this layer is detectable

No terminal answers "do you do OSC 1337". So every capability carries a static projection, and emit returns it whenever support is absent or unknown: an image becomes its caption, a notification a printed line, a hyperlink text (url). Emitting the bytes and hoping is what puts \u001B]1337;File=inline=1;… across the screen of anyone who piped your output to a file, and it is what every incumbent does.

import { emit, processRuntime } from 'paratext';

const runtime = processRuntime();
emit(runtime, 'link', { text: 'Docs', url: 'https://x.dev' });
// iTerm2:  \e]8;;https://x.dev\aDocs\e]8;;\a
// a pipe:  Docs (https://x.dev)

A capability is data

Terminals invent OSC codes faster than any package ships releases — Kitty's graphics protocol, WezTerm's user-vars, Ghostty's progress bar. So the surface is a registry of plain objects rather than a fixed list of functions, and the seven built-ins register through the same public call a third party makes.

import { register } from 'paratext';

register({
  name: 'kitty-image',
  osc: 'BEL',
  when: { tty: true, term: 'xterm-kitty' },
  encode: '\u001B_Ga=T,f=100;{base64}\u001B\\',
  fallback: '{caption}', // required — there is no opt-out
});

No functions, so a capability travels through JSON, is diffable, and can be validated without running its author's code. check(document) grades one against paratext/schema.json, the same file every package in the family ships. paratext/plugin is the host: a plugin's capabilities arrive under capabilities, and every other key — another layer's tokens, spinners, handlers — is ignored without complaint.

What "grades against the schema" means, exactly. check() and register() read the capability entry of that file and enforce required, type, oneOf, const, minLength, minimum, items, and additionalProperties: false, which between them is every keyword the capability shape writes. Each refusal names the path — capabilities.link.when.tty, not just the capability — and carries the family's error code. What it does not read is $ref, pattern, minItems, maxLength, enum, allOf, anyOf and not: none of them appears under a capability, so nothing is silently unchecked today, but a keyword added to the file tomorrow would be. Until 0.3 this was presence-checking only, and when: 'not an object' was therefore accepted — a string destructures to four empty clauses, so the support guess said yes and the sequence went into the pipe. That is now a refusal.

The ansi-escapes members it replaces

The root is call-compatible with ansi-escapes for the four OSC members of its surface, so the bytes are the incumbent's where the terminal understands them and the projection everywhere else:

import ansiEscapes, { link, image, setCwd, beep } from 'paratext';

Its CSI half is out of scope — the cursor, erasing, scroll regions, the alternate screen. That is the character grid, which flagstaff draws and closeout puts back; a second implementation here is the copy this family exists to avoid. Those names are still declared, as undefined, so a drop-in module loads rather than dying on an ESM named import — and TypeScript types them such that calling one is a compile error, not a surprise at run time.

Graded by the compat oracle against ansi-escapes@7.3.0's own suite. Three of its four cases assert CSI, so the ceiling on that row is 1 / 4: paratext scores it, and 25% there means complete rather than a quarter.

MIT © Ofri Peretz

Benchmarks

Every number here is produced by npm run bench and published at /docs/benchmarks.

Graded by the incumbent's own test suite:

suitepassing
ansi-escapes1 / 4
term-img12 / 18
terminal-link8 / 10

Weight, installed and tree-inclusive: 93,113 bytes against 30,912 for the incumbents it replaces — a ratio of 3.0122 (terminal-link, term-img measured but left out of the ceiling, so it is understated).

Where it sits

Plugins register under the capabilities key, against the one schema the whole family shares.

flagstaff builds on it, and it builds on nothing in this family.

On this page