codeflow-hook 1.4.0 → 2.0.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,748 @@
1
+ #!/usr/bin/env node
2
+
3
+ /**
4
+ * CLI Integration Service - Phase 4
5
+ *
6
+ * Bridges the local CLI commands with EKG backend services.
7
+ * Transforms CLI operations from local processing to backend-driven workflows.
8
+ *
9
+ * Key transformations:
10
+ * - `codeflow index` → EKG Ingestion Service webhook simulation
11
+ * - `codeflow analyze-diff` → EKG Query Service context-enhanced analysis
12
+ */
13
+
14
+ import axios from 'axios';
15
+ import { simpleGit, SimpleGit } from 'simple-git';
16
+ import { config } from 'dotenv';
17
+ import winston from 'winston';
18
+ import path from 'path';
19
+ import fs from 'fs';
20
+
21
+ // Load environment variables
22
+ config();
23
+
24
+ // Configure logger
25
+ const logger = winston.createLogger({
26
+ level: process.env.LOG_LEVEL || 'info',
27
+ format: winston.format.combine(
28
+ winston.format.timestamp(),
29
+ winston.format.errors({ stack: true }),
30
+ winston.format.json()
31
+ ),
32
+ defaultMeta: { service: 'cli-integration' },
33
+ transports: [
34
+ new winston.transports.Console({
35
+ format: winston.format.combine(
36
+ winston.format.colorize(),
37
+ winston.format.simple()
38
+ )
39
+ }),
40
+ new winston.transports.File({ filename: 'cli-integration.log' })
41
+ ]
42
+ });
43
+
44
+ /**
45
+ * Configuration for backend services
46
+ */
47
+ interface EKGBackendConfig {
48
+ ingestionServiceUrl: string;
49
+ queryServiceUrl: string;
50
+ timeout: number;
51
+ retries: number;
52
+ }
53
+
54
+ /**
55
+ * CLI Integration Service
56
+ * Provides methods that CLI commands can call to interact with EKG backend
57
+ */
58
+ export class CLIIntegrationService {
59
+ private config: EKGBackendConfig;
60
+ private git: SimpleGit;
61
+
62
+ constructor() {
63
+ this.config = {
64
+ ingestionServiceUrl: process.env.INGESTION_SERVICE_URL || 'http://localhost:3000',
65
+ queryServiceUrl: process.env.QUERY_SERVICE_URL || 'http://localhost:4000',
66
+ timeout: parseInt(process.env.REQUEST_TIMEOUT || '30000'),
67
+ retries: parseInt(process.env.REQUEST_RETRIES || '3')
68
+ };
69
+
70
+ this.git = simpleGit();
71
+ logger.info('CLI Integration Service initialized', this.config);
72
+ }
73
+
74
+ /**
75
+ * Index repository for EKG - equivalent to `codeflow index`
76
+ *
77
+ * Sends repository URL to EKG Ingestion Service for analysis and graph population
78
+ */
79
+ async indexRepository(options: {
80
+ repositoryUrl?: string;
81
+ dryRun?: boolean;
82
+ } = {}): Promise<{
83
+ success: boolean;
84
+ repositoryId?: string;
85
+ message: string;
86
+ stats?: {
87
+ indexedFiles: number;
88
+ analysisTime: number;
89
+ webhookAccepted: boolean;
90
+ }
91
+ }> {
92
+ const startTime = Date.now();
93
+
94
+ try {
95
+ // Get repository information
96
+ const repoInfo = await this.getRepositoryInfo();
97
+
98
+ if (options.dryRun) {
99
+ logger.info('Dry run mode - would index repository', repoInfo);
100
+ const filesToIndex = await this.getIndexableFiles(repoInfo.repositoryPath);
101
+
102
+ return {
103
+ success: true,
104
+ message: `Dry run: Would index ${filesToIndex.length} files from ${repoInfo.fullName}`,
105
+ stats: {
106
+ indexedFiles: filesToIndex.length,
107
+ analysisTime: Date.now() - startTime,
108
+ webhookAccepted: true // Would be accepted
109
+ }
110
+ };
111
+ }
112
+
113
+ // Calculate repository ID for tracking
114
+ const repositoryId = this.generateRepositoryId(repoInfo.fullName);
115
+
116
+ logger.info('Indexing repository via EKG backend', {
117
+ repositoryId,
118
+ fullName: repoInfo.fullName,
119
+ url: repoInfo.cloneUrl
120
+ });
121
+
122
+ // Send webhook payload to EKG Ingestion Service
123
+ const webhookPayload = {
124
+ action: 'repository.indexed',
125
+ repository: {
126
+ id: repositoryId,
127
+ name: repoInfo.name,
128
+ full_name: repoInfo.fullName,
129
+ clone_url: repoInfo.cloneUrl,
130
+ html_url: repoInfo.htmlUrl,
131
+ private: repoInfo.isPrivate
132
+ },
133
+ sender: {
134
+ login: this.getCurrentUser(),
135
+ type: 'User'
136
+ },
137
+ installation: {
138
+ id: 'cli-integration',
139
+ node_id: 'cli-integration-node'
140
+ },
141
+ // Add PRISM analysis request
142
+ prism: {
143
+ includePatterns: true,
144
+ includeDependencies: true,
145
+ includeMetrics: true
146
+ }
147
+ };
148
+
149
+ const response = await this.makeBackendRequest(
150
+ `${this.config.ingestionServiceUrl}/webhooks/github`,
151
+ webhookPayload,
152
+ {
153
+ 'X-GitHub-Event': 'repository.indexed',
154
+ 'X-GitHub-Delivery': `delivery-cli-${repositoryId}-${Date.now()}`,
155
+ 'X-Request-ID': `req-cli-index-${repositoryId}`,
156
+ 'Content-Type': 'application/json'
157
+ }
158
+ );
159
+
160
+ const analysisTime = Date.now() - startTime;
161
+
162
+ logger.info('Repository indexing initiated', {
163
+ repositoryId,
164
+ responseStatus: response.status,
165
+ analysisTime
166
+ });
167
+
168
+ return {
169
+ success: true,
170
+ repositoryId,
171
+ message: `Repository ${repoInfo.fullName} submitted for EKG analysis`,
172
+ stats: {
173
+ indexedFiles: 0, // Will be populated by backend
174
+ analysisTime,
175
+ webhookAccepted: response.status === 200
176
+ }
177
+ };
178
+
179
+ } catch (error) {
180
+ const errorMessage = this.formatError(error);
181
+ logger.error('Repository indexing failed', { error: errorMessage });
182
+
183
+ return {
184
+ success: false,
185
+ message: `Repository indexing failed: ${errorMessage}`,
186
+ stats: {
187
+ indexedFiles: 0,
188
+ analysisTime: Date.now() - startTime,
189
+ webhookAccepted: false
190
+ }
191
+ };
192
+ }
193
+ }
194
+
195
+ /**
196
+ * Analyze code diff with EKG context enhancement
197
+ *
198
+ * Sends diff to Query Service for EKG-enhanced analysis instead of local RAG
199
+ */
200
+ async analyzeDiff(diffContent: string, options: {
201
+ legacy?: boolean;
202
+ outputFormat?: 'console' | 'json';
203
+ } = {}): Promise<{
204
+ success: boolean;
205
+ analysis: any;
206
+ message: string;
207
+ stats?: {
208
+ ekg_queries: number;
209
+ similar_repos_found: number;
210
+ analysis_time: number;
211
+ }
212
+ }> {
213
+ const startTime = Date.now();
214
+
215
+ try {
216
+ if (!diffContent.trim()) {
217
+ return {
218
+ success: true,
219
+ analysis: { type: 'no-changes', message: 'No changes to analyze' },
220
+ message: 'No changes to analyze'
221
+ };
222
+ }
223
+
224
+ if (options.legacy) {
225
+ // Fallback to local analysis (would integrate with existing agents)
226
+ logger.warn('Legacy mode requested - falling back to local analysis');
227
+ return {
228
+ success: false,
229
+ analysis: null,
230
+ message: 'Legacy mode not yet implemented with EKG integration'
231
+ };
232
+ }
233
+
234
+ logger.info('Analyzing diff with EKG context enhancement', {
235
+ diffSize: diffContent.length,
236
+ lines: diffContent.split('\n').length
237
+ });
238
+
239
+ // Analyze diff and extract context
240
+ const diffAnalysis = this.analyzeDiffContent(diffContent);
241
+
242
+ if (diffAnalysis.files.length === 0) {
243
+ return {
244
+ success: true,
245
+ analysis: { type: 'no-relevant-changes', message: 'No code changes detected' },
246
+ message: 'No code changes detected in diff'
247
+ };
248
+ }
249
+
250
+ // Query EKG for context on affected files
251
+ const ekgContext = await this.getEKGContext(diffAnalysis);
252
+
253
+ // Generate enhanced analysis with EKG data
254
+ const enhancedAnalysis = await this.generateEKGEnhancedAnalysis(diffAnalysis, ekgContext);
255
+
256
+ const analysisTime = Date.now() - startTime;
257
+
258
+ logger.info('Diff analysis completed with EKG enhancement', {
259
+ affectedFiles: diffAnalysis.files.length,
260
+ ekgQueries: ekgContext.queriesMade,
261
+ similarReposFound: ekgContext.similarRepositories?.length || 0,
262
+ analysisTime
263
+ });
264
+
265
+ return {
266
+ success: true,
267
+ analysis: enhancedAnalysis,
268
+ message: 'Diff analyzed with EKG context enhancement',
269
+ stats: {
270
+ ekg_queries: ekgContext.queriesMade,
271
+ similar_repos_found: ekgContext.similarRepositories?.length || 0,
272
+ analysis_time: analysisTime
273
+ }
274
+ };
275
+
276
+ } catch (error) {
277
+ const errorMessage = this.formatError(error);
278
+ logger.error('Diff analysis failed', { error: errorMessage });
279
+
280
+ return {
281
+ success: false,
282
+ analysis: null,
283
+ message: `Diff analysis failed: ${errorMessage}`,
284
+ stats: {
285
+ ekg_queries: 0,
286
+ similar_repos_found: 0,
287
+ analysis_time: Date.now() - startTime
288
+ }
289
+ };
290
+ }
291
+ }
292
+
293
+ /**
294
+ * Analyze diff content and extract structured information
295
+ */
296
+ private analyzeDiffContent(diffContent: string): {
297
+ files: Array<{
298
+ path: string;
299
+ additions: number;
300
+ deletions: number;
301
+ isNew: boolean;
302
+ language: string;
303
+ }>;
304
+ totalAdditions: number;
305
+ totalDeletions: number;
306
+ summary: string;
307
+ } {
308
+ const files: Array<{
309
+ path: string;
310
+ additions: number;
311
+ deletions: number;
312
+ isNew: boolean;
313
+ language: string;
314
+ }> = [];
315
+
316
+ const lines = diffContent.split('\n');
317
+ let currentFile: Partial<typeof files[0]> | null = null;
318
+ let totalAdditions = 0;
319
+ let totalDeletions = 0;
320
+
321
+ for (const line of lines) {
322
+ if (line.startsWith('diff --git')) {
323
+ // Save previous file if exists
324
+ if (currentFile) {
325
+ files.push(currentFile as typeof files[0]);
326
+ }
327
+
328
+ // Extract file path
329
+ const match = line.match(/diff --git a\/(.+) b\/(.+)/);
330
+ if (match && match[2]) {
331
+ currentFile = {
332
+ path: match[2],
333
+ additions: 0,
334
+ deletions: 0,
335
+ isNew: false,
336
+ language: this.detectLanguage(match[2])
337
+ };
338
+ }
339
+ } else if (line.startsWith('new file mode')) {
340
+ if (currentFile) {
341
+ currentFile.isNew = true;
342
+ }
343
+ } else if (line.startsWith('+') && !line.startsWith('+++')) {
344
+ if (currentFile) {
345
+ currentFile.additions!++;
346
+ totalAdditions++;
347
+ }
348
+ } else if (line.startsWith('-') && !line.startsWith('---')) {
349
+ if (currentFile) {
350
+ currentFile.deletions!++;
351
+ totalDeletions++;
352
+ }
353
+ }
354
+ }
355
+
356
+ // Push final file
357
+ if (currentFile) {
358
+ files.push(currentFile as typeof files[0]);
359
+ }
360
+
361
+ const summary = `Modified ${files.length} files: +${totalAdditions} -${totalDeletions}`;
362
+
363
+ return {
364
+ files,
365
+ totalAdditions,
366
+ totalDeletions,
367
+ summary
368
+ };
369
+ }
370
+
371
+ /**
372
+ * Query EKG for context on affected files
373
+ */
374
+ private async getEKGContext(diffAnalysis: any): Promise<{
375
+ queriesMade: number;
376
+ repositoryIntelligence?: any;
377
+ similarRepositories: any[];
378
+ patterns: any[];
379
+ }> {
380
+ let queriesMade = 0;
381
+
382
+ try {
383
+ // Get current repository information
384
+ const repoInfo = await this.getRepositoryInfo();
385
+ const repositoryId = this.generateRepositoryId(repoInfo.fullName);
386
+
387
+ // Query repository intelligence if repository exists in EKG
388
+ let repositoryIntelligence = null;
389
+ try {
390
+ const response = await this.makeGraphQLRequest(
391
+ `
392
+ query GetRepositoryIntelligence($repoId: ID!) {
393
+ repositoryIntelligence(repositoryId: $repoId) {
394
+ repository {
395
+ id name fullName language
396
+ }
397
+ patterns {
398
+ name type confidence category
399
+ }
400
+ dependencies {
401
+ dependencyType currentVersion confidence
402
+ }
403
+ }
404
+ }
405
+ `,
406
+ { repoId: repositoryId }
407
+ );
408
+ repositoryIntelligence = response.data?.repositoryIntelligence;
409
+ queriesMade++;
410
+ } catch (error) {
411
+ logger.warn('Repository not found in EKG, continuing analysis', { repositoryId });
412
+ }
413
+
414
+ // Find similar repositories and patterns for context
415
+ let similarRepositories: any[] = [];
416
+ try {
417
+ const response = await this.makeGraphQLRequest(
418
+ `
419
+ query FindSimilarRepositories($repoId: ID!, $limit: Int) {
420
+ similarRepositories(repositoryId: $repoId, limit: $limit) {
421
+ repository { name fullName language }
422
+ similarityScore reasons
423
+ sharedPatterns sizeComparison
424
+ }
425
+ }
426
+ `,
427
+ { repoId: repositoryId, limit: 5 }
428
+ );
429
+ similarRepositories = response.data?.similarRepositories || [];
430
+ queriesMade++;
431
+ } catch (error) {
432
+ logger.warn('Could not fetch similar repositories', { error: this.formatError(error) });
433
+ }
434
+
435
+ // Get enterprise-wide patterns that might be relevant
436
+ let patterns: any[] = [];
437
+ try {
438
+ const languages = [...new Set(diffAnalysis.files.map((f: any) => f.language))];
439
+
440
+ const response = await this.makeGraphQLRequest(
441
+ `
442
+ query GetRelevantPatterns($language: String, $limit: Int) {
443
+ patterns(language: $language, minConfidence: 0.7, limit: $limit) {
444
+ name type category confidence observationCount
445
+ }
446
+ }
447
+ `,
448
+ { language: languages[0], limit: 10 }
449
+ );
450
+ patterns = response.data?.patterns || [];
451
+ queriesMade++;
452
+ } catch (error) {
453
+ logger.warn('Could not fetch patterns', { error: this.formatError(error) });
454
+ }
455
+
456
+ return {
457
+ queriesMade,
458
+ repositoryIntelligence,
459
+ similarRepositories,
460
+ patterns
461
+ };
462
+
463
+ } catch (error) {
464
+ logger.error('EKG context retrieval failed', { error: this.formatError(error) });
465
+ return {
466
+ queriesMade,
467
+ similarRepositories: [],
468
+ patterns: []
469
+ };
470
+ }
471
+ }
472
+
473
+ /**
474
+ * Generate enhanced analysis using EKG context
475
+ */
476
+ private async generateEKGEnhancedAnalysis(diffAnalysis: any, ekgContext: any): Promise<any> {
477
+ // Analyze changes with EKG context
478
+ const issues: any[] = [];
479
+ const recommendations: any[] = [];
480
+
481
+ // Check against existing patterns
482
+ if (ekgContext.patterns && ekgContext.patterns.length > 0) {
483
+ for (const file of diffAnalysis.files) {
484
+ const relevantPatterns = ekgContext.patterns.filter((p: any) =>
485
+ p.type === 'security' || p.type === 'architecture'
486
+ );
487
+
488
+ if (relevantPatterns.length > 0) {
489
+ recommendations.push({
490
+ type: 'ekg_pattern_alignment',
491
+ description: `File ${file.path} modified - consider these established patterns: ${relevantPatterns.map((p: any) => p.name).join(', ')}`,
492
+ severity: 'info',
493
+ file: file.path
494
+ });
495
+ }
496
+ }
497
+ }
498
+
499
+ // Compare against similar repositories
500
+ if (ekgContext.similarRepositories && ekgContext.similarRepositories.length > 0) {
501
+ const similarRepoNames = ekgContext.similarRepositories.map((sr: any) => sr.repository.fullName);
502
+ recommendations.push({
503
+ type: 'similar_repositories',
504
+ description: `Changes similar to patterns seen in: ${similarRepoNames.slice(0, 3).join(', ')}`,
505
+ severity: 'info'
506
+ });
507
+ }
508
+
509
+ // Add repository-specific context if available
510
+ if (ekgContext.repositoryIntelligence) {
511
+ const repo = ekgContext.repositoryIntelligence.repository;
512
+ issues.push({
513
+ type: 'repository_context',
514
+ description: `Analyzing changes in repository ${repo.fullName} (${repo.language})`,
515
+ severity: 'info'
516
+ });
517
+ }
518
+
519
+ return {
520
+ summary: {
521
+ totalFiles: diffAnalysis.files.length,
522
+ totalAdditions: diffAnalysis.totalAdditions,
523
+ totalDeletions: diffAnalysis.totalDeletions,
524
+ ekgEnhanced: true
525
+ },
526
+ files: diffAnalysis.files.map((file: any) => ({
527
+ path: file.path,
528
+ language: file.language,
529
+ additions: file.additions,
530
+ deletions: file.deletions,
531
+ isNew: file.isNew
532
+ })),
533
+ issues,
534
+ recommendations,
535
+ ekg_context: {
536
+ patterns_analyzed: ekgContext.patterns?.length || 0,
537
+ similar_repositories_found: ekgContext.similarRepositories?.length || 0,
538
+ repository_known: !!ekgContext.repositoryIntelligence
539
+ }
540
+ };
541
+ }
542
+
543
+ /**
544
+ * Get current repository information
545
+ */
546
+ private async getRepositoryInfo(): Promise<{
547
+ name: string;
548
+ fullName: string;
549
+ repositoryPath: string;
550
+ cloneUrl: string;
551
+ htmlUrl: string;
552
+ isPrivate: boolean;
553
+ }> {
554
+ try {
555
+ const remotes = await this.git.getRemotes(true);
556
+ const originRemote = remotes.find((r: any) => r.name === 'origin');
557
+
558
+ if (!originRemote) {
559
+ throw new Error('No origin remote found');
560
+ }
561
+
562
+ // Parse GitHub URL
563
+ const urlMatch = originRemote.refs.fetch.match(/github\.com[:/](.+?)(\.git)?$/);
564
+ if (!urlMatch) {
565
+ throw new Error('Remote URL is not a GitHub repository');
566
+ }
567
+
568
+ const fullName = urlMatch[1];
569
+ if (!fullName) {
570
+ throw new Error('Could not extract repository name from remote URL');
571
+ }
572
+
573
+ const repoParts = fullName.split('/');
574
+ const repo = repoParts[1];
575
+ if (!repo) {
576
+ throw new Error('Could not extract repository name from full name');
577
+ }
578
+
579
+ return {
580
+ name: repo,
581
+ fullName,
582
+ repositoryPath: process.cwd(),
583
+ cloneUrl: `https://github.com/${fullName}.git`,
584
+ htmlUrl: `https://github.com/${fullName}`,
585
+ isPrivate: false // Assume public unless told otherwise
586
+ };
587
+ } catch (error) {
588
+ throw new Error(`Could not determine repository information: ${this.formatError(error)}`);
589
+ }
590
+ }
591
+
592
+ /**
593
+ * Get list of files that would be indexed
594
+ */
595
+ private async getIndexableFiles(repoPath: string): Promise<string[]> {
596
+ // This mimics the logic from the original RAG indexer
597
+ const keyFileExtensions = [
598
+ '.js', '.ts', '.tsx', '.jsx', '.py', '.java', '.cpp', '.c', '.cs',
599
+ '.php', '.rb', '.go', '.rs', '.swift', '.kt', '.scala', '.clj'
600
+ ];
601
+
602
+ try {
603
+ const files: string[] = [];
604
+
605
+ const walkDirectory = (dir: string, relativePath = '') => {
606
+ const items = fs.readdirSync(dir);
607
+
608
+ for (const item of items) {
609
+ const fullPath = path.join(dir, item);
610
+ const relativeFilePath = path.join(relativePath, item);
611
+ const stat = fs.statSync(fullPath);
612
+
613
+ // Skip common exclude patterns
614
+ if (item.startsWith('.') || item === 'node_modules' || item === 'dist' ||
615
+ item === 'build' || item === 'target' || item === '.git') {
616
+ continue;
617
+ }
618
+
619
+ if (stat.isDirectory()) {
620
+ walkDirectory(fullPath, relativeFilePath);
621
+ } else if (stat.isFile()) {
622
+ const ext = path.extname(item);
623
+ if (keyFileExtensions.includes(ext)) {
624
+ files.push(relativeFilePath);
625
+ }
626
+ }
627
+ }
628
+ };
629
+
630
+ walkDirectory(repoPath);
631
+ return files;
632
+ } catch (error) {
633
+ logger.warn('Could not scan repository files', { error: this.formatError(error) });
634
+ return [];
635
+ }
636
+ }
637
+
638
+ /**
639
+ * Make HTTP request to backend service with retry logic
640
+ */
641
+ private async makeBackendRequest(
642
+ url: string,
643
+ data: any,
644
+ headers: Record<string, string> = {}
645
+ ): Promise<any> {
646
+ let lastError: any;
647
+
648
+ for (let attempt = 1; attempt <= this.config.retries; attempt++) {
649
+ try {
650
+ const response = await axios.post(url, data, {
651
+ headers: {
652
+ ...headers,
653
+ 'X-Attempt': attempt.toString()
654
+ },
655
+ timeout: this.config.timeout
656
+ });
657
+
658
+ return response;
659
+ } catch (error) {
660
+ lastError = error;
661
+ logger.warn(`Backend request attempt ${attempt} failed`, {
662
+ url,
663
+ attempt,
664
+ error: this.formatError(error)
665
+ });
666
+
667
+ if (attempt < this.config.retries) {
668
+ // Wait before retry (exponential backoff)
669
+ await new Promise(resolve => setTimeout(resolve, Math.pow(2, attempt) * 1000));
670
+ }
671
+ }
672
+ }
673
+
674
+ throw lastError;
675
+ }
676
+
677
+ /**
678
+ * Make GraphQL request to Query Service
679
+ */
680
+ private async makeGraphQLRequest(query: string, variables: any = {}): Promise<any> {
681
+ return this.makeBackendRequest(
682
+ `${this.config.queryServiceUrl}/graphql`,
683
+ { query, variables },
684
+ { 'Content-Type': 'application/json' }
685
+ );
686
+ }
687
+
688
+ /**
689
+ * Generate repository ID (similar to ingestion service)
690
+ */
691
+ private generateRepositoryId(fullName: string): string {
692
+ return `${fullName.replace('/', '-')}-${Date.now().toString(36)}`;
693
+ }
694
+
695
+ /**
696
+ * Get current user information
697
+ */
698
+ private getCurrentUser(): string {
699
+ return process.env.USER || process.env.USERNAME || 'anonymous';
700
+ }
701
+
702
+ /**
703
+ * Detect language from file extension
704
+ */
705
+ private detectLanguage(filePath: string): string {
706
+ const ext = path.extname(filePath).toLowerCase();
707
+ const languageMap: Record<string, string> = {
708
+ '.js': 'javascript',
709
+ '.ts': 'typescript',
710
+ '.tsx': 'typescript',
711
+ '.jsx': 'javascript',
712
+ '.py': 'python',
713
+ '.java': 'java',
714
+ '.cpp': 'cpp',
715
+ '.c': 'c',
716
+ '.cs': 'csharp',
717
+ '.php': 'php',
718
+ '.rb': 'ruby',
719
+ '.go': 'go',
720
+ '.rs': 'rust',
721
+ '.swift': 'swift',
722
+ '.kt': 'kotlin',
723
+ '.scala': 'scala'
724
+ };
725
+
726
+ return languageMap[ext] || 'unknown';
727
+ }
728
+
729
+ /**
730
+ * Format error for logging and display
731
+ */
732
+ private formatError(error: any): string {
733
+ if (axios.isAxiosError(error)) {
734
+ return `HTTP ${error.response?.status}: ${error.response?.statusText || error.message}`;
735
+ }
736
+ return error.message || 'Unknown error';
737
+ }
738
+ }
739
+
740
+ // Export singleton instance for CLI use
741
+ export const cliIntegrationService = new CLIIntegrationService();
742
+
743
+ // Export main methods for backward compatibility
744
+ export const indexProject = cliIntegrationService.indexRepository.bind(cliIntegrationService);
745
+ export const analyzeDiff = cliIntegrationService.analyzeDiff.bind(cliIntegrationService);
746
+
747
+ // Default export
748
+ export default cliIntegrationService;