What is canon?
canon is two commands over one SQLite file, split by who is holding
them. canon writes content and is built for agents: no editor, no server,
no account — every command is a one-shot read or write that exits when it is done.
canonui only reads: it opens the same file and renders a document into a
static page.
| Command | For | What it does |
canon | agents | Create, read, search, change, move and delete content |
canonui | people | List what is there, build it into a page, serve it |
Installation
bash
npm i -g @apisurf/canon # the canon command — content
npm i -g @apisurf/canonui # the canonui command — pages
Both are published to GitHub Packages, so installing them needs a line in your
~/.npmrc:
~/.npmrc
@apisurf:registry=https://npm.pkg.github.com
//npm.pkg.github.com/:_authToken=<a PAT with read:packages>
They are separate packages so the CLI an agent drives does not drag a static site generator along. Both
print the schema version they expect with --version, which is how to tell
when they have drifted.
Five minutes
bash
canon new document "Retry Guide"
canon new block --document retry-guide --file intro.md
canon new block --document retry-guide --title "Budgets" --file budgets.md
canon ls blocks --document retry-guide
canonui serve retry-guide
The model
Two levels, the document owning its blocks:
| Level | What it is | Renders to |
| document | A title, a slug and an optional description | One page |
| block | One renderable chunk, ordered in the document | One section of that page |
A document is one piece of writing: a guide, a spec, a decision. When a part of it would be looked up or
linked to on its own, it is its own document. Documents have no order among themselves; blocks sit in an
order, and that order is the page. Everything a document holds is built — there is no draft state.
A block is markdown (the default), text,
html, img or
mermaid. Every type keeps its body in the block's
content; anything type-specific, like an image's alt text or a caption,
goes in --attr key=value. An optional
--title draws a heading and puts the block in the page's contents rail.
A document is referenced by id or slug, a block by id only.
Commands
| Command | Does |
canon new <noun> ... | Create a document or a block |
canon ls <plural> | List records, filtered and bounded |
canon get <noun> <ref> | Fetch records by id or slug |
canon find "<text>" | Full-text search across every block |
canon cat <ref...> | Documents as one markdown document |
canon set <noun> <ref> | Change one record |
canon rm <noun> <ref> | Delete records. A document with blocks needs --force |
canon mv block <id> | Reorder a block, or move it to another document |
canon howto [name] | Worked examples of whole tasks |
canon help [command] | One screen per command. Topics: documents, blocks, refs |
And on the other side
| Command | Does |
canonui ls | Every document, and the slug to build it by |
canonui build [document] | Render a document into static HTML |
canonui serve [document] | Build it and serve it over HTTP |
canonui open [dir] | Serve a folder that is already built |
canonui howto [name] | Worked examples: preview, publish, snapshot |
canonui help [command] | One screen per command. Topics: sites, snapshot |
Typing one at the other is answered rather than refused: canon build
prints the canonui line to run instead. Start at
canon help, or canon howto for whole
tasks worked end to end.
Where the data lives
One global file, ~/.apisurf/canon/canon.sqlite — not per
directory, so a document written in one shell is listable from any other. Point it elsewhere with
--db <path> on any command, or
$CANON_DB for a whole session.
bash
CANON_DB=./docs.sqlite canon ls documents # a database scoped to one repo
canon ls documents --db ~/work/handbook.sqlite
canon rm document --all --force # empty the database
Reading and writing
get and ls project every read to a
field list, and no default list contains a block's content. Listing thirty blocks gives you a table of
contents, not thirty documents.
bash
canon ls documents --since 7d
canon ls documents --sort updated_at --desc # what was worked on lately
canon ls blocks --document retry-guide --fields id,seq,type,title,size
canon get block 12 --fields content --max-field-bytes 0 # now give me the text
A block's body comes from exactly one of --content,
--file, or a bare - for stdin. New
blocks land last unless --at <n> says otherwise.
set changes what a block says;
mv changes where it lives.
bash
canon new block --document retry-guide --file ./intro.md --at 1
cat notes.md | canon new block --document retry-guide -
canon new block --document retry-guide --type img --content ./flow.png \
--attr alt="request flow" --attr caption="The happy path"
canon set block 12 --file ./intro.md
canon mv block 12 --to 1
canon mv block 12 --document overview
find searches block content and titles, best match first, with the text
around each hit. FTS5 syntax: a b for both,
OR, NOT,
"a phrase", pre*.
bash
canon find "idempotency"
canon find "retry budget" --document retry-guide
canon find "backoff OR jitter"
canon find "idempot*" --json
Handing content to a model
canon cat hands over the content itself as one markdown document: YAML
front matter, then the text, with the document as the h1, block titles as h2 and a block's own headings
beneath them. --toc prepends the titled blocks;
--no-meta drops the front matter. Its output is unbounded, and the size
goes to stderr, so redirecting stdout gives you a clean file.
bash
canon cat retry-guide # one document
canon cat retry-guide overview # several, separated by a rule
canon cat retry-guide --toc --no-meta > retry-guide.md
Building the site
One document builds into one page: its blocks top to bottom, with a contents rail drawn from their titles.
build and serve pick the document
themselves when there is only one, and print the list when there are several.
bash
canonui ls # what is buildable
canonui build retry-guide # -> ./dist/retry-guide
canonui build retry-guide --base /docs --site https://example.com
canonui serve retry-guide --port 4000 # build, then serve
canonui open ./public # serve what is already there
build writes to ./dist/<slug>.
serve builds into
~/.apisurf/canon/builds/<slug>, beside the database, then serves it
on 127.0.0.1:3000 (or the next free port up to 3020) and opens the browser; there is no watcher, so rerun it
after a change. Either takes --out <dir>.
The output is static: index.html, JavaScript only where a block needs it
(mermaid is bundled, no CDN), and index.md — the
canon cat markdown without front matter, which the page's copy button puts
on the clipboard.
The build reduces the document to a JSON snapshot and hands it to an Astro theme.
--snapshot-only --snapshot <path> writes it and stops, the hand-off
point for any other renderer. The database is opened read-only, so rendering can never change what it renders.
For agents
Records come as --json. Reads emit an
array of objects, writes emit the record they touched. Prose goes to stderr, data to stdout, so a pipe never
has to be filtered.
Output is bounded by default. ls and find stop at 20 rows and single
fields are clipped at 4 KB. --limit 0 (on
ls) and --max-field-bytes 0 lift them
once the caller wants the whole thing.
The incomplete command is a question, not an error. canon get prints the entity index;
canon get document prints that entity's fields, filters and an example.
Both exit 0, because a caller that sees a non-zero exit discards the output — and that output is exactly
the text meant to teach it the next command.