ai-first-cli 1.3.5 → 1.3.6

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 (51) hide show
  1. package/CHANGELOG.md +63 -0
  2. package/README.es.md +55 -0
  3. package/README.md +40 -15
  4. package/ai/graph/knowledge-graph.json +1 -1
  5. package/dist/analyzers/architecture.d.ts.map +1 -1
  6. package/dist/analyzers/architecture.js +72 -5
  7. package/dist/analyzers/architecture.js.map +1 -1
  8. package/dist/analyzers/entrypoints.d.ts.map +1 -1
  9. package/dist/analyzers/entrypoints.js +253 -0
  10. package/dist/analyzers/entrypoints.js.map +1 -1
  11. package/dist/analyzers/symbols.d.ts.map +1 -1
  12. package/dist/analyzers/symbols.js +47 -2
  13. package/dist/analyzers/symbols.js.map +1 -1
  14. package/dist/analyzers/techStack.d.ts.map +1 -1
  15. package/dist/analyzers/techStack.js +43 -0
  16. package/dist/analyzers/techStack.js.map +1 -1
  17. package/dist/utils/fileUtils.d.ts.map +1 -1
  18. package/dist/utils/fileUtils.js +5 -0
  19. package/dist/utils/fileUtils.js.map +1 -1
  20. package/package.json +1 -1
  21. package/src/analyzers/architecture.ts +75 -6
  22. package/src/analyzers/entrypoints.ts +285 -0
  23. package/src/analyzers/symbols.ts +52 -2
  24. package/src/analyzers/techStack.ts +44 -0
  25. package/src/utils/fileUtils.ts +5 -0
  26. package/tests/entrypoints-languages.test.ts +373 -0
  27. package/tests/framework-detection.test.ts +296 -0
  28. package/BETA_EVALUATION_REPORT.md +0 -151
  29. package/ai-context/context/flows/App.json +0 -17
  30. package/ai-context/context/flows/DashboardPage.json +0 -14
  31. package/ai-context/context/flows/LoginPage.json +0 -14
  32. package/ai-context/context/flows/admin.json +0 -10
  33. package/ai-context/context/flows/androidresources.json +0 -11
  34. package/ai-context/context/flows/authController.json +0 -14
  35. package/ai-context/context/flows/entrypoints.json +0 -9
  36. package/ai-context/context/flows/fastapiAdapter.json +0 -14
  37. package/ai-context/context/flows/fastapiadapter.json +0 -11
  38. package/ai-context/context/flows/index.json +0 -19
  39. package/ai-context/context/flows/indexer.json +0 -9
  40. package/ai-context/context/flows/indexstate.json +0 -9
  41. package/ai-context/context/flows/init.json +0 -22
  42. package/ai-context/context/flows/main.json +0 -18
  43. package/ai-context/context/flows/mainactivity.json +0 -9
  44. package/ai-context/context/flows/models.json +0 -15
  45. package/ai-context/context/flows/posts.json +0 -15
  46. package/ai-context/context/flows/repoMapper.json +0 -20
  47. package/ai-context/context/flows/repomapper.json +0 -11
  48. package/ai-context/context/flows/serializers.json +0 -10
  49. package/dist/scripts/ai-context-evaluator.js +0 -367
  50. package/quick-evaluation-report-1774396002305.md +0 -64
  51. package/quick-evaluator.ts +0 -200
@@ -96,6 +96,33 @@ export function discoverEntrypoints(files: FileInfo[], rootDir: string): Entrypo
96
96
  } catch {}
97
97
  }
98
98
 
