@jussmor/commit-memory-mcp 0.3.5 → 0.3.7
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 +194 -29
- package/dist/db/client.d.ts +26 -1
- package/dist/db/client.js +401 -0
- package/dist/git/insights.d.ts +51 -0
- package/dist/git/insights.js +146 -0
- package/dist/git/worktree.d.ts +3 -0
- package/dist/git/worktree.js +52 -0
- package/dist/mcp/server.js +435 -65
- package/dist/pr/sync.d.ts +13 -0
- package/dist/pr/sync.js +240 -0
- package/dist/types.d.ts +93 -0
- package/package.json +1 -1
package/dist/pr/sync.js
ADDED
|
@@ -0,0 +1,240 @@
|
|
|
1
|
+
import { execFileSync } from "node:child_process";
|
|
2
|
+
import path from "node:path";
|
|
3
|
+
import { openDatabase, replacePullRequestComments, replacePullRequestDecisions, replacePullRequestReviews, touchPullRequestSyncState, upsertContextFact, upsertPullRequest, } from "../db/client.js";
|
|
4
|
+
function runGh(repoPath, args) {
|
|
5
|
+
return execFileSync("gh", args, {
|
|
6
|
+
cwd: repoPath,
|
|
7
|
+
encoding: "utf8",
|
|
8
|
+
stdio: ["ignore", "pipe", "pipe"],
|
|
9
|
+
});
|
|
10
|
+
}
|
|
11
|
+
function summarize(text) {
|
|
12
|
+
const compact = text.replace(/\s+/g, " ").trim();
|
|
13
|
+
if (!compact) {
|
|
14
|
+
return "No summary available.";
|
|
15
|
+
}
|
|
16
|
+
if (compact.length <= 280) {
|
|
17
|
+
return compact;
|
|
18
|
+
}
|
|
19
|
+
return `${compact.slice(0, 277)}...`;
|
|
20
|
+
}
|
|
21
|
+
function classifySeverity(text) {
|
|
22
|
+
if (/\b(block|blocking|must|required|cannot|can't|broken|fail)\b/i.test(text)) {
|
|
23
|
+
return "blocker";
|
|
24
|
+
}
|
|
25
|
+
if (/\b(should|consider|follow\s*up|todo|risk|later)\b/i.test(text)) {
|
|
26
|
+
return "warning";
|
|
27
|
+
}
|
|
28
|
+
return "info";
|
|
29
|
+
}
|
|
30
|
+
function isDecisionSignal(text) {
|
|
31
|
+
return /\b(decision|decide|decided|agreed|resolved|approved|final|ship|merged?)\b/i.test(text);
|
|
32
|
+
}
|
|
33
|
+
function parseComments(prNumber, comments) {
|
|
34
|
+
if (!Array.isArray(comments)) {
|
|
35
|
+
return [];
|
|
36
|
+
}
|
|
37
|
+
return comments.map((comment, index) => ({
|
|
38
|
+
id: String(comment.id ?? `${prNumber}-comment-${index + 1}`),
|
|
39
|
+
prNumber,
|
|
40
|
+
author: comment.author?.login ?? "unknown",
|
|
41
|
+
body: comment.body?.trim() ?? "",
|
|
42
|
+
createdAt: comment.createdAt ?? new Date(0).toISOString(),
|
|
43
|
+
updatedAt: comment.updatedAt ?? comment.createdAt ?? new Date(0).toISOString(),
|
|
44
|
+
url: comment.url ?? "",
|
|
45
|
+
}));
|
|
46
|
+
}
|
|
47
|
+
function parseReviews(prNumber, reviews) {
|
|
48
|
+
if (!Array.isArray(reviews)) {
|
|
49
|
+
return [];
|
|
50
|
+
}
|
|
51
|
+
return reviews.map((review, index) => ({
|
|
52
|
+
id: String(review.id ?? `${prNumber}-review-${index + 1}`),
|
|
53
|
+
prNumber,
|
|
54
|
+
author: review.author?.login ?? "unknown",
|
|
55
|
+
state: review.state ?? "COMMENTED",
|
|
56
|
+
body: review.body?.trim() ?? "",
|
|
57
|
+
submittedAt: review.submittedAt ?? new Date(0).toISOString(),
|
|
58
|
+
}));
|
|
59
|
+
}
|
|
60
|
+
function createDecisionRecords(options) {
|
|
61
|
+
const decisions = [];
|
|
62
|
+
if (options.pr.body.trim()) {
|
|
63
|
+
decisions.push({
|
|
64
|
+
id: `pr-${options.pr.number}-description`,
|
|
65
|
+
prNumber: options.pr.number,
|
|
66
|
+
source: "description",
|
|
67
|
+
author: options.pr.author,
|
|
68
|
+
summary: summarize(options.pr.body),
|
|
69
|
+
severity: classifySeverity(options.pr.body),
|
|
70
|
+
createdAt: options.pr.updatedAt,
|
|
71
|
+
});
|
|
72
|
+
}
|
|
73
|
+
for (const review of options.reviews) {
|
|
74
|
+
if (!review.body && review.state === "COMMENTED") {
|
|
75
|
+
continue;
|
|
76
|
+
}
|
|
77
|
+
if (review.state === "CHANGES_REQUESTED" ||
|
|
78
|
+
review.state === "APPROVED" ||
|
|
79
|
+
isDecisionSignal(review.body)) {
|
|
80
|
+
const text = [review.state, review.body].filter(Boolean).join(" - ");
|
|
81
|
+
decisions.push({
|
|
82
|
+
id: `pr-${options.pr.number}-review-${review.id}`,
|
|
83
|
+
prNumber: options.pr.number,
|
|
84
|
+
source: "review",
|
|
85
|
+
author: review.author,
|
|
86
|
+
summary: summarize(text),
|
|
87
|
+
severity: review.state === "CHANGES_REQUESTED"
|
|
88
|
+
? "blocker"
|
|
89
|
+
: classifySeverity(text),
|
|
90
|
+
createdAt: review.submittedAt,
|
|
91
|
+
});
|
|
92
|
+
}
|
|
93
|
+
}
|
|
94
|
+
for (const comment of options.comments) {
|
|
95
|
+
if (!comment.body || !isDecisionSignal(comment.body)) {
|
|
96
|
+
continue;
|
|
97
|
+
}
|
|
98
|
+
decisions.push({
|
|
99
|
+
id: `pr-${options.pr.number}-comment-${comment.id}`,
|
|
100
|
+
prNumber: options.pr.number,
|
|
101
|
+
source: "comment",
|
|
102
|
+
author: comment.author,
|
|
103
|
+
summary: summarize(comment.body),
|
|
104
|
+
severity: classifySeverity(comment.body),
|
|
105
|
+
createdAt: comment.updatedAt,
|
|
106
|
+
});
|
|
107
|
+
}
|
|
108
|
+
return decisions;
|
|
109
|
+
}
|
|
110
|
+
function parsePullRequest(repoOwner, repoName, raw) {
|
|
111
|
+
const number = Number(raw.number ?? 0);
|
|
112
|
+
if (!Number.isFinite(number) || number <= 0) {
|
|
113
|
+
throw new Error("Invalid pull request number returned by gh");
|
|
114
|
+
}
|
|
115
|
+
const pr = {
|
|
116
|
+
repoOwner,
|
|
117
|
+
repoName,
|
|
118
|
+
number,
|
|
119
|
+
title: raw.title?.trim() ?? "",
|
|
120
|
+
body: raw.body?.trim() ?? "",
|
|
121
|
+
author: raw.author?.login ?? "unknown",
|
|
122
|
+
state: raw.state ?? "UNKNOWN",
|
|
123
|
+
createdAt: raw.createdAt ?? new Date(0).toISOString(),
|
|
124
|
+
updatedAt: raw.updatedAt ?? raw.createdAt ?? new Date(0).toISOString(),
|
|
125
|
+
mergedAt: raw.mergedAt ?? null,
|
|
126
|
+
url: raw.url ?? "",
|
|
127
|
+
};
|
|
128
|
+
const comments = parseComments(number, raw.comments);
|
|
129
|
+
const reviews = parseReviews(number, raw.reviews);
|
|
130
|
+
const decisions = createDecisionRecords({ pr, comments, reviews });
|
|
131
|
+
return { pr, comments, reviews, decisions };
|
|
132
|
+
}
|
|
133
|
+
function listRecentPullRequestNumbers(repoPath, repoOwner, repoName, limit) {
|
|
134
|
+
const output = runGh(repoPath, [
|
|
135
|
+
"pr",
|
|
136
|
+
"list",
|
|
137
|
+
"-R",
|
|
138
|
+
`${repoOwner}/${repoName}`,
|
|
139
|
+
"--state",
|
|
140
|
+
"merged",
|
|
141
|
+
"--limit",
|
|
142
|
+
String(limit),
|
|
143
|
+
"--json",
|
|
144
|
+
"number",
|
|
145
|
+
]);
|
|
146
|
+
const rows = JSON.parse(output);
|
|
147
|
+
return rows
|
|
148
|
+
.map((row) => Number(row.number ?? 0))
|
|
149
|
+
.filter((value) => Number.isFinite(value) && value > 0);
|
|
150
|
+
}
|
|
151
|
+
function fetchPullRequest(repoPath, repoOwner, repoName, prNumber) {
|
|
152
|
+
const output = runGh(repoPath, [
|
|
153
|
+
"pr",
|
|
154
|
+
"view",
|
|
155
|
+
String(prNumber),
|
|
156
|
+
"-R",
|
|
157
|
+
`${repoOwner}/${repoName}`,
|
|
158
|
+
"--json",
|
|
159
|
+
"number,title,body,author,state,createdAt,updatedAt,mergedAt,url,comments,reviews",
|
|
160
|
+
]);
|
|
161
|
+
return JSON.parse(output);
|
|
162
|
+
}
|
|
163
|
+
export async function syncPullRequestContext(options) {
|
|
164
|
+
const repoPath = path.resolve(options.repoPath);
|
|
165
|
+
const dbPath = path.resolve(options.dbPath);
|
|
166
|
+
const limit = Number.isFinite(options.limit) ? Number(options.limit) : 20;
|
|
167
|
+
const prNumbers = options.prNumbers && options.prNumbers.length > 0
|
|
168
|
+
? options.prNumbers
|
|
169
|
+
: listRecentPullRequestNumbers(repoPath, options.repoOwner, options.repoName, limit);
|
|
170
|
+
const db = openDatabase(dbPath);
|
|
171
|
+
let syncedPrs = 0;
|
|
172
|
+
let syncedComments = 0;
|
|
173
|
+
let syncedReviews = 0;
|
|
174
|
+
let promotedDecisions = 0;
|
|
175
|
+
const tx = db.transaction(() => {
|
|
176
|
+
for (const prNumber of prNumbers) {
|
|
177
|
+
const raw = fetchPullRequest(repoPath, options.repoOwner, options.repoName, prNumber);
|
|
178
|
+
const parsed = parsePullRequest(options.repoOwner, options.repoName, raw);
|
|
179
|
+
upsertPullRequest(db, parsed.pr);
|
|
180
|
+
replacePullRequestComments(db, options.repoOwner, options.repoName, parsed.pr.number, parsed.comments);
|
|
181
|
+
replacePullRequestReviews(db, options.repoOwner, options.repoName, parsed.pr.number, parsed.reviews);
|
|
182
|
+
replacePullRequestDecisions(db, options.repoOwner, options.repoName, parsed.pr.number, parsed.decisions);
|
|
183
|
+
const scopeDomain = (options.domain ?? options.repoName).trim();
|
|
184
|
+
const scopeFeature = (options.feature ?? `pr-${parsed.pr.number}`).trim() ||
|
|
185
|
+
`pr-${parsed.pr.number}`;
|
|
186
|
+
const scopeBranch = (options.branch ?? "main").trim() || "main";
|
|
187
|
+
const taskType = (options.taskType ?? "planning").trim() || "planning";
|
|
188
|
+
upsertContextFact(db, {
|
|
189
|
+
id: `pr:${options.repoOwner}/${options.repoName}#${parsed.pr.number}:description`,
|
|
190
|
+
sourceType: "pr_description",
|
|
191
|
+
sourceRef: `${options.repoOwner}/${options.repoName}#${parsed.pr.number}`,
|
|
192
|
+
domain: scopeDomain,
|
|
193
|
+
feature: scopeFeature,
|
|
194
|
+
branch: scopeBranch,
|
|
195
|
+
taskType,
|
|
196
|
+
title: parsed.pr.title,
|
|
197
|
+
content: parsed.pr.body,
|
|
198
|
+
priority: 0.85,
|
|
199
|
+
confidence: 0.9,
|
|
200
|
+
status: "promoted",
|
|
201
|
+
createdAt: parsed.pr.createdAt,
|
|
202
|
+
updatedAt: parsed.pr.updatedAt,
|
|
203
|
+
});
|
|
204
|
+
for (const decision of parsed.decisions) {
|
|
205
|
+
upsertContextFact(db, {
|
|
206
|
+
id: `decision:${options.repoOwner}/${options.repoName}#${parsed.pr.number}:${decision.id}`,
|
|
207
|
+
sourceType: `pr_${decision.source}`,
|
|
208
|
+
sourceRef: `${options.repoOwner}/${options.repoName}#${parsed.pr.number}`,
|
|
209
|
+
domain: scopeDomain,
|
|
210
|
+
feature: scopeFeature,
|
|
211
|
+
branch: scopeBranch,
|
|
212
|
+
taskType,
|
|
213
|
+
title: `Decision ${parsed.pr.number} (${decision.source})`,
|
|
214
|
+
content: decision.summary,
|
|
215
|
+
priority: decision.severity === "blocker" ? 1 : 0.75,
|
|
216
|
+
confidence: 0.8,
|
|
217
|
+
status: decision.source === "description" ? "promoted" : "draft",
|
|
218
|
+
createdAt: decision.createdAt,
|
|
219
|
+
updatedAt: decision.createdAt,
|
|
220
|
+
});
|
|
221
|
+
}
|
|
222
|
+
syncedPrs += 1;
|
|
223
|
+
syncedComments += parsed.comments.length;
|
|
224
|
+
syncedReviews += parsed.reviews.length;
|
|
225
|
+
promotedDecisions += parsed.decisions.length;
|
|
226
|
+
}
|
|
227
|
+
touchPullRequestSyncState(db, options.repoOwner, options.repoName);
|
|
228
|
+
});
|
|
229
|
+
tx();
|
|
230
|
+
db.close();
|
|
231
|
+
return {
|
|
232
|
+
syncedPrs,
|
|
233
|
+
syncedComments,
|
|
234
|
+
syncedReviews,
|
|
235
|
+
promotedDecisions,
|
|
236
|
+
repoOwner: options.repoOwner,
|
|
237
|
+
repoName: options.repoName,
|
|
238
|
+
syncedAt: new Date().toISOString(),
|
|
239
|
+
};
|
|
240
|
+
}
|
package/dist/types.d.ts
CHANGED
|
@@ -24,3 +24,96 @@ export type IndexSummary = {
|
|
|
24
24
|
indexedChunks: number;
|
|
25
25
|
skippedChunks: number;
|
|
26
26
|
};
|
|
27
|
+
export type PullRequestRecord = {
|
|
28
|
+
repoOwner: string;
|
|
29
|
+
repoName: string;
|
|
30
|
+
number: number;
|
|
31
|
+
title: string;
|
|
32
|
+
body: string;
|
|
33
|
+
author: string;
|
|
34
|
+
state: string;
|
|
35
|
+
createdAt: string;
|
|
36
|
+
updatedAt: string;
|
|
37
|
+
mergedAt: string | null;
|
|
38
|
+
url: string;
|
|
39
|
+
};
|
|
40
|
+
export type PullRequestCommentRecord = {
|
|
41
|
+
id: string;
|
|
42
|
+
prNumber: number;
|
|
43
|
+
author: string;
|
|
44
|
+
body: string;
|
|
45
|
+
createdAt: string;
|
|
46
|
+
updatedAt: string;
|
|
47
|
+
url: string;
|
|
48
|
+
};
|
|
49
|
+
export type PullRequestReviewRecord = {
|
|
50
|
+
id: string;
|
|
51
|
+
prNumber: number;
|
|
52
|
+
author: string;
|
|
53
|
+
state: string;
|
|
54
|
+
body: string;
|
|
55
|
+
submittedAt: string;
|
|
56
|
+
};
|
|
57
|
+
export type PullRequestDecisionRecord = {
|
|
58
|
+
id: string;
|
|
59
|
+
prNumber: number;
|
|
60
|
+
source: "description" | "comment" | "review";
|
|
61
|
+
author: string;
|
|
62
|
+
summary: string;
|
|
63
|
+
severity: "info" | "warning" | "blocker";
|
|
64
|
+
createdAt: string;
|
|
65
|
+
};
|
|
66
|
+
export type PullRequestSyncSummary = {
|
|
67
|
+
syncedPrs: number;
|
|
68
|
+
syncedComments: number;
|
|
69
|
+
syncedReviews: number;
|
|
70
|
+
promotedDecisions: number;
|
|
71
|
+
repoOwner: string;
|
|
72
|
+
repoName: string;
|
|
73
|
+
syncedAt: string;
|
|
74
|
+
};
|
|
75
|
+
export type WorktreeRecord = {
|
|
76
|
+
path: string;
|
|
77
|
+
branch: string;
|
|
78
|
+
headSha: string;
|
|
79
|
+
isCurrent: boolean;
|
|
80
|
+
};
|
|
81
|
+
export type WorktreeSessionRecord = {
|
|
82
|
+
path: string;
|
|
83
|
+
branch: string;
|
|
84
|
+
lastSyncedAt: string;
|
|
85
|
+
baseBranch: string;
|
|
86
|
+
};
|
|
87
|
+
export type ContextFactStatus = "draft" | "promoted" | "archived";
|
|
88
|
+
export type ContextFactRecord = {
|
|
89
|
+
id: string;
|
|
90
|
+
sourceType: string;
|
|
91
|
+
sourceRef: string;
|
|
92
|
+
domain: string;
|
|
93
|
+
feature: string;
|
|
94
|
+
branch: string;
|
|
95
|
+
taskType: string;
|
|
96
|
+
title: string;
|
|
97
|
+
content: string;
|
|
98
|
+
priority: number;
|
|
99
|
+
confidence: number;
|
|
100
|
+
status: ContextFactStatus;
|
|
101
|
+
createdAt: string;
|
|
102
|
+
updatedAt: string;
|
|
103
|
+
};
|
|
104
|
+
export type ContextPackRecord = {
|
|
105
|
+
id: string;
|
|
106
|
+
sourceType: string;
|
|
107
|
+
sourceRef: string;
|
|
108
|
+
title: string;
|
|
109
|
+
content: string;
|
|
110
|
+
domain: string;
|
|
111
|
+
feature: string;
|
|
112
|
+
branch: string;
|
|
113
|
+
taskType: string;
|
|
114
|
+
priority: number;
|
|
115
|
+
confidence: number;
|
|
116
|
+
score: number;
|
|
117
|
+
status: ContextFactStatus;
|
|
118
|
+
updatedAt: string;
|
|
119
|
+
};
|
package/package.json
CHANGED