@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.
Files changed (88) hide show
  1. package/.github/workflows/ci.yml +4 -1
  2. package/CHANGELOG.md +22 -0
  3. package/README.md +78 -3
  4. package/build/code/indexer.d.ts +2 -1
  5. package/build/code/indexer.d.ts.map +1 -1
  6. package/build/code/indexer.js +37 -7
  7. package/build/code/indexer.js.map +1 -1
  8. package/build/git/chunker.d.ts +39 -0
  9. package/build/git/chunker.d.ts.map +1 -0
  10. package/build/git/chunker.js +210 -0
  11. package/build/git/chunker.js.map +1 -0
  12. package/build/git/chunker.test.d.ts +2 -0
  13. package/build/git/chunker.test.d.ts.map +1 -0
  14. package/build/git/chunker.test.js +230 -0
  15. package/build/git/chunker.test.js.map +1 -0
  16. package/build/git/config.d.ts +34 -0
  17. package/build/git/config.d.ts.map +1 -0
  18. package/build/git/config.js +163 -0
  19. package/build/git/config.js.map +1 -0
  20. package/build/git/extractor.d.ts +57 -0
  21. package/build/git/extractor.d.ts.map +1 -0
  22. package/build/git/extractor.integration.test.d.ts +6 -0
  23. package/build/git/extractor.integration.test.d.ts.map +1 -0
  24. package/build/git/extractor.integration.test.js +166 -0
  25. package/build/git/extractor.integration.test.js.map +1 -0
  26. package/build/git/extractor.js +231 -0
  27. package/build/git/extractor.js.map +1 -0
  28. package/build/git/extractor.test.d.ts +2 -0
  29. package/build/git/extractor.test.d.ts.map +1 -0
  30. package/build/git/extractor.test.js +267 -0
  31. package/build/git/extractor.test.js.map +1 -0
  32. package/build/git/index.d.ts +10 -0
  33. package/build/git/index.d.ts.map +1 -0
  34. package/build/git/index.js +11 -0
  35. package/build/git/index.js.map +1 -0
  36. package/build/git/indexer.d.ts +50 -0
  37. package/build/git/indexer.d.ts.map +1 -0
  38. package/build/git/indexer.js +588 -0
  39. package/build/git/indexer.js.map +1 -0
  40. package/build/git/indexer.test.d.ts +2 -0
  41. package/build/git/indexer.test.d.ts.map +1 -0
  42. package/build/git/indexer.test.js +867 -0
  43. package/build/git/indexer.test.js.map +1 -0
  44. package/build/git/sync/synchronizer.d.ts +43 -0
  45. package/build/git/sync/synchronizer.d.ts.map +1 -0
  46. package/build/git/sync/synchronizer.js +108 -0
  47. package/build/git/sync/synchronizer.js.map +1 -0
  48. package/build/git/sync/synchronizer.test.d.ts +2 -0
  49. package/build/git/sync/synchronizer.test.d.ts.map +1 -0
  50. package/build/git/sync/synchronizer.test.js +188 -0
  51. package/build/git/sync/synchronizer.test.js.map +1 -0
  52. package/build/git/types.d.ts +159 -0
  53. package/build/git/types.d.ts.map +1 -0
  54. package/build/git/types.js +5 -0
  55. package/build/git/types.js.map +1 -0
  56. package/build/index.js +18 -0
  57. package/build/index.js.map +1 -1
  58. package/build/tools/git-history.d.ts +10 -0
  59. package/build/tools/git-history.d.ts.map +1 -0
  60. package/build/tools/git-history.js +144 -0
  61. package/build/tools/git-history.js.map +1 -0
  62. package/build/tools/index.d.ts +2 -0
  63. package/build/tools/index.d.ts.map +1 -1
  64. package/build/tools/index.js +4 -0
  65. package/build/tools/index.js.map +1 -1
  66. package/build/tools/schemas.d.ts +24 -0
  67. package/build/tools/schemas.d.ts.map +1 -1
  68. package/build/tools/schemas.js +64 -0
  69. package/build/tools/schemas.js.map +1 -1
  70. package/package.json +2 -2
  71. package/src/code/indexer.ts +49 -7
  72. package/src/git/chunker.test.ts +284 -0
  73. package/src/git/chunker.ts +256 -0
  74. package/src/git/config.ts +173 -0
  75. package/src/git/extractor.integration.test.ts +221 -0
  76. package/src/git/extractor.test.ts +403 -0
  77. package/src/git/extractor.ts +284 -0
  78. package/src/git/index.ts +31 -0
  79. package/src/git/indexer.test.ts +1089 -0
  80. package/src/git/indexer.ts +745 -0
  81. package/src/git/sync/synchronizer.test.ts +250 -0
  82. package/src/git/sync/synchronizer.ts +122 -0
  83. package/src/git/types.ts +192 -0
  84. package/src/index.ts +42 -0
  85. package/src/tools/git-history.ts +208 -0
  86. package/src/tools/index.ts +7 -0
  87. package/src/tools/schemas.ts +75 -0
  88. package/vitest.config.ts +2 -0
