annals 0.0.3__tar.gz

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -0,0 +1 @@
1
+ *.ipynb linguist-documentation
@@ -0,0 +1,119 @@
1
+ .claude/handoffs/
2
+ .claude/scratch/
3
+
4
+ # Byte-compiled / optimized / DLL files
5
+ __pycache__/
6
+ *.py[cod]
7
+ *$py.class
8
+
9
+
10
+ .DS_Store
11
+ # C extensions
12
+ *.so
13
+
14
+ # TLS certificates
15
+ ## Ignore all PEM files anywhere
16
+ *.pem
17
+ ## Also ignore any certs directory
18
+ certs/
19
+
20
+ # Distribution / packaging
21
+ .Python
22
+ build/
23
+ develop-eggs/
24
+ dist/
25
+ downloads/
26
+ eggs/
27
+ .eggs/
28
+ lib/
29
+ lib64/
30
+ parts/
31
+ sdist/
32
+ var/
33
+ wheels/
34
+ *.egg-info/
35
+ .installed.cfg
36
+ *.egg
37
+ MANIFEST
38
+ _build
39
+
40
+ # PyInstaller
41
+ # Usually these files are written by a python script from a template
42
+ # before PyInstaller builds the exe, so as to inject date/other infos into it.
43
+ *.manifest
44
+ *.spec
45
+
46
+ # Installer logs
47
+ pip-log.txt
48
+ pip-delete-this-directory.txt
49
+
50
+ # Unit test / coverage reports
51
+ htmlcov/
52
+ .tox/
53
+ .coverage
54
+ .coverage.*
55
+ .cache
56
+ nosetests.xml
57
+ coverage.xml
58
+ *.cover
59
+ .hypothesis/
60
+ .pytest_cache/
61
+
62
+ # Translations
63
+ *.mo
64
+ *.pot
65
+
66
+ # Django stuff:
67
+ *.log
68
+ local_settings.py
69
+ db.sqlite3
70
+
71
+ # Flask stuff:
72
+ instance/
73
+ .webassets-cache
74
+
75
+ # Scrapy stuff:
76
+ .scrapy
77
+
78
+ # Sphinx documentation
79
+ docs/_build/
80
+
81
+ # PyBuilder
82
+ target/
83
+
84
+ # Jupyter Notebook
85
+ .ipynb_checkpoints
86
+
87
+ # pyenv
88
+ .python-version
89
+
90
+ # celery beat schedule file
91
+ celerybeat-schedule
92
+
93
+ # SageMath parsed files
94
+ *.sage.py
95
+
96
+ # Environments
97
+ .env
98
+ .venv
99
+ env/
100
+ venv/
101
+ ENV/
102
+ env.bak/
103
+ venv.bak/
104
+
105
+ # Spyder project settings
106
+ .spyderproject
107
+ .spyproject
108
+
109
+ # Rope project settings
110
+ .ropeproject
111
+
112
+ # mkdocs documentation
113
+ /site
114
+
115
+ # mypy
116
+ .mypy_cache/
117
+
118
+ # PyCharm
119
+ .idea
annals-0.0.3/LICENSE ADDED
@@ -0,0 +1,21 @@
1
+ MIT License
2
+
3
+ Copyright (c) 2026 Thor Whalen
4
+
5
+ Permission is hereby granted, free of charge, to any person obtaining a copy
6
+ of this software and associated documentation files (the "Software"), to deal
7
+ in the Software without restriction, including without limitation the rights
8
+ to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
9
+ copies of the Software, and to permit persons to whom the Software is
10
+ furnished to do so, subject to the following conditions:
11
+
12
+ The above copyright notice and this permission notice shall be included in all
13
+ copies or substantial portions of the Software.
14
+
15
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16
+ IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
17
+ FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
18
+ AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
19
+ LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
20
+ OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
21
+ SOFTWARE.
annals-0.0.3/PKG-INFO ADDED
@@ -0,0 +1,135 @@
1
+ Metadata-Version: 2.5
2
+ Name: annals
3
+ Version: 0.0.3
4
+ Summary: Annals for AI agents: publish markdown, html, images, video, audio, PDFs and folders to a private page the owner reads on the phone, and get back a link.
5
+ Project-URL: Homepage, https://github.com/thorwhalen/annals
6
+ Project-URL: Repository, https://github.com/thorwhalen/annals
7
+ Project-URL: Documentation, https://thorwhalen.github.io/annals
8
+ Author: Thor Whalen
9
+ License-Expression: MIT
10
+ License-File: LICENSE
11
+ Keywords: agents,claude-code,html,in-tray,markdown,media,page,private,publish
12
+ Requires-Python: >=3.11
13
+ Requires-Dist: cw<0.2,>=0.1
14
+ Provides-Extra: dev
15
+ Requires-Dist: fastapi>=0.100; extra == 'dev'
16
+ Requires-Dist: httpx; extra == 'dev'
17
+ Requires-Dist: pillow>=10; extra == 'dev'
18
+ Requires-Dist: pytest-cov>=4.0; extra == 'dev'
19
+ Requires-Dist: pytest>=7.0; extra == 'dev'
20
+ Requires-Dist: ruff>=0.1.0; extra == 'dev'
21
+ Provides-Extra: docs
22
+ Requires-Dist: sphinx-rtd-theme>=1.0; extra == 'docs'
23
+ Requires-Dist: sphinx>=6.0; extra == 'docs'
24
+ Provides-Extra: mcp
25
+ Requires-Dist: py2mcp; extra == 'mcp'
26
+ Provides-Extra: server
27
+ Requires-Dist: fastapi>=0.100; extra == 'server'
28
+ Requires-Dist: pillow>=10; extra == 'server'
29
+ Requires-Dist: uvicorn>=0.20; extra == 'server'
30
+ Description-Content-Type: text/markdown
31
+
32
+ # annals
33
+
34
+ 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.
35
+
36
+ ```bash
37
+ pip install annals
38
+ annals publish report.md --session my-agent
39
+ # https://apps.example.com/annals/d/20261003-203301-quarterly-report-3f9a
40
+ ```
41
+
42
+ 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.
43
+
44
+ ## For agents
45
+
46
+ Load the shipped skill, `annals-publish`, and you have the whole protocol: one command, one link, put the link in your reply.
47
+
48
+ ```bash
49
+ annals publish report.md # one document, prints its link
50
+ annals publish a.md b.md c.html --group "Review" # one link for a set (the group link prints first)
51
+ annals publish ./renders # a folder: a gallery with inline images, video, audio
52
+ annals publish ./site_dir # an html page with its assets, as one document
53
+ annals publish clip.mp4 # media plays inline (streamed, seekable)
54
+ some-command | annals publish - --title "Log" # from stdin
55
+ annals ls "search words" # what is there, newest first
56
+ annals trash <id> / annals restore <id> # the recycle bin
57
+ ```
58
+
59
+ What each kind looks like on the page:
60
+
61
+ | Published | Shown as |
62
+ |---|---|
63
+ | 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 |
64
+ | an `.html` file, or a folder with `index.html` | the page itself, in a sandboxed frame |
65
+ | an image, video, audio file or PDF | inline: image, player (byte-range streaming, so video seeks), PDF viewer |
66
+ | a folder with one page in it | that page, with the other files below it |
67
+ | 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 |
68
+ | anything else | a download |
69
+
70
+ A folder skips hidden files and caches (`.*`, `__pycache__`, `*.pyc`); `--all-files` keeps them.
71
+
72
+ With `pip install 'annals[mcp]'`, the same operations are an MCP server: `python -m annals.mcp`.
73
+
74
+ ## Where documents go
75
+
76
+ ```bash
77
+ annals configure --target tw:/root/.local/share/annals --base-url https://apps.example.com/annals
78
+ ```
79
+
80
+ 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`.
81
+
82
+ ## Hosting the page
83
+
84
+ **Inside an app platform** (the usual case). Mount the API under the platform's prefix and let the platform's login gate it:
85
+
86
+ ```python
87
+ # server.py of the host app; the platform serves frontend/ at /annals/ and this at /api/annals
88
+ from annals.api import mk_api
89
+ app = mk_api(data_dir="/root/.local/share/annals")
90
+ ```
91
+
92
+ and write the page shell once as the app's frontend:
93
+
94
+ ```bash
95
+ annals page-shell --api /api/annals --base /annals > frontend/index.html
96
+ ```
97
+
98
+ 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.
99
+
100
+ **Standalone**, for a machine with no platform:
101
+
102
+ ```bash
103
+ pip install 'annals[server]'
104
+ ANNALS_DATA_DIR=~/.local/share/annals annals serve --host 127.0.0.1 --port 8765
105
+ ```
106
+
107
+ The page is at `http://127.0.0.1:8765/annals/`, its API under `/annals/api/`. Auth is chosen by environment variables:
108
+
109
+ | Situation | Set | Effect |
110
+ |---|---|---|
111
+ | 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 |
112
+ | No platform login | `ANNALS_BASIC_USER`, `ANNALS_BASIC_PASSWORD` | HTTP Basic |
113
+ | Localhost only | nothing | open |
114
+
115
+ ## Python API
116
+
117
+ ```python
118
+ from annals import DocStore, publish
119
+ meta = DocStore("~/.local/share/annals").publish("report.md", tags=["q3"])
120
+ publish(["report.md"], tags="q3")["url"] # same, through the configured target
121
+ ```
122
+
123
+ `annals.api.mk_api(...)` is the mountable API; `annals.api.mk_app(data_dir=..., authorizer=...)` the standalone app.
124
+
125
+ ## Design notes
126
+
127
+ 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`.
128
+
129
+ ## Skills
130
+
131
+ ```bash
132
+ gh skill install thorwhalen/annals annals-publish
133
+ ```
134
+
135
+ The skill also ships inside the package at `annals/data/skills/annals-publish/`.
annals-0.0.3/README.md ADDED
@@ -0,0 +1,104 @@
1
+ # annals
2
+
3
+ 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.
4
+
5
+ ```bash
6
+ pip install annals
7
+ annals publish report.md --session my-agent
8
+ # https://apps.example.com/annals/d/20261003-203301-quarterly-report-3f9a
9
+ ```
10
+
11
+ 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.
12
+
13
+ ## For agents
14
+
15
+ Load the shipped skill, `annals-publish`, and you have the whole protocol: one command, one link, put the link in your reply.
16
+
17
+ ```bash
18
+ annals publish report.md # one document, prints its link
19
+ annals publish a.md b.md c.html --group "Review" # one link for a set (the group link prints first)
20
+ annals publish ./renders # a folder: a gallery with inline images, video, audio
21
+ annals publish ./site_dir # an html page with its assets, as one document
22
+ annals publish clip.mp4 # media plays inline (streamed, seekable)
23
+ some-command | annals publish - --title "Log" # from stdin
24
+ annals ls "search words" # what is there, newest first
25
+ annals trash <id> / annals restore <id> # the recycle bin
26
+ ```
27
+
28
+ What each kind looks like on the page:
29
+
30
+ | Published | Shown as |
31
+ |---|---|
32
+ | 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 |
33
+ | an `.html` file, or a folder with `index.html` | the page itself, in a sandboxed frame |
34
+ | an image, video, audio file or PDF | inline: image, player (byte-range streaming, so video seeks), PDF viewer |
35
+ | a folder with one page in it | that page, with the other files below it |
36
+ | 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 |
37
+ | anything else | a download |
38
+
39
+ A folder skips hidden files and caches (`.*`, `__pycache__`, `*.pyc`); `--all-files` keeps them.
40
+
41
+ With `pip install 'annals[mcp]'`, the same operations are an MCP server: `python -m annals.mcp`.
42
+
43
+ ## Where documents go
44
+
45
+ ```bash
46
+ annals configure --target tw:/root/.local/share/annals --base-url https://apps.example.com/annals
47
+ ```
48
+
49
+ 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`.
50
+
51
+ ## Hosting the page
52
+
53
+ **Inside an app platform** (the usual case). Mount the API under the platform's prefix and let the platform's login gate it:
54
+
55
+ ```python
56
+ # server.py of the host app; the platform serves frontend/ at /annals/ and this at /api/annals
57
+ from annals.api import mk_api
58
+ app = mk_api(data_dir="/root/.local/share/annals")
59
+ ```
60
+
61
+ and write the page shell once as the app's frontend:
62
+
63
+ ```bash
64
+ annals page-shell --api /api/annals --base /annals > frontend/index.html
65
+ ```
66
+
67
+ 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.
68
+
69
+ **Standalone**, for a machine with no platform:
70
+
71
+ ```bash
72
+ pip install 'annals[server]'
73
+ ANNALS_DATA_DIR=~/.local/share/annals annals serve --host 127.0.0.1 --port 8765
74
+ ```
75
+
76
+ The page is at `http://127.0.0.1:8765/annals/`, its API under `/annals/api/`. Auth is chosen by environment variables:
77
+
78
+ | Situation | Set | Effect |
79
+ |---|---|---|
80
+ | 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 |
81
+ | No platform login | `ANNALS_BASIC_USER`, `ANNALS_BASIC_PASSWORD` | HTTP Basic |
82
+ | Localhost only | nothing | open |
83
+
84
+ ## Python API
85
+
86
+ ```python
87
+ from annals import DocStore, publish
88
+ meta = DocStore("~/.local/share/annals").publish("report.md", tags=["q3"])
89
+ publish(["report.md"], tags="q3")["url"] # same, through the configured target
90
+ ```
91
+
92
+ `annals.api.mk_api(...)` is the mountable API; `annals.api.mk_app(data_dir=..., authorizer=...)` the standalone app.
93
+
94
+ ## Design notes
95
+
96
+ 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`.
97
+
98
+ ## Skills
99
+
100
+ ```bash
101
+ gh skill install thorwhalen/annals annals-publish
102
+ ```
103
+
104
+ The skill also ships inside the package at `annals/data/skills/annals-publish/`.
@@ -0,0 +1,29 @@
1
+ """annals: an in-tray for AI agents. Publish documents and media to a private page; get a link.
2
+
3
+ Agents call :func:`annals.tools.publish` (or ``annals publish file.md`` from a shell) and hand
4
+ the printed URL to their human, who opens it on a phone. The documents are plain files in a
5
+ directory, local or on another machine over ssh; :func:`annals.api.mk_app` serves that
6
+ directory as a private page with search, sort, groups and a recycle bin.
7
+ """
8
+
9
+ from annals.config import Settings, load_settings
10
+ from annals.store import DocStore
11
+ from annals.target import LocalTarget, SshTarget, parse_target
12
+ from annals.tools import configure, group, groups, ls, publish, restore, show, trash
13
+
14
+ __all__ = [
15
+ "DocStore",
16
+ "LocalTarget",
17
+ "Settings",
18
+ "SshTarget",
19
+ "configure",
20
+ "group",
21
+ "groups",
22
+ "load_settings",
23
+ "ls",
24
+ "parse_target",
25
+ "publish",
26
+ "restore",
27
+ "show",
28
+ "trash",
29
+ ]
@@ -0,0 +1,72 @@
1
+ # PYTHON_ARGCOMPLETE_OK
2
+ """CLI entry point: ``annals publish report.md`` prints a link. Built by ``cw`` over :mod:`annals.tools`."""
3
+
4
+ from __future__ import annotations
5
+
6
+ import json
7
+ import sys
8
+
9
+ import cw
10
+
11
+ from annals.tools import _dispatch_funcs
12
+
13
+ #: the shape ``publish`` returns for one document; only that shape prints as a bare link
14
+ PUBLISH_KEYS = frozenset({"id", "title", "url", "group_id", "group_url", "warning"})
15
+ #: expected failures: one line on stderr and exit 1, no traceback
16
+ EXPECTED_ERRORS = (
17
+ ValueError,
18
+ KeyError,
19
+ FileNotFoundError,
20
+ PermissionError,
21
+ RuntimeError,
22
+ )
23
+
24
+
25
+ def _egress(result, *, out=None, err=None) -> int:
26
+ """Print a link when there is one (that is what an agent wants back), else JSON."""
27
+ out = out or sys.stdout
28
+ if result is None:
29
+ return 0
30
+ if isinstance(result, dict) and result.get("warning"):
31
+ print(f"annals: warning: {result.pop('warning')}", file=err or sys.stderr)
32
+ if isinstance(result, str):
33
+ print(result, file=out, end="" if result.endswith("\n") else "\n")
34
+ return 0
35
+ if isinstance(result, dict) and "url" in result and set(result) <= PUBLISH_KEYS:
36
+ print(result["url"], file=out)
37
+ if "group_url" in result and result["group_url"] != result["url"]:
38
+ print(result["group_url"], file=out)
39
+ return 0
40
+ if isinstance(result, dict) and "docs" in result and "group_url" in result:
41
+ print(result["group_url"], file=out)
42
+ for d in result["docs"]:
43
+ print(f" {d['url']} {d['title']}", file=out)
44
+ return 0
45
+ print(json.dumps(result, indent=1, ensure_ascii=False), file=out)
46
+ return 0
47
+
48
+
49
+ def run(argv=None, *, out=None, err=None) -> int:
50
+ """Dispatch ``argv`` (default ``sys.argv[1:]``) and return the exit code."""
51
+ try:
52
+ return cw.dispatch(
53
+ _dispatch_funcs,
54
+ argv,
55
+ prog="annals",
56
+ convention=cw.MODERN,
57
+ egress=_egress,
58
+ out=out,
59
+ err=err,
60
+ )
61
+ except EXPECTED_ERRORS as e:
62
+ print(f"annals: {type(e).__name__}: {e}", file=err or sys.stderr)
63
+ return 1
64
+
65
+
66
+ def main() -> None:
67
+ """Console-script entry point."""
68
+ raise SystemExit(run())
69
+
70
+
71
+ if __name__ == "__main__":
72
+ main()