codesummary 1.2.1 → 1.2.2

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/src/cli.js CHANGED
@@ -8,8 +8,19 @@ import { createRequire } from 'module';
8
8
  import ConfigManager from './configManager.js';
9
9
  import Scanner from './scanner.js';
10
10
  import PDFGenerator from './pdfGenerator.js';
11
- import RagGenerator from './ragGenerator.js';
11
+
12
12
  import LlmGenerator from './llmGenerator.js';
13
+ import LlmSummaryRenderer from './renderers/llmSummaryRenderer.js';
14
+ import ProviderClient from './ai/providerClient.js';
15
+ import {
16
+ buildPdfProjectIntroPrompt,
17
+ buildPdfArchitectureInsightsPrompt,
18
+ buildPdfOperationsInsightsPrompt,
19
+ buildPdfMaintenanceAndSecurityPrompt,
20
+ buildPdfVisualInsightsPrompt,
21
+ buildPdfApplicabilityPrompt,
22
+ buildPdfExecutiveBriefPrompt
23
+ } from './ai/promptTemplates.js';
13
24
  import ErrorHandler from './errorHandler.js';
14
25
  import { formatFileSize, resolveVersionedPath } from './utils.js';
15
26
 
@@ -74,6 +85,19 @@ export class CLI {
74
85
  help: false,
75
86
  version: false,
76
87
  noInteractive: false,
88
+ emitLlmSummary: false,
89
+ focus: null,
90
+ maxTokens: null,
91
+ aiSemantic: false,
92
+ aiProvider: null,
93
+ aiBaseUrl: null,
94
+ aiApiKey: null,
95
+ aiModel: null,
96
+ aiTimeoutMs: null,
97
+ aiRetries: null,
98
+ aiRetryBackoffMs: null,
99
+ aiMaxBackoffMs: null,
100
+ includeCode: null,
77
101
  format: 'pdf'
78
102
  };
79
103
 
