memgit 0.2.0__tar.gz → 0.3.1__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.1}/PKG-INFO +35 -10
- {memgit-0.2.0 → memgit-0.3.1}/README.md +31 -9
- {memgit-0.2.0 → memgit-0.3.1}/memgit/__init__.py +1 -1
- {memgit-0.2.0 → memgit-0.3.1}/memgit/cli.py +256 -32
- memgit-0.3.1/memgit/cloud/__init__.py +6 -0
- memgit-0.3.1/memgit/cloud/client.py +176 -0
- memgit-0.3.1/memgit/cloud/commands.py +379 -0
- memgit-0.3.1/memgit/cloud/crypto.py +129 -0
- memgit-0.3.1/memgit/cloud/state.py +81 -0
- memgit-0.3.1/memgit/cloud/sync.py +239 -0
- memgit-0.3.1/memgit/gitdigest.py +158 -0
- {memgit-0.2.0 → memgit-0.3.1}/memgit/importer.py +71 -11
- {memgit-0.2.0 → memgit-0.3.1}/memgit/mcp_server.py +94 -7
- {memgit-0.2.0 → memgit-0.3.1}/memgit/models.py +3 -1
- {memgit-0.2.0 → memgit-0.3.1}/memgit/repo.py +22 -3
- {memgit-0.2.0 → memgit-0.3.1}/memgit/scorer.py +17 -1
- {memgit-0.2.0 → memgit-0.3.1}/memgit/toon.py +63 -19
- {memgit-0.2.0 → memgit-0.3.1}/memgit.egg-info/PKG-INFO +35 -10
- {memgit-0.2.0 → memgit-0.3.1}/memgit.egg-info/SOURCES.txt +9 -1
- {memgit-0.2.0 → memgit-0.3.1}/memgit.egg-info/requires.txt +4 -0
- {memgit-0.2.0 → memgit-0.3.1}/pyproject.toml +2 -1
- memgit-0.3.1/tests/test_v030.py +376 -0
- {memgit-0.2.0 → memgit-0.3.1}/LICENSE +0 -0
- {memgit-0.2.0 → memgit-0.3.1}/memgit/graph.py +0 -0
- {memgit-0.2.0 → memgit-0.3.1}/memgit/http_server.py +0 -0
- {memgit-0.2.0 → memgit-0.3.1}/memgit/store.py +0 -0
- {memgit-0.2.0 → memgit-0.3.1}/memgit/tokens.py +0 -0
- {memgit-0.2.0 → memgit-0.3.1}/memgit.egg-info/dependency_links.txt +0 -0
- {memgit-0.2.0 → memgit-0.3.1}/memgit.egg-info/entry_points.txt +0 -0
- {memgit-0.2.0 → memgit-0.3.1}/memgit.egg-info/top_level.txt +0 -0
- {memgit-0.2.0 → memgit-0.3.1}/setup.cfg +0 -0
- {memgit-0.2.0 → memgit-0.3.1}/tests/test_advanced.py +0 -0
- {memgit-0.2.0 → memgit-0.3.1}/tests/test_setup.py +0 -0
- {memgit-0.2.0 → memgit-0.3.1}/tests/test_store_repo.py +0 -0
- {memgit-0.2.0 → memgit-0.3.1}/tests/test_toon.py +0 -0
- {memgit-0.2.0 → memgit-0.3.1}/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.1
|
|
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 # mines the repo, prints the bootstrap brief
|
|
197
|
+
```
|
|
198
|
+
|
|
199
|
+
`onboard` first extracts a **repo digest** deterministically — git history (recent commit subjects, hot files/directories by churn, authors, branch, tags), detected stack from manifests, and the docs worth reading — using bounded, read-only probes that stay near-instant even on huge repositories. The brief then tells your AI agent exactly what to do with it: read only the listed files (no tree crawling), 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 # mines the repo, prints the bootstrap brief
|
|
163
|
+
```
|
|
164
|
+
|
|
165
|
+
`onboard` first extracts a **repo digest** deterministically — git history (recent commit subjects, hot files/directories by churn, authors, branch, tags), detected stack from manifests, and the docs worth reading — using bounded, read-only probes that stay near-instant even on huge repositories. The brief then tells your AI agent exactly what to do with it: read only the listed files (no tree crawling), 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,109 @@ 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
|
+
{digest_section}
|
|
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
|
+
{reading_plan}
|
|
378
|
+
|
|
379
|
+
## What to save (one memory each, not a dump)
|
|
380
|
+
|
|
381
|
+
- `pj` project: what this project IS, its goal, its current state / active work
|
|
382
|
+
- `cn` convention: code style, naming, architecture rules an AI must follow
|
|
383
|
+
- `rf` reference: key entry points, dashboards, external services, URLs
|
|
384
|
+
- `fb` feedback: known constraints ("never touch X", "Y is production")
|
|
385
|
+
- `lx` lesson: past incidents or gotchas documented in the repo
|
|
386
|
+
|
|
387
|
+
Rules for good memories: one fact per memory; kebab-case slug; a one-line
|
|
388
|
+
`rule` stating the fact; details in `body`; set `project` to "{project}";
|
|
389
|
+
priority 3 ONLY for always-apply safety rules; tag with real topics.
|
|
390
|
+
|
|
391
|
+
## Finish
|
|
392
|
+
|
|
393
|
+
Checkpoint the seed set so it is versioned from day one:
|
|
394
|
+
|
|
395
|
+
memgit commit -m "onboard: {project}"
|
|
396
|
+
|
|
397
|
+
Then verify: `memgit search "<something about this project>"` should hit.
|
|
398
|
+
"""
|
|
399
|
+
|
|
400
|
+
_READING_PLAN_WITH_DIGEST = """\
|
|
401
|
+
The repo digest above was extracted deterministically from git and the
|
|
402
|
+
filesystem — treat it as ground truth and do NOT re-derive it. On a large
|
|
403
|
+
repo, do NOT crawl the tree. Work only from:
|
|
404
|
+
|
|
405
|
+
1. The "Read these first" files listed in the digest — purpose, architecture, setup
|
|
406
|
+
2. The manifests listed — stack, entry points, scripts, dependencies
|
|
407
|
+
3. The recent commit subjects + hot areas — what is being worked on RIGHT NOW
|
|
408
|
+
(turn these into the "current state / active work" memory)
|
|
409
|
+
4. Config/env samples and CI files if present — deploy targets, environments, gates"""
|
|
410
|
+
|
|
411
|
+
_READING_PLAN_GENERIC = """\
|
|
412
|
+
Read, in this order, whatever exists:
|
|
413
|
+
|
|
414
|
+
1. README / CLAUDE.md / CONTRIBUTING / docs/ — purpose, architecture, setup
|
|
415
|
+
2. Package manifests (package.json, pyproject.toml, go.mod, …) — stack, entry points, scripts
|
|
416
|
+
3. `git log --oneline -30` and recent PRs — what is being worked on RIGHT NOW
|
|
417
|
+
4. Config/env samples, CI files — deploy targets, environments, gates
|
|
418
|
+
5. The code layout itself — modules, boundaries, naming conventions"""
|
|
419
|
+
|
|
420
|
+
|
|
421
|
+
@cli.command()
|
|
422
|
+
@click.option('--project', '-P', default=None,
|
|
423
|
+
help='Project label (default: derived from the current directory)')
|
|
424
|
+
@click.option('--path', 'proj_path', default='.', type=click.Path(exists=True),
|
|
425
|
+
help='Project directory to onboard (default: cwd)')
|
|
426
|
+
@click.option('--json', 'fmt_json', is_flag=True, help='Emit the raw repo digest as JSON')
|
|
427
|
+
def onboard(project, proj_path, fmt_json):
|
|
428
|
+
"""Print the bootstrap brief for adopting memgit on an existing project.
|
|
429
|
+
|
|
430
|
+
memgit only knows what has been saved — a project adopted midway starts
|
|
431
|
+
with zero context. This mines the repo's git history and filesystem
|
|
432
|
+
(bounded and read-only, fast even on huge repos) into a factual digest,
|
|
433
|
+
then prints a step-by-step brief for an AI agent (or you) to seed the
|
|
434
|
+
store from it. Paste it into your AI session, or run
|
|
435
|
+
`memgit onboard | pbcopy`.
|
|
436
|
+
"""
|
|
437
|
+
from .gitdigest import build_digest, format_digest
|
|
438
|
+
from .importer import project_label_from_path
|
|
439
|
+
|
|
440
|
+
repo = _require_repo()
|
|
441
|
+
target = Path(proj_path)
|
|
442
|
+
label = project or project_label_from_path(target) or target.resolve().name
|
|
443
|
+
|
|
444
|
+
digest = build_digest(target)
|
|
445
|
+
if fmt_json:
|
|
446
|
+
import json as _j
|
|
447
|
+
digest['project'] = label
|
|
448
|
+
print(_j.dumps(digest, indent=2))
|
|
449
|
+
return
|
|
450
|
+
|
|
451
|
+
count = sum(1 for m in repo.list() if m.project == label)
|
|
452
|
+
count_note = '' if count else ' — it is a blank slate for this project'
|
|
453
|
+
|
|
454
|
+
rendered = format_digest(digest)
|
|
455
|
+
if rendered:
|
|
456
|
+
digest_section = ('\n## Repo digest (auto-extracted — trust it, don\'t re-derive)\n\n'
|
|
457
|
+
f'{rendered}\n')
|
|
458
|
+
reading_plan = _READING_PLAN_WITH_DIGEST
|
|
459
|
+
else:
|
|
460
|
+
digest_section = ''
|
|
461
|
+
reading_plan = _READING_PLAN_GENERIC
|
|
462
|
+
|
|
463
|
+
print(ONBOARD_BRIEF.format(project=label, count=count, count_note=count_note,
|
|
464
|
+
digest_section=digest_section, reading_plan=reading_plan))
|
|
465
|
+
|
|
466
|
+
|
|
317
467
|
# ── log ───────────────────────────────────────────────────────────────────────
|
|
318
468
|
|
|
319
469
|
@cli.command()
|
|
@@ -454,7 +604,8 @@ def show(slug, toon, fmt_markdown):
|
|
|
454
604
|
else:
|
|
455
605
|
sha_s = m.sha[:8] if m.sha else '?'
|
|
456
606
|
p_label = {1: 'low', 2: 'medium', 3: '[bold red]CRITICAL[/bold red]'}[m.priority]
|
|
457
|
-
|
|
607
|
+
proj = f' project={m.project}' if m.project else ''
|
|
608
|
+
console.print(f'[bold cyan]{m.slug}[/bold cyan] [{m.type_code}] priority={p_label}{proj} sha={sha_s}')
|
|
458
609
|
console.print(f'')
|
|
459
610
|
console.print(f'[bold]RULE[/bold] {m.rule}')
|
|
460
611
|
if m.why:
|
|
@@ -463,6 +614,9 @@ def show(slug, toon, fmt_markdown):
|
|
|
463
614
|
console.print(f'[bold]WHEN[/bold] {m.when}')
|
|
464
615
|
if m.desc:
|
|
465
616
|
console.print(f'[bold]DESC[/bold] {m.desc}')
|
|
617
|
+
if m.body:
|
|
618
|
+
console.print(f'\n[bold]BODY[/bold]')
|
|
619
|
+
console.print(m.body)
|
|
466
620
|
if m.who:
|
|
467
621
|
console.print(f'[bold]WHO[/bold] {m.who}')
|
|
468
622
|
if m.where:
|
|
@@ -486,8 +640,9 @@ def show(slug, toon, fmt_markdown):
|
|
|
486
640
|
type=click.Choice(['fb', 'us', 'pj', 'rf', 'cn', 'lx']),
|
|
487
641
|
help='Filter by type')
|
|
488
642
|
@click.option('--priority', '-p', default=None, type=click.IntRange(1, 3), help='Filter by priority')
|
|
643
|
+
@click.option('--project', '-P', 'project_filter', default=None, help='Filter by project')
|
|
489
644
|
@click.option('--toon', is_flag=True, help='Show TOON format')
|
|
490
|
-
def list_cmd(type_filter, priority, toon):
|
|
645
|
+
def list_cmd(type_filter, priority, project_filter, toon):
|
|
491
646
|
"""List all mnemonics in the current thread."""
|
|
492
647
|
repo = _require_repo()
|
|
493
648
|
mnemonics = repo.list()
|
|
@@ -495,6 +650,8 @@ def list_cmd(type_filter, priority, toon):
|
|
|
495
650
|
mnemonics = [m for m in mnemonics if m.type_code == type_filter]
|
|
496
651
|
if priority:
|
|
497
652
|
mnemonics = [m for m in mnemonics if m.priority == priority]
|
|
653
|
+
if project_filter:
|
|
654
|
+
mnemonics = [m for m in mnemonics if m.project == project_filter]
|
|
498
655
|
mnemonics.sort(key=lambda m: (m.type_code, m.slug))
|
|
499
656
|
|
|
500
657
|
if not mnemonics:
|
|
@@ -511,12 +668,14 @@ def list_cmd(type_filter, priority, toon):
|
|
|
511
668
|
table.add_column('Slug', style='cyan', min_width=20)
|
|
512
669
|
table.add_column('T', width=2)
|
|
513
670
|
table.add_column('P', width=1)
|
|
514
|
-
table.add_column('
|
|
671
|
+
table.add_column('Project', style='dim', max_width=18)
|
|
672
|
+
table.add_column('Rule', max_width=60)
|
|
515
673
|
|
|
516
674
|
for m in mnemonics:
|
|
517
675
|
p_str = '!' if m.priority == 3 else str(m.priority)
|
|
518
|
-
|
|
519
|
-
|
|
676
|
+
proj = (m.project or '')[:18]
|
|
677
|
+
rule_preview = m.rule[:58] + '..' if len(m.rule) > 58 else m.rule
|
|
678
|
+
table.add_row(m.slug, m.type_code, p_str, proj, rule_preview)
|
|
520
679
|
|
|
521
680
|
console.print(table)
|
|
522
681
|
console.print(f'\n[dim]{len(mnemonics)} mnemonic{"s" if len(mnemonics) != 1 else ""}[/dim]')
|
|
@@ -557,13 +716,11 @@ def import_claude_code(path, dry_run, no_commit):
|
|
|
557
716
|
console.print(f' [cyan]{m.slug}[/cyan] [{m.type_code}] {rule_preview}')
|
|
558
717
|
return
|
|
559
718
|
|
|
560
|
-
count =
|
|
561
|
-
for
|
|
562
|
-
|
|
563
|
-
|
|
564
|
-
|
|
565
|
-
except Exception as e:
|
|
566
|
-
err.print(f'[yellow]skip {m.slug}: {e}[/yellow]')
|
|
719
|
+
count, skipped, renamed = _stage_imported(repo, mnemonics)
|
|
720
|
+
for slug in renamed:
|
|
721
|
+
console.print(f' [yellow]collision[/yellow] stored as [cyan]{slug}[/cyan]')
|
|
722
|
+
if skipped:
|
|
723
|
+
err.print(f'[yellow]{skipped} skipped (parse/stage errors)[/yellow]')
|
|
567
724
|
|
|
568
725
|
console.print(f'[green]Staged {count} memories[/green]')
|
|
569
726
|
|
|
@@ -722,7 +879,9 @@ import re # noqa: E402 — needed for lint command
|
|
|
722
879
|
@click.option('--type', '-t', 'type_filter', default=None,
|
|
723
880
|
type=click.Choice(['fb', 'us', 'pj', 'rf', 'cn', 'lx']),
|
|
724
881
|
help='Filter by type before scoring')
|
|
725
|
-
|
|
882
|
+
@click.option('--project', '-P', 'project_filter', default=None,
|
|
883
|
+
help='Only memories from this project (as shown in `memgit list`)')
|
|
884
|
+
def search(query, top, toon, fmt_json, type_filter, project_filter):
|
|
726
885
|
"""Search memories by relevance.
|
|
727
886
|
|
|
728
887
|
Returns the top-k mnemonics scored against QUERY using BM25.
|
|
@@ -734,6 +893,8 @@ def search(query, top, toon, fmt_json, type_filter):
|
|
|
734
893
|
mnemonics = repo.list()
|
|
735
894
|
if type_filter:
|
|
736
895
|
mnemonics = [m for m in mnemonics if m.type_code == type_filter]
|
|
896
|
+
if project_filter:
|
|
897
|
+
mnemonics = [m for m in mnemonics if m.project == project_filter]
|
|
737
898
|
|
|
738
899
|
results = bm25_score(query, mnemonics, top_k=top)
|
|
739
900
|
|
|
@@ -782,6 +943,49 @@ def search(query, top, toon, fmt_json, type_filter):
|
|
|
782
943
|
|
|
783
944
|
# ── sync ──────────────────────────────────────────────────────────────────────
|
|
784
945
|
|
|
946
|
+
def _stage_imported(repo, mnemonics) -> tuple[int, int, list[str]]:
|
|
947
|
+
"""Stage imported mnemonics, re-slugging cross-project collisions.
|
|
948
|
+
|
|
949
|
+
If an incoming slug already belongs to a DIFFERENT project, the incoming
|
|
950
|
+
memory is stored as '<slug>--<project>' instead of silently overwriting.
|
|
951
|
+
Returns (staged, skipped, renamed_slugs).
|
|
952
|
+
"""
|
|
953
|
+
count = skipped = 0
|
|
954
|
+
renamed: list[str] = []
|
|
955
|
+
for m in mnemonics:
|
|
956
|
+
try:
|
|
957
|
+
existing = repo.get(m.slug)
|
|
958
|
+
if (existing is not None and m.project and existing.project
|
|
959
|
+
and existing.project != m.project):
|
|
960
|
+
suffix = re.sub(r'[^a-z0-9-]+', '-', m.project.lower()).strip('-')
|
|
961
|
+
m.slug = f'{m.slug}--{suffix}'
|
|
962
|
+
renamed.append(m.slug)
|
|
963
|
+
repo.add(m)
|
|
964
|
+
count += 1
|
|
965
|
+
except Exception:
|
|
966
|
+
skipped += 1
|
|
967
|
+
return count, skipped, renamed
|
|
968
|
+
|
|
969
|
+
|
|
970
|
+
def _staged_diff_message(repo) -> Optional[str]:
|
|
971
|
+
"""Build a checkpoint message from what is actually staged vs HEAD."""
|
|
972
|
+
index = repo.get_index()
|
|
973
|
+
committed = repo._mindstate_map(repo.head_sha())
|
|
974
|
+
new = sorted(s for s in index if s not in committed)
|
|
975
|
+
upd = sorted(s for s in index if s in committed and index[s] != committed[s])
|
|
976
|
+
rem = sorted(s for s in committed if s not in index)
|
|
977
|
+
if not (new or upd or rem):
|
|
978
|
+
return None
|
|
979
|
+
changed = new + upd
|
|
980
|
+
preview = ', '.join(changed[:4]) + (', …' if len(changed) > 4 else '')
|
|
981
|
+
parts = [f'+{len(new)}'] if new else []
|
|
982
|
+
if upd:
|
|
983
|
+
parts.append(f'~{len(upd)}')
|
|
984
|
+
if rem:
|
|
985
|
+
parts.append(f'-{len(rem)}')
|
|
986
|
+
return f'sync: {" ".join(parts)} ({preview})' if preview else f'sync: {" ".join(parts)}'
|
|
987
|
+
|
|
988
|
+
|
|
785
989
|
@cli.command()
|
|
786
990
|
@click.option('--message', '-m', default=None, help='Custom checkpoint message')
|
|
787
991
|
@click.option('--dry-run', is_flag=True, help='Show what would be imported, no writes')
|
|
@@ -790,6 +994,7 @@ def sync(message, dry_run):
|
|
|
790
994
|
|
|
791
995
|
Imports all Claude Code markdown memory files, stages changes,
|
|
792
996
|
and creates a checkpoint if anything changed. Safe to run repeatedly.
|
|
997
|
+
The checkpoint message names what actually changed.
|
|
793
998
|
"""
|
|
794
999
|
from .importer import from_claude_code
|
|
795
1000
|
|
|
@@ -803,26 +1008,22 @@ def sync(message, dry_run):
|
|
|
803
1008
|
if dry_run:
|
|
804
1009
|
console.print(f'Would import [bold]{len(mnemonics)}[/bold] memories:')
|
|
805
1010
|
for m in mnemonics[:10]:
|
|
806
|
-
|
|
1011
|
+
proj = f' [dim]{m.project}[/dim]' if m.project else ''
|
|
1012
|
+
console.print(f' [cyan]{m.slug}[/cyan] [{m.type_code}]{proj}')
|
|
807
1013
|
if len(mnemonics) > 10:
|
|
808
1014
|
console.print(f' [dim]… and {len(mnemonics) - 10} more[/dim]')
|
|
809
1015
|
return
|
|
810
1016
|
|
|
811
|
-
count =
|
|
812
|
-
|
|
813
|
-
|
|
814
|
-
try:
|
|
815
|
-
repo.add(m)
|
|
816
|
-
count += 1
|
|
817
|
-
except Exception:
|
|
818
|
-
skipped += 1
|
|
1017
|
+
count, skipped, renamed = _stage_imported(repo, mnemonics)
|
|
1018
|
+
for slug in renamed:
|
|
1019
|
+
console.print(f' [yellow]collision[/yellow] stored as [cyan]{slug}[/cyan]')
|
|
819
1020
|
|
|
820
|
-
msg = message or f'sync: {count} memories from Claude Code'
|
|
1021
|
+
msg = message or _staged_diff_message(repo) or f'sync: {count} memories from Claude Code'
|
|
821
1022
|
sha = repo.commit(message=msg, trigger='session_end')
|
|
822
1023
|
|
|
823
1024
|
if sha:
|
|
824
|
-
console.print(f'[green]sync[/green] {sha[:8]} {
|
|
825
|
-
(f'
|
|
1025
|
+
console.print(f'[green]sync[/green] {sha[:8]} {msg}' +
|
|
1026
|
+
(f' [dim]({skipped} skipped)[/dim]' if skipped else ''))
|
|
826
1027
|
else:
|
|
827
1028
|
console.print(f'[dim]sync: no changes ({count} memories already current)[/dim]')
|
|
828
1029
|
|
|
@@ -987,6 +1188,13 @@ def stats(fmt_json):
|
|
|
987
1188
|
|
|
988
1189
|
t.add_row('Total memories', f'[bold]{s["total"]}[/bold] {type_str}')
|
|
989
1190
|
t.add_row('Priority breakdown', prio_str)
|
|
1191
|
+
by_project = s.get('by_project') or {}
|
|
1192
|
+
if len(by_project) > 1:
|
|
1193
|
+
top = sorted(by_project.items(), key=lambda kv: -kv[1])
|
|
1194
|
+
proj_str = ' · '.join(f'{n} {name}' for name, n in top[:6])
|
|
1195
|
+
if len(top) > 6:
|
|
1196
|
+
proj_str += f' · … {len(top) - 6} more'
|
|
1197
|
+
t.add_row('Projects', f'{len(by_project)} {proj_str}')
|
|
990
1198
|
t.add_row('Checkpoints', f'{ck_count} {first_ts} → {last_ts}')
|
|
991
1199
|
console.print(t)
|
|
992
1200
|
console.print()
|
|
@@ -1810,3 +2018,19 @@ if __name__ == '__main__':
|
|
|
1810
2018
|
# Required for the `python -m memgit.cli` fallback used by _memgit_cmd();
|
|
1811
2019
|
# without it the module imports and exits silently.
|
|
1812
2020
|
cli()
|
|
2021
|
+
|
|
2022
|
+
|
|
2023
|
+
# ── cloud (E2E-encrypted team sync — optional extra) ──────────────────────────
|
|
2024
|
+
|
|
2025
|
+
try:
|
|
2026
|
+
from .cloud.commands import cloud as _cloud_group
|
|
2027
|
+
cli.add_command(_cloud_group)
|
|
2028
|
+
except Exception:
|
|
2029
|
+
# extras missing or broken — plain memgit must keep working untouched
|
|
2030
|
+
@cli.group()
|
|
2031
|
+
def cloud():
|
|
2032
|
+
"""E2E-encrypted team sync — requires: pip install 'memgit[cloud]'"""
|
|
2033
|
+
|
|
2034
|
+
@cloud.command('setup', help="Show how to enable cloud sync.")
|
|
2035
|
+
def _cloud_setup_hint():
|
|
2036
|
+
console.print(r"Install the cloud extra first: [bold]pip install 'memgit\[cloud]'[/bold]")
|