gitlab-mcp 1.1.0 → 1.2.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/LICENSE +21 -0
- package/dist/config/env.d.ts +56 -0
- package/dist/config/env.js +163 -0
- package/dist/config/env.js.map +1 -0
- package/dist/http-app.d.ts +45 -0
- package/dist/http-app.js +550 -0
- package/dist/http-app.js.map +1 -0
- package/dist/http.d.ts +2 -0
- package/dist/http.js +65 -0
- package/dist/http.js.map +1 -0
- package/dist/index.d.ts +2 -0
- package/dist/index.js +65 -0
- package/dist/index.js.map +1 -0
- package/dist/lib/auth-context.d.ts +9 -0
- package/dist/lib/auth-context.js +9 -0
- package/dist/lib/auth-context.js.map +1 -0
- package/dist/lib/gitlab-client.d.ts +331 -0
- package/dist/lib/gitlab-client.js +1025 -0
- package/dist/lib/gitlab-client.js.map +1 -0
- package/dist/lib/logger.d.ts +2 -0
- package/dist/lib/logger.js +13 -0
- package/dist/lib/logger.js.map +1 -0
- package/dist/lib/network.d.ts +3 -0
- package/dist/lib/network.js +38 -0
- package/dist/lib/network.js.map +1 -0
- package/dist/lib/oauth.d.ts +29 -0
- package/dist/lib/oauth.js +220 -0
- package/dist/lib/oauth.js.map +1 -0
- package/dist/lib/output.d.ts +14 -0
- package/dist/lib/output.js +38 -0
- package/dist/lib/output.js.map +1 -0
- package/dist/lib/policy.d.ts +25 -0
- package/dist/lib/policy.js +48 -0
- package/dist/lib/policy.js.map +1 -0
- package/dist/lib/request-runtime.d.ts +26 -0
- package/dist/lib/request-runtime.js +323 -0
- package/dist/lib/request-runtime.js.map +1 -0
- package/dist/lib/sanitize.d.ts +1 -0
- package/dist/lib/sanitize.js +21 -0
- package/dist/lib/sanitize.js.map +1 -0
- package/dist/lib/session-capacity.d.ts +8 -0
- package/dist/lib/session-capacity.js +7 -0
- package/dist/lib/session-capacity.js.map +1 -0
- package/dist/server/build-server.d.ts +3 -0
- package/dist/server/build-server.js +13 -0
- package/dist/server/build-server.js.map +1 -0
- package/dist/tools/gitlab.d.ts +9 -0
- package/dist/tools/gitlab.js +2576 -0
- package/dist/tools/gitlab.js.map +1 -0
- package/dist/tools/health.d.ts +2 -0
- package/dist/tools/health.js +21 -0
- package/dist/tools/health.js.map +1 -0
- package/dist/tools/mr-code-context.d.ts +38 -0
- package/dist/tools/mr-code-context.js +330 -0
- package/dist/tools/mr-code-context.js.map +1 -0
- package/{src/types/context.ts → dist/types/context.d.ts} +5 -6
- package/dist/types/context.js +2 -0
- package/dist/types/context.js.map +1 -0
- package/docs/configuration.md +6 -6
- package/docs/mcp-integration-testing-best-practices.md +981 -0
- package/package.json +13 -1
- package/.dockerignore +0 -7
- package/.editorconfig +0 -9
- package/.env.example +0 -75
- package/.github/workflows/nodejs.yml +0 -31
- package/.github/workflows/npm-publish.yml +0 -31
- package/.husky/pre-commit +0 -1
- package/.nvmrc +0 -1
- package/.prettierrc.json +0 -6
- package/Dockerfile +0 -20
- package/docker-compose.yml +0 -10
- package/eslint.config.js +0 -23
- package/scripts/get-oauth-token.example.sh +0 -15
- package/src/config/env.ts +0 -171
- package/src/http.ts +0 -620
- package/src/index.ts +0 -77
- package/src/lib/auth-context.ts +0 -19
- package/src/lib/gitlab-client.ts +0 -1810
- package/src/lib/logger.ts +0 -17
- package/src/lib/network.ts +0 -45
- package/src/lib/oauth.ts +0 -287
- package/src/lib/output.ts +0 -51
- package/src/lib/policy.ts +0 -78
- package/src/lib/request-runtime.ts +0 -376
- package/src/lib/sanitize.ts +0 -25
- package/src/lib/session-capacity.ts +0 -14
- package/src/server/build-server.ts +0 -17
- package/src/tools/gitlab.ts +0 -3135
- package/src/tools/health.ts +0 -27
- package/src/tools/mr-code-context.ts +0 -473
- package/tests/auth-context.test.ts +0 -102
- package/tests/gitlab-client.test.ts +0 -672
- package/tests/graphql-guard.test.ts +0 -121
- package/tests/integration/agent-loop.integration.test.ts +0 -558
- package/tests/integration/server.integration.test.ts +0 -543
- package/tests/mr-code-context.test.ts +0 -600
- package/tests/oauth.test.ts +0 -43
- package/tests/output.test.ts +0 -186
- package/tests/policy.test.ts +0 -324
- package/tests/request-runtime.test.ts +0 -252
- package/tests/sanitize.test.ts +0 -123
- package/tests/session-capacity.test.ts +0 -49
- package/tests/upload-reference.test.ts +0 -88
- package/tsconfig.build.json +0 -11
- package/tsconfig.json +0 -21
- package/vitest.config.ts +0 -12
package/src/tools/health.ts
DELETED
|
@@ -1,27 +0,0 @@
|
|
|
1
|
-
import type { McpServer } from "@modelcontextprotocol/sdk/server/mcp.js";
|
|
2
|
-
|
|
3
|
-
export function registerHealthTool(server: McpServer): void {
|
|
4
|
-
server.registerTool(
|
|
5
|
-
"health_check",
|
|
6
|
-
{
|
|
7
|
-
title: "Health Check",
|
|
8
|
-
description: "Return server liveness and current timestamp."
|
|
9
|
-
},
|
|
10
|
-
async () => {
|
|
11
|
-
const now = new Date().toISOString();
|
|
12
|
-
|
|
13
|
-
return {
|
|
14
|
-
content: [
|
|
15
|
-
{
|
|
16
|
-
type: "text" as const,
|
|
17
|
-
text: `ok (${now})`
|
|
18
|
-
}
|
|
19
|
-
],
|
|
20
|
-
structuredContent: {
|
|
21
|
-
status: "ok",
|
|
22
|
-
timestamp: now
|
|
23
|
-
}
|
|
24
|
-
};
|
|
25
|
-
}
|
|
26
|
-
);
|
|
27
|
-
}
|
|
@@ -1,473 +0,0 @@
|
|
|
1
|
-
import picomatch from "picomatch";
|
|
2
|
-
import { z } from "zod";
|
|
3
|
-
|
|
4
|
-
import type { AppContext } from "../types/context.js";
|
|
5
|
-
|
|
6
|
-
export const mergeRequestCodeContextSchema = {
|
|
7
|
-
project_id: z.string().optional(),
|
|
8
|
-
merge_request_iid: z.string().min(1),
|
|
9
|
-
include_paths: z.array(z.string()).optional(),
|
|
10
|
-
exclude_paths: z.array(z.string()).optional(),
|
|
11
|
-
extensions: z.array(z.string()).optional(),
|
|
12
|
-
languages: z.array(z.string()).optional(),
|
|
13
|
-
max_files: z.number().int().min(1).max(500).default(30),
|
|
14
|
-
max_total_chars: z.number().int().min(500).max(2_000_000).default(120_000),
|
|
15
|
-
context_lines: z.number().int().min(0).max(200).default(20),
|
|
16
|
-
mode: z.enum(["patch", "surrounding", "fullfile"]).default("patch"),
|
|
17
|
-
sort: z.enum(["changed_lines", "path", "file_size"]).default("changed_lines"),
|
|
18
|
-
list_only: z.boolean().default(false)
|
|
19
|
-
} as const;
|
|
20
|
-
|
|
21
|
-
interface MergeRequestInfo {
|
|
22
|
-
source_branch?: string;
|
|
23
|
-
target_branch?: string;
|
|
24
|
-
}
|
|
25
|
-
|
|
26
|
-
interface DiffFile {
|
|
27
|
-
old_path: string;
|
|
28
|
-
new_path: string;
|
|
29
|
-
new_file: boolean;
|
|
30
|
-
renamed_file: boolean;
|
|
31
|
-
deleted_file: boolean;
|
|
32
|
-
diff: string;
|
|
33
|
-
}
|
|
34
|
-
|
|
35
|
-
interface Budget {
|
|
36
|
-
maxChars: number;
|
|
37
|
-
usedChars: number;
|
|
38
|
-
}
|
|
39
|
-
|
|
40
|
-
export async function getMergeRequestCodeContext(
|
|
41
|
-
args: {
|
|
42
|
-
projectId: string;
|
|
43
|
-
mergeRequestIid: string;
|
|
44
|
-
includePaths?: string[];
|
|
45
|
-
excludePaths?: string[];
|
|
46
|
-
extensions?: string[];
|
|
47
|
-
languages?: string[];
|
|
48
|
-
maxFiles: number;
|
|
49
|
-
maxTotalChars: number;
|
|
50
|
-
contextLines: number;
|
|
51
|
-
mode: "patch" | "surrounding" | "fullfile";
|
|
52
|
-
sort: "changed_lines" | "path" | "file_size";
|
|
53
|
-
listOnly: boolean;
|
|
54
|
-
},
|
|
55
|
-
context: AppContext
|
|
56
|
-
): Promise<Record<string, unknown>> {
|
|
57
|
-
const mergeRequest = (await context.gitlab.getMergeRequest(
|
|
58
|
-
args.projectId,
|
|
59
|
-
args.mergeRequestIid
|
|
60
|
-
)) as MergeRequestInfo;
|
|
61
|
-
const diffResponse = await context.gitlab.getMergeRequestDiffs(
|
|
62
|
-
args.projectId,
|
|
63
|
-
args.mergeRequestIid
|
|
64
|
-
);
|
|
65
|
-
const diffFiles = extractDiffFiles(diffResponse);
|
|
66
|
-
|
|
67
|
-
const filtered = filterDiffFiles(diffFiles, {
|
|
68
|
-
includePaths: args.includePaths,
|
|
69
|
-
excludePaths: args.excludePaths,
|
|
70
|
-
extensions: args.extensions,
|
|
71
|
-
languages: args.languages
|
|
72
|
-
});
|
|
73
|
-
|
|
74
|
-
const sorted = sortDiffFiles(filtered, args.sort);
|
|
75
|
-
const selected = sorted.slice(0, args.maxFiles);
|
|
76
|
-
|
|
77
|
-
if (args.listOnly) {
|
|
78
|
-
return {
|
|
79
|
-
project_id: args.projectId,
|
|
80
|
-
merge_request_iid: args.mergeRequestIid,
|
|
81
|
-
mode: args.mode,
|
|
82
|
-
list_only: true,
|
|
83
|
-
total_files: diffFiles.length,
|
|
84
|
-
selected_files: selected.length,
|
|
85
|
-
files: selected.map((file) => summarizeFile(file))
|
|
86
|
-
};
|
|
87
|
-
}
|
|
88
|
-
|
|
89
|
-
const ref = mergeRequest.source_branch ?? "main";
|
|
90
|
-
const budget: Budget = {
|
|
91
|
-
maxChars: args.maxTotalChars,
|
|
92
|
-
usedChars: 0
|
|
93
|
-
};
|
|
94
|
-
|
|
95
|
-
const files: Array<Record<string, unknown>> = [];
|
|
96
|
-
|
|
97
|
-
for (const file of selected) {
|
|
98
|
-
const item = await buildFilePayload(file, {
|
|
99
|
-
mode: args.mode,
|
|
100
|
-
contextLines: args.contextLines,
|
|
101
|
-
projectId: args.projectId,
|
|
102
|
-
ref,
|
|
103
|
-
gitlab: context.gitlab,
|
|
104
|
-
budget
|
|
105
|
-
});
|
|
106
|
-
|
|
107
|
-
if (item) {
|
|
108
|
-
files.push(item);
|
|
109
|
-
}
|
|
110
|
-
|
|
111
|
-
if (budget.usedChars >= budget.maxChars) {
|
|
112
|
-
break;
|
|
113
|
-
}
|
|
114
|
-
}
|
|
115
|
-
|
|
116
|
-
return {
|
|
117
|
-
project_id: args.projectId,
|
|
118
|
-
merge_request_iid: args.mergeRequestIid,
|
|
119
|
-
source_branch: mergeRequest.source_branch,
|
|
120
|
-
target_branch: mergeRequest.target_branch,
|
|
121
|
-
mode: args.mode,
|
|
122
|
-
total_files: diffFiles.length,
|
|
123
|
-
filtered_files: filtered.length,
|
|
124
|
-
selected_files: selected.length,
|
|
125
|
-
returned_files: files.length,
|
|
126
|
-
budget: {
|
|
127
|
-
max_total_chars: budget.maxChars,
|
|
128
|
-
used_chars: budget.usedChars,
|
|
129
|
-
remaining_chars: Math.max(0, budget.maxChars - budget.usedChars)
|
|
130
|
-
},
|
|
131
|
-
files
|
|
132
|
-
};
|
|
133
|
-
}
|
|
134
|
-
|
|
135
|
-
function extractDiffFiles(response: unknown): DiffFile[] {
|
|
136
|
-
if (Array.isArray(response)) {
|
|
137
|
-
return response as DiffFile[];
|
|
138
|
-
}
|
|
139
|
-
|
|
140
|
-
if (typeof response === "object" && response !== null) {
|
|
141
|
-
const record = response as Record<string, unknown>;
|
|
142
|
-
if (Array.isArray(record.changes)) {
|
|
143
|
-
return record.changes as DiffFile[];
|
|
144
|
-
}
|
|
145
|
-
if (Array.isArray(record.diffs)) {
|
|
146
|
-
return record.diffs as DiffFile[];
|
|
147
|
-
}
|
|
148
|
-
}
|
|
149
|
-
|
|
150
|
-
return [];
|
|
151
|
-
}
|
|
152
|
-
|
|
153
|
-
function filterDiffFiles(
|
|
154
|
-
files: DiffFile[],
|
|
155
|
-
options: {
|
|
156
|
-
includePaths?: string[];
|
|
157
|
-
excludePaths?: string[];
|
|
158
|
-
extensions?: string[];
|
|
159
|
-
languages?: string[];
|
|
160
|
-
}
|
|
161
|
-
): DiffFile[] {
|
|
162
|
-
const includeMatchers = (options.includePaths ?? []).map((pattern) => picomatch(pattern));
|
|
163
|
-
const excludeMatchers = (options.excludePaths ?? []).map((pattern) => picomatch(pattern));
|
|
164
|
-
const extensionSet = new Set((options.extensions ?? []).map((item) => normalizeExtension(item)));
|
|
165
|
-
const languageExtSet = new Set(
|
|
166
|
-
(options.languages ?? []).flatMap((language) => languageToExtensions(language.toLowerCase()))
|
|
167
|
-
);
|
|
168
|
-
|
|
169
|
-
return files.filter((file) => {
|
|
170
|
-
const path = file.new_path || file.old_path;
|
|
171
|
-
|
|
172
|
-
if (includeMatchers.length > 0 && !includeMatchers.some((matcher) => matcher(path))) {
|
|
173
|
-
return false;
|
|
174
|
-
}
|
|
175
|
-
|
|
176
|
-
if (excludeMatchers.some((matcher) => matcher(path))) {
|
|
177
|
-
return false;
|
|
178
|
-
}
|
|
179
|
-
|
|
180
|
-
const extension = extractExtension(path);
|
|
181
|
-
|
|
182
|
-
if (extensionSet.size > 0 && !extensionSet.has(extension)) {
|
|
183
|
-
return false;
|
|
184
|
-
}
|
|
185
|
-
|
|
186
|
-
if (languageExtSet.size > 0 && !languageExtSet.has(extension)) {
|
|
187
|
-
return false;
|
|
188
|
-
}
|
|
189
|
-
|
|
190
|
-
return true;
|
|
191
|
-
});
|
|
192
|
-
}
|
|
193
|
-
|
|
194
|
-
function sortDiffFiles(
|
|
195
|
-
files: DiffFile[],
|
|
196
|
-
sortMode: "changed_lines" | "path" | "file_size"
|
|
197
|
-
): DiffFile[] {
|
|
198
|
-
const cloned = [...files];
|
|
199
|
-
|
|
200
|
-
switch (sortMode) {
|
|
201
|
-
case "path":
|
|
202
|
-
cloned.sort((a, b) => (a.new_path || a.old_path).localeCompare(b.new_path || b.old_path));
|
|
203
|
-
return cloned;
|
|
204
|
-
case "file_size":
|
|
205
|
-
cloned.sort((a, b) => (b.diff?.length ?? 0) - (a.diff?.length ?? 0));
|
|
206
|
-
return cloned;
|
|
207
|
-
case "changed_lines":
|
|
208
|
-
default:
|
|
209
|
-
cloned.sort((a, b) => countChangedLines(b.diff) - countChangedLines(a.diff));
|
|
210
|
-
return cloned;
|
|
211
|
-
}
|
|
212
|
-
}
|
|
213
|
-
|
|
214
|
-
async function buildFilePayload(
|
|
215
|
-
file: DiffFile,
|
|
216
|
-
options: {
|
|
217
|
-
mode: "patch" | "surrounding" | "fullfile";
|
|
218
|
-
contextLines: number;
|
|
219
|
-
projectId: string;
|
|
220
|
-
ref: string;
|
|
221
|
-
gitlab: AppContext["gitlab"];
|
|
222
|
-
budget: Budget;
|
|
223
|
-
}
|
|
224
|
-
): Promise<Record<string, unknown> | undefined> {
|
|
225
|
-
const summary = summarizeFile(file);
|
|
226
|
-
|
|
227
|
-
if (options.mode === "patch" || file.deleted_file) {
|
|
228
|
-
const taken = takeWithinBudget(file.diff ?? "", options.budget);
|
|
229
|
-
return {
|
|
230
|
-
...summary,
|
|
231
|
-
content: taken.value,
|
|
232
|
-
content_mode: "patch",
|
|
233
|
-
truncated: taken.truncated
|
|
234
|
-
};
|
|
235
|
-
}
|
|
236
|
-
|
|
237
|
-
const rawFile = (await options.gitlab.getFileContents(
|
|
238
|
-
options.projectId,
|
|
239
|
-
file.new_path,
|
|
240
|
-
options.ref
|
|
241
|
-
)) as Record<string, unknown>;
|
|
242
|
-
const decoded = decodeGitLabFileContent(rawFile);
|
|
243
|
-
|
|
244
|
-
if (options.mode === "fullfile") {
|
|
245
|
-
const taken = takeWithinBudget(decoded, options.budget);
|
|
246
|
-
return {
|
|
247
|
-
...summary,
|
|
248
|
-
content: taken.value,
|
|
249
|
-
content_mode: "fullfile",
|
|
250
|
-
truncated: taken.truncated
|
|
251
|
-
};
|
|
252
|
-
}
|
|
253
|
-
|
|
254
|
-
const changedLines = extractChangedNewLines(file.diff ?? "");
|
|
255
|
-
const snippets = extractSurroundingSnippets(decoded, changedLines, options.contextLines);
|
|
256
|
-
const payload = snippets
|
|
257
|
-
.map((snippet) => `@@ ${snippet.start}-${snippet.end} @@\n${snippet.content}`)
|
|
258
|
-
.join("\n\n");
|
|
259
|
-
const taken = takeWithinBudget(payload, options.budget);
|
|
260
|
-
|
|
261
|
-
return {
|
|
262
|
-
...summary,
|
|
263
|
-
content: taken.value,
|
|
264
|
-
content_mode: "surrounding",
|
|
265
|
-
snippet_windows: snippets.map((item) => ({ start: item.start, end: item.end })),
|
|
266
|
-
truncated: taken.truncated
|
|
267
|
-
};
|
|
268
|
-
}
|
|
269
|
-
|
|
270
|
-
function summarizeFile(file: DiffFile): Record<string, unknown> {
|
|
271
|
-
return {
|
|
272
|
-
old_path: file.old_path,
|
|
273
|
-
new_path: file.new_path,
|
|
274
|
-
new_file: file.new_file,
|
|
275
|
-
renamed_file: file.renamed_file,
|
|
276
|
-
deleted_file: file.deleted_file,
|
|
277
|
-
changed_lines: countChangedLines(file.diff)
|
|
278
|
-
};
|
|
279
|
-
}
|
|
280
|
-
|
|
281
|
-
function countChangedLines(diff: string | undefined): number {
|
|
282
|
-
if (!diff) {
|
|
283
|
-
return 0;
|
|
284
|
-
}
|
|
285
|
-
|
|
286
|
-
let count = 0;
|
|
287
|
-
|
|
288
|
-
for (const line of diff.split("\n")) {
|
|
289
|
-
if (line.startsWith("+") && !line.startsWith("+++")) {
|
|
290
|
-
count += 1;
|
|
291
|
-
continue;
|
|
292
|
-
}
|
|
293
|
-
|
|
294
|
-
if (line.startsWith("-") && !line.startsWith("---")) {
|
|
295
|
-
count += 1;
|
|
296
|
-
}
|
|
297
|
-
}
|
|
298
|
-
|
|
299
|
-
return count;
|
|
300
|
-
}
|
|
301
|
-
|
|
302
|
-
function extractChangedNewLines(diff: string): number[] {
|
|
303
|
-
const lines = diff.split("\n");
|
|
304
|
-
const changed = new Set<number>();
|
|
305
|
-
let currentNewLine = 0;
|
|
306
|
-
|
|
307
|
-
for (const line of lines) {
|
|
308
|
-
if (line.startsWith("@@")) {
|
|
309
|
-
const match = /\+([0-9]+)(?:,([0-9]+))?/.exec(line);
|
|
310
|
-
if (match?.[1]) {
|
|
311
|
-
currentNewLine = Number.parseInt(match[1], 10);
|
|
312
|
-
}
|
|
313
|
-
continue;
|
|
314
|
-
}
|
|
315
|
-
|
|
316
|
-
if (line.startsWith("+") && !line.startsWith("+++")) {
|
|
317
|
-
changed.add(currentNewLine);
|
|
318
|
-
currentNewLine += 1;
|
|
319
|
-
continue;
|
|
320
|
-
}
|
|
321
|
-
|
|
322
|
-
if (line.startsWith("-") && !line.startsWith("---")) {
|
|
323
|
-
continue;
|
|
324
|
-
}
|
|
325
|
-
|
|
326
|
-
currentNewLine += 1;
|
|
327
|
-
}
|
|
328
|
-
|
|
329
|
-
return [...changed].sort((a, b) => a - b);
|
|
330
|
-
}
|
|
331
|
-
|
|
332
|
-
function extractSurroundingSnippets(
|
|
333
|
-
source: string,
|
|
334
|
-
changedLines: number[],
|
|
335
|
-
contextLines: number
|
|
336
|
-
): Array<{ start: number; end: number; content: string }> {
|
|
337
|
-
const sourceLines = source.split("\n");
|
|
338
|
-
|
|
339
|
-
if (changedLines.length === 0) {
|
|
340
|
-
return [
|
|
341
|
-
{
|
|
342
|
-
start: 1,
|
|
343
|
-
end: Math.min(sourceLines.length, contextLines * 2 + 1),
|
|
344
|
-
content: sourceLines.slice(0, Math.min(sourceLines.length, contextLines * 2 + 1)).join("\n")
|
|
345
|
-
}
|
|
346
|
-
];
|
|
347
|
-
}
|
|
348
|
-
|
|
349
|
-
const windows = mergeWindows(
|
|
350
|
-
changedLines.map((lineNumber) => ({
|
|
351
|
-
start: Math.max(1, lineNumber - contextLines),
|
|
352
|
-
end: Math.min(sourceLines.length, lineNumber + contextLines)
|
|
353
|
-
}))
|
|
354
|
-
);
|
|
355
|
-
|
|
356
|
-
return windows.map((window) => ({
|
|
357
|
-
start: window.start,
|
|
358
|
-
end: window.end,
|
|
359
|
-
content: sourceLines.slice(window.start - 1, window.end).join("\n")
|
|
360
|
-
}));
|
|
361
|
-
}
|
|
362
|
-
|
|
363
|
-
function mergeWindows(
|
|
364
|
-
windows: Array<{ start: number; end: number }>
|
|
365
|
-
): Array<{ start: number; end: number }> {
|
|
366
|
-
if (windows.length === 0) {
|
|
367
|
-
return [];
|
|
368
|
-
}
|
|
369
|
-
|
|
370
|
-
const sorted = [...windows].sort((a, b) => a.start - b.start);
|
|
371
|
-
const first = sorted[0];
|
|
372
|
-
if (!first) {
|
|
373
|
-
return [];
|
|
374
|
-
}
|
|
375
|
-
|
|
376
|
-
const merged: Array<{ start: number; end: number }> = [first];
|
|
377
|
-
|
|
378
|
-
for (const window of sorted.slice(1)) {
|
|
379
|
-
const last = merged[merged.length - 1];
|
|
380
|
-
if (!last) {
|
|
381
|
-
merged.push(window);
|
|
382
|
-
continue;
|
|
383
|
-
}
|
|
384
|
-
|
|
385
|
-
if (window.start <= last.end + 1) {
|
|
386
|
-
last.end = Math.max(last.end, window.end);
|
|
387
|
-
continue;
|
|
388
|
-
}
|
|
389
|
-
|
|
390
|
-
merged.push(window);
|
|
391
|
-
}
|
|
392
|
-
|
|
393
|
-
return merged;
|
|
394
|
-
}
|
|
395
|
-
|
|
396
|
-
function decodeGitLabFileContent(raw: Record<string, unknown>): string {
|
|
397
|
-
const content = typeof raw.content === "string" ? raw.content : "";
|
|
398
|
-
const encoding = typeof raw.encoding === "string" ? raw.encoding.toLowerCase() : "";
|
|
399
|
-
|
|
400
|
-
if (encoding === "base64") {
|
|
401
|
-
return Buffer.from(content, "base64").toString("utf8");
|
|
402
|
-
}
|
|
403
|
-
|
|
404
|
-
return content;
|
|
405
|
-
}
|
|
406
|
-
|
|
407
|
-
function takeWithinBudget(value: string, budget: Budget): { value: string; truncated: boolean } {
|
|
408
|
-
const remaining = Math.max(0, budget.maxChars - budget.usedChars);
|
|
409
|
-
|
|
410
|
-
if (remaining <= 0) {
|
|
411
|
-
return {
|
|
412
|
-
value: "",
|
|
413
|
-
truncated: true
|
|
414
|
-
};
|
|
415
|
-
}
|
|
416
|
-
|
|
417
|
-
if (value.length <= remaining) {
|
|
418
|
-
budget.usedChars += value.length;
|
|
419
|
-
return {
|
|
420
|
-
value,
|
|
421
|
-
truncated: false
|
|
422
|
-
};
|
|
423
|
-
}
|
|
424
|
-
|
|
425
|
-
budget.usedChars = budget.maxChars;
|
|
426
|
-
return {
|
|
427
|
-
value: `${value.slice(0, remaining)}\n... [truncated]`,
|
|
428
|
-
truncated: true
|
|
429
|
-
};
|
|
430
|
-
}
|
|
431
|
-
|
|
432
|
-
function normalizeExtension(extension: string): string {
|
|
433
|
-
const cleaned = extension.trim().toLowerCase();
|
|
434
|
-
if (cleaned.length === 0) {
|
|
435
|
-
return "";
|
|
436
|
-
}
|
|
437
|
-
|
|
438
|
-
return cleaned.startsWith(".") ? cleaned : `.${cleaned}`;
|
|
439
|
-
}
|
|
440
|
-
|
|
441
|
-
function extractExtension(path: string): string {
|
|
442
|
-
const match = /\.([^./]+)$/.exec(path.toLowerCase());
|
|
443
|
-
if (!match?.[1]) {
|
|
444
|
-
return "";
|
|
445
|
-
}
|
|
446
|
-
|
|
447
|
-
return `.${match[1]}`;
|
|
448
|
-
}
|
|
449
|
-
|
|
450
|
-
function languageToExtensions(language: string): string[] {
|
|
451
|
-
const map: Record<string, string[]> = {
|
|
452
|
-
typescript: [".ts", ".tsx", ".mts", ".cts"],
|
|
453
|
-
javascript: [".js", ".jsx", ".mjs", ".cjs"],
|
|
454
|
-
python: [".py"],
|
|
455
|
-
go: [".go"],
|
|
456
|
-
rust: [".rs"],
|
|
457
|
-
java: [".java"],
|
|
458
|
-
kotlin: [".kt", ".kts"],
|
|
459
|
-
csharp: [".cs"],
|
|
460
|
-
cpp: [".cpp", ".cc", ".cxx", ".hpp", ".hh", ".hxx"],
|
|
461
|
-
c: [".c", ".h"],
|
|
462
|
-
ruby: [".rb"],
|
|
463
|
-
php: [".php"],
|
|
464
|
-
swift: [".swift"],
|
|
465
|
-
scala: [".scala"],
|
|
466
|
-
shell: [".sh", ".bash", ".zsh"],
|
|
467
|
-
yaml: [".yaml", ".yml"],
|
|
468
|
-
json: [".json"],
|
|
469
|
-
markdown: [".md", ".markdown"]
|
|
470
|
-
};
|
|
471
|
-
|
|
472
|
-
return map[language] ?? [];
|
|
473
|
-
}
|
|
@@ -1,102 +0,0 @@
|
|
|
1
|
-
import { describe, expect, it } from "vitest";
|
|
2
|
-
|
|
3
|
-
import { getSessionAuth, runWithSessionAuth, type SessionAuth } from "../src/lib/auth-context.js";
|
|
4
|
-
|
|
5
|
-
describe("auth-context", () => {
|
|
6
|
-
describe("runWithSessionAuth", () => {
|
|
7
|
-
it("provides auth data within callback", () => {
|
|
8
|
-
const auth: SessionAuth = {
|
|
9
|
-
sessionId: "sess-1",
|
|
10
|
-
token: "my-token",
|
|
11
|
-
apiUrl: "https://gitlab.example.com/api/v4",
|
|
12
|
-
header: "private-token",
|
|
13
|
-
updatedAt: Date.now()
|
|
14
|
-
};
|
|
15
|
-
|
|
16
|
-
runWithSessionAuth(auth, () => {
|
|
17
|
-
const retrieved = getSessionAuth();
|
|
18
|
-
expect(retrieved).toEqual(auth);
|
|
19
|
-
});
|
|
20
|
-
});
|
|
21
|
-
|
|
22
|
-
it("returns undefined outside of context", () => {
|
|
23
|
-
expect(getSessionAuth()).toBeUndefined();
|
|
24
|
-
});
|
|
25
|
-
|
|
26
|
-
it("supports undefined auth", () => {
|
|
27
|
-
runWithSessionAuth(undefined, () => {
|
|
28
|
-
expect(getSessionAuth()).toBeUndefined();
|
|
29
|
-
});
|
|
30
|
-
});
|
|
31
|
-
|
|
32
|
-
it("returns the callback result", () => {
|
|
33
|
-
const result = runWithSessionAuth(undefined, () => "hello");
|
|
34
|
-
expect(result).toBe("hello");
|
|
35
|
-
});
|
|
36
|
-
|
|
37
|
-
it("supports nested contexts", () => {
|
|
38
|
-
const outerAuth: SessionAuth = {
|
|
39
|
-
token: "outer-token",
|
|
40
|
-
updatedAt: Date.now()
|
|
41
|
-
};
|
|
42
|
-
|
|
43
|
-
const innerAuth: SessionAuth = {
|
|
44
|
-
token: "inner-token",
|
|
45
|
-
updatedAt: Date.now()
|
|
46
|
-
};
|
|
47
|
-
|
|
48
|
-
runWithSessionAuth(outerAuth, () => {
|
|
49
|
-
expect(getSessionAuth()?.token).toBe("outer-token");
|
|
50
|
-
|
|
51
|
-
runWithSessionAuth(innerAuth, () => {
|
|
52
|
-
expect(getSessionAuth()?.token).toBe("inner-token");
|
|
53
|
-
});
|
|
54
|
-
|
|
55
|
-
// Outer context is restored
|
|
56
|
-
expect(getSessionAuth()?.token).toBe("outer-token");
|
|
57
|
-
});
|
|
58
|
-
});
|
|
59
|
-
|
|
60
|
-
it("isolates context between concurrent async operations", async () => {
|
|
61
|
-
const results: string[] = [];
|
|
62
|
-
|
|
63
|
-
const task1 = new Promise<void>((resolve) => {
|
|
64
|
-
runWithSessionAuth({ token: "token-a", updatedAt: 1 }, () => {
|
|
65
|
-
setTimeout(() => {
|
|
66
|
-
runWithSessionAuth({ token: "token-a", updatedAt: 1 }, () => {
|
|
67
|
-
results.push(getSessionAuth()?.token ?? "none");
|
|
68
|
-
resolve();
|
|
69
|
-
});
|
|
70
|
-
}, 10);
|
|
71
|
-
});
|
|
72
|
-
});
|
|
73
|
-
|
|
74
|
-
const task2 = new Promise<void>((resolve) => {
|
|
75
|
-
runWithSessionAuth({ token: "token-b", updatedAt: 2 }, () => {
|
|
76
|
-
results.push(getSessionAuth()?.token ?? "none");
|
|
77
|
-
resolve();
|
|
78
|
-
});
|
|
79
|
-
});
|
|
80
|
-
|
|
81
|
-
await Promise.all([task1, task2]);
|
|
82
|
-
|
|
83
|
-
expect(results).toContain("token-a");
|
|
84
|
-
expect(results).toContain("token-b");
|
|
85
|
-
});
|
|
86
|
-
|
|
87
|
-
it("handles auth with minimal fields", () => {
|
|
88
|
-
const auth: SessionAuth = {
|
|
89
|
-
updatedAt: Date.now()
|
|
90
|
-
};
|
|
91
|
-
|
|
92
|
-
runWithSessionAuth(auth, () => {
|
|
93
|
-
const retrieved = getSessionAuth();
|
|
94
|
-
expect(retrieved?.token).toBeUndefined();
|
|
95
|
-
expect(retrieved?.apiUrl).toBeUndefined();
|
|
96
|
-
expect(retrieved?.header).toBeUndefined();
|
|
97
|
-
expect(retrieved?.sessionId).toBeUndefined();
|
|
98
|
-
expect(retrieved?.updatedAt).toBeDefined();
|
|
99
|
-
});
|
|
100
|
-
});
|
|
101
|
-
});
|
|
102
|
-
});
|