> built 2026-10-04 09:49 UTC from 537becd (main) · annals 0.0.3. Details: build_info.json

# index.html.md

<!-- generated by epythet -->

# annals

Annals for AI agents. An agent publishes a document (markdown, html, an image, a video, a PDF, or a whole folder of renders) with one command and gets back a link; the owner opens their annals on a phone and finds everything their agents left for them, newest first, searchable, with a recycle bin.

```bash
pip install annals
annals publish report.md --session my-agent
# https://apps.example.com/annals/d/20261003-203301-quarterly-report-3f9a
```

Documents are plain files in a directory. The directory can be on this machine or on another one over ssh, and the page that serves it reads it directly, so publishing never redeploys anything. No database, no build step, no API token.

## For agents

Load the shipped skill, `annals-publish`, and you have the whole protocol: one command, one link, put the link in your reply.

```bash
annals publish report.md                          # one document, prints its link
annals publish a.md b.md c.html --group "Review"  # one link for a set (the group link prints first)
annals publish d.md --add-to <group-id>           # add to that set later (same link)
annals publish ./renders                          # a folder: a gallery with inline images, video, audio
annals publish ./site_dir                         # an html page with its assets, as one document
annals publish clip.mp4                           # media plays inline (streamed, seekable)
some-command | annals publish - --title "Log"     # from stdin
annals ls "search words"                          # what is there, newest first
annals trash <id>  /  annals restore <id>         # the recycle bin
```

What each kind looks like on the page:

| Published                                      | Shown as                                                                                                                 |
|------------------------------------------------|--------------------------------------------------------------------------------------------------------------------------|
| a `.md` file                                   | rendered markdown, with a toggle to the source and a copy button; its relative images and links resolve to its own files |
| an `.html` file, or a folder with `index.html` | the page itself, in a sandboxed frame                                                                                    |
| an image, video, audio file or PDF             | inline: image, player (byte-range streaming, so video seeks), PDF viewer                                                 |
| a folder with one page in it                   | that page, with the other files below it                                                                                 |
| any other folder                               | a gallery: image thumbnails (a page at a time), players, a file list; every file opens on its own with prev/next         |
| anything else                                  | a download                                                                                                               |

A folder skips hidden files and caches (`.*`, `__pycache__`, `*.pyc`); `--all-files` keeps them.

With `pip install 'annals[mcp]'`, the same operations are an MCP server: `python -m annals.mcp`.

## Where documents go

```bash
annals configure --target tw:/root/.local/share/annals --base-url https://apps.example.com/annals
```

That writes `~/.config/annals/config.toml`. `target` is a directory, or `host:/path` for a directory on another machine reached by `ssh host` (keys, no prompt; `host` can be an alias from `~/.ssh/config`). `ANNALS_TARGET` and `ANNALS_BASE_URL` override it per shell. With no configuration, documents go to `~/.local/share/annals` and the link points at a local `annals serve`.

## Hosting the page

**Inside an app platform** (the usual case). Mount the API under the platform’s prefix and let the platform’s login gate it:

```python
# server.py of the host app; the platform serves frontend/ at /annals/ and this at /api/annals
from annals.api import mk_api
app = mk_api(data_dir="/root/.local/share/annals")
```

and write the page shell once as the app’s frontend:

```bash
annals page-shell --api /api/annals --base /annals > frontend/index.html
```

The shell only names those two paths; the page’s script and style load from the API, so upgrading the package upgrades the page. Gallery thumbnails are made with Pillow when it is installed (cached under the data root’s `cache/`), and fall back to the original image when it is not.

**Standalone**, for a machine with no platform:

```bash
pip install 'annals[server]'
ANNALS_DATA_DIR=~/.local/share/annals annals serve --host 127.0.0.1 --port 8765
```

The page is at `http://127.0.0.1:8765/annals/`, its API under `/annals/api/`. Auth is chosen by environment variables:

| Situation                                                 | Set                                                                                                   | Effect                                                                                                                        |
|-----------------------------------------------------------|-------------------------------------------------------------------------------------------------------|-------------------------------------------------------------------------------------------------------------------------------|
| Behind an existing login that exposes a who-am-I endpoint | `ANNALS_WHOAMI_URL`, `ANNALS_ALLOWED_USERS=me@example.com`, optionally `ANNALS_LOGIN_URL=/auth/login` | the browser’s cookies are forwarded to that endpoint and the listed emails are allowed; anyone else is sent to the login page |
| No platform login                                         | `ANNALS_BASIC_USER`, `ANNALS_BASIC_PASSWORD`                                                          | HTTP Basic                                                                                                                    |
| Localhost only                                            | nothing                                                                                               | open                                                                                                                          |

## Python API

```python
from annals import DocStore, publish
meta = DocStore("~/.local/share/annals").publish("report.md", tags=["q3"])
publish(["report.md"], tags="q3")["url"]          # same, through the configured target
```

`annals.api.mk_api(...)` is the mountable API; `annals.api.mk_app(data_dir=..., authorizer=...)` the standalone app.

## Design notes

Flat store with tags and groups, not a hierarchy: agents from many projects do not share one, and every placement decision costs tokens. A document is `docs/<id>/meta.json` plus its files; the bin is `trash/<id>/`; a group is `groups/<gid>.json`. Ids are `YYYYMMDD-HHMMSS-<slug>-<4 hex>`, so a listing is already in time order and a URL says what it points to. The transport is a seam (`annals.target`): local directory, ssh, and later an http ingest endpoint. More in `misc/docs/design.md`.

## Skills

```bash
gh skill install thorwhalen/annals annals-publish
```

The skill also ships inside the package at `annals/data/skills/annals-publish/`.

<p class="epythet-aggregates">This documentation as a single file: <a href="annals.md">annals.md</a> (Markdown, for agents).</p>


# _autosummary/annals.auth.html.md

# annals.auth

The auth seam: who may read the annals.

