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.
Files changed (36) hide show
  1. {memgit-0.2.0 → memgit-0.3.1}/PKG-INFO +35 -10
  2. {memgit-0.2.0 → memgit-0.3.1}/README.md +31 -9
  3. {memgit-0.2.0 → memgit-0.3.1}/memgit/__init__.py +1 -1
  4. {memgit-0.2.0 → memgit-0.3.1}/memgit/cli.py +256 -32
  5. memgit-0.3.1/memgit/cloud/__init__.py +6 -0
  6. memgit-0.3.1/memgit/cloud/client.py +176 -0
  7. memgit-0.3.1/memgit/cloud/commands.py +379 -0
  8. memgit-0.3.1/memgit/cloud/crypto.py +129 -0
  9. memgit-0.3.1/memgit/cloud/state.py +81 -0
  10. memgit-0.3.1/memgit/cloud/sync.py +239 -0
  11. memgit-0.3.1/memgit/gitdigest.py +158 -0
  12. {memgit-0.2.0 → memgit-0.3.1}/memgit/importer.py +71 -11
  13. {memgit-0.2.0 → memgit-0.3.1}/memgit/mcp_server.py +94 -7
  14. {memgit-0.2.0 → memgit-0.3.1}/memgit/models.py +3 -1
  15. {memgit-0.2.0 → memgit-0.3.1}/memgit/repo.py +22 -3
  16. {memgit-0.2.0 → memgit-0.3.1}/memgit/scorer.py +17 -1
  17. {memgit-0.2.0 → memgit-0.3.1}/memgit/toon.py +63 -19
  18. {memgit-0.2.0 → memgit-0.3.1}/memgit.egg-info/PKG-INFO +35 -10
  19. {memgit-0.2.0 → memgit-0.3.1}/memgit.egg-info/SOURCES.txt +9 -1
  20. {memgit-0.2.0 → memgit-0.3.1}/memgit.egg-info/requires.txt +4 -0
  21. {memgit-0.2.0 → memgit-0.3.1}/pyproject.toml +2 -1
  22. memgit-0.3.1/tests/test_v030.py +376 -0
  23. {memgit-0.2.0 → memgit-0.3.1}/LICENSE +0 -0
  24. {memgit-0.2.0 → memgit-0.3.1}/memgit/graph.py +0 -0
  25. {memgit-0.2.0 → memgit-0.3.1}/memgit/http_server.py +0 -0
  26. {memgit-0.2.0 → memgit-0.3.1}/memgit/store.py +0 -0
  27. {memgit-0.2.0 → memgit-0.3.1}/memgit/tokens.py +0 -0
  28. {memgit-0.2.0 → memgit-0.3.1}/memgit.egg-info/dependency_links.txt +0 -0
  29. {memgit-0.2.0 → memgit-0.3.1}/memgit.egg-info/entry_points.txt +0 -0
  30. {memgit-0.2.0 → memgit-0.3.1}/memgit.egg-info/top_level.txt +0 -0
  31. {memgit-0.2.0 → memgit-0.3.1}/setup.cfg +0 -0
  32. {memgit-0.2.0 → memgit-0.3.1}/tests/test_advanced.py +0 -0
  33. {memgit-0.2.0 → memgit-0.3.1}/tests/test_setup.py +0 -0
  34. {memgit-0.2.0 → memgit-0.3.1}/tests/test_store_repo.py +0 -0
  35. {memgit-0.2.0 → memgit-0.3.1}/tests/test_toon.py +0 -0
  36. {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.2.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 (~/.claude/memgit-store etc.)
174
+ memgit init # auto-detects the best location, finds your existing
175
+ # Claude Code memories, and offers to import them
170
176
 
171
- # 2. Import existing memories (if you use Claude Code)
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
- # 4. See your token savings
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 add <slug> <rule> # stage a memory
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 <path>
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 (~/.claude/memgit-store etc.)
140
+ memgit init # auto-detects the best location, finds your existing
141
+ # Claude Code memories, and offers to import them
139
142
 
140
- # 2. Import existing memories (if you use Claude Code)
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
- # 4. See your token savings
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 add <slug> <rule> # stage a memory
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 <path>
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)
@@ -1,3 +1,3 @@
1
1
  """memgit — git for AI memory."""
2
2
 
3
- __version__ = "0.1.0"
3
+ __version__ = "0.3.1"
@@ -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
- console.print(f'[dim]Run [bold]memgit setup[/bold] to register with your AI tools.[/dim]')
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
- def add(slug, rule, type_code, why, when, tags, priority):
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
- def resume(checkpoints, recent, plain, fmt_json):
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
- ctx = repo.resume_context(checkpoints=checkpoints, recent=recent)
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
- lines.append(f'- {m["slug"]} ({m["type"]}, {ts}): {clip(m["rule"], 160)}')
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
- console.print(f'[bold cyan]{m.slug}[/bold cyan] [{m.type_code}] priority={p_label} sha={sha_s}')
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('Rule', max_width=70)
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
- rule_preview = m.rule[:68] + '..' if len(m.rule) > 68 else m.rule
519
- table.add_row(m.slug, m.type_code, p_str, rule_preview)
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 = 0
561
- for m in mnemonics:
562
- try:
563
- repo.add(m)
564
- count += 1
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
- def search(query, top, toon, fmt_json, type_filter):
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
- console.print(f' [cyan]{m.slug}[/cyan] [{m.type_code}]')
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 = 0
812
- skipped = 0
813
- for m in mnemonics:
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]} {count} staged' +
825
- (f', {skipped} skipped' if skipped else ''))
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]")
@@ -0,0 +1,6 @@
1
+ """memgit cloud — E2E-encrypted team sync (app.memgit.dev).
2
+
3
+ Everything in this package is optional: it is only imported when a `memgit cloud …`
4
+ command runs, and requires the `memgit[cloud]` extra (pynacl + httpx). Core memgit
5
+ never imports it.
6
+ """