memgit 0.3.0__tar.gz → 0.3.1__tar.gz
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- {memgit-0.3.0 → memgit-0.3.1}/PKG-INFO +3 -3
- {memgit-0.3.0 → memgit-0.3.1}/README.md +2 -2
- {memgit-0.3.0 → memgit-0.3.1}/memgit/__init__.py +1 -1
- {memgit-0.3.0 → memgit-0.3.1}/memgit/cli.py +51 -14
- memgit-0.3.1/memgit/gitdigest.py +158 -0
- {memgit-0.3.0 → memgit-0.3.1}/memgit/mcp_server.py +3 -1
- {memgit-0.3.0 → memgit-0.3.1}/memgit.egg-info/PKG-INFO +3 -3
- {memgit-0.3.0 → memgit-0.3.1}/memgit.egg-info/SOURCES.txt +1 -0
- {memgit-0.3.0 → memgit-0.3.1}/pyproject.toml +1 -1
- {memgit-0.3.0 → memgit-0.3.1}/tests/test_v030.py +79 -0
- {memgit-0.3.0 → memgit-0.3.1}/LICENSE +0 -0
- {memgit-0.3.0 → memgit-0.3.1}/memgit/cloud/__init__.py +0 -0
- {memgit-0.3.0 → memgit-0.3.1}/memgit/cloud/client.py +0 -0
- {memgit-0.3.0 → memgit-0.3.1}/memgit/cloud/commands.py +0 -0
- {memgit-0.3.0 → memgit-0.3.1}/memgit/cloud/crypto.py +0 -0
- {memgit-0.3.0 → memgit-0.3.1}/memgit/cloud/state.py +0 -0
- {memgit-0.3.0 → memgit-0.3.1}/memgit/cloud/sync.py +0 -0
- {memgit-0.3.0 → memgit-0.3.1}/memgit/graph.py +0 -0
- {memgit-0.3.0 → memgit-0.3.1}/memgit/http_server.py +0 -0
- {memgit-0.3.0 → memgit-0.3.1}/memgit/importer.py +0 -0
- {memgit-0.3.0 → memgit-0.3.1}/memgit/models.py +0 -0
- {memgit-0.3.0 → memgit-0.3.1}/memgit/repo.py +0 -0
- {memgit-0.3.0 → memgit-0.3.1}/memgit/scorer.py +0 -0
- {memgit-0.3.0 → memgit-0.3.1}/memgit/store.py +0 -0
- {memgit-0.3.0 → memgit-0.3.1}/memgit/tokens.py +0 -0
- {memgit-0.3.0 → memgit-0.3.1}/memgit/toon.py +0 -0
- {memgit-0.3.0 → memgit-0.3.1}/memgit.egg-info/dependency_links.txt +0 -0
- {memgit-0.3.0 → memgit-0.3.1}/memgit.egg-info/entry_points.txt +0 -0
- {memgit-0.3.0 → memgit-0.3.1}/memgit.egg-info/requires.txt +0 -0
- {memgit-0.3.0 → memgit-0.3.1}/memgit.egg-info/top_level.txt +0 -0
- {memgit-0.3.0 → memgit-0.3.1}/setup.cfg +0 -0
- {memgit-0.3.0 → memgit-0.3.1}/tests/test_advanced.py +0 -0
- {memgit-0.3.0 → memgit-0.3.1}/tests/test_setup.py +0 -0
- {memgit-0.3.0 → memgit-0.3.1}/tests/test_store_repo.py +0 -0
- {memgit-0.3.0 → memgit-0.3.1}/tests/test_toon.py +0 -0
- {memgit-0.3.0 → memgit-0.3.1}/tests/test_v020.py +0 -0
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
Metadata-Version: 2.4
|
|
2
2
|
Name: memgit
|
|
3
|
-
Version: 0.3.
|
|
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
|
|
@@ -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
|
|
|
@@ -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
|
|
|
@@ -369,18 +369,12 @@ ONBOARD_BRIEF = """\
|
|
|
369
369
|
This project has {count} memories in memgit{count_note}. A memory store that
|
|
370
370
|
starts empty mid-project is useless until it is seeded — do that now, once,
|
|
371
371
|
and every future session (in any AI tool) starts oriented.
|
|
372
|
-
|
|
372
|
+
{digest_section}
|
|
373
373
|
## Instructions for the AI operator
|
|
374
374
|
|
|
375
375
|
Extract 10–20 DURABLE facts about this project and save each one as a memory
|
|
376
376
|
(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
|
|
377
|
+
{reading_plan}
|
|
384
378
|
|
|
385
379
|
## What to save (one memory each, not a dump)
|
|
386
380
|
|
|
@@ -403,28 +397,71 @@ Checkpoint the seed set so it is versioned from day one:
|
|
|
403
397
|
Then verify: `memgit search "<something about this project>"` should hit.
|
|
404
398
|
"""
|
|
405
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
|
+
|
|
406
420
|
|
|
407
421
|
@cli.command()
|
|
408
422
|
@click.option('--project', '-P', default=None,
|
|
409
423
|
help='Project label (default: derived from the current directory)')
|
|
410
424
|
@click.option('--path', 'proj_path', default='.', type=click.Path(exists=True),
|
|
411
425
|
help='Project directory to onboard (default: cwd)')
|
|
412
|
-
|
|
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):
|
|
413
428
|
"""Print the bootstrap brief for adopting memgit on an existing project.
|
|
414
429
|
|
|
415
430
|
memgit only knows what has been saved — a project adopted midway starts
|
|
416
|
-
with zero context. This
|
|
417
|
-
|
|
418
|
-
|
|
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
|
|
419
435
|
`memgit onboard | pbcopy`.
|
|
420
436
|
"""
|
|
437
|
+
from .gitdigest import build_digest, format_digest
|
|
421
438
|
from .importer import project_label_from_path
|
|
422
439
|
|
|
423
440
|
repo = _require_repo()
|
|
424
|
-
|
|
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
|
+
|
|
425
451
|
count = sum(1 for m in repo.list() if m.project == label)
|
|
426
452
|
count_note = '' if count else ' — it is a blank slate for this project'
|
|
427
|
-
|
|
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))
|
|
428
465
|
|
|
429
466
|
|
|
430
467
|
# ── log ───────────────────────────────────────────────────────────────────────
|
|
@@ -0,0 +1,158 @@
|
|
|
1
|
+
"""Deterministic repo digest for onboarding — mine git + the filesystem
|
|
2
|
+
so the AI operator starts from extracted facts, not exploration.
|
|
3
|
+
|
|
4
|
+
Every probe is bounded (commit caps, timeouts, output clips) so this stays
|
|
5
|
+
fast and safe on very large repositories, and every probe is read-only.
|
|
6
|
+
A probe that fails or times out is simply omitted — never an error.
|
|
7
|
+
"""
|
|
8
|
+
|
|
9
|
+
from __future__ import annotations
|
|
10
|
+
import subprocess
|
|
11
|
+
from collections import Counter
|
|
12
|
+
from pathlib import Path
|
|
13
|
+
from typing import Optional
|
|
14
|
+
|
|
15
|
+
# Bounds — chosen so the digest is near-instant even on monorepos.
|
|
16
|
+
_GIT_TIMEOUT = 5 # seconds per git call
|
|
17
|
+
_CHURN_COMMITS = 300 # commits sampled for hot-file analysis
|
|
18
|
+
_RECENT_SUBJECTS = 15 # recent commit subjects shown
|
|
19
|
+
_AUTHOR_SAMPLE = 500 # commits sampled for contributor list
|
|
20
|
+
|
|
21
|
+
# Well-known files worth reading during onboarding, in reading order.
|
|
22
|
+
_DOC_FILES = [
|
|
23
|
+
'CLAUDE.md', 'README.md', 'README.rst', 'CONTRIBUTING.md',
|
|
24
|
+
'ARCHITECTURE.md', 'AGENTS.md', 'CHANGELOG.md',
|
|
25
|
+
]
|
|
26
|
+
_MANIFEST_FILES = [
|
|
27
|
+
'package.json', 'pyproject.toml', 'setup.py', 'requirements.txt',
|
|
28
|
+
'go.mod', 'Cargo.toml', 'pom.xml', 'build.gradle', 'Gemfile',
|
|
29
|
+
'composer.json', 'mix.exs', 'Makefile', 'docker-compose.yml',
|
|
30
|
+
'Dockerfile',
|
|
31
|
+
]
|
|
32
|
+
|
|
33
|
+
_STACK_HINTS = {
|
|
34
|
+
'package.json': 'Node.js/TypeScript',
|
|
35
|
+
'pyproject.toml': 'Python',
|
|
36
|
+
'setup.py': 'Python',
|
|
37
|
+
'requirements.txt': 'Python',
|
|
38
|
+
'go.mod': 'Go',
|
|
39
|
+
'Cargo.toml': 'Rust',
|
|
40
|
+
'pom.xml': 'Java (Maven)',
|
|
41
|
+
'build.gradle': 'Java/Kotlin (Gradle)',
|
|
42
|
+
'Gemfile': 'Ruby',
|
|
43
|
+
'composer.json': 'PHP',
|
|
44
|
+
'mix.exs': 'Elixir',
|
|
45
|
+
}
|
|
46
|
+
|
|
47
|
+
|
|
48
|
+
def _git(args: list[str], cwd: Path) -> Optional[str]:
|
|
49
|
+
"""Run one read-only git command; None on any failure or timeout."""
|
|
50
|
+
try:
|
|
51
|
+
out = subprocess.run(
|
|
52
|
+
['git', '--no-pager'] + args,
|
|
53
|
+
cwd=str(cwd), capture_output=True, text=True, timeout=_GIT_TIMEOUT,
|
|
54
|
+
)
|
|
55
|
+
return out.stdout.strip() if out.returncode == 0 else None
|
|
56
|
+
except Exception:
|
|
57
|
+
return None
|
|
58
|
+
|
|
59
|
+
|
|
60
|
+
def build_digest(path: Path) -> dict:
|
|
61
|
+
"""Extract a bounded, factual digest of the repo at `path`.
|
|
62
|
+
|
|
63
|
+
Returns a dict; `has_git` False means only the filesystem probes ran.
|
|
64
|
+
"""
|
|
65
|
+
path = path.resolve()
|
|
66
|
+
digest: dict = {'path': str(path), 'has_git': False}
|
|
67
|
+
|
|
68
|
+
# Filesystem probes (work with or without git)
|
|
69
|
+
docs = [f for f in _DOC_FILES if (path / f).is_file()]
|
|
70
|
+
manifests = [f for f in _MANIFEST_FILES if (path / f).is_file()]
|
|
71
|
+
stacks = sorted({_STACK_HINTS[m] for m in manifests if m in _STACK_HINTS})
|
|
72
|
+
digest['docs'] = docs
|
|
73
|
+
digest['manifests'] = manifests
|
|
74
|
+
digest['stacks'] = stacks
|
|
75
|
+
digest['has_docs_dir'] = (path / 'docs').is_dir()
|
|
76
|
+
digest['has_ci'] = (path / '.github' / 'workflows').is_dir() or (path / '.gitlab-ci.yml').is_file()
|
|
77
|
+
|
|
78
|
+
if _git(['rev-parse', '--is-inside-work-tree'], path) != 'true':
|
|
79
|
+
return digest
|
|
80
|
+
digest['has_git'] = True
|
|
81
|
+
|
|
82
|
+
digest['branch'] = _git(['branch', '--show-current'], path) or None
|
|
83
|
+
digest['last_commit_date'] = _git(['log', '-1', '--format=%as'], path)
|
|
84
|
+
count = _git(['rev-list', '--count', 'HEAD'], path)
|
|
85
|
+
digest['commit_count'] = int(count) if count and count.isdigit() else None
|
|
86
|
+
digest['latest_tag'] = _git(['describe', '--tags', '--abbrev=0'], path)
|
|
87
|
+
|
|
88
|
+
subjects = _git(['log', f'-{_RECENT_SUBJECTS}', '--no-merges', '--format=%s'], path)
|
|
89
|
+
digest['recent_subjects'] = (
|
|
90
|
+
[s.strip()[:100] for s in subjects.splitlines() if s.strip()] if subjects else []
|
|
91
|
+
)
|
|
92
|
+
|
|
93
|
+
authors = _git(['log', f'-{_AUTHOR_SAMPLE}', '--format=%an'], path)
|
|
94
|
+
if authors:
|
|
95
|
+
top = Counter(a.strip() for a in authors.splitlines() if a.strip())
|
|
96
|
+
digest['top_authors'] = [name for name, _ in top.most_common(5)]
|
|
97
|
+
else:
|
|
98
|
+
digest['top_authors'] = []
|
|
99
|
+
|
|
100
|
+
# Hot files/dirs: what actually changes is where the project lives.
|
|
101
|
+
churn = _git(
|
|
102
|
+
['log', f'-{_CHURN_COMMITS}', '--no-merges', '--name-only', '--format='],
|
|
103
|
+
path,
|
|
104
|
+
)
|
|
105
|
+
if churn:
|
|
106
|
+
files = [f.strip() for f in churn.splitlines() if f.strip()]
|
|
107
|
+
file_counts = Counter(files)
|
|
108
|
+
dir_counts: Counter = Counter()
|
|
109
|
+
for f, n in file_counts.items():
|
|
110
|
+
top_level = f.split('/', 1)[0] if '/' in f else '(root)'
|
|
111
|
+
dir_counts[top_level] += n
|
|
112
|
+
digest['hot_files'] = [f for f, _ in file_counts.most_common(10)]
|
|
113
|
+
digest['hot_dirs'] = [d for d, _ in dir_counts.most_common(6)]
|
|
114
|
+
else:
|
|
115
|
+
digest['hot_files'] = []
|
|
116
|
+
digest['hot_dirs'] = []
|
|
117
|
+
|
|
118
|
+
return digest
|
|
119
|
+
|
|
120
|
+
|
|
121
|
+
def format_digest(digest: dict) -> str:
|
|
122
|
+
"""Render the digest as a markdown section for the onboarding brief."""
|
|
123
|
+
lines: list[str] = []
|
|
124
|
+
if digest.get('has_git'):
|
|
125
|
+
facts = []
|
|
126
|
+
if digest.get('commit_count'):
|
|
127
|
+
facts.append(f"{digest['commit_count']} commits")
|
|
128
|
+
if digest.get('branch'):
|
|
129
|
+
facts.append(f"branch `{digest['branch']}`")
|
|
130
|
+
if digest.get('last_commit_date'):
|
|
131
|
+
facts.append(f"last commit {digest['last_commit_date']}")
|
|
132
|
+
if digest.get('latest_tag'):
|
|
133
|
+
facts.append(f"latest tag {digest['latest_tag']}")
|
|
134
|
+
if facts:
|
|
135
|
+
lines.append(f"- Git: {' · '.join(facts)}")
|
|
136
|
+
if digest.get('top_authors'):
|
|
137
|
+
lines.append(f"- Recent authors: {', '.join(digest['top_authors'])}")
|
|
138
|
+
if digest.get('stacks'):
|
|
139
|
+
lines.append(f"- Stack: {', '.join(digest['stacks'])} "
|
|
140
|
+
f"(manifests: {', '.join(digest['manifests'])})")
|
|
141
|
+
elif digest.get('manifests'):
|
|
142
|
+
lines.append(f"- Manifests: {', '.join(digest['manifests'])}")
|
|
143
|
+
read_first = list(digest.get('docs', []))
|
|
144
|
+
if digest.get('has_docs_dir'):
|
|
145
|
+
read_first.append('docs/')
|
|
146
|
+
if read_first:
|
|
147
|
+
lines.append(f"- Read these first: {', '.join(read_first)}")
|
|
148
|
+
if digest.get('hot_dirs'):
|
|
149
|
+
lines.append(f"- Hot areas (most-changed recently): {', '.join(digest['hot_dirs'])}")
|
|
150
|
+
if digest.get('hot_files'):
|
|
151
|
+
lines.append(f"- Most-changed files: {', '.join(digest['hot_files'][:6])}")
|
|
152
|
+
if digest.get('recent_subjects'):
|
|
153
|
+
lines.append('- Recent commit subjects (newest first):')
|
|
154
|
+
for s in digest['recent_subjects']:
|
|
155
|
+
lines.append(f' · {s}')
|
|
156
|
+
if digest.get('has_ci'):
|
|
157
|
+
lines.append('- CI config present (.github/workflows or .gitlab-ci.yml)')
|
|
158
|
+
return '\n'.join(lines)
|
|
@@ -403,7 +403,9 @@ def run_server(store_path: Path | None = None) -> None:
|
|
|
403
403
|
"docs and `git log --oneline -30`, extract 10-20 durable facts "
|
|
404
404
|
"(purpose, architecture, conventions, current state, gotchas), and "
|
|
405
405
|
"save_memory each with a one-line rule + full body. "
|
|
406
|
-
"`memgit onboard`
|
|
406
|
+
"Run `memgit onboard` first — it mines the repo's git history and "
|
|
407
|
+
"manifests into a factual digest plus the complete seeding brief, "
|
|
408
|
+
"so you read only what matters instead of crawling the tree."
|
|
407
409
|
)
|
|
408
410
|
return [TextContent(type="text", text=text)]
|
|
409
411
|
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
Metadata-Version: 2.4
|
|
2
2
|
Name: memgit
|
|
3
|
-
Version: 0.3.
|
|
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
|
|
@@ -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
|
|
|
@@ -4,7 +4,7 @@ build-backend = "setuptools.build_meta"
|
|
|
4
4
|
|
|
5
5
|
[project]
|
|
6
6
|
name = "memgit"
|
|
7
|
-
version = "0.3.
|
|
7
|
+
version = "0.3.1"
|
|
8
8
|
description = "Git for AI memory — version-controlled context persistence across Claude, GPT, Gemini, Cursor, Windsurf, and more"
|
|
9
9
|
readme = "README.md"
|
|
10
10
|
requires-python = ">=3.11"
|
|
@@ -281,6 +281,85 @@ class TestCli:
|
|
|
281
281
|
assert msg == "sync: +1 (brand-new)"
|
|
282
282
|
|
|
283
283
|
|
|
284
|
+
# ── git digest (onboarding intelligence) ────────────────────────────────────
|
|
285
|
+
|
|
286
|
+
def _make_git_repo(root: Path):
|
|
287
|
+
import subprocess
|
|
288
|
+
def g(*args):
|
|
289
|
+
subprocess.run(["git"] + list(args), cwd=root, capture_output=True,
|
|
290
|
+
env={"GIT_AUTHOR_NAME": "Tester", "GIT_AUTHOR_EMAIL": "t@t",
|
|
291
|
+
"GIT_COMMITTER_NAME": "Tester", "GIT_COMMITTER_EMAIL": "t@t",
|
|
292
|
+
"PATH": os.environ["PATH"], "HOME": os.environ.get("HOME", "")})
|
|
293
|
+
g("init", "-q", "-b", "main")
|
|
294
|
+
(root / "README.md").write_text("# proj")
|
|
295
|
+
(root / "pyproject.toml").write_text("[project]\nname='x'")
|
|
296
|
+
(root / "src").mkdir()
|
|
297
|
+
(root / "src" / "app.py").write_text("print(1)")
|
|
298
|
+
g("add", "-A")
|
|
299
|
+
g("commit", "-qm", "initial scaffold")
|
|
300
|
+
(root / "src" / "app.py").write_text("print(2)")
|
|
301
|
+
g("add", "-A")
|
|
302
|
+
g("commit", "-qm", "fix: app output")
|
|
303
|
+
|
|
304
|
+
|
|
305
|
+
class TestGitDigest:
|
|
306
|
+
def test_digest_extracts_facts(self, tmp_path):
|
|
307
|
+
from memgit.gitdigest import build_digest
|
|
308
|
+
_make_git_repo(tmp_path)
|
|
309
|
+
d = build_digest(tmp_path)
|
|
310
|
+
assert d["has_git"] is True
|
|
311
|
+
assert d["branch"] == "main"
|
|
312
|
+
assert d["commit_count"] == 2
|
|
313
|
+
assert "fix: app output" in d["recent_subjects"]
|
|
314
|
+
assert "README.md" in d["docs"]
|
|
315
|
+
assert "Python" in d["stacks"]
|
|
316
|
+
assert "src" in d["hot_dirs"]
|
|
317
|
+
|
|
318
|
+
def test_digest_without_git(self, tmp_path):
|
|
319
|
+
from memgit.gitdigest import build_digest
|
|
320
|
+
(tmp_path / "README.md").write_text("# hi")
|
|
321
|
+
d = build_digest(tmp_path)
|
|
322
|
+
assert d["has_git"] is False
|
|
323
|
+
assert d["docs"] == ["README.md"]
|
|
324
|
+
|
|
325
|
+
def test_format_digest_renders(self, tmp_path):
|
|
326
|
+
from memgit.gitdigest import build_digest, format_digest
|
|
327
|
+
_make_git_repo(tmp_path)
|
|
328
|
+
out = format_digest(build_digest(tmp_path))
|
|
329
|
+
assert "Read these first: README.md" in out
|
|
330
|
+
assert "Stack: Python" in out
|
|
331
|
+
assert "fix: app output" in out
|
|
332
|
+
|
|
333
|
+
def test_onboard_embeds_digest(self, tmp_path, monkeypatch):
|
|
334
|
+
_make_git_repo(tmp_path)
|
|
335
|
+
monkeypatch.chdir(tmp_path)
|
|
336
|
+
Repository.init(tmp_path)
|
|
337
|
+
r = CliRunner()
|
|
338
|
+
res = r.invoke(cli, ["onboard", "--project", "MyProj"])
|
|
339
|
+
assert res.exit_code == 0
|
|
340
|
+
assert "Repo digest" in res.output
|
|
341
|
+
assert "trust it, don't re-derive" in res.output
|
|
342
|
+
assert "do NOT crawl the tree" in res.output
|
|
343
|
+
|
|
344
|
+
def test_onboard_json_digest(self, tmp_path, monkeypatch):
|
|
345
|
+
import json
|
|
346
|
+
_make_git_repo(tmp_path)
|
|
347
|
+
monkeypatch.chdir(tmp_path)
|
|
348
|
+
Repository.init(tmp_path)
|
|
349
|
+
r = CliRunner()
|
|
350
|
+
res = r.invoke(cli, ["onboard", "--project", "MyProj", "--json"])
|
|
351
|
+
d = json.loads(res.output)
|
|
352
|
+
assert d["project"] == "MyProj" and d["has_git"] is True
|
|
353
|
+
|
|
354
|
+
def test_onboard_no_git_falls_back_generic(self, tmp_path, monkeypatch):
|
|
355
|
+
monkeypatch.chdir(tmp_path)
|
|
356
|
+
Repository.init(tmp_path)
|
|
357
|
+
r = CliRunner()
|
|
358
|
+
res = r.invoke(cli, ["onboard", "--project", "MyProj"])
|
|
359
|
+
assert res.exit_code == 0
|
|
360
|
+
assert "git log --oneline -30" in res.output # generic reading plan
|
|
361
|
+
|
|
362
|
+
|
|
284
363
|
# ── MCP project detection ────────────────────────────────────────────────────
|
|
285
364
|
|
|
286
365
|
class TestMcpDetect:
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|