Back to home
Apisurf Logo
canon

canon

Docs, specs and notes written by an agent into one file, built into static pages for people.

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.

CommandForWhat it does
canonagentsCreate, read, search, change, move and delete content
canonuipeopleList 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:

LevelWhat it isRenders to
documentA title, a slug and an optional descriptionOne page
blockOne renderable chunk, ordered in the documentOne 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

CommandDoes
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

CommandDoes
canonui lsEvery 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.