@jussmor/commit-memory-mcp 0.3.1 → 0.3.3
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 +11 -0
- package/dist/search/query.js +12 -4
- package/dist/search/rerank.d.ts +12 -0
- package/dist/search/rerank.js +72 -0
- package/package.json +1 -1
package/README.md
CHANGED
|
@@ -47,3 +47,14 @@ For MCP Registry publication, keep `package.json` `mcpName` and `server.json` `n
|
|
|
47
47
|
- `OLLAMA_EMBED_MODEL` local embedding model name
|
|
48
48
|
|
|
49
49
|
If `OLLAMA_EMBED_MODEL` is not set, the package uses deterministic local fallback embeddings.
|
|
50
|
+
|
|
51
|
+
### Copilot LLM reranking (optional)
|
|
52
|
+
|
|
53
|
+
Set `COPILOT_TOKEN` to a GitHub token with Copilot access to enable LLM-based reranking.
|
|
54
|
+
After initial vector/keyword retrieval, results are sent to Copilot for semantic scoring and re-sorted.
|
|
55
|
+
|
|
56
|
+
- `COPILOT_TOKEN` GitHub PAT or token with Copilot access (enables reranking)
|
|
57
|
+
- `COPILOT_MODEL` model slug (default: `gpt-4o-mini`, supports `claude-sonnet-4-5`, `gpt-4o`, etc.)
|
|
58
|
+
- `COPILOT_BASE_URL` API base URL (default: `https://api.githubcopilot.com`)
|
|
59
|
+
|
|
60
|
+
Reranking works alongside or instead of Ollama — no embedding model required.
|
package/dist/search/query.js
CHANGED
|
@@ -1,4 +1,5 @@
|
|
|
1
1
|
import { embedText } from "./embeddings.js";
|
|
2
|
+
import { copilotRerankEnabled, rerankWithCopilot } from "./rerank.js";
|
|
2
3
|
function scoreWithBoost(base, row, activeFile) {
|
|
3
4
|
let score = base;
|
|
4
5
|
if (activeFile && row.file_path === activeFile) {
|
|
@@ -16,6 +17,9 @@ function createPreview(hunkText) {
|
|
|
16
17
|
return hunkText.split("\n").slice(0, 6).join("\n");
|
|
17
18
|
}
|
|
18
19
|
export async function searchRelatedCommits(db, query, limit, activeFile) {
|
|
20
|
+
// Fetch extra candidates when Copilot reranking is enabled so the LLM has
|
|
21
|
+
// more to work with before we trim to the requested limit.
|
|
22
|
+
const fetchLimit = copilotRerankEnabled() ? limit * 2 : limit;
|
|
19
23
|
const embedding = await embedText(query);
|
|
20
24
|
const embeddingJson = JSON.stringify(embedding);
|
|
21
25
|
try {
|
|
@@ -36,8 +40,8 @@ export async function searchRelatedCommits(db, query, limit, activeFile) {
|
|
|
36
40
|
JOIN commits cm ON cm.sha = c.sha
|
|
37
41
|
WHERE v.embedding MATCH ? AND k = ?
|
|
38
42
|
`)
|
|
39
|
-
.all(embeddingJson,
|
|
40
|
-
|
|
43
|
+
.all(embeddingJson, fetchLimit);
|
|
44
|
+
const candidates = rows.map((row) => {
|
|
41
45
|
const base = 1 / (1 + Math.max(0, row.distance));
|
|
42
46
|
const score = scoreWithBoost(base, row, activeFile);
|
|
43
47
|
return {
|
|
@@ -51,6 +55,8 @@ export async function searchRelatedCommits(db, query, limit, activeFile) {
|
|
|
51
55
|
preview: createPreview(row.hunk_text),
|
|
52
56
|
};
|
|
53
57
|
});
|
|
58
|
+
const reranked = await rerankWithCopilot(query, candidates);
|
|
59
|
+
return reranked.slice(0, limit);
|
|
54
60
|
}
|
|
55
61
|
catch {
|
|
56
62
|
const rows = db
|
|
@@ -69,8 +75,8 @@ export async function searchRelatedCommits(db, query, limit, activeFile) {
|
|
|
69
75
|
ORDER BY cm.date DESC
|
|
70
76
|
LIMIT ?
|
|
71
77
|
`)
|
|
72
|
-
.all(`%${query}%`,
|
|
73
|
-
|
|
78
|
+
.all(`%${query}%`, fetchLimit);
|
|
79
|
+
const keywordCandidates = rows.map((row, idx) => ({
|
|
74
80
|
chunkId: row.chunk_id,
|
|
75
81
|
sha: row.sha,
|
|
76
82
|
filePath: row.file_path,
|
|
@@ -80,6 +86,8 @@ export async function searchRelatedCommits(db, query, limit, activeFile) {
|
|
|
80
86
|
author: row.author,
|
|
81
87
|
preview: createPreview(row.hunk_text),
|
|
82
88
|
}));
|
|
89
|
+
const rerankedKeyword = await rerankWithCopilot(query, keywordCandidates);
|
|
90
|
+
return rerankedKeyword.slice(0, limit);
|
|
83
91
|
}
|
|
84
92
|
}
|
|
85
93
|
export function explainCommitMatch(db, chunkId) {
|
|
@@ -0,0 +1,12 @@
|
|
|
1
|
+
import type { SearchResult } from "../types.js";
|
|
2
|
+
/**
|
|
3
|
+
* Rerank search results using GitHub Copilot chat completions (Claude / GPT).
|
|
4
|
+
*
|
|
5
|
+
* Activated when COPILOT_TOKEN is set.
|
|
6
|
+
* Env vars:
|
|
7
|
+
* COPILOT_TOKEN – GitHub PAT or token with Copilot access (required)
|
|
8
|
+
* COPILOT_MODEL – model slug (default: "gpt-4o-mini")
|
|
9
|
+
* COPILOT_BASE_URL – API base URL (default: https://api.githubcopilot.com)
|
|
10
|
+
*/
|
|
11
|
+
export declare function rerankWithCopilot(query: string, results: SearchResult[]): Promise<SearchResult[]>;
|
|
12
|
+
export declare function copilotRerankEnabled(): boolean;
|
|
@@ -0,0 +1,72 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Rerank search results using GitHub Copilot chat completions (Claude / GPT).
|
|
3
|
+
*
|
|
4
|
+
* Activated when COPILOT_TOKEN is set.
|
|
5
|
+
* Env vars:
|
|
6
|
+
* COPILOT_TOKEN – GitHub PAT or token with Copilot access (required)
|
|
7
|
+
* COPILOT_MODEL – model slug (default: "gpt-4o-mini")
|
|
8
|
+
* COPILOT_BASE_URL – API base URL (default: https://api.githubcopilot.com)
|
|
9
|
+
*/
|
|
10
|
+
export async function rerankWithCopilot(query, results) {
|
|
11
|
+
const token = process.env.COPILOT_TOKEN;
|
|
12
|
+
if (!token || results.length === 0) {
|
|
13
|
+
return results;
|
|
14
|
+
}
|
|
15
|
+
const baseUrl = process.env.COPILOT_BASE_URL ?? "https://api.githubcopilot.com";
|
|
16
|
+
const model = process.env.COPILOT_MODEL ?? "gpt-4o-mini";
|
|
17
|
+
const commitList = results
|
|
18
|
+
.map((r, i) => `${i + 1}. [${r.date}] ${r.author} — ${r.subject}\n File: ${r.filePath}\n ${r.preview.split("\n").slice(0, 3).join(" | ")}`)
|
|
19
|
+
.join("\n\n");
|
|
20
|
+
const prompt = `You are a code search assistant. Rate each commit excerpt's relevance to the query on a scale from 0.0 to 1.0.
|
|
21
|
+
|
|
22
|
+
Query: "${query}"
|
|
23
|
+
|
|
24
|
+
Commits:
|
|
25
|
+
${commitList}
|
|
26
|
+
|
|
27
|
+
Respond with ONLY a JSON array of numbers, one per commit, in the same order. Example: [0.9, 0.2, 0.7]`;
|
|
28
|
+
const messages = [{ role: "user", content: prompt }];
|
|
29
|
+
const response = await fetch(`${baseUrl}/chat/completions`, {
|
|
30
|
+
method: "POST",
|
|
31
|
+
headers: {
|
|
32
|
+
"Content-Type": "application/json",
|
|
33
|
+
Authorization: `Bearer ${token}`,
|
|
34
|
+
"Copilot-Integration-Id": "commit-memory-mcp",
|
|
35
|
+
},
|
|
36
|
+
body: JSON.stringify({
|
|
37
|
+
model,
|
|
38
|
+
messages,
|
|
39
|
+
temperature: 0,
|
|
40
|
+
max_tokens: 256,
|
|
41
|
+
}),
|
|
42
|
+
});
|
|
43
|
+
if (!response.ok) {
|
|
44
|
+
// Fall back to original order if the API call fails
|
|
45
|
+
console.error(`[rerank] Copilot API error ${response.status}, using original order`);
|
|
46
|
+
return results;
|
|
47
|
+
}
|
|
48
|
+
const data = (await response.json());
|
|
49
|
+
const content = data.choices?.[0]?.message?.content ?? "";
|
|
50
|
+
// Extract JSON array from the response (tolerates markdown fences)
|
|
51
|
+
const match = content.match(/\[[\d.,\s]+\]/);
|
|
52
|
+
if (!match) {
|
|
53
|
+
console.error("[rerank] Could not parse scores from Copilot response");
|
|
54
|
+
return results;
|
|
55
|
+
}
|
|
56
|
+
let scores;
|
|
57
|
+
try {
|
|
58
|
+
scores = JSON.parse(match[0]);
|
|
59
|
+
}
|
|
60
|
+
catch {
|
|
61
|
+
return results;
|
|
62
|
+
}
|
|
63
|
+
return results
|
|
64
|
+
.map((result, i) => ({
|
|
65
|
+
...result,
|
|
66
|
+
score: typeof scores[i] === "number" ? scores[i] : result.score,
|
|
67
|
+
}))
|
|
68
|
+
.sort((a, b) => b.score - a.score);
|
|
69
|
+
}
|
|
70
|
+
export function copilotRerankEnabled() {
|
|
71
|
+
return Boolean(process.env.COPILOT_TOKEN);
|
|
72
|
+
}
|
package/package.json
CHANGED