@mars167/git-ai 2.3.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 (122) hide show
  1. package/LICENSE +22 -0
  2. package/README.md +364 -0
  3. package/README.zh-CN.md +361 -0
  4. package/assets/hooks/post-checkout +28 -0
  5. package/assets/hooks/post-merge +28 -0
  6. package/assets/hooks/pre-commit +17 -0
  7. package/assets/hooks/pre-push +29 -0
  8. package/dist/bin/git-ai.js +62 -0
  9. package/dist/src/commands/ai.js +30 -0
  10. package/dist/src/commands/checkIndex.js +19 -0
  11. package/dist/src/commands/dsr.js +156 -0
  12. package/dist/src/commands/graph.js +203 -0
  13. package/dist/src/commands/hooks.js +125 -0
  14. package/dist/src/commands/index.js +92 -0
  15. package/dist/src/commands/pack.js +31 -0
  16. package/dist/src/commands/query.js +139 -0
  17. package/dist/src/commands/semantic.js +134 -0
  18. package/dist/src/commands/serve.js +14 -0
  19. package/dist/src/commands/status.js +78 -0
  20. package/dist/src/commands/trae.js +75 -0
  21. package/dist/src/commands/unpack.js +28 -0
  22. package/dist/src/core/archive.js +91 -0
  23. package/dist/src/core/astGraph.js +127 -0
  24. package/dist/src/core/astGraphQuery.js +142 -0
  25. package/dist/src/core/cozo.js +266 -0
  26. package/dist/src/core/cpg/astLayer.js +56 -0
  27. package/dist/src/core/cpg/callGraph.js +483 -0
  28. package/dist/src/core/cpg/cfgLayer.js +490 -0
  29. package/dist/src/core/cpg/dfgLayer.js +237 -0
  30. package/dist/src/core/cpg/index.js +80 -0
  31. package/dist/src/core/cpg/types.js +108 -0
  32. package/dist/src/core/crypto.js +10 -0
  33. package/dist/src/core/dsr/generate.js +308 -0
  34. package/dist/src/core/dsr/gitContext.js +74 -0
  35. package/dist/src/core/dsr/indexMaterialize.js +106 -0
  36. package/dist/src/core/dsr/paths.js +26 -0
  37. package/dist/src/core/dsr/query.js +73 -0
  38. package/dist/src/core/dsr/snapshotParser.js +73 -0
  39. package/dist/src/core/dsr/state.js +27 -0
  40. package/dist/src/core/dsr/types.js +2 -0
  41. package/dist/src/core/embedding/fusion.js +52 -0
  42. package/dist/src/core/embedding/index.js +43 -0
  43. package/dist/src/core/embedding/parser.js +14 -0
  44. package/dist/src/core/embedding/semantic.js +254 -0
  45. package/dist/src/core/embedding/structural.js +97 -0
  46. package/dist/src/core/embedding/symbolic.js +117 -0
  47. package/dist/src/core/embedding/tokenizer.js +91 -0
  48. package/dist/src/core/embedding/types.js +2 -0
  49. package/dist/src/core/embedding.js +36 -0
  50. package/dist/src/core/git.js +49 -0
  51. package/dist/src/core/gitDiff.js +73 -0
  52. package/dist/src/core/indexCheck.js +131 -0
  53. package/dist/src/core/indexer.js +185 -0
  54. package/dist/src/core/indexerIncremental.js +303 -0
  55. package/dist/src/core/indexing/config.js +51 -0
  56. package/dist/src/core/indexing/hnsw.js +568 -0
  57. package/dist/src/core/indexing/index.js +17 -0
  58. package/dist/src/core/indexing/monitor.js +82 -0
  59. package/dist/src/core/indexing/parallel.js +252 -0
  60. package/dist/src/core/lancedb.js +111 -0
  61. package/dist/src/core/lfs.js +27 -0
  62. package/dist/src/core/log.js +62 -0
  63. package/dist/src/core/manifest.js +88 -0
  64. package/dist/src/core/parser/adapter.js +2 -0
  65. package/dist/src/core/parser/c.js +93 -0
  66. package/dist/src/core/parser/chunkRelations.js +178 -0
  67. package/dist/src/core/parser/chunker.js +274 -0
  68. package/dist/src/core/parser/go.js +98 -0
  69. package/dist/src/core/parser/java.js +80 -0
  70. package/dist/src/core/parser/markdown.js +76 -0
  71. package/dist/src/core/parser/python.js +81 -0
  72. package/dist/src/core/parser/rust.js +103 -0
  73. package/dist/src/core/parser/typescript.js +98 -0
  74. package/dist/src/core/parser/utils.js +62 -0
  75. package/dist/src/core/parser/yaml.js +53 -0
  76. package/dist/src/core/parser.js +75 -0
  77. package/dist/src/core/paths.js +10 -0
  78. package/dist/src/core/repoMap.js +164 -0
  79. package/dist/src/core/retrieval/cache.js +31 -0
  80. package/dist/src/core/retrieval/classifier.js +74 -0
  81. package/dist/src/core/retrieval/expander.js +80 -0
  82. package/dist/src/core/retrieval/fuser.js +40 -0
  83. package/dist/src/core/retrieval/index.js +32 -0
  84. package/dist/src/core/retrieval/reranker.js +304 -0
  85. package/dist/src/core/retrieval/types.js +2 -0
  86. package/dist/src/core/retrieval/weights.js +42 -0
  87. package/dist/src/core/search.js +41 -0
  88. package/dist/src/core/sq8.js +65 -0
  89. package/dist/src/core/symbolSearch.js +143 -0
  90. package/dist/src/core/types.js +2 -0
  91. package/dist/src/core/workspace.js +116 -0
  92. package/dist/src/mcp/server.js +794 -0
  93. package/docs/README.md +44 -0
  94. package/docs/cross-encoder.md +157 -0
  95. package/docs/embedding.md +158 -0
  96. package/docs/logo.png +0 -0
  97. package/docs/windows-setup.md +67 -0
  98. package/docs/zh-CN/DESIGN.md +102 -0
  99. package/docs/zh-CN/README.md +46 -0
  100. package/docs/zh-CN/advanced.md +26 -0
  101. package/docs/zh-CN/architecture_explained.md +116 -0
  102. package/docs/zh-CN/cli.md +109 -0
  103. package/docs/zh-CN/dsr.md +91 -0
  104. package/docs/zh-CN/graph_scenarios.md +173 -0
  105. package/docs/zh-CN/hooks.md +14 -0
  106. package/docs/zh-CN/manifests.md +136 -0
  107. package/docs/zh-CN/mcp.md +205 -0
  108. package/docs/zh-CN/quickstart.md +35 -0
  109. package/docs/zh-CN/rules.md +7 -0
  110. package/docs/zh-CN/technical-details.md +454 -0
  111. package/docs/zh-CN/troubleshooting.md +19 -0
  112. package/docs/zh-CN/windows-setup.md +67 -0
  113. package/install.sh +183 -0
  114. package/package.json +97 -0
  115. package/skills/git-ai-mcp/SKILL.md +86 -0
  116. package/skills/git-ai-mcp/references/constraints.md +143 -0
  117. package/skills/git-ai-mcp/references/tools.md +263 -0
  118. package/templates/agents/common/documents/Fix EISDIR error and enable multi-language indexing.md +14 -0
  119. package/templates/agents/common/documents/Fix git-ai index error in CodaGraph directory.md +13 -0
  120. package/templates/agents/common/skills/git-ai-mcp/SKILL.md +86 -0
  121. package/templates/agents/common/skills/git-ai-mcp/references/constraints.md +143 -0
  122. package/templates/agents/common/skills/git-ai-mcp/references/tools.md +263 -0