99
+ // Detect Go entrypoints
100
+ const goFiles = files.filter(f => f.extension === "go");
101
+ if (goFiles.length > 0) {
102
+ try {
103
+ const goEntrypoints = discoverGoEntrypoints(goFiles, rootDir);
104
+ entrypoints.push(...goEntrypoints);
105
+ } catch {}
106
+ }
107
+
108
+ // Detect Rust entrypoints
109
+ const rustFiles = files.filter(f => f.extension === "rs");
110
+ if (rustFiles.length > 0) {
111
+ try {
112
+ const rustEntrypoints = discoverRustEntrypoints(rustFiles, rootDir);
113
+ entrypoints.push(...rustEntrypoints);
114
+ } catch {}
115
+ }
116
+
117
+ // Detect PHP entrypoints
118
+ const phpFiles = files.filter(f => f.extension === "php");
119
+ if (phpFiles.length > 0) {
120
+ try {
121
+ const phpEntrypoints = discoverPHPEntrypoints(phpFiles, rootDir);
122
+ entrypoints.push(...phpEntrypoints);
123
+ } catch {}
124
+ }
125
+
99
126
  return entrypoints;
100
127
  }
101
128
 
@@ -276,6 +303,264 @@ function getScriptType(name: string): Entrypoint["type"] | null {
276
303
  return null;
277
304
  }
278
305
 
