sessionmemory 0.2.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.
Files changed (35) hide show
  1. sessionmemory-0.2.0/PKG-INFO +250 -0
  2. sessionmemory-0.2.0/README.md +235 -0
  3. sessionmemory-0.2.0/pyproject.toml +192 -0
  4. sessionmemory-0.2.0/pyproject.toml.orig +173 -0
  5. sessionmemory-0.2.0/src/sessionmemory/__init__.py +1 -0
  6. sessionmemory-0.2.0/src/sessionmemory/cli.py +62 -0
  7. sessionmemory-0.2.0/src/sessionmemory/commands/__init__.py +1 -0
  8. sessionmemory-0.2.0/src/sessionmemory/commands/_common.py +207 -0
  9. sessionmemory-0.2.0/src/sessionmemory/commands/delete.py +71 -0
  10. sessionmemory-0.2.0/src/sessionmemory/commands/doctor.py +30 -0
  11. sessionmemory-0.2.0/src/sessionmemory/commands/export.py +61 -0
  12. sessionmemory-0.2.0/src/sessionmemory/commands/init.py +88 -0
  13. sessionmemory-0.2.0/src/sessionmemory/commands/inject.py +25 -0
  14. sessionmemory-0.2.0/src/sessionmemory/commands/log.py +64 -0
  15. sessionmemory-0.2.0/src/sessionmemory/commands/new.py +102 -0
  16. sessionmemory-0.2.0/src/sessionmemory/commands/project.py +321 -0
  17. sessionmemory-0.2.0/src/sessionmemory/commands/reindex.py +46 -0
  18. sessionmemory-0.2.0/src/sessionmemory/commands/search.py +83 -0
  19. sessionmemory-0.2.0/src/sessionmemory/lib/__init__.py +1 -0
  20. sessionmemory-0.2.0/src/sessionmemory/lib/atomic.py +71 -0
  21. sessionmemory-0.2.0/src/sessionmemory/lib/bootstrap.py +138 -0
  22. sessionmemory-0.2.0/src/sessionmemory/lib/config.py +102 -0
  23. sessionmemory-0.2.0/src/sessionmemory/lib/doctor.py +186 -0
  24. sessionmemory-0.2.0/src/sessionmemory/lib/embed.py +123 -0
  25. sessionmemory-0.2.0/src/sessionmemory/lib/export.py +25 -0
  26. sessionmemory-0.2.0/src/sessionmemory/lib/field.py +181 -0
  27. sessionmemory-0.2.0/src/sessionmemory/lib/fieldindex.py +214 -0
  28. sessionmemory-0.2.0/src/sessionmemory/lib/frontmatter.py +163 -0
  29. sessionmemory-0.2.0/src/sessionmemory/lib/gitinfo.py +177 -0
  30. sessionmemory-0.2.0/src/sessionmemory/lib/ids.py +101 -0
  31. sessionmemory-0.2.0/src/sessionmemory/lib/inject.py +97 -0
  32. sessionmemory-0.2.0/src/sessionmemory/lib/log.py +73 -0
  33. sessionmemory-0.2.0/src/sessionmemory/lib/paths.py +77 -0
  34. sessionmemory-0.2.0/src/sessionmemory/lib/registry.py +269 -0
  35. sessionmemory-0.2.0/src/sessionmemory/lib/resolve.py +75 -0
