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.
- annals-0.0.3/.gitattributes +1 -0
- annals-0.0.3/.gitignore +119 -0
- annals-0.0.3/LICENSE +21 -0
- annals-0.0.3/PKG-INFO +135 -0
- annals-0.0.3/README.md +104 -0
- annals-0.0.3/annals/__init__.py +29 -0
- annals-0.0.3/annals/__main__.py +72 -0
- annals-0.0.3/annals/api.py +471 -0
- annals-0.0.3/annals/auth.py +120 -0
- annals-0.0.3/annals/config.py +121 -0
- annals-0.0.3/annals/data/skills/annals-publish/SKILL.md +67 -0
- annals-0.0.3/annals/data/ui/app.js +418 -0
- annals-0.0.3/annals/data/ui/index.html +18 -0
- annals-0.0.3/annals/data/ui/marked.min.js +69 -0
- annals-0.0.3/annals/data/ui/purify.min.js +3 -0
- annals-0.0.3/annals/data/ui/style.css +138 -0
- annals-0.0.3/annals/mcp.py +32 -0
- annals-0.0.3/annals/store.py +377 -0
- annals-0.0.3/annals/target.py +248 -0
- annals-0.0.3/annals/tools.py +256 -0
- annals-0.0.3/misc/docs/design.md +52 -0
- annals-0.0.3/misc/docs/research.md +10 -0
- annals-0.0.3/misc/docs/roadmap.md +12 -0
- annals-0.0.3/pyproject.toml +188 -0
- annals-0.0.3/tests/test_api.py +237 -0
- annals-0.0.3/tests/test_cli.py +101 -0
- annals-0.0.3/tests/test_store.py +108 -0
|
@@ -0,0 +1 @@
|
|
|
1
|
+
*.ipynb linguist-documentation
|
annals-0.0.3/.gitignore
ADDED
|
@@ -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()
|