@mhalder/qdrant-mcp-server 2.2.0 → 3.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/.github/workflows/ci.yml +4 -1
- package/CHANGELOG.md +22 -0
- package/README.md +78 -3
- package/build/code/indexer.d.ts +2 -1
- package/build/code/indexer.d.ts.map +1 -1
- package/build/code/indexer.js +37 -7
- package/build/code/indexer.js.map +1 -1
- package/build/git/chunker.d.ts +39 -0
- package/build/git/chunker.d.ts.map +1 -0
- package/build/git/chunker.js +210 -0
- package/build/git/chunker.js.map +1 -0
- package/build/git/chunker.test.d.ts +2 -0
- package/build/git/chunker.test.d.ts.map +1 -0
- package/build/git/chunker.test.js +230 -0
- package/build/git/chunker.test.js.map +1 -0
- package/build/git/config.d.ts +34 -0
- package/build/git/config.d.ts.map +1 -0
- package/build/git/config.js +163 -0
- package/build/git/config.js.map +1 -0
- package/build/git/extractor.d.ts +57 -0
- package/build/git/extractor.d.ts.map +1 -0
- package/build/git/extractor.integration.test.d.ts +6 -0
- package/build/git/extractor.integration.test.d.ts.map +1 -0
- package/build/git/extractor.integration.test.js +166 -0
- package/build/git/extractor.integration.test.js.map +1 -0
- package/build/git/extractor.js +231 -0
- package/build/git/extractor.js.map +1 -0
- package/build/git/extractor.test.d.ts +2 -0
- package/build/git/extractor.test.d.ts.map +1 -0
- package/build/git/extractor.test.js +267 -0
- package/build/git/extractor.test.js.map +1 -0
- package/build/git/index.d.ts +10 -0
- package/build/git/index.d.ts.map +1 -0
- package/build/git/index.js +11 -0
- package/build/git/index.js.map +1 -0
- package/build/git/indexer.d.ts +50 -0
- package/build/git/indexer.d.ts.map +1 -0
- package/build/git/indexer.js +588 -0
- package/build/git/indexer.js.map +1 -0
- package/build/git/indexer.test.d.ts +2 -0
- package/build/git/indexer.test.d.ts.map +1 -0
- package/build/git/indexer.test.js +867 -0
- package/build/git/indexer.test.js.map +1 -0
- package/build/git/sync/synchronizer.d.ts +43 -0
- package/build/git/sync/synchronizer.d.ts.map +1 -0
- package/build/git/sync/synchronizer.js +108 -0
- package/build/git/sync/synchronizer.js.map +1 -0
- package/build/git/sync/synchronizer.test.d.ts +2 -0
- package/build/git/sync/synchronizer.test.d.ts.map +1 -0
- package/build/git/sync/synchronizer.test.js +188 -0
- package/build/git/sync/synchronizer.test.js.map +1 -0
- package/build/git/types.d.ts +159 -0
- package/build/git/types.d.ts.map +1 -0
- package/build/git/types.js +5 -0
- package/build/git/types.js.map +1 -0
- package/build/index.js +18 -0
- package/build/index.js.map +1 -1
- package/build/tools/git-history.d.ts +10 -0
- package/build/tools/git-history.d.ts.map +1 -0
- package/build/tools/git-history.js +144 -0
- package/build/tools/git-history.js.map +1 -0
- package/build/tools/index.d.ts +2 -0
- package/build/tools/index.d.ts.map +1 -1
- package/build/tools/index.js +4 -0
- package/build/tools/index.js.map +1 -1
- package/build/tools/schemas.d.ts +24 -0
- package/build/tools/schemas.d.ts.map +1 -1
- package/build/tools/schemas.js +64 -0
- package/build/tools/schemas.js.map +1 -1
- package/package.json +2 -2
- package/src/code/indexer.ts +49 -7
- package/src/git/chunker.test.ts +284 -0
- package/src/git/chunker.ts +256 -0
- package/src/git/config.ts +173 -0
- package/src/git/extractor.integration.test.ts +221 -0
- package/src/git/extractor.test.ts +403 -0
- package/src/git/extractor.ts +284 -0
- package/src/git/index.ts +31 -0
- package/src/git/indexer.test.ts +1089 -0
- package/src/git/indexer.ts +745 -0
- package/src/git/sync/synchronizer.test.ts +250 -0
- package/src/git/sync/synchronizer.ts +122 -0
- package/src/git/types.ts +192 -0
- package/src/index.ts +42 -0
- package/src/tools/git-history.ts +208 -0
- package/src/tools/index.ts +7 -0
- package/src/tools/schemas.ts +75 -0
- package/vitest.config.ts +2 -0
|
@@ -0,0 +1,250 @@
|
|
|
1
|
+
import { describe, it, expect, vi, beforeEach, afterEach } from "vitest";
|
|
2
|
+
import { GitSynchronizer } from "./synchronizer.js";
|
|
3
|
+
|
|
4
|
+
// Mock fs module
|
|
5
|
+
vi.mock("node:fs", () => ({
|
|
6
|
+
promises: {
|
|
7
|
+
readFile: vi.fn(),
|
|
8
|
+
writeFile: vi.fn(),
|
|
9
|
+
rename: vi.fn(),
|
|
10
|
+
unlink: vi.fn(),
|
|
11
|
+
access: vi.fn(),
|
|
12
|
+
mkdir: vi.fn(),
|
|
13
|
+
},
|
|
14
|
+
}));
|
|
15
|
+
|
|
16
|
+
// Mock os module
|
|
17
|
+
vi.mock("node:os", () => ({
|
|
18
|
+
homedir: vi.fn(() => "/home/test"),
|
|
19
|
+
}));
|
|
20
|
+
|
|
21
|
+
import { promises as fs } from "node:fs";
|
|
22
|
+
|
|
23
|
+
describe("GitSynchronizer", () => {
|
|
24
|
+
let synchronizer: GitSynchronizer;
|
|
25
|
+
|
|
26
|
+
beforeEach(() => {
|
|
27
|
+
vi.clearAllMocks();
|
|
28
|
+
synchronizer = new GitSynchronizer("/test/repo", "git_abc12345");
|
|
29
|
+
});
|
|
30
|
+
|
|
31
|
+
afterEach(() => {
|
|
32
|
+
vi.restoreAllMocks();
|
|
33
|
+
});
|
|
34
|
+
|
|
35
|
+
describe("initialize", () => {
|
|
36
|
+
it("should return true when snapshot exists and matches repo", async () => {
|
|
37
|
+
const snapshot = {
|
|
38
|
+
repoPath: "/test/repo",
|
|
39
|
+
lastCommitHash: "abc123",
|
|
40
|
+
lastIndexedAt: Date.now(),
|
|
41
|
+
commitsIndexed: 100,
|
|
42
|
+
};
|
|
43
|
+
|
|
44
|
+
vi.mocked(fs.readFile).mockResolvedValue(JSON.stringify(snapshot));
|
|
45
|
+
|
|
46
|
+
const result = await synchronizer.initialize();
|
|
47
|
+
|
|
48
|
+
expect(result).toBe(true);
|
|
49
|
+
expect(synchronizer.getLastCommitHash()).toBe("abc123");
|
|
50
|
+
expect(synchronizer.getCommitsIndexed()).toBe(100);
|
|
51
|
+
});
|
|
52
|
+
|
|
53
|
+
it("should return false when snapshot does not exist", async () => {
|
|
54
|
+
vi.mocked(fs.readFile).mockRejectedValue(
|
|
55
|
+
new Error("ENOENT: no such file"),
|
|
56
|
+
);
|
|
57
|
+
|
|
58
|
+
const result = await synchronizer.initialize();
|
|
59
|
+
|
|
60
|
+
expect(result).toBe(false);
|
|
61
|
+
expect(synchronizer.getLastCommitHash()).toBeNull();
|
|
62
|
+
});
|
|
63
|
+
|
|
64
|
+
it("should return false when snapshot is for different repo", async () => {
|
|
65
|
+
const snapshot = {
|
|
66
|
+
repoPath: "/different/repo",
|
|
67
|
+
lastCommitHash: "abc123",
|
|
68
|
+
lastIndexedAt: Date.now(),
|
|
69
|
+
commitsIndexed: 100,
|
|
70
|
+
};
|
|
71
|
+
|
|
72
|
+
vi.mocked(fs.readFile).mockResolvedValue(JSON.stringify(snapshot));
|
|
73
|
+
|
|
74
|
+
const result = await synchronizer.initialize();
|
|
75
|
+
|
|
76
|
+
expect(result).toBe(false);
|
|
77
|
+
expect(synchronizer.getLastCommitHash()).toBeNull();
|
|
78
|
+
});
|
|
79
|
+
|
|
80
|
+
it("should return false when snapshot is invalid JSON", async () => {
|
|
81
|
+
vi.mocked(fs.readFile).mockResolvedValue("not valid json");
|
|
82
|
+
|
|
83
|
+
const result = await synchronizer.initialize();
|
|
84
|
+
|
|
85
|
+
expect(result).toBe(false);
|
|
86
|
+
});
|
|
87
|
+
});
|
|
88
|
+
|
|
89
|
+
describe("getLastCommitHash", () => {
|
|
90
|
+
it("should return null when no snapshot loaded", () => {
|
|
91
|
+
expect(synchronizer.getLastCommitHash()).toBeNull();
|
|
92
|
+
});
|
|
93
|
+
|
|
94
|
+
it("should return hash after loading snapshot", async () => {
|
|
95
|
+
const snapshot = {
|
|
96
|
+
repoPath: "/test/repo",
|
|
97
|
+
lastCommitHash: "xyz789",
|
|
98
|
+
lastIndexedAt: Date.now(),
|
|
99
|
+
commitsIndexed: 50,
|
|
100
|
+
};
|
|
101
|
+
|
|
102
|
+
vi.mocked(fs.readFile).mockResolvedValue(JSON.stringify(snapshot));
|
|
103
|
+
await synchronizer.initialize();
|
|
104
|
+
|
|
105
|
+
expect(synchronizer.getLastCommitHash()).toBe("xyz789");
|
|
106
|
+
});
|
|
107
|
+
});
|
|
108
|
+
|
|
109
|
+
describe("getLastIndexedAt", () => {
|
|
110
|
+
it("should return null when no snapshot loaded", () => {
|
|
111
|
+
expect(synchronizer.getLastIndexedAt()).toBeNull();
|
|
112
|
+
});
|
|
113
|
+
|
|
114
|
+
it("should return date after loading snapshot", async () => {
|
|
115
|
+
const timestamp = Date.now();
|
|
116
|
+
const snapshot = {
|
|
117
|
+
repoPath: "/test/repo",
|
|
118
|
+
lastCommitHash: "abc123",
|
|
119
|
+
lastIndexedAt: timestamp,
|
|
120
|
+
commitsIndexed: 50,
|
|
121
|
+
};
|
|
122
|
+
|
|
123
|
+
vi.mocked(fs.readFile).mockResolvedValue(JSON.stringify(snapshot));
|
|
124
|
+
await synchronizer.initialize();
|
|
125
|
+
|
|
126
|
+
const date = synchronizer.getLastIndexedAt();
|
|
127
|
+
expect(date).toBeInstanceOf(Date);
|
|
128
|
+
expect(date?.getTime()).toBe(timestamp);
|
|
129
|
+
});
|
|
130
|
+
});
|
|
131
|
+
|
|
132
|
+
describe("getCommitsIndexed", () => {
|
|
133
|
+
it("should return 0 when no snapshot loaded", () => {
|
|
134
|
+
expect(synchronizer.getCommitsIndexed()).toBe(0);
|
|
135
|
+
});
|
|
136
|
+
|
|
137
|
+
it("should return count after loading snapshot", async () => {
|
|
138
|
+
const snapshot = {
|
|
139
|
+
repoPath: "/test/repo",
|
|
140
|
+
lastCommitHash: "abc123",
|
|
141
|
+
lastIndexedAt: Date.now(),
|
|
142
|
+
commitsIndexed: 150,
|
|
143
|
+
};
|
|
144
|
+
|
|
145
|
+
vi.mocked(fs.readFile).mockResolvedValue(JSON.stringify(snapshot));
|
|
146
|
+
await synchronizer.initialize();
|
|
147
|
+
|
|
148
|
+
expect(synchronizer.getCommitsIndexed()).toBe(150);
|
|
149
|
+
});
|
|
150
|
+
});
|
|
151
|
+
|
|
152
|
+
describe("updateSnapshot", () => {
|
|
153
|
+
it("should write snapshot to file", async () => {
|
|
154
|
+
vi.mocked(fs.mkdir).mockResolvedValue(undefined);
|
|
155
|
+
vi.mocked(fs.writeFile).mockResolvedValue(undefined);
|
|
156
|
+
vi.mocked(fs.rename).mockResolvedValue(undefined);
|
|
157
|
+
|
|
158
|
+
await synchronizer.updateSnapshot("def456", 200);
|
|
159
|
+
|
|
160
|
+
expect(fs.mkdir).toHaveBeenCalledWith(
|
|
161
|
+
expect.stringContaining("git-snapshots"),
|
|
162
|
+
{ recursive: true },
|
|
163
|
+
);
|
|
164
|
+
// Check the file was written with correct data (JSON may be formatted)
|
|
165
|
+
expect(fs.writeFile).toHaveBeenCalledWith(
|
|
166
|
+
expect.stringContaining(".tmp"),
|
|
167
|
+
expect.stringMatching(/lastCommitHash.*def456/s),
|
|
168
|
+
);
|
|
169
|
+
expect(fs.rename).toHaveBeenCalled();
|
|
170
|
+
|
|
171
|
+
// Verify internal state was updated
|
|
172
|
+
expect(synchronizer.getLastCommitHash()).toBe("def456");
|
|
173
|
+
expect(synchronizer.getCommitsIndexed()).toBe(200);
|
|
174
|
+
});
|
|
175
|
+
|
|
176
|
+
it("should use atomic write with rename", async () => {
|
|
177
|
+
vi.mocked(fs.mkdir).mockResolvedValue(undefined);
|
|
178
|
+
vi.mocked(fs.writeFile).mockResolvedValue(undefined);
|
|
179
|
+
vi.mocked(fs.rename).mockResolvedValue(undefined);
|
|
180
|
+
|
|
181
|
+
await synchronizer.updateSnapshot("abc123", 100);
|
|
182
|
+
|
|
183
|
+
// Verify write to temp file first, then rename
|
|
184
|
+
const writeCall = vi.mocked(fs.writeFile).mock.calls[0];
|
|
185
|
+
const renameCall = vi.mocked(fs.rename).mock.calls[0];
|
|
186
|
+
|
|
187
|
+
expect(writeCall[0]).toContain(".tmp");
|
|
188
|
+
expect(renameCall[0]).toContain(".tmp");
|
|
189
|
+
expect(renameCall[1]).not.toContain(".tmp");
|
|
190
|
+
});
|
|
191
|
+
});
|
|
192
|
+
|
|
193
|
+
describe("deleteSnapshot", () => {
|
|
194
|
+
it("should delete snapshot file", async () => {
|
|
195
|
+
vi.mocked(fs.unlink).mockResolvedValue(undefined);
|
|
196
|
+
|
|
197
|
+
await synchronizer.deleteSnapshot();
|
|
198
|
+
|
|
199
|
+
expect(fs.unlink).toHaveBeenCalled();
|
|
200
|
+
expect(synchronizer.getLastCommitHash()).toBeNull();
|
|
201
|
+
});
|
|
202
|
+
|
|
203
|
+
it("should not throw when file does not exist", async () => {
|
|
204
|
+
vi.mocked(fs.unlink).mockRejectedValue(new Error("ENOENT"));
|
|
205
|
+
|
|
206
|
+
await expect(synchronizer.deleteSnapshot()).resolves.not.toThrow();
|
|
207
|
+
});
|
|
208
|
+
});
|
|
209
|
+
|
|
210
|
+
describe("exists", () => {
|
|
211
|
+
it("should return true when snapshot file exists", async () => {
|
|
212
|
+
vi.mocked(fs.access).mockResolvedValue(undefined);
|
|
213
|
+
|
|
214
|
+
const result = await synchronizer.exists();
|
|
215
|
+
|
|
216
|
+
expect(result).toBe(true);
|
|
217
|
+
});
|
|
218
|
+
|
|
219
|
+
it("should return false when snapshot file does not exist", async () => {
|
|
220
|
+
vi.mocked(fs.access).mockRejectedValue(new Error("ENOENT"));
|
|
221
|
+
|
|
222
|
+
const result = await synchronizer.exists();
|
|
223
|
+
|
|
224
|
+
expect(result).toBe(false);
|
|
225
|
+
});
|
|
226
|
+
});
|
|
227
|
+
|
|
228
|
+
describe("getSnapshotAge", () => {
|
|
229
|
+
it("should return null when no snapshot loaded", () => {
|
|
230
|
+
expect(synchronizer.getSnapshotAge()).toBeNull();
|
|
231
|
+
});
|
|
232
|
+
|
|
233
|
+
it("should return age in milliseconds", async () => {
|
|
234
|
+
const timestamp = Date.now() - 10000; // 10 seconds ago
|
|
235
|
+
const snapshot = {
|
|
236
|
+
repoPath: "/test/repo",
|
|
237
|
+
lastCommitHash: "abc123",
|
|
238
|
+
lastIndexedAt: timestamp,
|
|
239
|
+
commitsIndexed: 50,
|
|
240
|
+
};
|
|
241
|
+
|
|
242
|
+
vi.mocked(fs.readFile).mockResolvedValue(JSON.stringify(snapshot));
|
|
243
|
+
await synchronizer.initialize();
|
|
244
|
+
|
|
245
|
+
const age = synchronizer.getSnapshotAge();
|
|
246
|
+
expect(age).toBeGreaterThanOrEqual(10000);
|
|
247
|
+
expect(age).toBeLessThan(11000);
|
|
248
|
+
});
|
|
249
|
+
});
|
|
250
|
+
});
|
|
@@ -0,0 +1,122 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* GitSynchronizer - Track last indexed commit for incremental updates
|
|
3
|
+
*/
|
|
4
|
+
|
|
5
|
+
import { promises as fs } from "node:fs";
|
|
6
|
+
import { homedir } from "node:os";
|
|
7
|
+
import { dirname, join } from "node:path";
|
|
8
|
+
import type { GitSnapshot } from "../types.js";
|
|
9
|
+
|
|
10
|
+
export class GitSynchronizer {
|
|
11
|
+
private snapshotPath: string;
|
|
12
|
+
private snapshot: GitSnapshot | null = null;
|
|
13
|
+
|
|
14
|
+
constructor(
|
|
15
|
+
private repoPath: string,
|
|
16
|
+
collectionName: string,
|
|
17
|
+
) {
|
|
18
|
+
// Store snapshots in ~/.qdrant-mcp/git-snapshots/
|
|
19
|
+
const snapshotDir = join(homedir(), ".qdrant-mcp", "git-snapshots");
|
|
20
|
+
this.snapshotPath = join(snapshotDir, `${collectionName}.json`);
|
|
21
|
+
}
|
|
22
|
+
|
|
23
|
+
/**
|
|
24
|
+
* Initialize synchronizer by loading existing snapshot
|
|
25
|
+
* @returns true if snapshot exists and was loaded
|
|
26
|
+
*/
|
|
27
|
+
async initialize(): Promise<boolean> {
|
|
28
|
+
try {
|
|
29
|
+
const content = await fs.readFile(this.snapshotPath, "utf-8");
|
|
30
|
+
this.snapshot = JSON.parse(content) as GitSnapshot;
|
|
31
|
+
|
|
32
|
+
// Validate snapshot is for the same repo
|
|
33
|
+
if (this.snapshot.repoPath !== this.repoPath) {
|
|
34
|
+
this.snapshot = null;
|
|
35
|
+
return false;
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
return true;
|
|
39
|
+
} catch {
|
|
40
|
+
this.snapshot = null;
|
|
41
|
+
return false;
|
|
42
|
+
}
|
|
43
|
+
}
|
|
44
|
+
|
|
45
|
+
/**
|
|
46
|
+
* Get the last indexed commit hash
|
|
47
|
+
*/
|
|
48
|
+
getLastCommitHash(): string | null {
|
|
49
|
+
return this.snapshot?.lastCommitHash ?? null;
|
|
50
|
+
}
|
|
51
|
+
|
|
52
|
+
/**
|
|
53
|
+
* Get the timestamp of last indexing
|
|
54
|
+
*/
|
|
55
|
+
getLastIndexedAt(): Date | null {
|
|
56
|
+
if (!this.snapshot) return null;
|
|
57
|
+
return new Date(this.snapshot.lastIndexedAt);
|
|
58
|
+
}
|
|
59
|
+
|
|
60
|
+
/**
|
|
61
|
+
* Get the number of commits indexed
|
|
62
|
+
*/
|
|
63
|
+
getCommitsIndexed(): number {
|
|
64
|
+
return this.snapshot?.commitsIndexed ?? 0;
|
|
65
|
+
}
|
|
66
|
+
|
|
67
|
+
/**
|
|
68
|
+
* Update snapshot with new indexing state
|
|
69
|
+
*/
|
|
70
|
+
async updateSnapshot(
|
|
71
|
+
lastCommitHash: string,
|
|
72
|
+
commitsIndexed: number,
|
|
73
|
+
): Promise<void> {
|
|
74
|
+
this.snapshot = {
|
|
75
|
+
repoPath: this.repoPath,
|
|
76
|
+
lastCommitHash,
|
|
77
|
+
lastIndexedAt: Date.now(),
|
|
78
|
+
commitsIndexed,
|
|
79
|
+
};
|
|
80
|
+
|
|
81
|
+
// Ensure directory exists
|
|
82
|
+
const dir = dirname(this.snapshotPath);
|
|
83
|
+
await fs.mkdir(dir, { recursive: true });
|
|
84
|
+
|
|
85
|
+
// Write snapshot atomically using temp file
|
|
86
|
+
const tempPath = `${this.snapshotPath}.tmp`;
|
|
87
|
+
await fs.writeFile(tempPath, JSON.stringify(this.snapshot, null, 2));
|
|
88
|
+
await fs.rename(tempPath, this.snapshotPath);
|
|
89
|
+
}
|
|
90
|
+
|
|
91
|
+
/**
|
|
92
|
+
* Delete the snapshot file
|
|
93
|
+
*/
|
|
94
|
+
async deleteSnapshot(): Promise<void> {
|
|
95
|
+
try {
|
|
96
|
+
await fs.unlink(this.snapshotPath);
|
|
97
|
+
this.snapshot = null;
|
|
98
|
+
} catch {
|
|
99
|
+
// Ignore if file doesn't exist
|
|
100
|
+
}
|
|
101
|
+
}
|
|
102
|
+
|
|
103
|
+
/**
|
|
104
|
+
* Check if snapshot exists
|
|
105
|
+
*/
|
|
106
|
+
async exists(): Promise<boolean> {
|
|
107
|
+
try {
|
|
108
|
+
await fs.access(this.snapshotPath);
|
|
109
|
+
return true;
|
|
110
|
+
} catch {
|
|
111
|
+
return false;
|
|
112
|
+
}
|
|
113
|
+
}
|
|
114
|
+
|
|
115
|
+
/**
|
|
116
|
+
* Get snapshot age in milliseconds
|
|
117
|
+
*/
|
|
118
|
+
getSnapshotAge(): number | null {
|
|
119
|
+
if (!this.snapshot) return null;
|
|
120
|
+
return Date.now() - this.snapshot.lastIndexedAt;
|
|
121
|
+
}
|
|
122
|
+
}
|
package/src/git/types.ts
ADDED
|
@@ -0,0 +1,192 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Type definitions for git history indexing module
|
|
3
|
+
*/
|
|
4
|
+
|
|
5
|
+
/**
|
|
6
|
+
* Commit type classification based on conventional commits
|
|
7
|
+
*/
|
|
8
|
+
export type CommitType =
|
|
9
|
+
| "feat"
|
|
10
|
+
| "fix"
|
|
11
|
+
| "refactor"
|
|
12
|
+
| "docs"
|
|
13
|
+
| "test"
|
|
14
|
+
| "chore"
|
|
15
|
+
| "style"
|
|
16
|
+
| "perf"
|
|
17
|
+
| "build"
|
|
18
|
+
| "ci"
|
|
19
|
+
| "revert"
|
|
20
|
+
| "other";
|
|
21
|
+
|
|
22
|
+
/**
|
|
23
|
+
* Configuration for git history indexing
|
|
24
|
+
*/
|
|
25
|
+
export interface GitConfig {
|
|
26
|
+
// Commit extraction
|
|
27
|
+
maxCommits: number; // Max commits to index per run
|
|
28
|
+
includeFileList: boolean; // Include changed file list in chunks
|
|
29
|
+
includeDiff: boolean; // Include truncated diff in chunks
|
|
30
|
+
maxDiffSize: number; // Max bytes of diff per commit
|
|
31
|
+
gitTimeout: number; // Timeout for git commands in milliseconds
|
|
32
|
+
|
|
33
|
+
// Chunking
|
|
34
|
+
maxChunkSize: number; // Max characters per chunk
|
|
35
|
+
|
|
36
|
+
// Indexing
|
|
37
|
+
batchSize: number; // Embeddings per batch
|
|
38
|
+
batchRetryAttempts: number; // Number of retry attempts for failed batches
|
|
39
|
+
|
|
40
|
+
// Search
|
|
41
|
+
defaultSearchLimit: number; // Default search results
|
|
42
|
+
enableHybridSearch: boolean; // Enable hybrid search
|
|
43
|
+
}
|
|
44
|
+
|
|
45
|
+
/**
|
|
46
|
+
* Raw commit data parsed from git log
|
|
47
|
+
*/
|
|
48
|
+
export interface RawCommit {
|
|
49
|
+
hash: string; // Full commit hash
|
|
50
|
+
shortHash: string; // Short hash (7 chars)
|
|
51
|
+
author: string; // Author name
|
|
52
|
+
authorEmail: string; // Author email
|
|
53
|
+
date: Date; // Commit date
|
|
54
|
+
subject: string; // First line of commit message
|
|
55
|
+
body: string; // Full commit message body
|
|
56
|
+
files: string[]; // Changed files
|
|
57
|
+
insertions: number; // Lines added
|
|
58
|
+
deletions: number; // Lines deleted
|
|
59
|
+
}
|
|
60
|
+
|
|
61
|
+
/**
|
|
62
|
+
* Embeddable chunk created from a commit
|
|
63
|
+
*/
|
|
64
|
+
export interface CommitChunk {
|
|
65
|
+
content: string; // Text content for embedding
|
|
66
|
+
metadata: {
|
|
67
|
+
commitHash: string;
|
|
68
|
+
shortHash: string;
|
|
69
|
+
author: string;
|
|
70
|
+
authorEmail: string;
|
|
71
|
+
date: string; // ISO date string
|
|
72
|
+
subject: string;
|
|
73
|
+
commitType: CommitType;
|
|
74
|
+
files: string[];
|
|
75
|
+
insertions: number;
|
|
76
|
+
deletions: number;
|
|
77
|
+
repoPath: string;
|
|
78
|
+
};
|
|
79
|
+
}
|
|
80
|
+
|
|
81
|
+
/**
|
|
82
|
+
* Options for indexing git history
|
|
83
|
+
*/
|
|
84
|
+
export interface GitIndexOptions {
|
|
85
|
+
forceReindex?: boolean; // Delete existing and re-index
|
|
86
|
+
sinceDate?: string; // ISO date string - only index commits after this date
|
|
87
|
+
maxCommits?: number; // Override config maxCommits
|
|
88
|
+
}
|
|
89
|
+
|
|
90
|
+
/**
|
|
91
|
+
* Statistics from indexing operation
|
|
92
|
+
*/
|
|
93
|
+
export interface GitIndexStats {
|
|
94
|
+
commitsScanned: number;
|
|
95
|
+
commitsIndexed: number;
|
|
96
|
+
chunksCreated: number;
|
|
97
|
+
durationMs: number;
|
|
98
|
+
status: "completed" | "partial" | "failed";
|
|
99
|
+
errors?: string[];
|
|
100
|
+
}
|
|
101
|
+
|
|
102
|
+
/**
|
|
103
|
+
* Statistics from incremental update operation
|
|
104
|
+
*/
|
|
105
|
+
export interface GitChangeStats {
|
|
106
|
+
newCommits: number;
|
|
107
|
+
chunksAdded: number;
|
|
108
|
+
durationMs: number;
|
|
109
|
+
}
|
|
110
|
+
|
|
111
|
+
/**
|
|
112
|
+
* Search result from git history
|
|
113
|
+
*/
|
|
114
|
+
export interface GitSearchResult {
|
|
115
|
+
content: string;
|
|
116
|
+
commitHash: string;
|
|
117
|
+
shortHash: string;
|
|
118
|
+
author: string;
|
|
119
|
+
date: string;
|
|
120
|
+
subject: string;
|
|
121
|
+
commitType: CommitType;
|
|
122
|
+
files: string[];
|
|
123
|
+
score: number;
|
|
124
|
+
}
|
|
125
|
+
|
|
126
|
+
/**
|
|
127
|
+
* Search options for git history
|
|
128
|
+
*/
|
|
129
|
+
export interface GitSearchOptions {
|
|
130
|
+
limit?: number;
|
|
131
|
+
useHybrid?: boolean;
|
|
132
|
+
commitTypes?: CommitType[]; // Filter by commit type
|
|
133
|
+
authors?: string[]; // Filter by author name/email
|
|
134
|
+
dateFrom?: string; // ISO date string - only commits after
|
|
135
|
+
dateTo?: string; // ISO date string - only commits before
|
|
136
|
+
scoreThreshold?: number;
|
|
137
|
+
}
|
|
138
|
+
|
|
139
|
+
/**
|
|
140
|
+
* Indexing status for a repository
|
|
141
|
+
*/
|
|
142
|
+
export type GitIndexingStatus = "not_indexed" | "indexing" | "indexed";
|
|
143
|
+
|
|
144
|
+
/**
|
|
145
|
+
* Status information for a git history index
|
|
146
|
+
*/
|
|
147
|
+
export interface GitIndexStatus {
|
|
148
|
+
/** @deprecated Use `status` instead. True only when status is 'indexed'. */
|
|
149
|
+
isIndexed: boolean;
|
|
150
|
+
/** Current indexing status: 'not_indexed', 'indexing', or 'indexed' */
|
|
151
|
+
status: GitIndexingStatus;
|
|
152
|
+
collectionName?: string;
|
|
153
|
+
commitsCount?: number;
|
|
154
|
+
chunksCount?: number;
|
|
155
|
+
lastCommitHash?: string;
|
|
156
|
+
lastIndexedAt?: Date;
|
|
157
|
+
}
|
|
158
|
+
|
|
159
|
+
/**
|
|
160
|
+
* Progress callback for indexing operations
|
|
161
|
+
*/
|
|
162
|
+
export type GitProgressCallback = (progress: GitProgressUpdate) => void;
|
|
163
|
+
|
|
164
|
+
/**
|
|
165
|
+
* Progress update during indexing
|
|
166
|
+
*/
|
|
167
|
+
export interface GitProgressUpdate {
|
|
168
|
+
phase: "extracting" | "chunking" | "embedding" | "storing";
|
|
169
|
+
current: number;
|
|
170
|
+
total: number;
|
|
171
|
+
percentage: number;
|
|
172
|
+
message: string;
|
|
173
|
+
}
|
|
174
|
+
|
|
175
|
+
/**
|
|
176
|
+
* Snapshot for tracking last indexed commit
|
|
177
|
+
*/
|
|
178
|
+
export interface GitSnapshot {
|
|
179
|
+
repoPath: string;
|
|
180
|
+
lastCommitHash: string;
|
|
181
|
+
lastIndexedAt: number; // Unix timestamp
|
|
182
|
+
commitsIndexed: number;
|
|
183
|
+
}
|
|
184
|
+
|
|
185
|
+
/**
|
|
186
|
+
* Options for extracting commits from git
|
|
187
|
+
*/
|
|
188
|
+
export interface GitExtractOptions {
|
|
189
|
+
sinceCommit?: string; // Only commits after this hash
|
|
190
|
+
sinceDate?: string; // Only commits after this date (ISO string)
|
|
191
|
+
maxCommits?: number; // Limit number of commits
|
|
192
|
+
}
|
package/src/index.ts
CHANGED
|
@@ -18,6 +18,8 @@ import {
|
|
|
18
18
|
} from "./code/config.js";
|
|
19
19
|
import { CodeIndexer } from "./code/indexer.js";
|
|
20
20
|
import type { CodeConfig } from "./code/types.js";
|
|
21
|
+
import { DEFAULT_GIT_CONFIG, GitHistoryIndexer } from "./git/index.js";
|
|
22
|
+
import type { GitConfig } from "./git/types.js";
|
|
21
23
|
import { EmbeddingProviderFactory } from "./embeddings/factory.js";
|
|
22
24
|
import { loadPromptsConfig, type PromptsConfig } from "./prompts/index.js";
|
|
23
25
|
import { registerAllPrompts } from "./prompts/register.js";
|
|
@@ -183,6 +185,45 @@ const codeConfig: CodeConfig = {
|
|
|
183
185
|
|
|
184
186
|
const codeIndexer = new CodeIndexer(qdrant, embeddings, codeConfig);
|
|
185
187
|
|
|
188
|
+
// Initialize git history indexer
|
|
189
|
+
const gitConfig: GitConfig = {
|
|
190
|
+
maxCommits: parseInt(
|
|
191
|
+
process.env.GIT_MAX_COMMITS || String(DEFAULT_GIT_CONFIG.maxCommits),
|
|
192
|
+
10,
|
|
193
|
+
),
|
|
194
|
+
includeFileList: process.env.GIT_INCLUDE_FILES !== "false",
|
|
195
|
+
includeDiff: process.env.GIT_INCLUDE_DIFF !== "false",
|
|
196
|
+
maxDiffSize: parseInt(
|
|
197
|
+
process.env.GIT_MAX_DIFF_SIZE || String(DEFAULT_GIT_CONFIG.maxDiffSize),
|
|
198
|
+
10,
|
|
199
|
+
),
|
|
200
|
+
gitTimeout: parseInt(
|
|
201
|
+
process.env.GIT_TIMEOUT || String(DEFAULT_GIT_CONFIG.gitTimeout),
|
|
202
|
+
10,
|
|
203
|
+
),
|
|
204
|
+
maxChunkSize: parseInt(
|
|
205
|
+
process.env.GIT_MAX_CHUNK_SIZE || String(DEFAULT_GIT_CONFIG.maxChunkSize),
|
|
206
|
+
10,
|
|
207
|
+
),
|
|
208
|
+
batchSize: parseInt(
|
|
209
|
+
process.env.GIT_BATCH_SIZE || String(DEFAULT_GIT_CONFIG.batchSize),
|
|
210
|
+
10,
|
|
211
|
+
),
|
|
212
|
+
batchRetryAttempts: parseInt(
|
|
213
|
+
process.env.GIT_BATCH_RETRY_ATTEMPTS ||
|
|
214
|
+
String(DEFAULT_GIT_CONFIG.batchRetryAttempts),
|
|
215
|
+
10,
|
|
216
|
+
),
|
|
217
|
+
defaultSearchLimit: parseInt(
|
|
218
|
+
process.env.GIT_SEARCH_LIMIT ||
|
|
219
|
+
String(DEFAULT_GIT_CONFIG.defaultSearchLimit),
|
|
220
|
+
10,
|
|
221
|
+
),
|
|
222
|
+
enableHybridSearch: process.env.GIT_ENABLE_HYBRID !== "false",
|
|
223
|
+
};
|
|
224
|
+
|
|
225
|
+
const gitHistoryIndexer = new GitHistoryIndexer(qdrant, embeddings, gitConfig);
|
|
226
|
+
|
|
186
227
|
// Load prompts configuration if file exists
|
|
187
228
|
let promptsConfig: PromptsConfig | null = null;
|
|
188
229
|
if (existsSync(PROMPTS_CONFIG_FILE)) {
|
|
@@ -213,6 +254,7 @@ function createAndConfigureServer(): McpServer {
|
|
|
213
254
|
qdrant,
|
|
214
255
|
embeddings,
|
|
215
256
|
codeIndexer,
|
|
257
|
+
gitHistoryIndexer,
|
|
216
258
|
});
|
|
217
259
|
|
|
218
260
|
// Register all resources
|