@@ -0,0 +1,250 @@
1
+ Metadata-Version: 2.3
2
+ Name: sessionmemory
3
+ Version: 0.2.0
4
+ Summary: Durable memory for coding agents, one folder of searchable pages per project.
5
+ Author: Nathaniel Landau
6
+ Author-email: Nathaniel Landau <github@natelandau.com>
7
+ Requires-Dist: fastembed>=0.8.0
8
+ Requires-Dist: nclutils>=3.4.4
9
+ Requires-Dist: pyyaml>=6.0.3
10
+ Requires-Dist: sqlite-vec>=0.1.9
11
+ Requires-Dist: tomli-w>=1.2.0
12
+ Requires-Dist: typer>=0.27.2
13
+ Requires-Python: >=3.13, <3.15
14
+ Description-Content-Type: text/markdown
15
+
16
+ # sessionmemory
17
+
18
+ Durable memory for coding agents, one folder of searchable pages per project.
19
+
20
+ An agent starts every session knowing nothing about the last one. Yesterday's session
21
+ worked around a trap, rejected the obvious approach for a reason, and found the one flag
22
+ that makes a library behave. All of it ends with the session, and the next one works it
23
+ out again from nothing. `sessionmemory` keeps what a project learned and hands it back
24
+ when the next session starts. Each project keeps its own memory, and no page is ever
25
+ shared between projects.
26
+
27
+ | Part | What it is |
28
+ | -------------------------- | ------------------------------------------------------------------------------ |
29
+ | The vault | Your knowledge as markdown pages, one folder per project |
30
+ | The `sessionmemory` plugin | Claude Code hooks that feed a session at its start and record it at its end |
31
+ | The `sessionmemory` CLI | Searches pages by meaning and creates them. Everything else is a file you edit |
32
+
33
+ The pages follow the memoryfield format by Cal Paterson, described in
34
+ [his article](https://calpaterson.com/memoryfields.html) and defined in
35
+ [the memoryfield spec](https://github.com/calpaterson/memoryfield-spec). Each project's
36
+ `learnings/` folder is one field in that format: a flat directory of markdown pages beside one
37
+ vector index file. You can export that folder, share it, or read it with any tool that
38
+ speaks the format.
39
+
40
+ ## Requirements
41
+
42
+ - Python 3.13 or 3.14
43
+ - [uv](https://docs.astral.sh/uv/)
44
+ - git. The vault is a git repository, and a project is registered by its git remote.
45
+ - Claude Code, to run the plugin. The CLI works on its own without it.
46
+
47
+ The first search downloads the `nomic-embed-text-v1.5` embedding model, about 520MB, and
48
+ caches it under `~/.cache/sessionmemory/models`. Nothing else touches the network.
49
+
50
+ ## Install the CLI
51
+
52
+ Clone the repository and install its dependencies. Keep the clone: it is the source the
53
+ plugin installs from, and it is where you pull updates.
54
+
55
+ ```bash
56
+ git clone https://github.com/natelandau/sessionmemory ~/repos/sessionmemory
57
+ cd ~/repos/sessionmemory
58
+ uv sync
59
+ ```
60
+
61
+ To put a `sessionmemory` command on your `PATH`, install the package as a tool:
62
+
63
+ ```bash
64
+ uv tool install .
65
+ ```
66
+
67
+ Without that step, run the CLI as `uv run sessionmemory` from the clone, or through the
68
+ `bin/sessionmemory` shim, which works from any directory.
69
+
70
+ ## Create a vault
71
+
72
+ The vault is its own directory. Make it a git repository of its own, so your pages and
73
+ this code do not share a history.
74
+
75
+ ```bash
76
+ mkdir -p ~/repos/my-vault
77
+ cd ~/repos/my-vault
78
+ git init
79
+ export SESSIONMEMORY_VAULT=~/repos/my-vault
80
+ sessionmemory init
81
+ ```
82
+
83
+ Put the `export` in your shell profile, and give it an absolute path. Every directory the
84
+ CLI prints is built from that value.
85
+
86
+ `sessionmemory init` writes the three files a vault needs. A marker in `_system/vault.toml`
87
+ identifies the directory as a vault. A `.gitignore` keeps the derived index out of your
88
+ history. A README explains the layout to whoever opens the vault later. `sessionmemory init`
89
+ never overwrites a file, so you can run it again safely.
90
+
91
+ Until a directory holds that marker, every command refuses to touch it. The refusal
92
+ protects you. If `SESSIONMEMORY_VAULT` points at your home directory by mistake, the
93
+ first page written scatters a `projects/` tree into it.
94
+
95
+ Nothing commits the vault on a timer. The plugin commits it when a session starts and
96
+ again when a session ends, so a page reaches git within the session that wrote it.
97
+ Pushing that history to a remote stays yours to do.
98
+
99
+ > **Note:** To bring an existing directory of notes under the CLI, run
100
+ > `sessionmemory init --force ~/repos/my-vault` once. `--force` means only that the
101
+ > directory already has contents. Nothing existing is overwritten.
102
+
103
+ ## Register a project
104
+
105
+ A project gets memory when its repository is registered. Registration is the only
106
+ decision, and it happens once, from inside the repository:
107
+
108
+ ```bash
109
+ cd ~/repos/invoice-api
110
+ sessionmemory project --register --cwd .
111
+ ```
112
+
113
+ ```
114
+ ✓ registered 'invoice-api'
115
+ └─ root: ~/repos/invoice-api
116
+ ```
117
+
118
+ `--register` reads the git remote and the repository root, and derives the slug from
119
+ them. There is nothing else to choose: no tags, no scope, no note type. The slug is
120
+ permanent once pages carry it, so an unregistered directory is told to run this command
121
+ rather than registered for you.
122
+
123
+ ## Search and write pages
124
+
125
+ The CLI does two things. It finds pages by meaning, and it creates pages. Reading and
126
+ editing a page is a job for your editor or your agent's own tools.
127
+
128
+ ```bash
129
+ sessionmemory search "why does the same stripe event arrive twice" --limit 2 --cwd .
130
+ ```
131
+
132
+ ```
133
+ ~/repos/my-vault/projects/invoice-api/learnings/stripe-retries-a-webhook-for-72-hours-so-the-handler-must-be-idempotent.md
134
+ Stripe retries a webhook for 72 hours, so the handler must be idempotent
135
+ Stripe redelivers an unacknowledged webhook for up to 72 hours, so the handler records the event id and ignores a repeat.
136
+
137
+ ~/repos/my-vault/projects/invoice-api/learnings/the-nightly-reconciliation-job-must-start-after-the-02-00-bank-feed.md
138
+ The nightly reconciliation job must start after the 02:00 bank feed
139
+ The bank feed lands at 02:00 UTC; a reconciliation run before it reports every open invoice as unpaid.
140
+ ```
141
+
142
+ A result is a path, a title, and a summary. A paraphrase finds the page, because search
143
+ ranks by meaning and not by words in common. A query that nothing answers returns no
144
+ results rather than the nearest pages. Pass `--read` to print every hit in full.
145
+
146
+ ```bash
147
+ sessionmemory new learning \
148
+ --title "Stripe retries a webhook for 72 hours, so the handler must be idempotent" \
149
+ --summary "Stripe redelivers an unacknowledged webhook for up to 72 hours, so the handler records the event id and ignores a repeat." \
150
+ --cwd .
151
+ ```
152
+
153
+ ```
154
+ ✓ created stripe-retries-a-webhook-for-72-hours-so-the-handler-must-be-idempotent.md
155
+ └─ ~/repos/my-vault/projects/invoice-api/learnings/stripe-retries-a-webhook-for-72-hours-so-the-handler-must-be-idempotent.md
156
+ ```
157
+
158
+ The vault path is shortened to `~` here; the command prints absolute paths.
159
+
160
+ The command writes the frontmatter and prints the path. Write the body into that file,
161
+ or pass it with `--body-file`. The title is what every future session sees at its start,
162
+ and the summary is what a search result shows. Both state the fact and not the topic.
163
+
164
+ ## Install the Claude Code plugin
165
+
166
+ Add the clone as a marketplace, then install the plugin from it:
167
+
168
+ ```
169
+ /plugin marketplace add ~/repos/sessionmemory
170
+ /plugin install sessionmemory@sessionmemory
171
+ ```
172
+
173
+ The GitHub shorthand `natelandau/sessionmemory` works as a marketplace source too.
174
+ Either way, Claude Code copies the plugin into its own cache and runs the hooks from that
175
+ copy, not from your clone. After you pull changes into the clone, run
176
+ `/plugin update sessionmemory@sessionmemory` to refresh the copy.
177
+
178
+ A hook does not start from an interactive shell. A session launched from a GUI or an IDE
179
+ cannot see a root exported in `.zshrc`, so record the vault root in
180
+ `~/.claude/sessionmemory.toml` as well:
181
+
182
+ ```toml
183
+ [vault]
184
+ root = "~/repos/my-vault"
185
+ ```
186
+
187
+ From then on, a session that starts in a registered repository receives that project's
188
+ memory. A session that ends or compacts hands its transcript to a background pass, which
189
+ records what was worth keeping.
190
+
191
+ ## What a session sees
192
+
193
+ `sessionmemory inject` prints the block a session starts with. This is the block for a
194
+ project holding four learnings, one spec, one plan, and two open backlog items:
195
+
196
+ ```
197
+ ## Using this vault
198
+
199
+ Durable memory for this project lives in a vault of markdown pages. Below is the
200
+ list of what it already knows; each title is one `sessionmemory search` away.
201
+
202
+ - A title below matches what you are doing: `sessionmemory search "<words>"` returns
203
+ the page's path, and you Read it. Search before assuming nothing was written down.
204
+ - Past sessions: `sessionmemory search "<words>" --logs`. Open work: read `backlog.md`
205
+ in the project's vault folder (`sessionmemory project --json` prints every path).
206
+ - Something worth keeping past this session: `sessionmemory new learning --title "..."
207
+ --summary "..." --cwd .` creates the page and prints the path to write prose into.
208
+ Keep a page under 8KB; more detail is another page.
209
+ - Specs and plans: `sessionmemory new spec|plan --title "..." --cwd .` creates the file.
210
+ Edit `backlog.md`, specs, and plans directly; the CLI only creates pages.
211
+
212
+ ## What this project knows
213
+
214
+ - Invoice numbers come from a Postgres sequence, never from max(id) plus one
215
+ - pytest-asyncio needs asyncio_mode = auto or every async test is skipped
216
+ - Stripe retries a webhook for 72 hours, so the handler must be idempotent
217
+ - The nightly reconciliation job must start after the 02:00 bank feed
218
+
219
+ ## Open work
220
+
221
+ 2 open backlog items
222
+ spec: Export invoices as UBL 2.1 XML
223
+ plan: Move PDF rendering to a worker queue
224
+ ```
225
+
226
+ A page body never enters that block, so its cost grows with the number of pages and not
227
+ with their length. The titles say what exists. `sessionmemory search` returns what they say.
228
+
229
+ ## Documentation
230
+
231
+ | Page | What it covers |
232
+ | ---------------------------------------- | ------------------------------------------------------------ |
233
+ | [Concepts](docs/concepts.md) | Pages, fields, the index, and the layout of a vault |
234
+ | [CLI reference](docs/cli.md) | Every command, its options, and its output |
235
+ | [The Claude Code plugin](docs/plugin.md) | The hooks, the sweep, every setting, and the slash commands |
236
+ | [Vault health](docs/vault-health.md) | What `sessionmemory doctor` reports, and what to do about it |
237
+
238
+ ## Development
239
+
240
+ ```bash
241
+ uv sync # install dependencies
242
+ uv run duty lint # ruff, ty, typos, yamllint, shellcheck, prek
243
+ uv run duty test # pytest with coverage
244
+ ```
245
+
246
+ `CLAUDE.md` records the conventions this project holds itself to.
247
+
248
+ ## License
249
+
250
+ MIT. See [LICENSE](LICENSE).
@@ -0,0 +1,235 @@
1
+ # sessionmemory
2
+
3
+ Durable memory for coding agents, one folder of searchable pages per project.
4
+
5
+ An agent starts every session knowing nothing about the last one. Yesterday's session
6
+ worked around a trap, rejected the obvious approach for a reason, and found the one flag
7
+ that makes a library behave. All of it ends with the session, and the next one works it
8
+ out again from nothing. `sessionmemory` keeps what a project learned and hands it back
9
+ when the next session starts. Each project keeps its own memory, and no page is ever
10
+ shared between projects.
11
+
12
+ | Part | What it is |
13
+ | -------------------------- | ------------------------------------------------------------------------------ |
14
+ | The vault | Your knowledge as markdown pages, one folder per project |
15
+ | The `sessionmemory` plugin | Claude Code hooks that feed a session at its start and record it at its end |
16
+ | The `sessionmemory` CLI | Searches pages by meaning and creates them. Everything else is a file you edit |
17
+
18
+ The pages follow the memoryfield format by Cal Paterson, described in
19
+ [his article](https://calpaterson.com/memoryfields.html) and defined in
20
+ [the memoryfield spec](https://github.com/calpaterson/memoryfield-spec). Each project's
21
+ `learnings/` folder is one field in that format: a flat directory of markdown pages beside one
22
+ vector index file. You can export that folder, share it, or read it with any tool that
23
+ speaks the format.
24
+
25
+ ## Requirements
26
+
27
+ - Python 3.13 or 3.14
28
+ - [uv](https://docs.astral.sh/uv/)
29
+ - git. The vault is a git repository, and a project is registered by its git remote.
30
+ - Claude Code, to run the plugin. The CLI works on its own without it.
31
+
32
+ The first search downloads the `nomic-embed-text-v1.5` embedding model, about 520MB, and
33
+ caches it under `~/.cache/sessionmemory/models`. Nothing else touches the network.
34
+
35
+ ## Install the CLI
36
+
37
+ Clone the repository and install its dependencies. Keep the clone: it is the source the
38
+ plugin installs from, and it is where you pull updates.
39
+
40
+ ```bash
41
+ git clone https://github.com/natelandau/sessionmemory ~/repos/sessionmemory
42
+ cd ~/repos/sessionmemory
43
+ uv sync
44
+ ```
45
+
46
+ To put a `sessionmemory` command on your `PATH`, install the package as a tool:
47
+
48
+ ```bash
49
+ uv tool install .
50
+ ```
51
+
52
+ Without that step, run the CLI as `uv run sessionmemory` from the clone, or through the
53
+ `bin/sessionmemory` shim, which works from any directory.
54
+
55
+ ## Create a vault
56
+
57
+ The vault is its own directory. Make it a git repository of its own, so your pages and
58
+ this code do not share a history.
59
+
60
+ ```bash
61
+ mkdir -p ~/repos/my-vault
62
+ cd ~/repos/my-vault
63
+ git init
64
+ export SESSIONMEMORY_VAULT=~/repos/my-vault
65
+ sessionmemory init
66
+ ```
67
+
68
+ Put the `export` in your shell profile, and give it an absolute path. Every directory the
69
+ CLI prints is built from that value.
70
+
71
+ `sessionmemory init` writes the three files a vault needs. A marker in `_system/vault.toml`
72
+ identifies the directory as a vault. A `.gitignore` keeps the derived index out of your
73
+ history. A README explains the layout to whoever opens the vault later. `sessionmemory init`
74
+ never overwrites a file, so you can run it again safely.
75
+
76
+ Until a directory holds that marker, every command refuses to touch it. The refusal
77
+ protects you. If `SESSIONMEMORY_VAULT` points at your home directory by mistake, the
78
+ first page written scatters a `projects/` tree into it.
79
+
80
+ Nothing commits the vault on a timer. The plugin commits it when a session starts and
81
+ again when a session ends, so a page reaches git within the session that wrote it.
82
+ Pushing that history to a remote stays yours to do.
83
+
84
+ > **Note:** To bring an existing directory of notes under the CLI, run
85
+ > `sessionmemory init --force ~/repos/my-vault` once. `--force` means only that the
86
+ > directory already has contents. Nothing existing is overwritten.
87
+
88
+ ## Register a project
89
+
90
+ A project gets memory when its repository is registered. Registration is the only
91
+ decision, and it happens once, from inside the repository:
92
+
93
+ ```bash
94
+ cd ~/repos/invoice-api
95
+ sessionmemory project --register --cwd .
96
+ ```
97
+
98
+ ```
99
+ ✓ registered 'invoice-api'
100
+ └─ root: ~/repos/invoice-api
101
+ ```
102
+
103
+ `--register` reads the git remote and the repository root, and derives the slug from
104
+ them. There is nothing else to choose: no tags, no scope, no note type. The slug is
105
+ permanent once pages carry it, so an unregistered directory is told to run this command
106
+ rather than registered for you.
107
+
108
+ ## Search and write pages
109
+
110
+ The CLI does two things. It finds pages by meaning, and it creates pages. Reading and
111
+ editing a page is a job for your editor or your agent's own tools.
112
+
113
+ ```bash
114
+ sessionmemory search "why does the same stripe event arrive twice" --limit 2 --cwd .
115
+ ```
116
+
117
+ ```
118
+ ~/repos/my-vault/projects/invoice-api/learnings/stripe-retries-a-webhook-for-72-hours-so-the-handler-must-be-idempotent.md
119
+ Stripe retries a webhook for 72 hours, so the handler must be idempotent
120
+ Stripe redelivers an unacknowledged webhook for up to 72 hours, so the handler records the event id and ignores a repeat.
121
+
122
+ ~/repos/my-vault/projects/invoice-api/learnings/the-nightly-reconciliation-job-must-start-after-the-02-00-bank-feed.md
123
+ The nightly reconciliation job must start after the 02:00 bank feed
124
+ The bank feed lands at 02:00 UTC; a reconciliation run before it reports every open invoice as unpaid.
125
+ ```
126
+
127
+ A result is a path, a title, and a summary. A paraphrase finds the page, because search
128
+ ranks by meaning and not by words in common. A query that nothing answers returns no
129
+ results rather than the nearest pages. Pass `--read` to print every hit in full.
130
+
131
+ ```bash
132
+ sessionmemory new learning \
133
+ --title "Stripe retries a webhook for 72 hours, so the handler must be idempotent" \
134
+ --summary "Stripe redelivers an unacknowledged webhook for up to 72 hours, so the handler records the event id and ignores a repeat." \
135
+ --cwd .
136
+ ```
137
+
138
+ ```
139
+ ✓ created stripe-retries-a-webhook-for-72-hours-so-the-handler-must-be-idempotent.md
140
+ └─ ~/repos/my-vault/projects/invoice-api/learnings/stripe-retries-a-webhook-for-72-hours-so-the-handler-must-be-idempotent.md
141
+ ```
142
+
143
+ The vault path is shortened to `~` here; the command prints absolute paths.
144
+
145
+ The command writes the frontmatter and prints the path. Write the body into that file,
146
+ or pass it with `--body-file`. The title is what every future session sees at its start,
147
+ and the summary is what a search result shows. Both state the fact and not the topic.
148
+
149
+ ## Install the Claude Code plugin
150
+
151
+ Add the clone as a marketplace, then install the plugin from it:
152
+
153
+ ```
154
+ /plugin marketplace add ~/repos/sessionmemory
155
+ /plugin install sessionmemory@sessionmemory
156
+ ```
157
+
158
+ The GitHub shorthand `natelandau/sessionmemory` works as a marketplace source too.
159
+ Either way, Claude Code copies the plugin into its own cache and runs the hooks from that
160
+ copy, not from your clone. After you pull changes into the clone, run
161
+ `/plugin update sessionmemory@sessionmemory` to refresh the copy.
162
+
163
+ A hook does not start from an interactive shell. A session launched from a GUI or an IDE
164
+ cannot see a root exported in `.zshrc`, so record the vault root in
165
+ `~/.claude/sessionmemory.toml` as well:
166
+
167
+ ```toml
168
+ [vault]
169
+ root = "~/repos/my-vault"
170
+ ```
171
+
172
+ From then on, a session that starts in a registered repository receives that project's
173
+ memory. A session that ends or compacts hands its transcript to a background pass, which
174
+ records what was worth keeping.
175
+
176
+ ## What a session sees
177
+
178
+ `sessionmemory inject` prints the block a session starts with. This is the block for a
179
+ project holding four learnings, one spec, one plan, and two open backlog items:
180
+
181
+ ```
182
+ ## Using this vault
183
+
184
+ Durable memory for this project lives in a vault of markdown pages. Below is the
185
+ list of what it already knows; each title is one `sessionmemory search` away.
186
+
187
+ - A title below matches what you are doing: `sessionmemory search "<words>"` returns
188
+ the page's path, and you Read it. Search before assuming nothing was written down.
189
+ - Past sessions: `sessionmemory search "<words>" --logs`. Open work: read `backlog.md`
190
+ in the project's vault folder (`sessionmemory project --json` prints every path).
191
+ - Something worth keeping past this session: `sessionmemory new learning --title "..."
192
+ --summary "..." --cwd .` creates the page and prints the path to write prose into.
193
+ Keep a page under 8KB; more detail is another page.
194
+ - Specs and plans: `sessionmemory new spec|plan --title "..." --cwd .` creates the file.
195
+ Edit `backlog.md`, specs, and plans directly; the CLI only creates pages.
196
+
197
+ ## What this project knows
198
+
199
+ - Invoice numbers come from a Postgres sequence, never from max(id) plus one
200
+ - pytest-asyncio needs asyncio_mode = auto or every async test is skipped
201
+ - Stripe retries a webhook for 72 hours, so the handler must be idempotent
202
+ - The nightly reconciliation job must start after the 02:00 bank feed
203
+
204
+ ## Open work
205
+
206
+ 2 open backlog items
207
+ spec: Export invoices as UBL 2.1 XML
208
+ plan: Move PDF rendering to a worker queue
209
+ ```
210
+
211
+ A page body never enters that block, so its cost grows with the number of pages and not
212
+ with their length. The titles say what exists. `sessionmemory search` returns what they say.
213
+
214
+ ## Documentation
215
+
216
+ | Page | What it covers |
217
+ | ---------------------------------------- | ------------------------------------------------------------ |
218
+ | [Concepts](docs/concepts.md) | Pages, fields, the index, and the layout of a vault |
219
+ | [CLI reference](docs/cli.md) | Every command, its options, and its output |
220
+ | [The Claude Code plugin](docs/plugin.md) | The hooks, the sweep, every setting, and the slash commands |
221
+ | [Vault health](docs/vault-health.md) | What `sessionmemory doctor` reports, and what to do about it |
222
+
223
+ ## Development
224
+
225
+ ```bash
226
+ uv sync # install dependencies
227
+ uv run duty lint # ruff, ty, typos, yamllint, shellcheck, prek
228
+ uv run duty test # pytest with coverage
229
+ ```
230
+
231
+ `CLAUDE.md` records the conventions this project holds itself to.
232
+
233
+ ## License
234
+
235
+ MIT. See [LICENSE](LICENSE).
@@ -0,0 +1,192 @@
1
+ [project]
2
+ dependencies = [
3
+ "fastembed>=0.8.0",
4
+ "nclutils>=3.4.4",
5
+ "pyyaml>=6.0.3",
6
+ "sqlite-vec>=0.1.9",
7
+ "tomli-w>=1.2.0",
8
+ "typer>=0.27.2",
9
+ ]
10
+ description = "Durable memory for coding agents, one folder of searchable pages per project."
11
+ name = "sessionmemory"
12
+ readme = "README.md"
13
+ requires-python = ">=3.13,<3.15"
14
+ version = "0.2.0"
15
+
16
+ [[project.authors]]
17
+ name = "Nathaniel Landau"
18
+ email = "github@natelandau.com"
19
+
20
+ [project.scripts]
21
+ sessionmemory = "sessionmemory.cli:main"
22
+
23
+ [dependency-groups]
24
+ dev = [
25
+ "commitizen>=4.18.0",
26
+ "coverage>=7.16.0",
27
+ "duty>=1.9.0",
28
+ "prek>=0.5.2",
29
+ "pytest-clarity>=1.0.1",
30
+ "pytest-cov>=7.1.0",
31
+ "pytest-devtools>=1.3.0",
32
+ "pytest-mock>=3.15.1",
33
+ "pytest-xdist>=3.8.0",
34
+ "pytest>=9.1.1",
35
+ "rich>=15.0.0",
36
+ "ruff>=0.16.5",
37
+ "shellcheck-py>=0.11.0.1",
38
+ "ty>=0.0.78",
39
+ "types-pyyaml>=6.0.12.20260815",
40
+ "typos>=1.50.1",
41
+ "yamllint>=1.38.0",
42
+ ]
43
+
44
+ [build-system]
45
+ build-backend = "uv_build"
46
+ requires = ["uv_build>=0.12.5,<0.13.0"]
47
+
48
+ [tool.commitizen]
49
+ bump_message = "bump(release): v$current_version → v$new_version"
50
+ changelog_merge_prerelease = true
51
+ tag_format = "v$version"
52
+ update_changelog_on_bump = true
53
+ version_provider = "uv"
54
+
55
+ [tool.coverage.report]
56
+ exclude_lines = [
57
+ "def __repr__",
58
+ 'except [\w\s\._]+ as .*:',
59
+ "if TYPE_CHECKING",
60
+ "pragma: no cover",
61
+ ]
62
+ fail_under = 95
63
+ show_missing = true
64
+ skip_covered = true
65
+ skip_empty = true
66
+
67
+ [tool.coverage.run]
68
+ branch = true
69
+ command_line = "--module pytest"
70
+ data_file = ".cache/coverage"
71
+ omit = [
72
+ "hooks/precompact.py",
73
+ "hooks/sessionend.py",
74
+ "hooks/vault-path.py",
75
+ "tests/*",
76
+ ]
77
+ source = [
78
+ "hooks",
79
+ "src",
80
+ ]
81
+
82
+ [tool.coverage.xml]
83
+ output = ".cache/coverage.xml"
84
+
85
+ [tool.pytest.ini_options]
86
+ addopts = "--color=yes --doctest-modules --strict-config --strict-markers -n auto --dist loadfile"
87
+ cache_dir = ".cache/pytest"
88
+ cli_runner_patch_result = true
89
+ columns = 1000
90
+ filterwarnings = [
91
+ "error",
92
+ "ignore:.*Pydantic.*:UserWarning",
93
+ "ignore::DeprecationWarning:",
94
+ ]
95
+ markers = ["serial"]
96
+ set_columns = true
97
+ testpaths = ["tests"]
98
+ xfail_strict = true
99
+
100
+ [tool.ruff]
101
+ exclude = [
102
+ ".cache",
103
+ ".git",
104
+ ".venv",
105
+ "build",
106
+ "dist",
107
+ "reference",
108
+ ]
109
+ fix = true
110
+ line-length = 100
111
+ output-format = "grouped"
112
+ src = [
113
+ "src",
114
+ "tests",
115
+ ]
116
+ target-version = "py313"
117
+
118
+ [tool.ruff.lint]
119
+ ignore = [
120
+ "ANN002",
121
+ "ANN003",
122
+ "ANN204",
123
+ "COM812",
124
+ "CPY001",
125
+ "D107",
126
+ "E501",
127
+ "FIX002",
128
+ "PLC0415",
129
+ "S311",
130
+ "TD001",
131
+ "TD002",
132
+ "TD003",
133
+ ]
134
+ select = ["ALL"]
135
+ unfixable = [
136
+ "ERA001",
137
+ "F401",
138
+ "F841",
139
+ ]
140
+
141
+ [tool.ruff.lint.per-file-ignores]
142
+ "tests/**/*.py" = [
143
+ "A002",
144
+ "A003",
145
+ "ANN001",
146
+ "ANN002",
147
+ "ANN003",
148
+ "ANN201",
149
+ "ARG001",
150
+ "ARG002",
151
+ "ARG005",
152
+ "D102",
153
+ "ERA001",
154
+ "F403",
155
+ "F405",
156
+ "FBT001",
157
+ "PGH003",
158
+ "PLC0415",
159
+ "PLR0913",
160
+ "PLR0917",
161
+ "PLR2004",
162
+ "PLW0108",
163
+ "S101",
164
+ "S603",
165
+ "S607",
166
+ "SLF001",
167
+ "W292",
168
+ ]
169
+
170
+ [tool.ruff.lint.mccabe]
171
+ max-complexity = 10
172
+
173
+ [tool.ruff.lint.pydocstyle]
174
+ convention = "google"
175
+
176
+ [tool.ruff.lint.pylint]
177
+ max-args = 7
178
+
179
+ [tool.ruff.lint.flake8-annotations]
180
+ allow-star-arg-any = true
181
+
182
+ [tool.ruff.format]
183
+ indent-style = "space"
184
+ line-ending = "auto"
185
+ quote-style = "double"
186
+ skip-magic-trailing-comma = false
187
+
188
+ [tool.ty.terminal]
189
+ error-on-warning = true
190
+
191
+ [tool.ty.environment]
192
+ extra-paths = ["hooks/"]