A annals is private by construction, so the server asks every request who is calling. The
answer comes from one of three `Authorizer` callables, chosen by [`authorizer_from_env()`](_autosummary/annals.auth.html.md#annals.auth.authorizer_from_env)
(or passed to `annals.api.mk_app()`):

* [`no_auth()`](_autosummary/annals.auth.html.md#annals.auth.no_auth): everyone is the owner. Right for `annals serve` bound to `127.0.0.1`,
  wrong anywhere else.
* [`CookieWhoami`](_autosummary/annals.auth.html.md#annals.auth.CookieWhoami): forward the request’s cookies to an identity endpoint that answers
  `{"email": ...}` (enlace_auth’s `/auth/whoami`), and allow the listed emails. This is
  how a annals sits behind an existing login without owning passwords: the login page, the
  session cookie and the logout belong to the platform; the annals only checks the allowlist.
  Env: `ANNALS_WHOAMI_URL`, `ANNALS_ALLOWED_USERS` (comma separated), `ANNALS_LOGIN_URL`.
* [`BasicAuth`](_autosummary/annals.auth.html.md#annals.auth.BasicAuth): one username and password (HTTP Basic). For a annals with no platform
  login in front of it. Env: `ANNALS_BASIC_USER`, `ANNALS_BASIC_PASSWORD`.

An authorizer returns the caller’s identity (a string) or `None`. The API turns `None`
into a 303 to the login page for a browser GET, and a 401 for anything else, mirroring
what enlace_auth does so the two are indistinguishable from the phone.

### Functions

| [`authorizer_from_env`](_autosummary/annals.auth.html.md#annals.auth.authorizer_from_env)([env])   | Pick the authorizer the environment describes; see the module docstring.   |
|-------------------------------------------------------------------------------|----------------------------------------------------------------------------|
| [`no_auth`](_autosummary/annals.auth.html.md#annals.auth.no_auth)(request)             | Everyone is `owner`.                                                       |

### Classes

| [`BasicAuth`](_autosummary/annals.auth.html.md#annals.auth.BasicAuth)(username, password)                      | HTTP Basic with one username and password, compared in constant time.           |
|-----------------------------------------------------------------------------------------------------|---------------------------------------------------------------------------------|
| [`CookieWhoami`](_autosummary/annals.auth.html.md#annals.auth.CookieWhoami)(whoami_url, allowed_users, \*[, ...]) | Ask an identity endpoint who holds this request's cookies; allow listed emails. |
| [`RequestLike`](_autosummary/annals.auth.html.md#annals.auth.RequestLike)(\*args, \*\*kwargs)                    | The two things an authorizer reads from a request.                              |

### *class* annals.auth.BasicAuth(username, password)

Bases: [`object`](https://docs.python.org/3/builtins/functions.html#object)

HTTP Basic with one username and password, compared in constant time.

### *class* annals.auth.CookieWhoami(whoami_url, allowed_users, , login_url='/auth/login')

Bases: [`object`](https://docs.python.org/3/builtins/functions.html#object)

Ask an identity endpoint who holds this request’s cookies; allow listed emails.

### *class* annals.auth.RequestLike(\*args, \*\*kwargs)

Bases: [`Protocol`](https://docs.python.org/3/library/typing.html#typing.Protocol)

The two things an authorizer reads from a request.

### annals.auth.authorizer_from_env(env=None)

Pick the authorizer the environment describes; see the module docstring.

* **Return type:**
  [`Callable`](https://docs.python.org/3/library/typing.html#typing.Callable)[[[`RequestLike`](_autosummary/annals.auth.html.md#annals.auth.RequestLike)], [`Optional`](https://docs.python.org/3/library/typing.html#typing.Optional)[[`str`](https://docs.python.org/3/builtins/stdtypes.html#str)]]

### annals.auth.no_auth(request)

Everyone is `owner`. Only for a server that listens on localhost.

* **Return type:**
  [`Optional`](https://docs.python.org/3/library/typing.html#typing.Optional)[[`str`](https://docs.python.org/3/builtins/stdtypes.html#str)]


# _autosummary/annals.config.html.md

# annals.config

Settings: where documents are published and what link to print.

Two values matter to a publisher:

* `target`: where the documents go. A directory (`~/.local/share/annals`) or an ssh
  destination (`tw:/root/.local/share/annals`). This is the data root the annals server
  reads, locally or on another machine.
* `base_url`: the public root of the annals app, so a publish can print the link the owner
  opens (`https://apps.example.com/annals`).

Resolution order, highest first: explicit keyword arguments, environment variables
(`ANNALS_TARGET`, `ANNALS_BASE_URL`), the config file (`$ANNALS_CONFIG` or
`$XDG_CONFIG_HOME/annals/config.toml`, default `~/.config/annals/config.toml`), then the
local defaults (publish into the local data dir, link to a local `annals serve`).

The server side has one knob of its own, `ANNALS_DATA_DIR`: the directory the API reads.
It defaults to `~/.local/share/annals`, which is also the default publish target, so with
no configuration at all `annals publish` and `annals serve` meet in the same place.

### Functions

| [`default_config_path`](_autosummary/annals.config.html.md#annals.config.default_config_path)()                              | `$ANNALS_CONFIG`, else `$XDG_CONFIG_HOME/annals/config.toml`.                     |
|-----------------------------------------------------------------------------------------------------|-----------------------------------------------------------------------------------|
| [`default_data_dir`](_autosummary/annals.config.html.md#annals.config.default_data_dir)()                                 | The local data root: `$ANNALS_DATA_DIR` or `~/.local/share/annals`.               |
| [`load_settings`](_autosummary/annals.config.html.md#annals.config.load_settings)(\*[, target, base_url, config_path]) | Resolve settings: arguments, then env, then the config file, then local defaults. |
| [`write_config`](_autosummary/annals.config.html.md#annals.config.write_config)(settings, \*[, config_path])          | Write the config file (two string keys; no TOML writer dependency needed).        |

### Classes

| [`Settings`](_autosummary/annals.config.html.md#annals.config.Settings)(target, base_url[, configured])   | Where to publish and what link to print.   |
|---------------------------------------------------------------------------------------------|--------------------------------------------|

### *class* annals.config.Settings(target, base_url, configured=True)

Bases: [`object`](https://docs.python.org/3/builtins/functions.html#object)

Where to publish and what link to print. See the module docstring for precedence.

#### as_dict()

Plain dict, for the CLI and MCP surfaces.

* **Return type:**
  [`dict`](https://docs.python.org/3/builtins/stdtypes.html#dict)

#### configured *: [bool](https://docs.python.org/3/builtins/functions.html#bool)* *= True*

publishing then lands
in the local data dir and the link points at a local server, which is rarely meant

* **Type:**
  False when nothing (argument, env, config file) named a target

### annals.config.default_config_path()

`$ANNALS_CONFIG`, else `$XDG_CONFIG_HOME/annals/config.toml`.

* **Return type:**
  [`Path`](https://docs.python.org/3/library/pathlib.html#pathlib.Path)

### annals.config.default_data_dir()

The local data root: `$ANNALS_DATA_DIR` or `~/.local/share/annals`.

* **Return type:**
  [`Path`](https://docs.python.org/3/library/pathlib.html#pathlib.Path)

### annals.config.load_settings(, target=None, base_url=None, config_path=None)

Resolve settings: arguments, then env, then the config file, then local defaults.

* **Return type:**
  [`Settings`](_autosummary/annals.config.html.md#annals.config.Settings)

### annals.config.write_config(settings, , config_path=None)

Write the config file (two string keys; no TOML writer dependency needed).

* **Return type:**
  [`Path`](https://docs.python.org/3/library/pathlib.html#pathlib.Path)


# _autosummary/annals.html.md

# annals

annals: an in-tray for AI agents. Publish documents and media to a private page; get a link.

Agents call [`annals.tools.publish()`](_autosummary/annals.tools.html.md#annals.tools.publish) (or `annals publish file.md` from a shell) and hand
the printed URL to their human, who opens it on a phone. The documents are plain files in a
directory, local or on another machine over ssh; `annals.api.mk_app()` serves that
directory as a private page with search, sort, groups and a recycle bin.

### Functions

| [`configure`](_autosummary/annals.html.md#annals.configure)(\*[, target, base_url])                  | Write the publisher config (where to publish, what link to print) and show it.                                                                                    |
|-----------------------------------------------------------------------------------------------------|-------------------------------------------------------------------------------------------------------------------------------------------------------------------|
| [`group`](_autosummary/annals.html.md#annals.group)(title, doc_ids, \*[, target, base_url])      | Make a group (one URL for a set of documents) from existing document ids.                                                                                         |
| [`groups`](_autosummary/annals.html.md#annals.groups)(\*[, target, base_url])                     | List groups, newest first.                                                                                                                                        |
| [`load_settings`](_autosummary/annals.html.md#annals.load_settings)(\*[, target, base_url, config_path]) | Resolve settings: arguments, then env, then the config file, then local defaults.                                                                                 |
| [`ls`](_autosummary/annals.html.md#annals.ls)([q, trash, limit, target, base_url])            | List documents (newest first), optionally filtered by words or showing the bin.                                                                                   |
| [`parse_target`](_autosummary/annals.html.md#annals.parse_target)(spec)                                 | `host:/path` becomes an [`SshTarget`](_autosummary/annals.html.md#annals.SshTarget); anything else a [`LocalTarget`](_autosummary/annals.html.md#annals.LocalTarget). |
| [`publish`](_autosummary/annals.html.md#annals.publish)(paths, \*[, title, tags, group, ...])      | Publish one document and print its link; several paths become several documents.                                                                                  |
| [`restore`](_autosummary/annals.html.md#annals.restore)(doc_ids, \*[, target])                     | Bring documents back from the recycle bin.                                                                                                                        |
| [`show`](_autosummary/annals.html.md#annals.show)(doc_id, \*[, target, base_url])               | A document's metadata and link.                                                                                                                                   |
| [`trash`](_autosummary/annals.html.md#annals.trash)(doc_ids, \*[, target])                       | Move documents to the recycle bin (restorable).                                                                                                                   |

### Classes

| [`DocStore`](_autosummary/annals.html.md#annals.DocStore)(target)                         | Publish, list, read, trash, restore and group documents on a `Target`.      |
|-------------------------------------------------------------------------------------------|-----------------------------------------------------------------------------|
| [`LocalTarget`](_autosummary/annals.html.md#annals.LocalTarget)(root)                        | A directory on this machine.                                                |
| [`Settings`](_autosummary/annals.html.md#annals.Settings)(target, base_url[, configured]) | Where to publish and what link to print.                                    |
| [`SshTarget`](_autosummary/annals.html.md#annals.SshTarget)(host, root)                    | `host:path` on another machine, via the system ssh and rsync in batch mode. |

### *class* annals.DocStore(target)

Bases: [`object`](https://docs.python.org/3/builtins/functions.html#object)

Publish, list, read, trash, restore and group documents on a `Target`.

#### add_to_group(gid, doc_ids)

Append ids to an existing group, keeping order and skipping duplicates.

* **Return type:**
  [`dict`](https://docs.python.org/3/builtins/stdtypes.html#dict)

#### group(gid)

One group, by id.

* **Return type:**
  [`dict`](https://docs.python.org/3/builtins/stdtypes.html#dict)

#### list(, trash=False)

Every document’s meta, newest first (ids sort by time; `created` breaks ties).

* **Return type:**
  [`list`](_autosummary/annals.html.md#annals.DocStore.list)[[`dict`](https://docs.python.org/3/builtins/stdtypes.html#dict)]

#### list_groups()

Every group, newest first.

* **Return type:**
  [`list`](_autosummary/annals.html.md#annals.DocStore.list)[[`dict`](https://docs.python.org/3/builtins/stdtypes.html#dict)]

#### make_group(title, doc_ids, , gid=None)

Create (or overwrite) a group: a titled, ordered list of document ids.

* **Return type:**
  [`dict`](https://docs.python.org/3/builtins/stdtypes.html#dict)

#### meta(doc_id)

One document’s meta; `in_trash` says which side of the bin it is on.

* **Return type:**
  [`dict`](https://docs.python.org/3/builtins/stdtypes.html#dict)

#### publish(\*sources, title=None, tags=(), source=None, text=None, filename='document.md', exclude=('.\*', '_\_pycache_\_', '\*.pyc', '\*.pyo'))

Publish files (or a directory, or `text`) as ONE document; return its meta.

One markdown or html file is the common case. A directory is published whole, with
`index.html` (or the first renderable file) as the page shown. `text` publishes a
string as `filename` instead of reading sources. `exclude` lists glob patterns
skipped inside a directory (hidden files and caches by default; `()` keeps all).

* **Return type:**
  [`dict`](https://docs.python.org/3/builtins/stdtypes.html#dict)

#### purge(doc_id)

Delete a document that is in the bin. Refuses one that is not.

* **Return type:**
  [`None`](https://docs.python.org/3/builtins/constants.html#None)

#### read_text(doc_id, rel=None)

A document’s main file (or one of its files) as text.

* **Return type:**
  [`str`](https://docs.python.org/3/builtins/stdtypes.html#str)

#### restore(doc_id)

Move a document out of the bin (idempotent).

* **Return type:**
  [`dict`](https://docs.python.org/3/builtins/stdtypes.html#dict)

#### search(query, , trash=False)

Case-insensitive match of every query word against title, tags, excerpt, source.

* **Return type:**
  [`list`](_autosummary/annals.html.md#annals.DocStore.list)[[`dict`](https://docs.python.org/3/builtins/stdtypes.html#dict)]

#### trash(doc_id)

Move a document into the bin (idempotent).

* **Return type:**
  [`dict`](https://docs.python.org/3/builtins/stdtypes.html#dict)

### *class* annals.LocalTarget(root)

Bases: [`object`](https://docs.python.org/3/builtins/functions.html#object)

A directory on this machine.

#### local_path(rel)

`rel` under the root, resolved; `None` if it escapes the root (a symlink).

* **Return type:**
  [`Path`](https://docs.python.org/3/library/pathlib.html#pathlib.Path) | [`None`](https://docs.python.org/3/builtins/constants.html#None)

### *class* annals.Settings(target, base_url, configured=True)

Bases: [`object`](https://docs.python.org/3/builtins/functions.html#object)

Where to publish and what link to print. See the module docstring for precedence.

#### as_dict()

Plain dict, for the CLI and MCP surfaces.

* **Return type:**
  [`dict`](https://docs.python.org/3/builtins/stdtypes.html#dict)

#### configured *: [bool](https://docs.python.org/3/builtins/functions.html#bool)* *= True*

publishing then lands
in the local data dir and the link points at a local server, which is rarely meant

* **Type:**
  False when nothing (argument, env, config file) named a target

### *class* annals.SshTarget(host, root)

Bases: [`object`](https://docs.python.org/3/builtins/functions.html#object)

`host:path` on another machine, via the system ssh and rsync in batch mode.

### annals.configure(, target=None, base_url=None)

Write the publisher config (where to publish, what link to print) and show it.

* **Return type:**
  [`dict`](https://docs.python.org/3/builtins/stdtypes.html#dict)

### Example

`annals configure --target tw:/root/.local/share/annals --base-url https://apps.example.com/annals`.
With no arguments, shows the resolved settings without writing.

### annals.group(title, doc_ids, , target=None, base_url=None)

Make a group (one URL for a set of documents) from existing document ids.

* **Return type:**
  [`dict`](https://docs.python.org/3/builtins/stdtypes.html#dict)

### annals.groups(, target=None, base_url=None)

List groups, newest first.

* **Return type:**
  [`dict`](https://docs.python.org/3/builtins/stdtypes.html#dict)

### annals.load_settings(, target=None, base_url=None, config_path=None)

Resolve settings: arguments, then env, then the config file, then local defaults.

* **Return type:**
  [`Settings`](_autosummary/annals.config.html.md#annals.config.Settings)

### annals.ls(q='', , trash=False, limit=50, target=None, base_url=None)

List documents (newest first), optionally filtered by words or showing the bin.

* **Return type:**
  [`dict`](https://docs.python.org/3/builtins/stdtypes.html#dict)

### annals.parse_target(spec)

`host:/path` becomes an [`SshTarget`](_autosummary/annals.html.md#annals.SshTarget); anything else a [`LocalTarget`](_autosummary/annals.html.md#annals.LocalTarget).

* **Return type:**
  [`Target`](_autosummary/annals.target.html.md#annals.target.Target)

### annals.publish(paths, , title=None, tags='', group=None, add_to=None, session=None, all_files=False, target=None, base_url=None)

Publish one document and print its link; several paths become several documents.

`paths`: files of any kind (markdown, html, images, video, audio, pdf, text), a
directory (one document: an `index.html` or single page with its assets, else a
gallery of everything in it), or `-` for stdin (markdown). `tags` is comma separated. `group`
names a group to create from the published documents; the reply then carries
`group_url` too. `add_to` appends them to an existing group instead (its id, as
printed in a group link), so a second batch lands under the link the owner already has. `session` records who published (the source shown on the page). A directory skips
hidden files and caches (`.*`, `__pycache__`, `*.pyc`) unless `all_files`.

* **Return type:**
  [`dict`](https://docs.python.org/3/builtins/stdtypes.html#dict)

### annals.restore(doc_ids, , target=None)

Bring documents back from the recycle bin.

* **Return type:**
  [`dict`](https://docs.python.org/3/builtins/stdtypes.html#dict)

### annals.show(doc_id, , target=None, base_url=None)

A document’s metadata and link.

* **Return type:**
  [`dict`](https://docs.python.org/3/builtins/stdtypes.html#dict)

### annals.trash(doc_ids, , target=None)

Move documents to the recycle bin (restorable).

* **Return type:**
  [`dict`](https://docs.python.org/3/builtins/stdtypes.html#dict)

### Modules

| [`auth`](_autosummary/annals.auth.html.md#module-annals.auth)     | The auth seam: who may read the annals.                                                                                                            |
|------------------------------------------------------------------------------|----------------------------------------------------------------------------------------------------------------------------------------------------|
| [`config`](_autosummary/annals.config.html.md#module-annals.config) | Settings: where documents are published and what link to print.                                                                                    |
| [`mcp`](_autosummary/annals.mcp.html.md#module-annals.mcp)       | MCP surface: the same operations as the CLI, from [`annals.tools`](_autosummary/annals.tools.html.md#module-annals.tools), via `py2mcp`. |
| [`store`](_autosummary/annals.store.html.md#module-annals.store)   | The document store: a flat set of documents, tags, groups and a recycle bin, as plain files.                                                       |
| [`target`](_autosummary/annals.target.html.md#module-annals.target) | The transport seam: a document tree lands in a directory, here or on another machine.                                                              |
| [`tools`](_autosummary/annals.tools.html.md#module-annals.tools)   | The operations, as plain functions: JSON-able arguments in, JSON-able dicts out.                                                                   |


# _autosummary/annals.mcp.html.md

# annals.mcp

MCP surface: the same operations as the CLI, from [`annals.tools`](_autosummary/annals.tools.html.md#module-annals.tools), via `py2mcp`.

Run with `python -m annals.mcp` (needs `pip install 'annals[mcp]'`). String refs keep the
core free of any MCP import; the function list is the one the CLI dispatches.

### Functions

| [`mk_server`](_autosummary/annals.mcp.html.md#annals.mcp.mk_server)()   | Build the MCP server over [`annals.tools`](_autosummary/annals.tools.html.md#module-annals.tools).   |
|----------------------------------------------------------------|----------------------------------------------------------------------------------------------------------------|

### annals.mcp.mk_server()

Build the MCP server over [`annals.tools`](_autosummary/annals.tools.html.md#module-annals.tools).


# _autosummary/annals.store.html.md

# annals.store

The document store: a flat set of documents, tags, groups and a recycle bin, as plain files.

Layout under the data root (local or remote, see [`annals.target`](_autosummary/annals.target.html.md#module-annals.target)):

```default
docs/<id>/meta.json         one document: metadata
docs/<id>/<main file>       its content (index.html, report.md, ...), plus any assets
trash/<id>/...              the same shape; the recycle bin is a move
groups/<gid>.json           a named, ordered list of document ids with its own URL
```

Flat on purpose: agents from many corpora do not share a hierarchy, and every placement
decision costs tokens. Tags and groups carry the structure. Ids are time-sortable and
readable (`20261003-203301-starwars-theme-report-3f9a`), so a listing is already in
publication order and a URL says what it points to.

Plain files, written whole, because the same tree is written by rsync from another machine
and read by the server; there is no index to keep consistent.

### Module Attributes

| [`DFLT_EXCLUDE`](_autosummary/annals.store.html.md#annals.store.DFLT_EXCLUDE)   | hidden files, caches, compiled python   |
|-----------------------------------------------------------------|-----------------------------------------|

### Functions

| [`check_id`](_autosummary/annals.store.html.md#annals.store.check_id)(doc_id)             | Raise `ValueError` on an id that could not have come from [`mk_id()`](_autosummary/annals.store.html.md#annals.store.mk_id).   |
|-------------------------------------------------------------------------------|-----------------------------------------------------------------------------------------------------------------------|
| [`kind_of`](_autosummary/annals.store.html.md#annals.store.kind_of)(filename)            | The rendering kind of a file, from its extension.                                                                     |
| [`mk_id`](_autosummary/annals.store.html.md#annals.store.mk_id)(title, \*[, when])     | `YYYYMMDD-HHMMSS-<slug>-<4 hex>`: sortable by time, readable in a URL.                                                |
| [`now_iso`](_autosummary/annals.store.html.md#annals.store.now_iso)()                    | UTC timestamp with second precision, the format every meta.json uses.                                                 |
| [`slugify`](_autosummary/annals.store.html.md#annals.store.slugify)(text, \*[, max_len]) | Lowercase ascii words joined by hyphens; empty input becomes `doc`.                                                   |

### Classes

| [`DocStore`](_autosummary/annals.store.html.md#annals.store.DocStore)(target)   | Publish, list, read, trash, restore and group documents on a `Target`.   |
|---------------------------------------------------------------------|--------------------------------------------------------------------------|

### annals.store.DFLT_EXCLUDE *= ('.\*', '_\_pycache_\_', '\*.pyc', '\*.pyo')*

hidden files, caches, compiled python

* **Type:**
  names never published from a directory

### *class* annals.store.DocStore(target)

Bases: [`object`](https://docs.python.org/3/builtins/functions.html#object)

Publish, list, read, trash, restore and group documents on a `Target`.

#### add_to_group(gid, doc_ids)

Append ids to an existing group, keeping order and skipping duplicates.

* **Return type:**
  [`dict`](https://docs.python.org/3/builtins/stdtypes.html#dict)

#### group(gid)

One group, by id.

* **Return type:**
  [`dict`](https://docs.python.org/3/builtins/stdtypes.html#dict)

#### list(, trash=False)

Every document’s meta, newest first (ids sort by time; `created` breaks ties).

* **Return type:**
  [`list`](_autosummary/annals.store.html.md#annals.store.DocStore.list)[[`dict`](https://docs.python.org/3/builtins/stdtypes.html#dict)]

#### list_groups()

Every group, newest first.

* **Return type:**
  [`list`](_autosummary/annals.store.html.md#annals.store.DocStore.list)[[`dict`](https://docs.python.org/3/builtins/stdtypes.html#dict)]

#### make_group(title, doc_ids, , gid=None)

Create (or overwrite) a group: a titled, ordered list of document ids.

* **Return type:**
  [`dict`](https://docs.python.org/3/builtins/stdtypes.html#dict)

#### meta(doc_id)

One document’s meta; `in_trash` says which side of the bin it is on.

* **Return type:**
  [`dict`](https://docs.python.org/3/builtins/stdtypes.html#dict)

#### publish(\*sources, title=None, tags=(), source=None, text=None, filename='document.md', exclude=('.\*', '_\_pycache_\_', '\*.pyc', '\*.pyo'))

Publish files (or a directory, or `text`) as ONE document; return its meta.

One markdown or html file is the common case. A directory is published whole, with
`index.html` (or the first renderable file) as the page shown. `text` publishes a
string as `filename` instead of reading sources. `exclude` lists glob patterns
skipped inside a directory (hidden files and caches by default; `()` keeps all).

* **Return type:**
  [`dict`](https://docs.python.org/3/builtins/stdtypes.html#dict)

#### purge(doc_id)

Delete a document that is in the bin. Refuses one that is not.

* **Return type:**
  [`None`](https://docs.python.org/3/builtins/constants.html#None)

#### read_text(doc_id, rel=None)

A document’s main file (or one of its files) as text.

* **Return type:**
  [`str`](https://docs.python.org/3/builtins/stdtypes.html#str)

#### restore(doc_id)

Move a document out of the bin (idempotent).

* **Return type:**
  [`dict`](https://docs.python.org/3/builtins/stdtypes.html#dict)

#### search(query, , trash=False)

Case-insensitive match of every query word against title, tags, excerpt, source.

* **Return type:**
  [`list`](_autosummary/annals.store.html.md#annals.store.DocStore.list)[[`dict`](https://docs.python.org/3/builtins/stdtypes.html#dict)]

#### trash(doc_id)

Move a document into the bin (idempotent).

* **Return type:**
  [`dict`](https://docs.python.org/3/builtins/stdtypes.html#dict)

### annals.store.check_id(doc_id)

Raise `ValueError` on an id that could not have come from [`mk_id()`](_autosummary/annals.store.html.md#annals.store.mk_id).

* **Return type:**
  [`str`](https://docs.python.org/3/builtins/stdtypes.html#str)

### annals.store.kind_of(filename)

The rendering kind of a file, from its extension.

* **Return type:**
  [`str`](https://docs.python.org/3/builtins/stdtypes.html#str)

### annals.store.mk_id(title, , when=None)

`YYYYMMDD-HHMMSS-<slug>-<4 hex>`: sortable by time, readable in a URL.

* **Return type:**
  [`str`](https://docs.python.org/3/builtins/stdtypes.html#str)

### annals.store.now_iso()

UTC timestamp with second precision, the format every meta.json uses.

* **Return type:**
  [`str`](https://docs.python.org/3/builtins/stdtypes.html#str)

### annals.store.slugify(text, , max_len=48)

Lowercase ascii words joined by hyphens; empty input becomes `doc`.

* **Return type:**
  [`str`](https://docs.python.org/3/builtins/stdtypes.html#str)


# _autosummary/annals.target.html.md

# annals.target

The transport seam: a document tree lands in a directory, here or on another machine.

A `Target` is the handful of filesystem verbs the store needs (put a directory, move,
remove, read and write a text file, list sub-directories, read every `meta.json` under a
directory). Two implementations:

* [`LocalTarget`](_autosummary/annals.target.html.md#annals.target.LocalTarget): a directory on this machine, plain `pathlib`/`shutil`.
* [`SshTarget`](_autosummary/annals.target.html.md#annals.target.SshTarget): `host:path` on another machine, through the system `ssh` and
  `rsync` in batch mode (no prompts, so an agent never hangs). The host is whatever the
  user’s `~/.ssh/config` resolves, so an alias such as `tw` works.

[`parse_target()`](_autosummary/annals.target.html.md#annals.target.parse_target) picks one from a string. An HTTP target (a bearer-token ingest
endpoint) is the declared replacement for the ssh one and is not built yet.

### Module Attributes

| [`RECORD_SEP`](_autosummary/annals.target.html.md#annals.target.RECORD_SEP)   | Separator between concatenated meta.json files when reading many at once over ssh.   |
|---------------------------------------------------------------|--------------------------------------------------------------------------------------|

### Functions

| [`check_rel`](_autosummary/annals.target.html.md#annals.target.check_rel)(rel)     | Refuse anything that could escape the root; the store only ever passes safe paths.                                                                                |
|---------------------------------------------------------------------|-------------------------------------------------------------------------------------------------------------------------------------------------------------------|
| [`parse_target`](_autosummary/annals.target.html.md#annals.target.parse_target)(spec) | `host:/path` becomes an [`SshTarget`](_autosummary/annals.target.html.md#annals.target.SshTarget); anything else a [`LocalTarget`](_autosummary/annals.target.html.md#annals.target.LocalTarget). |

### Classes

| [`LocalTarget`](_autosummary/annals.target.html.md#annals.target.LocalTarget)(root)          | A directory on this machine.                                                |
|-----------------------------------------------------------------------------|-----------------------------------------------------------------------------|
| [`SshTarget`](_autosummary/annals.target.html.md#annals.target.SshTarget)(host, root)      | `host:path` on another machine, via the system ssh and rsync in batch mode. |
| [`Target`](_autosummary/annals.target.html.md#annals.target.Target)(\*args, \*\*kwargs) | What the store needs from wherever the documents live.                      |

### *class* annals.target.LocalTarget(root)

Bases: [`object`](https://docs.python.org/3/builtins/functions.html#object)

A directory on this machine.

#### local_path(rel)

`rel` under the root, resolved; `None` if it escapes the root (a symlink).

* **Return type:**
  [`Path`](https://docs.python.org/3/library/pathlib.html#pathlib.Path) | [`None`](https://docs.python.org/3/builtins/constants.html#None)

### annals.target.RECORD_SEP *= '\\x1e'*

Separator between concatenated meta.json files when reading many at once over ssh.

### *class* annals.target.SshTarget(host, root)

Bases: [`object`](https://docs.python.org/3/builtins/functions.html#object)

`host:path` on another machine, via the system ssh and rsync in batch mode.

### *class* annals.target.Target(\*args, \*\*kwargs)

Bases: [`Protocol`](https://docs.python.org/3/library/typing.html#typing.Protocol)

What the store needs from wherever the documents live.

#### local_path(rel)

The file on this machine’s disk, when the documents are local (to stream it).

* **Return type:**
  [`Path`](https://docs.python.org/3/library/pathlib.html#pathlib.Path) | [`None`](https://docs.python.org/3/builtins/constants.html#None)

### annals.target.check_rel(rel)

Refuse anything that could escape the root; the store only ever passes safe paths.

* **Return type:**
  [`str`](https://docs.python.org/3/builtins/stdtypes.html#str)

### annals.target.parse_target(spec)

`host:/path` becomes an [`SshTarget`](_autosummary/annals.target.html.md#annals.target.SshTarget); anything else a [`LocalTarget`](_autosummary/annals.target.html.md#annals.target.LocalTarget).

* **Return type:**
  [`Target`](_autosummary/annals.target.html.md#annals.target.Target)


# _autosummary/annals.tools.html.md

# annals.tools

The operations, as plain functions: JSON-able arguments in, JSON-able dicts out.

This module is the single source of truth for what a annals can do. The CLI
(`annals.__main__`, via `cw`), the MCP server (`py2mcp` over string refs to these
names) and the shipped skill all describe the same functions, so there is nothing to keep
in parity. Nothing here prints or exits; the surfaces do that.

### Functions

| [`configure`](_autosummary/annals.tools.html.md#annals.tools.configure)(\*[, target, base_url])             | Write the publisher config (where to publish, what link to print) and show it.   |
|------------------------------------------------------------------------------------------------|----------------------------------------------------------------------------------|
| [`group`](_autosummary/annals.tools.html.md#annals.tools.group)(title, doc_ids, \*[, target, base_url]) | Make a group (one URL for a set of documents) from existing document ids.        |
| [`groups`](_autosummary/annals.tools.html.md#annals.tools.groups)(\*[, target, base_url])                | List groups, newest first.                                                       |
| [`ls`](_autosummary/annals.tools.html.md#annals.tools.ls)([q, trash, limit, target, base_url])       | List documents (newest first), optionally filtered by words or showing the bin.  |
| [`page_shell`](_autosummary/annals.tools.html.md#annals.tools.page_shell)(\*[, api, base, title])            | The page's html shell, for a host that serves the API under its own prefix.      |
| [`publish`](_autosummary/annals.tools.html.md#annals.tools.publish)(paths, \*[, title, tags, group, ...]) | Publish one document and print its link; several paths become several documents. |
| [`restore`](_autosummary/annals.tools.html.md#annals.tools.restore)(doc_ids, \*[, target])                | Bring documents back from the recycle bin.                                       |
| [`serve`](_autosummary/annals.tools.html.md#annals.tools.serve)(\*[, host, port, data_dir, base_path])  | Serve the annals page and API (needs `pip install 'annals[server]'`).            |
| [`show`](_autosummary/annals.tools.html.md#annals.tools.show)(doc_id, \*[, target, base_url])          | A document's metadata and link.                                                  |
| [`trash`](_autosummary/annals.tools.html.md#annals.tools.trash)(doc_ids, \*[, target])                  | Move documents to the recycle bin (restorable).                                  |

### annals.tools.configure(, target=None, base_url=None)

Write the publisher config (where to publish, what link to print) and show it.

* **Return type:**
  [`dict`](https://docs.python.org/3/builtins/stdtypes.html#dict)

### Example

`annals configure --target tw:/root/.local/share/annals --base-url https://apps.example.com/annals`.
With no arguments, shows the resolved settings without writing.

### annals.tools.group(title, doc_ids, , target=None, base_url=None)

Make a group (one URL for a set of documents) from existing document ids.

* **Return type:**
  [`dict`](https://docs.python.org/3/builtins/stdtypes.html#dict)

### annals.tools.groups(, target=None, base_url=None)

List groups, newest first.

* **Return type:**
  [`dict`](https://docs.python.org/3/builtins/stdtypes.html#dict)

### annals.tools.ls(q='', , trash=False, limit=50, target=None, base_url=None)

List documents (newest first), optionally filtered by words or showing the bin.

* **Return type:**
  [`dict`](https://docs.python.org/3/builtins/stdtypes.html#dict)

### annals.tools.page_shell(, api='/api/annals', base='/annals', title='annals')

The page’s html shell, for a host that serves the API under its own prefix.

An enlace app writes it once as its frontend:
`annals page-shell --api /api/annals --base /annals > frontend/index.html`.
The shell only names the two paths; the page’s code loads from the API, so it upgrades
with the package.

* **Return type:**
  [`str`](https://docs.python.org/3/builtins/stdtypes.html#str)

### annals.tools.publish(paths, , title=None, tags='', group=None, add_to=None, session=None, all_files=False, target=None, base_url=None)

Publish one document and print its link; several paths become several documents.

`paths`: files of any kind (markdown, html, images, video, audio, pdf, text), a
directory (one document: an `index.html` or single page with its assets, else a
gallery of everything in it), or `-` for stdin (markdown). `tags` is comma separated. `group`
names a group to create from the published documents; the reply then carries
`group_url` too. `add_to` appends them to an existing group instead (its id, as
printed in a group link), so a second batch lands under the link the owner already has. `session` records who published (the source shown on the page). A directory skips
hidden files and caches (`.*`, `__pycache__`, `*.pyc`) unless `all_files`.

* **Return type:**
  [`dict`](https://docs.python.org/3/builtins/stdtypes.html#dict)

### annals.tools.restore(doc_ids, , target=None)

Bring documents back from the recycle bin.

* **Return type:**
  [`dict`](https://docs.python.org/3/builtins/stdtypes.html#dict)

### annals.tools.serve(, host='127.0.0.1', port=8765, data_dir=None, base_path='/annals')

Serve the annals page and API (needs `pip install 'annals[server]'`).

Auth comes from the environment: `ANNALS_WHOAMI_URL` + `ANNALS_ALLOWED_USERS` to sit
behind an existing login, `ANNALS_BASIC_USER` + `ANNALS_BASIC_PASSWORD` for HTTP Basic,
nothing for an open server on localhost.

* **Return type:**
  [`None`](https://docs.python.org/3/builtins/constants.html#None)

### annals.tools.show(doc_id, , target=None, base_url=None)

A document’s metadata and link.

* **Return type:**
  [`dict`](https://docs.python.org/3/builtins/stdtypes.html#dict)

### annals.tools.trash(doc_ids, , target=None)

Move documents to the recycle bin (restorable).

* **Return type:**
  [`dict`](https://docs.python.org/3/builtins/stdtypes.html#dict)


# about-this-build.html.md

<!-- generated by epythet -->

# About this build

This documentation was built on **2026-10-04 09:49 UTC** from commit <a href="https://github.com/thorwhalen/annals/commit/537becd3d683b1d72bf495b9ea40c49eb9b2c594"><code>537becd</code></a> on branch <code>main</code>, for **annals 0.0.3** (from <code>pyproject.toml</code>).

#### NOTE
Nothing suggests a mismatch: the tree was clean at the commit above, and the documented version is the one on PyPI.

## Source

|                     |                                                                                                                                                          |
|---------------------|----------------------------------------------------------------------------------------------------------------------------------------------------------|
| Commit              | <a href="https://github.com/thorwhalen/annals/commit/537becd3d683b1d72bf495b9ea40c49eb9b2c594"><code>537becd3d683b1d72bf495b9ea40c49eb9b2c594</code></a> |
| Branch              | <code>main</code>                                                                                                                                        |
| Tags at this commit | none                                                                                                                                                     |
| Working tree        | clean                                                                                                                                                    |
| Remote              | <code>https://github.com/thorwhalen/annals</code>                                                                                                        |

## Continuous integration

|              |                                                                                            |
|--------------|--------------------------------------------------------------------------------------------|
| Repository   | <code>thorwhalen/annals</code>                                                             |
| Run          | <a href="https://github.com/thorwhalen/annals/actions/runs/37193299772">37193299772</a>    |
| Ref          | <code>refs/heads/main</code>                                                               |
| Event commit | <code>537becd3d683b1d72bf495b9ea40c49eb9b2c594</code> (in the history of the built commit) |

## Tools

|          |         |
|----------|---------|
| epythet  | 0.2.12  |
| Sphinx   | 9.1.0   |
| docutils | 0.22.4  |
| Python   | 3.12.14 |

## Configuration as resolved

|               |                                                                  |
|---------------|------------------------------------------------------------------|
| theme         | <code>auto</code> (Sphinx theme <code>shibuya</code>)            |
| accent        | <code>#624496</code>                                             |
| api_generator | <code>autosummary</code>                                         |
| ignore        | <code>tests/</code>, <code>scrap/</code>, <code>examples/</code> |
| agent_outputs | <code>true</code>                                                |
| aggregates    | <code>md</code>                                                  |
| ai_artifacts  | <code>true</code>                                                |

## Package on PyPI

Latest release: <a href="https://pypi.org/project/annals/0.0.3/">0.0.3</a>, the same as the documented version.

## Reproduce

```bash
git clone https://github.com/thorwhalen/annals && cd annals
git checkout 537becd3d683b1d72bf495b9ea40c49eb9b2c594
pip install "epythet==0.2.12"
epythet quickstart . --ignore tests/ scrap/ examples/
```

The same data, for machines: <a href="build_info.json"><code>build_info.json</code></a> (schema version 1).


# ai-agents.html.md

<!-- generated by epythet -->

# For AI agents

`annals` ships artifacts for coding agents alongside its code. This page lists
them, says where each lives in the repository, and points at the
machine-readable copies of this documentation.

## Skills

Skills are folders holding a `SKILL.md` (the [Agent Skills](https://agentskills.io) format): a description that tells an agent when to use it and a body with the procedure. Install one into your agent with `gh skill` (any host: `--agent claude-code`, `copilot`, `cursor`, `codex`, `gemini`), or use the copy bundled in the wheel.

### `annals-publish`

Publish a document or media (a markdown report, an html page, images, video, audio, a PDF, or a whole folder of renders) to the owner’s private annals and hand back the link, so they can read it on a phone or tablet away from the terminal. Use whenever the user asks to “publish this”, “post it to my annals”, “put it where I can read it on my phone”, “give me a link to that report”, “show me the images”, “where can I see the renders”, “share the page with me”, or whenever you have produced a document longer than a few lines that the user will want to read later or elsewhere. Also covers grouping several documents under one link, listing what is in the annals, and moving a document to the recycle bin.

```bash
gh skill install thorwhalen/annals annals-publish --agent claude-code
```

Source: [`annals/data/skills/annals-publish`](https://github.com/thorwhalen/annals/tree/HEAD/annals/data/skills/annals-publish) (bundled with the pip package).

The bundled skills are also on disk after `pip install annals`, under the package’s `data/skills/` directory; link them into an agent without network access with `skill link-skills <that directory>`.

## Machine-readable documentation

This site publishes the same documentation in forms that fit an agent’s context window:

- [`llms.txt`](https://thorwhalen.github.io/annals/llms.txt): an index of every page with a one-line description ([llms.txt](https://llmstxt.org) format)
- [`annals.md`](https://thorwhalen.github.io/annals/annals.md): the whole documentation as one Markdown file
- `<page>.html.md`: a rendered Markdown twin of every page, advertised from each page’s `<head>` with `<link rel="alternate" type="text/markdown">`
- [`objects.inv`](https://thorwhalen.github.io/annals/objects.inv): the Sphinx inventory: a symbol-to-URL index (`sphobjinv convert plain objects.inv -`)


# api.html.md

# API reference

| [`annals`](_autosummary/annals.html.md#module-annals)   | annals: an in-tray for AI agents.   |
|-------------------------------------------------------------------------|-------------------------------------|