@@ -0,0 +1,794 @@
1
+ "use strict";
2
+ var __importDefault = (this && this.__importDefault) || function (mod) {
3
+ return (mod && mod.__esModule) ? mod : { "default": mod };
4
+ };
5
+ Object.defineProperty(exports, "__esModule", { value: true });
6
+ exports.GitAIV2MCPServer = void 0;
7
+ const index_js_1 = require("@modelcontextprotocol/sdk/server/index.js");
8
+ const stdio_js_1 = require("@modelcontextprotocol/sdk/server/stdio.js");
9
+ const types_js_1 = require("@modelcontextprotocol/sdk/types.js");
10
+ const fs_extra_1 = __importDefault(require("fs-extra"));
11
+ const path_1 = __importDefault(require("path"));
12
+ const os_1 = __importDefault(require("os"));
13
+ const glob_1 = require("glob");
14
+ const git_1 = require("../core/git");
15
+ const archive_1 = require("../core/archive");
16
+ const lancedb_1 = require("../core/lancedb");
17
+ const lfs_1 = require("../core/lfs");
18
+ const search_1 = require("../core/search");
19
+ const indexer_1 = require("../core/indexer");
20
+ const workspace_1 = require("../core/workspace");
21
+ const astGraphQuery_1 = require("../core/astGraphQuery");
22
+ const symbolSearch_1 = require("../core/symbolSearch");
23
+ const crypto_1 = require("../core/crypto");
24
+ const paths_1 = require("../core/paths");
25
+ const log_1 = require("../core/log");
26
+ const indexCheck_1 = require("../core/indexCheck");
27
+ const repoMap_1 = require("../core/repoMap");
28
+ const gitContext_1 = require("../core/dsr/gitContext");
29
+ const generate_1 = require("../core/dsr/generate");
30
+ const indexMaterialize_1 = require("../core/dsr/indexMaterialize");
31
+ const query_1 = require("../core/dsr/query");
32
+ const state_1 = require("../core/dsr/state");
33
+ class GitAIV2MCPServer {
34
+ constructor(startDir, options = {}) {
35
+ this.startDir = path_1.default.resolve(startDir);
36
+ this.options = options;
37
+ this.server = new index_js_1.Server({ name: 'git-ai-v2', version: '2.0.0' }, { capabilities: { tools: {} } });
38
+ this.setupHandlers();
39
+ }
40
+ async openRepoContext(startDir) {
41
+ const repoRoot = await (0, git_1.resolveGitRoot)(path_1.default.resolve(startDir ?? this.startDir));
42
+ const metaPath = path_1.default.join(repoRoot, '.git-ai', 'meta.json');
43
+ const meta = await fs_extra_1.default.pathExists(metaPath) ? await fs_extra_1.default.readJSON(metaPath).catch(() => null) : null;
44
+ const dim = typeof meta?.dim === 'number' ? meta.dim : 256;
45
+ const scanRoot = path_1.default.resolve(repoRoot, typeof meta?.scanRoot === 'string' ? meta.scanRoot : path_1.default.relative(repoRoot, (0, git_1.inferScanRoot)(repoRoot)));
46
+ return { repoRoot, scanRoot, dim, meta };
47
+ }
48
+ async resolveRepoRoot(callPath) {
49
+ return (0, git_1.resolveGitRoot)(path_1.default.resolve(callPath ?? this.startDir));
50
+ }
51
+ assertPathInsideRoot(rootDir, file) {
52
+ const abs = path_1.default.resolve(rootDir, file);
53
+ const rel = path_1.default.relative(path_1.default.resolve(rootDir), abs);
54
+ if (rel.startsWith('..') || path_1.default.isAbsolute(rel))
55
+ throw new Error('Path escapes repository root');
56
+ return abs;
57
+ }
58
+ async writeAccessLog(name, args, duration, ok, repoRoot) {
59
+ if (this.options.disableAccessLog)
60
+ return;
61
+ if (process.env.GIT_AI_DISABLE_MCP_ACCESS_LOG === 'true' || process.env.GIT_AI_DISABLE_MCP_ACCESS_LOG === '1')
62
+ return;
63
+ try {
64
+ const logDir = path_1.default.join(os_1.default.homedir(), '.git-ai', 'logs');
65
+ await fs_extra_1.default.ensureDir(logDir);
66
+ const logFile = path_1.default.join(logDir, 'mcp-access.log');
67
+ const entry = {
68
+ ts: new Date().toISOString(),
69
+ tool: name,
70
+ repo: repoRoot ? path_1.default.basename(repoRoot) : 'unknown',
71
+ duration_ms: duration,
72
+ ok,
73
+ args: JSON.stringify(args).slice(0, 1000), // Avoid overly large logs
74
+ };
75
+ await fs_extra_1.default.appendFile(logFile, JSON.stringify(entry) + '\n', 'utf-8');
76
+ }
77
+ catch (e) {
78
+ // ignore
79
+ }
80
+ }
81
+ setupHandlers() {
82
+ this.server.setRequestHandler(types_js_1.ListToolsRequestSchema, async () => {
83
+ return {
84
+ tools: [
85
+ {
86
+ name: 'get_repo',
87
+ description: 'Resolve repository root and scan root for a given path. Risk: low (read-only).',
88
+ inputSchema: {
89
+ type: 'object',
90
+ properties: {
91
+ path: { type: 'string', description: 'Repository root path' },
92
+ },
93
+ required: ['path'],
94
+ },
95
+ },
96
+ {
97
+ name: 'search_symbols',
98
+ description: 'Search symbols and return file locations (substring/prefix/wildcard/regex/fuzzy). Risk: low (read-only).',
99
+ inputSchema: {
100
+ type: 'object',
101
+ properties: {
102
+ query: { type: 'string' },
103
+ mode: { type: 'string', enum: ['substring', 'prefix', 'wildcard', 'regex', 'fuzzy'] },
104
+ case_insensitive: { type: 'boolean', default: false },
105
+ max_candidates: { type: 'number', default: 1000 },
106
+ lang: { type: 'string', enum: ['auto', 'all', 'java', 'ts'], default: 'auto' },
107
+ path: { type: 'string', description: 'Repository root path' },
108
+ limit: { type: 'number', default: 50 },
109
+ with_repo_map: { type: 'boolean', default: false },
110
+ repo_map_max_files: { type: 'number', default: 20 },
111
+ repo_map_max_symbols: { type: 'number', default: 5 },
112
+ wiki_dir: { type: 'string', description: 'Wiki dir relative to repo root (optional)' },
113
+ },
114
+ required: ['path', 'query'],
115
+ },
116
+ },
117
+ {
118
+ name: 'semantic_search',
119
+ description: 'Semantic search using SQ8 vectors stored in LanceDB (brute-force). Risk: low (read-only).',
120
+ inputSchema: {
121
+ type: 'object',
122
+ properties: {
123
+ query: { type: 'string' },
124
+ path: { type: 'string', description: 'Repository root path' },
125
+ topk: { type: 'number', default: 10 },
126
+ lang: { type: 'string', enum: ['auto', 'all', 'java', 'ts'], default: 'auto' },
127
+ with_repo_map: { type: 'boolean', default: false },
128
+ repo_map_max_files: { type: 'number', default: 20 },
129
+ repo_map_max_symbols: { type: 'number', default: 5 },
130
+ wiki_dir: { type: 'string', description: 'Wiki dir relative to repo root (optional)' },
131
+ },
132
+ required: ['path', 'query'],
133
+ },
134
+ },
135
+ {
136
+ name: 'repo_map',
137
+ description: 'Generate a lightweight repository map (ranked files + top symbols + wiki links). Risk: low (read-only).',
138
+ inputSchema: {
139
+ type: 'object',
140
+ properties: {
141
+ path: { type: 'string', description: 'Repository root path' },
142
+ max_files: { type: 'number', default: 20 },
143
+ max_symbols: { type: 'number', default: 5 },
144
+ wiki_dir: { type: 'string', description: 'Wiki dir relative to repo root (optional)' },
145
+ },
146
+ required: ['path'],
147
+ },
148
+ },
149
+ {
150
+ name: 'check_index',
151
+ description: 'Check whether the repository index structure matches current expected schema. Risk: low (read-only).',
152
+ inputSchema: {
153
+ type: 'object',
154
+ properties: {
155
+ path: { type: 'string', description: 'Repository root path' },
156
+ },
157
+ required: ['path'],
158
+ },
159
+ },
160
+ {
161
+ name: 'rebuild_index',
162
+ description: 'Rebuild full repository index under .git-ai (LanceDB + AST graph). Risk: high (writes .git-ai; can be slow).',
163
+ inputSchema: {
164
+ type: 'object',
165
+ properties: {
166
+ path: { type: 'string', description: 'Repository root path' },
167
+ dim: { type: 'number', default: 256 },
168
+ overwrite: { type: 'boolean', default: true },
169
+ },
170
+ required: ['path'],
171
+ },
172
+ },
173
+ {
174
+ name: 'pack_index',
175
+ description: 'Pack .git-ai/lancedb into .git-ai/lancedb.tar.gz. Risk: medium (writes archive; may touch git-lfs config).',
176
+ inputSchema: {
177
+ type: 'object',
178
+ properties: {
179
+ path: { type: 'string', description: 'Repository root path' },
180
+ lfs: { type: 'boolean', default: false, description: 'Run git lfs track for .git-ai/lancedb.tar.gz' },
181
+ },
182
+ required: ['path'],
183
+ },
184
+ },
185
+ {
186
+ name: 'unpack_index',
187
+ description: 'Unpack .git-ai/lancedb.tar.gz into .git-ai/lancedb. Risk: medium (writes .git-ai/lancedb).',
188
+ inputSchema: {
189
+ type: 'object',
190
+ properties: {
191
+ path: { type: 'string', description: 'Repository root path' },
192
+ },
193
+ required: ['path'],
194
+ },
195
+ },
196
+ {
197
+ name: 'list_files',
198
+ description: 'List repository files by glob pattern. Risk: low (read-only).',
199
+ inputSchema: {
200
+ type: 'object',
201
+ properties: {
202
+ path: { type: 'string', description: 'Repository root path' },
203
+ pattern: { type: 'string', default: '**/*' },
204
+ limit: { type: 'number', default: 500 },
205
+ },
206
+ required: ['path'],
207
+ },
208
+ },
209
+ {
210
+ name: 'read_file',
211
+ description: 'Read a repository file with optional line range. Risk: low (read-only).',
212
+ inputSchema: {
213
+ type: 'object',
214
+ properties: {
215
+ path: { type: 'string', description: 'Repository root path' },
216
+ file: { type: 'string', description: 'File path relative to repo root' },
217
+ start_line: { type: 'number', default: 1 },
218
+ end_line: { type: 'number', default: 200 },
219
+ },
220
+ required: ['path', 'file'],
221
+ },
222
+ },
223
+ {
224
+ name: 'ast_graph_query',
225
+ description: 'Run a CozoScript query against the AST graph database (advanced). Risk: low (read-only).',
226
+ inputSchema: {
227
+ type: 'object',
228
+ properties: {
229
+ query: { type: 'string' },
230
+ params: { type: 'object', default: {} },
231
+ path: { type: 'string', description: 'Repository root path' },
232
+ },
233
+ required: ['path', 'query'],
234
+ },
235
+ },
236
+ {
237
+ name: 'ast_graph_find',
238
+ description: 'Find symbols by name prefix (case-insensitive) using the AST graph. Risk: low (read-only).',
239
+ inputSchema: {
240
+ type: 'object',
241
+ properties: {
242
+ prefix: { type: 'string' },
243
+ path: { type: 'string', description: 'Repository root path' },
244
+ limit: { type: 'number', default: 50 },
245
+ lang: { type: 'string', enum: ['auto', 'all', 'java', 'ts'], default: 'auto' },
246
+ },
247
+ required: ['path', 'prefix'],
248
+ },
249
+ },
250
+ {
251
+ name: 'ast_graph_children',
252
+ description: 'List direct children in the AST containment graph (file -> top-level symbols, class -> methods). Risk: low (read-only).',
253
+ inputSchema: {
254
+ type: 'object',
255
+ properties: {
256
+ id: { type: 'string', description: 'Parent id (ref_id or file_id; or file path when as_file=true)' },
257
+ as_file: { type: 'boolean', default: false },
258
+ path: { type: 'string', description: 'Repository root path' },
259
+ },
260
+ required: ['path', 'id'],
261
+ },
262
+ },
263
+ {
264
+ name: 'ast_graph_refs',
265
+ description: 'Find reference locations by name (calls/new/type). Risk: low (read-only).',
266
+ inputSchema: {
267
+ type: 'object',
268
+ properties: {
269
+ name: { type: 'string' },
270
+ limit: { type: 'number', default: 200 },
271
+ lang: { type: 'string', enum: ['auto', 'all', 'java', 'ts'], default: 'auto' },
272
+ path: { type: 'string', description: 'Repository root path' },
273
+ },
274
+ required: ['path', 'name'],
275
+ },
276
+ },
277
+ {
278
+ name: 'ast_graph_callers',
279
+ description: 'Find callers by callee name. Risk: low (read-only).',
280
+ inputSchema: {
281
+ type: 'object',
282
+ properties: {
283
+ name: { type: 'string' },
284
+ limit: { type: 'number', default: 200 },
285
+ lang: { type: 'string', enum: ['auto', 'all', 'java', 'ts'], default: 'auto' },
286
+ path: { type: 'string', description: 'Repository root path' },
287
+ },
288
+ required: ['path', 'name'],
289
+ },
290
+ },
291
+ {
292
+ name: 'ast_graph_callees',
293
+ description: 'Find callees by caller name. Risk: low (read-only).',
294
+ inputSchema: {
295
+ type: 'object',
296
+ properties: {
297
+ name: { type: 'string' },
298
+ limit: { type: 'number', default: 200 },
299
+ lang: { type: 'string', enum: ['auto', 'all', 'java', 'ts'], default: 'auto' },
300
+ path: { type: 'string', description: 'Repository root path' },
301
+ },
302
+ required: ['path', 'name'],
303
+ },
304
+ },
305
+ {
306
+ name: 'ast_graph_chain',
307
+ description: 'Compute call chain by symbol name (heuristic, name-based). Risk: low (read-only).',
308
+ inputSchema: {
309
+ type: 'object',
310
+ properties: {
311
+ name: { type: 'string' },
312
+ direction: { type: 'string', enum: ['downstream', 'upstream'], default: 'downstream' },
313
+ max_depth: { type: 'number', default: 3 },
314
+ limit: { type: 'number', default: 500 },
315
+ min_name_len: { type: 'number', default: 1 },
316
+ lang: { type: 'string', enum: ['auto', 'all', 'java', 'ts'], default: 'auto' },
317
+ path: { type: 'string', description: 'Repository root path' },
318
+ },
319
+ required: ['path', 'name'],
320
+ },
321
+ },
322
+ {
323
+ name: 'dsr_context',
324
+ description: 'Get repository Git context and DSR directory state. Risk: low (read-only).',
325
+ inputSchema: {
326
+ type: 'object',
327
+ properties: {
328
+ path: { type: 'string', description: 'Repository root path' },
329
+ },
330
+ required: ['path'],
331
+ },
332
+ },
333
+ {
334
+ name: 'dsr_generate',
335
+ description: 'Generate DSR (Deterministic Semantic Record) for a specific commit. Risk: medium (writes .git-ai/dsr).',
336
+ inputSchema: {
337
+ type: 'object',
338
+ properties: {
339
+ path: { type: 'string', description: 'Repository root path' },
340
+ commit: { type: 'string', description: 'Commit hash or ref' },
341
+ },
342
+ required: ['path', 'commit'],
343
+ },
344
+ },
345
+ {
346
+ name: 'dsr_rebuild_index',
347
+ description: 'Rebuild DSR index from DSR files for faster queries. Risk: medium (writes .git-ai/dsr-index).',
348
+ inputSchema: {
349
+ type: 'object',
350
+ properties: {
351
+ path: { type: 'string', description: 'Repository root path' },
352
+ },
353
+ required: ['path'],
354
+ },
355
+ },
356
+ {
357
+ name: 'dsr_symbol_evolution',
358
+ description: 'Query symbol evolution history across commits using DSR. Risk: low (read-only).',
359
+ inputSchema: {
360
+ type: 'object',
361
+ properties: {
362
+ path: { type: 'string', description: 'Repository root path' },
363
+ symbol: { type: 'string', description: 'Symbol name to query' },
364
+ start: { type: 'string', description: 'Start commit (default: HEAD)' },
365
+ all: { type: 'boolean', default: false, description: 'Traverse all refs instead of just HEAD' },
366
+ limit: { type: 'number', default: 200, description: 'Max commits to traverse' },
367
+ contains: { type: 'boolean', default: false, description: 'Match by substring instead of exact' },
368
+ },
369
+ required: ['path', 'symbol'],
370
+ },
371
+ },
372
+ ],
373
+ };
374
+ });
375
+ this.server.setRequestHandler(types_js_1.CallToolRequestSchema, async (request) => {
376
+ const name = request.params.name;
377
+ const args = request.params.arguments ?? {};
378
+ const callPath = typeof args.path === 'string' ? String(args.path) : undefined;
379
+ const log = (0, log_1.createLogger)({ component: 'mcp', tool: name });
380
+ const startedAt = Date.now();
381
+ const response = await (async () => {
382
+ if (typeof callPath !== 'string' || callPath.trim() === '') {
383
+ throw new Error('Missing required argument: path');
384
+ }
385
+ if (name === 'get_repo') {
386
+ const ctx = await this.openRepoContext(callPath);
387
+ const repoRoot = ctx.repoRoot;
388
+ const scanRoot = ctx.scanRoot;
389
+ return {
390
+ content: [{ type: 'text', text: JSON.stringify({ ok: true, startDir: this.startDir, repoRoot, scanRoot }, null, 2) }],
391
+ };
392
+ }
393
+ if (name === 'dsr_context') {
394
+ const repoRoot = await this.resolveRepoRoot(callPath);
395
+ const ctx = await (0, gitContext_1.detectRepoGitContext)(repoRoot);
396
+ const state = await (0, state_1.getDsrDirectoryState)(ctx.repo_root);
397
+ return {
398
+ content: [{ type: 'text', text: JSON.stringify({
399
+ ok: true,
400
+ commit_hash: ctx.head_commit,
401
+ repo_root: ctx.repo_root,
402
+ branch: ctx.branch,
403
+ detached: ctx.detached,
404
+ dsr_directory_state: state,
405
+ }, null, 2) }],
406
+ };
407
+ }
408
+ if (name === 'dsr_generate') {
409
+ const repoRoot = await this.resolveRepoRoot(callPath);
410
+ const commit = String(args.commit ?? 'HEAD');
411
+ const res = await (0, generate_1.generateDsrForCommit)(repoRoot, commit);
412
+ return {
413
+ content: [{ type: 'text', text: JSON.stringify({
414
+ ok: true,
415
+ commit_hash: res.dsr.commit_hash,
416
+ file_path: res.file_path,
417
+ existed: res.existed,
418
+ counts: {
419
+ affected_symbols: res.dsr.affected_symbols.length,
420
+ ast_operations: res.dsr.ast_operations.length,
421
+ },
422
+ semantic_change_type: res.dsr.semantic_change_type,
423
+ risk_level: res.dsr.risk_level,
424
+ }, null, 2) }],
425
+ };
426
+ }
427
+ if (name === 'dsr_rebuild_index') {
428
+ const repoRoot = await this.resolveRepoRoot(callPath);
429
+ const res = await (0, indexMaterialize_1.materializeDsrIndex)(repoRoot);
430
+ return {
431
+ content: [{ type: 'text', text: JSON.stringify({ repoRoot, ...res }, null, 2) }],
432
+ isError: !res.enabled,
433
+ };
434
+ }
435
+ if (name === 'dsr_symbol_evolution') {
436
+ const repoRoot = await this.resolveRepoRoot(callPath);
437
+ const symbol = String(args.symbol ?? '');
438
+ const opts = {
439
+ start: args.start ? String(args.start) : undefined,
440
+ all: Boolean(args.all ?? false),
441
+ limit: Number(args.limit ?? 200),
442
+ contains: Boolean(args.contains ?? false),
443
+ };
444
+ const res = await (0, query_1.symbolEvolution)(repoRoot, symbol, opts);
445
+ return {
446
+ content: [{ type: 'text', text: JSON.stringify({ repoRoot, symbol, ...res }, null, 2) }],
447
+ isError: !res.ok,
448
+ };
449
+ }
450
+ if (name === 'check_index') {
451
+ const repoRoot = await this.resolveRepoRoot(callPath);
452
+ const res = await (0, indexCheck_1.checkIndex)(repoRoot);
453
+ return {
454
+ content: [{ type: 'text', text: JSON.stringify({ repoRoot, ...res }, null, 2) }],
455
+ isError: !res.ok,
456
+ };
457
+ }
458
+ if (name === 'rebuild_index') {
459
+ const { repoRoot, scanRoot, meta } = await this.openRepoContext(callPath);
460
+ const overwrite = Boolean(args.overwrite ?? true);
461
+ const dimOpt = Number(args.dim ?? 256);
462
+ const dim = typeof meta?.dim === 'number' ? meta.dim : dimOpt;
463
+ const indexer = new indexer_1.IndexerV2({ repoRoot, scanRoot, dim, overwrite });
464
+ await indexer.run();
465
+ return {
466
+ content: [{ type: 'text', text: JSON.stringify({ ok: true, repoRoot, scanRoot, dim, overwrite }, null, 2) }],
467
+ };
468
+ }
469
+ if (name === 'pack_index') {
470
+ const repoRoot = await this.resolveRepoRoot(callPath);
471
+ const packed = await (0, archive_1.packLanceDb)(repoRoot);
472
+ const lfs = Boolean(args.lfs ?? false) ? (0, lfs_1.ensureLfsTracking)(repoRoot, '.git-ai/lancedb.tar.gz') : { tracked: false };
473
+ return {
474
+ content: [{ type: 'text', text: JSON.stringify({ ok: true, repoRoot, ...packed, lfs }, null, 2) }],
475
+ };
476
+ }
477
+ if (name === 'unpack_index') {
478
+ const repoRoot = await this.resolveRepoRoot(callPath);
479
+ const unpacked = await (0, archive_1.unpackLanceDb)(repoRoot);
480
+ return {
481
+ content: [{ type: 'text', text: JSON.stringify({ ok: true, repoRoot, ...unpacked }, null, 2) }],
482
+ };
483
+ }
484
+ if (name === 'list_files') {
485
+ const { repoRoot, scanRoot } = await this.openRepoContext(callPath);
486
+ const pattern = String(args.pattern ?? '**/*');
487
+ const limit = Number(args.limit ?? 500);
488
+ const files = await (0, glob_1.glob)(pattern, {
489
+ cwd: scanRoot,
490
+ dot: true,
491
+ nodir: true,
492
+ ignore: ['node_modules/**', '.git/**', '**/.git/**', '.git-ai/**', '**/.git-ai/**', '.repo/**', '**/.repo/**', 'dist/**', 'target/**', '**/target/**', 'build/**', '**/build/**', '.gradle/**', '**/.gradle/**'],
493
+ });
494
+ return {
495
+ content: [{ type: 'text', text: JSON.stringify({ ok: true, repoRoot, scanRoot, files: files.slice(0, limit) }, null, 2) }],
496
+ };
497
+ }
498
+ if (name === 'read_file') {
499
+ const { repoRoot, scanRoot } = await this.openRepoContext(callPath);
500
+ const file = String(args.file ?? '');
501
+ const startLine = Math.max(1, Number(args.start_line ?? 1));
502
+ const endLine = Math.max(startLine, Number(args.end_line ?? startLine + 199));
503
+ const abs = this.assertPathInsideRoot(scanRoot, file);
504
+ const raw = await fs_extra_1.default.readFile(abs, 'utf-8');
505
+ const lines = raw.split(/\r?\n/);
506
+ const slice = lines.slice(startLine - 1, endLine);
507
+ const numbered = slice.map((l, idx) => `${String(startLine + idx).padStart(6, ' ')}→${l}`).join('\n');
508
+ return {
509
+ content: [{ type: 'text', text: JSON.stringify({ ok: true, repoRoot, scanRoot, file, start_line: startLine, end_line: endLine, text: numbered }, null, 2) }],
510
+ };
511
+ }
512
+ if (name === 'ast_graph_query') {
513
+ const repoRoot = await this.resolveRepoRoot(callPath);
514
+ const query = String(args.query ?? '');
515
+ const params = args.params && typeof args.params === 'object' ? args.params : {};
516
+ const result = await (0, astGraphQuery_1.runAstGraphQuery)(repoRoot, query, params);
517
+ return {
518
+ content: [{ type: 'text', text: JSON.stringify({ repoRoot, result }, null, 2) }],
519
+ };
520
+ }
521
+ if (name === 'ast_graph_find') {
522
+ const repoRoot = await this.resolveRepoRoot(callPath);
523
+ const prefix = String(args.prefix ?? '');
524
+ const limit = Number(args.limit ?? 50);
525
+ const langSel = String(args.lang ?? 'auto');
526
+ const status = await (0, indexCheck_1.checkIndex)(repoRoot);
527
+ if (!status.ok) {
528
+ return { content: [{ type: 'text', text: JSON.stringify({ ...status, ok: false, reason: 'index_incompatible' }, null, 2) }], isError: true };
529
+ }
530
+ const langs = (0, indexCheck_1.resolveLangs)(status.found.meta ?? null, langSel);
531
+ const allRows = [];
532
+ for (const lang of langs) {
533
+ const result = await (0, astGraphQuery_1.runAstGraphQuery)(repoRoot, (0, astGraphQuery_1.buildFindSymbolsQuery)(lang), { prefix, lang });
534
+ const rows = Array.isArray(result?.rows) ? result.rows : [];
535
+ for (const r of rows)
536
+ allRows.push(r);
537
+ }
538
+ return {
539
+ content: [{ type: 'text', text: JSON.stringify({ repoRoot, lang: langSel, result: { headers: ['ref_id', 'file', 'lang', 'name', 'kind', 'signature', 'start_line', 'end_line'], rows: allRows.slice(0, limit) } }, null, 2) }],
540
+ };
541
+ }
542
+ if (name === 'ast_graph_children') {
543
+ const repoRoot = await this.resolveRepoRoot(callPath);
544
+ const id = String(args.id ?? '');
545
+ const asFile = Boolean(args.as_file ?? false);
546
+ const parent_id = asFile ? (0, crypto_1.sha256Hex)(`file:${(0, paths_1.toPosixPath)(id)}`) : id;
547
+ const result = await (0, astGraphQuery_1.runAstGraphQuery)(repoRoot, (0, astGraphQuery_1.buildChildrenQuery)(), { parent_id });
548
+ return {
549
+ content: [{ type: 'text', text: JSON.stringify({ repoRoot, parent_id, result }, null, 2) }],
550
+ };
551
+ }
552
+ if (name === 'ast_graph_refs') {
553
+ const repoRoot = await this.resolveRepoRoot(callPath);
554
+ const target = String(args.name ?? '');
555
+ const limit = Number(args.limit ?? 200);
556
+ const langSel = String(args.lang ?? 'auto');
557
+ const status = await (0, indexCheck_1.checkIndex)(repoRoot);
558
+ if (!status.ok) {
559
+ return { content: [{ type: 'text', text: JSON.stringify({ ...status, ok: false, reason: 'index_incompatible' }, null, 2) }], isError: true };
560
+ }
561
+ const langs = (0, indexCheck_1.resolveLangs)(status.found.meta ?? null, langSel);
562
+ const allRows = [];
563
+ for (const lang of langs) {
564
+ const result = await (0, astGraphQuery_1.runAstGraphQuery)(repoRoot, (0, astGraphQuery_1.buildFindReferencesQuery)(lang), { name: target, lang });
565
+ const rows = Array.isArray(result?.rows) ? result.rows : [];
566
+ for (const r of rows)
567
+ allRows.push(r);
568
+ }
569
+ return {
570
+ content: [{ type: 'text', text: JSON.stringify({ repoRoot, name: target, lang: langSel, result: { headers: ['file', 'line', 'col', 'ref_kind', 'from_id', 'from_kind', 'from_name', 'from_lang'], rows: allRows.slice(0, limit) } }, null, 2) }],
571
+ };
572
+ }
573
+ if (name === 'ast_graph_callers') {
574
+ const repoRoot = await this.resolveRepoRoot(callPath);
575
+ const target = String(args.name ?? '');
576
+ const limit = Number(args.limit ?? 200);
577
+ const langSel = String(args.lang ?? 'auto');
578
+ const status = await (0, indexCheck_1.checkIndex)(repoRoot);
579
+ if (!status.ok) {
580
+ return { content: [{ type: 'text', text: JSON.stringify({ ...status, ok: false, reason: 'index_incompatible' }, null, 2) }], isError: true };
581
+ }
582
+ const langs = (0, indexCheck_1.resolveLangs)(status.found.meta ?? null, langSel);
583
+ const allRows = [];
584
+ for (const lang of langs) {
585
+ const result = await (0, astGraphQuery_1.runAstGraphQuery)(repoRoot, (0, astGraphQuery_1.buildCallersByNameQuery)(lang), { name: target, lang });
586
+ const rows = Array.isArray(result?.rows) ? result.rows : [];
587
+ for (const r of rows)
588
+ allRows.push(r);
589
+ }
590
+ return {
591
+ content: [{ type: 'text', text: JSON.stringify({ repoRoot, name: target, lang: langSel, result: { headers: ['caller_id', 'caller_kind', 'caller_name', 'file', 'line', 'col', 'caller_lang'], rows: allRows.slice(0, limit) } }, null, 2) }],
592
+ };
593
+ }
594
+ if (name === 'ast_graph_callees') {
595
+ const repoRoot = await this.resolveRepoRoot(callPath);
596
+ const target = String(args.name ?? '');
597
+ const limit = Number(args.limit ?? 200);
598
+ const langSel = String(args.lang ?? 'auto');
599
+ const status = await (0, indexCheck_1.checkIndex)(repoRoot);
600
+ if (!status.ok) {
601
+ return { content: [{ type: 'text', text: JSON.stringify({ ...status, ok: false, reason: 'index_incompatible' }, null, 2) }], isError: true };
602
+ }
603
+ const langs = (0, indexCheck_1.resolveLangs)(status.found.meta ?? null, langSel);
604
+ const allRows = [];
605
+ for (const lang of langs) {
606
+ const result = await (0, astGraphQuery_1.runAstGraphQuery)(repoRoot, (0, astGraphQuery_1.buildCalleesByNameQuery)(lang), { name: target, lang });
607
+ const rows = Array.isArray(result?.rows) ? result.rows : [];
608
+ for (const r of rows)
609
+ allRows.push(r);
610
+ }
611
+ return {
612
+ content: [{ type: 'text', text: JSON.stringify({ repoRoot, name: target, lang: langSel, result: { headers: ['caller_id', 'caller_lang', 'callee_id', 'callee_file', 'callee_name', 'callee_kind', 'file', 'line', 'col'], rows: allRows.slice(0, limit) } }, null, 2) }],
613
+ };
614
+ }
615
+ if (name === 'ast_graph_chain') {
616
+ const repoRoot = await this.resolveRepoRoot(callPath);
617
+ const target = String(args.name ?? '');
618
+ const direction = String(args.direction ?? 'downstream');
619
+ const maxDepth = Number(args.max_depth ?? 3);
620
+ const limit = Number(args.limit ?? 500);
621
+ const minNameLen = Math.max(1, Number(args.min_name_len ?? 1));
622
+ const langSel = String(args.lang ?? 'auto');
623
+ const status = await (0, indexCheck_1.checkIndex)(repoRoot);
624
+ if (!status.ok) {
625
+ return { content: [{ type: 'text', text: JSON.stringify({ ...status, ok: false, reason: 'index_incompatible' }, null, 2) }], isError: true };
626
+ }
627
+ const langs = (0, indexCheck_1.resolveLangs)(status.found.meta ?? null, langSel);
628
+ const query = direction === 'upstream' ? (0, astGraphQuery_1.buildCallChainUpstreamByNameQuery)() : (0, astGraphQuery_1.buildCallChainDownstreamByNameQuery)();
629
+ const rawRows = [];
630
+ for (const lang of langs) {
631
+ const result = await (0, astGraphQuery_1.runAstGraphQuery)(repoRoot, query, { name: target, max_depth: maxDepth, lang });
632
+ const rows = Array.isArray(result?.rows) ? result.rows : [];
633
+ for (const r of rows)
634
+ rawRows.push(r);
635
+ }
636
+ const filtered = minNameLen > 1
637
+ ? rawRows.filter((r) => String(r?.[3] ?? '').length >= minNameLen && String(r?.[4] ?? '').length >= minNameLen)
638
+ : rawRows;
639
+ const rows = filtered.slice(0, limit);
640
+ return {
641
+ content: [{ type: 'text', text: JSON.stringify({ repoRoot, name: target, lang: langSel, direction, max_depth: maxDepth, min_name_len: minNameLen, result: { headers: ['caller_id', 'callee_id', 'depth', 'caller_name', 'callee_name', 'lang'], rows } }, null, 2) }],
642
+ };
643
+ }
644
+ const repoRootForDispatch = await this.resolveRepoRoot(callPath);
645
+ if (name === 'repo_map') {
646
+ const wikiDir = resolveWikiDirInsideRepo(repoRootForDispatch, String(args.wiki_dir ?? ''));
647
+ const maxFiles = Number(args.max_files ?? 20);
648
+ const maxSymbolsPerFile = Number(args.max_symbols ?? 5);
649
+ const repoMap = await buildRepoMapAttachment(repoRootForDispatch, wikiDir, maxFiles, maxSymbolsPerFile);
650
+ return { content: [{ type: 'text', text: JSON.stringify({ repoRoot: repoRootForDispatch, repo_map: repoMap }, null, 2) }] };
651
+ }
652
+ if (name === 'search_symbols' && (0, git_1.inferWorkspaceRoot)(repoRootForDispatch)) {
653
+ const query = String(args.query ?? '');
654
+ const limit = Number(args.limit ?? 50);
655
+ const mode = (0, symbolSearch_1.inferSymbolSearchMode)(query, args.mode);
656
+ const caseInsensitive = Boolean(args.case_insensitive ?? false);
657
+ const maxCandidates = Math.max(limit, Number(args.max_candidates ?? Math.min(2000, limit * 20)));
658
+ const keyword = (mode === 'substring' || mode === 'prefix') ? query : (0, symbolSearch_1.pickCoarseToken)(query);
659
+ const res = await (0, workspace_1.queryManifestWorkspace)({ manifestRepoRoot: repoRootForDispatch, keyword, limit: maxCandidates });
660
+ const langSel = String(args.lang ?? 'auto');
661
+ const filteredByLang = (langSel === 'java')
662
+ ? res.rows.filter(r => String(r.file ?? '').endsWith('.java'))
663
+ : (langSel === 'ts')
664
+ ? res.rows.filter(r => !String(r.file ?? '').endsWith('.java'))
665
+ : res.rows;
666
+ const rows = (0, symbolSearch_1.filterAndRankSymbolRows)(filteredByLang, { query, mode, caseInsensitive, limit });
667
+ const withRepoMap = Boolean(args.with_repo_map ?? false);
668
+ const repoMap = withRepoMap ? { enabled: false, skippedReason: 'workspace_mode_not_supported' } : undefined;
669
+ return {
670
+ content: [{ type: 'text', text: JSON.stringify({ repoRoot: repoRootForDispatch, lang: langSel, rows, ...(repoMap ? { repo_map: repoMap } : {}) }, null, 2) }],
671
+ };
672
+ }
673
+ if (name === 'search_symbols') {
674
+ const query = String(args.query ?? '');
675
+ const limit = Number(args.limit ?? 50);
676
+ const langSel = String(args.lang ?? 'auto');
677
+ const mode = (0, symbolSearch_1.inferSymbolSearchMode)(query, args.mode);
678
+ const caseInsensitive = Boolean(args.case_insensitive ?? false);
679
+ const maxCandidates = Math.max(limit, Number(args.max_candidates ?? Math.min(2000, limit * 20)));
680
+ const withRepoMap = Boolean(args.with_repo_map ?? false);
681
+ const wikiDir = resolveWikiDirInsideRepo(repoRootForDispatch, String(args.wiki_dir ?? ''));
682
+ const repoMapMaxFiles = Number(args.repo_map_max_files ?? 20);
683
+ const repoMapMaxSymbols = Number(args.repo_map_max_symbols ?? 5);
684
+ const status = await (0, indexCheck_1.checkIndex)(repoRootForDispatch);
685
+ if (!status.ok) {
686
+ return { content: [{ type: 'text', text: JSON.stringify({ ...status, ok: false, reason: 'index_incompatible' }, null, 2) }], isError: true };
687
+ }
688
+ const langs = (0, indexCheck_1.resolveLangs)(status.found.meta ?? null, langSel);
689
+ const dim = typeof status.found.meta?.dim === 'number' ? status.found.meta.dim : 256;
690
+ const dbDir = (0, lancedb_1.defaultDbDir)(repoRootForDispatch);
691
+ const { byLang } = await (0, lancedb_1.openTablesByLang)({ dbDir, dim, mode: 'open_only', languages: langs });
692
+ const where = (0, symbolSearch_1.buildCoarseWhere)({ query, mode, caseInsensitive });
693
+ const candidates = [];
694
+ for (const lang of langs) {
695
+ const t = byLang[lang];
696
+ if (!t)
697
+ continue;
698
+ const rows = where
699
+ ? await t.refs.query().where(where).limit(maxCandidates).toArray()
700
+ : await t.refs.query().limit(maxCandidates).toArray();
701
+ for (const r of rows)
702
+ candidates.push({ ...r, lang });
703
+ }
704
+ const rows = (0, symbolSearch_1.filterAndRankSymbolRows)(candidates, { query, mode, caseInsensitive, limit });
705
+ const repoMap = withRepoMap ? await buildRepoMapAttachment(repoRootForDispatch, wikiDir, repoMapMaxFiles, repoMapMaxSymbols) : undefined;
706
+ return { content: [{ type: 'text', text: JSON.stringify({ repoRoot: repoRootForDispatch, lang: langSel, rows, ...(repoMap ? { repo_map: repoMap } : {}) }, null, 2) }] };
707
+ }
708
+ if (name === 'semantic_search') {
709
+ const query = String(args.query ?? '');
710
+ const topk = Number(args.topk ?? 10);
711
+ const langSel = String(args.lang ?? 'auto');
712
+ const withRepoMap = Boolean(args.with_repo_map ?? false);
713
+ const wikiDir = resolveWikiDirInsideRepo(repoRootForDispatch, String(args.wiki_dir ?? ''));
714
+ const repoMapMaxFiles = Number(args.repo_map_max_files ?? 20);
715
+ const repoMapMaxSymbols = Number(args.repo_map_max_symbols ?? 5);
716
+ const status = await (0, indexCheck_1.checkIndex)(repoRootForDispatch);
717
+ if (!status.ok) {
718
+ return { content: [{ type: 'text', text: JSON.stringify({ ...status, ok: false, reason: 'index_incompatible' }, null, 2) }], isError: true };
719
+ }
720
+ const langs = (0, indexCheck_1.resolveLangs)(status.found.meta ?? null, langSel);
721
+ const dim = typeof status.found.meta?.dim === 'number' ? status.found.meta.dim : 256;
722
+ const dbDir = (0, lancedb_1.defaultDbDir)(repoRootForDispatch);
723
+ const { byLang } = await (0, lancedb_1.openTablesByLang)({ dbDir, dim, mode: 'open_only', languages: langs });
724
+ const q = (0, search_1.buildQueryVector)(query, dim);
725
+ const allScored = [];
726
+ for (const lang of langs) {
727
+ const t = byLang[lang];
728
+ if (!t)
729
+ continue;
730
+ const chunkRows = await t.chunks.query().select(['content_hash', 'text', 'dim', 'scale', 'qvec_b64']).limit(1000000).toArray();
731
+ for (const r of chunkRows) {
732
+ allScored.push({
733
+ lang,
734
+ content_hash: String(r.content_hash),
735
+ score: (0, search_1.scoreAgainst)(q, { dim: Number(r.dim), scale: Number(r.scale), qvec: new Int8Array(Buffer.from(String(r.qvec_b64), 'base64')) }),
736
+ text: String(r.text),
737
+ });
738
+ }
739
+ }
740
+ const rows = allScored.sort((a, b) => b.score - a.score).slice(0, topk);
741
+ const repoMap = withRepoMap ? await buildRepoMapAttachment(repoRootForDispatch, wikiDir, repoMapMaxFiles, repoMapMaxSymbols) : undefined;
742
+ return { content: [{ type: 'text', text: JSON.stringify({ repoRoot: repoRootForDispatch, lang: langSel, rows, ...(repoMap ? { repo_map: repoMap } : {}) }, null, 2) }] };
743
+ }
744
+ return {
745
+ content: [{ type: 'text', text: 'Tool not found' }],
746
+ isError: true,
747
+ };
748
+ })().catch((e) => {
749
+ const err = e instanceof Error ? { name: e.name, message: e.message, stack: e.stack } : { message: String(e) };
750
+ return {
751
+ content: [{ type: 'text', text: JSON.stringify({ ok: false, tool: name, error: err }, null, 2) }],
752
+ isError: true,
753
+ };
754
+ });
755
+ log.info('tool_call', { ok: !response.isError, duration_ms: Date.now() - startedAt, path: callPath });
756
+ const repoRootForLog = await this.resolveRepoRoot(callPath).catch(() => undefined);
757
+ await this.writeAccessLog(name, args, Date.now() - startedAt, !response.isError, repoRootForLog);
758
+ return response;
759
+ });
760
+ }
761
+ async start() {
762
+ const transport = new stdio_js_1.StdioServerTransport();
763
+ await this.server.connect(transport);
764
+ (0, log_1.createLogger)({ component: 'mcp' }).info('server_started', { startDir: this.startDir, transport: 'stdio' });
765
+ }
766
+ }
767
+ exports.GitAIV2MCPServer = GitAIV2MCPServer;
768
+ async function buildRepoMapAttachment(repoRoot, wikiDir, maxFiles, maxSymbolsPerFile) {
769
+ try {
770
+ const files = await (0, repoMap_1.generateRepoMap)({ repoRoot, maxFiles, maxSymbolsPerFile, wikiDir: wikiDir || undefined });
771
+ return { enabled: true, wikiDir, files };
772
+ }
773
+ catch (e) {
774
+ return { enabled: false, skippedReason: String(e?.message ?? e) };
775
+ }
776
+ }
777
+ function resolveWikiDirInsideRepo(repoRoot, wikiOpt) {
778
+ const w = String(wikiOpt ?? '').trim();
779
+ if (w) {
780
+ const abs = path_1.default.resolve(repoRoot, w);
781
+ const rel = path_1.default.relative(repoRoot, abs);
782
+ if (rel.startsWith('..') || path_1.default.isAbsolute(rel))
783
+ throw new Error('wiki_dir escapes repository root');
784
+ if (fs_extra_1.default.existsSync(abs))
785
+ return abs;
786
+ return '';
787
+ }
788
+ const candidates = [path_1.default.join(repoRoot, 'docs', 'wiki'), path_1.default.join(repoRoot, 'wiki')];
789
+ for (const c of candidates) {
790
+ if (fs_extra_1.default.existsSync(c))
791
+ return c;
792
+ }
793
+ return '';
794
+ }