@aiscene/aiserver 2.0.8 → 2.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.
@@ -0,0 +1,907 @@
1
+ import { execFile } from 'child_process';
2
+ import { createHash } from 'crypto';
3
+ import fs from 'fs/promises';
4
+ import path from 'path';
5
+ import { promisify } from 'util';
6
+ import { createLogger } from '../core/logger.js';
7
+ const execFileAsync = promisify(execFile);
8
+ const logger = createLogger('LocalCodeService');
9
+ const LOCAL_CODE_CAPABILITIES = [
10
+ 'local_code',
11
+ 'local_git',
12
+ 'local_codegraph',
13
+ 'local_code.open_project',
14
+ 'local_code.list_projects',
15
+ 'local_git.snapshot',
16
+ 'local_code.search',
17
+ 'local_code.read_file',
18
+ 'local_codegraph.build',
19
+ 'local_codegraph.query',
20
+ 'local_codegraph.trace',
21
+ 'local_code_open_project',
22
+ 'local_code_list_projects',
23
+ 'local_git_snapshot',
24
+ 'local_code_search',
25
+ 'local_code_read_file',
26
+ 'local_codegraph_build',
27
+ 'local_codegraph_query',
28
+ 'local_codegraph_trace',
29
+ ];
30
+ const EXCLUDED_DIR_NAMES = new Set([
31
+ '.git',
32
+ '.hg',
33
+ '.svn',
34
+ '.idea',
35
+ '.vscode',
36
+ 'node_modules',
37
+ 'dist',
38
+ 'build',
39
+ 'coverage',
40
+ '.next',
41
+ '.nuxt',
42
+ '.turbo',
43
+ '.cache',
44
+ 'target',
45
+ 'out',
46
+ 'vendor',
47
+ '__pycache__',
48
+ ]);
49
+ const TEXT_EXTENSIONS = new Set([
50
+ '.ts',
51
+ '.tsx',
52
+ '.js',
53
+ '.jsx',
54
+ '.mjs',
55
+ '.cjs',
56
+ '.vue',
57
+ '.java',
58
+ '.kt',
59
+ '.kts',
60
+ '.go',
61
+ '.py',
62
+ '.rb',
63
+ '.php',
64
+ '.rs',
65
+ '.c',
66
+ '.cc',
67
+ '.cpp',
68
+ '.h',
69
+ '.hpp',
70
+ '.cs',
71
+ '.swift',
72
+ '.dart',
73
+ '.json',
74
+ '.jsonc',
75
+ '.yaml',
76
+ '.yml',
77
+ '.xml',
78
+ '.html',
79
+ '.css',
80
+ '.scss',
81
+ '.less',
82
+ '.md',
83
+ '.mdx',
84
+ '.txt',
85
+ '.sql',
86
+ '.sh',
87
+ '.bash',
88
+ '.zsh',
89
+ '.gradle',
90
+ '.properties',
91
+ ]);
92
+ const EXTENSION_CANDIDATES = [
93
+ '',
94
+ '.ts',
95
+ '.tsx',
96
+ '.js',
97
+ '.jsx',
98
+ '.mjs',
99
+ '.cjs',
100
+ '.vue',
101
+ '.java',
102
+ '.kt',
103
+ '.go',
104
+ '.py',
105
+ '.json',
106
+ '/index.ts',
107
+ '/index.tsx',
108
+ '/index.js',
109
+ '/index.jsx',
110
+ '/index.vue',
111
+ ];
112
+ function getLocalCodeCapabilities() {
113
+ return [...LOCAL_CODE_CAPABILITIES];
114
+ }
115
+ function normalizeMethod(method) {
116
+ return method.replace(/\./g, '_');
117
+ }
118
+ function truncateText(value, maxBytes) {
119
+ const buffer = Buffer.from(value);
120
+ if (buffer.byteLength <= maxBytes)
121
+ return value;
122
+ return `${buffer.subarray(0, maxBytes).toString('utf8')}\n...[truncated ${buffer.byteLength - maxBytes} bytes]`;
123
+ }
124
+ function toPosix(value) {
125
+ return value.split(path.sep).join('/');
126
+ }
127
+ function unique(values) {
128
+ return Array.from(new Set(values));
129
+ }
130
+ function normalizeBranchName(value) {
131
+ return String(value || '')
132
+ .trim()
133
+ .replace(/^\*\s*/, '')
134
+ .replace(/^refs\/heads\//, '')
135
+ .replace(/^refs\/remotes\//, '')
136
+ .replace(/^remotes\//, '')
137
+ .trim();
138
+ }
139
+ function isRealBranchName(value) {
140
+ return Boolean(value) && value !== 'HEAD' && value !== 'origin' && !/\/HEAD$/.test(value) && !/HEAD\s*->/.test(value);
141
+ }
142
+ function parseBranchRefs(text) {
143
+ return unique(String(text || '')
144
+ .split(/\r?\n/)
145
+ .map(normalizeBranchName)
146
+ .filter(isRealBranchName));
147
+ }
148
+ export class LocalCodeService {
149
+ projects = new Map();
150
+ graphs = new Map();
151
+ canHandle(method) {
152
+ return new Set([
153
+ 'local_code_open_project',
154
+ 'local_code_list_projects',
155
+ 'local_git_snapshot',
156
+ 'local_code_search',
157
+ 'local_code_read_file',
158
+ 'local_codegraph_build',
159
+ 'local_codegraph_query',
160
+ 'local_codegraph_trace',
161
+ ]).has(normalizeMethod(method));
162
+ }
163
+ getCapabilities() {
164
+ return getLocalCodeCapabilities();
165
+ }
166
+ async execute(method, params = {}) {
167
+ switch (normalizeMethod(method)) {
168
+ case 'local_code_open_project':
169
+ return this.openProject(params);
170
+ case 'local_code_list_projects':
171
+ return this.listProjects(params);
172
+ case 'local_git_snapshot':
173
+ return this.gitSnapshot(params);
174
+ case 'local_code_search':
175
+ return this.searchCode(params);
176
+ case 'local_code_read_file':
177
+ return this.readFile(params);
178
+ case 'local_codegraph_build':
179
+ return this.buildCodegraph(params);
180
+ case 'local_codegraph_query':
181
+ return this.queryCodegraph(params);
182
+ case 'local_codegraph_trace':
183
+ return this.traceCodegraph(params);
184
+ default:
185
+ throw new Error(`Unsupported local code method: ${method}`);
186
+ }
187
+ }
188
+ async openProject(params) {
189
+ const rootPath = await this.resolveDirectory(String(params.path || ''));
190
+ const name = String(params.projectName || params.project_name || path.basename(rootPath) || 'local-project');
191
+ const sessionId = stringValue(params.sessionId || params.session_id);
192
+ const baseBranch = stringValue(params.baseBranch || params.base_branch);
193
+ const id = this.projectIdFor(rootPath, sessionId);
194
+ const now = Date.now();
195
+ const project = {
196
+ id,
197
+ name,
198
+ rootPath,
199
+ sessionId,
200
+ baseBranch,
201
+ createdAt: this.projects.get(id)?.createdAt || now,
202
+ updatedAt: now,
203
+ };
204
+ this.projects.set(id, project);
205
+ const [tree, git] = await Promise.all([
206
+ this.buildTree(rootPath, 2, 120),
207
+ this.getGitSnapshot(project, {
208
+ includeDiff: false,
209
+ baseBranch,
210
+ maxDiffBytes: 60000,
211
+ }).catch((error) => ({ isGitRepo: false, error: error.message })),
212
+ ]);
213
+ logger.info(`Local project opened: id=${id}, path=${rootPath}, sessionId=${sessionId || '-'}`);
214
+ return {
215
+ success: true,
216
+ projectId: id,
217
+ project_id: id,
218
+ name,
219
+ path: rootPath,
220
+ sessionId,
221
+ session_id: sessionId,
222
+ capabilities: this.getCapabilities(),
223
+ git,
224
+ tree,
225
+ };
226
+ }
227
+ listProjects(params) {
228
+ const sessionId = stringValue(params.sessionId || params.session_id);
229
+ const projects = Array.from(this.projects.values())
230
+ .filter((project) => !sessionId || project.sessionId === sessionId)
231
+ .map((project) => ({
232
+ projectId: project.id,
233
+ project_id: project.id,
234
+ name: project.name,
235
+ path: project.rootPath,
236
+ sessionId: project.sessionId,
237
+ session_id: project.sessionId,
238
+ baseBranch: project.baseBranch,
239
+ base_branch: project.baseBranch,
240
+ createdAt: new Date(project.createdAt).toISOString(),
241
+ updatedAt: new Date(project.updatedAt).toISOString(),
242
+ }));
243
+ return { success: true, projects };
244
+ }
245
+ async gitSnapshot(params) {
246
+ const project = await this.resolveProject(params);
247
+ return this.getGitSnapshot(project, {
248
+ baseBranch: stringValue(params.baseBranch || params.base_branch || project.baseBranch),
249
+ includeDiff: params.includeDiff ?? params.include_diff ?? true,
250
+ maxDiffBytes: numberValue(params.maxDiffBytes || params.max_diff_bytes, 200000),
251
+ });
252
+ }
253
+ async searchCode(params) {
254
+ const project = await this.resolveProject(params);
255
+ const query = String(params.query || '').trim();
256
+ if (!query)
257
+ throw new Error('query is required');
258
+ const maxResults = Math.min(numberValue(params.maxResults || params.max_results, 50), 200);
259
+ const type = String(params.type || 'text');
260
+ const includeGlobs = stringArray(params.includeGlobs || params.include_globs);
261
+ const excludeGlobs = stringArray(params.excludeGlobs || params.exclude_globs);
262
+ const files = await this.collectFiles(project.rootPath, {
263
+ maxFiles: 20000,
264
+ includePatterns: includeGlobs,
265
+ excludePatterns: excludeGlobs,
266
+ });
267
+ const regex = type === 'regex' ? new RegExp(query, 'i') : null;
268
+ const symbolRegex = type === 'symbol' ? new RegExp(`\\b${escapeRegExp(query)}\\b`, 'i') : null;
269
+ const lowerQuery = query.toLowerCase();
270
+ const results = [];
271
+ for (const filePath of files) {
272
+ if (results.length >= maxResults)
273
+ break;
274
+ const content = await this.readSmallTextFile(filePath, 900000).catch(() => '');
275
+ if (!content)
276
+ continue;
277
+ const lines = content.split(/\r?\n/);
278
+ for (let index = 0; index < lines.length && results.length < maxResults; index += 1) {
279
+ const line = lines[index];
280
+ const matched = regex
281
+ ? regex.test(line)
282
+ : symbolRegex
283
+ ? symbolRegex.test(line)
284
+ : line.toLowerCase().includes(lowerQuery);
285
+ if (!matched)
286
+ continue;
287
+ results.push({
288
+ filePath: toPosix(path.relative(project.rootPath, filePath)),
289
+ file_path: toPosix(path.relative(project.rootPath, filePath)),
290
+ line: index + 1,
291
+ snippet: line.trim().slice(0, 500),
292
+ });
293
+ }
294
+ }
295
+ return {
296
+ success: true,
297
+ projectId: project.id,
298
+ project_id: project.id,
299
+ query,
300
+ type,
301
+ total: results.length,
302
+ truncated: results.length >= maxResults,
303
+ results,
304
+ };
305
+ }
306
+ async readFile(params) {
307
+ const project = await this.resolveProject(params);
308
+ const rawFilePath = String(params.filePath || params.file_path || '');
309
+ if (!rawFilePath.trim())
310
+ throw new Error('file_path is required');
311
+ const filePath = await this.resolveFileInProject(project, rawFilePath);
312
+ const content = await this.readSmallTextFile(filePath, Math.min(numberValue(params.maxBytes || params.max_bytes, 120000), 500000));
313
+ const lines = content.split(/\r?\n/);
314
+ const startLine = Math.max(1, numberValue(params.startLine || params.start_line, 1));
315
+ const endLine = Math.min(lines.length, numberValue(params.endLine || params.end_line, params.startLine || params.start_line ? startLine + 220 : lines.length));
316
+ const selected = lines.slice(startLine - 1, endLine);
317
+ return {
318
+ success: true,
319
+ projectId: project.id,
320
+ project_id: project.id,
321
+ filePath: toPosix(path.relative(project.rootPath, filePath)),
322
+ file_path: toPosix(path.relative(project.rootPath, filePath)),
323
+ absolutePath: filePath,
324
+ absolute_path: filePath,
325
+ startLine,
326
+ start_line: startLine,
327
+ endLine,
328
+ end_line: endLine,
329
+ totalLines: lines.length,
330
+ total_lines: lines.length,
331
+ content: selected.join('\n'),
332
+ };
333
+ }
334
+ async buildCodegraph(params) {
335
+ const project = await this.resolveProject(params);
336
+ const maxFiles = Math.min(numberValue(params.maxFiles || params.max_files, 6000), 20000);
337
+ const files = await this.collectFiles(project.rootPath, {
338
+ maxFiles,
339
+ includePatterns: stringArray(params.includePatterns || params.include_patterns),
340
+ excludePatterns: stringArray(params.excludePatterns || params.exclude_patterns),
341
+ });
342
+ const graphFiles = [];
343
+ const skippedCount = Math.max(0, files.length - maxFiles);
344
+ for (const filePath of files.slice(0, maxFiles)) {
345
+ const content = await this.readSmallTextFile(filePath, 900000).catch(() => '');
346
+ if (!content)
347
+ continue;
348
+ const relativePath = toPosix(path.relative(project.rootPath, filePath));
349
+ graphFiles.push({
350
+ path: relativePath,
351
+ language: detectLanguage(filePath),
352
+ lines: content.split(/\r?\n/).length,
353
+ imports: unique(extractImports(content, filePath)),
354
+ symbols: unique(extractSymbols(content, filePath)).slice(0, 120),
355
+ });
356
+ }
357
+ const fileSet = new Set(graphFiles.map((file) => file.path));
358
+ const edges = [];
359
+ for (const file of graphFiles) {
360
+ for (const specifier of file.imports) {
361
+ const resolved = this.resolveImport(project.rootPath, file.path, specifier, fileSet);
362
+ edges.push({
363
+ from: file.path,
364
+ to: resolved || `external:${specifier}`,
365
+ type: resolved ? 'imports' : 'external',
366
+ specifier,
367
+ });
368
+ }
369
+ }
370
+ const graph = {
371
+ projectId: project.id,
372
+ rootPath: project.rootPath,
373
+ builtAt: Date.now(),
374
+ fileCount: graphFiles.length,
375
+ skippedCount,
376
+ files: graphFiles,
377
+ edges,
378
+ };
379
+ this.graphs.set(project.id, graph);
380
+ return {
381
+ success: true,
382
+ projectId: project.id,
383
+ project_id: project.id,
384
+ builtAt: new Date(graph.builtAt).toISOString(),
385
+ built_at: new Date(graph.builtAt).toISOString(),
386
+ fileCount: graph.fileCount,
387
+ file_count: graph.fileCount,
388
+ skippedCount: graph.skippedCount,
389
+ skipped_count: graph.skippedCount,
390
+ edgeCount: graph.edges.length,
391
+ edge_count: graph.edges.length,
392
+ topFiles: graph.files.slice(0, 80),
393
+ top_files: graph.files.slice(0, 80),
394
+ note: 'Codegraph is stored in local AIServer memory. Query or trace it by project_id.',
395
+ };
396
+ }
397
+ async queryCodegraph(params) {
398
+ const project = await this.resolveProject(params);
399
+ const graph = await this.ensureGraph(project, params);
400
+ const type = String(params.type || 'overview');
401
+ const query = String(params.query || '').trim().toLowerCase();
402
+ const maxResults = Math.min(numberValue(params.maxResults || params.max_results, 80), 200);
403
+ if (type === 'overview') {
404
+ const languages = countBy(graph.files.map((file) => file.language));
405
+ const highDegreeFiles = [...graph.files]
406
+ .map((file) => ({
407
+ path: file.path,
408
+ imports: graph.edges.filter((edge) => edge.from === file.path && edge.type === 'imports').length,
409
+ importedBy: graph.edges.filter((edge) => edge.to === file.path).length,
410
+ symbols: file.symbols.slice(0, 20),
411
+ }))
412
+ .sort((a, b) => b.importedBy + b.imports - (a.importedBy + a.imports))
413
+ .slice(0, maxResults);
414
+ return {
415
+ success: true,
416
+ projectId: project.id,
417
+ project_id: project.id,
418
+ type,
419
+ fileCount: graph.fileCount,
420
+ file_count: graph.fileCount,
421
+ edgeCount: graph.edges.length,
422
+ edge_count: graph.edges.length,
423
+ languages,
424
+ highDegreeFiles,
425
+ high_degree_files: highDegreeFiles,
426
+ };
427
+ }
428
+ if (type === 'dependency') {
429
+ const edges = graph.edges
430
+ .filter((edge) => !query || edge.from.toLowerCase().includes(query) || edge.to.toLowerCase().includes(query) || edge.specifier.toLowerCase().includes(query))
431
+ .slice(0, maxResults);
432
+ return { success: true, projectId: project.id, project_id: project.id, type, query, results: edges };
433
+ }
434
+ if (type === 'reference') {
435
+ const results = graph.files
436
+ .filter((file) => !query || file.imports.some((item) => item.toLowerCase().includes(query)) || file.symbols.some((item) => item.toLowerCase().includes(query)))
437
+ .slice(0, maxResults);
438
+ return { success: true, projectId: project.id, project_id: project.id, type, query, results };
439
+ }
440
+ const results = graph.files
441
+ .filter((file) => {
442
+ if (!query)
443
+ return true;
444
+ if (type === 'file')
445
+ return file.path.toLowerCase().includes(query);
446
+ if (type === 'symbol')
447
+ return file.symbols.some((symbol) => symbol.toLowerCase().includes(query));
448
+ return file.path.toLowerCase().includes(query) || file.symbols.some((symbol) => symbol.toLowerCase().includes(query));
449
+ })
450
+ .slice(0, maxResults);
451
+ return { success: true, projectId: project.id, project_id: project.id, type, query, results };
452
+ }
453
+ async traceCodegraph(params) {
454
+ const project = await this.resolveProject(params);
455
+ const graph = await this.ensureGraph(project, params);
456
+ const target = String(params.target || '').trim();
457
+ if (!target)
458
+ throw new Error('target is required');
459
+ const direction = String(params.direction || 'both');
460
+ const depth = Math.min(numberValue(params.depth, 1), 3);
461
+ const maxResults = Math.min(numberValue(params.maxResults || params.max_results, 80), 200);
462
+ const startFiles = this.findTargetFiles(graph, target);
463
+ const visited = new Set(startFiles);
464
+ const queue = startFiles.map((file) => ({ file, depth: 0 }));
465
+ const edges = [];
466
+ while (queue.length && edges.length < maxResults) {
467
+ const current = queue.shift();
468
+ if (current.depth >= depth)
469
+ continue;
470
+ const related = graph.edges.filter((edge) => {
471
+ if (edge.type !== 'imports')
472
+ return false;
473
+ if (direction === 'imports')
474
+ return edge.from === current.file;
475
+ if (direction === 'imported_by')
476
+ return edge.to === current.file;
477
+ if (direction === 'references')
478
+ return edge.to === current.file || edge.specifier.includes(target);
479
+ return edge.from === current.file || edge.to === current.file;
480
+ });
481
+ for (const edge of related) {
482
+ if (edges.length >= maxResults)
483
+ break;
484
+ edges.push(edge);
485
+ const next = edge.from === current.file ? edge.to : edge.from;
486
+ if (!visited.has(next)) {
487
+ visited.add(next);
488
+ queue.push({ file: next, depth: current.depth + 1 });
489
+ }
490
+ }
491
+ }
492
+ const files = graph.files.filter((file) => visited.has(file.path));
493
+ return {
494
+ success: true,
495
+ projectId: project.id,
496
+ project_id: project.id,
497
+ target,
498
+ direction,
499
+ depth,
500
+ startFiles,
501
+ start_files: startFiles,
502
+ files,
503
+ edges,
504
+ truncated: edges.length >= maxResults,
505
+ };
506
+ }
507
+ async ensureGraph(project, params) {
508
+ const existing = this.graphs.get(project.id);
509
+ if (existing)
510
+ return existing;
511
+ await this.buildCodegraph({ ...params, projectId: project.id });
512
+ const built = this.graphs.get(project.id);
513
+ if (!built)
514
+ throw new Error('Failed to build local codegraph');
515
+ return built;
516
+ }
517
+ async resolveProject(params) {
518
+ const projectId = stringValue(params.projectId || params.project_id);
519
+ if (projectId) {
520
+ const project = this.projects.get(projectId);
521
+ if (!project)
522
+ throw new Error(`Local project not found: ${projectId}. Please call open_local_project first.`);
523
+ project.updatedAt = Date.now();
524
+ return project;
525
+ }
526
+ const rawPath = stringValue(params.path);
527
+ if (rawPath) {
528
+ const rootPath = await this.resolveDirectory(rawPath);
529
+ const sessionId = stringValue(params.sessionId || params.session_id);
530
+ const id = this.projectIdFor(rootPath, sessionId);
531
+ const existing = this.projects.get(id);
532
+ if (existing) {
533
+ existing.updatedAt = Date.now();
534
+ return existing;
535
+ }
536
+ await this.openProject({
537
+ path: rootPath,
538
+ projectName: params.projectName || params.project_name,
539
+ baseBranch: params.baseBranch || params.base_branch,
540
+ sessionId,
541
+ });
542
+ return this.projects.get(id);
543
+ }
544
+ throw new Error('project_id or path is required');
545
+ }
546
+ async resolveDirectory(rawPath) {
547
+ if (!rawPath.trim())
548
+ throw new Error('path is required');
549
+ const resolved = path.resolve(rawPath);
550
+ const realPath = await fs.realpath(resolved);
551
+ const stat = await fs.stat(realPath);
552
+ if (!stat.isDirectory())
553
+ throw new Error(`Path is not a directory: ${rawPath}`);
554
+ return realPath;
555
+ }
556
+ async resolveFileInProject(project, rawFilePath) {
557
+ const candidate = path.isAbsolute(rawFilePath)
558
+ ? path.resolve(rawFilePath)
559
+ : path.resolve(project.rootPath, rawFilePath);
560
+ const realPath = await fs.realpath(candidate);
561
+ this.ensureInsideProject(project.rootPath, realPath);
562
+ const stat = await fs.stat(realPath);
563
+ if (!stat.isFile())
564
+ throw new Error(`Path is not a file: ${rawFilePath}`);
565
+ return realPath;
566
+ }
567
+ ensureInsideProject(rootPath, candidate) {
568
+ const relative = path.relative(rootPath, candidate);
569
+ if (relative === '')
570
+ return;
571
+ if (relative.startsWith('..') || path.isAbsolute(relative)) {
572
+ throw new Error('Requested path is outside of the opened local project');
573
+ }
574
+ }
575
+ projectIdFor(rootPath, sessionId) {
576
+ const hash = createHash('sha1').update(`${sessionId || 'global'}:${rootPath}`).digest('hex').slice(0, 12);
577
+ return `local_${hash}`;
578
+ }
579
+ async getGitSnapshot(project, options) {
580
+ const root = project.rootPath;
581
+ const insideWorkTree = await this.gitOutput(root, ['rev-parse', '--is-inside-work-tree']).catch(() => '');
582
+ if (insideWorkTree.trim() !== 'true') {
583
+ return { success: true, isGitRepo: false, is_git_repo: false, path: root };
584
+ }
585
+ const [branch, head, statusText, localBranchesText, remoteBranchesText, defaultRemoteRef] = await Promise.all([
586
+ this.gitOutput(root, ['rev-parse', '--abbrev-ref', 'HEAD']).catch(() => ''),
587
+ this.gitOutput(root, ['rev-parse', '--short', 'HEAD']).catch(() => ''),
588
+ this.gitOutput(root, ['status', '--porcelain=v1']).catch(() => ''),
589
+ this.gitOutput(root, ['for-each-ref', '--format=%(refname:short)', 'refs/heads'], 4 * 1024 * 1024).catch(() => ''),
590
+ this.gitOutput(root, ['for-each-ref', '--format=%(refname:short)', 'refs/remotes'], 4 * 1024 * 1024).catch(() => ''),
591
+ this.gitOutput(root, ['symbolic-ref', '--quiet', '--short', 'refs/remotes/origin/HEAD']).catch(() => ''),
592
+ ]);
593
+ const currentBranch = normalizeBranchName(branch);
594
+ const localBranches = unique([
595
+ ...parseBranchRefs(localBranchesText),
596
+ currentBranch,
597
+ ].filter(isRealBranchName));
598
+ const remoteBranches = parseBranchRefs(remoteBranchesText);
599
+ const branches = unique([...localBranches, ...remoteBranches]);
600
+ const defaultRemoteBranch = normalizeBranchName(defaultRemoteRef).replace(/\/HEAD$/, '');
601
+ const baseBranch = options.baseBranch || defaultRemoteBranch || await this.detectBaseBranch(root);
602
+ const changedFiles = parseGitStatus(statusText);
603
+ const result = {
604
+ success: true,
605
+ isGitRepo: true,
606
+ is_git_repo: true,
607
+ path: root,
608
+ projectId: project.id,
609
+ project_id: project.id,
610
+ branch: currentBranch || undefined,
611
+ currentBranch: currentBranch || undefined,
612
+ current_branch: currentBranch || undefined,
613
+ head: head.trim() || undefined,
614
+ baseBranch,
615
+ base_branch: baseBranch,
616
+ defaultBranch: defaultRemoteBranch || undefined,
617
+ default_branch: defaultRemoteBranch || undefined,
618
+ branches,
619
+ branchList: branches,
620
+ branch_list: branches,
621
+ allBranches: branches,
622
+ all_branches: branches,
623
+ localBranches,
624
+ local_branches: localBranches,
625
+ remoteBranches,
626
+ remote_branches: remoteBranches,
627
+ changedFiles,
628
+ changed_files: changedFiles,
629
+ status: statusText.trim(),
630
+ };
631
+ if (baseBranch) {
632
+ const mergeBase = await this.gitOutput(root, ['merge-base', 'HEAD', baseBranch]).catch(() => '');
633
+ if (mergeBase.trim()) {
634
+ const filesAgainstBase = await this.gitOutput(root, ['diff', '--name-status', mergeBase.trim(), 'HEAD']).catch(() => '');
635
+ result.committedChangedFiles = parseNameStatus(filesAgainstBase);
636
+ result.committed_changed_files = result.committedChangedFiles;
637
+ }
638
+ }
639
+ if (options.includeDiff !== false) {
640
+ const diffArgs = baseBranch
641
+ ? ['diff', '--stat', baseBranch, 'HEAD']
642
+ : ['diff', '--stat'];
643
+ const patchArgs = baseBranch
644
+ ? ['diff', baseBranch, 'HEAD']
645
+ : ['diff'];
646
+ result.diffStat = await this.gitOutput(root, diffArgs).catch(() => '');
647
+ result.diff_stat = result.diffStat;
648
+ result.diff = truncateText(await this.gitOutput(root, patchArgs, options.maxDiffBytes || 200000).catch(() => ''), options.maxDiffBytes || 200000);
649
+ result.diffTruncated = Buffer.byteLength(result.diff || '') >= (options.maxDiffBytes || 200000);
650
+ result.diff_truncated = result.diffTruncated;
651
+ }
652
+ return result;
653
+ }
654
+ async detectBaseBranch(rootPath) {
655
+ const candidates = ['origin/master', 'master', 'origin/main', 'main', 'origin/develop', 'develop'];
656
+ for (const candidate of candidates) {
657
+ const exists = await this.gitOutput(rootPath, ['rev-parse', '--verify', candidate]).then(() => true).catch(() => false);
658
+ if (exists)
659
+ return candidate;
660
+ }
661
+ return undefined;
662
+ }
663
+ async gitOutput(rootPath, args, maxBuffer = 1024 * 1024) {
664
+ const { stdout } = await execFileAsync('git', ['-C', rootPath, ...args], {
665
+ timeout: 30000,
666
+ maxBuffer,
667
+ env: { ...process.env, GIT_TERMINAL_PROMPT: '0' },
668
+ });
669
+ return String(stdout || '');
670
+ }
671
+ async collectFiles(rootPath, options) {
672
+ const results = [];
673
+ await this.walk(rootPath, rootPath, results, options);
674
+ return results;
675
+ }
676
+ async walk(rootPath, currentPath, results, options) {
677
+ if (results.length >= options.maxFiles)
678
+ return;
679
+ const entries = await fs.readdir(currentPath, { withFileTypes: true }).catch(() => []);
680
+ for (const entry of entries) {
681
+ if (results.length >= options.maxFiles)
682
+ break;
683
+ if (entry.name.startsWith('.') && entry.name !== '.env.example') {
684
+ if (entry.isDirectory() || EXCLUDED_DIR_NAMES.has(entry.name))
685
+ continue;
686
+ }
687
+ const absolutePath = path.join(currentPath, entry.name);
688
+ const relativePath = toPosix(path.relative(rootPath, absolutePath));
689
+ if (entry.isDirectory()) {
690
+ if (EXCLUDED_DIR_NAMES.has(entry.name) || this.matchesAny(relativePath, options.excludePatterns || []))
691
+ continue;
692
+ await this.walk(rootPath, absolutePath, results, options);
693
+ }
694
+ else if (entry.isFile()) {
695
+ if (!this.isTextLike(absolutePath))
696
+ continue;
697
+ if (options.includePatterns?.length && !this.matchesAny(relativePath, options.includePatterns))
698
+ continue;
699
+ if (this.matchesAny(relativePath, options.excludePatterns || []))
700
+ continue;
701
+ results.push(absolutePath);
702
+ }
703
+ }
704
+ }
705
+ isTextLike(filePath) {
706
+ const ext = path.extname(filePath).toLowerCase();
707
+ if (TEXT_EXTENSIONS.has(ext))
708
+ return true;
709
+ const base = path.basename(filePath).toLowerCase();
710
+ return ['dockerfile', 'makefile', 'gradlew', 'pom.xml'].includes(base);
711
+ }
712
+ async readSmallTextFile(filePath, maxBytes) {
713
+ const stat = await fs.stat(filePath);
714
+ if (stat.size > maxBytes) {
715
+ const handle = await fs.open(filePath, 'r');
716
+ try {
717
+ const buffer = Buffer.alloc(maxBytes);
718
+ const { bytesRead } = await handle.read(buffer, 0, maxBytes, 0);
719
+ return `${buffer.subarray(0, bytesRead).toString('utf8')}\n...[truncated ${stat.size - bytesRead} bytes]`;
720
+ }
721
+ finally {
722
+ await handle.close();
723
+ }
724
+ }
725
+ return fs.readFile(filePath, 'utf8');
726
+ }
727
+ async buildTree(rootPath, maxDepth, maxEntries) {
728
+ const entries = [];
729
+ await this.appendTree(rootPath, rootPath, entries, 0, maxDepth, maxEntries);
730
+ return entries;
731
+ }
732
+ async appendTree(rootPath, currentPath, entries, depth, maxDepth, maxEntries) {
733
+ if (entries.length >= maxEntries || depth > maxDepth)
734
+ return;
735
+ const dirents = await fs.readdir(currentPath, { withFileTypes: true }).catch(() => []);
736
+ for (const dirent of dirents) {
737
+ if (entries.length >= maxEntries)
738
+ break;
739
+ if (EXCLUDED_DIR_NAMES.has(dirent.name))
740
+ continue;
741
+ if (dirent.name.startsWith('.') && dirent.name !== '.env.example')
742
+ continue;
743
+ const absolutePath = path.join(currentPath, dirent.name);
744
+ entries.push({
745
+ path: toPosix(path.relative(rootPath, absolutePath)),
746
+ type: dirent.isDirectory() ? 'directory' : 'file',
747
+ });
748
+ if (dirent.isDirectory()) {
749
+ await this.appendTree(rootPath, absolutePath, entries, depth + 1, maxDepth, maxEntries);
750
+ }
751
+ }
752
+ }
753
+ resolveImport(rootPath, fromRelativePath, specifier, fileSet) {
754
+ if (!specifier.startsWith('.'))
755
+ return null;
756
+ const fromDir = path.dirname(path.resolve(rootPath, fromRelativePath));
757
+ const basePath = path.resolve(fromDir, specifier);
758
+ for (const suffix of EXTENSION_CANDIDATES) {
759
+ const relative = toPosix(path.relative(rootPath, `${basePath}${suffix}`));
760
+ if (fileSet.has(relative))
761
+ return relative;
762
+ }
763
+ return null;
764
+ }
765
+ findTargetFiles(graph, target) {
766
+ const lower = target.toLowerCase();
767
+ const matches = graph.files
768
+ .filter((file) => (file.path.toLowerCase() === lower ||
769
+ file.path.toLowerCase().endsWith(lower) ||
770
+ file.path.toLowerCase().includes(lower) ||
771
+ file.symbols.some((symbol) => symbol.toLowerCase().includes(lower))))
772
+ .map((file) => file.path);
773
+ return matches.slice(0, 20);
774
+ }
775
+ matchesAny(relativePath, patterns) {
776
+ return patterns.some((pattern) => globToRegExp(pattern).test(relativePath));
777
+ }
778
+ }
779
+ export const localCodeService = new LocalCodeService();
780
+ export function localCodeCapabilities() {
781
+ return localCodeService.getCapabilities();
782
+ }
783
+ function stringValue(value) {
784
+ if (value === undefined || value === null)
785
+ return undefined;
786
+ const text = String(value).trim();
787
+ return text || undefined;
788
+ }
789
+ function numberValue(value, fallback) {
790
+ const parsed = Number(value);
791
+ return Number.isFinite(parsed) ? parsed : fallback;
792
+ }
793
+ function stringArray(value) {
794
+ return Array.isArray(value) ? value.map((item) => String(item)).filter(Boolean) : [];
795
+ }
796
+ function escapeRegExp(value) {
797
+ return value.replace(/[.*+?^${}()|[\]\\]/g, '\\$&');
798
+ }
799
+ function globToRegExp(pattern) {
800
+ const escaped = pattern
801
+ .split('*')
802
+ .map((part) => escapeRegExp(part))
803
+ .join('.*')
804
+ .replace(/\.\\\*\\\*\/?/g, '(?:.*/)?');
805
+ return new RegExp(`^${escaped}$`);
806
+ }
807
+ function detectLanguage(filePath) {
808
+ const ext = path.extname(filePath).toLowerCase();
809
+ const map = {
810
+ '.ts': 'typescript',
811
+ '.tsx': 'typescript-react',
812
+ '.js': 'javascript',
813
+ '.jsx': 'javascript-react',
814
+ '.vue': 'vue',
815
+ '.java': 'java',
816
+ '.kt': 'kotlin',
817
+ '.go': 'go',
818
+ '.py': 'python',
819
+ '.rb': 'ruby',
820
+ '.php': 'php',
821
+ '.rs': 'rust',
822
+ '.cs': 'csharp',
823
+ '.swift': 'swift',
824
+ '.dart': 'dart',
825
+ };
826
+ return map[ext] || ext.replace('.', '') || 'text';
827
+ }
828
+ function extractImports(content, filePath) {
829
+ const imports = [];
830
+ const ext = path.extname(filePath).toLowerCase();
831
+ const patterns = [
832
+ /\bimport\s+(?:type\s+)?(?:[^'"]+\s+from\s+)?['"]([^'"]+)['"]/g,
833
+ /\bexport\s+[^'"]+\s+from\s+['"]([^'"]+)['"]/g,
834
+ /\brequire\(\s*['"]([^'"]+)['"]\s*\)/g,
835
+ /\bimport\(\s*['"]([^'"]+)['"]\s*\)/g,
836
+ ];
837
+ if (['.py'].includes(ext)) {
838
+ patterns.push(/^\s*from\s+([A-Za-z0-9_.$]+)\s+import\s+/gm, /^\s*import\s+([A-Za-z0-9_.$]+)/gm);
839
+ }
840
+ if (['.java', '.kt'].includes(ext)) {
841
+ patterns.push(/^\s*import\s+(?:static\s+)?([A-Za-z0-9_.*]+);?/gm);
842
+ }
843
+ if (['.go'].includes(ext)) {
844
+ patterns.push(/^\s*import\s+(?:\(\s*)?["`]([^"`]+)["`]/gm);
845
+ }
846
+ for (const pattern of patterns) {
847
+ for (const match of content.matchAll(pattern)) {
848
+ if (match[1])
849
+ imports.push(match[1]);
850
+ }
851
+ }
852
+ return imports;
853
+ }
854
+ function extractSymbols(content, filePath) {
855
+ const symbols = [];
856
+ const ext = path.extname(filePath).toLowerCase();
857
+ const patterns = [
858
+ /\b(?:export\s+)?(?:default\s+)?class\s+([A-Za-z_$][\w$]*)/g,
859
+ /\b(?:export\s+)?(?:async\s+)?function\s+([A-Za-z_$][\w$]*)/g,
860
+ /\b(?:export\s+)?(?:const|let|var)\s+([A-Za-z_$][\w$]*)\s*=/g,
861
+ /\b(?:export\s+)?interface\s+([A-Za-z_$][\w$]*)/g,
862
+ /\b(?:export\s+)?type\s+([A-Za-z_$][\w$]*)\s*=/g,
863
+ ];
864
+ if (['.java', '.kt', '.cs'].includes(ext)) {
865
+ patterns.push(/\b(?:public|private|protected|internal)?\s*(?:class|interface|enum)\s+([A-Za-z_$][\w$]*)/g);
866
+ }
867
+ if (ext === '.py') {
868
+ patterns.push(/^\s*(?:async\s+)?def\s+([A-Za-z_][\w]*)\s*\(/gm, /^\s*class\s+([A-Za-z_][\w]*)\s*[:(]/gm);
869
+ }
870
+ if (ext === '.go') {
871
+ patterns.push(/\bfunc\s+(?:\([^)]+\)\s*)?([A-Za-z_][\w]*)\s*\(/g, /\btype\s+([A-Za-z_][\w]*)\s+(?:struct|interface)/g);
872
+ }
873
+ for (const pattern of patterns) {
874
+ for (const match of content.matchAll(pattern)) {
875
+ if (match[1])
876
+ symbols.push(match[1]);
877
+ }
878
+ }
879
+ return symbols;
880
+ }
881
+ function parseGitStatus(statusText) {
882
+ return statusText
883
+ .split(/\r?\n/)
884
+ .map((line) => line.trimEnd())
885
+ .filter(Boolean)
886
+ .map((line) => ({
887
+ status: line.slice(0, 2).trim(),
888
+ path: line.slice(3),
889
+ }));
890
+ }
891
+ function parseNameStatus(text) {
892
+ return text
893
+ .split(/\r?\n/)
894
+ .map((line) => line.trim())
895
+ .filter(Boolean)
896
+ .map((line) => {
897
+ const [status, ...rest] = line.split(/\s+/);
898
+ return { status, path: rest.join(' ') };
899
+ });
900
+ }
901
+ function countBy(values) {
902
+ return values.reduce((acc, value) => {
903
+ acc[value] = (acc[value] || 0) + 1;
904
+ return acc;
905
+ }, {});
906
+ }
907
+ //# sourceMappingURL=local-code-service.js.map