codesummary 1.2.0 → 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/CHANGELOG.md +26 -213
- package/README.md +61 -395
- package/features.md +25 -386
- package/package.json +13 -17
- package/src/ai/errors.js +85 -0
- package/src/ai/featureFlags.js +8 -0
- package/src/ai/promptTemplates.js +337 -0
- package/src/ai/providerClient.js +81 -0
- package/src/ai/providers/ollama.js +92 -0
- package/src/ai/providers/openaiCompatible.js +96 -0
- package/src/analysis/repositorySignals.js +196 -0
- package/src/cli.js +819 -77
- package/src/configManager.js +21 -0
- package/src/graph/adapters/baseAdapter.js +24 -0
- package/src/graph/adapters/javascriptAdapter.js +53 -0
- package/src/graph/adapters/pythonAdapter.js +77 -0
- package/src/graph/graphEngine.js +151 -0
- package/src/graph/graphMetrics.js +79 -0
- package/src/graph/graphSchema.js +30 -0
- package/src/graph/universalExtractor.js +29 -0
- package/src/llmGenerator.js +723 -8
- package/src/pdfGenerator.js +1189 -275
- package/src/renderers/llmSummaryRenderer.js +14 -0
- package/src/renderers/pdfThemeRenderer.js +685 -0
- package/src/scanner.js +115 -8
- package/rag-schema.json +0 -114
- package/src/ragConfig.js +0 -369
- package/src/ragGenerator.js +0 -1740
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
|
-
|
|
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
|
|
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', '
|
|
147
|
-
throw new Error(`Invalid format: ${format}. Use 'pdf', '
|
|
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
|
|
343
|
+
// Generate output based on format
|
|
220
344
|
const runPdf = options.format === 'pdf' || options.format === 'both';
|
|
221
|
-
const
|
|
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
|
|
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
|
-
|
|
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
|
-
|
|
530
|
+
|
|
531
|
+
return PDFGenerator.generateOutputPath(sanitizedProjectName, outputDir);
|
|
397
532
|
}
|
|
398
533
|
|
|
399
534
|
/**
|
|
400
|
-
* Determine final output path for
|
|
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
|
-
|
|
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
|
|
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
|
-
|
|
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
|
-
|
|
445
|
-
|
|
446
|
-
|
|
447
|
-
|
|
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
|
-
|
|
452
|
-
|
|
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:
|
|
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;
|