@jussmor/commit-memory-mcp 0.4.0 → 0.4.1
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/dist/git/insights.d.ts +22 -0
- package/dist/git/insights.js +79 -0
- package/dist/mcp/server.js +355 -2
- package/dist/search/embeddings.d.ts +1 -0
- package/dist/search/embeddings.js +24 -0
- package/package.json +1 -1
package/dist/git/insights.d.ts
CHANGED
|
@@ -60,6 +60,28 @@ export declare function mainBranchOvernightBrief(options: {
|
|
|
60
60
|
subject: string;
|
|
61
61
|
}>;
|
|
62
62
|
};
|
|
63
|
+
export declare function extractFeatureBranchCommits(options: {
|
|
64
|
+
repoPath: string;
|
|
65
|
+
featureBranch: string;
|
|
66
|
+
baseBranch: string;
|
|
67
|
+
limit: number;
|
|
68
|
+
}): {
|
|
69
|
+
featureName: string;
|
|
70
|
+
featureBranch: string;
|
|
71
|
+
baseBranch: string;
|
|
72
|
+
commits: Array<{
|
|
73
|
+
sha: string;
|
|
74
|
+
author: string;
|
|
75
|
+
date: string;
|
|
76
|
+
subject: string;
|
|
77
|
+
files: string[];
|
|
78
|
+
}>;
|
|
79
|
+
topFiles: Array<{
|
|
80
|
+
filePath: string;
|
|
81
|
+
touchCount: number;
|
|
82
|
+
}>;
|
|
83
|
+
affectedModules: string[];
|
|
84
|
+
};
|
|
63
85
|
export declare function resumeFeatureSessionBrief(options: {
|
|
64
86
|
worktreePath: string;
|
|
65
87
|
baseBranch: string;
|
package/dist/git/insights.js
CHANGED
|
@@ -154,6 +154,85 @@ export function mainBranchOvernightBrief(options) {
|
|
|
154
154
|
commits,
|
|
155
155
|
};
|
|
156
156
|
}
|
|
157
|
+
export function extractFeatureBranchCommits(options) {
|
|
158
|
+
const repoPath = path.resolve(options.repoPath);
|
|
159
|
+
const featureName = options.featureBranch
|
|
160
|
+
.replace(/^feature\//, "")
|
|
161
|
+
.replace(/[^a-zA-Z0-9-_]/g, "-");
|
|
162
|
+
const limit = Math.max(1, options.limit);
|
|
163
|
+
let commitsRaw;
|
|
164
|
+
try {
|
|
165
|
+
commitsRaw = runGit(repoPath, [
|
|
166
|
+
"log",
|
|
167
|
+
`--format=%H%x1f%an%x1f%aI%x1f%s`,
|
|
168
|
+
`-n${limit}`,
|
|
169
|
+
`${options.baseBranch}..${options.featureBranch}`,
|
|
170
|
+
]).trim();
|
|
171
|
+
}
|
|
172
|
+
catch {
|
|
173
|
+
return {
|
|
174
|
+
featureName,
|
|
175
|
+
featureBranch: options.featureBranch,
|
|
176
|
+
baseBranch: options.baseBranch,
|
|
177
|
+
commits: [],
|
|
178
|
+
topFiles: [],
|
|
179
|
+
affectedModules: [],
|
|
180
|
+
};
|
|
181
|
+
}
|
|
182
|
+
if (!commitsRaw) {
|
|
183
|
+
return {
|
|
184
|
+
featureName,
|
|
185
|
+
featureBranch: options.featureBranch,
|
|
186
|
+
baseBranch: options.baseBranch,
|
|
187
|
+
commits: [],
|
|
188
|
+
topFiles: [],
|
|
189
|
+
affectedModules: [],
|
|
190
|
+
};
|
|
191
|
+
}
|
|
192
|
+
const fileCounts = new Map();
|
|
193
|
+
const commits = commitsRaw.split("\n").map((line) => {
|
|
194
|
+
const [sha = "", author = "", date = "", subject = ""] = line.split("\x1f");
|
|
195
|
+
let files = [];
|
|
196
|
+
try {
|
|
197
|
+
const filesRaw = runGit(repoPath, [
|
|
198
|
+
"show",
|
|
199
|
+
"--name-only",
|
|
200
|
+
"--pretty=format:",
|
|
201
|
+
sha,
|
|
202
|
+
]).trim();
|
|
203
|
+
files = filesRaw
|
|
204
|
+
.split("\n")
|
|
205
|
+
.map((f) => f.trim())
|
|
206
|
+
.filter(Boolean);
|
|
207
|
+
}
|
|
208
|
+
catch {
|
|
209
|
+
// ignore per-commit failures
|
|
210
|
+
}
|
|
211
|
+
for (const f of files) {
|
|
212
|
+
fileCounts.set(f, (fileCounts.get(f) ?? 0) + 1);
|
|
213
|
+
}
|
|
214
|
+
return { sha, author, date, subject, files };
|
|
215
|
+
});
|
|
216
|
+
const topFiles = Array.from(fileCounts.entries())
|
|
217
|
+
.map(([filePath, touchCount]) => ({ filePath, touchCount }))
|
|
218
|
+
.sort((a, b) => b.touchCount - a.touchCount)
|
|
219
|
+
.slice(0, 15);
|
|
220
|
+
const moduleSet = new Set();
|
|
221
|
+
for (const { filePath } of topFiles) {
|
|
222
|
+
const parts = filePath.split("/");
|
|
223
|
+
if (parts.length >= 2) {
|
|
224
|
+
moduleSet.add(parts.slice(0, 2).join("/"));
|
|
225
|
+
}
|
|
226
|
+
}
|
|
227
|
+
return {
|
|
228
|
+
featureName,
|
|
229
|
+
featureBranch: options.featureBranch,
|
|
230
|
+
baseBranch: options.baseBranch,
|
|
231
|
+
commits,
|
|
232
|
+
topFiles,
|
|
233
|
+
affectedModules: Array.from(moduleSet).slice(0, 10),
|
|
234
|
+
};
|
|
235
|
+
}
|
|
157
236
|
export function resumeFeatureSessionBrief(options) {
|
|
158
237
|
const worktreePath = path.resolve(options.worktreePath);
|
|
159
238
|
const branch = runGit(worktreePath, [
|
package/dist/mcp/server.js
CHANGED
|
@@ -6,10 +6,14 @@ import { execFileSync } from "node:child_process";
|
|
|
6
6
|
import fs from "node:fs";
|
|
7
7
|
import path from "node:path";
|
|
8
8
|
import { pathToFileURL } from "node:url";
|
|
9
|
-
import { archiveFeatureContext, buildContextPack, openDatabase, promoteContextFacts, upsertWorktreeSession, } from "../db/client.js";
|
|
10
|
-
import { commitDetails, explainPathActivity, latestCommitForFile, mainBranchOvernightBrief, resumeFeatureSessionBrief, whoChangedFile, } from "../git/insights.js";
|
|
9
|
+
import { archiveFeatureContext, buildContextPack, openDatabase, promoteContextFacts, upsertContextFact, upsertWorktreeSession, } from "../db/client.js";
|
|
10
|
+
import { commitDetails, explainPathActivity, extractFeatureBranchCommits, 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
|
+
import { callOllamaLlm } from "../search/embeddings.js";
|
|
14
|
+
function normalizeFeatureName(branch) {
|
|
15
|
+
return branch.replace(/^feature\//, "").replace(/[^a-zA-Z0-9-_]/g, "-");
|
|
16
|
+
}
|
|
13
17
|
function fetchRemote(repoPath) {
|
|
14
18
|
execFileSync("git", ["-C", repoPath, "fetch", "--all", "--prune"], {
|
|
15
19
|
stdio: ["ignore", "pipe", "pipe"],
|
|
@@ -287,6 +291,41 @@ export async function startMcpServer() {
|
|
|
287
291
|
required: [],
|
|
288
292
|
},
|
|
289
293
|
},
|
|
294
|
+
{
|
|
295
|
+
name: "learn_feature",
|
|
296
|
+
description: "Save feature knowledge to the RAG DB. If agentContent is provided (recommended), it is stored directly as the feature's understanding — the agent should investigate the source code itself and pass its findings here. If agentContent is omitted, falls back to git-commit-based inference.",
|
|
297
|
+
inputSchema: {
|
|
298
|
+
type: "object",
|
|
299
|
+
properties: {
|
|
300
|
+
featureBranch: {
|
|
301
|
+
type: "string",
|
|
302
|
+
description: "e.g. feature/messaging",
|
|
303
|
+
},
|
|
304
|
+
baseBranch: { type: "string" },
|
|
305
|
+
limit: { type: "number" },
|
|
306
|
+
agentContent: {
|
|
307
|
+
type: "string",
|
|
308
|
+
description: "Plain-text description of what the feature does, written by the agent after reading actual source files. When provided, this becomes the stored knowledge (confidence 0.95) and git metadata is appended as supporting context.",
|
|
309
|
+
},
|
|
310
|
+
},
|
|
311
|
+
required: ["featureBranch"],
|
|
312
|
+
},
|
|
313
|
+
},
|
|
314
|
+
{
|
|
315
|
+
name: "sync_feature_knowledge",
|
|
316
|
+
description: "Update the AI knowledge for a feature branch using new commits and PR decisions since the last sync. Bootstraps automatically if no prior knowledge exists.",
|
|
317
|
+
inputSchema: {
|
|
318
|
+
type: "object",
|
|
319
|
+
properties: {
|
|
320
|
+
featureBranch: { type: "string" },
|
|
321
|
+
baseBranch: { type: "string" },
|
|
322
|
+
owner: { type: "string" },
|
|
323
|
+
repo: { type: "string" },
|
|
324
|
+
limit: { type: "number" },
|
|
325
|
+
},
|
|
326
|
+
required: ["featureBranch"],
|
|
327
|
+
},
|
|
328
|
+
},
|
|
290
329
|
{
|
|
291
330
|
name: "pre_plan_sync_brief",
|
|
292
331
|
description: "Run sync + overnight + feature resume analysis before planning work.",
|
|
@@ -641,6 +680,320 @@ export async function startMcpServer() {
|
|
|
641
680
|
content: [{ type: "text", text: JSON.stringify(prePlan, null, 2) }],
|
|
642
681
|
};
|
|
643
682
|
}
|
|
683
|
+
if (request.params.name === "learn_feature") {
|
|
684
|
+
const featureBranch = String(request.params.arguments?.featureBranch ?? "").trim();
|
|
685
|
+
const baseBranch = String(request.params.arguments?.baseBranch ?? "").trim() || "main";
|
|
686
|
+
const limit = Number(request.params.arguments?.limit ?? 50);
|
|
687
|
+
const agentContent = String(request.params.arguments?.agentContent ?? "").trim() || null;
|
|
688
|
+
if (!featureBranch) {
|
|
689
|
+
return {
|
|
690
|
+
content: [{ type: "text", text: "featureBranch is required" }],
|
|
691
|
+
isError: true,
|
|
692
|
+
};
|
|
693
|
+
}
|
|
694
|
+
const featureName = normalizeFeatureName(featureBranch);
|
|
695
|
+
const data = extractFeatureBranchCommits({
|
|
696
|
+
repoPath,
|
|
697
|
+
featureBranch,
|
|
698
|
+
baseBranch,
|
|
699
|
+
limit: Number.isFinite(limit) && limit > 0 ? limit : 50,
|
|
700
|
+
});
|
|
701
|
+
const authors = [...new Set(data.commits.map((c) => c.author))].join(", ");
|
|
702
|
+
const now = new Date().toISOString();
|
|
703
|
+
let content;
|
|
704
|
+
let confidence;
|
|
705
|
+
let aiGenerated;
|
|
706
|
+
if (agentContent) {
|
|
707
|
+
// Agent investigated the source code directly — highest confidence.
|
|
708
|
+
// Append git metadata as supporting context.
|
|
709
|
+
const gitMeta = [
|
|
710
|
+
``,
|
|
711
|
+
`--- Git metadata (${data.commits.length} commits, authors: ${authors || "unknown"}) ---`,
|
|
712
|
+
`Top files: ${data.topFiles
|
|
713
|
+
.slice(0, 5)
|
|
714
|
+
.map((f) => f.filePath)
|
|
715
|
+
.join(", ") || "(none)"}`,
|
|
716
|
+
`Top modules: ${data.affectedModules.slice(0, 4).join(", ") || "(none)"}`,
|
|
717
|
+
].join("\n");
|
|
718
|
+
content = agentContent + gitMeta;
|
|
719
|
+
confidence = 0.95;
|
|
720
|
+
aiGenerated = true;
|
|
721
|
+
}
|
|
722
|
+
else {
|
|
723
|
+
// Fallback: infer from git commits + optional Ollama synthesis.
|
|
724
|
+
const fileList = data.topFiles
|
|
725
|
+
.map((f) => ` - ${f.filePath} (touched ${f.touchCount}x)`)
|
|
726
|
+
.join("\n");
|
|
727
|
+
const commitList = data.commits
|
|
728
|
+
.slice(0, 20)
|
|
729
|
+
.map((c) => ` - ${c.subject} (${c.author})`)
|
|
730
|
+
.join("\n");
|
|
731
|
+
const prompt = [
|
|
732
|
+
`You are analyzing a Git feature branch called "${featureBranch}".`,
|
|
733
|
+
``,
|
|
734
|
+
`Top changed files:`,
|
|
735
|
+
fileList || " (none)",
|
|
736
|
+
``,
|
|
737
|
+
`Commit history (most recent first):`,
|
|
738
|
+
commitList || " (none)",
|
|
739
|
+
``,
|
|
740
|
+
`Answer in 3-5 sentences:`,
|
|
741
|
+
`1. What does this feature do?`,
|
|
742
|
+
`2. What modules/areas of the codebase does it affect?`,
|
|
743
|
+
`3. What does it NOT do or what is explicitly out of scope?`,
|
|
744
|
+
].join("\n");
|
|
745
|
+
const llmSummary = await callOllamaLlm(prompt);
|
|
746
|
+
const fallbackSummary = [
|
|
747
|
+
`Feature "${featureName}" spans ${data.commits.length} commit(s) by ${authors || "(unknown)"}.`,
|
|
748
|
+
`Top modules: ${data.affectedModules.slice(0, 4).join(", ") || "(unknown)"}.`,
|
|
749
|
+
`Top files: ${data.topFiles
|
|
750
|
+
.slice(0, 3)
|
|
751
|
+
.map((f) => f.filePath)
|
|
752
|
+
.join(", ") || "(none)"}.`,
|
|
753
|
+
`Commit subjects: ${data.commits
|
|
754
|
+
.slice(0, 5)
|
|
755
|
+
.map((c) => c.subject)
|
|
756
|
+
.join("; ")}.`,
|
|
757
|
+
].join(" ");
|
|
758
|
+
content = llmSummary ?? fallbackSummary;
|
|
759
|
+
confidence = llmSummary ? 0.85 : 0.6;
|
|
760
|
+
aiGenerated = llmSummary !== null;
|
|
761
|
+
}
|
|
762
|
+
const db = openDatabase(dbPath);
|
|
763
|
+
try {
|
|
764
|
+
upsertContextFact(db, {
|
|
765
|
+
id: `feature-knowledge:${featureName}`,
|
|
766
|
+
sourceType: "feature-agent",
|
|
767
|
+
sourceRef: featureBranch,
|
|
768
|
+
domain: "",
|
|
769
|
+
feature: featureName,
|
|
770
|
+
branch: featureBranch,
|
|
771
|
+
taskType: "feature-knowledge",
|
|
772
|
+
title: `Feature knowledge: ${featureName}`,
|
|
773
|
+
content,
|
|
774
|
+
priority: 0.9,
|
|
775
|
+
confidence,
|
|
776
|
+
status: "promoted",
|
|
777
|
+
createdAt: now,
|
|
778
|
+
updatedAt: now,
|
|
779
|
+
});
|
|
780
|
+
}
|
|
781
|
+
finally {
|
|
782
|
+
db.close();
|
|
783
|
+
}
|
|
784
|
+
return {
|
|
785
|
+
content: [
|
|
786
|
+
{
|
|
787
|
+
type: "text",
|
|
788
|
+
text: JSON.stringify({
|
|
789
|
+
featureName,
|
|
790
|
+
learned: content,
|
|
791
|
+
filesAnalyzed: data.topFiles.length,
|
|
792
|
+
commitsAnalyzed: data.commits.length,
|
|
793
|
+
agentProvided: agentContent !== null,
|
|
794
|
+
aiGenerated,
|
|
795
|
+
confidence,
|
|
796
|
+
savedAt: now,
|
|
797
|
+
}, null, 2),
|
|
798
|
+
},
|
|
799
|
+
],
|
|
800
|
+
};
|
|
801
|
+
}
|
|
802
|
+
if (request.params.name === "sync_feature_knowledge") {
|
|
803
|
+
const featureBranch = String(request.params.arguments?.featureBranch ?? "").trim();
|
|
804
|
+
const baseBranch = String(request.params.arguments?.baseBranch ?? "").trim() || "main";
|
|
805
|
+
const owner = String(request.params.arguments?.owner ?? "").trim();
|
|
806
|
+
const repo = String(request.params.arguments?.repo ?? "").trim();
|
|
807
|
+
const limit = Number(request.params.arguments?.limit ?? 50);
|
|
808
|
+
if (!featureBranch) {
|
|
809
|
+
return {
|
|
810
|
+
content: [{ type: "text", text: "featureBranch is required" }],
|
|
811
|
+
isError: true,
|
|
812
|
+
};
|
|
813
|
+
}
|
|
814
|
+
const featureName = normalizeFeatureName(featureBranch);
|
|
815
|
+
const safeLimit = Number.isFinite(limit) && limit > 0 ? limit : 50;
|
|
816
|
+
// Load existing knowledge from DB
|
|
817
|
+
let existingKnowledge = null;
|
|
818
|
+
let existingUpdatedAt = null;
|
|
819
|
+
let existingCreatedAt = null;
|
|
820
|
+
const dbRead = openDatabase(dbPath);
|
|
821
|
+
try {
|
|
822
|
+
const existing = dbRead
|
|
823
|
+
.prepare("SELECT content, created_at, updated_at FROM context_facts WHERE id = ? LIMIT 1")
|
|
824
|
+
.get(`feature-knowledge:${featureName}`);
|
|
825
|
+
existingKnowledge = existing?.content ?? null;
|
|
826
|
+
existingUpdatedAt = existing?.updated_at ?? null;
|
|
827
|
+
existingCreatedAt = existing?.created_at ?? null;
|
|
828
|
+
}
|
|
829
|
+
finally {
|
|
830
|
+
dbRead.close();
|
|
831
|
+
}
|
|
832
|
+
// Bootstrap via learn_feature logic if no prior knowledge
|
|
833
|
+
if (!existingKnowledge) {
|
|
834
|
+
const bootData = extractFeatureBranchCommits({
|
|
835
|
+
repoPath,
|
|
836
|
+
featureBranch,
|
|
837
|
+
baseBranch,
|
|
838
|
+
limit: safeLimit,
|
|
839
|
+
});
|
|
840
|
+
const bootFileList = bootData.topFiles
|
|
841
|
+
.map((f) => ` - ${f.filePath} (touched ${f.touchCount}x)`)
|
|
842
|
+
.join("\n");
|
|
843
|
+
const bootCommitList = bootData.commits
|
|
844
|
+
.slice(0, 20)
|
|
845
|
+
.map((c) => ` - ${c.subject} (${c.author})`)
|
|
846
|
+
.join("\n");
|
|
847
|
+
const bootPrompt = [
|
|
848
|
+
`You are analyzing a Git feature branch called "${featureBranch}".`,
|
|
849
|
+
`Top changed files:\n${bootFileList || " (none)"}`,
|
|
850
|
+
`Commit history:\n${bootCommitList || " (none)"}`,
|
|
851
|
+
`Answer in 3-5 sentences: 1. What does this feature do? 2. What modules does it affect? 3. What is out of scope?`,
|
|
852
|
+
].join("\n\n");
|
|
853
|
+
const bootLlm = await callOllamaLlm(bootPrompt);
|
|
854
|
+
existingKnowledge =
|
|
855
|
+
bootLlm ??
|
|
856
|
+
`Feature "${featureName}" has ${bootData.commits.length} commit(s). Top modules: ${bootData.affectedModules.slice(0, 4).join(", ") || "(unknown)"}.`;
|
|
857
|
+
const now = new Date().toISOString();
|
|
858
|
+
const dbBoot = openDatabase(dbPath);
|
|
859
|
+
try {
|
|
860
|
+
upsertContextFact(dbBoot, {
|
|
861
|
+
id: `feature-knowledge:${featureName}`,
|
|
862
|
+
sourceType: "feature-agent",
|
|
863
|
+
sourceRef: featureBranch,
|
|
864
|
+
domain: "",
|
|
865
|
+
feature: featureName,
|
|
866
|
+
branch: featureBranch,
|
|
867
|
+
taskType: "feature-knowledge",
|
|
868
|
+
title: `Feature knowledge: ${featureName}`,
|
|
869
|
+
content: existingKnowledge,
|
|
870
|
+
priority: 0.9,
|
|
871
|
+
confidence: bootLlm ? 0.85 : 0.6,
|
|
872
|
+
status: "promoted",
|
|
873
|
+
createdAt: now,
|
|
874
|
+
updatedAt: now,
|
|
875
|
+
});
|
|
876
|
+
}
|
|
877
|
+
finally {
|
|
878
|
+
dbBoot.close();
|
|
879
|
+
}
|
|
880
|
+
existingUpdatedAt = now;
|
|
881
|
+
existingCreatedAt = now;
|
|
882
|
+
}
|
|
883
|
+
// Fetch remote and extract commits
|
|
884
|
+
fetchRemote(repoPath);
|
|
885
|
+
const data = extractFeatureBranchCommits({
|
|
886
|
+
repoPath,
|
|
887
|
+
featureBranch,
|
|
888
|
+
baseBranch,
|
|
889
|
+
limit: safeLimit,
|
|
890
|
+
});
|
|
891
|
+
// Only process commits newer than last sync
|
|
892
|
+
const newCommits = existingUpdatedAt
|
|
893
|
+
? data.commits.filter((c) => c.date > existingUpdatedAt)
|
|
894
|
+
: data.commits;
|
|
895
|
+
// Gather PR decisions for referenced PRs in new commits
|
|
896
|
+
const referencedPrNumbers = newCommits
|
|
897
|
+
.map((c) => detectReferencedPrNumber(c.subject))
|
|
898
|
+
.filter((n) => n !== null);
|
|
899
|
+
let prDecisionsSummary = "";
|
|
900
|
+
if (referencedPrNumbers.length > 0) {
|
|
901
|
+
const dbPr = openDatabase(dbPath);
|
|
902
|
+
try {
|
|
903
|
+
const parts = [];
|
|
904
|
+
for (const prNum of referencedPrNumbers.slice(0, 5)) {
|
|
905
|
+
const { pr, decisions } = loadPullRequestContext(dbPr, prNum, owner || undefined, repo || undefined);
|
|
906
|
+
if (pr) {
|
|
907
|
+
parts.push(`PR #${prNum} "${pr["title"]}": ${decisions
|
|
908
|
+
.slice(0, 3)
|
|
909
|
+
.map((d) => d["summary"])
|
|
910
|
+
.join("; ")}`);
|
|
911
|
+
}
|
|
912
|
+
}
|
|
913
|
+
prDecisionsSummary = parts.join("\n");
|
|
914
|
+
}
|
|
915
|
+
finally {
|
|
916
|
+
dbPr.close();
|
|
917
|
+
}
|
|
918
|
+
}
|
|
919
|
+
const newCommitList = newCommits
|
|
920
|
+
.slice(0, 20)
|
|
921
|
+
.map((c) => ` - ${c.subject} (${c.author})`)
|
|
922
|
+
.join("\n");
|
|
923
|
+
const updatePrompt = [
|
|
924
|
+
`You previously documented this feature:`,
|
|
925
|
+
`"${existingKnowledge}"`,
|
|
926
|
+
``,
|
|
927
|
+
`New commits since last sync:`,
|
|
928
|
+
newCommitList || " (no new commits)",
|
|
929
|
+
prDecisionsSummary ? `\nPR decisions:\n${prDecisionsSummary}` : "",
|
|
930
|
+
``,
|
|
931
|
+
`Write an updated 3-5 sentence understanding of the feature. If nothing changed, return the previous text unchanged.`,
|
|
932
|
+
].join("\n");
|
|
933
|
+
const updatedSummary = newCommits.length > 0
|
|
934
|
+
? ((await callOllamaLlm(updatePrompt)) ?? existingKnowledge)
|
|
935
|
+
: existingKnowledge;
|
|
936
|
+
const now = new Date().toISOString();
|
|
937
|
+
const dbWrite = openDatabase(dbPath);
|
|
938
|
+
try {
|
|
939
|
+
upsertContextFact(dbWrite, {
|
|
940
|
+
id: `feature-knowledge:${featureName}`,
|
|
941
|
+
sourceType: "feature-agent",
|
|
942
|
+
sourceRef: featureBranch,
|
|
943
|
+
domain: "",
|
|
944
|
+
feature: featureName,
|
|
945
|
+
branch: featureBranch,
|
|
946
|
+
taskType: "feature-knowledge",
|
|
947
|
+
title: `Feature knowledge: ${featureName}`,
|
|
948
|
+
content: updatedSummary,
|
|
949
|
+
priority: 0.9,
|
|
950
|
+
confidence: 0.85,
|
|
951
|
+
status: "promoted",
|
|
952
|
+
createdAt: existingCreatedAt ?? now,
|
|
953
|
+
updatedAt: now,
|
|
954
|
+
});
|
|
955
|
+
// Audit log row (status: draft — invisible to default build_context_pack)
|
|
956
|
+
if (newCommits.length > 0) {
|
|
957
|
+
const auditDate = now.split("T")[0] ?? now;
|
|
958
|
+
upsertContextFact(dbWrite, {
|
|
959
|
+
id: `feature-change-log:${featureName}:${auditDate}`,
|
|
960
|
+
sourceType: "feature-agent",
|
|
961
|
+
sourceRef: featureBranch,
|
|
962
|
+
domain: "",
|
|
963
|
+
feature: featureName,
|
|
964
|
+
branch: featureBranch,
|
|
965
|
+
taskType: "change-log",
|
|
966
|
+
title: `Change log: ${featureName} on ${auditDate}`,
|
|
967
|
+
content: `New commits: ${newCommits
|
|
968
|
+
.map((c) => c.subject)
|
|
969
|
+
.join("; ")}. ${prDecisionsSummary}`.trim(),
|
|
970
|
+
priority: 0.7,
|
|
971
|
+
confidence: 0.8,
|
|
972
|
+
status: "draft",
|
|
973
|
+
createdAt: now,
|
|
974
|
+
updatedAt: now,
|
|
975
|
+
});
|
|
976
|
+
}
|
|
977
|
+
}
|
|
978
|
+
finally {
|
|
979
|
+
dbWrite.close();
|
|
980
|
+
}
|
|
981
|
+
return {
|
|
982
|
+
content: [
|
|
983
|
+
{
|
|
984
|
+
type: "text",
|
|
985
|
+
text: JSON.stringify({
|
|
986
|
+
featureName,
|
|
987
|
+
previousKnowledge: existingKnowledge,
|
|
988
|
+
updatedKnowledge: updatedSummary,
|
|
989
|
+
newCommitsAnalyzed: newCommits.length,
|
|
990
|
+
totalCommitsInBranch: data.commits.length,
|
|
991
|
+
syncedAt: now,
|
|
992
|
+
}, null, 2),
|
|
993
|
+
},
|
|
994
|
+
],
|
|
995
|
+
};
|
|
996
|
+
}
|
|
644
997
|
return {
|
|
645
998
|
content: [{ type: "text", text: `Unknown tool: ${request.params.name}` }],
|
|
646
999
|
isError: true,
|
|
@@ -52,6 +52,30 @@ export async function embedText(text) {
|
|
|
52
52
|
}
|
|
53
53
|
return normalize(payload.embedding);
|
|
54
54
|
}
|
|
55
|
+
export async function callOllamaLlm(prompt) {
|
|
56
|
+
const model = process.env.OLLAMA_CHAT_MODEL;
|
|
57
|
+
if (!model) {
|
|
58
|
+
return null;
|
|
59
|
+
}
|
|
60
|
+
const baseUrl = process.env.OLLAMA_BASE_URL ?? "http://127.0.0.1:11434";
|
|
61
|
+
try {
|
|
62
|
+
const response = await fetch(`${baseUrl}/api/generate`, {
|
|
63
|
+
method: "POST",
|
|
64
|
+
headers: { "Content-Type": "application/json" },
|
|
65
|
+
body: JSON.stringify({ model, prompt, stream: false }),
|
|
66
|
+
});
|
|
67
|
+
if (!response.ok) {
|
|
68
|
+
return null;
|
|
69
|
+
}
|
|
70
|
+
const payload = (await response.json());
|
|
71
|
+
return typeof payload.response === "string" && payload.response.trim()
|
|
72
|
+
? payload.response.trim()
|
|
73
|
+
: null;
|
|
74
|
+
}
|
|
75
|
+
catch {
|
|
76
|
+
return null;
|
|
77
|
+
}
|
|
78
|
+
}
|
|
55
79
|
export function getExpectedDimension() {
|
|
56
80
|
return process.env.OLLAMA_EMBED_MODEL
|
|
57
81
|
? Number.parseInt(process.env.COMMIT_RAG_DIMENSION ?? "", 10) ||
|
package/package.json
CHANGED