306
+ function discoverGoEntrypoints(goFiles: FileInfo[], rootDir: string): Entrypoint[] {
307
+ const entrypoints: Entrypoint[] = [];
308
+
309
+ for (const file of goFiles) {
310
+ try {
311
+ const content = readFile(path.join(rootDir, file.relativePath));
312
+ const fileName = file.name;
313
+
314
+ if (fileName === "main.go") {
315
+ const hasMain = content.match(/func\s+main\s*\(\s*\)/);
316
+ const packageMatch = content.match(/package\s+(\w+)/);
317
+ const packageName = packageMatch ? packageMatch[1] : "main";
318
+
319
+ const handlers: string[] = [];
320
+ const handlerMatches = content.matchAll(/http\.HandleFunc\s*\(\s*["']([^"']+)["']/g);
321
+ for (const match of handlerMatches) {
322
+ handlers.push(match[1]);
323
+ }
324
+
325
+ const portMatches = content.matchAll(/:\s*(\d{2,5})/g);
326
+ const ports: string[] = [];
327
+ for (const match of portMatches) {
328
+ ports.push(match[1]);
329
+ }
330
+
331
+ let description = `Go main package (${packageName})`;
332
+ if (handlers.length > 0) {
333
+ description += ` with HTTP handlers: ${handlers.join(", ")}`;
334
+ }
335
+ if (ports.length > 0) {
336
+ description += ` on port${ports.length > 1 ? "s" : ""} :${ports.join(", :")}`;
337
+ }
338
+
339
+ entrypoints.push({
340
+ name: "main.go",
341
+ path: file.relativePath,
342
+ type: hasMain ? "server" : "library",
343
+ description,
344
+ });
345
+ } else {
346
+ const structMatches = content.matchAll(/type\s+(\w+)\s+struct/g);
347
+ const structs: string[] = [];
348
+ for (const match of structMatches) {
349
+ structs.push(match[1]);
350
+ }
351
+
352
+ const methodMatches = content.matchAll(/func\s*\(?\s*\*?\s*(\w+)\s*\)?\s*(\w+)\s*\(/g);
353
+ const methods: string[] = [];
354
+ for (const match of methodMatches) {
355
+ methods.push(match[2]);
356
+ }
357
+
358
+ if (structs.length > 0 || methods.length > 0) {
359
+ let description = "Go module";
360
+ if (structs.length > 0) {
361
+ description += ` with structs: ${structs.slice(0, 3).join(", ")}`;
362
+ }
363
+ if (methods.length > 0) {
364
+ description += `, methods: ${methods.slice(0, 3).join(", ")}`;
365
+ }
366
+
367
+ entrypoints.push({
368
+ name: fileName,
369
+ path: file.relativePath,
370
+ type: "library",
371
+ description,
372
+ });
373
+ }
374
+ }
375
+ } catch {}
376
+ }
377
+
378
+ const goModPath = path.join(rootDir, "go.mod");
379
+ try {
380
+ const goMod = readFile(goModPath);
381
+ const moduleMatch = goMod.match(/module\s+(\S+)/);
382
+ if (moduleMatch) {
383
+ entrypoints.push({
384
+ name: "go.mod",
385
+ path: "go.mod",
386
+ type: "config",
387
+ description: `Go module: ${moduleMatch[1]}`,
388
+ });
389
+ }
390
+ } catch {}
391
+
392
+ return entrypoints;
393
+ }
394
+
395
+ function discoverRustEntrypoints(rustFiles: FileInfo[], rootDir: string): Entrypoint[] {
396
+ const entrypoints: Entrypoint[] = [];
397
+
398
+ for (const file of rustFiles) {
399
+ try {
400
+ const content = readFile(path.join(rootDir, file.relativePath));
401
+ const fileName = file.name;
402
+
403
+ if (fileName === "main.rs") {
404
+ const hasMain = content.match(/fn\s+main\s*\(\s*\)/);
405
+
406
+ const structMatches = content.matchAll(/struct\s+(\w+)/g);
407
+ const structs: string[] = [];
408
+ for (const match of structMatches) {
409
+ structs.push(match[1]);
410
+ }
411
+
412
+ const implMatches = content.matchAll(/impl\s+(?:\w+\s+for\s+)?(\w+)/g);
413
+ const implementations: string[] = [];
414
+ for (const match of implMatches) {
415
+ implementations.push(match[1]);
416
+ }
417
+
418
+ let description = "Rust main";
419
+ if (structs.length > 0) {
420
+ description += ` with structs: ${structs.slice(0, 3).join(", ")}`;
421
+ }
422
+ if (implementations.length > 0) {
423
+ description += `, implementations: ${implementations.slice(0, 3).join(", ")}`;
424
+ }
425
+
426
+ entrypoints.push({
427
+ name: "main.rs",
428
+ path: file.relativePath,
429
+ type: hasMain ? "cli" : "library",
430
+ description,
431
+ });
432
+ } else if (fileName === "lib.rs") {
433
+ const pubFnMatches = content.matchAll(/pub\s+fn\s+(\w+)/g);
434
+ const publicFns: string[] = [];
435
+ for (const match of pubFnMatches) {
436
+ publicFns.push(match[1]);
437
+ }
438
+
439
+ let description = "Rust library";
440
+ if (publicFns.length > 0) {
441
+ description += ` with public functions: ${publicFns.slice(0, 3).join(", ")}`;
442
+ }
443
+
444
+ entrypoints.push({
445
+ name: "lib.rs",
446
+ path: file.relativePath,
447
+ type: "library",
448
+ description,
449
+ });
450
+ }
451
+ } catch {}
452
+ }
453
+
454
+ const cargoPath = path.join(rootDir, "Cargo.toml");
455
+ try {
456
+ const cargoContent = readFile(cargoPath);
457
+ const nameMatch = cargoContent.match(/name\s*=\s*"([^"]+)"/);
458
+ const versionMatch = cargoContent.match(/version\s*=\s*"([^"]+)"/);
459
+
460
+ let description = "Rust project";
461
+ if (nameMatch) {
462
+ description += `: ${nameMatch[1]}`;
463
+ }
464
+ if (versionMatch) {
465
+ description += ` v${versionMatch[1]}`;
466
+ }
467
+
468
+ const binMatch = cargoContent.match(/\[\[bin\]\]/);
469
+ if (binMatch) {
470
+ description += " (has binaries)";
471
+ }
472
+
473
+ entrypoints.push({
474
+ name: "Cargo.toml",
475
+ path: "Cargo.toml",
476
+ type: "config",
477
+ description,
478
+ });
479
+ } catch {}
480
+
481
+ return entrypoints;
482
+ }
483
+
484
+ function discoverPHPEntrypoints(phpFiles: FileInfo[], rootDir: string): Entrypoint[] {
485
+ const entrypoints: Entrypoint[] = [];
486
+
487
+ const hasIndexPhp = phpFiles.some(f => f.name === "index.php");
488
+ const hasPublicIndex = phpFiles.some(f => f.relativePath.includes("public/index.php"));
489
+
490
+ if (hasIndexPhp) {
491
+ const indexFile = phpFiles.find(f => f.name === "index.php")!;
492
+ try {
493
+ const content = readFile(path.join(rootDir, indexFile.relativePath));
494
+
495
+ const classMatches = content.matchAll(/class\s+(\w+)/g);
496
+ const classes: string[] = [];
497
+ for (const match of classMatches) {
498
+ classes.push(match[1]);
499
+ }
500
+
501
+ const routes: string[] = [];
502
+
503
+ // Match $router->get('/path') or $app->post('/path') patterns
504
+ const httpMethodMatches = content.matchAll(/->\s*(?:get|post|put|delete|patch)\s*\(\s*["']([^"']+)["']/g);
505
+ for (const match of httpMethodMatches) {
506
+ routes.push(match[1]);
507
+ }
508
+
509
+ // Match $router->add('METHOD', '/path') patterns (second argument is the path)
510
+ const addMethodMatches = content.matchAll(/->\s*add\s*\(\s*["'][^"']+["']\s*,\s*["']([^"']+)["']/g);
511
+ for (const match of addMethodMatches) {
512
+ routes.push(match[1]);
513
+ }
514
+
515
+ let description = "PHP entry point";
516
+ if (classes.length > 0) {
517
+ description += ` with classes: ${classes.slice(0, 3).join(", ")}`;
518
+ }
519
+ if (routes.length > 0) {
520
+ description += `, routes: ${routes.slice(0, 3).join(", ")}`;
521
+ }
522
+
523
+ entrypoints.push({
524
+ name: "index.php",
525
+ path: indexFile.relativePath,
526
+ type: hasPublicIndex ? "server" : "api",
527
+ description,
528
+ });
529
+ } catch {}
530
+ }
531
+
532
+ const composerPath = path.join(rootDir, "composer.json");
533
+ try {
534
+ const composer = readJsonFile(composerPath) as { name?: string; description?: string; require?: Record<string, string> };
535
+
536
+ let description = "PHP project";
537
+ if (composer.name) {
538
+ description += `: ${composer.name}`;
539
+ }
540
+ if (composer.description) {
541
+ description += ` - ${composer.description}`;
542
+ }
543
+
544
+ const hasLaravel = composer.require && (composer.require["laravel/framework"] || composer.require["illuminate/support"]);
545
+ const hasSymfony = composer.require && (composer.require["symfony/framework-bundle"] || composer.require["symfony/symfony"]);
546
+
547
+ if (hasLaravel) {
548
+ description += " (Laravel)";
549
+ } else if (hasSymfony) {
550
+ description += " (Symfony)";
551
+ }
552
+
553
+ entrypoints.push({
554
+ name: "composer.json",
555
+ path: "composer.json",
556
+ type: "config",
557
+ description,
558
+ });
559
+ } catch {}
560
+
561
+ return entrypoints;
562
+ }
563
+
279
564
  export function generateEntrypointsFile(entrypoints: Entrypoint[]): string {
280
565
  const grouped = new Map<string, Entrypoint[]>();
281
566
  for (const ep of entrypoints) {
@@ -533,8 +533,24 @@ function parseSwift(file: FileInfo, content: string, lines: string[], symbols: S
533
533
  * Parse Apex (Salesforce) files
534
534
  */
535
535
  function parseApex(file: FileInfo, content: string, lines: string[], symbols: Symbol[]): void {
536
+ // Track annotations across lines
537
+ let pendingAnnotations: string[] = [];
538
+
536
539
  for (let i = 0; i < lines.length; i++) {
537
540
  const line = lines[i].trim();
541
+
542
+ // Skip empty lines
543
+ if (!line) {
544
+ continue;
545
+ }
546
+
547
+ // Collect annotations (@AuraEnabled, @IsTest, etc.) - handles both single-line and multi-line
548
+ // Match patterns like @AuraEnabled, @AuraEnabled(cacheable=true), @IsTest
549
+ const annotationMatch = line.match(/^@(\w+)(?:\s*\([^)]*\))?\s*$/);
550
+ if (annotationMatch) {
551
+ pendingAnnotations.push(annotationMatch[1]);
552
+ continue;
553
+ }
538
554
 
539
555
  // Classes: public with sharing class ClassName, public class ClassName, etc.
540
556
  const classMatch = line.match(/^(?:\s*(?:public|private|global)(?:\s+(?:with|without|inherited)\s+sharing)?\s+)?class\s+(\w+)/);
@@ -547,6 +563,8 @@ function parseApex(file: FileInfo, content: string, lines: string[], symbols: Sy
547
563
  line: i + 1,
548
564
  export: true,
549
565
  });
566
+ pendingAnnotations = [];
567
+ continue;
550
568
  }
551
569
 
552
570
  // Interfaces
@@ -560,11 +578,16 @@ function parseApex(file: FileInfo, content: string, lines: string[], symbols: Sy
560
578
  line: i + 1,
561
579
  export: true,
562
580
  });
581
+ pendingAnnotations = [];
582
+ continue;
563
583
  }
564
584
 
565
585
  // Methods: public static ReturnType methodName(
566
586
  // Also handles @AuraEnabled public static ReturnType methodName(
567
- const methodMatch = line.match(/^(?:@\w+\s+)?(?:public|private|protected|global)\s+(?:static\s+)?(?:\w+)\s+(\w+)\s*\(/);
587
+ // Also handles @AuraEnabled(cacheable=true) on separate line
588
+ // Handles generic return types like List<Account>, Map<String, Object>
589
+ // Also handles webservice methods
590
+ const methodMatch = line.match(/^(?:@\w+(?:\s*\([^)]*\))?\s+)?(?:public|private|protected|global|webservice)\s+(?:static\s+)?(?:[\w<>,\s]+?)\s+(\w+)\s*\(/);
568
591
  if (methodMatch && !["if", "for", "while", "switch"].includes(methodMatch[1])) {
569
592
  symbols.push({
570
593
  id: generateSymbolId(file.relativePath, methodMatch[1]),
@@ -572,8 +595,28 @@ function parseApex(file: FileInfo, content: string, lines: string[], symbols: Sy
572
595
  type: "function",
573
596
  file: file.relativePath,
574
597
  line: i + 1,
575
- export: line.includes("public") || line.includes("global"),
598
+ export: line.includes("public") || line.includes("global") || line.includes("webservice"),
576
599
  });
600
+ pendingAnnotations = [];
601
+ continue;
602
+ }
603
+
604
+ // Alternative: Method with annotations on previous lines
605
+ // Check if we have pending annotations and current line looks like a method
606
+ if (pendingAnnotations.length > 0) {
607
+ const methodWithAnnotationMatch = line.match(/^(?:public|private|protected|global|webservice)\s+(?:static\s+)?(?:[\w<>,\s]+?)\s+(\w+)\s*\(/);
608
+ if (methodWithAnnotationMatch && !["if", "for", "while", "switch"].includes(methodWithAnnotationMatch[1])) {
609
+ symbols.push({
610
+ id: generateSymbolId(file.relativePath, methodWithAnnotationMatch[1]),
611
+ name: methodWithAnnotationMatch[1],
612
+ type: "function",
613
+ file: file.relativePath,
614
+ line: i + 1,
615
+ export: line.includes("public") || line.includes("global") || line.includes("webservice"),
616
+ });
617
+ pendingAnnotations = [];
618
+ continue;
619
+ }
577
620
  }
578
621
 
579
622
  // Triggers: trigger TriggerName on ObjectName
@@ -587,6 +630,13 @@ function parseApex(file: FileInfo, content: string, lines: string[], symbols: Sy
587
630
  line: i + 1,
588
631
  export: true,
589
632
  });
633
+ pendingAnnotations = [];
634
+ continue;
635
+ }
636
+
637
+ // Reset pending annotations if we encounter non-annotation, non-method line
638
+ if (!line.startsWith("@")) {
639
+ pendingAnnotations = [];
590
640
  }
591
641
  }
592
642
  }
@@ -134,6 +134,18 @@ function detectFrameworks(files: FileInfo[], fileNames: Set<string>, rootDir: st
134
134
  frameworks.push(...names);
135
135
  }
136
136
  }
