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.
Files changed (35) hide show
  1. {memgit-0.2.0 → memgit-0.3.0}/PKG-INFO +35 -10
  2. {memgit-0.2.0 → memgit-0.3.0}/README.md +31 -9
  3. {memgit-0.2.0 → memgit-0.3.0}/memgit/__init__.py +1 -1
  4. {memgit-0.2.0 → memgit-0.3.0}/memgit/cli.py +219 -32
  5. memgit-0.3.0/memgit/cloud/__init__.py +6 -0
  6. memgit-0.3.0/memgit/cloud/client.py +176 -0
  7. memgit-0.3.0/memgit/cloud/commands.py +379 -0
  8. memgit-0.3.0/memgit/cloud/crypto.py +129 -0
  9. memgit-0.3.0/memgit/cloud/state.py +81 -0
  10. memgit-0.3.0/memgit/cloud/sync.py +239 -0
  11. {memgit-0.2.0 → memgit-0.3.0}/memgit/importer.py +71 -11
  12. {memgit-0.2.0 → memgit-0.3.0}/memgit/mcp_server.py +92 -7
  13. {memgit-0.2.0 → memgit-0.3.0}/memgit/models.py +3 -1
  14. {memgit-0.2.0 → memgit-0.3.0}/memgit/repo.py +22 -3
  15. {memgit-0.2.0 → memgit-0.3.0}/memgit/scorer.py +17 -1
  16. {memgit-0.2.0 → memgit-0.3.0}/memgit/toon.py +63 -19
  17. {memgit-0.2.0 → memgit-0.3.0}/memgit.egg-info/PKG-INFO +35 -10
  18. {memgit-0.2.0 → memgit-0.3.0}/memgit.egg-info/SOURCES.txt +8 -1
  19. {memgit-0.2.0 → memgit-0.3.0}/memgit.egg-info/requires.txt +4 -0
  20. {memgit-0.2.0 → memgit-0.3.0}/pyproject.toml +2 -1
  21. memgit-0.3.0/tests/test_v030.py +297 -0
  22. {memgit-0.2.0 → memgit-0.3.0}/LICENSE +0 -0
  23. {memgit-0.2.0 → memgit-0.3.0}/memgit/graph.py +0 -0
  24. {memgit-0.2.0 → memgit-0.3.0}/memgit/http_server.py +0 -0
  25. {memgit-0.2.0 → memgit-0.3.0}/memgit/store.py +0 -0
  26. {memgit-0.2.0 → memgit-0.3.0}/memgit/tokens.py +0 -0
  27. {memgit-0.2.0 → memgit-0.3.0}/memgit.egg-info/dependency_links.txt +0 -0
  28. {memgit-0.2.0 → memgit-0.3.0}/memgit.egg-info/entry_points.txt +0 -0
  29. {memgit-0.2.0 → memgit-0.3.0}/memgit.egg-info/top_level.txt +0 -0
  30. {memgit-0.2.0 → memgit-0.3.0}/setup.cfg +0 -0
  31. {memgit-0.2.0 → memgit-0.3.0}/tests/test_advanced.py +0 -0
  32. {memgit-0.2.0 → memgit-0.3.0}/tests/test_setup.py +0 -0
  33. {memgit-0.2.0 → memgit-0.3.0}/tests/test_store_repo.py +0 -0
  34. {memgit-0.2.0 → memgit-0.3.0}/tests/test_toon.py +0 -0
  35. {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.2.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 (~/.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 # 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 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 # 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 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.0"
@@ -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,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
- console.print(f'[bold cyan]{m.slug}[/bold cyan] [{m.type_code}] priority={p_label} sha={sha_s}')
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('Rule', max_width=70)
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
- 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)
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 = 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]')
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
- def search(query, top, toon, fmt_json, type_filter):
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
- console.print(f' [cyan]{m.slug}[/cyan] [{m.type_code}]')
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 = 0
812
- skipped = 0
813
- for m in mnemonics:
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]} {count} staged' +
825
- (f', {skipped} skipped' if skipped else ''))
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]")
@@ -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
+ """