git-memory 0.1.0
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.
- package/LICENSE +661 -0
- package/config/mcp_config_example.json +16 -0
- package/dist/chroma-index.d.ts +63 -0
- package/dist/chroma-index.d.ts.map +1 -0
- package/dist/chroma-index.js +166 -0
- package/dist/chroma-index.js.map +1 -0
- package/dist/cli.d.ts +3 -0
- package/dist/cli.d.ts.map +1 -0
- package/dist/cli.js +311 -0
- package/dist/cli.js.map +1 -0
- package/dist/context-store.d.ts +54 -0
- package/dist/context-store.d.ts.map +1 -0
- package/dist/context-store.js +187 -0
- package/dist/context-store.js.map +1 -0
- package/dist/embeddings.d.ts +10 -0
- package/dist/embeddings.d.ts.map +1 -0
- package/dist/embeddings.js +42 -0
- package/dist/embeddings.js.map +1 -0
- package/dist/filters.d.ts +70 -0
- package/dist/filters.d.ts.map +1 -0
- package/dist/filters.js +131 -0
- package/dist/filters.js.map +1 -0
- package/dist/index.d.ts +14 -0
- package/dist/index.d.ts.map +1 -0
- package/dist/index.js +14 -0
- package/dist/index.js.map +1 -0
- package/dist/indexer.d.ts +45 -0
- package/dist/indexer.d.ts.map +1 -0
- package/dist/indexer.js +242 -0
- package/dist/indexer.js.map +1 -0
- package/dist/mcp-server.d.ts +8 -0
- package/dist/mcp-server.d.ts.map +1 -0
- package/dist/mcp-server.js +373 -0
- package/dist/mcp-server.js.map +1 -0
- package/dist/store.d.ts +17 -0
- package/dist/store.d.ts.map +1 -0
- package/dist/store.js +47 -0
- package/dist/store.js.map +1 -0
- package/dist/types.d.ts +59 -0
- package/dist/types.d.ts.map +1 -0
- package/dist/types.js +25 -0
- package/dist/types.js.map +1 -0
- package/dist/vector-store.d.ts +65 -0
- package/dist/vector-store.d.ts.map +1 -0
- package/dist/vector-store.js +152 -0
- package/dist/vector-store.js.map +1 -0
- package/package.json +52 -0
- package/skills/git-memory-debug/SKILL.md +99 -0
- package/skills/git-memory-index/SKILL.md +88 -0
- package/skills/git-memory-search/SKILL.md +92 -0
- package/skills/git-memory-status/SKILL.md +69 -0
package/dist/indexer.js
ADDED
|
@@ -0,0 +1,242 @@
|
|
|
1
|
+
import { simpleGit } from 'simple-git';
|
|
2
|
+
import { basename } from 'path';
|
|
3
|
+
import { ChromaCommitIndex } from './chroma-index.js';
|
|
4
|
+
import { ContextStore } from './context-store.js';
|
|
5
|
+
import { createEmbeddingFunction } from './embeddings.js';
|
|
6
|
+
import { isRelevant, buildMetadata, summarizeCommit, buildStatsStr } from './filters.js';
|
|
7
|
+
import { configFromEnv } from './types.js';
|
|
8
|
+
export class GitMemoryIndexer {
|
|
9
|
+
git;
|
|
10
|
+
repoName;
|
|
11
|
+
chromaIndex;
|
|
12
|
+
contextStore;
|
|
13
|
+
userId;
|
|
14
|
+
constructor(git, repoName, chromaIndex, contextStore, userId) {
|
|
15
|
+
this.git = git;
|
|
16
|
+
this.repoName = repoName;
|
|
17
|
+
this.chromaIndex = chromaIndex;
|
|
18
|
+
this.contextStore = contextStore;
|
|
19
|
+
this.userId = userId;
|
|
20
|
+
}
|
|
21
|
+
/** Async factory — initialises git, ChromaDB (Layer 1), and ContextStore (Layer 2). */
|
|
22
|
+
static async create(config) {
|
|
23
|
+
const git = simpleGit(config.repoPath);
|
|
24
|
+
let repoName;
|
|
25
|
+
try {
|
|
26
|
+
const root = await git.revparse(['--show-toplevel']);
|
|
27
|
+
repoName = basename(root.trim());
|
|
28
|
+
}
|
|
29
|
+
catch {
|
|
30
|
+
repoName = basename(config.repoPath);
|
|
31
|
+
}
|
|
32
|
+
const embedFn = await createEmbeddingFunction(config.embedProvider, config.embedModel, config.ollamaUrl, config.openaiApiKey);
|
|
33
|
+
const chromaIndex = await ChromaCommitIndex.create(config.chromaDir, embedFn);
|
|
34
|
+
const contextStore = await ContextStore.create(config, embedFn);
|
|
35
|
+
return new GitMemoryIndexer(git, repoName, chromaIndex, contextStore, config.userId);
|
|
36
|
+
}
|
|
37
|
+
/** Convenience factory using environment variables. */
|
|
38
|
+
static async fromEnv() {
|
|
39
|
+
return GitMemoryIndexer.create(configFromEnv());
|
|
40
|
+
}
|
|
41
|
+
/**
|
|
42
|
+
* Index a single commit by hash.
|
|
43
|
+
* Matches Python index_commit() exactly:
|
|
44
|
+
* 1. Get commit details
|
|
45
|
+
* 2. isRelevant() check — skip if false (unless force)
|
|
46
|
+
* 3. Build metadata + document
|
|
47
|
+
* 4. Layer 1: chromaIndex.upsertCommit() — returns false if duplicate
|
|
48
|
+
* 5. If Layer 1 returned false and !force → return false
|
|
49
|
+
* 6. Layer 2: contextStore.addCommit() — no-throw
|
|
50
|
+
* 7. Return true
|
|
51
|
+
*/
|
|
52
|
+
async indexCommit(hash, force = false) {
|
|
53
|
+
let details;
|
|
54
|
+
try {
|
|
55
|
+
details = await this.getCommitDetails(hash);
|
|
56
|
+
}
|
|
57
|
+
catch (e) {
|
|
58
|
+
process.stderr.write(`indexCommit: failed to get details for ${hash.slice(0, 8)}: ${e}\n`);
|
|
59
|
+
return false;
|
|
60
|
+
}
|
|
61
|
+
if (!isRelevant(details.message) && !force)
|
|
62
|
+
return false;
|
|
63
|
+
const meta = buildMetadata({
|
|
64
|
+
hash: details.hash,
|
|
65
|
+
authorName: details.authorName,
|
|
66
|
+
authorEmail: details.authorEmail,
|
|
67
|
+
date: details.date,
|
|
68
|
+
message: details.message,
|
|
69
|
+
files: details.files,
|
|
70
|
+
repoName: this.repoName,
|
|
71
|
+
});
|
|
72
|
+
const statsStr = buildStatsStr(details.insertions, details.deletions);
|
|
73
|
+
let inserted;
|
|
74
|
+
try {
|
|
75
|
+
inserted = await this.chromaIndex.upsertCommit({
|
|
76
|
+
commitHash: details.hash,
|
|
77
|
+
authorName: meta.author_name,
|
|
78
|
+
authorEmail: meta.author_email,
|
|
79
|
+
committedDate: meta.committed_date,
|
|
80
|
+
message: details.message,
|
|
81
|
+
category: meta.category,
|
|
82
|
+
files: meta.files_changed,
|
|
83
|
+
statsStr,
|
|
84
|
+
repo: this.repoName,
|
|
85
|
+
});
|
|
86
|
+
}
|
|
87
|
+
catch (e) {
|
|
88
|
+
process.stderr.write(`Chroma insert failed ${details.hash.slice(0, 8)}: ${e}\n`);
|
|
89
|
+
inserted = false;
|
|
90
|
+
}
|
|
91
|
+
if (!inserted && !force)
|
|
92
|
+
return false;
|
|
93
|
+
// Layer 2 — fire-and-forget, no-throw
|
|
94
|
+
const summary = summarizeCommit({
|
|
95
|
+
hash: details.hash,
|
|
96
|
+
authorName: details.authorName,
|
|
97
|
+
authorEmail: details.authorEmail,
|
|
98
|
+
date: details.date,
|
|
99
|
+
message: details.message,
|
|
100
|
+
insertions: details.insertions,
|
|
101
|
+
deletions: details.deletions,
|
|
102
|
+
fileCount: details.fileCount,
|
|
103
|
+
files: details.files,
|
|
104
|
+
});
|
|
105
|
+
this.contextStore.addCommit({
|
|
106
|
+
hash: details.hash,
|
|
107
|
+
summary,
|
|
108
|
+
metadata: meta,
|
|
109
|
+
userId: this.userId,
|
|
110
|
+
}).catch((e) => {
|
|
111
|
+
process.stderr.write(`Mem0-equiv failed for ${details.hash.slice(0, 8)} (Chroma OK): ${e}\n`);
|
|
112
|
+
});
|
|
113
|
+
return true;
|
|
114
|
+
}
|
|
115
|
+
/**
|
|
116
|
+
* Index all commits on a branch.
|
|
117
|
+
* Matches Python index_all() exactly:
|
|
118
|
+
* - Iterates commits, skips irrelevant, logs progress every 100
|
|
119
|
+
* - Returns IndexStats
|
|
120
|
+
*/
|
|
121
|
+
async indexAll(params = {}) {
|
|
122
|
+
const branch = params.branch ?? 'HEAD';
|
|
123
|
+
const stats = {
|
|
124
|
+
total_evaluated: 0,
|
|
125
|
+
stored: 0,
|
|
126
|
+
skipped_irrelevant: 0,
|
|
127
|
+
skipped_duplicate: 0,
|
|
128
|
+
errors: 0,
|
|
129
|
+
};
|
|
130
|
+
const logOptions = {
|
|
131
|
+
format: { hash: '%H', message: '%s' },
|
|
132
|
+
};
|
|
133
|
+
if (params.limit)
|
|
134
|
+
logOptions.maxCount = params.limit;
|
|
135
|
+
if (branch !== 'HEAD')
|
|
136
|
+
logOptions.from = branch;
|
|
137
|
+
let commits;
|
|
138
|
+
try {
|
|
139
|
+
const log = await this.git.log(logOptions);
|
|
140
|
+
commits = log.all;
|
|
141
|
+
}
|
|
142
|
+
catch (e) {
|
|
143
|
+
process.stderr.write(`indexAll: git log failed: ${e}\n`);
|
|
144
|
+
return stats;
|
|
145
|
+
}
|
|
146
|
+
process.stderr.write(`Found ${commits.length} commits to evaluate\n`);
|
|
147
|
+
stats.total_evaluated = commits.length;
|
|
148
|
+
for (let i = 0; i < commits.length; i++) {
|
|
149
|
+
if ((i + 1) % 100 === 0) {
|
|
150
|
+
process.stderr.write(`Progress: ${i + 1} / ${commits.length}\n`);
|
|
151
|
+
}
|
|
152
|
+
const { hash, message } = commits[i];
|
|
153
|
+
if (!isRelevant(message)) {
|
|
154
|
+
stats.skipped_irrelevant++;
|
|
155
|
+
continue;
|
|
156
|
+
}
|
|
157
|
+
if (params.dryRun) {
|
|
158
|
+
process.stderr.write(`[DRY-RUN] ${hash.slice(0, 8)} ${message.slice(0, 72)}\n`);
|
|
159
|
+
stats.stored++;
|
|
160
|
+
continue;
|
|
161
|
+
}
|
|
162
|
+
try {
|
|
163
|
+
const stored = await this.indexCommit(hash);
|
|
164
|
+
if (stored) {
|
|
165
|
+
stats.stored++;
|
|
166
|
+
}
|
|
167
|
+
else {
|
|
168
|
+
stats.skipped_duplicate++;
|
|
169
|
+
}
|
|
170
|
+
}
|
|
171
|
+
catch (e) {
|
|
172
|
+
process.stderr.write(`Error ${hash.slice(0, 8)}: ${e}\n`);
|
|
173
|
+
stats.errors++;
|
|
174
|
+
}
|
|
175
|
+
}
|
|
176
|
+
process.stderr.write(`Done. ${JSON.stringify(stats)}\n`);
|
|
177
|
+
return stats;
|
|
178
|
+
}
|
|
179
|
+
/**
|
|
180
|
+
* Get full commit details via git show + diff-tree.
|
|
181
|
+
* Uses git show for author/date/message, diff-tree for exact file list.
|
|
182
|
+
*/
|
|
183
|
+
async getCommitDetails(hash) {
|
|
184
|
+
// Get author, date, message
|
|
185
|
+
const showRaw = await this.git.raw([
|
|
186
|
+
'show',
|
|
187
|
+
'--no-patch',
|
|
188
|
+
'--format=%H%n%an%n%ae%n%aI%n%s',
|
|
189
|
+
hash,
|
|
190
|
+
]);
|
|
191
|
+
const lines = showRaw.trim().split('\n');
|
|
192
|
+
const authorName = lines[1]?.trim() ?? 'Unknown';
|
|
193
|
+
const authorEmail = lines[2]?.trim() ?? '';
|
|
194
|
+
const dateStr = lines[3]?.trim() ?? new Date().toISOString();
|
|
195
|
+
const message = lines[4]?.trim() ?? '';
|
|
196
|
+
// Get file list
|
|
197
|
+
let files = [];
|
|
198
|
+
try {
|
|
199
|
+
const filesRaw = await this.git.raw([
|
|
200
|
+
'diff-tree', '--no-commit-id', '-r', '--name-only', hash,
|
|
201
|
+
]);
|
|
202
|
+
files = filesRaw.trim().split('\n').filter(Boolean);
|
|
203
|
+
}
|
|
204
|
+
catch { /* root commit or shallow clone — leave empty */ }
|
|
205
|
+
// Get stat numbers
|
|
206
|
+
let insertions = 0;
|
|
207
|
+
let deletions = 0;
|
|
208
|
+
let fileCount = files.length;
|
|
209
|
+
try {
|
|
210
|
+
const statRaw = await this.git.raw([
|
|
211
|
+
'show', '--stat', '--format=', hash,
|
|
212
|
+
]);
|
|
213
|
+
// Last line: "N files changed, N insertions(+), N deletions(-)"
|
|
214
|
+
const statLines = statRaw.trim().split('\n');
|
|
215
|
+
const summaryLine = statLines[statLines.length - 1] ?? '';
|
|
216
|
+
const insMatch = summaryLine.match(/(\d+) insertion/);
|
|
217
|
+
const delMatch = summaryLine.match(/(\d+) deletion/);
|
|
218
|
+
const fileMatch = summaryLine.match(/(\d+) file/);
|
|
219
|
+
if (insMatch)
|
|
220
|
+
insertions = parseInt(insMatch[1], 10);
|
|
221
|
+
if (delMatch)
|
|
222
|
+
deletions = parseInt(delMatch[1], 10);
|
|
223
|
+
if (fileMatch)
|
|
224
|
+
fileCount = parseInt(fileMatch[1], 10);
|
|
225
|
+
}
|
|
226
|
+
catch { /* leave zeros */ }
|
|
227
|
+
return {
|
|
228
|
+
hash,
|
|
229
|
+
authorName,
|
|
230
|
+
authorEmail,
|
|
231
|
+
date: new Date(dateStr),
|
|
232
|
+
message,
|
|
233
|
+
files,
|
|
234
|
+
insertions,
|
|
235
|
+
deletions,
|
|
236
|
+
fileCount,
|
|
237
|
+
};
|
|
238
|
+
}
|
|
239
|
+
get name() { return this.repoName; }
|
|
240
|
+
get chroma() { return this.chromaIndex; }
|
|
241
|
+
}
|
|
242
|
+
//# sourceMappingURL=indexer.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"indexer.js","sourceRoot":"","sources":["../src/indexer.ts"],"names":[],"mappings":"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"}
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"mcp-server.d.ts","sourceRoot":"","sources":["../src/mcp-server.ts"],"names":[],"mappings":";AACA;;;;GAIG"}
|
|
@@ -0,0 +1,373 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
/**
|
|
3
|
+
* git-memory MCP server
|
|
4
|
+
* Exposes 5 tools to Claude Code via stdio transport.
|
|
5
|
+
* All logging goes to stderr — stdout is reserved for JSON-RPC protocol.
|
|
6
|
+
*/
|
|
7
|
+
import { Server } from '@modelcontextprotocol/sdk/server/index.js';
|
|
8
|
+
import { StdioServerTransport } from '@modelcontextprotocol/sdk/server/stdio.js';
|
|
9
|
+
import { CallToolRequestSchema, ListToolsRequestSchema, } from '@modelcontextprotocol/sdk/types.js';
|
|
10
|
+
import { basename } from 'path';
|
|
11
|
+
import { simpleGit } from 'simple-git';
|
|
12
|
+
import { ChromaCommitIndex } from './chroma-index.js';
|
|
13
|
+
import { ContextStore } from './context-store.js';
|
|
14
|
+
import { createEmbeddingFunction } from './embeddings.js';
|
|
15
|
+
import { configFromEnv } from './types.js';
|
|
16
|
+
// ── Logging (stderr only — stdout is the MCP channel) ────────────────────────
|
|
17
|
+
function log(...args) {
|
|
18
|
+
process.stderr.write(args.map(String).join(' ') + '\n');
|
|
19
|
+
}
|
|
20
|
+
// ── Lazy-initialised singletons ───────────────────────────────────────────────
|
|
21
|
+
const config = configFromEnv();
|
|
22
|
+
let _chroma = null;
|
|
23
|
+
let _context = null;
|
|
24
|
+
let _git;
|
|
25
|
+
let _repoName = basename(config.repoPath);
|
|
26
|
+
async function getChroma() {
|
|
27
|
+
if (!_chroma) {
|
|
28
|
+
const embedFn = await createEmbeddingFunction(config.embedProvider, config.embedModel, config.ollamaUrl, config.openaiApiKey);
|
|
29
|
+
_chroma = await ChromaCommitIndex.create(config.chromaDir, embedFn);
|
|
30
|
+
try {
|
|
31
|
+
const git = simpleGit(config.repoPath);
|
|
32
|
+
_repoName = basename((await git.revparse(['--show-toplevel'])).trim());
|
|
33
|
+
}
|
|
34
|
+
catch { /* keep basename fallback */ }
|
|
35
|
+
}
|
|
36
|
+
return _chroma;
|
|
37
|
+
}
|
|
38
|
+
async function getContext() {
|
|
39
|
+
if (_context === null) {
|
|
40
|
+
try {
|
|
41
|
+
const embedFn = await createEmbeddingFunction(config.embedProvider, config.embedModel, config.ollamaUrl, config.openaiApiKey);
|
|
42
|
+
_context = await ContextStore.create(config, embedFn);
|
|
43
|
+
}
|
|
44
|
+
catch (e) {
|
|
45
|
+
log('ContextStore init failed (Layer 2 disabled):', e);
|
|
46
|
+
}
|
|
47
|
+
}
|
|
48
|
+
return _context;
|
|
49
|
+
}
|
|
50
|
+
function getGit() {
|
|
51
|
+
if (!_git)
|
|
52
|
+
_git = simpleGit(config.repoPath);
|
|
53
|
+
return _git;
|
|
54
|
+
}
|
|
55
|
+
// ── Shared helpers ────────────────────────────────────────────────────────────
|
|
56
|
+
/** Deduplicate by commit_hash, keep highest relevance_score. Matches Python _dedupe(). */
|
|
57
|
+
function dedup(records) {
|
|
58
|
+
const seen = new Map();
|
|
59
|
+
for (const r of records) {
|
|
60
|
+
const h = r.commit_hash ?? 'unknown';
|
|
61
|
+
const existing = seen.get(h);
|
|
62
|
+
if (!existing || (r.relevance_score ?? 0) > (existing.relevance_score ?? 0)) {
|
|
63
|
+
seen.set(h, r);
|
|
64
|
+
}
|
|
65
|
+
}
|
|
66
|
+
return [...seen.values()];
|
|
67
|
+
}
|
|
68
|
+
/**
|
|
69
|
+
* Merge ChromaDB facts with ContextStore learned context.
|
|
70
|
+
* Matches Python _merge_results() exactly.
|
|
71
|
+
*/
|
|
72
|
+
function mergeResults(chromaRecords, contextRecords) {
|
|
73
|
+
const merged = new Map();
|
|
74
|
+
for (const r of chromaRecords) {
|
|
75
|
+
merged.set(r.commit_hash ?? 'unknown', { ...r, learned_context: [] });
|
|
76
|
+
}
|
|
77
|
+
for (const r of contextRecords) {
|
|
78
|
+
const h = r.commit_hash ?? 'unknown';
|
|
79
|
+
const contextText = r.summary ?? '';
|
|
80
|
+
const existing = merged.get(h);
|
|
81
|
+
if (existing) {
|
|
82
|
+
existing.learned_context = existing.learned_context ?? [];
|
|
83
|
+
existing.learned_context.push(contextText);
|
|
84
|
+
}
|
|
85
|
+
else {
|
|
86
|
+
merged.set(h, { ...r, learned_context: [contextText], source: 'context_only' });
|
|
87
|
+
}
|
|
88
|
+
}
|
|
89
|
+
const results = [...merged.values()];
|
|
90
|
+
results.sort((a, b) => (b.relevance_score ?? 0) - (a.relevance_score ?? 0));
|
|
91
|
+
return results;
|
|
92
|
+
}
|
|
93
|
+
/** commitRow helper for commits_touching_file — matches Python _commit_row(). */
|
|
94
|
+
function commitRow(c) {
|
|
95
|
+
return {
|
|
96
|
+
commit_hash: c.hash,
|
|
97
|
+
short_hash: c.hash.slice(0, 8),
|
|
98
|
+
author: c.author_name,
|
|
99
|
+
date: c.date,
|
|
100
|
+
category: 'general',
|
|
101
|
+
files_changed: [],
|
|
102
|
+
summary: c.message.trim(),
|
|
103
|
+
relevance_score: 0,
|
|
104
|
+
};
|
|
105
|
+
}
|
|
106
|
+
function ok(data) {
|
|
107
|
+
return { content: [{ type: 'text', text: JSON.stringify(data, null, 2) }] };
|
|
108
|
+
}
|
|
109
|
+
// ── Tool schemas ──────────────────────────────────────────────────────────────
|
|
110
|
+
const tools = [
|
|
111
|
+
{
|
|
112
|
+
name: 'search_git_history',
|
|
113
|
+
description: 'Semantically search the indexed Git history for commits related to a topic.',
|
|
114
|
+
inputSchema: {
|
|
115
|
+
type: 'object',
|
|
116
|
+
properties: {
|
|
117
|
+
query: { type: 'string', description: 'Natural-language description of what you are looking for' },
|
|
118
|
+
limit: { type: 'number', description: 'Max results (default 10, max 50)', default: 10 },
|
|
119
|
+
category: { type: 'string', description: 'Optional filter: fix|feat|refactor|arch|perf|security|migration|general' },
|
|
120
|
+
},
|
|
121
|
+
required: ['query'],
|
|
122
|
+
},
|
|
123
|
+
},
|
|
124
|
+
{
|
|
125
|
+
name: 'latest_commits',
|
|
126
|
+
description: 'Retrieve the most-recently indexed commits from memory.',
|
|
127
|
+
inputSchema: {
|
|
128
|
+
type: 'object',
|
|
129
|
+
properties: {
|
|
130
|
+
limit: { type: 'number', description: 'Number of recent commits (default 10, max 100)', default: 10 },
|
|
131
|
+
},
|
|
132
|
+
},
|
|
133
|
+
},
|
|
134
|
+
{
|
|
135
|
+
name: 'commits_touching_file',
|
|
136
|
+
description: 'Find commits that modified a specific file. Partial path match supported.',
|
|
137
|
+
inputSchema: {
|
|
138
|
+
type: 'object',
|
|
139
|
+
properties: {
|
|
140
|
+
filename: { type: 'string', description: 'File path relative to repo root (partial match supported)' },
|
|
141
|
+
limit: { type: 'number', description: 'Max results (default 20, max 100)', default: 20 },
|
|
142
|
+
},
|
|
143
|
+
required: ['filename'],
|
|
144
|
+
},
|
|
145
|
+
},
|
|
146
|
+
{
|
|
147
|
+
name: 'bug_fix_history',
|
|
148
|
+
description: 'Retrieve bug-fix and security commits related to a component or topic.',
|
|
149
|
+
inputSchema: {
|
|
150
|
+
type: 'object',
|
|
151
|
+
properties: {
|
|
152
|
+
component: { type: 'string', description: 'Component name, module path, or topic keyword' },
|
|
153
|
+
limit: { type: 'number', description: 'Max commits (default 15, max 100)', default: 15 },
|
|
154
|
+
include_security: { type: 'boolean', description: 'Include security-category commits (default true)', default: true },
|
|
155
|
+
},
|
|
156
|
+
required: ['component'],
|
|
157
|
+
},
|
|
158
|
+
},
|
|
159
|
+
{
|
|
160
|
+
name: 'architecture_decisions',
|
|
161
|
+
description: 'Surface architectural decision commits — refactors, migrations, design changes.',
|
|
162
|
+
inputSchema: {
|
|
163
|
+
type: 'object',
|
|
164
|
+
properties: {
|
|
165
|
+
topic: { type: 'string', description: 'Optional topic to narrow the search', default: '' },
|
|
166
|
+
limit: { type: 'number', description: 'Max commits (default 10, max 50)', default: 10 },
|
|
167
|
+
},
|
|
168
|
+
},
|
|
169
|
+
},
|
|
170
|
+
];
|
|
171
|
+
// ── MCP Server ────────────────────────────────────────────────────────────────
|
|
172
|
+
const server = new Server({ name: 'git-memory', version: '0.1.0' }, { capabilities: { tools: {} } });
|
|
173
|
+
server.setRequestHandler(ListToolsRequestSchema, async () => ({ tools }));
|
|
174
|
+
server.setRequestHandler(CallToolRequestSchema, async (request) => {
|
|
175
|
+
const { name, arguments: args = {} } = request.params;
|
|
176
|
+
switch (name) {
|
|
177
|
+
// ── Tool 1: search_git_history ────────────────────────────────────────────
|
|
178
|
+
case 'search_git_history': {
|
|
179
|
+
const { query, limit: rawLimit = 10, category } = args;
|
|
180
|
+
const limit = Math.min(Math.max(1, rawLimit), 50);
|
|
181
|
+
const chroma = await getChroma();
|
|
182
|
+
const chromaResults = await chroma.search({ query, nResults: limit, category, repo: _repoName });
|
|
183
|
+
let contextResults = [];
|
|
184
|
+
const ctx = await getContext();
|
|
185
|
+
if (ctx) {
|
|
186
|
+
try {
|
|
187
|
+
contextResults = dedup(await ctx.search({ query, userId: config.userId, limit }));
|
|
188
|
+
}
|
|
189
|
+
catch (e) {
|
|
190
|
+
log('ContextStore search failed:', e);
|
|
191
|
+
}
|
|
192
|
+
}
|
|
193
|
+
let results = mergeResults(chromaResults, contextResults);
|
|
194
|
+
if (category)
|
|
195
|
+
results = results.filter(r => r.category === category);
|
|
196
|
+
if (results.length === 0) {
|
|
197
|
+
return ok([{ message: `No commits found matching '${query}'` }]);
|
|
198
|
+
}
|
|
199
|
+
return ok(results.slice(0, limit));
|
|
200
|
+
}
|
|
201
|
+
// ── Tool 2: latest_commits ────────────────────────────────────────────────
|
|
202
|
+
case 'latest_commits': {
|
|
203
|
+
const { limit: rawLimit = 10 } = args;
|
|
204
|
+
const limit = Math.min(Math.max(1, rawLimit), 100);
|
|
205
|
+
const chroma = await getChroma();
|
|
206
|
+
const records = await chroma.getLatest({ n: limit, repo: _repoName });
|
|
207
|
+
const contextMap = new Map();
|
|
208
|
+
const ctx = await getContext();
|
|
209
|
+
if (ctx) {
|
|
210
|
+
try {
|
|
211
|
+
for (const r of dedup(await ctx.getAll(config.userId))) {
|
|
212
|
+
if (r.commit_hash) {
|
|
213
|
+
const arr = contextMap.get(r.commit_hash) ?? [];
|
|
214
|
+
arr.push(r.summary ?? '');
|
|
215
|
+
contextMap.set(r.commit_hash, arr);
|
|
216
|
+
}
|
|
217
|
+
}
|
|
218
|
+
}
|
|
219
|
+
catch { /* ignore */ }
|
|
220
|
+
}
|
|
221
|
+
for (const r of records) {
|
|
222
|
+
r.learned_context = contextMap.get(r.commit_hash) ?? [];
|
|
223
|
+
}
|
|
224
|
+
return ok(records);
|
|
225
|
+
}
|
|
226
|
+
// ── Tool 3: commits_touching_file ─────────────────────────────────────────
|
|
227
|
+
case 'commits_touching_file': {
|
|
228
|
+
const { filename, limit: rawLimit = 20 } = args;
|
|
229
|
+
const limit = Math.min(Math.max(1, rawLimit), 100);
|
|
230
|
+
const git = getGit();
|
|
231
|
+
let gitCommits = [];
|
|
232
|
+
// 1. Exact path match (fast)
|
|
233
|
+
try {
|
|
234
|
+
const log = await git.log({ file: filename, maxCount: limit * 3 });
|
|
235
|
+
gitCommits = log.all.map(commitRow);
|
|
236
|
+
}
|
|
237
|
+
catch { /* ignore */ }
|
|
238
|
+
// 2. Fallback: basename scan across 500 recent commits
|
|
239
|
+
if (gitCommits.length === 0) {
|
|
240
|
+
const needle = filename.toLowerCase();
|
|
241
|
+
try {
|
|
242
|
+
const allLog = await git.log({ maxCount: 500 });
|
|
243
|
+
for (const c of allLog.all) {
|
|
244
|
+
const filesRaw = await git.raw(['diff-tree', '--no-commit-id', '-r', '--name-only', c.hash]);
|
|
245
|
+
if (filesRaw.toLowerCase().includes(needle)) {
|
|
246
|
+
gitCommits.push(commitRow(c));
|
|
247
|
+
if (gitCommits.length >= limit * 3)
|
|
248
|
+
break;
|
|
249
|
+
}
|
|
250
|
+
}
|
|
251
|
+
}
|
|
252
|
+
catch (e) {
|
|
253
|
+
return ok([{ error: String(e) }]);
|
|
254
|
+
}
|
|
255
|
+
}
|
|
256
|
+
if (gitCommits.length === 0) {
|
|
257
|
+
return ok([{ message: `No commits found touching '${filename}'` }]);
|
|
258
|
+
}
|
|
259
|
+
// 3. ChromaDB: category enrichment
|
|
260
|
+
const chroma = await getChroma();
|
|
261
|
+
const chromaMap = new Map((await chroma.searchByFile({ filename, nResults: limit * 2, repo: _repoName }))
|
|
262
|
+
.map(r => [r.commit_hash, r]));
|
|
263
|
+
// 4. ContextStore: learned_context enrichment
|
|
264
|
+
const contextMap = new Map();
|
|
265
|
+
const ctx = await getContext();
|
|
266
|
+
if (ctx) {
|
|
267
|
+
try {
|
|
268
|
+
for (const r of dedup(await ctx.search({ query: `changes to ${filename}`, userId: config.userId, limit: 50 }))) {
|
|
269
|
+
if (r.commit_hash) {
|
|
270
|
+
const arr = contextMap.get(r.commit_hash) ?? [];
|
|
271
|
+
arr.push(r.summary ?? '');
|
|
272
|
+
contextMap.set(r.commit_hash, arr);
|
|
273
|
+
}
|
|
274
|
+
}
|
|
275
|
+
}
|
|
276
|
+
catch { /* ignore */ }
|
|
277
|
+
}
|
|
278
|
+
// 5. Enrich and return
|
|
279
|
+
const enriched = gitCommits.slice(0, limit).map(gc => ({
|
|
280
|
+
...gc,
|
|
281
|
+
category: chromaMap.get(gc.commit_hash)?.category ?? 'general',
|
|
282
|
+
in_chroma_index: chromaMap.has(gc.commit_hash),
|
|
283
|
+
learned_context: contextMap.get(gc.commit_hash) ?? [],
|
|
284
|
+
}));
|
|
285
|
+
return ok(enriched);
|
|
286
|
+
}
|
|
287
|
+
// ── Tool 4: bug_fix_history ───────────────────────────────────────────────
|
|
288
|
+
case 'bug_fix_history': {
|
|
289
|
+
const { component, limit: rawLimit = 15, include_security = true } = args;
|
|
290
|
+
const limit = Math.min(Math.max(1, rawLimit), 100);
|
|
291
|
+
const targetCategories = new Set(['fix', 'bug', 'hotfix', 'patch', 'revert']);
|
|
292
|
+
if (include_security)
|
|
293
|
+
targetCategories.add('security');
|
|
294
|
+
const chroma = await getChroma();
|
|
295
|
+
const chromaResults = [];
|
|
296
|
+
for (const cat of targetCategories) {
|
|
297
|
+
chromaResults.push(...await chroma.search({ query: `${component} ${cat}`, nResults: limit, category: cat, repo: _repoName }));
|
|
298
|
+
}
|
|
299
|
+
let contextResults = [];
|
|
300
|
+
const ctx = await getContext();
|
|
301
|
+
if (ctx) {
|
|
302
|
+
try {
|
|
303
|
+
for (const q of [`bug fix ${component}`, `security ${component}`]) {
|
|
304
|
+
contextResults.push(...await ctx.search({ query: q, userId: config.userId, limit: 20 }));
|
|
305
|
+
}
|
|
306
|
+
}
|
|
307
|
+
catch (e) {
|
|
308
|
+
log('ContextStore bug_fix search failed:', e);
|
|
309
|
+
}
|
|
310
|
+
}
|
|
311
|
+
const merged = mergeResults(chromaResults, dedup(contextResults));
|
|
312
|
+
let filtered = merged.filter(r => targetCategories.has(r.category));
|
|
313
|
+
if (filtered.length < 3)
|
|
314
|
+
filtered = merged;
|
|
315
|
+
if (filtered.length === 0) {
|
|
316
|
+
return ok([{ message: `No bug-fix history found for component '${component}'` }]);
|
|
317
|
+
}
|
|
318
|
+
return ok(filtered.slice(0, limit));
|
|
319
|
+
}
|
|
320
|
+
// ── Tool 5: architecture_decisions ───────────────────────────────────────
|
|
321
|
+
case 'architecture_decisions': {
|
|
322
|
+
const { topic = '', limit: rawLimit = 10 } = args;
|
|
323
|
+
const limit = Math.min(Math.max(1, rawLimit), 50);
|
|
324
|
+
// Python uses 5 categories: arch, architecture, refactor, migration, redesign
|
|
325
|
+
const archCategories = new Set(['arch', 'architecture', 'refactor', 'migration', 'redesign']);
|
|
326
|
+
const query = `architecture design decision ${topic}`.trim();
|
|
327
|
+
const chroma = await getChroma();
|
|
328
|
+
// Pass 1: category-filtered
|
|
329
|
+
const chromaArch = [];
|
|
330
|
+
for (const cat of archCategories) {
|
|
331
|
+
chromaArch.push(...await chroma.search({ query, nResults: limit, category: cat, repo: _repoName }));
|
|
332
|
+
}
|
|
333
|
+
// Pass 2: broad semantic (catches significant feat commits)
|
|
334
|
+
const chromaBroad = await chroma.search({ query, nResults: limit, repo: _repoName });
|
|
335
|
+
const archHashes = new Set(chromaArch.map(r => r.commit_hash));
|
|
336
|
+
const chromaResults = [
|
|
337
|
+
...chromaArch,
|
|
338
|
+
...chromaBroad.filter(r => !archHashes.has(r.commit_hash)),
|
|
339
|
+
];
|
|
340
|
+
let contextResults = [];
|
|
341
|
+
const ctx = await getContext();
|
|
342
|
+
if (ctx) {
|
|
343
|
+
try {
|
|
344
|
+
contextResults = dedup(await ctx.search({ query, userId: config.userId, limit: limit * 2 }));
|
|
345
|
+
}
|
|
346
|
+
catch (e) {
|
|
347
|
+
log('ContextStore arch search failed:', e);
|
|
348
|
+
}
|
|
349
|
+
}
|
|
350
|
+
const merged = mergeResults(chromaResults, contextResults);
|
|
351
|
+
const archFirst = merged.filter(r => archCategories.has(r.category));
|
|
352
|
+
const others = merged.filter(r => !archCategories.has(r.category));
|
|
353
|
+
const combined = [...archFirst, ...others].slice(0, limit);
|
|
354
|
+
if (combined.length === 0) {
|
|
355
|
+
return ok([{ message: 'No architectural commits found' }]);
|
|
356
|
+
}
|
|
357
|
+
return ok(combined);
|
|
358
|
+
}
|
|
359
|
+
default:
|
|
360
|
+
return ok([{ error: `Unknown tool: ${name}` }]);
|
|
361
|
+
}
|
|
362
|
+
});
|
|
363
|
+
// ── Entry point ───────────────────────────────────────────────────────────────
|
|
364
|
+
async function main() {
|
|
365
|
+
const transport = new StdioServerTransport();
|
|
366
|
+
await server.connect(transport);
|
|
367
|
+
log(`git-memory MCP server started (repo: ${config.repoPath}, user: ${config.userId})`);
|
|
368
|
+
}
|
|
369
|
+
main().catch(e => {
|
|
370
|
+
process.stderr.write(`Fatal: ${e}\n`);
|
|
371
|
+
process.exit(1);
|
|
372
|
+
});
|
|
373
|
+
//# sourceMappingURL=mcp-server.js.map
|