137
+
138
+ // Check for @nestjs/* packages (NestJS framework)
139
+ const hasNestJs = Object.keys(deps).some(dep => dep.startsWith("@nestjs/"));
140
+ if (hasNestJs && !frameworks.includes("NestJS")) {
141
+ frameworks.push("NestJS");
142
+ }
143
+
144
+ // Check for Spring Boot in dependencies
145
+ const hasSpringBoot = Object.keys(deps).some(dep => dep.startsWith("@spring.io/") || dep.includes("spring-boot"));
146
+ if (hasSpringBoot && !frameworks.includes("Spring Boot")) {
147
+ frameworks.push("Spring Boot");
148
+ }
137
149
  } catch {}
138
150
 
139
151
  const frameworkIndicators: Record<string, string> = {
@@ -165,6 +177,38 @@ function detectFrameworks(files: FileInfo[], fileNames: Set<string>, rootDir: st
165
177
  }
166
178
  }
167
179
 
180
+ // Detect Spring Boot from pom.xml
181
+ try {
182
+ const pomPath = path.join(rootDir, "pom.xml");
183
+ if (fs.existsSync(pomPath)) {
184
+ const pomContent = readFile(pomPath);
185
+ if (pomContent.includes("spring-boot") && !frameworks.includes("Spring Boot")) {
186
+ frameworks.push("Spring Boot");
187
+ }
188
+ if (pomContent.includes("spring-boot-starter-parent") && !frameworks.includes("Spring Boot")) {
189
+ frameworks.push("Spring Boot");
190
+ }
191
+ }
192
+ } catch {}
193
+
194
+ // Detect Spring Boot from build.gradle
195
+ try {
196
+ const gradlePath = path.join(rootDir, "build.gradle");
197
+ const gradleKtsPath = path.join(rootDir, "build.gradle.kts");
198
+ let gradleContent = "";
199
+ if (fs.existsSync(gradlePath)) {
200
+ gradleContent = readFile(gradlePath);
201
+ } else if (fs.existsSync(gradleKtsPath)) {
202
+ gradleContent = readFile(gradleKtsPath);
203
+ }
204
+ if (gradleContent.includes("spring-boot") && !frameworks.includes("Spring Boot")) {
205
+ frameworks.push("Spring Boot");
206
+ }
207
+ if (gradleContent.includes("org.springframework.boot") && !frameworks.includes("Spring Boot")) {
208
+ frameworks.push("Spring Boot");
209
+ }
210
+ } catch {}
211
+
168
212
  // Detect SwiftUI from Swift files
169
213
  const swiftFiles = files.filter(f => f.extension === "swift");
170
214
  for (const swiftFile of swiftFiles) {
@@ -22,6 +22,11 @@ export const DEFAULT_EXCLUDE_PATTERNS = [
22
22
  "target",
23
23
  ".cache",
24
24
  ".DS_Store",
25
+ "test-projects",
26
+ ".ai-dev",
27
+ ".ai-dev-out",
28
+ "ai-context",
29
+ ".ai-first-ignore",
25
30
  ];
26
31
 
27
32
  /**