@jussmor/commit-memory-mcp 0.3.7 → 0.3.8
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/README.md +17 -0
- package/dist/db/client.js +72 -47
- package/dist/git/insights.d.ts +21 -0
- package/dist/git/insights.js +60 -0
- package/dist/mcp/server.js +133 -1
- package/package.json +1 -1
package/README.md
CHANGED
|
@@ -108,6 +108,9 @@ build_context_pack({
|
|
|
108
108
|
```
|
|
109
109
|
|
|
110
110
|
Use this before invoking a coding subagent to keep prompts small and focused.
|
|
111
|
+
If no rows are found in strict scope, the server now falls back automatically to broader scope levels.
|
|
112
|
+
|
|
113
|
+
Important: if you provide `domain`/`feature`/`branch` tags in `build_context_pack`, use the same tags during `sync_pr_context` for best precision.
|
|
111
114
|
|
|
112
115
|
### Promote draft facts after review
|
|
113
116
|
|
|
@@ -143,6 +146,20 @@ who_changed_this({
|
|
|
143
146
|
|
|
144
147
|
Use this to discover recent authors and commit history for a target file.
|
|
145
148
|
|
|
149
|
+
### Explain what is happening in a Next.js folder
|
|
150
|
+
|
|
151
|
+
```text
|
|
152
|
+
explain_path_activity({
|
|
153
|
+
targetPath: "app/dashboard",
|
|
154
|
+
owner: "MaxwellClinic-Development",
|
|
155
|
+
repo: "EverBetter-Pro",
|
|
156
|
+
limit: 30
|
|
157
|
+
})
|
|
158
|
+
```
|
|
159
|
+
|
|
160
|
+
Use this when you want a fast folder-level summary for areas like `app/dashboard`, `app/(authenticated)`, or `src/components`.
|
|
161
|
+
It returns recent commits, top authors, most touched files, and related PR context/decisions when available.
|
|
162
|
+
|
|
146
163
|
### Explain intent for a change
|
|
147
164
|
|
|
148
165
|
```text
|
package/dist/db/client.js
CHANGED
|
@@ -371,32 +371,30 @@ export function promoteContextFacts(db, options) {
|
|
|
371
371
|
return Number(result.changes ?? 0);
|
|
372
372
|
}
|
|
373
373
|
export function buildContextPack(db, options) {
|
|
374
|
-
const
|
|
375
|
-
|
|
376
|
-
|
|
377
|
-
|
|
378
|
-
params.
|
|
379
|
-
|
|
380
|
-
|
|
381
|
-
|
|
382
|
-
params.
|
|
383
|
-
|
|
384
|
-
|
|
385
|
-
|
|
386
|
-
params.
|
|
387
|
-
|
|
388
|
-
|
|
389
|
-
|
|
390
|
-
|
|
391
|
-
|
|
392
|
-
|
|
393
|
-
|
|
394
|
-
|
|
395
|
-
|
|
396
|
-
clauses.
|
|
397
|
-
|
|
398
|
-
const where = clauses.length > 0 ? `WHERE ${clauses.join(" AND ")}` : "";
|
|
399
|
-
const sql = `
|
|
374
|
+
const taskType = options.taskType ?? "general";
|
|
375
|
+
function runQuery(params) {
|
|
376
|
+
const clauses = [];
|
|
377
|
+
const values = [taskType, taskType];
|
|
378
|
+
if (params.includeDomain && options.domain) {
|
|
379
|
+
clauses.push("scope_domain = ?");
|
|
380
|
+
values.push(options.domain);
|
|
381
|
+
}
|
|
382
|
+
if (params.includeFeature && options.feature) {
|
|
383
|
+
clauses.push("scope_feature = ?");
|
|
384
|
+
values.push(options.feature);
|
|
385
|
+
}
|
|
386
|
+
if (params.includeBranch && options.branch) {
|
|
387
|
+
clauses.push("scope_branch = ?");
|
|
388
|
+
values.push(options.branch);
|
|
389
|
+
}
|
|
390
|
+
if (options.includeDraft) {
|
|
391
|
+
clauses.push("status IN ('promoted', 'draft')");
|
|
392
|
+
}
|
|
393
|
+
else {
|
|
394
|
+
clauses.push("status = 'promoted'");
|
|
395
|
+
}
|
|
396
|
+
const where = clauses.length > 0 ? `WHERE ${clauses.join(" AND ")}` : "";
|
|
397
|
+
const sql = `
|
|
400
398
|
SELECT
|
|
401
399
|
id,
|
|
402
400
|
source_type,
|
|
@@ -411,10 +409,15 @@ export function buildContextPack(db, options) {
|
|
|
411
409
|
confidence,
|
|
412
410
|
status,
|
|
413
411
|
updated_at,
|
|
414
|
-
((priority * 0.
|
|
412
|
+
((priority * 0.40) + (confidence * 0.30) +
|
|
415
413
|
CASE
|
|
416
|
-
WHEN scope_task_type = ? THEN 0.
|
|
417
|
-
WHEN scope_task_type = 'general' THEN 0.
|
|
414
|
+
WHEN scope_task_type = ? THEN 0.30
|
|
415
|
+
WHEN scope_task_type = 'general' THEN 0.15
|
|
416
|
+
ELSE 0.0
|
|
417
|
+
END +
|
|
418
|
+
CASE
|
|
419
|
+
WHEN source_type = 'pr_description' THEN 0.15
|
|
420
|
+
WHEN source_type LIKE 'pr_%' THEN 0.08
|
|
418
421
|
ELSE 0.0
|
|
419
422
|
END) AS score
|
|
420
423
|
FROM context_facts
|
|
@@ -422,24 +425,46 @@ export function buildContextPack(db, options) {
|
|
|
422
425
|
ORDER BY score DESC, updated_at DESC
|
|
423
426
|
LIMIT ?
|
|
424
427
|
`;
|
|
425
|
-
|
|
426
|
-
|
|
427
|
-
|
|
428
|
-
|
|
429
|
-
|
|
430
|
-
|
|
431
|
-
|
|
432
|
-
|
|
433
|
-
|
|
434
|
-
|
|
435
|
-
|
|
436
|
-
|
|
437
|
-
|
|
438
|
-
|
|
439
|
-
|
|
440
|
-
|
|
441
|
-
|
|
442
|
-
|
|
428
|
+
values.push(options.limit);
|
|
429
|
+
const rows = db.prepare(sql).all(...values);
|
|
430
|
+
return rows.map((row) => ({
|
|
431
|
+
id: String(row.id ?? ""),
|
|
432
|
+
sourceType: String(row.source_type ?? ""),
|
|
433
|
+
sourceRef: String(row.source_ref ?? ""),
|
|
434
|
+
title: String(row.title ?? ""),
|
|
435
|
+
content: String(row.content ?? ""),
|
|
436
|
+
domain: String(row.scope_domain ?? ""),
|
|
437
|
+
feature: String(row.scope_feature ?? ""),
|
|
438
|
+
branch: String(row.scope_branch ?? ""),
|
|
439
|
+
taskType: String(row.scope_task_type ?? ""),
|
|
440
|
+
priority: Number(row.priority ?? 0),
|
|
441
|
+
confidence: Number(row.confidence ?? 0),
|
|
442
|
+
score: Number(row.score ?? 0),
|
|
443
|
+
status: String(row.status ?? "promoted"),
|
|
444
|
+
updatedAt: String(row.updated_at ?? ""),
|
|
445
|
+
}));
|
|
446
|
+
}
|
|
447
|
+
const strictRows = runQuery({
|
|
448
|
+
includeDomain: true,
|
|
449
|
+
includeFeature: true,
|
|
450
|
+
includeBranch: true,
|
|
451
|
+
});
|
|
452
|
+
if (strictRows.length > 0) {
|
|
453
|
+
return strictRows;
|
|
454
|
+
}
|
|
455
|
+
const broadRows = runQuery({
|
|
456
|
+
includeDomain: true,
|
|
457
|
+
includeFeature: false,
|
|
458
|
+
includeBranch: false,
|
|
459
|
+
});
|
|
460
|
+
if (broadRows.length > 0) {
|
|
461
|
+
return broadRows;
|
|
462
|
+
}
|
|
463
|
+
return runQuery({
|
|
464
|
+
includeDomain: false,
|
|
465
|
+
includeFeature: false,
|
|
466
|
+
includeBranch: false,
|
|
467
|
+
});
|
|
443
468
|
}
|
|
444
469
|
export function archiveFeatureContext(db, options) {
|
|
445
470
|
const now = new Date().toISOString();
|
package/dist/git/insights.d.ts
CHANGED
|
@@ -16,6 +16,27 @@ export declare function whoChangedFile(options: {
|
|
|
16
16
|
lastCommitAt: string;
|
|
17
17
|
}>;
|
|
18
18
|
};
|
|
19
|
+
export declare function explainPathActivity(options: {
|
|
20
|
+
repoPath: string;
|
|
21
|
+
targetPath: string;
|
|
22
|
+
limit: number;
|
|
23
|
+
}): {
|
|
24
|
+
targetPath: string;
|
|
25
|
+
commits: Array<{
|
|
26
|
+
sha: string;
|
|
27
|
+
author: string;
|
|
28
|
+
date: string;
|
|
29
|
+
subject: string;
|
|
30
|
+
}>;
|
|
31
|
+
topAuthors: Array<{
|
|
32
|
+
author: string;
|
|
33
|
+
commitCount: number;
|
|
34
|
+
}>;
|
|
35
|
+
topFiles: Array<{
|
|
36
|
+
filePath: string;
|
|
37
|
+
touchCount: number;
|
|
38
|
+
}>;
|
|
39
|
+
};
|
|
19
40
|
export declare function latestCommitForFile(repoPath: string, filePath: string): string | null;
|
|
20
41
|
export declare function commitDetails(repoPath: string, sha: string): {
|
|
21
42
|
sha: string;
|
package/dist/git/insights.js
CHANGED
|
@@ -46,6 +46,66 @@ export function whoChangedFile(options) {
|
|
|
46
46
|
authors,
|
|
47
47
|
};
|
|
48
48
|
}
|
|
49
|
+
export function explainPathActivity(options) {
|
|
50
|
+
const repoPath = path.resolve(options.repoPath);
|
|
51
|
+
const limit = Math.max(1, options.limit);
|
|
52
|
+
const targetPath = options.targetPath.trim();
|
|
53
|
+
const commitsRaw = runGit(repoPath, [
|
|
54
|
+
"log",
|
|
55
|
+
`-n${limit}`,
|
|
56
|
+
"--format=%H%x1f%an%x1f%aI%x1f%s",
|
|
57
|
+
"--",
|
|
58
|
+
targetPath,
|
|
59
|
+
]).trim();
|
|
60
|
+
if (!commitsRaw) {
|
|
61
|
+
return {
|
|
62
|
+
targetPath,
|
|
63
|
+
commits: [],
|
|
64
|
+
topAuthors: [],
|
|
65
|
+
topFiles: [],
|
|
66
|
+
};
|
|
67
|
+
}
|
|
68
|
+
const commits = commitsRaw.split("\n").map((line) => {
|
|
69
|
+
const [sha = "", author = "", date = "", subject = ""] = line.split("\x1f");
|
|
70
|
+
return { sha, author, date, subject };
|
|
71
|
+
});
|
|
72
|
+
const authorCounts = new Map();
|
|
73
|
+
const fileCounts = new Map();
|
|
74
|
+
for (const commit of commits) {
|
|
75
|
+
authorCounts.set(commit.author, (authorCounts.get(commit.author) ?? 0) + 1);
|
|
76
|
+
const filesRaw = runGit(repoPath, [
|
|
77
|
+
"show",
|
|
78
|
+
"--name-only",
|
|
79
|
+
"--pretty=format:",
|
|
80
|
+
commit.sha,
|
|
81
|
+
"--",
|
|
82
|
+
targetPath,
|
|
83
|
+
]).trim();
|
|
84
|
+
if (!filesRaw) {
|
|
85
|
+
continue;
|
|
86
|
+
}
|
|
87
|
+
for (const filePath of filesRaw
|
|
88
|
+
.split("\n")
|
|
89
|
+
.map((line) => line.trim())
|
|
90
|
+
.filter(Boolean)) {
|
|
91
|
+
fileCounts.set(filePath, (fileCounts.get(filePath) ?? 0) + 1);
|
|
92
|
+
}
|
|
93
|
+
}
|
|
94
|
+
const topAuthors = Array.from(authorCounts.entries())
|
|
95
|
+
.map(([author, commitCount]) => ({ author, commitCount }))
|
|
96
|
+
.sort((a, b) => b.commitCount - a.commitCount)
|
|
97
|
+
.slice(0, 8);
|
|
98
|
+
const topFiles = Array.from(fileCounts.entries())
|
|
99
|
+
.map(([filePath, touchCount]) => ({ filePath, touchCount }))
|
|
100
|
+
.sort((a, b) => b.touchCount - a.touchCount)
|
|
101
|
+
.slice(0, 12);
|
|
102
|
+
return {
|
|
103
|
+
targetPath,
|
|
104
|
+
commits,
|
|
105
|
+
topAuthors,
|
|
106
|
+
topFiles,
|
|
107
|
+
};
|
|
108
|
+
}
|
|
49
109
|
export function latestCommitForFile(repoPath, filePath) {
|
|
50
110
|
const output = runGit(path.resolve(repoPath), [
|
|
51
111
|
"log",
|
package/dist/mcp/server.js
CHANGED
|
@@ -7,7 +7,7 @@ import fs from "node:fs";
|
|
|
7
7
|
import path from "node:path";
|
|
8
8
|
import { pathToFileURL } from "node:url";
|
|
9
9
|
import { archiveFeatureContext, buildContextPack, openDatabase, promoteContextFacts, upsertWorktreeSession, } from "../db/client.js";
|
|
10
|
-
import { commitDetails, latestCommitForFile, mainBranchOvernightBrief, resumeFeatureSessionBrief, whoChangedFile, } from "../git/insights.js";
|
|
10
|
+
import { commitDetails, explainPathActivity, latestCommitForFile, mainBranchOvernightBrief, resumeFeatureSessionBrief, whoChangedFile, } from "../git/insights.js";
|
|
11
11
|
import { listActiveWorktrees } from "../git/worktree.js";
|
|
12
12
|
import { syncPullRequestContext } from "../pr/sync.js";
|
|
13
13
|
function fetchRemote(repoPath) {
|
|
@@ -26,6 +26,78 @@ function detectReferencedPrNumber(text) {
|
|
|
26
26
|
}
|
|
27
27
|
return value;
|
|
28
28
|
}
|
|
29
|
+
function parsePrNumberFromSourceRef(sourceRef) {
|
|
30
|
+
const match = sourceRef.match(/#(\d{1,8})\b/);
|
|
31
|
+
if (!match) {
|
|
32
|
+
return null;
|
|
33
|
+
}
|
|
34
|
+
const value = Number.parseInt(match[1] ?? "", 10);
|
|
35
|
+
if (!Number.isFinite(value) || value <= 0) {
|
|
36
|
+
return null;
|
|
37
|
+
}
|
|
38
|
+
return value;
|
|
39
|
+
}
|
|
40
|
+
function loadPathPullRequestContext(db, options) {
|
|
41
|
+
const prNumbers = new Set(options.referencedPrNumbers);
|
|
42
|
+
const likePattern = `%${options.targetPath}%`;
|
|
43
|
+
const sourceRows = db
|
|
44
|
+
.prepare(`
|
|
45
|
+
SELECT source_ref
|
|
46
|
+
FROM context_facts
|
|
47
|
+
WHERE (title LIKE ? OR content LIKE ?)
|
|
48
|
+
AND source_ref LIKE '%#%'
|
|
49
|
+
ORDER BY updated_at DESC
|
|
50
|
+
LIMIT 80
|
|
51
|
+
`)
|
|
52
|
+
.all(likePattern, likePattern);
|
|
53
|
+
for (const row of sourceRows) {
|
|
54
|
+
const number = parsePrNumberFromSourceRef(String(row.source_ref ?? ""));
|
|
55
|
+
if (number) {
|
|
56
|
+
prNumbers.add(number);
|
|
57
|
+
}
|
|
58
|
+
}
|
|
59
|
+
if (prNumbers.size === 0) {
|
|
60
|
+
return [];
|
|
61
|
+
}
|
|
62
|
+
const results = [];
|
|
63
|
+
for (const prNumber of Array.from(prNumbers).slice(0, 20)) {
|
|
64
|
+
const pr = options.owner && options.repo
|
|
65
|
+
? (db
|
|
66
|
+
.prepare(`
|
|
67
|
+
SELECT repo_owner, repo_name, pr_number, title, body, author, state, created_at, updated_at, merged_at, url
|
|
68
|
+
FROM prs
|
|
69
|
+
WHERE repo_owner = ? AND repo_name = ? AND pr_number = ?
|
|
70
|
+
LIMIT 1
|
|
71
|
+
`)
|
|
72
|
+
.get(options.owner, options.repo, prNumber) ?? null)
|
|
73
|
+
: (db
|
|
74
|
+
.prepare(`
|
|
75
|
+
SELECT repo_owner, repo_name, pr_number, title, body, author, state, created_at, updated_at, merged_at, url
|
|
76
|
+
FROM prs
|
|
77
|
+
WHERE pr_number = ?
|
|
78
|
+
ORDER BY updated_at DESC
|
|
79
|
+
LIMIT 1
|
|
80
|
+
`)
|
|
81
|
+
.get(prNumber) ?? null);
|
|
82
|
+
if (!pr) {
|
|
83
|
+
continue;
|
|
84
|
+
}
|
|
85
|
+
const decisions = db
|
|
86
|
+
.prepare(`
|
|
87
|
+
SELECT id, source, author, summary, severity, created_at
|
|
88
|
+
FROM pr_decisions
|
|
89
|
+
WHERE repo_owner = ? AND repo_name = ? AND pr_number = ?
|
|
90
|
+
ORDER BY created_at DESC
|
|
91
|
+
LIMIT 20
|
|
92
|
+
`)
|
|
93
|
+
.all(pr.repo_owner, pr.repo_name, pr.pr_number);
|
|
94
|
+
results.push({
|
|
95
|
+
pr,
|
|
96
|
+
decisions,
|
|
97
|
+
});
|
|
98
|
+
}
|
|
99
|
+
return results;
|
|
100
|
+
}
|
|
29
101
|
function loadPullRequestContext(db, prNumber, repoOwner, repoName) {
|
|
30
102
|
const pr = repoOwner && repoName
|
|
31
103
|
? (db
|
|
@@ -162,6 +234,20 @@ export async function startMcpServer() {
|
|
|
162
234
|
required: ["filePath"],
|
|
163
235
|
},
|
|
164
236
|
},
|
|
237
|
+
{
|
|
238
|
+
name: "explain_path_activity",
|
|
239
|
+
description: "Given a file or folder path, summarize activity, top files/authors, and related PR context.",
|
|
240
|
+
inputSchema: {
|
|
241
|
+
type: "object",
|
|
242
|
+
properties: {
|
|
243
|
+
targetPath: { type: "string" },
|
|
244
|
+
owner: { type: "string" },
|
|
245
|
+
repo: { type: "string" },
|
|
246
|
+
limit: { type: "number" },
|
|
247
|
+
},
|
|
248
|
+
required: ["targetPath"],
|
|
249
|
+
},
|
|
250
|
+
},
|
|
165
251
|
{
|
|
166
252
|
name: "why_was_this_changed",
|
|
167
253
|
description: "Explain intent for a commit or file using git history and synced PR decisions.",
|
|
@@ -376,6 +462,52 @@ export async function startMcpServer() {
|
|
|
376
462
|
content: [{ type: "text", text: JSON.stringify(output, null, 2) }],
|
|
377
463
|
};
|
|
378
464
|
}
|
|
465
|
+
if (request.params.name === "explain_path_activity") {
|
|
466
|
+
const targetPath = String(request.params.arguments?.targetPath ?? "").trim();
|
|
467
|
+
const owner = String(request.params.arguments?.owner ?? "").trim();
|
|
468
|
+
const repo = String(request.params.arguments?.repo ?? "").trim();
|
|
469
|
+
const limit = Number(request.params.arguments?.limit ?? 25);
|
|
470
|
+
if (!targetPath) {
|
|
471
|
+
return {
|
|
472
|
+
content: [{ type: "text", text: "targetPath is required" }],
|
|
473
|
+
isError: true,
|
|
474
|
+
};
|
|
475
|
+
}
|
|
476
|
+
const output = explainPathActivity({
|
|
477
|
+
repoPath,
|
|
478
|
+
targetPath,
|
|
479
|
+
limit: Number.isFinite(limit) && limit > 0 ? limit : 25,
|
|
480
|
+
});
|
|
481
|
+
const referencedPrNumbers = output.commits
|
|
482
|
+
.map((commit) => {
|
|
483
|
+
const details = commitDetails(repoPath, commit.sha);
|
|
484
|
+
return detectReferencedPrNumber(`${details.subject}\n${details.body}`);
|
|
485
|
+
})
|
|
486
|
+
.filter((value) => Number.isFinite(value));
|
|
487
|
+
const db = openDatabase(dbPath);
|
|
488
|
+
try {
|
|
489
|
+
const relatedPullRequests = loadPathPullRequestContext(db, {
|
|
490
|
+
targetPath,
|
|
491
|
+
owner: owner || undefined,
|
|
492
|
+
repo: repo || undefined,
|
|
493
|
+
referencedPrNumbers,
|
|
494
|
+
});
|
|
495
|
+
return {
|
|
496
|
+
content: [
|
|
497
|
+
{
|
|
498
|
+
type: "text",
|
|
499
|
+
text: JSON.stringify({
|
|
500
|
+
...output,
|
|
501
|
+
relatedPullRequests,
|
|
502
|
+
}, null, 2),
|
|
503
|
+
},
|
|
504
|
+
],
|
|
505
|
+
};
|
|
506
|
+
}
|
|
507
|
+
finally {
|
|
508
|
+
db.close();
|
|
509
|
+
}
|
|
510
|
+
}
|
|
379
511
|
if (request.params.name === "why_was_this_changed") {
|
|
380
512
|
const owner = String(request.params.arguments?.owner ?? "").trim();
|
|
381
513
|
const repo = String(request.params.arguments?.repo ?? "").trim();
|
package/package.json
CHANGED