magic-beans 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.
|
@@ -0,0 +1,185 @@
|
|
|
1
|
+
Metadata-Version: 2.4
|
|
2
|
+
Name: magic-beans
|
|
3
|
+
Version: 0.1.0
|
|
4
|
+
Summary: Graph-based issue tracker for AI agent coordination
|
|
5
|
+
Author: Henrique Bastos
|
|
6
|
+
Author-email: Henrique Bastos <henrique@bastos.net>
|
|
7
|
+
License-Expression: MIT
|
|
8
|
+
Requires-Dist: pydantic>=2.0
|
|
9
|
+
Requires-Dist: typer>=0.9
|
|
10
|
+
Requires-Python: >=3.14
|
|
11
|
+
Description-Content-Type: text/markdown
|
|
12
|
+
|
|
13
|
+
# 🫘 Beans
|
|
14
|
+
|
|
15
|
+
**Graph-based issue tracker for AI agent coordination.**
|
|
16
|
+
|
|
17
|
+
Coding with AI makes developers absurdly productive. Coffee keeps them going.
|
|
18
|
+
Beans keep the whole loop fed. ☕
|
|
19
|
+
|
|
20
|
+
Beans is a lightweight, embedded issue tracker designed for AI agents to coordinate work
|
|
21
|
+
across tasks. It models issues as nodes in a dependency graph, backed by SQLite, with a
|
|
22
|
+
CLI that both humans and agents can use.
|
|
23
|
+
|
|
24
|
+
```
|
|
25
|
+
$ beans create "Fix auth middleware"
|
|
26
|
+
bean-a3f2dd1c 2025-06-15 10:42 Fix auth middleware
|
|
27
|
+
|
|
28
|
+
$ beans list
|
|
29
|
+
bean-a3f2dd1c 2025-06-15 10:42 Fix auth middleware
|
|
30
|
+
bean-7e2b9f01 2025-06-15 10:43 Add rate limiting
|
|
31
|
+
|
|
32
|
+
$ beans --json list
|
|
33
|
+
[{"id": "bean-a3f2dd1c", "title": "Fix auth middleware", "status": "open", ...}]
|
|
34
|
+
```
|
|
35
|
+
|
|
36
|
+
## Why Beans exists
|
|
37
|
+
|
|
38
|
+
Beans was born from analyzing [beads](https://github.com/steveyegge/beads), a
|
|
39
|
+
graph-based issue tracker with a similar goal: giving AI agents a structured way to
|
|
40
|
+
coordinate work. The idea is excellent. The execution, however, raised serious concerns.
|
|
41
|
+
|
|
42
|
+
### The problem with beads
|
|
43
|
+
|
|
44
|
+
Beads is invasive software that assumes too much about the user's intentions:
|
|
45
|
+
|
|
46
|
+
- **Curl-pipe-bash installer** that silently escalates to `sudo` to install a ~200MB
|
|
47
|
+
binary system-wide
|
|
48
|
+
- **Strips macOS code signatures** and applies ad-hoc signatures to bypass Gatekeeper —
|
|
49
|
+
the same technique used by actual malware
|
|
50
|
+
- **Installs 5 persistent git hooks** (`pre-commit`, `post-merge`, `pre-push`,
|
|
51
|
+
`post-checkout`, `prepare-commit-msg`) that run on nearly every git operation
|
|
52
|
+
- **Runs a background database daemon** (Dolt SQL server) that binds to TCP ports
|
|
53
|
+
3307/3308 — a MySQL-protocol server running on your machine
|
|
54
|
+
- **Silently modifies commit messages** by appending metadata trailers without asking
|
|
55
|
+
- **Auto-pushes data** to remote servers every 5 minutes
|
|
56
|
+
- **Fingerprints your machine** with unique UUIDs for the repo, clone, and project
|
|
57
|
+
- **Injects system prompts** into AI agent configuration files (e.g.,
|
|
58
|
+
`.claude/settings.local.json`) to force agents to use the tool
|
|
59
|
+
- **Includes a "stealth mode"** that hides its presence from collaborators
|
|
60
|
+
- **Ships with telemetry** that records every command and all arguments, including who
|
|
61
|
+
ran them
|
|
62
|
+
|
|
63
|
+
The core data model is a 50+ field struct covering everything from agent heartbeats to
|
|
64
|
+
ephemeral "wisps." It's not a simple tool — it's a complex system that assumes you want
|
|
65
|
+
all of it.
|
|
66
|
+
|
|
67
|
+
### What beans does differently
|
|
68
|
+
|
|
69
|
+
Beans keeps the good idea — a dependency graph for AI agent coordination — and throws
|
|
70
|
+
away everything else:
|
|
71
|
+
|
|
72
|
+
| | Beads | Beans |
|
|
73
|
+
|---|---|---|
|
|
74
|
+
| **Storage** | Dolt SQL server (~200MB, background daemon, TCP ports) | SQLite (zero-config, embedded, stdlib) |
|
|
75
|
+
| **Installation** | `curl \| bash` + `sudo` + signature stripping | `uv add beans` or `pip install beans` |
|
|
76
|
+
| **Git hooks** | 5 persistent hooks installed silently | None |
|
|
77
|
+
| **Commit modification** | Silent trailer injection | Never touches your commits |
|
|
78
|
+
| **Background processes** | Persistent MySQL-protocol daemon | None |
|
|
79
|
+
| **Network** | Opens TCP ports, auto-pushes every 5 min | Fully offline |
|
|
80
|
+
| **Telemetry** | OTel spans with actor identity + full args | None |
|
|
81
|
+
| **Stealth mode** | Yes, hides from collaborators | No — transparency is a feature |
|
|
82
|
+
| **AI config injection** | Writes to `.claude/settings.local.json` | Provides recipes you copy yourself |
|
|
83
|
+
| **Data model** | 50+ fields | ~10 fields + dependency edges |
|
|
84
|
+
| **Agent integration** | Forced via injected prompts | Opt-in via `AGENTS.md` instructions |
|
|
85
|
+
|
|
86
|
+
## Design principles
|
|
87
|
+
|
|
88
|
+
**Polite software.** Beans never modifies files without asking, never installs hooks,
|
|
89
|
+
never runs background processes, and never phones home. It's a CLI tool that reads and
|
|
90
|
+
writes to a local SQLite file. That's it.
|
|
91
|
+
|
|
92
|
+
**Embedded storage.** A single `.beans/beans.db` SQLite file with WAL mode. No servers,
|
|
93
|
+
no ports, no daemons. Works offline, works in CI, works anywhere Python runs.
|
|
94
|
+
|
|
95
|
+
**Graph-native.** Issues (beans) are nodes. Dependencies are typed edges. "What's ready
|
|
96
|
+
to work on?" is a graph query — show me all open beans with no open blockers.
|
|
97
|
+
|
|
98
|
+
**Agent-friendly.** The `--json` flag on every command makes beans machine-readable.
|
|
99
|
+
Agents don't need to parse human output. The CLI is a thin wrapper around a clean Python
|
|
100
|
+
API — agents can also use the library directly.
|
|
101
|
+
|
|
102
|
+
**Minimal by default.** A bean has an id, title, status, and timestamps. Everything else
|
|
103
|
+
is optional. No 50-field structs, no agent heartbeat tracking, no ephemeral wisps.
|
|
104
|
+
|
|
105
|
+
**Journal-based sync.** Changes are recorded in an append-only JSONL journal that can be
|
|
106
|
+
committed to git. The SQLite database is a materialized view that can be rebuilt from the
|
|
107
|
+
journal at any time. Sync through git, not through custom protocols.
|
|
108
|
+
|
|
109
|
+
## Installation
|
|
110
|
+
|
|
111
|
+
```bash
|
|
112
|
+
uv add beans
|
|
113
|
+
```
|
|
114
|
+
|
|
115
|
+
Or with pip:
|
|
116
|
+
|
|
117
|
+
```bash
|
|
118
|
+
pip install beans
|
|
119
|
+
```
|
|
120
|
+
|
|
121
|
+
## Quick start
|
|
122
|
+
|
|
123
|
+
```bash
|
|
124
|
+
# Create some beans
|
|
125
|
+
beans create "Set up database schema"
|
|
126
|
+
beans create "Build API endpoints"
|
|
127
|
+
beans create "Write integration tests"
|
|
128
|
+
|
|
129
|
+
# List all beans
|
|
130
|
+
beans list
|
|
131
|
+
|
|
132
|
+
# JSON output for agent consumption
|
|
133
|
+
beans --json list
|
|
134
|
+
```
|
|
135
|
+
|
|
136
|
+
## Architecture
|
|
137
|
+
|
|
138
|
+
```
|
|
139
|
+
src/beans/
|
|
140
|
+
├── models.py # Pydantic models (pure, no I/O)
|
|
141
|
+
├── store.py # SQLite storage (I/O boundary)
|
|
142
|
+
└── cli.py # Typer CLI (thin wiring layer)
|
|
143
|
+
```
|
|
144
|
+
|
|
145
|
+
- **models.py** — Pure data. Bean is a Pydantic model with validation. No I/O, no side
|
|
146
|
+
effects, easy to test.
|
|
147
|
+
- **store.py** — The I/O boundary. BeanStore wraps a SQLite connection with
|
|
148
|
+
create/read/update/delete operations. Accepts an injected connection for testing.
|
|
149
|
+
- **cli.py** — Thin wiring. Parses args, calls store methods, formats output. No
|
|
150
|
+
business logic.
|
|
151
|
+
|
|
152
|
+
## For AI agents
|
|
153
|
+
|
|
154
|
+
Beans is designed to be used by AI agents as a coordination mechanism. Add this to your
|
|
155
|
+
project's `AGENTS.md`:
|
|
156
|
+
|
|
157
|
+
```markdown
|
|
158
|
+
## Task tracking
|
|
159
|
+
|
|
160
|
+
This project uses `beans` for task tracking. Use `beans --json` for all commands.
|
|
161
|
+
|
|
162
|
+
- Check available work: `beans --json ready`
|
|
163
|
+
- Claim a task: `beans claim <id>`
|
|
164
|
+
- Mark done: `beans close <id>`
|
|
165
|
+
- Create subtasks: `beans create "<title>" --parent <id>`
|
|
166
|
+
```
|
|
167
|
+
|
|
168
|
+
## License
|
|
169
|
+
|
|
170
|
+
MIT
|
|
171
|
+
|
|
172
|
+
## Contributing
|
|
173
|
+
|
|
174
|
+
Beans is built with Python 3.14, managed with [uv](https://docs.astral.sh/uv/).
|
|
175
|
+
|
|
176
|
+
```bash
|
|
177
|
+
git clone https://github.com/henriquebastos/beans.git
|
|
178
|
+
cd beans
|
|
179
|
+
uv sync
|
|
180
|
+
uv run pytest
|
|
181
|
+
uv run ruff check src/ tests/
|
|
182
|
+
```
|
|
183
|
+
|
|
184
|
+
Tests use real SQLite `:memory:` databases — no mocks. The test suite runs in under a
|
|
185
|
+
second.
|
|
@@ -0,0 +1,173 @@
|
|
|
1
|
+
# 🫘 Beans
|
|
2
|
+
|
|
3
|
+
**Graph-based issue tracker for AI agent coordination.**
|
|
4
|
+
|
|
5
|
+
Coding with AI makes developers absurdly productive. Coffee keeps them going.
|
|
6
|
+
Beans keep the whole loop fed. ☕
|
|
7
|
+
|
|
8
|
+
Beans is a lightweight, embedded issue tracker designed for AI agents to coordinate work
|
|
9
|
+
across tasks. It models issues as nodes in a dependency graph, backed by SQLite, with a
|
|
10
|
+
CLI that both humans and agents can use.
|
|
11
|
+
|
|
12
|
+
```
|
|
13
|
+
$ beans create "Fix auth middleware"
|
|
14
|
+
bean-a3f2dd1c 2025-06-15 10:42 Fix auth middleware
|
|
15
|
+
|
|
16
|
+
$ beans list
|
|
17
|
+
bean-a3f2dd1c 2025-06-15 10:42 Fix auth middleware
|
|
18
|
+
bean-7e2b9f01 2025-06-15 10:43 Add rate limiting
|
|
19
|
+
|
|
20
|
+
$ beans --json list
|
|
21
|
+
[{"id": "bean-a3f2dd1c", "title": "Fix auth middleware", "status": "open", ...}]
|
|
22
|
+
```
|
|
23
|
+
|
|
24
|
+
## Why Beans exists
|
|
25
|
+
|
|
26
|
+
Beans was born from analyzing [beads](https://github.com/steveyegge/beads), a
|
|
27
|
+
graph-based issue tracker with a similar goal: giving AI agents a structured way to
|
|
28
|
+
coordinate work. The idea is excellent. The execution, however, raised serious concerns.
|
|
29
|
+
|
|
30
|
+
### The problem with beads
|
|
31
|
+
|
|
32
|
+
Beads is invasive software that assumes too much about the user's intentions:
|
|
33
|
+
|
|
34
|
+
- **Curl-pipe-bash installer** that silently escalates to `sudo` to install a ~200MB
|
|
35
|
+
binary system-wide
|
|
36
|
+
- **Strips macOS code signatures** and applies ad-hoc signatures to bypass Gatekeeper —
|
|
37
|
+
the same technique used by actual malware
|
|
38
|
+
- **Installs 5 persistent git hooks** (`pre-commit`, `post-merge`, `pre-push`,
|
|
39
|
+
`post-checkout`, `prepare-commit-msg`) that run on nearly every git operation
|
|
40
|
+
- **Runs a background database daemon** (Dolt SQL server) that binds to TCP ports
|
|
41
|
+
3307/3308 — a MySQL-protocol server running on your machine
|
|
42
|
+
- **Silently modifies commit messages** by appending metadata trailers without asking
|
|
43
|
+
- **Auto-pushes data** to remote servers every 5 minutes
|
|
44
|
+
- **Fingerprints your machine** with unique UUIDs for the repo, clone, and project
|
|
45
|
+
- **Injects system prompts** into AI agent configuration files (e.g.,
|
|
46
|
+
`.claude/settings.local.json`) to force agents to use the tool
|
|
47
|
+
- **Includes a "stealth mode"** that hides its presence from collaborators
|
|
48
|
+
- **Ships with telemetry** that records every command and all arguments, including who
|
|
49
|
+
ran them
|
|
50
|
+
|
|
51
|
+
The core data model is a 50+ field struct covering everything from agent heartbeats to
|
|
52
|
+
ephemeral "wisps." It's not a simple tool — it's a complex system that assumes you want
|
|
53
|
+
all of it.
|
|
54
|
+
|
|
55
|
+
### What beans does differently
|
|
56
|
+
|
|
57
|
+
Beans keeps the good idea — a dependency graph for AI agent coordination — and throws
|
|
58
|
+
away everything else:
|
|
59
|
+
|
|
60
|
+
| | Beads | Beans |
|
|
61
|
+
|---|---|---|
|
|
62
|
+
| **Storage** | Dolt SQL server (~200MB, background daemon, TCP ports) | SQLite (zero-config, embedded, stdlib) |
|
|
63
|
+
| **Installation** | `curl \| bash` + `sudo` + signature stripping | `uv add beans` or `pip install beans` |
|
|
64
|
+
| **Git hooks** | 5 persistent hooks installed silently | None |
|
|
65
|
+
| **Commit modification** | Silent trailer injection | Never touches your commits |
|
|
66
|
+
| **Background processes** | Persistent MySQL-protocol daemon | None |
|
|
67
|
+
| **Network** | Opens TCP ports, auto-pushes every 5 min | Fully offline |
|
|
68
|
+
| **Telemetry** | OTel spans with actor identity + full args | None |
|
|
69
|
+
| **Stealth mode** | Yes, hides from collaborators | No — transparency is a feature |
|
|
70
|
+
| **AI config injection** | Writes to `.claude/settings.local.json` | Provides recipes you copy yourself |
|
|
71
|
+
| **Data model** | 50+ fields | ~10 fields + dependency edges |
|
|
72
|
+
| **Agent integration** | Forced via injected prompts | Opt-in via `AGENTS.md` instructions |
|
|
73
|
+
|
|
74
|
+
## Design principles
|
|
75
|
+
|
|
76
|
+
**Polite software.** Beans never modifies files without asking, never installs hooks,
|
|
77
|
+
never runs background processes, and never phones home. It's a CLI tool that reads and
|
|
78
|
+
writes to a local SQLite file. That's it.
|
|
79
|
+
|
|
80
|
+
**Embedded storage.** A single `.beans/beans.db` SQLite file with WAL mode. No servers,
|
|
81
|
+
no ports, no daemons. Works offline, works in CI, works anywhere Python runs.
|
|
82
|
+
|
|
83
|
+
**Graph-native.** Issues (beans) are nodes. Dependencies are typed edges. "What's ready
|
|
84
|
+
to work on?" is a graph query — show me all open beans with no open blockers.
|
|
85
|
+
|
|
86
|
+
**Agent-friendly.** The `--json` flag on every command makes beans machine-readable.
|
|
87
|
+
Agents don't need to parse human output. The CLI is a thin wrapper around a clean Python
|
|
88
|
+
API — agents can also use the library directly.
|
|
89
|
+
|
|
90
|
+
**Minimal by default.** A bean has an id, title, status, and timestamps. Everything else
|
|
91
|
+
is optional. No 50-field structs, no agent heartbeat tracking, no ephemeral wisps.
|
|
92
|
+
|
|
93
|
+
**Journal-based sync.** Changes are recorded in an append-only JSONL journal that can be
|
|
94
|
+
committed to git. The SQLite database is a materialized view that can be rebuilt from the
|
|
95
|
+
journal at any time. Sync through git, not through custom protocols.
|
|
96
|
+
|
|
97
|
+
## Installation
|
|
98
|
+
|
|
99
|
+
```bash
|
|
100
|
+
uv add beans
|
|
101
|
+
```
|
|
102
|
+
|
|
103
|
+
Or with pip:
|
|
104
|
+
|
|
105
|
+
```bash
|
|
106
|
+
pip install beans
|
|
107
|
+
```
|
|
108
|
+
|
|
109
|
+
## Quick start
|
|
110
|
+
|
|
111
|
+
```bash
|
|
112
|
+
# Create some beans
|
|
113
|
+
beans create "Set up database schema"
|
|
114
|
+
beans create "Build API endpoints"
|
|
115
|
+
beans create "Write integration tests"
|
|
116
|
+
|
|
117
|
+
# List all beans
|
|
118
|
+
beans list
|
|
119
|
+
|
|
120
|
+
# JSON output for agent consumption
|
|
121
|
+
beans --json list
|
|
122
|
+
```
|
|
123
|
+
|
|
124
|
+
## Architecture
|
|
125
|
+
|
|
126
|
+
```
|
|
127
|
+
src/beans/
|
|
128
|
+
├── models.py # Pydantic models (pure, no I/O)
|
|
129
|
+
├── store.py # SQLite storage (I/O boundary)
|
|
130
|
+
└── cli.py # Typer CLI (thin wiring layer)
|
|
131
|
+
```
|
|
132
|
+
|
|
133
|
+
- **models.py** — Pure data. Bean is a Pydantic model with validation. No I/O, no side
|
|
134
|
+
effects, easy to test.
|
|
135
|
+
- **store.py** — The I/O boundary. BeanStore wraps a SQLite connection with
|
|
136
|
+
create/read/update/delete operations. Accepts an injected connection for testing.
|
|
137
|
+
- **cli.py** — Thin wiring. Parses args, calls store methods, formats output. No
|
|
138
|
+
business logic.
|
|
139
|
+
|
|
140
|
+
## For AI agents
|
|
141
|
+
|
|
142
|
+
Beans is designed to be used by AI agents as a coordination mechanism. Add this to your
|
|
143
|
+
project's `AGENTS.md`:
|
|
144
|
+
|
|
145
|
+
```markdown
|
|
146
|
+
## Task tracking
|
|
147
|
+
|
|
148
|
+
This project uses `beans` for task tracking. Use `beans --json` for all commands.
|
|
149
|
+
|
|
150
|
+
- Check available work: `beans --json ready`
|
|
151
|
+
- Claim a task: `beans claim <id>`
|
|
152
|
+
- Mark done: `beans close <id>`
|
|
153
|
+
- Create subtasks: `beans create "<title>" --parent <id>`
|
|
154
|
+
```
|
|
155
|
+
|
|
156
|
+
## License
|
|
157
|
+
|
|
158
|
+
MIT
|
|
159
|
+
|
|
160
|
+
## Contributing
|
|
161
|
+
|
|
162
|
+
Beans is built with Python 3.14, managed with [uv](https://docs.astral.sh/uv/).
|
|
163
|
+
|
|
164
|
+
```bash
|
|
165
|
+
git clone https://github.com/henriquebastos/beans.git
|
|
166
|
+
cd beans
|
|
167
|
+
uv sync
|
|
168
|
+
uv run pytest
|
|
169
|
+
uv run ruff check src/ tests/
|
|
170
|
+
```
|
|
171
|
+
|
|
172
|
+
Tests use real SQLite `:memory:` databases — no mocks. The test suite runs in under a
|
|
173
|
+
second.
|
|
@@ -0,0 +1,43 @@
|
|
|
1
|
+
[project]
|
|
2
|
+
name = "magic-beans"
|
|
3
|
+
version = "0.1.0"
|
|
4
|
+
description = "Graph-based issue tracker for AI agent coordination"
|
|
5
|
+
readme = "README.md"
|
|
6
|
+
license = "MIT"
|
|
7
|
+
authors = [
|
|
8
|
+
{ name = "Henrique Bastos", email = "henrique@bastos.net" }
|
|
9
|
+
]
|
|
10
|
+
requires-python = ">=3.14"
|
|
11
|
+
dependencies = [
|
|
12
|
+
"pydantic>=2.0",
|
|
13
|
+
"typer>=0.9",
|
|
14
|
+
]
|
|
15
|
+
|
|
16
|
+
[build-system]
|
|
17
|
+
requires = ["uv_build>=0.10.7,<0.11.0"]
|
|
18
|
+
build-backend = "uv_build"
|
|
19
|
+
|
|
20
|
+
[tool.uv.build-backend]
|
|
21
|
+
module-name = "beans"
|
|
22
|
+
|
|
23
|
+
[project.scripts]
|
|
24
|
+
beans = "beans.cli:app"
|
|
25
|
+
|
|
26
|
+
[dependency-groups]
|
|
27
|
+
dev = [
|
|
28
|
+
"hypothesis>=6.151.9",
|
|
29
|
+
"pytest>=9.0.2",
|
|
30
|
+
"pytest-cov>=7.0.0",
|
|
31
|
+
"ruff>=0.15.6",
|
|
32
|
+
"time-machine>=3.2.0",
|
|
33
|
+
]
|
|
34
|
+
|
|
35
|
+
[tool.pytest.ini_options]
|
|
36
|
+
testpaths = ["tests"]
|
|
37
|
+
addopts = "--cov=beans --cov-report=term-missing --no-header -q"
|
|
38
|
+
|
|
39
|
+
[tool.ruff]
|
|
40
|
+
line-length = 120
|
|
41
|
+
|
|
42
|
+
[tool.ruff.lint]
|
|
43
|
+
select = ["E", "F", "I", "N", "UP", "RUF"]
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
|
|
@@ -0,0 +1,64 @@
|
|
|
1
|
+
# Python imports
|
|
2
|
+
import json
|
|
3
|
+
from datetime import datetime
|
|
4
|
+
from typing import Annotated
|
|
5
|
+
|
|
6
|
+
# Pip imports
|
|
7
|
+
import typer
|
|
8
|
+
|
|
9
|
+
# Internal imports
|
|
10
|
+
from beans.models import Bean
|
|
11
|
+
from beans.store import BeanStore
|
|
12
|
+
|
|
13
|
+
app = typer.Typer()
|
|
14
|
+
|
|
15
|
+
# Global state shared across commands
|
|
16
|
+
state: dict = {}
|
|
17
|
+
|
|
18
|
+
|
|
19
|
+
@app.callback()
|
|
20
|
+
def main(
|
|
21
|
+
db: Annotated[str | None, typer.Option(help="Path to SQLite database")] = None,
|
|
22
|
+
json_output: Annotated[bool, typer.Option("--json", help="Output as JSON")] = False,
|
|
23
|
+
):
|
|
24
|
+
state["db"] = db
|
|
25
|
+
state["json"] = json_output
|
|
26
|
+
|
|
27
|
+
|
|
28
|
+
def local_timestamp(dt: datetime, fmt="%Y-%m-%d %H:%M") -> str:
|
|
29
|
+
return dt.astimezone().strftime(fmt)
|
|
30
|
+
|
|
31
|
+
|
|
32
|
+
def format_bean(bean: Bean) -> str:
|
|
33
|
+
return f"{bean.id} {local_timestamp(bean.created_at)} {bean.title}"
|
|
34
|
+
|
|
35
|
+
|
|
36
|
+
def get_store() -> BeanStore:
|
|
37
|
+
db_path = state.get("db") or "beans.db" # TODO: project discovery (Phase 6.2)
|
|
38
|
+
return BeanStore.from_path(db_path)
|
|
39
|
+
|
|
40
|
+
|
|
41
|
+
@app.command()
|
|
42
|
+
def create(title: str):
|
|
43
|
+
"""Create a new bean."""
|
|
44
|
+
bean = Bean(title=title)
|
|
45
|
+
with get_store() as store:
|
|
46
|
+
store.create_bean(bean)
|
|
47
|
+
|
|
48
|
+
if state.get("json"):
|
|
49
|
+
typer.echo(bean.model_dump_json())
|
|
50
|
+
else:
|
|
51
|
+
typer.echo(format_bean(bean))
|
|
52
|
+
|
|
53
|
+
|
|
54
|
+
@app.command("list")
|
|
55
|
+
def list_beans():
|
|
56
|
+
"""List all beans."""
|
|
57
|
+
with get_store() as store:
|
|
58
|
+
beans = store.list_beans()
|
|
59
|
+
|
|
60
|
+
if state.get("json"):
|
|
61
|
+
typer.echo(json.dumps([b.model_dump(mode="json") for b in beans]))
|
|
62
|
+
else:
|
|
63
|
+
for bean in beans:
|
|
64
|
+
typer.echo(format_bean(bean))
|
|
@@ -0,0 +1,29 @@
|
|
|
1
|
+
# Python imports
|
|
2
|
+
import secrets
|
|
3
|
+
from datetime import UTC, datetime
|
|
4
|
+
from functools import partial
|
|
5
|
+
from typing import Literal
|
|
6
|
+
|
|
7
|
+
# Pip imports
|
|
8
|
+
from pydantic import BaseModel, Field
|
|
9
|
+
|
|
10
|
+
|
|
11
|
+
ID_BYTES = 4
|
|
12
|
+
|
|
13
|
+
|
|
14
|
+
def generate_id(prefix="bean-", fn=partial(secrets.token_hex, ID_BYTES)) -> str:
|
|
15
|
+
return prefix + fn()
|
|
16
|
+
|
|
17
|
+
|
|
18
|
+
class Bean(BaseModel):
|
|
19
|
+
id: str = Field(default_factory=generate_id)
|
|
20
|
+
title: str
|
|
21
|
+
type: str = "task"
|
|
22
|
+
status: Literal["open", "in_progress", "closed"] = "open"
|
|
23
|
+
priority: int = Field(default=2, ge=0, le=4)
|
|
24
|
+
body: str = ""
|
|
25
|
+
parent_id: str | None = None
|
|
26
|
+
assignee: str | None = None
|
|
27
|
+
created_by: str | None = None
|
|
28
|
+
ref_id: str | None = None
|
|
29
|
+
created_at: datetime = Field(default_factory=lambda: datetime.now(UTC))
|
|
@@ -0,0 +1,82 @@
|
|
|
1
|
+
# Python imports
|
|
2
|
+
import sqlite3
|
|
3
|
+
|
|
4
|
+
# Internal imports
|
|
5
|
+
from beans.models import Bean
|
|
6
|
+
|
|
7
|
+
def columns(cursor: sqlite3.Cursor) -> list[str]:
|
|
8
|
+
return [desc[0] for desc in cursor.description]
|
|
9
|
+
|
|
10
|
+
|
|
11
|
+
def row(cols: list[str], values: tuple) -> dict:
|
|
12
|
+
return dict(zip(cols, values))
|
|
13
|
+
|
|
14
|
+
|
|
15
|
+
SCHEMA = """
|
|
16
|
+
PRAGMA journal_mode=WAL;
|
|
17
|
+
PRAGMA foreign_keys=ON;
|
|
18
|
+
|
|
19
|
+
CREATE TABLE IF NOT EXISTS beans (
|
|
20
|
+
id TEXT PRIMARY KEY,
|
|
21
|
+
title TEXT NOT NULL,
|
|
22
|
+
type TEXT NOT NULL DEFAULT 'task',
|
|
23
|
+
status TEXT NOT NULL DEFAULT 'open',
|
|
24
|
+
priority INTEGER NOT NULL DEFAULT 2,
|
|
25
|
+
body TEXT NOT NULL DEFAULT '',
|
|
26
|
+
parent_id TEXT,
|
|
27
|
+
assignee TEXT,
|
|
28
|
+
created_by TEXT,
|
|
29
|
+
ref_id TEXT,
|
|
30
|
+
created_at TEXT NOT NULL
|
|
31
|
+
);
|
|
32
|
+
"""
|
|
33
|
+
|
|
34
|
+
|
|
35
|
+
class BeanStore:
|
|
36
|
+
def __init__(self, conn: sqlite3.Connection):
|
|
37
|
+
self.conn = conn
|
|
38
|
+
self.init_db(conn)
|
|
39
|
+
|
|
40
|
+
@staticmethod
|
|
41
|
+
def init_db(conn: sqlite3.Connection, schema: str = SCHEMA):
|
|
42
|
+
conn.executescript(schema)
|
|
43
|
+
|
|
44
|
+
@classmethod
|
|
45
|
+
def from_path(cls, db_path: str) -> BeanStore:
|
|
46
|
+
return cls(sqlite3.connect(db_path))
|
|
47
|
+
|
|
48
|
+
def close(self):
|
|
49
|
+
self.conn.close()
|
|
50
|
+
|
|
51
|
+
def __enter__(self):
|
|
52
|
+
return self
|
|
53
|
+
|
|
54
|
+
def __exit__(self, *args):
|
|
55
|
+
self.close()
|
|
56
|
+
|
|
57
|
+
def create_bean(self, bean: Bean) -> Bean:
|
|
58
|
+
self.conn.execute(
|
|
59
|
+
"""INSERT INTO beans
|
|
60
|
+
(id, title, type, status, priority, body, parent_id, assignee, created_by, ref_id, created_at)
|
|
61
|
+
VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?)""",
|
|
62
|
+
(
|
|
63
|
+
bean.id,
|
|
64
|
+
bean.title,
|
|
65
|
+
bean.type,
|
|
66
|
+
bean.status,
|
|
67
|
+
bean.priority,
|
|
68
|
+
bean.body,
|
|
69
|
+
bean.parent_id,
|
|
70
|
+
bean.assignee,
|
|
71
|
+
bean.created_by,
|
|
72
|
+
bean.ref_id,
|
|
73
|
+
bean.created_at.isoformat(),
|
|
74
|
+
),
|
|
75
|
+
)
|
|
76
|
+
self.conn.commit()
|
|
77
|
+
return bean
|
|
78
|
+
|
|
79
|
+
def list_beans(self) -> list[Bean]:
|
|
80
|
+
cursor = self.conn.execute("SELECT * FROM beans")
|
|
81
|
+
cols = columns(cursor)
|
|
82
|
+
return [Bean(**row(cols, values)) for values in cursor.fetchall()]
|