memgit 0.2.0__tar.gz → 0.3.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.
- {memgit-0.2.0 → memgit-0.3.0}/PKG-INFO +35 -10
- {memgit-0.2.0 → memgit-0.3.0}/README.md +31 -9
- {memgit-0.2.0 → memgit-0.3.0}/memgit/__init__.py +1 -1
- {memgit-0.2.0 → memgit-0.3.0}/memgit/cli.py +219 -32
- memgit-0.3.0/memgit/cloud/__init__.py +6 -0
- memgit-0.3.0/memgit/cloud/client.py +176 -0
- memgit-0.3.0/memgit/cloud/commands.py +379 -0
- memgit-0.3.0/memgit/cloud/crypto.py +129 -0
- memgit-0.3.0/memgit/cloud/state.py +81 -0
- memgit-0.3.0/memgit/cloud/sync.py +239 -0
- {memgit-0.2.0 → memgit-0.3.0}/memgit/importer.py +71 -11
- {memgit-0.2.0 → memgit-0.3.0}/memgit/mcp_server.py +92 -7
- {memgit-0.2.0 → memgit-0.3.0}/memgit/models.py +3 -1
- {memgit-0.2.0 → memgit-0.3.0}/memgit/repo.py +22 -3
- {memgit-0.2.0 → memgit-0.3.0}/memgit/scorer.py +17 -1
- {memgit-0.2.0 → memgit-0.3.0}/memgit/toon.py +63 -19
- {memgit-0.2.0 → memgit-0.3.0}/memgit.egg-info/PKG-INFO +35 -10
- {memgit-0.2.0 → memgit-0.3.0}/memgit.egg-info/SOURCES.txt +8 -1
- {memgit-0.2.0 → memgit-0.3.0}/memgit.egg-info/requires.txt +4 -0
- {memgit-0.2.0 → memgit-0.3.0}/pyproject.toml +2 -1
- memgit-0.3.0/tests/test_v030.py +297 -0
- {memgit-0.2.0 → memgit-0.3.0}/LICENSE +0 -0
- {memgit-0.2.0 → memgit-0.3.0}/memgit/graph.py +0 -0
- {memgit-0.2.0 → memgit-0.3.0}/memgit/http_server.py +0 -0
- {memgit-0.2.0 → memgit-0.3.0}/memgit/store.py +0 -0
- {memgit-0.2.0 → memgit-0.3.0}/memgit/tokens.py +0 -0
- {memgit-0.2.0 → memgit-0.3.0}/memgit.egg-info/dependency_links.txt +0 -0
- {memgit-0.2.0 → memgit-0.3.0}/memgit.egg-info/entry_points.txt +0 -0
- {memgit-0.2.0 → memgit-0.3.0}/memgit.egg-info/top_level.txt +0 -0
- {memgit-0.2.0 → memgit-0.3.0}/setup.cfg +0 -0
- {memgit-0.2.0 → memgit-0.3.0}/tests/test_advanced.py +0 -0
- {memgit-0.2.0 → memgit-0.3.0}/tests/test_setup.py +0 -0
- {memgit-0.2.0 → memgit-0.3.0}/tests/test_store_repo.py +0 -0
- {memgit-0.2.0 → memgit-0.3.0}/tests/test_toon.py +0 -0
- {memgit-0.2.0 → memgit-0.3.0}/tests/test_v020.py +0 -0
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
Metadata-Version: 2.4
|
|
2
2
|
Name: memgit
|
|
3
|
-
Version: 0.
|
|
3
|
+
Version: 0.3.0
|
|
4
4
|
Summary: Git for AI memory — version-controlled context persistence across Claude, GPT, Gemini, Cursor, Windsurf, and more
|
|
5
5
|
License: MIT
|
|
6
6
|
Project-URL: Homepage, https://memgit.dev
|
|
@@ -27,6 +27,9 @@ Requires-Dist: pytest>=8.0; extra == "dev"
|
|
|
27
27
|
Requires-Dist: pytest-anyio>=0.0.0; extra == "dev"
|
|
28
28
|
Provides-Extra: tokens
|
|
29
29
|
Requires-Dist: tiktoken>=0.7; extra == "tokens"
|
|
30
|
+
Provides-Extra: cloud
|
|
31
|
+
Requires-Dist: pynacl>=1.5; extra == "cloud"
|
|
32
|
+
Requires-Dist: httpx>=0.27; extra == "cloud"
|
|
30
33
|
Dynamic: license-file
|
|
31
34
|
|
|
32
35
|
<p align="center">
|
|
@@ -52,6 +55,8 @@ You've probably already tried both. Here's why they hit a ceiling:
|
|
|
52
55
|
| Capability | claude.md | mem-search plugin | **memgit** |
|
|
53
56
|
|---|---|---|---|
|
|
54
57
|
| Loads only relevant context | ❌ loads everything | ⚠️ loads recent observations | ✅ BM25 search — top-k per query |
|
|
58
|
+
| Project-aware across a multi-repo life | ❌ per-file | ❌ | ✅ memories carry a `project`; the current workspace ranks first |
|
|
59
|
+
| Adopt on an existing codebase | ❌ starts blank | ❌ starts blank | ✅ `memgit onboard` — seed the store from the repo in one pass |
|
|
55
60
|
| Version history | ❌ | ❌ | ✅ full commit log |
|
|
56
61
|
| Diff between sessions | ❌ | ❌ | ✅ `memgit diff` |
|
|
57
62
|
| Roll back a wrong memory | ❌ manual edit | ❌ | ✅ `memgit rollback` |
|
|
@@ -166,22 +171,37 @@ pip install memgit
|
|
|
166
171
|
```bash
|
|
167
172
|
# 1. Install and initialize
|
|
168
173
|
pip install memgit
|
|
169
|
-
memgit init # auto-detects best location
|
|
174
|
+
memgit init # auto-detects the best location, finds your existing
|
|
175
|
+
# Claude Code memories, and offers to import them
|
|
170
176
|
|
|
171
|
-
# 2.
|
|
172
|
-
memgit import claude-code ~/.claude/projects/
|
|
173
|
-
|
|
174
|
-
# 3. Register with your AI tools (interactive picker)
|
|
177
|
+
# 2. Register with your AI tools (interactive picker)
|
|
175
178
|
memgit setup
|
|
176
179
|
|
|
177
|
-
#
|
|
180
|
+
# 3. See your token savings
|
|
178
181
|
memgit stats
|
|
179
182
|
```
|
|
180
183
|
|
|
184
|
+
`init` walks you through it — no paths to hunt down. (Importing later is one command with no arguments: `memgit sync` auto-finds `~/.claude/projects/*/memory`.)
|
|
185
|
+
|
|
181
186
|
Restart your AI tool — it now searches your memory store at the start of every session.
|
|
182
187
|
|
|
183
188
|
---
|
|
184
189
|
|
|
190
|
+
## Adopting memgit mid-project
|
|
191
|
+
|
|
192
|
+
Memory tools have a cold-start problem: install one halfway through a project and it knows *nothing* — there's no initial point, and context only trickles in from future sessions. memgit solves this with a one-time seeding pass:
|
|
193
|
+
|
|
194
|
+
```bash
|
|
195
|
+
cd your-project
|
|
196
|
+
memgit onboard # prints the bootstrap brief
|
|
197
|
+
```
|
|
198
|
+
|
|
199
|
+
The brief tells your AI agent exactly what to do: read the README/docs/manifests and recent git history, extract 10–20 durable facts (purpose, architecture, conventions, current state, gotchas), save each as a typed memory, and checkpoint the seed set. Paste it into a session — or don't: if the AI searches memory in a project that has none, the MCP server itself replies with the bootstrap instructions instead of a bare "no results."
|
|
200
|
+
|
|
201
|
+
Memories are **project-scoped**: each carries the workspace it belongs to, searches boost the project you're standing in (global rules still surface), and the resume digest leads with *your current project's* recent work — not whatever repo you touched last night.
|
|
202
|
+
|
|
203
|
+
---
|
|
204
|
+
|
|
185
205
|
## Resume where you left off
|
|
186
206
|
|
|
187
207
|
Ask an AI "can we proceed on the pending tasks?" in a fresh session and it will guess from whatever file happens to be open. `memgit resume` replaces the guess with the record:
|
|
@@ -260,7 +280,8 @@ The tool descriptions teach the AI **judgment** — "does this request depend on
|
|
|
260
280
|
```bash
|
|
261
281
|
# Core (git-like)
|
|
262
282
|
memgit init # initialize store (auto-detects best path)
|
|
263
|
-
memgit
|
|
283
|
+
memgit onboard # bootstrap brief for an existing codebase
|
|
284
|
+
memgit add <slug> <rule> # stage a memory (--body for full detail, --project to scope)
|
|
264
285
|
memgit commit -m "message" # checkpoint current state
|
|
265
286
|
memgit log # history
|
|
266
287
|
memgit diff [sha1] [sha2] # what changed
|
|
@@ -280,8 +301,8 @@ memgit lint # validate all memories
|
|
|
280
301
|
memgit fsck # verify store integrity
|
|
281
302
|
|
|
282
303
|
# Import / export
|
|
283
|
-
memgit sync # sync from Claude Code files + commit
|
|
284
|
-
memgit import claude-code
|
|
304
|
+
memgit sync # sync from Claude Code files + commit (auto-finds them)
|
|
305
|
+
memgit import claude-code [path] # path optional — defaults to ~/.claude/projects/*/memory
|
|
285
306
|
memgit import file <path>
|
|
286
307
|
memgit export <slug>
|
|
287
308
|
|
|
@@ -346,9 +367,11 @@ The same memory in TOON:
|
|
|
346
367
|
```
|
|
347
368
|
TOON1|fb|no-db-mock|2026-07-01T10:00Z
|
|
348
369
|
#testing #database
|
|
370
|
+
PROJ:my-app
|
|
349
371
|
RULE:Never mock the database in tests
|
|
350
372
|
WHY:Mocked tests passed but prod migration failed last quarter
|
|
351
373
|
WHEN:Any persistence test
|
|
374
|
+
BODY:Full long-form detail lives here, losslessly (newlines escaped).\nSearch returns the compact RULE; get_memory returns everything.
|
|
352
375
|
```
|
|
353
376
|
|
|
354
377
|
Measured with a real tokenizer, TOON is ~5–10% leaner than equivalent markdown — a nice bonus, not the headline. **The headline saving is retrieval**: memgit loads the top-8 relevant memories per query instead of everything.
|
|
@@ -408,6 +431,8 @@ See [CONTRIBUTING.md](CONTRIBUTING.md).
|
|
|
408
431
|
- [ ] Chocolatey (not yet live on community.chocolatey.org)
|
|
409
432
|
- [x] Interactive setup wizard (`memgit setup`)
|
|
410
433
|
- [x] Smart `memgit init` (auto-detects tool, no path needed)
|
|
434
|
+
- [x] Lossless memories — full `body` alongside the compact rule (v0.3.0)
|
|
435
|
+
- [x] Project-scoped memories + `memgit onboard` mid-project bootstrap (v0.3.0)
|
|
411
436
|
- [x] VS Code extension (v0.1.5, Marketplace: code416-memgit.memgit)
|
|
412
437
|
- [ ] JetBrains plugin (Phase 3)
|
|
413
438
|
- [ ] Semantic search via embeddings (Phase 4)
|
|
@@ -21,6 +21,8 @@ You've probably already tried both. Here's why they hit a ceiling:
|
|
|
21
21
|
| Capability | claude.md | mem-search plugin | **memgit** |
|
|
22
22
|
|---|---|---|---|
|
|
23
23
|
| Loads only relevant context | ❌ loads everything | ⚠️ loads recent observations | ✅ BM25 search — top-k per query |
|
|
24
|
+
| Project-aware across a multi-repo life | ❌ per-file | ❌ | ✅ memories carry a `project`; the current workspace ranks first |
|
|
25
|
+
| Adopt on an existing codebase | ❌ starts blank | ❌ starts blank | ✅ `memgit onboard` — seed the store from the repo in one pass |
|
|
24
26
|
| Version history | ❌ | ❌ | ✅ full commit log |
|
|
25
27
|
| Diff between sessions | ❌ | ❌ | ✅ `memgit diff` |
|
|
26
28
|
| Roll back a wrong memory | ❌ manual edit | ❌ | ✅ `memgit rollback` |
|
|
@@ -135,22 +137,37 @@ pip install memgit
|
|
|
135
137
|
```bash
|
|
136
138
|
# 1. Install and initialize
|
|
137
139
|
pip install memgit
|
|
138
|
-
memgit init # auto-detects best location
|
|
140
|
+
memgit init # auto-detects the best location, finds your existing
|
|
141
|
+
# Claude Code memories, and offers to import them
|
|
139
142
|
|
|
140
|
-
# 2.
|
|
141
|
-
memgit import claude-code ~/.claude/projects/
|
|
142
|
-
|
|
143
|
-
# 3. Register with your AI tools (interactive picker)
|
|
143
|
+
# 2. Register with your AI tools (interactive picker)
|
|
144
144
|
memgit setup
|
|
145
145
|
|
|
146
|
-
#
|
|
146
|
+
# 3. See your token savings
|
|
147
147
|
memgit stats
|
|
148
148
|
```
|
|
149
149
|
|
|
150
|
+
`init` walks you through it — no paths to hunt down. (Importing later is one command with no arguments: `memgit sync` auto-finds `~/.claude/projects/*/memory`.)
|
|
151
|
+
|
|
150
152
|
Restart your AI tool — it now searches your memory store at the start of every session.
|
|
151
153
|
|
|
152
154
|
---
|
|
153
155
|
|
|
156
|
+
## Adopting memgit mid-project
|
|
157
|
+
|
|
158
|
+
Memory tools have a cold-start problem: install one halfway through a project and it knows *nothing* — there's no initial point, and context only trickles in from future sessions. memgit solves this with a one-time seeding pass:
|
|
159
|
+
|
|
160
|
+
```bash
|
|
161
|
+
cd your-project
|
|
162
|
+
memgit onboard # prints the bootstrap brief
|
|
163
|
+
```
|
|
164
|
+
|
|
165
|
+
The brief tells your AI agent exactly what to do: read the README/docs/manifests and recent git history, extract 10–20 durable facts (purpose, architecture, conventions, current state, gotchas), save each as a typed memory, and checkpoint the seed set. Paste it into a session — or don't: if the AI searches memory in a project that has none, the MCP server itself replies with the bootstrap instructions instead of a bare "no results."
|
|
166
|
+
|
|
167
|
+
Memories are **project-scoped**: each carries the workspace it belongs to, searches boost the project you're standing in (global rules still surface), and the resume digest leads with *your current project's* recent work — not whatever repo you touched last night.
|
|
168
|
+
|
|
169
|
+
---
|
|
170
|
+
|
|
154
171
|
## Resume where you left off
|
|
155
172
|
|
|
156
173
|
Ask an AI "can we proceed on the pending tasks?" in a fresh session and it will guess from whatever file happens to be open. `memgit resume` replaces the guess with the record:
|
|
@@ -229,7 +246,8 @@ The tool descriptions teach the AI **judgment** — "does this request depend on
|
|
|
229
246
|
```bash
|
|
230
247
|
# Core (git-like)
|
|
231
248
|
memgit init # initialize store (auto-detects best path)
|
|
232
|
-
memgit
|
|
249
|
+
memgit onboard # bootstrap brief for an existing codebase
|
|
250
|
+
memgit add <slug> <rule> # stage a memory (--body for full detail, --project to scope)
|
|
233
251
|
memgit commit -m "message" # checkpoint current state
|
|
234
252
|
memgit log # history
|
|
235
253
|
memgit diff [sha1] [sha2] # what changed
|
|
@@ -249,8 +267,8 @@ memgit lint # validate all memories
|
|
|
249
267
|
memgit fsck # verify store integrity
|
|
250
268
|
|
|
251
269
|
# Import / export
|
|
252
|
-
memgit sync # sync from Claude Code files + commit
|
|
253
|
-
memgit import claude-code
|
|
270
|
+
memgit sync # sync from Claude Code files + commit (auto-finds them)
|
|
271
|
+
memgit import claude-code [path] # path optional — defaults to ~/.claude/projects/*/memory
|
|
254
272
|
memgit import file <path>
|
|
255
273
|
memgit export <slug>
|
|
256
274
|
|
|
@@ -315,9 +333,11 @@ The same memory in TOON:
|
|
|
315
333
|
```
|
|
316
334
|
TOON1|fb|no-db-mock|2026-07-01T10:00Z
|
|
317
335
|
#testing #database
|
|
336
|
+
PROJ:my-app
|
|
318
337
|
RULE:Never mock the database in tests
|
|
319
338
|
WHY:Mocked tests passed but prod migration failed last quarter
|
|
320
339
|
WHEN:Any persistence test
|
|
340
|
+
BODY:Full long-form detail lives here, losslessly (newlines escaped).\nSearch returns the compact RULE; get_memory returns everything.
|
|
321
341
|
```
|
|
322
342
|
|
|
323
343
|
Measured with a real tokenizer, TOON is ~5–10% leaner than equivalent markdown — a nice bonus, not the headline. **The headline saving is retrieval**: memgit loads the top-8 relevant memories per query instead of everything.
|
|
@@ -377,6 +397,8 @@ See [CONTRIBUTING.md](CONTRIBUTING.md).
|
|
|
377
397
|
- [ ] Chocolatey (not yet live on community.chocolatey.org)
|
|
378
398
|
- [x] Interactive setup wizard (`memgit setup`)
|
|
379
399
|
- [x] Smart `memgit init` (auto-detects tool, no path needed)
|
|
400
|
+
- [x] Lossless memories — full `body` alongside the compact rule (v0.3.0)
|
|
401
|
+
- [x] Project-scoped memories + `memgit onboard` mid-project bootstrap (v0.3.0)
|
|
380
402
|
- [x] VS Code extension (v0.1.5, Marketplace: code416-memgit.memgit)
|
|
381
403
|
- [ ] JetBrains plugin (Phase 3)
|
|
382
404
|
- [ ] Semantic search via embeddings (Phase 4)
|
|
@@ -80,7 +80,35 @@ def init(directory):
|
|
|
80
80
|
return
|
|
81
81
|
repo = Repository.init(path)
|
|
82
82
|
console.print(f'[green]Initialized[/green] memgit store in [cyan]{repo.path}[/cyan]')
|
|
83
|
-
|
|
83
|
+
|
|
84
|
+
# Step-by-step flow: find existing memories automatically and offer the
|
|
85
|
+
# import, instead of making the user discover the right path themselves.
|
|
86
|
+
try:
|
|
87
|
+
from .importer import from_claude_code
|
|
88
|
+
found = from_claude_code()
|
|
89
|
+
except Exception:
|
|
90
|
+
found = []
|
|
91
|
+
if found:
|
|
92
|
+
projects = {m.project for m in found if m.project}
|
|
93
|
+
console.print(f'\nFound [bold]{len(found)}[/bold] existing Claude Code memories '
|
|
94
|
+
f'across [bold]{len(projects)}[/bold] projects '
|
|
95
|
+
f'[dim](~/.claude/projects/*/memory)[/dim]')
|
|
96
|
+
do_import = True
|
|
97
|
+
if sys.stdin.isatty():
|
|
98
|
+
do_import = click.confirm('Import them now?', default=True)
|
|
99
|
+
if do_import:
|
|
100
|
+
count, skipped, renamed = _stage_imported(repo, found)
|
|
101
|
+
sha = repo.commit(message=f'onboard: imported {count} Claude Code memories',
|
|
102
|
+
trigger='import')
|
|
103
|
+
console.print(f'[green]Imported {count} memories[/green]'
|
|
104
|
+
+ (f' [dim]checkpoint {sha[:8]}[/dim]' if sha else ''))
|
|
105
|
+
else:
|
|
106
|
+
console.print('[dim]Skipped — run [bold]memgit sync[/bold] anytime to import.[/dim]')
|
|
107
|
+
|
|
108
|
+
console.print(f'\n[bold]Next steps[/bold]')
|
|
109
|
+
console.print(f' 1. [bold]memgit setup[/bold] register with your AI tools (interactive)')
|
|
110
|
+
console.print(f' 2. [bold]memgit onboard[/bold] seed memories for a project that has none')
|
|
111
|
+
console.print(f' 3. [bold]memgit stats[/bold] see what you saved')
|
|
84
112
|
|
|
85
113
|
|
|
86
114
|
# ── add ───────────────────────────────────────────────────────────────────────
|
|
@@ -96,7 +124,11 @@ def init(directory):
|
|
|
96
124
|
@click.option('--tags', default=None, help='Comma-separated tags')
|
|
97
125
|
@click.option('--priority', '-p', default=2, type=click.IntRange(1, 3),
|
|
98
126
|
help='1=low 2=medium 3=critical (always loaded)')
|
|
99
|
-
|
|
127
|
+
@click.option('--body', '-b', default=None,
|
|
128
|
+
help='Full long-form detail (multi-line ok, or "-" to read stdin)')
|
|
129
|
+
@click.option('--project', '-P', default=None,
|
|
130
|
+
help='Project this memory belongs to (see `memgit list`)')
|
|
131
|
+
def add(slug, rule, type_code, why, when, tags, priority, body, project):
|
|
100
132
|
"""Add or update a mnemonic.
|
|
101
133
|
|
|
102
134
|
SLUG kebab-case identifier (e.g. ig-pipeline-no-fallback)\n
|
|
@@ -104,6 +136,8 @@ def add(slug, rule, type_code, why, when, tags, priority):
|
|
|
104
136
|
"""
|
|
105
137
|
repo = _require_repo()
|
|
106
138
|
tag_list = [t.strip() for t in tags.split(',')] if tags else []
|
|
139
|
+
if body == '-':
|
|
140
|
+
body = sys.stdin.read().strip() or None
|
|
107
141
|
|
|
108
142
|
m = Mnemonic(
|
|
109
143
|
type_code=type_code,
|
|
@@ -114,6 +148,8 @@ def add(slug, rule, type_code, why, when, tags, priority):
|
|
|
114
148
|
when=when,
|
|
115
149
|
tags=tag_list,
|
|
116
150
|
priority=priority,
|
|
151
|
+
body=body,
|
|
152
|
+
project=project,
|
|
117
153
|
)
|
|
118
154
|
sha = repo.add(m)
|
|
119
155
|
console.print(f'[green]staged[/green] {slug} [{sha[:8]}]')
|
|
@@ -204,17 +240,24 @@ def status():
|
|
|
204
240
|
@click.option('--recent', '-r', default=10, help='Recently updated memories to include')
|
|
205
241
|
@click.option('--plain', is_flag=True, help='Plain text (for hooks / piping into an AI context)')
|
|
206
242
|
@click.option('--json', 'fmt_json', is_flag=True, help='JSON output')
|
|
207
|
-
|
|
243
|
+
@click.option('--project', '-P', default=None,
|
|
244
|
+
help='Prefer this project\'s memories in the digest '
|
|
245
|
+
'(default: derived from the current directory)')
|
|
246
|
+
def resume(checkpoints, recent, plain, fmt_json, project):
|
|
208
247
|
"""Show where you left off — the session-start primer.
|
|
209
248
|
|
|
210
249
|
Prints the last checkpoints, staged work in flight, recently updated
|
|
211
250
|
memories, and critical rules. Designed to orient an AI agent (or you)
|
|
212
251
|
at the start of a session. Wire it into Claude Code automatically with
|
|
213
|
-
`memgit setup hooks`.
|
|
252
|
+
`memgit setup hooks`. Runs project-aware: the recent-memories section
|
|
253
|
+
leads with the project you are standing in.
|
|
214
254
|
"""
|
|
215
255
|
import json as _j
|
|
256
|
+
from .importer import project_label_from_path
|
|
216
257
|
repo = _require_repo()
|
|
217
|
-
|
|
258
|
+
if project is None:
|
|
259
|
+
project = project_label_from_path(Path.cwd())
|
|
260
|
+
ctx = repo.resume_context(checkpoints=checkpoints, recent=recent, project=project)
|
|
218
261
|
|
|
219
262
|
if fmt_json:
|
|
220
263
|
print(_j.dumps(ctx, indent=2, default=str))
|
|
@@ -271,9 +314,10 @@ def _format_resume_plain(ctx: dict) -> str:
|
|
|
271
314
|
def clip(text: str, n: int = 200) -> str:
|
|
272
315
|
return text if len(text) <= n else text[:n - 1] + '…'
|
|
273
316
|
|
|
317
|
+
proj = f' · project {ctx["project"]}' if ctx.get('project') else ''
|
|
274
318
|
lines = [
|
|
275
319
|
f'# memgit resume — thread {ctx["thread"]} @ {ctx["head"] or "none"} '
|
|
276
|
-
f'({ctx["checkpoint_count"]} checkpoints, {ctx["total_memories"]} memories)',
|
|
320
|
+
f'({ctx["checkpoint_count"]} checkpoints, {ctx["total_memories"]} memories{proj})',
|
|
277
321
|
]
|
|
278
322
|
st = ctx['staged']
|
|
279
323
|
if st['new'] or st['updated'] or st['removed']:
|
|
@@ -296,7 +340,10 @@ def _format_resume_plain(ctx: dict) -> str:
|
|
|
296
340
|
lines.append('## Recently updated memories')
|
|
297
341
|
for m in ctx['recent_memories']:
|
|
298
342
|
ts = m['timestamp'].strftime('%Y-%m-%d')
|
|
299
|
-
|
|
343
|
+
# flag memories from OTHER projects so the agent doesn't conflate them
|
|
344
|
+
other = (f' [{m["project"]}]'
|
|
345
|
+
if m.get('project') and m['project'] != ctx.get('project') else '')
|
|
346
|
+
lines.append(f'- {m["slug"]} ({m["type"]}, {ts}){other}: {clip(m["rule"], 160)}')
|
|
300
347
|
if ctx['critical_memories']:
|
|
301
348
|
lines.append('')
|
|
302
349
|
lines.append('## Critical rules — always apply')
|
|
@@ -314,6 +361,72 @@ def _format_resume_plain(ctx: dict) -> str:
|
|
|
314
361
|
return '\n'.join(lines)
|
|
315
362
|
|
|
316
363
|
|
|
364
|
+
# ── onboard ───────────────────────────────────────────────────────────────────
|
|
365
|
+
|
|
366
|
+
ONBOARD_BRIEF = """\
|
|
367
|
+
# memgit onboard — bootstrap memory for {project}
|
|
368
|
+
|
|
369
|
+
This project has {count} memories in memgit{count_note}. A memory store that
|
|
370
|
+
starts empty mid-project is useless until it is seeded — do that now, once,
|
|
371
|
+
and every future session (in any AI tool) starts oriented.
|
|
372
|
+
|
|
373
|
+
## Instructions for the AI operator
|
|
374
|
+
|
|
375
|
+
Extract 10–20 DURABLE facts about this project and save each one as a memory
|
|
376
|
+
(via the memgit MCP `save_memory` tool, or `memgit add` in a shell).
|
|
377
|
+
Read, in this order, whatever exists:
|
|
378
|
+
|
|
379
|
+
1. README / CLAUDE.md / CONTRIBUTING / docs/ — purpose, architecture, setup
|
|
380
|
+
2. Package manifests (package.json, pyproject.toml, go.mod, …) — stack, entry points, scripts
|
|
381
|
+
3. `git log --oneline -30` and recent PRs — what is being worked on RIGHT NOW
|
|
382
|
+
4. Config/env samples, CI files — deploy targets, environments, gates
|
|
383
|
+
5. The code layout itself — modules, boundaries, naming conventions
|
|
384
|
+
|
|
385
|
+
## What to save (one memory each, not a dump)
|
|
386
|
+
|
|
387
|
+
- `pj` project: what this project IS, its goal, its current state / active work
|
|
388
|
+
- `cn` convention: code style, naming, architecture rules an AI must follow
|
|
389
|
+
- `rf` reference: key entry points, dashboards, external services, URLs
|
|
390
|
+
- `fb` feedback: known constraints ("never touch X", "Y is production")
|
|
391
|
+
- `lx` lesson: past incidents or gotchas documented in the repo
|
|
392
|
+
|
|
393
|
+
Rules for good memories: one fact per memory; kebab-case slug; a one-line
|
|
394
|
+
`rule` stating the fact; details in `body`; set `project` to "{project}";
|
|
395
|
+
priority 3 ONLY for always-apply safety rules; tag with real topics.
|
|
396
|
+
|
|
397
|
+
## Finish
|
|
398
|
+
|
|
399
|
+
Checkpoint the seed set so it is versioned from day one:
|
|
400
|
+
|
|
401
|
+
memgit commit -m "onboard: {project}"
|
|
402
|
+
|
|
403
|
+
Then verify: `memgit search "<something about this project>"` should hit.
|
|
404
|
+
"""
|
|
405
|
+
|
|
406
|
+
|
|
407
|
+
@cli.command()
|
|
408
|
+
@click.option('--project', '-P', default=None,
|
|
409
|
+
help='Project label (default: derived from the current directory)')
|
|
410
|
+
@click.option('--path', 'proj_path', default='.', type=click.Path(exists=True),
|
|
411
|
+
help='Project directory to onboard (default: cwd)')
|
|
412
|
+
def onboard(project, proj_path):
|
|
413
|
+
"""Print the bootstrap brief for adopting memgit on an existing project.
|
|
414
|
+
|
|
415
|
+
memgit only knows what has been saved — a project adopted midway starts
|
|
416
|
+
with zero context. This prints a step-by-step brief for an AI agent (or
|
|
417
|
+
you) to seed the store from the codebase: README, docs, git history,
|
|
418
|
+
conventions. Paste it into your AI session, or run
|
|
419
|
+
`memgit onboard | pbcopy`.
|
|
420
|
+
"""
|
|
421
|
+
from .importer import project_label_from_path
|
|
422
|
+
|
|
423
|
+
repo = _require_repo()
|
|
424
|
+
label = project or project_label_from_path(Path(proj_path)) or Path(proj_path).resolve().name
|
|
425
|
+
count = sum(1 for m in repo.list() if m.project == label)
|
|
426
|
+
count_note = '' if count else ' — it is a blank slate for this project'
|
|
427
|
+
print(ONBOARD_BRIEF.format(project=label, count=count, count_note=count_note))
|
|
428
|
+
|
|
429
|
+
|
|
317
430
|
# ── log ───────────────────────────────────────────────────────────────────────
|
|
318
431
|
|
|
319
432
|
@cli.command()
|
|
@@ -454,7 +567,8 @@ def show(slug, toon, fmt_markdown):
|
|
|
454
567
|
else:
|
|
455
568
|
sha_s = m.sha[:8] if m.sha else '?'
|
|
456
569
|
p_label = {1: 'low', 2: 'medium', 3: '[bold red]CRITICAL[/bold red]'}[m.priority]
|
|
457
|
-
|
|
570
|
+
proj = f' project={m.project}' if m.project else ''
|
|
571
|
+
console.print(f'[bold cyan]{m.slug}[/bold cyan] [{m.type_code}] priority={p_label}{proj} sha={sha_s}')
|
|
458
572
|
console.print(f'')
|
|
459
573
|
console.print(f'[bold]RULE[/bold] {m.rule}')
|
|
460
574
|
if m.why:
|
|
@@ -463,6 +577,9 @@ def show(slug, toon, fmt_markdown):
|
|
|
463
577
|
console.print(f'[bold]WHEN[/bold] {m.when}')
|
|
464
578
|
if m.desc:
|
|
465
579
|
console.print(f'[bold]DESC[/bold] {m.desc}')
|
|
580
|
+
if m.body:
|
|
581
|
+
console.print(f'\n[bold]BODY[/bold]')
|
|
582
|
+
console.print(m.body)
|
|
466
583
|
if m.who:
|
|
467
584
|
console.print(f'[bold]WHO[/bold] {m.who}')
|
|
468
585
|
if m.where:
|
|
@@ -486,8 +603,9 @@ def show(slug, toon, fmt_markdown):
|
|
|
486
603
|
type=click.Choice(['fb', 'us', 'pj', 'rf', 'cn', 'lx']),
|
|
487
604
|
help='Filter by type')
|
|
488
605
|
@click.option('--priority', '-p', default=None, type=click.IntRange(1, 3), help='Filter by priority')
|
|
606
|
+
@click.option('--project', '-P', 'project_filter', default=None, help='Filter by project')
|
|
489
607
|
@click.option('--toon', is_flag=True, help='Show TOON format')
|
|
490
|
-
def list_cmd(type_filter, priority, toon):
|
|
608
|
+
def list_cmd(type_filter, priority, project_filter, toon):
|
|
491
609
|
"""List all mnemonics in the current thread."""
|
|
492
610
|
repo = _require_repo()
|
|
493
611
|
mnemonics = repo.list()
|
|
@@ -495,6 +613,8 @@ def list_cmd(type_filter, priority, toon):
|
|
|
495
613
|
mnemonics = [m for m in mnemonics if m.type_code == type_filter]
|
|
496
614
|
if priority:
|
|
497
615
|
mnemonics = [m for m in mnemonics if m.priority == priority]
|
|
616
|
+
if project_filter:
|
|
617
|
+
mnemonics = [m for m in mnemonics if m.project == project_filter]
|
|
498
618
|
mnemonics.sort(key=lambda m: (m.type_code, m.slug))
|
|
499
619
|
|
|
500
620
|
if not mnemonics:
|
|
@@ -511,12 +631,14 @@ def list_cmd(type_filter, priority, toon):
|
|
|
511
631
|
table.add_column('Slug', style='cyan', min_width=20)
|
|
512
632
|
table.add_column('T', width=2)
|
|
513
633
|
table.add_column('P', width=1)
|
|
514
|
-
table.add_column('
|
|
634
|
+
table.add_column('Project', style='dim', max_width=18)
|
|
635
|
+
table.add_column('Rule', max_width=60)
|
|
515
636
|
|
|
516
637
|
for m in mnemonics:
|
|
517
638
|
p_str = '!' if m.priority == 3 else str(m.priority)
|
|
518
|
-
|
|
519
|
-
|
|
639
|
+
proj = (m.project or '')[:18]
|
|
640
|
+
rule_preview = m.rule[:58] + '..' if len(m.rule) > 58 else m.rule
|
|
641
|
+
table.add_row(m.slug, m.type_code, p_str, proj, rule_preview)
|
|
520
642
|
|
|
521
643
|
console.print(table)
|
|
522
644
|
console.print(f'\n[dim]{len(mnemonics)} mnemonic{"s" if len(mnemonics) != 1 else ""}[/dim]')
|
|
@@ -557,13 +679,11 @@ def import_claude_code(path, dry_run, no_commit):
|
|
|
557
679
|
console.print(f' [cyan]{m.slug}[/cyan] [{m.type_code}] {rule_preview}')
|
|
558
680
|
return
|
|
559
681
|
|
|
560
|
-
count =
|
|
561
|
-
for
|
|
562
|
-
|
|
563
|
-
|
|
564
|
-
|
|
565
|
-
except Exception as e:
|
|
566
|
-
err.print(f'[yellow]skip {m.slug}: {e}[/yellow]')
|
|
682
|
+
count, skipped, renamed = _stage_imported(repo, mnemonics)
|
|
683
|
+
for slug in renamed:
|
|
684
|
+
console.print(f' [yellow]collision[/yellow] stored as [cyan]{slug}[/cyan]')
|
|
685
|
+
if skipped:
|
|
686
|
+
err.print(f'[yellow]{skipped} skipped (parse/stage errors)[/yellow]')
|
|
567
687
|
|
|
568
688
|
console.print(f'[green]Staged {count} memories[/green]')
|
|
569
689
|
|
|
@@ -722,7 +842,9 @@ import re # noqa: E402 — needed for lint command
|
|
|
722
842
|
@click.option('--type', '-t', 'type_filter', default=None,
|
|
723
843
|
type=click.Choice(['fb', 'us', 'pj', 'rf', 'cn', 'lx']),
|
|
724
844
|
help='Filter by type before scoring')
|
|
725
|
-
|
|
845
|
+
@click.option('--project', '-P', 'project_filter', default=None,
|
|
846
|
+
help='Only memories from this project (as shown in `memgit list`)')
|
|
847
|
+
def search(query, top, toon, fmt_json, type_filter, project_filter):
|
|
726
848
|
"""Search memories by relevance.
|
|
727
849
|
|
|
728
850
|
Returns the top-k mnemonics scored against QUERY using BM25.
|
|
@@ -734,6 +856,8 @@ def search(query, top, toon, fmt_json, type_filter):
|
|
|
734
856
|
mnemonics = repo.list()
|
|
735
857
|
if type_filter:
|
|
736
858
|
mnemonics = [m for m in mnemonics if m.type_code == type_filter]
|
|
859
|
+
if project_filter:
|
|
860
|
+
mnemonics = [m for m in mnemonics if m.project == project_filter]
|
|
737
861
|
|
|
738
862
|
results = bm25_score(query, mnemonics, top_k=top)
|
|
739
863
|
|
|
@@ -782,6 +906,49 @@ def search(query, top, toon, fmt_json, type_filter):
|
|
|
782
906
|
|
|
783
907
|
# ── sync ──────────────────────────────────────────────────────────────────────
|
|
784
908
|
|
|
909
|
+
def _stage_imported(repo, mnemonics) -> tuple[int, int, list[str]]:
|
|
910
|
+
"""Stage imported mnemonics, re-slugging cross-project collisions.
|
|
911
|
+
|
|
912
|
+
If an incoming slug already belongs to a DIFFERENT project, the incoming
|
|
913
|
+
memory is stored as '<slug>--<project>' instead of silently overwriting.
|
|
914
|
+
Returns (staged, skipped, renamed_slugs).
|
|
915
|
+
"""
|
|
916
|
+
count = skipped = 0
|
|
917
|
+
renamed: list[str] = []
|
|
918
|
+
for m in mnemonics:
|
|
919
|
+
try:
|
|
920
|
+
existing = repo.get(m.slug)
|
|
921
|
+
if (existing is not None and m.project and existing.project
|
|
922
|
+
and existing.project != m.project):
|
|
923
|
+
suffix = re.sub(r'[^a-z0-9-]+', '-', m.project.lower()).strip('-')
|
|
924
|
+
m.slug = f'{m.slug}--{suffix}'
|
|
925
|
+
renamed.append(m.slug)
|
|
926
|
+
repo.add(m)
|
|
927
|
+
count += 1
|
|
928
|
+
except Exception:
|
|
929
|
+
skipped += 1
|
|
930
|
+
return count, skipped, renamed
|
|
931
|
+
|
|
932
|
+
|
|
933
|
+
def _staged_diff_message(repo) -> Optional[str]:
|
|
934
|
+
"""Build a checkpoint message from what is actually staged vs HEAD."""
|
|
935
|
+
index = repo.get_index()
|
|
936
|
+
committed = repo._mindstate_map(repo.head_sha())
|
|
937
|
+
new = sorted(s for s in index if s not in committed)
|
|
938
|
+
upd = sorted(s for s in index if s in committed and index[s] != committed[s])
|
|
939
|
+
rem = sorted(s for s in committed if s not in index)
|
|
940
|
+
if not (new or upd or rem):
|
|
941
|
+
return None
|
|
942
|
+
changed = new + upd
|
|
943
|
+
preview = ', '.join(changed[:4]) + (', …' if len(changed) > 4 else '')
|
|
944
|
+
parts = [f'+{len(new)}'] if new else []
|
|
945
|
+
if upd:
|
|
946
|
+
parts.append(f'~{len(upd)}')
|
|
947
|
+
if rem:
|
|
948
|
+
parts.append(f'-{len(rem)}')
|
|
949
|
+
return f'sync: {" ".join(parts)} ({preview})' if preview else f'sync: {" ".join(parts)}'
|
|
950
|
+
|
|
951
|
+
|
|
785
952
|
@cli.command()
|
|
786
953
|
@click.option('--message', '-m', default=None, help='Custom checkpoint message')
|
|
787
954
|
@click.option('--dry-run', is_flag=True, help='Show what would be imported, no writes')
|
|
@@ -790,6 +957,7 @@ def sync(message, dry_run):
|
|
|
790
957
|
|
|
791
958
|
Imports all Claude Code markdown memory files, stages changes,
|
|
792
959
|
and creates a checkpoint if anything changed. Safe to run repeatedly.
|
|
960
|
+
The checkpoint message names what actually changed.
|
|
793
961
|
"""
|
|
794
962
|
from .importer import from_claude_code
|
|
795
963
|
|
|
@@ -803,26 +971,22 @@ def sync(message, dry_run):
|
|
|
803
971
|
if dry_run:
|
|
804
972
|
console.print(f'Would import [bold]{len(mnemonics)}[/bold] memories:')
|
|
805
973
|
for m in mnemonics[:10]:
|
|
806
|
-
|
|
974
|
+
proj = f' [dim]{m.project}[/dim]' if m.project else ''
|
|
975
|
+
console.print(f' [cyan]{m.slug}[/cyan] [{m.type_code}]{proj}')
|
|
807
976
|
if len(mnemonics) > 10:
|
|
808
977
|
console.print(f' [dim]… and {len(mnemonics) - 10} more[/dim]')
|
|
809
978
|
return
|
|
810
979
|
|
|
811
|
-
count =
|
|
812
|
-
|
|
813
|
-
|
|
814
|
-
try:
|
|
815
|
-
repo.add(m)
|
|
816
|
-
count += 1
|
|
817
|
-
except Exception:
|
|
818
|
-
skipped += 1
|
|
980
|
+
count, skipped, renamed = _stage_imported(repo, mnemonics)
|
|
981
|
+
for slug in renamed:
|
|
982
|
+
console.print(f' [yellow]collision[/yellow] stored as [cyan]{slug}[/cyan]')
|
|
819
983
|
|
|
820
|
-
msg = message or f'sync: {count} memories from Claude Code'
|
|
984
|
+
msg = message or _staged_diff_message(repo) or f'sync: {count} memories from Claude Code'
|
|
821
985
|
sha = repo.commit(message=msg, trigger='session_end')
|
|
822
986
|
|
|
823
987
|
if sha:
|
|
824
|
-
console.print(f'[green]sync[/green] {sha[:8]} {
|
|
825
|
-
(f'
|
|
988
|
+
console.print(f'[green]sync[/green] {sha[:8]} {msg}' +
|
|
989
|
+
(f' [dim]({skipped} skipped)[/dim]' if skipped else ''))
|
|
826
990
|
else:
|
|
827
991
|
console.print(f'[dim]sync: no changes ({count} memories already current)[/dim]')
|
|
828
992
|
|
|
@@ -987,6 +1151,13 @@ def stats(fmt_json):
|
|
|
987
1151
|
|
|
988
1152
|
t.add_row('Total memories', f'[bold]{s["total"]}[/bold] {type_str}')
|
|
989
1153
|
t.add_row('Priority breakdown', prio_str)
|
|
1154
|
+
by_project = s.get('by_project') or {}
|
|
1155
|
+
if len(by_project) > 1:
|
|
1156
|
+
top = sorted(by_project.items(), key=lambda kv: -kv[1])
|
|
1157
|
+
proj_str = ' · '.join(f'{n} {name}' for name, n in top[:6])
|
|
1158
|
+
if len(top) > 6:
|
|
1159
|
+
proj_str += f' · … {len(top) - 6} more'
|
|
1160
|
+
t.add_row('Projects', f'{len(by_project)} {proj_str}')
|
|
990
1161
|
t.add_row('Checkpoints', f'{ck_count} {first_ts} → {last_ts}')
|
|
991
1162
|
console.print(t)
|
|
992
1163
|
console.print()
|
|
@@ -1810,3 +1981,19 @@ if __name__ == '__main__':
|
|
|
1810
1981
|
# Required for the `python -m memgit.cli` fallback used by _memgit_cmd();
|
|
1811
1982
|
# without it the module imports and exits silently.
|
|
1812
1983
|
cli()
|
|
1984
|
+
|
|
1985
|
+
|
|
1986
|
+
# ── cloud (E2E-encrypted team sync — optional extra) ──────────────────────────
|
|
1987
|
+
|
|
1988
|
+
try:
|
|
1989
|
+
from .cloud.commands import cloud as _cloud_group
|
|
1990
|
+
cli.add_command(_cloud_group)
|
|
1991
|
+
except Exception:
|
|
1992
|
+
# extras missing or broken — plain memgit must keep working untouched
|
|
1993
|
+
@cli.group()
|
|
1994
|
+
def cloud():
|
|
1995
|
+
"""E2E-encrypted team sync — requires: pip install 'memgit[cloud]'"""
|
|
1996
|
+
|
|
1997
|
+
@cloud.command('setup', help="Show how to enable cloud sync.")
|
|
1998
|
+
def _cloud_setup_hint():
|
|
1999
|
+
console.print(r"Install the cloud extra first: [bold]pip install 'memgit\[cloud]'[/bold]")
|