@@ -136,15 +160,112 @@ export class CLI {
136
160
  case '--no-interactive':
137
161
  options.noInteractive = true;
138
162
  break;
163
+ case '--emit-llmsummary':
164
+ options.emitLlmSummary = true;
165
+ break;
166
+ case '--focus':
167
+ if (i + 1 >= args.length) {
168
+ throw new Error(`Option ${arg} requires a value`);
169
+ }
170
+ i++;
171
+ options.focus = args[i];
172
+ break;
173
+ case '--max-tokens':
174
+ if (i + 1 >= args.length) {
175
+ throw new Error(`Option ${arg} requires a value`);
176
+ }
177
+ i++;
178
+ options.maxTokens = Number.parseInt(args[i], 10);
179
+ if (!Number.isInteger(options.maxTokens) || options.maxTokens <= 0) {
180
+ throw new Error(`Invalid --max-tokens value: ${args[i]}. Use a positive integer.`);
181
+ }
182
+ break;
183
+ case '--ai-semantic':
184
+ options.aiSemantic = true;
185
+ break;
186
+ case '--provider':
187
+ if (i + 1 >= args.length) {
188
+ throw new Error(`Option ${arg} requires a value`);
189
+ }
190
+ i++;
191
+ options.aiProvider = args[i];
192
+ break;
193
+ case '--base-url':
194
+ if (i + 1 >= args.length) {
195
+ throw new Error(`Option ${arg} requires a value`);
196
+ }
197
+ i++;
198
+ options.aiBaseUrl = args[i];
199
+ break;
200
+ case '--api-key':
201
+ if (i + 1 >= args.length) {
202
+ throw new Error(`Option ${arg} requires a value`);
203
+ }
204
+ i++;
205
+ options.aiApiKey = args[i];
206
+ break;
207
+ case '--model':
208
+ if (i + 1 >= args.length) {
209
+ throw new Error(`Option ${arg} requires a value`);
210
+ }
211
+ i++;
212
+ options.aiModel = args[i];
213
+ break;
214
+ case '--timeout-ms':
215
+ if (i + 1 >= args.length) {
216
+ throw new Error(`Option ${arg} requires a value`);
217
+ }
218
+ i++;
219
+ options.aiTimeoutMs = Number.parseInt(args[i], 10);
220
+ if (!Number.isInteger(options.aiTimeoutMs) || options.aiTimeoutMs <= 0) {
221
+ throw new Error(`Invalid --timeout-ms value: ${args[i]}. Use a positive integer.`);
222
+ }
223
+ break;
224
+ case '--ai-retries':
225
+ if (i + 1 >= args.length) {
226
+ throw new Error(`Option ${arg} requires a value`);
227
+ }
228
+ i++;
229
+ options.aiRetries = Number.parseInt(args[i], 10);
230
+ if (!Number.isInteger(options.aiRetries) || options.aiRetries < 0) {
231
+ throw new Error(`Invalid --ai-retries value: ${args[i]}. Use an integer >= 0.`);
232
+ }
233
+ break;
234
+ case '--ai-retry-backoff-ms':
235
+ if (i + 1 >= args.length) {
236
+ throw new Error(`Option ${arg} requires a value`);
237
+ }
238
+ i++;
239
+ options.aiRetryBackoffMs = Number.parseInt(args[i], 10);
240
+ if (!Number.isInteger(options.aiRetryBackoffMs) || options.aiRetryBackoffMs <= 0) {
241
+ throw new Error(`Invalid --ai-retry-backoff-ms value: ${args[i]}. Use a positive integer.`);
242
+ }
243
+ break;
244
+ case '--ai-max-backoff-ms':
245
+ if (i + 1 >= args.length) {
246
+ throw new Error(`Option ${arg} requires a value`);
247
+ }
248
+ i++;
249
+ options.aiMaxBackoffMs = Number.parseInt(args[i], 10);
250
+ if (!Number.isInteger(options.aiMaxBackoffMs) || options.aiMaxBackoffMs <= 0) {
251
+ throw new Error(`Invalid --ai-max-backoff-ms value: ${args[i]}. Use a positive integer.`);
252
+ }
253
+ break;
254
+ case '--no-code':
255
+ options.includeCode = false;
256
+ break;
257
+ case '--include-code':
258
+ options.includeCode = true;
259
+ break;
139
260
  case '--format':
140
261
  case '-f':
141
262
  if (i + 1 >= args.length) {
142
- throw new Error(`Option ${arg} requires a value (pdf or rag)`);
263
+ throw new Error(`Option ${arg} requires a value (pdf, llm, or both)`);
143
264
  }
144
265
  i++;
145
266
  const format = args[i].toLowerCase();
146
- if (!['pdf', 'rag', 'both', 'llm'].includes(format)) {
147
- throw new Error(`Invalid format: ${format}. Use 'pdf', 'rag', 'llm', or 'both'`);
267
+ if (!['pdf', 'both', 'llm'].includes(format)) {
268
+ throw new Error(`Invalid format: ${format}. Use 'pdf', 'llm', or 'both'`);
148
269
  }
149
270
  options.format = format;
150
271
  break;
@@ -185,6 +306,9 @@ export class CLI {
185
306
  // Determine scan path (default: current working directory)
186
307
  const scanPath = process.cwd();
187
308
  const projectName = path.basename(scanPath);
309
+ const aiRuntimeOptions = this.buildAiRuntimeOptions(options);
310
+
311
+ await this.confirmAiShareRiskIfNeeded(options, aiRuntimeOptions);
188
312
 
189
313
  console.log(chalk.cyan(`CodeSummary - Scanning project: ${chalk.bold(projectName)}\n`));
190
314
 
@@ -216,22 +340,33 @@ export class CLI {
216
340
  const totalFiles = this.calculateTotalFiles(filesByExtension, selectedExtensions);
217
341
  await this.checkFileCountThreshold(totalFiles, options.noInteractive);
218
342
 
219
- // Generate output based on format (--both runs pdf+rag; errors are collected)
343
+ // Generate output based on format
220
344
  const runPdf = options.format === 'pdf' || options.format === 'both';
221
- const runRag = options.format === 'rag' || options.format === 'both';
222
- const runLlm = options.format === 'llm';
345
+ const runLlm = options.format === 'llm' || options.format === 'both';
223
346
  const generationErrors = [];
224
347
 
225
348
  if (runPdf) {
226
349
  try {
227
350
  const outputPath = this.determineOutputPath(options.output, projectName);
228
351
  await PDFGenerator.ensureOutputDirectory(path.dirname(outputPath));
352
+ const pdfContentMode = this.resolvePdfContentMode(options, aiRuntimeOptions);
353
+ const aiProjectContext = await this.generateAiPdfProjectContext(
354
+ filesByExtension,
355
+ selectedExtensions,
356
+ projectName,
357
+ aiRuntimeOptions
358
+ );
229
359
  const pdfSpinner = ora('Generating PDF document...').start();
230
360
  const result = await this.pdfGenerator.generatePDF(
231
361
  filesByExtension,
232
362
  selectedExtensions,
233
363
  outputPath,
234
- projectName
364
+ projectName,
365
+ {
366
+ aiProjectContext,
367
+ includeFileTree: pdfContentMode.includeFileTree,
368
+ includeSourceCode: pdfContentMode.includeSourceCode
369
+ }
235
370
  );
236
371
  pdfSpinner.succeed('PDF generation completed');
237
372
  await this.displayCompletionSummary(result.outputPath, selectedExtensions, totalFiles, result.pageCount);
@@ -241,27 +376,6 @@ export class CLI {
241
376
  }
242
377
  }
243
378
 
244
- if (runRag) {
245
- try {
246
- const ragOutputPath = this.determineRagOutputPath(options.output, projectName);
247
- await fs.ensureDir(path.dirname(ragOutputPath));
248
- const ragSpinner = ora('Generating RAG-optimized output...').start();
249
- const ragGenerator = new RagGenerator();
250
- const result = await ragGenerator.generateRagOutput(
251
- filesByExtension,
252
- selectedExtensions,
253
- ragOutputPath,
254
- projectName,
255
- scanPath
256
- );
257
- ragSpinner.succeed('RAG output generation completed');
258
- await this.displayRagCompletionSummary(result.outputPath, selectedExtensions, totalFiles, result.totalChunks);
259
- } catch (error) {
260
- generationErrors.push(`RAG: ${error.message}`);
261
- console.error(chalk.red(`ERROR generating RAG output: ${error.message}`));
262
- }
263
- }
264
-
265
379
  if (runLlm) {
266
380
  try {
267
381
  const llmOutputPath = this.determineLlmOutputPath(options.output, projectName);
@@ -272,10 +386,25 @@ export class CLI {
272
386
  filesByExtension,
273
387
  selectedExtensions,
274
388
  llmOutputPath,
275
- projectName
389
+ projectName,
390
+ {
391
+ focus: options.focus,
392
+ maxTokens: options.maxTokens,
393
+ ai: aiRuntimeOptions
394
+ }
276
395
  );
277
396
  llmSpinner.succeed('LLM output generation completed');
278
397
  await this.displayLlmCompletionSummary(result.outputPath, selectedExtensions, totalFiles);
398
+
399
+ if (options.emitLlmSummary) {
400
+ const llmSummaryOutputPath = this.determineLlmSummaryOutputPath(options.output);
401
+ await fs.ensureDir(path.dirname(llmSummaryOutputPath));
402
+ const summarySpinner = ora('Generating .llmsummary.json...').start();
403
+ const llmSummaryRenderer = new LlmSummaryRenderer();
404
+ const summaryResult = await llmSummaryRenderer.render(llmSummaryOutputPath, result.summaryData);
405
+ summarySpinner.succeed('.llmsummary.json generation completed');
406
+ await this.displayLlmSummaryCompletionSummary(summaryResult.outputPath);
407
+ }
279
408
  } catch (error) {
280
409
  generationErrors.push(`LLM: ${error.message}`);
281
410
  console.error(chalk.red(`ERROR generating LLM output: ${error.message}`));
@@ -372,19 +501,23 @@ export class CLI {
372
501
  }
373
502
 
374
503
  /**
375
- * Determine final output path for RAG format
504
+ * Determine final output path for PDF
376
505
  * @param {string} overridePath - Optional override path from CLI
377
506
  * @param {string} projectName - Project name
378
507
  * @returns {string} Final output path
379
508
  */
380
- determineRagOutputPath(overridePath, projectName) {
509
+ determineOutputPath(overridePath, projectName) {
381
510
  let outputDir;
382
511
 
383
512
  if (overridePath) {
513
+ // Validate and sanitize override path from CLI
384
514
  const sanitizedPath = ErrorHandler.sanitizeInput(overridePath, { allowPath: true });
385
515
  ErrorHandler.validatePath(sanitizedPath, { preventTraversal: true });
516
+
386
517
  outputDir = path.resolve(sanitizedPath);
518
+ console.log(chalk.gray(`PDF will be saved to: ${outputDir}`));
387
519
  } else {
520
+ // Use config settings
388
521
  if (this.config.output.mode === 'relative') {
389
522
  outputDir = process.cwd();
390
523
  } else {
@@ -392,28 +525,26 @@ export class CLI {
392
525
  }
393
526
  }
394
527
 
528
+ // Sanitize project name for filename
395
529
  const sanitizedProjectName = ErrorHandler.sanitizeInput(projectName);
396
- return resolveVersionedPath(path.join(outputDir, `${sanitizedProjectName}_rag.json`));
530
+
531
+ return PDFGenerator.generateOutputPath(sanitizedProjectName, outputDir);
397
532
  }
398
533
 
399
534
  /**
400
- * Determine final output path for PDF
535
+ * Determine final output path for LLM Markdown format
401
536
  * @param {string} overridePath - Optional override path from CLI
402
537
  * @param {string} projectName - Project name
403
538
  * @returns {string} Final output path
404
539
  */
405
- determineOutputPath(overridePath, projectName) {
540
+ determineLlmOutputPath(overridePath, projectName) {
406
541
  let outputDir;
407
542
 
408
543
  if (overridePath) {
409
- // Validate and sanitize override path from CLI
410
544
  const sanitizedPath = ErrorHandler.sanitizeInput(overridePath, { allowPath: true });
411
545
  ErrorHandler.validatePath(sanitizedPath, { preventTraversal: true });
412
-
413
546
  outputDir = path.resolve(sanitizedPath);
414
- console.log(chalk.gray(`PDF will be saved to: ${outputDir}`));
415
547
  } else {
416
- // Use config settings
417
548
  if (this.config.output.mode === 'relative') {
418
549
  outputDir = process.cwd();
419
550
  } else {
@@ -421,35 +552,634 @@ export class CLI {
421
552
  }
422
553
  }
423
554
 
424
- // Sanitize project name for filename
425
555
  const sanitizedProjectName = ErrorHandler.sanitizeInput(projectName);
426
-
427
- return PDFGenerator.generateOutputPath(sanitizedProjectName, outputDir);
556
+ return resolveVersionedPath(path.join(outputDir, `${sanitizedProjectName}_llm.md`));
428
557
  }
429
558
 
430
559
  /**
431
- * Determine final output path for LLM Markdown format
560
+ * Determine final output path for .llmsummary.json artifact
432
561
  * @param {string} overridePath - Optional override path from CLI
433
- * @param {string} projectName - Project name
434
562
  * @returns {string} Final output path
435
563
  */
436
- determineLlmOutputPath(overridePath, projectName) {
564
+ determineLlmSummaryOutputPath(overridePath) {
437
565
  let outputDir;
438
566
 
439
567
  if (overridePath) {
440
568
  const sanitizedPath = ErrorHandler.sanitizeInput(overridePath, { allowPath: true });
441
569
  ErrorHandler.validatePath(sanitizedPath, { preventTraversal: true });
442
570
  outputDir = path.resolve(sanitizedPath);
571
+ } else if (this.config.output.mode === 'relative') {
572
+ outputDir = process.cwd();
443
573
  } else {
444
- if (this.config.output.mode === 'relative') {
445
- outputDir = process.cwd();
446
- } else {
447
- outputDir = path.resolve(this.config.output.fixedPath);
574
+ outputDir = path.resolve(this.config.output.fixedPath);
575
+ }
576
+
577
+ return resolveVersionedPath(path.join(outputDir, '.llmsummary.json'));
578
+ }
579
+
580
+ buildAiRuntimeOptions(options) {
581
+ const configAi = this.config?.ai || {};
582
+ const provider = options.aiProvider || configAi.provider || 'openai-compatible';
583
+ const baseUrl = options.aiBaseUrl || configAi.baseUrl || 'http://localhost:11434/v1';
584
+ const apiKey = options.aiApiKey ?? configAi.apiKey ?? '';
585
+ const model = options.aiModel || configAi.model || (provider === 'ollama' ? 'llama3.1' : '');
586
+ const timeoutMs = options.aiTimeoutMs || configAi.timeoutMs || 30000;
587
+ const maxRetries = Number.isInteger(options.aiRetries) ? options.aiRetries : (configAi.maxRetries ?? 2);
588
+ const retryBackoffMs = Number.isInteger(options.aiRetryBackoffMs) ? options.aiRetryBackoffMs : (configAi.retryBackoffMs ?? 500);
589
+ const maxBackoffMs = Number.isInteger(options.aiMaxBackoffMs) ? options.aiMaxBackoffMs : (configAi.maxBackoffMs ?? 5000);
590
+ const enabled = Boolean(options.aiSemantic) || configAi.enabled === true;
591
+
592
+ return {
593
+ enabled,
594
+ semantic: options.aiSemantic || configAi.enabled === true,
595
+ provider,
596
+ baseUrl,
597
+ apiKey,
598
+ model,
599
+ timeoutMs,
600
+ maxRetries,
601
+ retryBackoffMs,
602
+ maxBackoffMs
603
+ };
604
+ }
605
+
606
+ resolvePdfContentMode(options, aiRuntimeOptions) {
607
+ if (typeof options.includeCode === 'boolean') {
608
+ return {
609
+ includeFileTree: true,
610
+ includeSourceCode: options.includeCode
611
+ };
612
+ }
613
+
614
+ const aiExecutiveMode = Boolean(aiRuntimeOptions?.enabled && aiRuntimeOptions?.semantic);
615
+ return {
616
+ includeFileTree: true,
617
+ includeSourceCode: !aiExecutiveMode
618
+ };
619
+ }
620
+
621
+ buildSelectedFilesList(filesByExtension, selectedExtensions) {
622
+ const allFiles = [];
623
+ selectedExtensions.forEach(ext => {
624
+ if (filesByExtension[ext]) {
625
+ allFiles.push(...filesByExtension[ext]);
626
+ }
627
+ });
628
+ allFiles.sort((a, b) => a.relativePath.localeCompare(b.relativePath));
629
+ return allFiles;
630
+ }
631
+
632
+ safeParseJson(rawText) {
633
+ if (typeof rawText !== 'string') return null;
634
+ const trimmed = rawText.trim();
635
+ try {
636
+ return JSON.parse(trimmed);
637
+ } catch {
638
+ const fencedMatch = trimmed.match(/```(?:json)?\s*([\s\S]*?)```/i);
639
+ if (!fencedMatch) return null;
640
+ try {
641
+ return JSON.parse(fencedMatch[1].trim());
642
+ } catch {
643
+ const start = trimmed.indexOf('{');
644
+ const end = trimmed.lastIndexOf('}');
645
+ if (start === -1 || end === -1 || end <= start) return null;
646
+ try {
647
+ return JSON.parse(trimmed.slice(start, end + 1));
648
+ } catch {
649
+ return null;
650
+ }
448
651
  }
449
652
  }
653
+ }
450
654
 
451
- const sanitizedProjectName = ErrorHandler.sanitizeInput(projectName);
452
- return resolveVersionedPath(path.join(outputDir, `${sanitizedProjectName}_llm.md`));
655
+ normalizePdfProjectContext(payload) {
656
+ if (!payload || typeof payload !== 'object') return null;
657
+ const overview = typeof payload.overview === 'string' ? payload.overview.trim() : '';
658
+ const primaryPurpose = typeof payload.primaryPurpose === 'string' ? payload.primaryPurpose.trim() : '';
659
+ const keyComponents = Array.isArray(payload.keyComponents)
660
+ ? payload.keyComponents.filter(item => typeof item === 'string').map(item => item.trim()).filter(Boolean).slice(0, 8)
661
+ : [];
662
+ const suggestedReadingPath = Array.isArray(payload.suggestedReadingPath)
663
+ ? payload.suggestedReadingPath.filter(item => typeof item === 'string').map(item => item.trim()).filter(Boolean).slice(0, 8)
664
+ : [];
665
+
666
+ if (!overview && !primaryPurpose && keyComponents.length === 0 && suggestedReadingPath.length === 0) {
667
+ return null;
668
+ }
669
+
670
+ return {
671
+ overview,
672
+ primaryPurpose,
673
+ keyComponents,
674
+ suggestedReadingPath
675
+ };
676
+ }
677
+
678
+ normalizeStringList(list, maxItems = 10) {
679
+ if (!Array.isArray(list)) return [];
680
+ return list
681
+ .filter(item => typeof item === 'string')
682
+ .map(item => item.trim())
683
+ .filter(Boolean)
684
+ .slice(0, maxItems);
685
+ }
686
+
687
+ normalizePdfArchitectureContext(payload) {
688
+ if (!payload || typeof payload !== 'object') return null;
689
+ const structuralPatterns = this.normalizeStringList(payload.structuralPatterns, 8);
690
+ const implementationParadigm = this.normalizeStringList(payload.implementationParadigm, 8);
691
+ const couplingPoints = this.normalizeStringList(payload.couplingPoints, 8);
692
+
693
+ if (
694
+ structuralPatterns.length === 0 &&
695
+ implementationParadigm.length === 0 &&
696
+ couplingPoints.length === 0
697
+ ) {
698
+ return null;
699
+ }
700
+
701
+ return {
702
+ structuralPatterns,
703
+ implementationParadigm,
704
+ couplingPoints
705
+ };
706
+ }
707
+
708
+ normalizePdfOperationsContext(payload) {
709
+ if (!payload || typeof payload !== 'object') return null;
710
+ const inboundOutbound = this.normalizeStringList(payload?.dataAndStateLifecycle?.inboundOutbound, 10);
711
+ const criticalTransformations = this.normalizeStringList(payload?.dataAndStateLifecycle?.criticalTransformations, 10);
712
+ const stateManagement = this.normalizeStringList(payload?.dataAndStateLifecycle?.stateManagement, 10);
713
+ const configurationHierarchy = this.normalizeStringList(payload?.configurationAndEnvironmentStrategy?.configurationHierarchy, 10);
714
+ const infrastructureDependencies = this.normalizeStringList(payload?.configurationAndEnvironmentStrategy?.infrastructureDependencies, 10);
715
+ const responsibilityDistribution = this.normalizeStringList(payload?.languageSpecificBreakdown?.responsibilityDistribution, 10);
716
+ const interoperability = this.normalizeStringList(payload?.languageSpecificBreakdown?.interoperability, 10);
717
+
718
+ if (
719
+ inboundOutbound.length === 0 &&
720
+ criticalTransformations.length === 0 &&
721
+ stateManagement.length === 0 &&
722
+ configurationHierarchy.length === 0 &&
723
+ infrastructureDependencies.length === 0 &&
724
+ responsibilityDistribution.length === 0 &&
725
+ interoperability.length === 0
726
+ ) {
727
+ return null;
728
+ }
729
+
730
+ return {
731
+ dataAndStateLifecycle: {
732
+ inboundOutbound,
733
+ criticalTransformations,
734
+ stateManagement
735
+ },
736
+ configurationAndEnvironmentStrategy: {
737
+ configurationHierarchy,
738
+ infrastructureDependencies
739
+ },
740
+ languageSpecificBreakdown: {
741
+ responsibilityDistribution,
742
+ interoperability
743
+ }
744
+ };
745
+ }
746
+
747
+ normalizePdfMaintenanceAndSecurityContext(payload) {
748
+ if (!payload || typeof payload !== 'object') return null;
749
+ const complexityHotspots = this.normalizeStringList(payload?.onboardingAndMaintenanceGuide?.complexityHotspots, 10);
750
+ const modificationGuide = this.normalizeStringList(payload?.onboardingAndMaintenanceGuide?.modificationGuide, 10);
751
+ const domainGlossary = this.normalizeStringList(payload?.onboardingAndMaintenanceGuide?.domainGlossary, 16);
752
+ const exposureSurface = this.normalizeStringList(payload?.securityAndComplianceSurface?.exposureSurface, 10);
753
+ const sensitiveDataHandling = this.normalizeStringList(payload?.securityAndComplianceSurface?.sensitiveDataHandling, 10);
754
+
755
+ if (
756
+ complexityHotspots.length === 0 &&
757
+ modificationGuide.length === 0 &&
758
+ domainGlossary.length === 0 &&
759
+ exposureSurface.length === 0 &&
760
+ sensitiveDataHandling.length === 0
761
+ ) {
762
+ return null;
763
+ }
764
+
765
+ return {
766
+ onboardingAndMaintenanceGuide: {
767
+ complexityHotspots,
768
+ modificationGuide,
769
+ domainGlossary
770
+ },
771
+ securityAndComplianceSurface: {
772
+ exposureSurface,
773
+ sensitiveDataHandling
774
+ }
775
+ };
776
+ }
777
+
778
+ normalizePdfVisualContext(payload) {
779
+ if (!payload || typeof payload !== 'object') return null;
780
+ const semanticDirectoryTree = this.normalizeStringList(payload.semanticDirectoryTree, 60);
781
+ const dependencyTextGraph = this.normalizeStringList(payload.dependencyTextGraph, 60);
782
+
783
+ if (semanticDirectoryTree.length === 0 && dependencyTextGraph.length === 0) {
784
+ return null;
785
+ }
786
+
787
+ return {
788
+ semanticDirectoryTree,
789
+ dependencyTextGraph
790
+ };
791
+ }
792
+
793
+ normalizePdfApplicabilityContext(payload) {
794
+ if (!payload || typeof payload !== 'object') return null;
795
+ const infrastructureAsCode = this.normalizeStringList(payload?.whyItHelpsAcrossProjects?.infrastructureAsCode, 8);
796
+ const dataPlatforms = this.normalizeStringList(payload?.whyItHelpsAcrossProjects?.dataPlatforms, 8);
797
+ const frontendApplications = this.normalizeStringList(payload?.whyItHelpsAcrossProjects?.frontendApplications, 8);
798
+ const backendServices = this.normalizeStringList(payload?.whyItHelpsAcrossProjects?.backendServices, 8);
799
+ const genericTransferableValue = this.normalizeStringList(payload?.whyItHelpsAcrossProjects?.genericTransferableValue, 8);
800
+
801
+ if (
802
+ infrastructureAsCode.length === 0 &&
803
+ dataPlatforms.length === 0 &&
804
+ frontendApplications.length === 0 &&
805
+ backendServices.length === 0 &&
806
+ genericTransferableValue.length === 0
807
+ ) {
808
+ return null;
809
+ }
810
+
811
+ return {
812
+ whyItHelpsAcrossProjects: {
813
+ infrastructureAsCode,
814
+ dataPlatforms,
815
+ frontendApplications,
816
+ backendServices,
817
+ genericTransferableValue
818
+ }
819
+ };
820
+ }
821
+
822
+ normalizePdfExecutiveBrief(payload) {
823
+ if (!payload || typeof payload !== 'object') return null;
824
+
825
+ const purpose = typeof payload?.executiveSummary?.purpose === 'string'
826
+ ? payload.executiveSummary.purpose.trim()
827
+ : '';
828
+ const killerFeatures = this.normalizeStringList(payload?.executiveSummary?.killerFeatures, 3).slice(0, 3);
829
+
830
+ const architectureBlueprint = Array.isArray(payload.architectureBlueprint)
831
+ ? payload.architectureBlueprint
832
+ .map(item => ({
833
+ pattern: typeof item?.pattern === 'string' ? item.pattern.trim() : '',
834
+ primaryModule: typeof item?.primaryModule === 'string' ? item.primaryModule.trim() : '',
835
+ value: typeof item?.value === 'string' ? item.value.trim() : ''
836
+ }))
837
+ .filter(item => item.pattern && item.primaryModule && item.value)
838
+ .slice(0, 3)
839
+ : [];
840
+
841
+ const primaryFlow = typeof payload?.dataFlow?.primaryFlow === 'string'
842
+ ? payload.dataFlow.primaryFlow.trim()
843
+ : '';
844
+ const keyFlows = this.normalizeStringList(payload?.dataFlow?.keyFlows, 5);
845
+
846
+ const onboardingQuickGuide = Array.isArray(payload.onboardingQuickGuide)
847
+ ? payload.onboardingQuickGuide
848
+ .map(item => ({
849
+ goal: typeof item?.goal === 'string' ? item.goal.trim() : '',
850
+ modify: typeof item?.modify === 'string' ? item.modify.trim() : ''
851
+ }))
852
+ .filter(item => item.goal && item.modify)
853
+ .slice(0, 5)
854
+ : [];
855
+
856
+ const riskHotspots = Array.isArray(payload.riskHotspots)
857
+ ? payload.riskHotspots
858
+ .map(item => ({
859
+ severity: typeof item?.severity === 'string' ? item.severity.trim() : 'High',
860
+ file: typeof item?.file === 'string' ? item.file.trim() : '',
861
+ reason: typeof item?.reason === 'string' ? item.reason.trim() : ''
862
+ }))
863
+ .filter(item => item.file && item.reason)
864
+ .slice(0, 3)
865
+ : [];
866
+
867
+ if (
868
+ !purpose &&
869
+ killerFeatures.length === 0 &&
870
+ architectureBlueprint.length === 0 &&
871
+ !primaryFlow &&
872
+ keyFlows.length === 0 &&
873
+ onboardingQuickGuide.length === 0 &&
874
+ riskHotspots.length === 0
875
+ ) {
876
+ return null;
877
+ }
878
+
879
+ return {
880
+ executiveSummary: {
881
+ purpose,
882
+ killerFeatures
883
+ },
884
+ architectureBlueprint,
885
+ dataFlow: {
886
+ primaryFlow,
887
+ keyFlows
888
+ },
889
+ onboardingQuickGuide,
890
+ riskHotspots
891
+ };
892
+ }
893
+
894
+ buildPdfComponentLanguageMap(fileInfos) {
895
+ const bucket = new Map();
896
+ for (const info of fileInfos) {
897
+ const parts = info.path.split('/');
898
+ const topLevel = parts.length > 1 ? parts[0] : '.';
899
+ const key = `${topLevel}:${info.ext || 'unknown'}`;
900
+ bucket.set(key, (bucket.get(key) || 0) + 1);
901
+ }
902
+
903
+ return [...bucket.entries()]
904
+ .sort((a, b) => b[1] - a[1])
905
+ .slice(0, 12)
906
+ .map(([key, count]) => {
907
+ const [segment, ext] = key.split(':');
908
+ return `${segment} -> ${ext} (${count} files)`;
909
+ });
910
+ }
911
+
912
+ async loadDependencySnapshot() {
913
+ const packageJsonPath = path.join(process.cwd(), 'package.json');
914
+ if (!(await fs.pathExists(packageJsonPath))) return [];
915
+
916
+ try {
917
+ const pkg = await fs.readJson(packageJsonPath);
918
+ const deps = Object.entries(pkg.dependencies || {}).map(([name, version]) => `${name}@${version}`);
919
+ const devDeps = Object.entries(pkg.devDependencies || {}).map(([name, version]) => `${name}@${version} (dev)`);
920
+ return [...deps, ...devDeps].slice(0, 20);
921
+ } catch {
922
+ return [];
923
+ }
924
+ }
925
+
926
+ buildAiPdfContextInput(projectName, allFiles, selectedExtensions, filesByExtension, projectUnderstanding, dependencySnapshot) {
927
+ const topExtensions = selectedExtensions
928
+ .map(ext => ({
929
+ ext,
930
+ count: filesByExtension[ext]?.length || 0
931
+ }))
932
+ .sort((a, b) => b.count - a.count)
933
+ .slice(0, 8);
934
+
935
+ return {
936
+ projectName,
937
+ totalFiles: allFiles.length,
938
+ selectedExtensions,
939
+ topExtensions: topExtensions.map(item => `${item.ext} (${item.count})`),
940
+ entrypoints: projectUnderstanding.entrypoints.slice(0, 10),
941
+ coreModules: projectUnderstanding.coreModules.slice(0, 10).map(item => ({
942
+ path: item.path,
943
+ centrality: Number(item.centrality?.toFixed?.(2) ?? item.centrality ?? 0),
944
+ inDegree: item.indeg || 0,
945
+ outDegree: item.outdeg || 0,
946
+ hotspotScore: Number(item.hotspotScore?.toFixed?.(2) ?? item.hotspotScore ?? 0)
947
+ })),
948
+ hotspots: projectUnderstanding.hotspots.slice(0, 10).map(item => ({
949
+ path: item.path,
950
+ hotspotScore: Number(item.hotspotScore?.toFixed?.(2) ?? item.hotspotScore ?? 0),
951
+ complexity: Number(item.complexity?.toFixed?.(2) ?? item.complexity ?? 0),
952
+ centrality: Number(item.centrality?.toFixed?.(2) ?? item.centrality ?? 0)
953
+ })),
954
+ semanticClusters: projectUnderstanding.semanticClusters.slice(0, 8).map(cluster => ({
955
+ name: cluster.name,
956
+ description: cluster.description || '',
957
+ files: (cluster.files || []).slice(0, 8)
958
+ })),
959
+ adapterModes: projectUnderstanding.adapterModes || {},
960
+ graphMetrics: {
961
+ hubs: (projectUnderstanding.graphMetrics?.hubs || []).slice(0, 8),
962
+ centralNodes: (projectUnderstanding.graphMetrics?.centralNodes || []).slice(0, 8),
963
+ isolatedCount: projectUnderstanding.graphMetrics?.isolated?.length || 0,
964
+ isolatedExamples: (projectUnderstanding.graphMetrics?.isolated || []).slice(0, 8)
965
+ },
966
+ connectedSubmodules: (projectUnderstanding.connectedSubmodules || [])
967
+ .slice(0, 8)
968
+ .map(component => component.slice(0, 8)),
969
+ suggestedReadingOrder: projectUnderstanding.suggestedReadingOrder.slice(0, 12),
970
+ dependencyEdges: projectUnderstanding.dependencyEdges.slice(0, 120).map(([from, to]) => `${from} -> ${to}`),
971
+ componentLanguageMap: this.buildPdfComponentLanguageMap(projectUnderstanding.fileInfos || []),
972
+ dependencySnapshot,
973
+ outputContext: {
974
+ outputModes: ['pdf', 'llm'],
975
+ configResolution: [
976
+ 'Global config: ~/.codesummary/config.json (POSIX) or %APPDATA%/CodeSummary/config.json (Windows)',
977
+ 'Runtime path: process.cwd()',
978
+ 'Sensitive path filtering: .csignore'
979
+ ],
980
+ pipeline: [
981
+ 'CLI argument parsing and config merge',
982
+ 'Scanner recursive traversal and extension filtering',
983
+ 'GraphEngine dependency extraction and metrics',
984
+ 'PDF generator structured rendering'
985
+ ],
986
+ knownInterfaces: [
987
+ 'CLI options and config file settings',
988
+ '.csignore for sensitive path filtering',
989
+ 'LLM provider API endpoint'
990
+ ]
991
+ }
992
+ };
993
+ }
994
+
995
+ async requestAiPdfSection(client, prompt, aiOptions) {
996
+ try {
997
+ const response = await client.chat(prompt, {
998
+ model: aiOptions.model,
999
+ timeoutMs: aiOptions.timeoutMs,
1000
+ maxRetries: aiOptions.maxRetries,
1001
+ retryBackoffMs: aiOptions.retryBackoffMs,
1002
+ maxBackoffMs: aiOptions.maxBackoffMs
1003
+ });
1004
+ return this.safeParseJson(response?.content);
1005
+ } catch {
1006
+ return null;
1007
+ }
1008
+ }
1009
+
1010
+ normalizeUnifiedPdfAiContext({ executiveBriefPayload, introPayload, architecturePayload, operationsPayload, maintenancePayload, visualPayload, applicabilityPayload }) {
1011
+ const executiveBrief = this.normalizePdfExecutiveBrief(executiveBriefPayload);
1012
+ const intro = this.normalizePdfProjectContext(introPayload);
1013
+ const architecture = this.normalizePdfArchitectureContext(architecturePayload);
1014
+ const operations = this.normalizePdfOperationsContext(operationsPayload);
1015
+ const maintenanceAndSecurity = this.normalizePdfMaintenanceAndSecurityContext(maintenancePayload);
1016
+ const visualizations = this.normalizePdfVisualContext(visualPayload);
1017
+ const applicability = this.normalizePdfApplicabilityContext(applicabilityPayload);
1018
+
1019
+ if (!executiveBrief && !intro && !architecture && !operations && !maintenanceAndSecurity && !visualizations && !applicability) {
1020
+ return null;
1021
+ }
1022
+
1023
+ return {
1024
+ executiveBrief,
1025
+ intro,
1026
+ architecture,
1027
+ operations,
1028
+ maintenanceAndSecurity,
1029
+ visualizations,
1030
+ applicability
1031
+ };
1032
+ }
1033
+
1034
+ async generateAiPdfProjectContext(filesByExtension, selectedExtensions, projectName, aiOptions) {
1035
+ if (!aiOptions?.enabled || !aiOptions?.semantic) {
1036
+ console.log(chalk.gray('AI enrichment skipped: not enabled or semantic flag missing.'));
1037
+ return null;
1038
+ }
1039
+
1040
+ try {
1041
+ console.log(chalk.cyan('Generating AI project context...'));
1042
+ const allFiles = this.buildSelectedFilesList(filesByExtension, selectedExtensions);
1043
+ const llmGenerator = new LlmGenerator();
1044
+
1045
+ console.log(chalk.gray(' • Building project understanding...'));
1046
+ const projectUnderstanding = await llmGenerator.buildProjectUnderstanding(allFiles, {
1047
+ projectName,
1048
+ ai: { enabled: false, semantic: false }
1049
+ });
1050
+
1051
+ console.log(chalk.gray(' • Loading dependency snapshot...'));
1052
+ const dependencySnapshot = await this.loadDependencySnapshot();
1053
+
1054
+ const contextInput = this.buildAiPdfContextInput(
1055
+ projectName,
1056
+ allFiles,
1057
+ selectedExtensions,
1058
+ filesByExtension,
1059
+ projectUnderstanding,
1060
+ dependencySnapshot
1061
+ );
1062
+
1063
+ console.log(chalk.gray(` • Connecting to AI provider: ${aiOptions.provider}...`));
1064
+ const client = new ProviderClient(aiOptions);
1065
+ const health = await client.healthCheck();
1066
+ if (!health?.ok) {
1067
+ console.log(chalk.red(` • AI Health check failed: ${health?.error || 'Unknown error'}`));
1068
+ return null;
1069
+ }
1070
+
1071
+ console.log(chalk.gray(' • Fetching executive brief from AI...'));
1072
+ const executiveBriefPayload = await this.requestAiPdfSection(
1073
+ client,
1074
+ buildPdfExecutiveBriefPrompt(contextInput),
1075
+ aiOptions
1076
+ );
1077
+
1078
+ if (this.normalizePdfExecutiveBrief(executiveBriefPayload)) {
1079
+ console.log(chalk.green(' ✓ AI context generated successfully (Executive Brief).'));
1080
+ return this.normalizeUnifiedPdfAiContext({
1081
+ executiveBriefPayload
1082
+ });
1083
+ }
1084
+
1085
+ console.log(chalk.gray(' • Fetching detailed insights from AI (mult-request mode)...'));
1086
+ const introPayload = await this.requestAiPdfSection(
1087
+ client,
1088
+ buildPdfProjectIntroPrompt(contextInput),
1089
+ aiOptions
1090
+ );
1091
+ const architecturePayload = await this.requestAiPdfSection(
1092
+ client,
1093
+ buildPdfArchitectureInsightsPrompt(contextInput),
1094
+ aiOptions
1095
+ );
1096
+ const operationsPayload = await this.requestAiPdfSection(
1097
+ client,
1098
+ buildPdfOperationsInsightsPrompt(contextInput),
1099
+ aiOptions
1100
+ );
1101
+ const maintenancePayload = await this.requestAiPdfSection(
1102
+ client,
1103
+ buildPdfMaintenanceAndSecurityPrompt(contextInput),
1104
+ aiOptions
1105
+ );
1106
+ const visualPayload = await this.requestAiPdfSection(
1107
+ client,
1108
+ buildPdfVisualInsightsPrompt(contextInput),
1109
+ aiOptions
1110
+ );
1111
+ const applicabilityPayload = await this.requestAiPdfSection(
1112
+ client,
1113
+ buildPdfApplicabilityPrompt(contextInput),
1114
+ aiOptions
1115
+ );
1116
+
1117
+ console.log(chalk.green(' ✓ AI context generated successfully (Multi-section).'));
1118
+ return this.normalizeUnifiedPdfAiContext({
1119
+ executiveBriefPayload,
1120
+ introPayload,
1121
+ architecturePayload,
1122
+ operationsPayload,
1123
+ maintenancePayload,
1124
+ visualPayload,
1125
+ applicabilityPayload
1126
+ });
1127
+ } catch (error) {
1128
+ console.log(chalk.red(` • Error generating AI context: ${error.message}`));
1129
+ return null;
1130
+ }
1131
+ }
1132
+
1133
+ /**
1134
+ * Ask for explicit confirmation when AI enrichment may send repository content
1135
+ * to a configured external provider and no .csignore exists in the current working directory.
1136
+ * @param {object} options - Parsed CLI options
1137
+ * @param {object} aiOptions - Resolved AI runtime options
1138
+ */
1139
+ async confirmAiShareRiskIfNeeded(options, aiOptions) {
1140
+ const runLlm = options.format === 'llm' || options.format === 'both';
1141
+ const runPdf = options.format === 'pdf' || options.format === 'both';
1142
+ const willShareWithProvider = Boolean(
1143
+ (runLlm || runPdf) &&
1144
+ aiOptions?.enabled &&
1145
+ aiOptions?.semantic &&
1146
+ aiOptions?.provider
1147
+ );
1148
+
1149
+ if (!willShareWithProvider) {
1150
+ return;
1151
+ }
1152
+
1153
+ const csIgnorePath = path.join(process.cwd(), '.csignore');
1154
+ const hasCsIgnore = await fs.pathExists(csIgnorePath);
1155
+ if (hasCsIgnore) {
1156
+ return;
1157
+ }
1158
+
1159
+ console.log(chalk.yellow('\nSECURITY WARNING'));
1160
+ console.log(chalk.yellow(`Code may be shared with the configured LLM provider (${aiOptions.provider}${aiOptions.model ? ` / ${aiOptions.model}` : ''}).`));
1161
+ console.log(chalk.yellow('This may include credentials, API tokens, or other sensitive information.'));
1162
+ console.log(chalk.gray('To reduce exposure, add sensitive paths to a .csignore file in the current working directory.'));
1163
+ console.log(chalk.gray('.csignore uses .gitignore-style syntax and is applied to CodeSummary reports.\n'));
1164
+
1165
+ if (options.noInteractive || !process.stdin.isTTY) {
1166
+ console.error(chalk.red('ERROR: Confirmation cannot be requested in non-interactive mode without .csignore.'));
1167
+ console.error(chalk.red('Create .csignore in the current directory or run without --no-interactive to confirm explicitly.'));
1168
+ await ErrorHandler.safeExit(1, 'AI risk confirmation required');
1169
+ return;
1170
+ }
1171
+
1172
+ const { continueWithRisks } = await inquirer.prompt([{
1173
+ type: 'confirm',
1174
+ name: 'continueWithRisks',
1175
+ message: 'Do you want to continue, acknowledging these risks?',
1176
+ default: false
1177
+ }]);
1178
+
1179
+ if (!continueWithRisks) {
1180
+ console.log(chalk.gray('Operation cancelled to protect sensitive information.'));
1181
+ await ErrorHandler.safeExit(0, 'Operation cancelled by user due to AI sharing risk');
1182
+ }
453
1183
  }
454
1184
 
455
1185
  /**
@@ -472,28 +1202,6 @@ export class CLI {
472
1202
  console.log();
473
1203
  }
474
1204
 
475
- /**
476
- * Display RAG completion summary
477
- * @param {string} outputPath - Generated RAG JSON path
478
- * @param {Array} selectedExtensions - Selected extensions
479
- * @param {number} totalFiles - Total files processed
480
- * @param {number} totalChunks - Number of chunks generated
481
- */
482
- async displayRagCompletionSummary(outputPath, selectedExtensions, totalFiles, totalChunks) {
483
- const stats = await fs.stat(outputPath);
484
- const fileSizeFormatted = formatFileSize(stats.size);
485
-
486
- console.log(chalk.green('\nSUCCESS: RAG-optimized output generated successfully!\n'));
487
- console.log(chalk.cyan('Summary:'));
488
- console.log(chalk.gray(` Output: ${outputPath}`));
489
- console.log(chalk.gray(` Extensions: ${selectedExtensions.join(', ')}`));
490
- console.log(chalk.gray(` Total files: ${totalFiles}`));
491
- console.log(chalk.gray(` Total chunks: ${totalChunks}`));
492
- console.log(chalk.gray(` JSON size: ${fileSizeFormatted}`));
493
- console.log(chalk.gray(` Ready for RAG/LLM ingestion`));
494
- console.log();
495
- }
496
-
497
1205
  /**
498
1206
  * Display completion summary
499
1207
  * @param {string} outputPath - Generated PDF path
@@ -568,7 +1276,7 @@ export class CLI {
568
1276
  * Show help information
569
1277
  */
570
1278
  showHelp() {
571
- console.log(chalk.cyan('\nCodeSummary - Generate PDF documentation from source code\n'));
1279
+ console.log(chalk.cyan('\nCodeSummary - Generate AI and PDF documentation from source code\n'));
572
1280
 
573
1281
  console.log(chalk.white('Usage:'));
574
1282
  console.log(' codesummary [options] Scan current directory and generate PDF');
@@ -577,7 +1285,23 @@ export class CLI {
577
1285
 
578
1286
  console.log(chalk.white('Options:'));
579
1287
  console.log(' -o, --output <path> Override output directory');
580
- console.log(' -f, --format <format> Output format: pdf (default), rag, llm, or both (pdf+rag)');
1288
+ console.log(' -f, --format <format> Output format: llm (primary), pdf, or both (pdf+llm)');
1289
+ console.log(' --emit-llmsummary Emit structured repository artifact (.llmsummary.json)');
1290
+ console.log(' --focus <query> Generate focused context pack for a specific task/query');
1291
+ console.log(' --max-tokens <n> Cap output context to approximately n tokens');
1292
+ console.log(' --ai-semantic Enable optional LLM-assisted semantic enrichment');
1293
+ console.log(' Adds AI semantic enrichment in LLM output and AI project context in PDF when available');
1294
+ console.log(' If .csignore is missing, CodeSummary asks for risk confirmation before scan');
1295
+ console.log(' --provider <name> AI provider: openai-compatible (default) or ollama');
1296
+ console.log(' --base-url <url> Provider base URL');
1297
+ console.log(' --api-key <key> Provider API key (if required)');
1298
+ console.log(' --model <name> Provider model name');
1299
+ console.log(' --timeout-ms <n> AI request timeout in milliseconds');
1300
+ console.log(' --ai-retries <n> AI retry attempts on retryable API errors');
1301
+ console.log(' --ai-retry-backoff-ms <n> Initial retry backoff in milliseconds');
1302
+ console.log(' --ai-max-backoff-ms <n> Max retry backoff cap in milliseconds');
1303
+ console.log(' --no-code Exclude full source code section from PDF');
1304
+ console.log(' --include-code Force include full source code section in PDF');
581
1305
  console.log(' --show-config Display current configuration');
582
1306
  console.log(' --reset-config Reset configuration to defaults');
583
1307
  console.log(' -h, --help Show this help message');
@@ -586,8 +1310,15 @@ export class CLI {
586
1310
 
587
1311
  console.log(chalk.white('Examples:'));
588
1312
  console.log(' codesummary Scan current project (PDF)');
589
- console.log(' codesummary --format rag Generate RAG-optimised JSON');
590
1313
  console.log(' codesummary --format llm Generate LLM-optimised Markdown');
1314
+ console.log(' codesummary --format llm --emit-llmsummary Generate LLM Markdown + .llmsummary.json');
1315
+ console.log(' codesummary --format llm --focus "pdf generation" --max-tokens 12000');
1316
+ console.log(' codesummary --format llm --ai-semantic --provider ollama --model llama3.1');
1317
+ console.log(' # Add sensitive paths to .csignore (gitignore syntax) before enabling AI enrichment');
1318
+ console.log(' codesummary --format llm --ai-semantic --ai-retries 3 --ai-retry-backoff-ms 700');
1319
+ console.log(' codesummary --format pdf --ai-semantic Executive brief PDF (no code by default)');
1320
+ console.log(' codesummary --format pdf --ai-semantic --include-code Executive brief + full code appendix');
1321
+ console.log(' codesummary --format both Generate both LLM Markdown and PDF');
591
1322
  console.log(' codesummary --output ./docs Save output to ./docs folder');
592
1323
  console.log(' codesummary config Edit settings');
593
1324
  console.log(' codesummary --show-config View current settings');
@@ -595,6 +1326,17 @@ export class CLI {
595
1326
 
596
1327
  console.log(chalk.gray('For more information, visit: https://github.com/skamoll/CodeSummary'));
597
1328
  }
1329
+
1330
+ async displayLlmSummaryCompletionSummary(outputPath) {
1331
+ const stats = await fs.stat(outputPath);
1332
+ const fileSizeFormatted = formatFileSize(stats.size);
1333
+
1334
+ console.log(chalk.green('SUCCESS: Repository context artifact generated successfully!\n'));
1335
+ console.log(chalk.cyan('Summary:'));
1336
+ console.log(chalk.gray(` Output: ${outputPath}`));
1337
+ console.log(chalk.gray(` File size: ${fileSizeFormatted}`));
1338
+ console.log();
1339
+ }
598
1340
  }
599
1341
 
600
- export default CLI;
1342
+ export default CLI;