heartofgold 0.1.0__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.
- heartofgold-0.1.0/LICENSE +21 -0
- heartofgold-0.1.0/PKG-INFO +167 -0
- heartofgold-0.1.0/README.md +140 -0
- heartofgold-0.1.0/pyproject.toml +71 -0
- heartofgold-0.1.0/pyproject.toml.orig +60 -0
- heartofgold-0.1.0/src/heartofgold/__init__.py +3 -0
- heartofgold-0.1.0/src/heartofgold/__main__.py +3 -0
- heartofgold-0.1.0/src/heartofgold/api/__init__.py +0 -0
- heartofgold-0.1.0/src/heartofgold/api/errors.py +92 -0
- heartofgold-0.1.0/src/heartofgold/api/files.py +85 -0
- heartofgold-0.1.0/src/heartofgold/api/headers.py +44 -0
- heartofgold-0.1.0/src/heartofgold/api/limits.py +53 -0
- heartofgold-0.1.0/src/heartofgold/api/routes.py +443 -0
- heartofgold-0.1.0/src/heartofgold/app.py +106 -0
- heartofgold-0.1.0/src/heartofgold/auth/__init__.py +0 -0
- heartofgold-0.1.0/src/heartofgold/auth/deps.py +66 -0
- heartofgold-0.1.0/src/heartofgold/auth/oauth.py +273 -0
- heartofgold-0.1.0/src/heartofgold/auth/ratelimit.py +31 -0
- heartofgold-0.1.0/src/heartofgold/auth/sessions.py +101 -0
- heartofgold-0.1.0/src/heartofgold/backup.py +48 -0
- heartofgold-0.1.0/src/heartofgold/cli.py +225 -0
- heartofgold-0.1.0/src/heartofgold/config.py +171 -0
- heartofgold-0.1.0/src/heartofgold/db.py +114 -0
- heartofgold-0.1.0/src/heartofgold/log.py +77 -0
- heartofgold-0.1.0/src/heartofgold/mcp_render.py +152 -0
- heartofgold-0.1.0/src/heartofgold/mcp_server.py +869 -0
- heartofgold-0.1.0/src/heartofgold/migrations/0001_initial.sql +162 -0
- heartofgold-0.1.0/src/heartofgold/migrations/0002_oauth.sql +37 -0
- heartofgold-0.1.0/src/heartofgold/migrations/__init__.py +0 -0
- heartofgold-0.1.0/src/heartofgold/models.py +220 -0
- heartofgold-0.1.0/src/heartofgold/py.typed +0 -0
- heartofgold-0.1.0/src/heartofgold/schemas.py +235 -0
- heartofgold-0.1.0/src/heartofgold/services/__init__.py +0 -0
- heartofgold-0.1.0/src/heartofgold/services/actors.py +343 -0
- heartofgold-0.1.0/src/heartofgold/services/attachments.py +363 -0
- heartofgold-0.1.0/src/heartofgold/services/claims.py +132 -0
- heartofgold-0.1.0/src/heartofgold/services/comments.py +391 -0
- heartofgold-0.1.0/src/heartofgold/services/common.py +204 -0
- heartofgold-0.1.0/src/heartofgold/services/dependencies.py +183 -0
- heartofgold-0.1.0/src/heartofgold/services/events.py +185 -0
- heartofgold-0.1.0/src/heartofgold/services/history.py +196 -0
- heartofgold-0.1.0/src/heartofgold/services/inbox.py +91 -0
- heartofgold-0.1.0/src/heartofgold/services/markdown.py +128 -0
- heartofgold-0.1.0/src/heartofgold/services/oauth.py +356 -0
- heartofgold-0.1.0/src/heartofgold/services/ordering.py +26 -0
- heartofgold-0.1.0/src/heartofgold/services/projects.py +443 -0
- heartofgold-0.1.0/src/heartofgold/services/search.py +102 -0
- heartofgold-0.1.0/src/heartofgold/services/tasks.py +697 -0
- heartofgold-0.1.0/src/heartofgold/services/waiting.py +152 -0
- heartofgold-0.1.0/src/heartofgold/web/__init__.py +0 -0
- heartofgold-0.1.0/src/heartofgold/web/routes.py +851 -0
- heartofgold-0.1.0/src/heartofgold/web/static/app.css +76 -0
- heartofgold-0.1.0/src/heartofgold/web/static/app.js +137 -0
- heartofgold-0.1.0/src/heartofgold/web/static/vendor/README.md +7 -0
- heartofgold-0.1.0/src/heartofgold/web/static/vendor/SHA256SUMS +3 -0
- heartofgold-0.1.0/src/heartofgold/web/static/vendor/Sortable.min.js +2 -0
- heartofgold-0.1.0/src/heartofgold/web/static/vendor/htmx.min.js +1 -0
- heartofgold-0.1.0/src/heartofgold/web/static/vendor/mermaid.min.js +2314 -0
- heartofgold-0.1.0/src/heartofgold/web/templates/_board.html +41 -0
- heartofgold-0.1.0/src/heartofgold/web/templates/_task_body.html +104 -0
- heartofgold-0.1.0/src/heartofgold/web/templates/_task_live.html +50 -0
- heartofgold-0.1.0/src/heartofgold/web/templates/_task_panel.html +4 -0
- heartofgold-0.1.0/src/heartofgold/web/templates/admin.html +79 -0
- heartofgold-0.1.0/src/heartofgold/web/templates/base.html +33 -0
- heartofgold-0.1.0/src/heartofgold/web/templates/board.html +29 -0
- heartofgold-0.1.0/src/heartofgold/web/templates/consent.html +22 -0
- heartofgold-0.1.0/src/heartofgold/web/templates/error.html +9 -0
- heartofgold-0.1.0/src/heartofgold/web/templates/history.html +12 -0
- heartofgold-0.1.0/src/heartofgold/web/templates/login.html +12 -0
- heartofgold-0.1.0/src/heartofgold/web/templates/macros.html +21 -0
- heartofgold-0.1.0/src/heartofgold/web/templates/plan.html +22 -0
- heartofgold-0.1.0/src/heartofgold/web/templates/projects.html +29 -0
- heartofgold-0.1.0/src/heartofgold/web/templates/revision.html +18 -0
- heartofgold-0.1.0/src/heartofgold/web/templates/settings.html +46 -0
- heartofgold-0.1.0/src/heartofgold/web/templates/task.html +3 -0
- heartofgold-0.1.0/src/heartofgold/web/templates/waiting.html +35 -0
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
MIT License
|
|
2
|
+
|
|
3
|
+
Copyright (c) 2026 Daniel Takacs
|
|
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.
|
|
@@ -0,0 +1,167 @@
|
|
|
1
|
+
Metadata-Version: 2.4
|
|
2
|
+
Name: heartofgold
|
|
3
|
+
Version: 0.1.0
|
|
4
|
+
Summary: A small self-hosted kanban board shared by humans and AI agents, with REST and MCP access.
|
|
5
|
+
Author: Daniel Takacs
|
|
6
|
+
License-Expression: MIT
|
|
7
|
+
License-File: LICENSE
|
|
8
|
+
Requires-Dist: fastapi>=0.115
|
|
9
|
+
Requires-Dist: uvicorn>=0.30
|
|
10
|
+
Requires-Dist: pydantic>=2.7
|
|
11
|
+
Requires-Dist: pydantic-settings>=2.4
|
|
12
|
+
Requires-Dist: platformdirs>=4.2
|
|
13
|
+
Requires-Dist: typer>=0.12
|
|
14
|
+
Requires-Dist: sqlalchemy>=2.0
|
|
15
|
+
Requires-Dist: markdown-it-py>=3
|
|
16
|
+
Requires-Dist: mdit-py-plugins>=0.4
|
|
17
|
+
Requires-Dist: linkify-it-py>=2
|
|
18
|
+
Requires-Dist: nh3>=0.2.17
|
|
19
|
+
Requires-Dist: pillow>=10
|
|
20
|
+
Requires-Dist: argon2-cffi>=23
|
|
21
|
+
Requires-Dist: python-multipart>=0.0.9
|
|
22
|
+
Requires-Dist: mcp>=2.3,<3
|
|
23
|
+
Requires-Dist: jinja2>=3.1
|
|
24
|
+
Requires-Dist: itsdangerous>=2.2
|
|
25
|
+
Requires-Python: >=3.12
|
|
26
|
+
Description-Content-Type: text/markdown
|
|
27
|
+
|
|
28
|
+
# HeartOfGold
|
|
29
|
+
|
|
30
|
+
A small, self-hosted kanban board shared by you and your AI agents. You use the web UI; agents
|
|
31
|
+
(local Claude Code, Claude Code cloud sessions, claude.ai chat, Desktop, mobile and Cowork) use a
|
|
32
|
+
REST API and a remote MCP server. They hand work to each other, ask you questions, and you approve
|
|
33
|
+
and sign off. One Python package, one SQLite file, no other services.
|
|
34
|
+
|
|
35
|
+
- Projects with a Markdown plan and ordered columns; Draft → Ready → In Progress → Review → Done by
|
|
36
|
+
default, where Ready and Done are human-gated and Review/Done need a written resolution.
|
|
37
|
+
- Tasks with Markdown descriptions, comments with @mentions, dependencies, attachments (images,
|
|
38
|
+
PDFs, HTML reports, text), Mermaid diagrams and full revision history with one-click restore.
|
|
39
|
+
- Every agent has its own identity and inbox; parallel sessions claim tasks so they never collide.
|
|
40
|
+
- Reachable from anywhere at zero cost through Tailscale Funnel on the free plan.
|
|
41
|
+
|
|
42
|
+
Docs: [deployment](https://github.com/takacsd/HeartOfGold/blob/main/docs/deployment.md) ·
|
|
43
|
+
[client setup](https://github.com/takacsd/HeartOfGold/blob/main/docs/client-setup.md) ·
|
|
44
|
+
[agent conventions](https://github.com/takacsd/HeartOfGold/blob/main/docs/agent-conventions.md) ·
|
|
45
|
+
[spec](https://github.com/takacsd/HeartOfGold/blob/main/docs/spec.md)
|
|
46
|
+
|
|
47
|
+
## Quick start (about 10 minutes)
|
|
48
|
+
|
|
49
|
+
You need a machine that stays on (Linux, macOS or a Raspberry Pi with a 64-bit OS) and a free
|
|
50
|
+
[Tailscale](https://tailscale.com) account.
|
|
51
|
+
|
|
52
|
+
**1. Install uv and HeartOfGold.** uv also fetches Python 3.12 if your system has none.
|
|
53
|
+
|
|
54
|
+
```bash
|
|
55
|
+
curl -LsSf https://astral.sh/uv/install.sh | sh # see https://docs.astral.sh/uv/ for other ways
|
|
56
|
+
uv tool install heartofgold # or, before the first PyPI release:
|
|
57
|
+
# uv tool install git+https://github.com/takacsd/HeartOfGold
|
|
58
|
+
uv tool update-shell # puts ~/.local/bin on PATH; open a new shell
|
|
59
|
+
```
|
|
60
|
+
|
|
61
|
+
(`pipx install heartofgold` works too if you prefer pipx; it needs Python 3.12 or newer.)
|
|
62
|
+
|
|
63
|
+
**2. Create your account and an agent token.**
|
|
64
|
+
|
|
65
|
+
```bash
|
|
66
|
+
heartofgold init # data dir, secret, your owner account
|
|
67
|
+
heartofgold create-agent laptop
|
|
68
|
+
heartofgold issue-token laptop --name "laptop claude code" # prints hog_… once: keep it
|
|
69
|
+
```
|
|
70
|
+
|
|
71
|
+
**3. Put it on the internet with Tailscale Funnel.** Install Tailscale 1.52 or newer from
|
|
72
|
+
[tailscale.com/download](https://tailscale.com/download) (distribution packages are often older;
|
|
73
|
+
on macOS use the open-source `tailscaled` variant; keep MagicDNS on), log in, then:
|
|
74
|
+
|
|
75
|
+
```bash
|
|
76
|
+
sudo tailscale funnel --bg 8000 # approve the prompt that enables HTTPS and Funnel
|
|
77
|
+
tailscale funnel status # shows your URL, e.g. https://heartofgold.tail1234.ts.net
|
|
78
|
+
```
|
|
79
|
+
|
|
80
|
+
The public DNS name can take up to 10 minutes to appear the first time.
|
|
81
|
+
|
|
82
|
+
**4. Tell HeartOfGold its public URL and run it as a service.** The board only accepts MCP requests
|
|
83
|
+
for the host in `HEARTOFGOLD_BASE_URL`, so this step is required.
|
|
84
|
+
|
|
85
|
+
```bash
|
|
86
|
+
mkdir -p ~/.config/heartofgold
|
|
87
|
+
echo "HEARTOFGOLD_BASE_URL=https://heartofgold.tail1234.ts.net" > ~/.config/heartofgold/env
|
|
88
|
+
|
|
89
|
+
# Linux (systemd user service)
|
|
90
|
+
curl -fsSLo ~/.config/systemd/user/heartofgold.service --create-dirs \
|
|
91
|
+
https://raw.githubusercontent.com/takacsd/HeartOfGold/main/examples/heartofgold.service
|
|
92
|
+
systemctl --user daemon-reload && systemctl --user enable --now heartofgold
|
|
93
|
+
loginctl enable-linger "$USER" # keep it running when you are logged out
|
|
94
|
+
```
|
|
95
|
+
|
|
96
|
+
On macOS use [`examples/com.heartofgold.plist`](https://github.com/takacsd/HeartOfGold/blob/main/examples/com.heartofgold.plist)
|
|
97
|
+
with `launchctl`. On Windows, run `heartofgold serve` in a terminal or from Task Scheduler (not
|
|
98
|
+
tested). To just try it: `uvx heartofgold serve` (with the same environment variable exported).
|
|
99
|
+
|
|
100
|
+
**5. Log in** at your `https://…ts.net` URL (not `http://127.0.0.1`: with an https base URL the
|
|
101
|
+
session cookie is HTTPS-only). Then connect your agents: see
|
|
102
|
+
[client setup](https://github.com/takacsd/HeartOfGold/blob/main/docs/client-setup.md). For Claude Code
|
|
103
|
+
on this machine:
|
|
104
|
+
|
|
105
|
+
```bash
|
|
106
|
+
export HEARTOFGOLD_TOKEN=hog_… # also add this line to your shell profile
|
|
107
|
+
claude mcp add --transport http --scope user heartofgold https://heartofgold.tail1234.ts.net/mcp \
|
|
108
|
+
--header "Authorization: Bearer ${HEARTOFGOLD_TOKEN}"
|
|
109
|
+
claude mcp get heartofgold # check the header is not just "Bearer "
|
|
110
|
+
```
|
|
111
|
+
|
|
112
|
+
Paste [`docs/agent-conventions.md`](https://github.com/takacsd/HeartOfGold/blob/main/docs/agent-conventions.md)
|
|
113
|
+
into your `CLAUDE.md`, open Claude Code and ask "what's in my HeartOfGold inbox?".
|
|
114
|
+
|
|
115
|
+
## Everyday operations
|
|
116
|
+
|
|
117
|
+
- **CLI** (runs on the server, acts as admin): `init`, `serve`, `create-agent <handle> [--admin]`,
|
|
118
|
+
`issue-token <handle> --name <name>`, `revoke-token <prefix>`, `set-role <handle> admin|member`,
|
|
119
|
+
`reset-password <handle>`, `backup [--to PATH]`. If your env file sets anything besides the base
|
|
120
|
+
URL (such as `HEARTOFGOLD_DATA_DIR`), load it first: `set -a; . ~/.config/heartofgold/env; set +a`.
|
|
121
|
+
- **Undoing mistakes**: every text change has a History with diff and Restore; deleted comments and
|
|
122
|
+
attachments come back from Admin. Backups are for disasters.
|
|
123
|
+
- **Backups**: `heartofgold backup` writes a consistent snapshot, new attachment files and the
|
|
124
|
+
secret key to `<data dir>/backups` (or `--to` another disk) and keeps 14 snapshots. Schedule it
|
|
125
|
+
with [the backup timer](https://github.com/takacsd/HeartOfGold/blob/main/examples/heartofgold-backup.timer)
|
|
126
|
+
or cron. Restoring is described in
|
|
127
|
+
[deployment](https://github.com/takacsd/HeartOfGold/blob/main/docs/deployment.md#restore).
|
|
128
|
+
- **Upgrades**: `uv tool upgrade heartofgold` (or `pipx upgrade heartofgold`), then
|
|
129
|
+
`systemctl --user restart heartofgold`. Database migrations run at startup.
|
|
130
|
+
|
|
131
|
+
## Configuration
|
|
132
|
+
|
|
133
|
+
All settings are environment variables; all have defaults. Invalid values stop startup with a
|
|
134
|
+
message naming the variable.
|
|
135
|
+
|
|
136
|
+
| Variable | Default | Purpose |
|
|
137
|
+
| --- | --- | --- |
|
|
138
|
+
| `HEARTOFGOLD_BASE_URL` | `http://127.0.0.1:<port>` | Public URL without a path, e.g. `https://heartofgold.tail1234.ts.net` (may include `:8443`) |
|
|
139
|
+
| `HEARTOFGOLD_PORT` | `8000` | Listen port (the server binds to 127.0.0.1; `serve --host` changes that) |
|
|
140
|
+
| `HEARTOFGOLD_DATA_DIR` | platform user data dir (`~/.local/share/heartofgold` on Linux) | Database, `files/`, `backups/`, generated secret |
|
|
141
|
+
| `HEARTOFGOLD_SECRET_KEY` | generated on first run (stored with mode 0600) | Signs session cookies and CSRF tokens |
|
|
142
|
+
| `HEARTOFGOLD_RATE_LIMIT` | `120/minute` | Per credential; `0` disables |
|
|
143
|
+
| `HEARTOFGOLD_MAX_UPLOAD_MB` | `10` | Per-file REST upload limit (MCP uploads: 256 KB) |
|
|
144
|
+
| `HEARTOFGOLD_MAX_STORAGE_MB` | `2048` | Total attachment storage |
|
|
145
|
+
| `HEARTOFGOLD_LOG_LEVEL` | `info` | JSON log lines on stdout |
|
|
146
|
+
|
|
147
|
+
## API
|
|
148
|
+
|
|
149
|
+
- REST: `/api/v1` with `Authorization: Bearer hog_…`; interactive docs at `/api/v1/docs`; writes
|
|
150
|
+
accept an `X-Session-Label` header.
|
|
151
|
+
- MCP: `/mcp` (Streamable HTTP, stateless). Tools: whoami, projects, tasks, comments, questions,
|
|
152
|
+
claims, inbox, waiting, history, search, attachments.
|
|
153
|
+
|
|
154
|
+
## Development
|
|
155
|
+
|
|
156
|
+
```bash
|
|
157
|
+
uv sync
|
|
158
|
+
uv run pytest
|
|
159
|
+
uv run ruff check . && uv run ruff format --check . && uv run mypy
|
|
160
|
+
uv run heartofgold serve
|
|
161
|
+
```
|
|
162
|
+
|
|
163
|
+
Decisions taken while building: [`docs/DECISIONS.md`](https://github.com/takacsd/HeartOfGold/blob/main/docs/DECISIONS.md).
|
|
164
|
+
|
|
165
|
+
## License
|
|
166
|
+
|
|
167
|
+
MIT
|
|
@@ -0,0 +1,140 @@
|
|
|
1
|
+
# HeartOfGold
|
|
2
|
+
|
|
3
|
+
A small, self-hosted kanban board shared by you and your AI agents. You use the web UI; agents
|
|
4
|
+
(local Claude Code, Claude Code cloud sessions, claude.ai chat, Desktop, mobile and Cowork) use a
|
|
5
|
+
REST API and a remote MCP server. They hand work to each other, ask you questions, and you approve
|
|
6
|
+
and sign off. One Python package, one SQLite file, no other services.
|
|
7
|
+
|
|
8
|
+
- Projects with a Markdown plan and ordered columns; Draft → Ready → In Progress → Review → Done by
|
|
9
|
+
default, where Ready and Done are human-gated and Review/Done need a written resolution.
|
|
10
|
+
- Tasks with Markdown descriptions, comments with @mentions, dependencies, attachments (images,
|
|
11
|
+
PDFs, HTML reports, text), Mermaid diagrams and full revision history with one-click restore.
|
|
12
|
+
- Every agent has its own identity and inbox; parallel sessions claim tasks so they never collide.
|
|
13
|
+
- Reachable from anywhere at zero cost through Tailscale Funnel on the free plan.
|
|
14
|
+
|
|
15
|
+
Docs: [deployment](https://github.com/takacsd/HeartOfGold/blob/main/docs/deployment.md) ·
|
|
16
|
+
[client setup](https://github.com/takacsd/HeartOfGold/blob/main/docs/client-setup.md) ·
|
|
17
|
+
[agent conventions](https://github.com/takacsd/HeartOfGold/blob/main/docs/agent-conventions.md) ·
|
|
18
|
+
[spec](https://github.com/takacsd/HeartOfGold/blob/main/docs/spec.md)
|
|
19
|
+
|
|
20
|
+
## Quick start (about 10 minutes)
|
|
21
|
+
|
|
22
|
+
You need a machine that stays on (Linux, macOS or a Raspberry Pi with a 64-bit OS) and a free
|
|
23
|
+
[Tailscale](https://tailscale.com) account.
|
|
24
|
+
|
|
25
|
+
**1. Install uv and HeartOfGold.** uv also fetches Python 3.12 if your system has none.
|
|
26
|
+
|
|
27
|
+
```bash
|
|
28
|
+
curl -LsSf https://astral.sh/uv/install.sh | sh # see https://docs.astral.sh/uv/ for other ways
|
|
29
|
+
uv tool install heartofgold # or, before the first PyPI release:
|
|
30
|
+
# uv tool install git+https://github.com/takacsd/HeartOfGold
|
|
31
|
+
uv tool update-shell # puts ~/.local/bin on PATH; open a new shell
|
|
32
|
+
```
|
|
33
|
+
|
|
34
|
+
(`pipx install heartofgold` works too if you prefer pipx; it needs Python 3.12 or newer.)
|
|
35
|
+
|
|
36
|
+
**2. Create your account and an agent token.**
|
|
37
|
+
|
|
38
|
+
```bash
|
|
39
|
+
heartofgold init # data dir, secret, your owner account
|
|
40
|
+
heartofgold create-agent laptop
|
|
41
|
+
heartofgold issue-token laptop --name "laptop claude code" # prints hog_… once: keep it
|
|
42
|
+
```
|
|
43
|
+
|
|
44
|
+
**3. Put it on the internet with Tailscale Funnel.** Install Tailscale 1.52 or newer from
|
|
45
|
+
[tailscale.com/download](https://tailscale.com/download) (distribution packages are often older;
|
|
46
|
+
on macOS use the open-source `tailscaled` variant; keep MagicDNS on), log in, then:
|
|
47
|
+
|
|
48
|
+
```bash
|
|
49
|
+
sudo tailscale funnel --bg 8000 # approve the prompt that enables HTTPS and Funnel
|
|
50
|
+
tailscale funnel status # shows your URL, e.g. https://heartofgold.tail1234.ts.net
|
|
51
|
+
```
|
|
52
|
+
|
|
53
|
+
The public DNS name can take up to 10 minutes to appear the first time.
|
|
54
|
+
|
|
55
|
+
**4. Tell HeartOfGold its public URL and run it as a service.** The board only accepts MCP requests
|
|
56
|
+
for the host in `HEARTOFGOLD_BASE_URL`, so this step is required.
|
|
57
|
+
|
|
58
|
+
```bash
|
|
59
|
+
mkdir -p ~/.config/heartofgold
|
|
60
|
+
echo "HEARTOFGOLD_BASE_URL=https://heartofgold.tail1234.ts.net" > ~/.config/heartofgold/env
|
|
61
|
+
|
|
62
|
+
# Linux (systemd user service)
|
|
63
|
+
curl -fsSLo ~/.config/systemd/user/heartofgold.service --create-dirs \
|
|
64
|
+
https://raw.githubusercontent.com/takacsd/HeartOfGold/main/examples/heartofgold.service
|
|
65
|
+
systemctl --user daemon-reload && systemctl --user enable --now heartofgold
|
|
66
|
+
loginctl enable-linger "$USER" # keep it running when you are logged out
|
|
67
|
+
```
|
|
68
|
+
|
|
69
|
+
On macOS use [`examples/com.heartofgold.plist`](https://github.com/takacsd/HeartOfGold/blob/main/examples/com.heartofgold.plist)
|
|
70
|
+
with `launchctl`. On Windows, run `heartofgold serve` in a terminal or from Task Scheduler (not
|
|
71
|
+
tested). To just try it: `uvx heartofgold serve` (with the same environment variable exported).
|
|
72
|
+
|
|
73
|
+
**5. Log in** at your `https://…ts.net` URL (not `http://127.0.0.1`: with an https base URL the
|
|
74
|
+
session cookie is HTTPS-only). Then connect your agents: see
|
|
75
|
+
[client setup](https://github.com/takacsd/HeartOfGold/blob/main/docs/client-setup.md). For Claude Code
|
|
76
|
+
on this machine:
|
|
77
|
+
|
|
78
|
+
```bash
|
|
79
|
+
export HEARTOFGOLD_TOKEN=hog_… # also add this line to your shell profile
|
|
80
|
+
claude mcp add --transport http --scope user heartofgold https://heartofgold.tail1234.ts.net/mcp \
|
|
81
|
+
--header "Authorization: Bearer ${HEARTOFGOLD_TOKEN}"
|
|
82
|
+
claude mcp get heartofgold # check the header is not just "Bearer "
|
|
83
|
+
```
|
|
84
|
+
|
|
85
|
+
Paste [`docs/agent-conventions.md`](https://github.com/takacsd/HeartOfGold/blob/main/docs/agent-conventions.md)
|
|
86
|
+
into your `CLAUDE.md`, open Claude Code and ask "what's in my HeartOfGold inbox?".
|
|
87
|
+
|
|
88
|
+
## Everyday operations
|
|
89
|
+
|
|
90
|
+
- **CLI** (runs on the server, acts as admin): `init`, `serve`, `create-agent <handle> [--admin]`,
|
|
91
|
+
`issue-token <handle> --name <name>`, `revoke-token <prefix>`, `set-role <handle> admin|member`,
|
|
92
|
+
`reset-password <handle>`, `backup [--to PATH]`. If your env file sets anything besides the base
|
|
93
|
+
URL (such as `HEARTOFGOLD_DATA_DIR`), load it first: `set -a; . ~/.config/heartofgold/env; set +a`.
|
|
94
|
+
- **Undoing mistakes**: every text change has a History with diff and Restore; deleted comments and
|
|
95
|
+
attachments come back from Admin. Backups are for disasters.
|
|
96
|
+
- **Backups**: `heartofgold backup` writes a consistent snapshot, new attachment files and the
|
|
97
|
+
secret key to `<data dir>/backups` (or `--to` another disk) and keeps 14 snapshots. Schedule it
|
|
98
|
+
with [the backup timer](https://github.com/takacsd/HeartOfGold/blob/main/examples/heartofgold-backup.timer)
|
|
99
|
+
or cron. Restoring is described in
|
|
100
|
+
[deployment](https://github.com/takacsd/HeartOfGold/blob/main/docs/deployment.md#restore).
|
|
101
|
+
- **Upgrades**: `uv tool upgrade heartofgold` (or `pipx upgrade heartofgold`), then
|
|
102
|
+
`systemctl --user restart heartofgold`. Database migrations run at startup.
|
|
103
|
+
|
|
104
|
+
## Configuration
|
|
105
|
+
|
|
106
|
+
All settings are environment variables; all have defaults. Invalid values stop startup with a
|
|
107
|
+
message naming the variable.
|
|
108
|
+
|
|
109
|
+
| Variable | Default | Purpose |
|
|
110
|
+
| --- | --- | --- |
|
|
111
|
+
| `HEARTOFGOLD_BASE_URL` | `http://127.0.0.1:<port>` | Public URL without a path, e.g. `https://heartofgold.tail1234.ts.net` (may include `:8443`) |
|
|
112
|
+
| `HEARTOFGOLD_PORT` | `8000` | Listen port (the server binds to 127.0.0.1; `serve --host` changes that) |
|
|
113
|
+
| `HEARTOFGOLD_DATA_DIR` | platform user data dir (`~/.local/share/heartofgold` on Linux) | Database, `files/`, `backups/`, generated secret |
|
|
114
|
+
| `HEARTOFGOLD_SECRET_KEY` | generated on first run (stored with mode 0600) | Signs session cookies and CSRF tokens |
|
|
115
|
+
| `HEARTOFGOLD_RATE_LIMIT` | `120/minute` | Per credential; `0` disables |
|
|
116
|
+
| `HEARTOFGOLD_MAX_UPLOAD_MB` | `10` | Per-file REST upload limit (MCP uploads: 256 KB) |
|
|
117
|
+
| `HEARTOFGOLD_MAX_STORAGE_MB` | `2048` | Total attachment storage |
|
|
118
|
+
| `HEARTOFGOLD_LOG_LEVEL` | `info` | JSON log lines on stdout |
|
|
119
|
+
|
|
120
|
+
## API
|
|
121
|
+
|
|
122
|
+
- REST: `/api/v1` with `Authorization: Bearer hog_…`; interactive docs at `/api/v1/docs`; writes
|
|
123
|
+
accept an `X-Session-Label` header.
|
|
124
|
+
- MCP: `/mcp` (Streamable HTTP, stateless). Tools: whoami, projects, tasks, comments, questions,
|
|
125
|
+
claims, inbox, waiting, history, search, attachments.
|
|
126
|
+
|
|
127
|
+
## Development
|
|
128
|
+
|
|
129
|
+
```bash
|
|
130
|
+
uv sync
|
|
131
|
+
uv run pytest
|
|
132
|
+
uv run ruff check . && uv run ruff format --check . && uv run mypy
|
|
133
|
+
uv run heartofgold serve
|
|
134
|
+
```
|
|
135
|
+
|
|
136
|
+
Decisions taken while building: [`docs/DECISIONS.md`](https://github.com/takacsd/HeartOfGold/blob/main/docs/DECISIONS.md).
|
|
137
|
+
|
|
138
|
+
## License
|
|
139
|
+
|
|
140
|
+
MIT
|
|
@@ -0,0 +1,71 @@
|
|
|
1
|
+
[project]
|
|
2
|
+
name = "heartofgold"
|
|
3
|
+
version = "0.1.0"
|
|
4
|
+
description = "A small self-hosted kanban board shared by humans and AI agents, with REST and MCP access."
|
|
5
|
+
readme = "README.md"
|
|
6
|
+
license = "MIT"
|
|
7
|
+
license-files = ["LICENSE"]
|
|
8
|
+
requires-python = ">=3.12"
|
|
9
|
+
dependencies = [
|
|
10
|
+
"fastapi>=0.115",
|
|
11
|
+
"uvicorn>=0.30",
|
|
12
|
+
"pydantic>=2.7",
|
|
13
|
+
"pydantic-settings>=2.4",
|
|
14
|
+
"platformdirs>=4.2",
|
|
15
|
+
"typer>=0.12",
|
|
16
|
+
"sqlalchemy>=2.0",
|
|
17
|
+
"markdown-it-py>=3",
|
|
18
|
+
"mdit-py-plugins>=0.4",
|
|
19
|
+
"linkify-it-py>=2",
|
|
20
|
+
"nh3>=0.2.17",
|
|
21
|
+
"pillow>=10",
|
|
22
|
+
"argon2-cffi>=23",
|
|
23
|
+
"python-multipart>=0.0.9",
|
|
24
|
+
"mcp>=2.3,<3",
|
|
25
|
+
"jinja2>=3.1",
|
|
26
|
+
"itsdangerous>=2.2",
|
|
27
|
+
]
|
|
28
|
+
|
|
29
|
+
[[project.authors]]
|
|
30
|
+
name = "Daniel Takacs"
|
|
31
|
+
|
|
32
|
+
[project.scripts]
|
|
33
|
+
heartofgold = "heartofgold.cli:app"
|
|
34
|
+
|
|
35
|
+
[dependency-groups]
|
|
36
|
+
dev = [
|
|
37
|
+
"pytest>=8",
|
|
38
|
+
"pytest-cov>=5",
|
|
39
|
+
"httpx>=0.27",
|
|
40
|
+
"ruff>=0.6",
|
|
41
|
+
"mypy>=1.11",
|
|
42
|
+
]
|
|
43
|
+
|
|
44
|
+
[build-system]
|
|
45
|
+
requires = ["uv_build>=0.13.1,<0.14"]
|
|
46
|
+
build-backend = "uv_build"
|
|
47
|
+
|
|
48
|
+
[tool.ruff]
|
|
49
|
+
line-length = 100
|
|
50
|
+
target-version = "py312"
|
|
51
|
+
|
|
52
|
+
[tool.ruff.lint]
|
|
53
|
+
select = [
|
|
54
|
+
"E",
|
|
55
|
+
"F",
|
|
56
|
+
"I",
|
|
57
|
+
"UP",
|
|
58
|
+
"B",
|
|
59
|
+
"SIM",
|
|
60
|
+
"RUF",
|
|
61
|
+
"A",
|
|
62
|
+
]
|
|
63
|
+
|
|
64
|
+
[tool.mypy]
|
|
65
|
+
strict = true
|
|
66
|
+
files = ["src/heartofgold"]
|
|
67
|
+
|
|
68
|
+
[tool.pytest.ini_options]
|
|
69
|
+
testpaths = ["tests"]
|
|
70
|
+
addopts = "-q"
|
|
71
|
+
filterwarnings = ["ignore:Using `httpx` with `starlette.testclient` is deprecated"]
|
|
@@ -0,0 +1,60 @@
|
|
|
1
|
+
[project]
|
|
2
|
+
name = "heartofgold"
|
|
3
|
+
version = "0.1.0"
|
|
4
|
+
description = "A small self-hosted kanban board shared by humans and AI agents, with REST and MCP access."
|
|
5
|
+
readme = "README.md"
|
|
6
|
+
license = "MIT"
|
|
7
|
+
license-files = ["LICENSE"]
|
|
8
|
+
authors = [{ name = "Daniel Takacs" }]
|
|
9
|
+
requires-python = ">=3.12"
|
|
10
|
+
dependencies = [
|
|
11
|
+
"fastapi>=0.115",
|
|
12
|
+
"uvicorn>=0.30",
|
|
13
|
+
"pydantic>=2.7",
|
|
14
|
+
"pydantic-settings>=2.4",
|
|
15
|
+
"platformdirs>=4.2",
|
|
16
|
+
"typer>=0.12",
|
|
17
|
+
"sqlalchemy>=2.0",
|
|
18
|
+
"markdown-it-py>=3",
|
|
19
|
+
"mdit-py-plugins>=0.4",
|
|
20
|
+
"linkify-it-py>=2",
|
|
21
|
+
"nh3>=0.2.17",
|
|
22
|
+
"pillow>=10",
|
|
23
|
+
"argon2-cffi>=23",
|
|
24
|
+
"python-multipart>=0.0.9",
|
|
25
|
+
"mcp>=2.3,<3",
|
|
26
|
+
"jinja2>=3.1",
|
|
27
|
+
"itsdangerous>=2.2",
|
|
28
|
+
]
|
|
29
|
+
|
|
30
|
+
[project.scripts]
|
|
31
|
+
heartofgold = "heartofgold.cli:app"
|
|
32
|
+
|
|
33
|
+
[dependency-groups]
|
|
34
|
+
dev = [
|
|
35
|
+
"pytest>=8",
|
|
36
|
+
"pytest-cov>=5",
|
|
37
|
+
"httpx>=0.27",
|
|
38
|
+
"ruff>=0.6",
|
|
39
|
+
"mypy>=1.11",
|
|
40
|
+
]
|
|
41
|
+
|
|
42
|
+
[build-system]
|
|
43
|
+
requires = ["uv_build>=0.13.1,<0.14"]
|
|
44
|
+
build-backend = "uv_build"
|
|
45
|
+
|
|
46
|
+
[tool.ruff]
|
|
47
|
+
line-length = 100
|
|
48
|
+
target-version = "py312"
|
|
49
|
+
|
|
50
|
+
[tool.ruff.lint]
|
|
51
|
+
select = ["E", "F", "I", "UP", "B", "SIM", "RUF", "A"]
|
|
52
|
+
|
|
53
|
+
[tool.mypy]
|
|
54
|
+
strict = true
|
|
55
|
+
files = ["src/heartofgold"]
|
|
56
|
+
|
|
57
|
+
[tool.pytest.ini_options]
|
|
58
|
+
testpaths = ["tests"]
|
|
59
|
+
addopts = "-q"
|
|
60
|
+
filterwarnings = ["ignore:Using `httpx` with `starlette.testclient` is deprecated"]
|
|
File without changes
|
|
@@ -0,0 +1,92 @@
|
|
|
1
|
+
"""RFC 9457 problem+json responses for every error (section 6)."""
|
|
2
|
+
|
|
3
|
+
from collections.abc import Mapping
|
|
4
|
+
from http import HTTPStatus
|
|
5
|
+
|
|
6
|
+
from fastapi import FastAPI, Request
|
|
7
|
+
from fastapi.exceptions import RequestValidationError
|
|
8
|
+
from fastapi.responses import JSONResponse, Response
|
|
9
|
+
from starlette.exceptions import HTTPException
|
|
10
|
+
|
|
11
|
+
from heartofgold.services.common import ServiceError
|
|
12
|
+
|
|
13
|
+
_CODES = {
|
|
14
|
+
400: "invalid_request",
|
|
15
|
+
401: "unauthenticated",
|
|
16
|
+
403: "forbidden",
|
|
17
|
+
404: "not_found",
|
|
18
|
+
405: "invalid_request",
|
|
19
|
+
413: "too_large",
|
|
20
|
+
422: "validation_error",
|
|
21
|
+
429: "rate_limited",
|
|
22
|
+
}
|
|
23
|
+
|
|
24
|
+
|
|
25
|
+
_API_PREFIXES = ("/api/", "/files/", "/mcp", "/.well-known/", "/healthz")
|
|
26
|
+
|
|
27
|
+
|
|
28
|
+
def _respond(request: Request, status: int, code: str, detail: str, **kw: object) -> Response:
|
|
29
|
+
"""problem+json for API clients; a small HTML page for the browser UI."""
|
|
30
|
+
if not request.url.path.startswith(_API_PREFIXES):
|
|
31
|
+
from heartofgold.web.routes import error_page
|
|
32
|
+
|
|
33
|
+
return error_page(request, status, detail)
|
|
34
|
+
return problem(status, code, detail, **kw) # type: ignore[arg-type]
|
|
35
|
+
|
|
36
|
+
|
|
37
|
+
def problem(
|
|
38
|
+
status: int,
|
|
39
|
+
code: str,
|
|
40
|
+
detail: str,
|
|
41
|
+
headers: Mapping[str, str] | None = None,
|
|
42
|
+
extra: Mapping[str, object] | None = None,
|
|
43
|
+
) -> JSONResponse:
|
|
44
|
+
body = {
|
|
45
|
+
"type": "about:blank",
|
|
46
|
+
"title": HTTPStatus(status).phrase,
|
|
47
|
+
"status": status,
|
|
48
|
+
"detail": detail,
|
|
49
|
+
"code": code,
|
|
50
|
+
**(extra or {}),
|
|
51
|
+
}
|
|
52
|
+
return JSONResponse(body, status, headers=headers, media_type="application/problem+json")
|
|
53
|
+
|
|
54
|
+
|
|
55
|
+
def _service_error(request: Request, exc: Exception) -> Response:
|
|
56
|
+
assert isinstance(exc, ServiceError)
|
|
57
|
+
return _respond(request, exc.status, exc.code, exc.detail, extra=exc.extra)
|
|
58
|
+
|
|
59
|
+
|
|
60
|
+
def _http_error(request: Request, exc: Exception) -> Response:
|
|
61
|
+
assert isinstance(exc, HTTPException)
|
|
62
|
+
code = _CODES.get(exc.status_code, "invalid_request")
|
|
63
|
+
detail = exc.detail if isinstance(exc.detail, str) else HTTPStatus(exc.status_code).phrase
|
|
64
|
+
return _respond(request, exc.status_code, code, detail, headers=exc.headers)
|
|
65
|
+
|
|
66
|
+
|
|
67
|
+
def _validation_error(request: Request, exc: Exception) -> JSONResponse:
|
|
68
|
+
assert isinstance(exc, RequestValidationError)
|
|
69
|
+
errors = exc.errors()
|
|
70
|
+
if any(e.get("type") == "json_invalid" for e in errors):
|
|
71
|
+
return problem(400, "invalid_request", "The request body is not valid JSON.")
|
|
72
|
+
parts = []
|
|
73
|
+
for error in errors:
|
|
74
|
+
loc = ".".join(str(p) for p in error["loc"] if p not in ("body", "query", "path"))
|
|
75
|
+
parts.append(f"{loc or 'body'}: {error['msg']}")
|
|
76
|
+
return problem(422, "validation_error", "; ".join(parts))
|
|
77
|
+
|
|
78
|
+
|
|
79
|
+
def _unexpected(request: Request, exc: Exception) -> JSONResponse:
|
|
80
|
+
return problem(500, "internal_error", "Unexpected server error; see the server log.")
|
|
81
|
+
|
|
82
|
+
|
|
83
|
+
def _overflow(request: Request, exc: Exception) -> JSONResponse:
|
|
84
|
+
return problem(422, "validation_error", "A number in the request is out of range.")
|
|
85
|
+
|
|
86
|
+
|
|
87
|
+
def install(app: FastAPI) -> None:
|
|
88
|
+
app.add_exception_handler(Exception, _unexpected)
|
|
89
|
+
app.add_exception_handler(OverflowError, _overflow)
|
|
90
|
+
app.add_exception_handler(ServiceError, _service_error)
|
|
91
|
+
app.add_exception_handler(HTTPException, _http_error)
|
|
92
|
+
app.add_exception_handler(RequestValidationError, _validation_error)
|
|
@@ -0,0 +1,85 @@
|
|
|
1
|
+
"""Attachment upload, listing, delete/restore and download (FR-17..FR-23, section 8)."""
|
|
2
|
+
|
|
3
|
+
import os
|
|
4
|
+
from typing import Annotated
|
|
5
|
+
|
|
6
|
+
from fastapi import APIRouter, File, Form, UploadFile
|
|
7
|
+
from fastapi.responses import FileResponse
|
|
8
|
+
|
|
9
|
+
from heartofgold.auth.deps import CallerDep, DbDep, StorageDep
|
|
10
|
+
from heartofgold.schemas import AttachmentOut
|
|
11
|
+
from heartofgold.services import attachments
|
|
12
|
+
from heartofgold.services.common import not_found
|
|
13
|
+
|
|
14
|
+
router = APIRouter()
|
|
15
|
+
|
|
16
|
+
|
|
17
|
+
class BlobResponse(FileResponse):
|
|
18
|
+
"""A file response carrying only the headers section 8 lists (no etag or last-modified)."""
|
|
19
|
+
|
|
20
|
+
def set_stat_headers(self, stat_result: os.stat_result) -> None:
|
|
21
|
+
self.headers.setdefault("content-length", str(stat_result.st_size))
|
|
22
|
+
del self.headers["accept-ranges"]
|
|
23
|
+
|
|
24
|
+
def _should_use_range(self, http_if_range: str) -> bool:
|
|
25
|
+
return False
|
|
26
|
+
|
|
27
|
+
|
|
28
|
+
CACHE = "private, max-age=31536000, immutable"
|
|
29
|
+
SVG_CSP = "sandbox; default-src 'none'; style-src 'unsafe-inline'; img-src data:"
|
|
30
|
+
INLINE = {"image/png", "image/jpeg", "image/gif", "image/webp", "application/pdf"}
|
|
31
|
+
|
|
32
|
+
|
|
33
|
+
@router.post("/api/v1/tasks/{ref}/attachments", status_code=201)
|
|
34
|
+
def upload(
|
|
35
|
+
ref: str,
|
|
36
|
+
file: Annotated[UploadFile, File()],
|
|
37
|
+
caller: CallerDep,
|
|
38
|
+
db: DbDep,
|
|
39
|
+
storage: StorageDep,
|
|
40
|
+
alt_text: Annotated[str | None, Form()] = None,
|
|
41
|
+
) -> AttachmentOut:
|
|
42
|
+
data = file.file.read(storage.max_upload_bytes + 1)
|
|
43
|
+
return attachments.upload(db, caller, storage, ref, file.filename or "file", data, alt_text)
|
|
44
|
+
|
|
45
|
+
|
|
46
|
+
@router.get("/api/v1/tasks/{ref}/attachments")
|
|
47
|
+
def list_attachments(ref: str, caller: CallerDep, db: DbDep) -> list[AttachmentOut]:
|
|
48
|
+
return attachments.list_attachments(db, caller, ref)
|
|
49
|
+
|
|
50
|
+
|
|
51
|
+
@router.delete("/api/v1/attachments/{attachment_id}")
|
|
52
|
+
def delete_attachment(attachment_id: str, caller: CallerDep, db: DbDep) -> AttachmentOut:
|
|
53
|
+
return attachments.set_deleted(db, caller, attachment_id, True)
|
|
54
|
+
|
|
55
|
+
|
|
56
|
+
@router.post("/api/v1/attachments/{attachment_id}/restore")
|
|
57
|
+
def restore_attachment(attachment_id: str, caller: CallerDep, db: DbDep) -> AttachmentOut:
|
|
58
|
+
return attachments.set_deleted(db, caller, attachment_id, False)
|
|
59
|
+
|
|
60
|
+
|
|
61
|
+
@router.get("/files/{attachment_id}/{filename}")
|
|
62
|
+
def download(
|
|
63
|
+
attachment_id: str, filename: str, caller: CallerDep, db: DbDep, storage: StorageDep
|
|
64
|
+
) -> FileResponse:
|
|
65
|
+
meta, path = attachments.open_blob(db, caller, storage, attachment_id)
|
|
66
|
+
if filename != meta.filename:
|
|
67
|
+
raise not_found(f"Attachment {attachment_id} is named {meta.filename}.")
|
|
68
|
+
kind = meta.content_type.split(";")[0]
|
|
69
|
+
headers = {"Cache-Control": CACHE, "X-Content-Type-Options": "nosniff"}
|
|
70
|
+
disposition = "inline"
|
|
71
|
+
if kind == "image/svg+xml":
|
|
72
|
+
headers["Content-Security-Policy"] = SVG_CSP
|
|
73
|
+
elif kind == "text/html":
|
|
74
|
+
headers["Content-Security-Policy"] = "sandbox allow-scripts"
|
|
75
|
+
elif kind not in INLINE:
|
|
76
|
+
headers["Content-Security-Policy"] = "sandbox"
|
|
77
|
+
disposition = "attachment"
|
|
78
|
+
response = BlobResponse(
|
|
79
|
+
path,
|
|
80
|
+
media_type=meta.content_type,
|
|
81
|
+
filename=meta.filename,
|
|
82
|
+
content_disposition_type=disposition,
|
|
83
|
+
headers=headers,
|
|
84
|
+
)
|
|
85
|
+
return response
|