@@ -0,0 +1,284 @@
1
+ /**
2
+ * GitExtractor - Extract commit data from git repositories
3
+ * Uses child_process.execFile for security (no shell injection)
4
+ */
5
+
6
+ import { execFile } from "node:child_process";
7
+ import { promisify } from "node:util";
8
+ import {
9
+ GIT_LOG_COMMIT_DELIMITER,
10
+ GIT_LOG_FORMAT,
11
+ GIT_MAX_BUFFER,
12
+ } from "./config.js";
13
+ import type { GitConfig, GitExtractOptions, RawCommit } from "./types.js";
14
+
15
+ const execFileAsync = promisify(execFile);
16
+
17
+ /**
18
+ * Normalize git remote URL to consistent format for hashing.
19
+ * Handles both SSH and HTTPS URL formats.
20
+ *
21
+ * @example
22
+ * normalizeRemoteUrl("git@github.com:user/repo.git") // → "user/repo"
23
+ * normalizeRemoteUrl("https://github.com/user/repo.git") // → "user/repo"
24
+ * normalizeRemoteUrl("") // → ""
25
+ */
26
+ export function normalizeRemoteUrl(url: string): string {
27
+ if (!url) return "";
28
+ return url
29
+ .replace(/^git@[^:]+:/, "") // git@github.com:user/repo → user/repo
30
+ .replace(/^https?:\/\/[^/]+\//, "") // https://github.com/user/repo → user/repo
31
+ .replace(/\.git$/, ""); // user/repo.git → user/repo
32
+ }
33
+
34
+ export class GitExtractor {
35
+ constructor(
36
+ private repoPath: string,
37
+ private config: GitConfig,
38
+ ) {}
39
+
40
+ /**
41
+ * Validate that the path is a git repository
42
+ */
43
+ async validateRepository(): Promise<boolean> {
44
+ try {
45
+ await execFileAsync("git", ["rev-parse", "--git-dir"], {
46
+ cwd: this.repoPath,
47
+ maxBuffer: GIT_MAX_BUFFER,
48
+ timeout: this.config.gitTimeout,
49
+ });
50
+ return true;
51
+ } catch {
52
+ return false;
53
+ }
54
+ }
55
+
56
+ /**
57
+ * Get the latest commit hash
58
+ */
59
+ async getLatestCommitHash(): Promise<string> {
60
+ const { stdout } = await execFileAsync("git", ["rev-parse", "HEAD"], {
61
+ cwd: this.repoPath,
62
+ maxBuffer: GIT_MAX_BUFFER,
63
+ timeout: this.config.gitTimeout,
64
+ });
65
+ return stdout.trim();
66
+ }
67
+
68
+ /**
69
+ * Get the remote origin URL, or empty string if not configured
70
+ */
71
+ async getRemoteUrl(): Promise<string> {
72
+ try {
73
+ const { stdout } = await execFileAsync(
74
+ "git",
75
+ ["remote", "get-url", "origin"],
76
+ {
77
+ cwd: this.repoPath,
78
+ maxBuffer: GIT_MAX_BUFFER,
79
+ timeout: this.config.gitTimeout,
80
+ },
81
+ );
82
+ return stdout.trim();
83
+ } catch {
84
+ return ""; // No remote configured
85
+ }
86
+ }
87
+
88
+ /**
89
+ * Get total commit count (optionally after a specific commit)
90
+ */
91
+ async getCommitCount(sinceCommit?: string): Promise<number> {
92
+ const args = ["rev-list", "--count"];
93
+
94
+ if (sinceCommit) {
95
+ args.push(`${sinceCommit}..HEAD`);
96
+ } else {
97
+ args.push("HEAD");
98
+ }
99
+
100
+ const { stdout } = await execFileAsync("git", args, {
101
+ cwd: this.repoPath,
102
+ maxBuffer: GIT_MAX_BUFFER,
103
+ timeout: this.config.gitTimeout,
104
+ });
105
+
106
+ return parseInt(stdout.trim(), 10);
107
+ }
108
+
109
+ /**
110
+ * Extract commits from the repository
111
+ */
112
+ async getCommits(options?: GitExtractOptions): Promise<RawCommit[]> {
113
+ const maxCommits = options?.maxCommits ?? this.config.maxCommits;
114
+
115
+ // Build git log arguments
116
+ const args = [
117
+ "log",
118
+ `--pretty=format:${GIT_LOG_COMMIT_DELIMITER}${GIT_LOG_FORMAT}`,
119
+ "--numstat", // Include insertions/deletions per file
120
+ `-n${maxCommits}`,
121
+ ];
122
+
123
+ // Add range if sinceCommit is specified
124
+ if (options?.sinceCommit) {
125
+ args.push(`${options.sinceCommit}..HEAD`);
126
+ }
127
+
128
+ // Add date filter if sinceDate is specified
129
+ if (options?.sinceDate) {
130
+ args.push(`--since=${options.sinceDate}`);
131
+ }
132
+
133
+ const { stdout } = await execFileAsync("git", args, {
134
+ cwd: this.repoPath,
135
+ maxBuffer: GIT_MAX_BUFFER,
136
+ timeout: this.config.gitTimeout,
137
+ });
138
+
139
+ return this.parseGitLog(stdout);
140
+ }
141
+
142
+ /**
143
+ * Get the diff for a specific commit
144
+ */
145
+ async getCommitDiff(commitHash: string): Promise<string> {
146
+ try {
147
+ const { stdout } = await execFileAsync(
148
+ "git",
149
+ ["show", "--no-color", "-p", commitHash],
150
+ {
151
+ cwd: this.repoPath,
152
+ maxBuffer: GIT_MAX_BUFFER,
153
+ timeout: this.config.gitTimeout,
154
+ },
155
+ );
156
+
157
+ // Truncate diff if it exceeds maxDiffSize
158
+ if (stdout.length > this.config.maxDiffSize) {
159
+ return (
160
+ stdout.substring(0, this.config.maxDiffSize) +
161
+ `\n\n[diff truncated: showing ${this.config.maxDiffSize} of ${stdout.length} bytes]`
162
+ );
163
+ }
164
+
165
+ return stdout;
166
+ } catch {
167
+ return "";
168
+ }
169
+ }
170
+
171
+ /**
172
+ * Parse git log output into structured commits
173
+ */
174
+ private parseGitLog(output: string): RawCommit[] {
175
+ const commits: RawCommit[] = [];
176
+
177
+ // Split by commit delimiter
178
+ const commitBlocks = output.split(GIT_LOG_COMMIT_DELIMITER);
179
+
180
+ for (const block of commitBlocks) {
181
+ const trimmed = block.trim();
182
+ if (!trimmed) continue;
183
+
184
+ const commit = this.parseCommitBlock(trimmed);
185
+ if (commit) {
186
+ commits.push(commit);
187
+ }
188
+ }
189
+
190
+ return commits;
191
+ }
192
+
193
+ /**
194
+ * Parse a single commit block
195
+ */
196
+ private parseCommitBlock(block: string): RawCommit | null {
197
+ // The format line is first, followed by numstat output
198
+ const lines = block.split("\n");
199
+ if (lines.length === 0) return null;
200
+
201
+ // Parse the format line: hash|shortHash|author|authorEmail|date|subject|body
202
+ const formatLine = lines[0];
203
+ const parts = formatLine.split("|");
204
+
205
+ if (parts.length < 6) return null;
206
+
207
+ const [
208
+ hash,
209
+ shortHash,
210
+ author,
211
+ authorEmail,
212
+ dateStr,
213
+ subject,
214
+ ...bodyParts
215
+ ] = parts;
216
+
217
+ // Parse files and stats from numstat output
218
+ const { files, insertions, deletions } = this.parseNumstat(lines.slice(1));
219
+
220
+ return {
221
+ hash,
222
+ shortHash,
223
+ author,
224
+ authorEmail,
225
+ date: new Date(dateStr),
226
+ subject,
227
+ body: bodyParts.join("|").trim(), // Body might contain | characters
228
+ files,
229
+ insertions,
230
+ deletions,
231
+ };
232
+ }
233
+
234
+ /**
235
+ * Parse numstat output (lines after the format line)
236
+ */
237
+ private parseNumstat(lines: string[]): {
238
+ files: string[];
239
+ insertions: number;
240
+ deletions: number;
241
+ } {
242
+ const files: string[] = [];
243
+ let insertions = 0;
244
+ let deletions = 0;
245
+
246
+ for (const line of lines) {
247
+ const trimmed = line.trim();
248
+ if (!trimmed) continue;
249
+
250
+ // numstat format: insertions<tab>deletions<tab>filename
251
+ // Binary files show as "-" for insertions/deletions
252
+ const match = trimmed.match(/^(\d+|-)\s+(\d+|-)\s+(.+)$/);
253
+
254
+ if (match) {
255
+ const [, ins, del, filename] = match;
256
+
257
+ // Handle binary files (marked with -)
258
+ if (ins !== "-") {
259
+ insertions += parseInt(ins, 10);
260
+ }
261
+ if (del !== "-") {
262
+ deletions += parseInt(del, 10);
263
+ }
264
+
265
+ // Handle renamed files (old -> new)
266
+ if (filename.includes(" => ")) {
267
+ const renameParts = filename.match(/(.+)\{(.+) => (.+)\}(.+)?/);
268
+ if (renameParts) {
269
+ files.push(
270
+ `${renameParts[1]}${renameParts[3]}${renameParts[4] || ""}`,
271
+ );
272
+ } else {
273
+ const simpleRename = filename.split(" => ");
274
+ files.push(simpleRename[1] || filename);
275
+ }
276
+ } else {
277
+ files.push(filename);
278
+ }
279
+ }
280
+ }
281
+
282
+ return { files, insertions, deletions };
283
+ }
284
+ }
@@ -0,0 +1,31 @@
1
+ /**
2
+ * Git history indexing module - barrel export
3
+ */
4
+
5
+ // Types
6
+ export type {
7
+ CommitChunk,
8
+ CommitType,
9
+ GitChangeStats,
10
+ GitConfig,
11
+ GitExtractOptions,
12
+ GitIndexOptions,
13
+ GitIndexStats,
14
+ GitIndexStatus,
15
+ GitIndexingStatus,
16
+ GitProgressCallback,
17
+ GitProgressUpdate,
18
+ GitSearchOptions,
19
+ GitSearchResult,
20
+ GitSnapshot,
21
+ RawCommit,
22
+ } from "./types.js";
23
+
24
+ // Config
25
+ export { DEFAULT_GIT_CONFIG, COMMIT_TYPE_PATTERNS } from "./config.js";
26
+
27
+ // Main classes
28
+ export { GitHistoryIndexer } from "./indexer.js";
29
+ export { GitExtractor } from "./extractor.js";
30
+ export { CommitChunker } from "./chunker.js";
31
+ export { GitSynchronizer } from "./sync/synchronizer.js";