memgit 0.3.0__tar.gz → 0.4.0__tar.gz
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- {memgit-0.3.0 → memgit-0.4.0}/PKG-INFO +17 -7
- {memgit-0.3.0 → memgit-0.4.0}/README.md +16 -6
- {memgit-0.3.0 → memgit-0.4.0}/memgit/__init__.py +1 -1
- {memgit-0.3.0 → memgit-0.4.0}/memgit/cli.py +205 -75
- memgit-0.4.0/memgit/gitdigest.py +158 -0
- memgit-0.4.0/memgit/hooks.py +212 -0
- {memgit-0.3.0 → memgit-0.4.0}/memgit/importer.py +11 -31
- {memgit-0.3.0 → memgit-0.4.0}/memgit/mcp_server.py +33 -5
- memgit-0.4.0/memgit/project.py +90 -0
- {memgit-0.3.0 → memgit-0.4.0}/memgit/repo.py +44 -9
- {memgit-0.3.0 → memgit-0.4.0}/memgit/scorer.py +13 -4
- {memgit-0.3.0 → memgit-0.4.0}/memgit/toon.py +33 -6
- {memgit-0.3.0 → memgit-0.4.0}/memgit.egg-info/PKG-INFO +17 -7
- {memgit-0.3.0 → memgit-0.4.0}/memgit.egg-info/SOURCES.txt +5 -1
- {memgit-0.3.0 → memgit-0.4.0}/pyproject.toml +1 -1
- {memgit-0.3.0 → memgit-0.4.0}/tests/test_v030.py +79 -0
- memgit-0.4.0/tests/test_v040.py +371 -0
- {memgit-0.3.0 → memgit-0.4.0}/LICENSE +0 -0
- {memgit-0.3.0 → memgit-0.4.0}/memgit/cloud/__init__.py +0 -0
- {memgit-0.3.0 → memgit-0.4.0}/memgit/cloud/client.py +0 -0
- {memgit-0.3.0 → memgit-0.4.0}/memgit/cloud/commands.py +0 -0
- {memgit-0.3.0 → memgit-0.4.0}/memgit/cloud/crypto.py +0 -0
- {memgit-0.3.0 → memgit-0.4.0}/memgit/cloud/state.py +0 -0
- {memgit-0.3.0 → memgit-0.4.0}/memgit/cloud/sync.py +0 -0
- {memgit-0.3.0 → memgit-0.4.0}/memgit/graph.py +0 -0
- {memgit-0.3.0 → memgit-0.4.0}/memgit/http_server.py +0 -0
- {memgit-0.3.0 → memgit-0.4.0}/memgit/models.py +0 -0
- {memgit-0.3.0 → memgit-0.4.0}/memgit/store.py +0 -0
- {memgit-0.3.0 → memgit-0.4.0}/memgit/tokens.py +0 -0
- {memgit-0.3.0 → memgit-0.4.0}/memgit.egg-info/dependency_links.txt +0 -0
- {memgit-0.3.0 → memgit-0.4.0}/memgit.egg-info/entry_points.txt +0 -0
- {memgit-0.3.0 → memgit-0.4.0}/memgit.egg-info/requires.txt +0 -0
- {memgit-0.3.0 → memgit-0.4.0}/memgit.egg-info/top_level.txt +0 -0
- {memgit-0.3.0 → memgit-0.4.0}/setup.cfg +0 -0
- {memgit-0.3.0 → memgit-0.4.0}/tests/test_advanced.py +0 -0
- {memgit-0.3.0 → memgit-0.4.0}/tests/test_setup.py +0 -0
- {memgit-0.3.0 → memgit-0.4.0}/tests/test_store_repo.py +0 -0
- {memgit-0.3.0 → memgit-0.4.0}/tests/test_toon.py +0 -0
- {memgit-0.3.0 → memgit-0.4.0}/tests/test_v020.py +0 -0
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
Metadata-Version: 2.4
|
|
2
2
|
Name: memgit
|
|
3
|
-
Version: 0.
|
|
3
|
+
Version: 0.4.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
|
|
@@ -193,10 +193,10 @@ Memory tools have a cold-start problem: install one halfway through a project an
|
|
|
193
193
|
|
|
194
194
|
```bash
|
|
195
195
|
cd your-project
|
|
196
|
-
memgit onboard # prints the bootstrap brief
|
|
196
|
+
memgit onboard # mines the repo, prints the bootstrap brief
|
|
197
197
|
```
|
|
198
198
|
|
|
199
|
-
The brief tells your AI agent exactly what to do: read the
|
|
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
200
|
|
|
201
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
202
|
|
|
@@ -212,13 +212,22 @@ memgit resume --plain # plain text, for piping into an AI context
|
|
|
212
212
|
memgit resume --json # for tooling
|
|
213
213
|
```
|
|
214
214
|
|
|
215
|
-
Wire it into Claude Code so
|
|
215
|
+
Wire it into Claude Code so memory becomes **automatic** — no tool call, no judgment required:
|
|
216
216
|
|
|
217
217
|
```bash
|
|
218
|
-
memgit setup hooks # installs
|
|
218
|
+
memgit setup hooks # installs all four hooks (~/.claude/settings.json)
|
|
219
219
|
```
|
|
220
220
|
|
|
221
|
-
|
|
221
|
+
| Hook | What it enforces |
|
|
222
|
+
|---|---|
|
|
223
|
+
| `SessionStart` | every session opens with the resume digest in context |
|
|
224
|
+
| `UserPromptSubmit` | each prompt is BM25-matched against the store; relevant memories are injected (silent when nothing clears the relevance bar; never repeats within a session) — `--no-recall` to skip |
|
|
225
|
+
| `Stop` (guard) | a session that did real work but saved nothing gets ONE nudge to save durable facts before finishing — `--no-guard` to skip |
|
|
226
|
+
| `Stop` (sync) | markdown memories are checkpointed asynchronously at session end |
|
|
227
|
+
|
|
228
|
+
Why hooks and not just good tool descriptions? We measured it: across 166 real sessions, hook-injected context was delivered in **100%** of them while voluntary memory-tool calls happened in **6%**. What a hook enforces happens.
|
|
229
|
+
|
|
230
|
+
The resume digest is deliberately bounded (~350 tokens measured on a 500-memory store): rules are clipped, the critical list is capped, and full text is one `get_memory` call away.
|
|
222
231
|
|
|
223
232
|
---
|
|
224
233
|
|
|
@@ -322,7 +331,8 @@ memgit setup windsurf
|
|
|
322
331
|
memgit setup cline
|
|
323
332
|
memgit setup continue
|
|
324
333
|
memgit setup gemini-cli
|
|
325
|
-
memgit setup hooks # Claude Code
|
|
334
|
+
memgit setup hooks # Claude Code hooks: resume at start, per-prompt recall,
|
|
335
|
+
# capture guard + auto-sync at stop (--no-recall / --no-guard)
|
|
326
336
|
|
|
327
337
|
# Server
|
|
328
338
|
memgit serve # MCP stdio (Claude Code, Cursor, Windsurf, Cline)
|
|
@@ -159,10 +159,10 @@ Memory tools have a cold-start problem: install one halfway through a project an
|
|
|
159
159
|
|
|
160
160
|
```bash
|
|
161
161
|
cd your-project
|
|
162
|
-
memgit onboard # prints the bootstrap brief
|
|
162
|
+
memgit onboard # mines the repo, prints the bootstrap brief
|
|
163
163
|
```
|
|
164
164
|
|
|
165
|
-
The brief tells your AI agent exactly what to do: read the
|
|
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
166
|
|
|
167
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
168
|
|
|
@@ -178,13 +178,22 @@ memgit resume --plain # plain text, for piping into an AI context
|
|
|
178
178
|
memgit resume --json # for tooling
|
|
179
179
|
```
|
|
180
180
|
|
|
181
|
-
Wire it into Claude Code so
|
|
181
|
+
Wire it into Claude Code so memory becomes **automatic** — no tool call, no judgment required:
|
|
182
182
|
|
|
183
183
|
```bash
|
|
184
|
-
memgit setup hooks # installs
|
|
184
|
+
memgit setup hooks # installs all four hooks (~/.claude/settings.json)
|
|
185
185
|
```
|
|
186
186
|
|
|
187
|
-
|
|
187
|
+
| Hook | What it enforces |
|
|
188
|
+
|---|---|
|
|
189
|
+
| `SessionStart` | every session opens with the resume digest in context |
|
|
190
|
+
| `UserPromptSubmit` | each prompt is BM25-matched against the store; relevant memories are injected (silent when nothing clears the relevance bar; never repeats within a session) — `--no-recall` to skip |
|
|
191
|
+
| `Stop` (guard) | a session that did real work but saved nothing gets ONE nudge to save durable facts before finishing — `--no-guard` to skip |
|
|
192
|
+
| `Stop` (sync) | markdown memories are checkpointed asynchronously at session end |
|
|
193
|
+
|
|
194
|
+
Why hooks and not just good tool descriptions? We measured it: across 166 real sessions, hook-injected context was delivered in **100%** of them while voluntary memory-tool calls happened in **6%**. What a hook enforces happens.
|
|
195
|
+
|
|
196
|
+
The resume digest is deliberately bounded (~350 tokens measured on a 500-memory store): rules are clipped, the critical list is capped, and full text is one `get_memory` call away.
|
|
188
197
|
|
|
189
198
|
---
|
|
190
199
|
|
|
@@ -288,7 +297,8 @@ memgit setup windsurf
|
|
|
288
297
|
memgit setup cline
|
|
289
298
|
memgit setup continue
|
|
290
299
|
memgit setup gemini-cli
|
|
291
|
-
memgit setup hooks # Claude Code
|
|
300
|
+
memgit setup hooks # Claude Code hooks: resume at start, per-prompt recall,
|
|
301
|
+
# capture guard + auto-sync at stop (--no-recall / --no-guard)
|
|
292
302
|
|
|
293
303
|
# Server
|
|
294
304
|
memgit serve # MCP stdio (Claude Code, Cursor, Windsurf, Cline)
|
|
@@ -29,11 +29,10 @@ def _require_repo() -> Repository:
|
|
|
29
29
|
|
|
30
30
|
# ── Root group ────────────────────────────────────────────────────────────────
|
|
31
31
|
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
_version = '0.1.2'
|
|
32
|
+
# Source of truth is the code, not dist metadata: editable installs keep
|
|
33
|
+
# whatever metadata version existed at `pip install -e` time (observed: a
|
|
34
|
+
# 0.3.1 checkout reporting 0.1.0 via importlib.metadata).
|
|
35
|
+
from . import __version__ as _version
|
|
37
36
|
|
|
38
37
|
|
|
39
38
|
@click.group()
|
|
@@ -127,7 +126,8 @@ def init(directory):
|
|
|
127
126
|
@click.option('--body', '-b', default=None,
|
|
128
127
|
help='Full long-form detail (multi-line ok, or "-" to read stdin)')
|
|
129
128
|
@click.option('--project', '-P', default=None,
|
|
130
|
-
help='Project this memory belongs to (
|
|
129
|
+
help='Project this memory belongs to (default: derived from the '
|
|
130
|
+
'current directory; pass "" for a global memory)')
|
|
131
131
|
def add(slug, rule, type_code, why, when, tags, priority, body, project):
|
|
132
132
|
"""Add or update a mnemonic.
|
|
133
133
|
|
|
@@ -138,6 +138,13 @@ def add(slug, rule, type_code, why, when, tags, priority, body, project):
|
|
|
138
138
|
tag_list = [t.strip() for t in tags.split(',')] if tags else []
|
|
139
139
|
if body == '-':
|
|
140
140
|
body = sys.stdin.read().strip() or None
|
|
141
|
+
# Same scoping semantics as MCP save_memory: absent → this workspace,
|
|
142
|
+
# explicit empty → deliberately global.
|
|
143
|
+
if project is None:
|
|
144
|
+
from .project import project_label_from_path
|
|
145
|
+
project = project_label_from_path(Path.cwd())
|
|
146
|
+
elif not project.strip():
|
|
147
|
+
project = None
|
|
141
148
|
|
|
142
149
|
m = Mnemonic(
|
|
143
150
|
type_code=type_code,
|
|
@@ -152,7 +159,8 @@ def add(slug, rule, type_code, why, when, tags, priority, body, project):
|
|
|
152
159
|
project=project,
|
|
153
160
|
)
|
|
154
161
|
sha = repo.add(m)
|
|
155
|
-
|
|
162
|
+
from rich.markup import escape as _mesc
|
|
163
|
+
console.print(f'[green]staged[/green] {_mesc(m.slug)} {_mesc("[" + sha[:8] + "]")}')
|
|
156
164
|
|
|
157
165
|
|
|
158
166
|
# ── remove ────────────────────────────────────────────────────────────────────
|
|
@@ -355,12 +363,42 @@ def _format_resume_plain(ctx: dict) -> str:
|
|
|
355
363
|
if ctx.get('maintenance'):
|
|
356
364
|
lines.append('')
|
|
357
365
|
lines.append(f'## Maintenance needed\n- {ctx["maintenance"]}')
|
|
366
|
+
if ctx.get('project_is_new'):
|
|
367
|
+
lines.append('')
|
|
368
|
+
lines.append(
|
|
369
|
+
f'## This project has no memories yet ({ctx["project"]})\n'
|
|
370
|
+
'- memgit was adopted mid-project: nothing above is specific to this '
|
|
371
|
+
'workspace. Bootstrap it once — run `memgit onboard` for a repo '
|
|
372
|
+
'digest + seeding brief, then save 10-20 durable facts '
|
|
373
|
+
'(purpose, architecture, conventions, state, gotchas) via save_memory.'
|
|
374
|
+
)
|
|
358
375
|
lines.append('')
|
|
359
376
|
lines.append('(Check work-in-flight and the last checkpoints before assuming state; '
|
|
360
377
|
'use memgit search for anything task-specific.)')
|
|
361
378
|
return '\n'.join(lines)
|
|
362
379
|
|
|
363
380
|
|
|
381
|
+
# ── hook handlers (invoked by AI-tool hosts, not humans) ─────────────────────
|
|
382
|
+
|
|
383
|
+
@cli.group(name='hook')
|
|
384
|
+
def hook():
|
|
385
|
+
"""Hook handlers for AI-tool hosts (installed by `memgit setup hooks`)."""
|
|
386
|
+
|
|
387
|
+
|
|
388
|
+
@hook.command('prompt-recall')
|
|
389
|
+
def hook_prompt_recall():
|
|
390
|
+
"""UserPromptSubmit: inject memories relevant to the prompt (stdin JSON)."""
|
|
391
|
+
from .hooks import prompt_recall
|
|
392
|
+
sys.exit(prompt_recall())
|
|
393
|
+
|
|
394
|
+
|
|
395
|
+
@hook.command('stop-guard')
|
|
396
|
+
def hook_stop_guard():
|
|
397
|
+
"""Stop: nudge once if a substantive session saved nothing (stdin JSON)."""
|
|
398
|
+
from .hooks import stop_guard
|
|
399
|
+
sys.exit(stop_guard())
|
|
400
|
+
|
|
401
|
+
|
|
364
402
|
# ── onboard ───────────────────────────────────────────────────────────────────
|
|
365
403
|
|
|
366
404
|
ONBOARD_BRIEF = """\
|
|
@@ -369,18 +407,12 @@ ONBOARD_BRIEF = """\
|
|
|
369
407
|
This project has {count} memories in memgit{count_note}. A memory store that
|
|
370
408
|
starts empty mid-project is useless until it is seeded — do that now, once,
|
|
371
409
|
and every future session (in any AI tool) starts oriented.
|
|
372
|
-
|
|
410
|
+
{digest_section}
|
|
373
411
|
## Instructions for the AI operator
|
|
374
412
|
|
|
375
413
|
Extract 10–20 DURABLE facts about this project and save each one as a memory
|
|
376
414
|
(via the memgit MCP `save_memory` tool, or `memgit add` in a shell).
|
|
377
|
-
|
|
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
|
|
415
|
+
{reading_plan}
|
|
384
416
|
|
|
385
417
|
## What to save (one memory each, not a dump)
|
|
386
418
|
|
|
@@ -403,28 +435,71 @@ Checkpoint the seed set so it is versioned from day one:
|
|
|
403
435
|
Then verify: `memgit search "<something about this project>"` should hit.
|
|
404
436
|
"""
|
|
405
437
|
|
|
438
|
+
_READING_PLAN_WITH_DIGEST = """\
|
|
439
|
+
The repo digest above was extracted deterministically from git and the
|
|
440
|
+
filesystem — treat it as ground truth and do NOT re-derive it. On a large
|
|
441
|
+
repo, do NOT crawl the tree. Work only from:
|
|
442
|
+
|
|
443
|
+
1. The "Read these first" files listed in the digest — purpose, architecture, setup
|
|
444
|
+
2. The manifests listed — stack, entry points, scripts, dependencies
|
|
445
|
+
3. The recent commit subjects + hot areas — what is being worked on RIGHT NOW
|
|
446
|
+
(turn these into the "current state / active work" memory)
|
|
447
|
+
4. Config/env samples and CI files if present — deploy targets, environments, gates"""
|
|
448
|
+
|
|
449
|
+
_READING_PLAN_GENERIC = """\
|
|
450
|
+
Read, in this order, whatever exists:
|
|
451
|
+
|
|
452
|
+
1. README / CLAUDE.md / CONTRIBUTING / docs/ — purpose, architecture, setup
|
|
453
|
+
2. Package manifests (package.json, pyproject.toml, go.mod, …) — stack, entry points, scripts
|
|
454
|
+
3. `git log --oneline -30` and recent PRs — what is being worked on RIGHT NOW
|
|
455
|
+
4. Config/env samples, CI files — deploy targets, environments, gates
|
|
456
|
+
5. The code layout itself — modules, boundaries, naming conventions"""
|
|
457
|
+
|
|
406
458
|
|
|
407
459
|
@cli.command()
|
|
408
460
|
@click.option('--project', '-P', default=None,
|
|
409
461
|
help='Project label (default: derived from the current directory)')
|
|
410
462
|
@click.option('--path', 'proj_path', default='.', type=click.Path(exists=True),
|
|
411
463
|
help='Project directory to onboard (default: cwd)')
|
|
412
|
-
|
|
464
|
+
@click.option('--json', 'fmt_json', is_flag=True, help='Emit the raw repo digest as JSON')
|
|
465
|
+
def onboard(project, proj_path, fmt_json):
|
|
413
466
|
"""Print the bootstrap brief for adopting memgit on an existing project.
|
|
414
467
|
|
|
415
468
|
memgit only knows what has been saved — a project adopted midway starts
|
|
416
|
-
with zero context. This
|
|
417
|
-
|
|
418
|
-
|
|
469
|
+
with zero context. This mines the repo's git history and filesystem
|
|
470
|
+
(bounded and read-only, fast even on huge repos) into a factual digest,
|
|
471
|
+
then prints a step-by-step brief for an AI agent (or you) to seed the
|
|
472
|
+
store from it. Paste it into your AI session, or run
|
|
419
473
|
`memgit onboard | pbcopy`.
|
|
420
474
|
"""
|
|
475
|
+
from .gitdigest import build_digest, format_digest
|
|
421
476
|
from .importer import project_label_from_path
|
|
422
477
|
|
|
423
478
|
repo = _require_repo()
|
|
424
|
-
|
|
479
|
+
target = Path(proj_path)
|
|
480
|
+
label = project or project_label_from_path(target) or target.resolve().name
|
|
481
|
+
|
|
482
|
+
digest = build_digest(target)
|
|
483
|
+
if fmt_json:
|
|
484
|
+
import json as _j
|
|
485
|
+
digest['project'] = label
|
|
486
|
+
print(_j.dumps(digest, indent=2))
|
|
487
|
+
return
|
|
488
|
+
|
|
425
489
|
count = sum(1 for m in repo.list() if m.project == label)
|
|
426
490
|
count_note = '' if count else ' — it is a blank slate for this project'
|
|
427
|
-
|
|
491
|
+
|
|
492
|
+
rendered = format_digest(digest)
|
|
493
|
+
if rendered:
|
|
494
|
+
digest_section = ('\n## Repo digest (auto-extracted — trust it, don\'t re-derive)\n\n'
|
|
495
|
+
f'{rendered}\n')
|
|
496
|
+
reading_plan = _READING_PLAN_WITH_DIGEST
|
|
497
|
+
else:
|
|
498
|
+
digest_section = ''
|
|
499
|
+
reading_plan = _READING_PLAN_GENERIC
|
|
500
|
+
|
|
501
|
+
print(ONBOARD_BRIEF.format(project=label, count=count, count_note=count_note,
|
|
502
|
+
digest_section=digest_section, reading_plan=reading_plan))
|
|
428
503
|
|
|
429
504
|
|
|
430
505
|
# ── log ───────────────────────────────────────────────────────────────────────
|
|
@@ -565,35 +640,40 @@ def show(slug, toon, fmt_markdown):
|
|
|
565
640
|
elif toon:
|
|
566
641
|
print(serialize_mnemonic(m))
|
|
567
642
|
else:
|
|
643
|
+
# User content and shas go through markup escaping — `[pj]`,
|
|
644
|
+
# `[[wikilinks]]`, or a sha like [fadc1234] would otherwise be
|
|
645
|
+
# eaten as rich tags and silently altered on screen.
|
|
646
|
+
from rich.markup import escape as _mesc
|
|
568
647
|
sha_s = m.sha[:8] if m.sha else '?'
|
|
569
648
|
p_label = {1: 'low', 2: 'medium', 3: '[bold red]CRITICAL[/bold red]'}[m.priority]
|
|
570
|
-
proj = f' project={m.project}' if m.project else ''
|
|
571
|
-
console.print(f'[bold cyan]{m.slug}[/bold cyan] [
|
|
649
|
+
proj = f' project={_mesc(m.project)}' if m.project else ''
|
|
650
|
+
console.print(f'[bold cyan]{_mesc(m.slug)}[/bold cyan] {_mesc("[" + m.type_code + "]")} '
|
|
651
|
+
f'priority={p_label}{proj} sha={_mesc(sha_s)}')
|
|
572
652
|
console.print(f'')
|
|
573
|
-
console.print(f'[bold]RULE[/bold] {m.rule}')
|
|
653
|
+
console.print(f'[bold]RULE[/bold] {_mesc(m.rule)}')
|
|
574
654
|
if m.why:
|
|
575
|
-
console.print(f'[bold]WHY[/bold] {m.why}')
|
|
655
|
+
console.print(f'[bold]WHY[/bold] {_mesc(m.why)}')
|
|
576
656
|
if m.when:
|
|
577
|
-
console.print(f'[bold]WHEN[/bold] {m.when}')
|
|
657
|
+
console.print(f'[bold]WHEN[/bold] {_mesc(m.when)}')
|
|
578
658
|
if m.desc:
|
|
579
|
-
console.print(f'[bold]DESC[/bold] {m.desc}')
|
|
659
|
+
console.print(f'[bold]DESC[/bold] {_mesc(m.desc)}')
|
|
580
660
|
if m.body:
|
|
581
661
|
console.print(f'\n[bold]BODY[/bold]')
|
|
582
|
-
console.print(m.body)
|
|
662
|
+
console.print(m.body, markup=False)
|
|
583
663
|
if m.who:
|
|
584
|
-
console.print(f'[bold]WHO[/bold] {m.who}')
|
|
664
|
+
console.print(f'[bold]WHO[/bold] {_mesc(m.who)}')
|
|
585
665
|
if m.where:
|
|
586
|
-
console.print(f'[bold]WHERE[/bold] {m.where}')
|
|
666
|
+
console.print(f'[bold]WHERE[/bold] {_mesc(m.where)}')
|
|
587
667
|
if m.inc:
|
|
588
|
-
console.print(f'[bold]INC[/bold] {m.inc}')
|
|
668
|
+
console.print(f'[bold]INC[/bold] {_mesc(m.inc)}')
|
|
589
669
|
if m.cost:
|
|
590
|
-
console.print(f'[bold]COST[/bold] {m.cost}')
|
|
670
|
+
console.print(f'[bold]COST[/bold] {_mesc(m.cost)}')
|
|
591
671
|
if m.tags:
|
|
592
|
-
console.print(f'[dim]Tags: {", ".join(m.tags)}[/dim]')
|
|
672
|
+
console.print(f'[dim]Tags: {_mesc(", ".join(m.tags))}[/dim]')
|
|
593
673
|
if m.related:
|
|
594
|
-
console.print(f'[dim]Related: {", ".join(m.related)}[/dim]')
|
|
674
|
+
console.print(f'[dim]Related: {_mesc(", ".join(m.related))}[/dim]')
|
|
595
675
|
if m.supersedes:
|
|
596
|
-
console.print(f'[dim]Supersedes: {", ".join(m.supersedes)}[/dim]')
|
|
676
|
+
console.print(f'[dim]Supersedes: {_mesc(", ".join(m.supersedes))}[/dim]')
|
|
597
677
|
|
|
598
678
|
|
|
599
679
|
# ── list ──────────────────────────────────────────────────────────────────────
|
|
@@ -827,6 +907,7 @@ def lint():
|
|
|
827
907
|
console.print(f'[green]OK[/green] — {len(mnemonics)} mnemonics, no issues')
|
|
828
908
|
else:
|
|
829
909
|
console.print(f'[yellow]{issues} issue{"s" if issues != 1 else ""}[/yellow]')
|
|
910
|
+
sys.exit(1) # let scripts/CI gate on lint
|
|
830
911
|
|
|
831
912
|
|
|
832
913
|
import re # noqa: E402 — needed for lint command
|
|
@@ -965,6 +1046,15 @@ def sync(message, dry_run):
|
|
|
965
1046
|
mnemonics = from_claude_code()
|
|
966
1047
|
|
|
967
1048
|
if not mnemonics:
|
|
1049
|
+
# No markdown sources on this machine — but anything already staged
|
|
1050
|
+
# (MCP saves, CLI adds) must still be checkpointed, or it lingers
|
|
1051
|
+
# uncommitted forever on stores fed purely through MCP.
|
|
1052
|
+
if not dry_run:
|
|
1053
|
+
msg = message or _staged_diff_message(repo)
|
|
1054
|
+
sha = repo.commit(message=msg, trigger='session_end') if msg else None
|
|
1055
|
+
if sha:
|
|
1056
|
+
console.print(f'[green]sync[/green] {sha[:8]} {msg}')
|
|
1057
|
+
return
|
|
968
1058
|
console.print('[dim]No Claude Code memories found.[/dim]')
|
|
969
1059
|
return
|
|
970
1060
|
|
|
@@ -1889,15 +1979,41 @@ def setup_gemini_cli(dry_run):
|
|
|
1889
1979
|
_run_target('Gemini CLI', path, _patch_mcp_servers, dry_run)
|
|
1890
1980
|
|
|
1891
1981
|
|
|
1982
|
+
#: substrings identifying a hook command as one of ours (any generation)
|
|
1983
|
+
_MEMGIT_HOOK_SIGNS = ('resume --plain', 'hook prompt-recall', 'hook stop-guard', ' sync')
|
|
1984
|
+
|
|
1985
|
+
|
|
1986
|
+
def _is_memgit_hook_entry(h: dict) -> bool:
|
|
1987
|
+
return any(
|
|
1988
|
+
'memgit' in inner.get('command', '') and
|
|
1989
|
+
any(sign in inner.get('command', '') for sign in _MEMGIT_HOOK_SIGNS)
|
|
1990
|
+
for inner in h.get('hooks', []) if isinstance(inner, dict)
|
|
1991
|
+
)
|
|
1992
|
+
|
|
1993
|
+
|
|
1892
1994
|
@setup.command('hooks')
|
|
1893
|
-
@click.option('--remove', is_flag=True, help='Uninstall
|
|
1995
|
+
@click.option('--remove', is_flag=True, help='Uninstall all memgit hooks')
|
|
1996
|
+
@click.option('--no-recall', is_flag=True,
|
|
1997
|
+
help='Skip the per-prompt auto-recall hook (UserPromptSubmit)')
|
|
1998
|
+
@click.option('--no-guard', is_flag=True,
|
|
1999
|
+
help='Skip the end-of-session capture guard (Stop)')
|
|
1894
2000
|
@click.option('--dry-run', is_flag=True, help='Show the change without writing')
|
|
1895
|
-
def setup_hooks(remove, dry_run):
|
|
1896
|
-
"""Install
|
|
2001
|
+
def setup_hooks(remove, no_recall, no_guard, dry_run):
|
|
2002
|
+
"""Install the Claude Code hooks that make memory automatic.
|
|
2003
|
+
|
|
2004
|
+
Four hooks, one principle: what a hook enforces happens, what a tool
|
|
2005
|
+
description suggests mostly doesn't (measured: 6% voluntary engagement
|
|
2006
|
+
vs 100% hook delivery).
|
|
1897
2007
|
|
|
1898
|
-
|
|
1899
|
-
|
|
1900
|
-
|
|
2008
|
+
\b
|
|
2009
|
+
SessionStart inject `memgit resume` — last checkpoints, work in
|
|
2010
|
+
flight, critical rules
|
|
2011
|
+
UserPromptSubmit inject memories relevant to each prompt (BM25,
|
|
2012
|
+
silent when nothing clears the relevance bar)
|
|
2013
|
+
Stop capture guard — a substantive session ending with
|
|
2014
|
+
zero memory writes gets ONE nudge to save durable
|
|
2015
|
+
facts; plus async `memgit sync` to checkpoint
|
|
2016
|
+
markdown memories
|
|
1901
2017
|
|
|
1902
2018
|
Hooks live in ~/.claude/settings.json (unlike MCP servers, which live
|
|
1903
2019
|
in ~/.claude.json).
|
|
@@ -1905,8 +2021,10 @@ def setup_hooks(remove, dry_run):
|
|
|
1905
2021
|
import shlex
|
|
1906
2022
|
settings_path = Path.home() / '.claude' / 'settings.json'
|
|
1907
2023
|
base = ' '.join(shlex.quote(p) for p in _memgit_base_cmd())
|
|
1908
|
-
|
|
1909
|
-
|
|
2024
|
+
|
|
2025
|
+
from .repo import default_store_candidates
|
|
2026
|
+
store = next((c for c in default_store_candidates() if (c / '.memgit').is_dir()),
|
|
2027
|
+
Path.home() / '.claude' / 'memgit-store')
|
|
1910
2028
|
|
|
1911
2029
|
if settings_path.exists():
|
|
1912
2030
|
try:
|
|
@@ -1918,43 +2036,55 @@ def setup_hooks(remove, dry_run):
|
|
|
1918
2036
|
data = {}
|
|
1919
2037
|
|
|
1920
2038
|
hooks = data.setdefault('hooks', {})
|
|
1921
|
-
session_start = hooks.setdefault('SessionStart', [])
|
|
1922
2039
|
|
|
1923
|
-
|
|
1924
|
-
|
|
1925
|
-
|
|
2040
|
+
# `|| true` + stderr silenced everywhere: a broken store must never
|
|
2041
|
+
# block the user's session.
|
|
2042
|
+
plan: dict[str, list[dict]] = {
|
|
2043
|
+
'SessionStart': [
|
|
2044
|
+
{'type': 'command',
|
|
2045
|
+
'command': f'{base} resume --plain 2>/dev/null || true'},
|
|
2046
|
+
],
|
|
2047
|
+
'UserPromptSubmit': [] if no_recall else [
|
|
2048
|
+
{'type': 'command',
|
|
2049
|
+
'command': f'{base} hook prompt-recall 2>/dev/null || true'},
|
|
2050
|
+
],
|
|
2051
|
+
'Stop': ([] if no_guard else [
|
|
2052
|
+
{'type': 'command',
|
|
2053
|
+
'command': f'{base} hook stop-guard 2>/dev/null || true'},
|
|
2054
|
+
]) + [
|
|
2055
|
+
{'type': 'command',
|
|
2056
|
+
'command': f'cd {shlex.quote(str(store))} && {base} sync 2>/dev/null || true',
|
|
2057
|
+
'async': True},
|
|
2058
|
+
],
|
|
2059
|
+
}
|
|
2060
|
+
|
|
2061
|
+
changed = []
|
|
2062
|
+
for event in ('SessionStart', 'UserPromptSubmit', 'Stop'):
|
|
2063
|
+
entries = hooks.setdefault(event, [])
|
|
2064
|
+
had = [h for h in entries if isinstance(h, dict) and _is_memgit_hook_entry(h)]
|
|
2065
|
+
entries[:] = [h for h in entries if h not in had]
|
|
2066
|
+
if not remove and plan[event]:
|
|
2067
|
+
entries.append({'hooks': plan[event]})
|
|
2068
|
+
changed.append(event)
|
|
2069
|
+
if not entries:
|
|
2070
|
+
hooks.pop(event, None)
|
|
2071
|
+
if not hooks:
|
|
2072
|
+
data.pop('hooks', None)
|
|
1926
2073
|
|
|
1927
|
-
|
|
2074
|
+
if not dry_run:
|
|
2075
|
+
_write_json_safe(settings_path, data)
|
|
1928
2076
|
|
|
2077
|
+
suffix = ' [dim](dry run)[/dim]' if dry_run else ''
|
|
1929
2078
|
if remove:
|
|
1930
|
-
|
|
1931
|
-
console.print('[dim]No memgit SessionStart hook installed.[/dim]')
|
|
1932
|
-
return
|
|
1933
|
-
session_start[:] = [h for h in session_start if h not in existing]
|
|
1934
|
-
if not session_start:
|
|
1935
|
-
hooks.pop('SessionStart', None)
|
|
1936
|
-
if not hooks:
|
|
1937
|
-
data.pop('hooks', None)
|
|
1938
|
-
if not dry_run:
|
|
1939
|
-
_write_json_safe(settings_path, data)
|
|
1940
|
-
console.print(f'[yellow]removed[/yellow] memgit SessionStart hook from {settings_path}'
|
|
1941
|
-
+ (' [dim](dry run)[/dim]' if dry_run else ''))
|
|
2079
|
+
console.print(f'[yellow]removed[/yellow] all memgit hooks from {settings_path}{suffix}')
|
|
1942
2080
|
return
|
|
1943
|
-
|
|
1944
|
-
|
|
1945
|
-
|
|
1946
|
-
|
|
1947
|
-
console.print(f'[
|
|
1948
|
-
|
|
1949
|
-
|
|
1950
|
-
session_start.append(entry)
|
|
1951
|
-
|
|
1952
|
-
if not dry_run:
|
|
1953
|
-
_write_json_safe(settings_path, data)
|
|
1954
|
-
console.print(f'[green]✓[/green] SessionStart hook installed in {settings_path}'
|
|
1955
|
-
+ (' [dim](dry run)[/dim]' if dry_run else ''))
|
|
1956
|
-
console.print(f' [dim]{hook_command}[/dim]')
|
|
1957
|
-
console.print('[dim]Every new Claude Code session now starts with your memgit resume digest.[/dim]')
|
|
2081
|
+
console.print(f'[green]✓[/green] memgit hooks installed in {settings_path}{suffix}')
|
|
2082
|
+
for event in changed:
|
|
2083
|
+
for inner in plan[event]:
|
|
2084
|
+
tag = ' [dim](async)[/dim]' if inner.get('async') else ''
|
|
2085
|
+
console.print(f' [cyan]{event}[/cyan] [dim]{inner["command"]}[/dim]{tag}')
|
|
2086
|
+
console.print('[dim]Resume at session start, relevant memories per prompt, '
|
|
2087
|
+
'capture guard + sync at stop.[/dim]')
|
|
1958
2088
|
|
|
1959
2089
|
|
|
1960
2090
|
@setup.command('print-config')
|