cto-ai-cli 4.0.0 → 5.1.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/DOCS.md +201 -2
- package/README.md +217 -312
- package/dist/action/index.js +281 -162
- package/dist/api/dashboard.js +281 -162
- package/dist/api/dashboard.js.map +1 -1
- package/dist/api/server.js +362 -184
- package/dist/api/server.js.map +1 -1
- package/dist/cli/gateway.js +358 -229
- package/dist/cli/score.js +2426 -1225
- package/dist/cli/v2/index.js +290 -175
- package/dist/cli/v2/index.js.map +1 -1
- package/dist/engine/index.d.ts +150 -1
- package/dist/engine/index.js +1130 -219
- package/dist/engine/index.js.map +1 -1
- package/dist/fsevents-X6WP4TKM.node +0 -0
- package/dist/gateway/index.d.ts +2 -2
- package/dist/gateway/index.js +358 -229
- package/dist/gateway/index.js.map +1 -1
- package/dist/interact/index.js +263 -148
- package/dist/interact/index.js.map +1 -1
- package/dist/mcp/v2.js +297 -178
- package/dist/mcp/v2.js.map +1 -1
- package/package.json +8 -22
- package/dist/core/index.d.ts +0 -717
- package/dist/core/index.js +0 -4446
- package/dist/core/index.js.map +0 -1
package/dist/cli/score.js
CHANGED
|
@@ -621,8 +621,8 @@ async function analyzeProject(projectPath, config) {
|
|
|
621
621
|
maxDepth: mergedConfig.analysis.maxDepth
|
|
622
622
|
});
|
|
623
623
|
const tokenMethod = mergedConfig.tokens.method;
|
|
624
|
-
const
|
|
625
|
-
|
|
624
|
+
const BATCH_SIZE = 50;
|
|
625
|
+
async function estimateFileTokens(entry) {
|
|
626
626
|
let tokens;
|
|
627
627
|
if (tokenMethod === "tiktoken") {
|
|
628
628
|
try {
|
|
@@ -634,7 +634,7 @@ async function analyzeProject(projectPath, config) {
|
|
|
634
634
|
} else {
|
|
635
635
|
tokens = countTokensChars4(entry.size);
|
|
636
636
|
}
|
|
637
|
-
|
|
637
|
+
return {
|
|
638
638
|
path: entry.path,
|
|
639
639
|
relativePath: entry.relativePath,
|
|
640
640
|
extension: entry.extension,
|
|
@@ -643,16 +643,20 @@ async function analyzeProject(projectPath, config) {
|
|
|
643
643
|
lines: entry.lines,
|
|
644
644
|
lastModified: entry.lastModified,
|
|
645
645
|
kind: classifyFileKind(entry.relativePath),
|
|
646
|
-
// Graph data — populated by graph analysis
|
|
647
646
|
imports: [],
|
|
648
647
|
importedBy: [],
|
|
649
648
|
isHub: false,
|
|
650
649
|
complexity: 0,
|
|
651
|
-
// Risk data — populated by risk analysis
|
|
652
650
|
riskScore: 0,
|
|
653
651
|
riskFactors: [],
|
|
654
652
|
exclusionImpact: "none"
|
|
655
|
-
}
|
|
653
|
+
};
|
|
654
|
+
}
|
|
655
|
+
const files = [];
|
|
656
|
+
for (let i = 0; i < walkEntries.length; i += BATCH_SIZE) {
|
|
657
|
+
const batch = walkEntries.slice(i, i + BATCH_SIZE);
|
|
658
|
+
const results = await Promise.all(batch.map(estimateFileTokens));
|
|
659
|
+
files.push(...results);
|
|
656
660
|
}
|
|
657
661
|
const graph = buildProjectGraph(absPath, files);
|
|
658
662
|
for (const file of files) {
|
|
@@ -1344,10 +1348,7 @@ var init_secrets = __esm({
|
|
|
1344
1348
|
});
|
|
1345
1349
|
|
|
1346
1350
|
// src/engine/pruner.ts
|
|
1347
|
-
import { Project as Project2, SyntaxKind as SyntaxKind2 } from "ts-morph";
|
|
1348
1351
|
import { readFile as readFile4 } from "fs/promises";
|
|
1349
|
-
import { existsSync as existsSync3 } from "fs";
|
|
1350
|
-
import { join as join4 } from "path";
|
|
1351
1352
|
async function pruneFile(file, level) {
|
|
1352
1353
|
if (level === "excluded") {
|
|
1353
1354
|
return emptyResult(file, "excluded");
|
|
@@ -1369,23 +1370,7 @@ async function pruneTypeScript(file, level) {
|
|
|
1369
1370
|
} catch {
|
|
1370
1371
|
return emptyResult(file, level);
|
|
1371
1372
|
}
|
|
1372
|
-
|
|
1373
|
-
try {
|
|
1374
|
-
const tsConfigPath = findTsConfig(file.path);
|
|
1375
|
-
project = new Project2({
|
|
1376
|
-
tsConfigFilePath: tsConfigPath,
|
|
1377
|
-
skipAddingFilesFromTsConfig: true,
|
|
1378
|
-
compilerOptions: tsConfigPath ? void 0 : { allowJs: true, esModuleInterop: true }
|
|
1379
|
-
});
|
|
1380
|
-
project.createSourceFile(file.path, content, { overwrite: true });
|
|
1381
|
-
} catch {
|
|
1382
|
-
return pruneGenericFromContent(file, content, level);
|
|
1383
|
-
}
|
|
1384
|
-
const sourceFile = project.getSourceFiles()[0];
|
|
1385
|
-
if (!sourceFile) {
|
|
1386
|
-
return pruneGenericFromContent(file, content, level);
|
|
1387
|
-
}
|
|
1388
|
-
const prunedContent = level === "signatures" ? extractSignaturesAST(sourceFile) : extractSkeletonAST(sourceFile);
|
|
1373
|
+
const prunedContent = level === "signatures" ? extractSignaturesRegex(content) : extractSkeletonRegex(content);
|
|
1389
1374
|
const prunedTokens = countTokensChars4(Buffer.byteLength(prunedContent, "utf-8"));
|
|
1390
1375
|
const savingsPercent = file.tokens > 0 ? (file.tokens - prunedTokens) / file.tokens * 100 : 0;
|
|
1391
1376
|
return {
|
|
@@ -1397,131 +1382,281 @@ async function pruneTypeScript(file, level) {
|
|
|
1397
1382
|
savingsPercent: Math.max(0, savingsPercent)
|
|
1398
1383
|
};
|
|
1399
1384
|
}
|
|
1400
|
-
function
|
|
1385
|
+
function extractSignaturesRegex(content) {
|
|
1386
|
+
const lines = content.split("\n");
|
|
1401
1387
|
const parts = [];
|
|
1402
|
-
|
|
1403
|
-
|
|
1404
|
-
|
|
1405
|
-
|
|
1406
|
-
|
|
1407
|
-
|
|
1408
|
-
|
|
1409
|
-
|
|
1410
|
-
|
|
1411
|
-
|
|
1412
|
-
|
|
1413
|
-
|
|
1414
|
-
|
|
1415
|
-
|
|
1416
|
-
|
|
1417
|
-
|
|
1418
|
-
|
|
1419
|
-
addJSDoc(fn, parts);
|
|
1420
|
-
const isExported = fn.isExported();
|
|
1421
|
-
const isAsync = fn.isAsync();
|
|
1422
|
-
const name = fn.getName() ?? "<anonymous>";
|
|
1423
|
-
const params = fn.getParameters().map((p) => p.getText()).join(", ");
|
|
1424
|
-
const returnType = fn.getReturnTypeNode()?.getText();
|
|
1425
|
-
const returnStr = returnType ? `: ${returnType}` : "";
|
|
1426
|
-
const prefix = isExported ? "export " : "";
|
|
1427
|
-
const asyncStr = isAsync ? "async " : "";
|
|
1428
|
-
parts.push(`${prefix}${asyncStr}function ${name}(${params})${returnStr} { /* ... */ }`);
|
|
1429
|
-
}
|
|
1430
|
-
for (const stmt of sf.getVariableStatements()) {
|
|
1431
|
-
for (const decl of stmt.getDeclarations()) {
|
|
1432
|
-
const init = decl.getInitializer();
|
|
1433
|
-
if (init && (init.getKind() === SyntaxKind2.ArrowFunction || init.getKind() === SyntaxKind2.FunctionExpression)) {
|
|
1434
|
-
addJSDoc(stmt, parts);
|
|
1435
|
-
const isExported = stmt.isExported();
|
|
1436
|
-
const prefix = isExported ? "export " : "";
|
|
1437
|
-
const kind = stmt.getDeclarationKind();
|
|
1438
|
-
const name = decl.getName();
|
|
1439
|
-
const typeNode = decl.getTypeNode()?.getText();
|
|
1440
|
-
const typeStr = typeNode ? `: ${typeNode}` : "";
|
|
1441
|
-
parts.push(`${prefix}${kind} ${name}${typeStr} = /* ... */;`);
|
|
1442
|
-
} else {
|
|
1443
|
-
addJSDoc(stmt, parts);
|
|
1444
|
-
parts.push(stmt.getText());
|
|
1388
|
+
let i = 0;
|
|
1389
|
+
while (i < lines.length) {
|
|
1390
|
+
const line = lines[i];
|
|
1391
|
+
const trimmed = line.trim();
|
|
1392
|
+
if (trimmed === "") {
|
|
1393
|
+
i++;
|
|
1394
|
+
continue;
|
|
1395
|
+
}
|
|
1396
|
+
if (trimmed.startsWith("/**")) {
|
|
1397
|
+
const docLines = [];
|
|
1398
|
+
while (i < lines.length) {
|
|
1399
|
+
docLines.push(lines[i]);
|
|
1400
|
+
if (lines[i].includes("*/")) {
|
|
1401
|
+
i++;
|
|
1402
|
+
break;
|
|
1403
|
+
}
|
|
1404
|
+
i++;
|
|
1445
1405
|
}
|
|
1406
|
+
parts.push(docLines.join("\n"));
|
|
1407
|
+
continue;
|
|
1446
1408
|
}
|
|
1447
|
-
|
|
1448
|
-
|
|
1449
|
-
|
|
1450
|
-
|
|
1451
|
-
|
|
1452
|
-
|
|
1453
|
-
|
|
1454
|
-
|
|
1455
|
-
|
|
1456
|
-
|
|
1457
|
-
|
|
1458
|
-
|
|
1459
|
-
|
|
1460
|
-
|
|
1461
|
-
|
|
1462
|
-
|
|
1463
|
-
|
|
1464
|
-
if (
|
|
1465
|
-
const
|
|
1466
|
-
parts.push(
|
|
1467
|
-
|
|
1468
|
-
|
|
1469
|
-
|
|
1470
|
-
|
|
1471
|
-
const
|
|
1472
|
-
|
|
1473
|
-
|
|
1474
|
-
|
|
1475
|
-
|
|
1476
|
-
|
|
1477
|
-
|
|
1478
|
-
|
|
1479
|
-
|
|
1480
|
-
|
|
1481
|
-
|
|
1482
|
-
|
|
1483
|
-
|
|
1484
|
-
|
|
1485
|
-
|
|
1409
|
+
if (trimmed.startsWith("//")) {
|
|
1410
|
+
parts.push(line);
|
|
1411
|
+
i++;
|
|
1412
|
+
continue;
|
|
1413
|
+
}
|
|
1414
|
+
if (/^\s*(import|export)\s/.test(line) && (trimmed.includes(" from ") || trimmed.startsWith("import "))) {
|
|
1415
|
+
const block = collectBracedLine(lines, i);
|
|
1416
|
+
parts.push(block.text);
|
|
1417
|
+
i = block.nextIndex;
|
|
1418
|
+
continue;
|
|
1419
|
+
}
|
|
1420
|
+
if (/^\s*export\s*(\{|\*)/.test(trimmed)) {
|
|
1421
|
+
const block = collectBracedLine(lines, i);
|
|
1422
|
+
parts.push(block.text);
|
|
1423
|
+
i = block.nextIndex;
|
|
1424
|
+
continue;
|
|
1425
|
+
}
|
|
1426
|
+
if (/^\s*(export\s+)?type\s+\w/.test(trimmed) && !trimmed.startsWith("typeof")) {
|
|
1427
|
+
const block = collectBalanced(lines, i);
|
|
1428
|
+
parts.push(block.text);
|
|
1429
|
+
i = block.nextIndex;
|
|
1430
|
+
continue;
|
|
1431
|
+
}
|
|
1432
|
+
if (/^\s*(export\s+)?interface\s+\w/.test(trimmed)) {
|
|
1433
|
+
const block = collectBalanced(lines, i);
|
|
1434
|
+
parts.push(block.text);
|
|
1435
|
+
i = block.nextIndex;
|
|
1436
|
+
continue;
|
|
1437
|
+
}
|
|
1438
|
+
if (/^\s*(export\s+)?(const\s+)?enum\s+\w/.test(trimmed)) {
|
|
1439
|
+
const block = collectBalanced(lines, i);
|
|
1440
|
+
parts.push(block.text);
|
|
1441
|
+
i = block.nextIndex;
|
|
1442
|
+
continue;
|
|
1443
|
+
}
|
|
1444
|
+
const fnMatch = trimmed.match(/^(export\s+)?(async\s+)?function\s+(\w+)/);
|
|
1445
|
+
if (fnMatch) {
|
|
1446
|
+
const sig = extractFnSignature(lines, i);
|
|
1447
|
+
parts.push(`${sig} { /* ... */ }`);
|
|
1448
|
+
i = skipBlock(lines, i);
|
|
1449
|
+
continue;
|
|
1450
|
+
}
|
|
1451
|
+
const arrowMatch = trimmed.match(/^(export\s+)?(const|let|var)\s+(\w+)/);
|
|
1452
|
+
if (arrowMatch && looksLikeFunctionDecl(lines, i)) {
|
|
1453
|
+
const prefix = trimmed.match(/^((?:export\s+)?(?:const|let|var)\s+\w+[^=]*=)/)?.[1];
|
|
1454
|
+
if (prefix) {
|
|
1455
|
+
parts.push(`${prefix} /* ... */;`);
|
|
1456
|
+
}
|
|
1457
|
+
i = skipBlock(lines, i);
|
|
1458
|
+
continue;
|
|
1459
|
+
}
|
|
1460
|
+
if (arrowMatch) {
|
|
1461
|
+
const block = collectStatement(lines, i);
|
|
1462
|
+
parts.push(block.text);
|
|
1463
|
+
i = block.nextIndex;
|
|
1464
|
+
continue;
|
|
1465
|
+
}
|
|
1466
|
+
if (/^\s*(export\s+)?(abstract\s+)?class\s+\w/.test(trimmed)) {
|
|
1467
|
+
const classOutline = extractClassOutline(lines, i);
|
|
1468
|
+
parts.push(classOutline.text);
|
|
1469
|
+
i = classOutline.nextIndex;
|
|
1470
|
+
continue;
|
|
1471
|
+
}
|
|
1472
|
+
i++;
|
|
1486
1473
|
}
|
|
1487
1474
|
return parts.join("\n");
|
|
1488
1475
|
}
|
|
1489
|
-
function
|
|
1476
|
+
function extractSkeletonRegex(content) {
|
|
1477
|
+
const lines = content.split("\n");
|
|
1490
1478
|
const parts = [];
|
|
1491
|
-
|
|
1492
|
-
|
|
1493
|
-
|
|
1494
|
-
|
|
1495
|
-
|
|
1496
|
-
|
|
1497
|
-
|
|
1498
|
-
|
|
1499
|
-
|
|
1500
|
-
|
|
1501
|
-
|
|
1502
|
-
|
|
1503
|
-
|
|
1504
|
-
|
|
1505
|
-
|
|
1506
|
-
|
|
1507
|
-
|
|
1508
|
-
|
|
1509
|
-
|
|
1510
|
-
|
|
1511
|
-
|
|
1512
|
-
|
|
1513
|
-
|
|
1514
|
-
|
|
1515
|
-
|
|
1516
|
-
|
|
1517
|
-
|
|
1518
|
-
|
|
1519
|
-
|
|
1520
|
-
|
|
1521
|
-
|
|
1479
|
+
let i = 0;
|
|
1480
|
+
while (i < lines.length) {
|
|
1481
|
+
const trimmed = lines[i].trim();
|
|
1482
|
+
if (/^import\s/.test(trimmed)) {
|
|
1483
|
+
const block = collectBracedLine(lines, i);
|
|
1484
|
+
parts.push(block.text);
|
|
1485
|
+
i = block.nextIndex;
|
|
1486
|
+
continue;
|
|
1487
|
+
}
|
|
1488
|
+
if (/^export\s+(type|interface)\s+\w/.test(trimmed)) {
|
|
1489
|
+
const block = collectBalanced(lines, i);
|
|
1490
|
+
parts.push(block.text);
|
|
1491
|
+
i = block.nextIndex;
|
|
1492
|
+
continue;
|
|
1493
|
+
}
|
|
1494
|
+
if (/^export\s+(const\s+)?enum\s+\w/.test(trimmed)) {
|
|
1495
|
+
const block = collectBalanced(lines, i);
|
|
1496
|
+
parts.push(block.text);
|
|
1497
|
+
i = block.nextIndex;
|
|
1498
|
+
continue;
|
|
1499
|
+
}
|
|
1500
|
+
if (/^export\s+(async\s+)?function\s+\w/.test(trimmed)) {
|
|
1501
|
+
const sig = extractFnSignature(lines, i);
|
|
1502
|
+
parts.push(`${sig};`);
|
|
1503
|
+
i = skipBlock(lines, i);
|
|
1504
|
+
continue;
|
|
1505
|
+
}
|
|
1506
|
+
if (/^export\s+(abstract\s+)?class\s+/.test(trimmed)) {
|
|
1507
|
+
const nameMatch = trimmed.match(/class\s+(\w+)/);
|
|
1508
|
+
const name = nameMatch?.[1] ?? "Unknown";
|
|
1509
|
+
const end = skipBlock(lines, i);
|
|
1510
|
+
const methods = [];
|
|
1511
|
+
for (let j = i + 1; j < end; j++) {
|
|
1512
|
+
const mt = lines[j].trim();
|
|
1513
|
+
const mm = mt.match(/^(?:static\s+)?(?:async\s+)?(\w+)\s*\(/);
|
|
1514
|
+
if (mm && mm[1] !== "constructor") methods.push(mm[1]);
|
|
1515
|
+
}
|
|
1516
|
+
parts.push(`export class ${name} { /* methods: ${methods.join(", ")} */ }`);
|
|
1517
|
+
i = end;
|
|
1518
|
+
continue;
|
|
1519
|
+
}
|
|
1520
|
+
if (/^export\s*(\{|\*)/.test(trimmed)) {
|
|
1521
|
+
const block = collectBracedLine(lines, i);
|
|
1522
|
+
parts.push(block.text);
|
|
1523
|
+
i = block.nextIndex;
|
|
1524
|
+
continue;
|
|
1525
|
+
}
|
|
1526
|
+
i++;
|
|
1522
1527
|
}
|
|
1523
1528
|
return parts.join("\n");
|
|
1524
1529
|
}
|
|
1530
|
+
function collectBracedLine(lines, start) {
|
|
1531
|
+
let text = lines[start];
|
|
1532
|
+
let i = start + 1;
|
|
1533
|
+
while (i < lines.length && !text.includes(";") && !text.trimEnd().endsWith("'") && !text.trimEnd().endsWith('"')) {
|
|
1534
|
+
text += "\n" + lines[i];
|
|
1535
|
+
i++;
|
|
1536
|
+
}
|
|
1537
|
+
return { text, nextIndex: i };
|
|
1538
|
+
}
|
|
1539
|
+
function collectBalanced(lines, start) {
|
|
1540
|
+
let depth = 0;
|
|
1541
|
+
let text = "";
|
|
1542
|
+
let i = start;
|
|
1543
|
+
let started = false;
|
|
1544
|
+
while (i < lines.length) {
|
|
1545
|
+
const line = lines[i];
|
|
1546
|
+
text += (text ? "\n" : "") + line;
|
|
1547
|
+
for (const ch of line) {
|
|
1548
|
+
if (ch === "{" || ch === "(") {
|
|
1549
|
+
depth++;
|
|
1550
|
+
started = true;
|
|
1551
|
+
}
|
|
1552
|
+
if (ch === "}" || ch === ")") depth--;
|
|
1553
|
+
}
|
|
1554
|
+
i++;
|
|
1555
|
+
if (started && depth <= 0) break;
|
|
1556
|
+
if (!started && line.includes(";")) break;
|
|
1557
|
+
}
|
|
1558
|
+
return { text, nextIndex: i };
|
|
1559
|
+
}
|
|
1560
|
+
function collectStatement(lines, start) {
|
|
1561
|
+
let text = lines[start];
|
|
1562
|
+
let i = start + 1;
|
|
1563
|
+
if (text.includes(";")) return { text, nextIndex: i };
|
|
1564
|
+
let depth = 0;
|
|
1565
|
+
for (const ch of text) {
|
|
1566
|
+
if (ch === "{" || ch === "(" || ch === "[") depth++;
|
|
1567
|
+
if (ch === "}" || ch === ")" || ch === "]") depth--;
|
|
1568
|
+
}
|
|
1569
|
+
while (i < lines.length && depth > 0) {
|
|
1570
|
+
text += "\n" + lines[i];
|
|
1571
|
+
for (const ch of lines[i]) {
|
|
1572
|
+
if (ch === "{" || ch === "(" || ch === "[") depth++;
|
|
1573
|
+
if (ch === "}" || ch === ")" || ch === "]") depth--;
|
|
1574
|
+
}
|
|
1575
|
+
i++;
|
|
1576
|
+
}
|
|
1577
|
+
return { text, nextIndex: i };
|
|
1578
|
+
}
|
|
1579
|
+
function extractFnSignature(lines, start) {
|
|
1580
|
+
let sig = "";
|
|
1581
|
+
let i = start;
|
|
1582
|
+
while (i < lines.length) {
|
|
1583
|
+
const line = lines[i].trim();
|
|
1584
|
+
sig += (sig ? " " : "") + line;
|
|
1585
|
+
if (line.includes("{")) {
|
|
1586
|
+
sig = sig.replace(/\s*\{[^]*$/, "").trim();
|
|
1587
|
+
break;
|
|
1588
|
+
}
|
|
1589
|
+
i++;
|
|
1590
|
+
}
|
|
1591
|
+
return sig;
|
|
1592
|
+
}
|
|
1593
|
+
function skipBlock(lines, start) {
|
|
1594
|
+
let depth = 0;
|
|
1595
|
+
let i = start;
|
|
1596
|
+
let foundBrace = false;
|
|
1597
|
+
while (i < lines.length) {
|
|
1598
|
+
for (const ch of lines[i]) {
|
|
1599
|
+
if (ch === "{") {
|
|
1600
|
+
depth++;
|
|
1601
|
+
foundBrace = true;
|
|
1602
|
+
}
|
|
1603
|
+
if (ch === "}") depth--;
|
|
1604
|
+
}
|
|
1605
|
+
i++;
|
|
1606
|
+
if (foundBrace && depth <= 0) break;
|
|
1607
|
+
if (!foundBrace && lines[i - 1].includes(";")) break;
|
|
1608
|
+
}
|
|
1609
|
+
return i;
|
|
1610
|
+
}
|
|
1611
|
+
function looksLikeFunctionDecl(lines, start) {
|
|
1612
|
+
const chunk = lines.slice(start, Math.min(start + 5, lines.length)).join(" ");
|
|
1613
|
+
return /=>/.test(chunk) || /=\s*function/.test(chunk);
|
|
1614
|
+
}
|
|
1615
|
+
function extractClassOutline(lines, start) {
|
|
1616
|
+
const header = lines[start].trim();
|
|
1617
|
+
let headerText = header;
|
|
1618
|
+
let i = start + 1;
|
|
1619
|
+
if (!header.includes("{")) {
|
|
1620
|
+
while (i < lines.length) {
|
|
1621
|
+
headerText += " " + lines[i].trim();
|
|
1622
|
+
if (lines[i].includes("{")) {
|
|
1623
|
+
i++;
|
|
1624
|
+
break;
|
|
1625
|
+
}
|
|
1626
|
+
i++;
|
|
1627
|
+
}
|
|
1628
|
+
} else {
|
|
1629
|
+
i = start + 1;
|
|
1630
|
+
}
|
|
1631
|
+
const bodyParts = [headerText.replace(/\{[^]*$/, "{").trim()];
|
|
1632
|
+
let depth = 1;
|
|
1633
|
+
while (i < lines.length && depth > 0) {
|
|
1634
|
+
const line = lines[i];
|
|
1635
|
+
const trimmed = line.trim();
|
|
1636
|
+
for (const ch of line) {
|
|
1637
|
+
if (ch === "{") depth++;
|
|
1638
|
+
if (ch === "}") depth--;
|
|
1639
|
+
}
|
|
1640
|
+
if (depth <= 0) {
|
|
1641
|
+
i++;
|
|
1642
|
+
break;
|
|
1643
|
+
}
|
|
1644
|
+
if (depth === 1) {
|
|
1645
|
+
if (/^(private|protected|public|readonly|static|#)/.test(trimmed) && !trimmed.includes("(")) {
|
|
1646
|
+
bodyParts.push(` ${trimmed}`);
|
|
1647
|
+
} else if (/^constructor\s*\(/.test(trimmed)) {
|
|
1648
|
+
const sig = extractFnSignature(lines, i);
|
|
1649
|
+
bodyParts.push(` ${sig} { /* ... */ }`);
|
|
1650
|
+
} else if (/^(?:static\s+)?(?:async\s+)?(?:get\s+|set\s+)?\w+\s*[(<]/.test(trimmed) && !trimmed.startsWith("//")) {
|
|
1651
|
+
const sig = extractFnSignature(lines, i);
|
|
1652
|
+
bodyParts.push(` ${sig} { /* ... */ }`);
|
|
1653
|
+
}
|
|
1654
|
+
}
|
|
1655
|
+
i++;
|
|
1656
|
+
}
|
|
1657
|
+
bodyParts.push("}");
|
|
1658
|
+
return { text: bodyParts.join("\n"), nextIndex: i };
|
|
1659
|
+
}
|
|
1525
1660
|
async function pruneGeneric(file, level) {
|
|
1526
1661
|
let content;
|
|
1527
1662
|
try {
|
|
@@ -1582,22 +1717,6 @@ function emptyResult(file, level) {
|
|
|
1582
1717
|
savingsPercent: 100
|
|
1583
1718
|
};
|
|
1584
1719
|
}
|
|
1585
|
-
function addJSDoc(node, parts) {
|
|
1586
|
-
if (!node.getJsDocs) return;
|
|
1587
|
-
const docs = node.getJsDocs();
|
|
1588
|
-
if (docs.length > 0) {
|
|
1589
|
-
parts.push(docs[0].getText());
|
|
1590
|
-
}
|
|
1591
|
-
}
|
|
1592
|
-
function findTsConfig(filePath) {
|
|
1593
|
-
let dir = filePath;
|
|
1594
|
-
for (let i = 0; i < 10; i++) {
|
|
1595
|
-
dir = join4(dir, "..");
|
|
1596
|
-
const candidate = join4(dir, "tsconfig.json");
|
|
1597
|
-
if (existsSync3(candidate)) return candidate;
|
|
1598
|
-
}
|
|
1599
|
-
return void 0;
|
|
1600
|
-
}
|
|
1601
1720
|
var TS_EXTENSIONS2;
|
|
1602
1721
|
var init_pruner = __esm({
|
|
1603
1722
|
"src/engine/pruner.ts"() {
|
|
@@ -2045,7 +2164,8 @@ var init_types = __esm({
|
|
|
2045
2164
|
|
|
2046
2165
|
// src/gateway/providers.ts
|
|
2047
2166
|
function parseOpenAIRequest(body) {
|
|
2048
|
-
const
|
|
2167
|
+
const rawMessages = Array.isArray(body.messages) ? body.messages : [];
|
|
2168
|
+
const messages = rawMessages.map((m) => ({
|
|
2049
2169
|
role: m.role || "user",
|
|
2050
2170
|
content: typeof m.content === "string" ? m.content : JSON.stringify(m.content)
|
|
2051
2171
|
}));
|
|
@@ -2057,34 +2177,29 @@ function parseOpenAIRequest(body) {
|
|
|
2057
2177
|
temperature: body.temperature
|
|
2058
2178
|
};
|
|
2059
2179
|
}
|
|
2060
|
-
function parseOpenAIResponse(body,
|
|
2061
|
-
|
|
2062
|
-
|
|
2063
|
-
|
|
2064
|
-
|
|
2065
|
-
outputTokens: body.usage?.completion_tokens || 0,
|
|
2066
|
-
content: body.choices?.[0]?.message?.content || "",
|
|
2067
|
-
finishReason: body.choices?.[0]?.finish_reason || "stop"
|
|
2068
|
-
};
|
|
2069
|
-
}
|
|
2180
|
+
function parseOpenAIResponse(body, _streaming) {
|
|
2181
|
+
const usage = body.usage;
|
|
2182
|
+
const choices = Array.isArray(body.choices) ? body.choices : [];
|
|
2183
|
+
const first = choices[0];
|
|
2184
|
+
const msg = first?.message;
|
|
2070
2185
|
return {
|
|
2071
2186
|
model: body.model || "unknown",
|
|
2072
|
-
inputTokens:
|
|
2073
|
-
outputTokens:
|
|
2074
|
-
content:
|
|
2075
|
-
finishReason:
|
|
2187
|
+
inputTokens: usage?.prompt_tokens || 0,
|
|
2188
|
+
outputTokens: usage?.completion_tokens || 0,
|
|
2189
|
+
content: msg?.content || "",
|
|
2190
|
+
finishReason: first?.finish_reason || "stop"
|
|
2076
2191
|
};
|
|
2077
2192
|
}
|
|
2078
2193
|
function parseAnthropicRequest(body) {
|
|
2079
2194
|
const messages = [];
|
|
2080
2195
|
if (body.system) {
|
|
2081
|
-
messages.push({ role: "system", content: body.system });
|
|
2196
|
+
messages.push({ role: "system", content: String(body.system) });
|
|
2082
2197
|
}
|
|
2083
|
-
|
|
2084
|
-
|
|
2085
|
-
|
|
2086
|
-
|
|
2087
|
-
});
|
|
2198
|
+
const rawMessages = Array.isArray(body.messages) ? body.messages : [];
|
|
2199
|
+
for (const m of rawMessages) {
|
|
2200
|
+
const content = typeof m.content === "string" ? m.content : Array.isArray(m.content) ? m.content.map((b) => String(b.text ?? "")).join("\n") : "";
|
|
2201
|
+
const role = m.role || "user";
|
|
2202
|
+
messages.push({ role, content });
|
|
2088
2203
|
}
|
|
2089
2204
|
return {
|
|
2090
2205
|
model: body.model || "unknown",
|
|
@@ -2095,43 +2210,53 @@ function parseAnthropicRequest(body) {
|
|
|
2095
2210
|
};
|
|
2096
2211
|
}
|
|
2097
2212
|
function parseAnthropicResponse(body, _streaming) {
|
|
2213
|
+
const usage = body.usage;
|
|
2214
|
+
const contentBlocks = Array.isArray(body.content) ? body.content : [];
|
|
2098
2215
|
return {
|
|
2099
2216
|
model: body.model || "unknown",
|
|
2100
|
-
inputTokens:
|
|
2101
|
-
outputTokens:
|
|
2102
|
-
content:
|
|
2217
|
+
inputTokens: usage?.input_tokens || 0,
|
|
2218
|
+
outputTokens: usage?.output_tokens || 0,
|
|
2219
|
+
content: contentBlocks.map((b) => String(b.text ?? "")).join("\n"),
|
|
2103
2220
|
finishReason: body.stop_reason || "end_turn"
|
|
2104
2221
|
};
|
|
2105
2222
|
}
|
|
2106
2223
|
function parseGoogleRequest(body) {
|
|
2107
2224
|
const messages = [];
|
|
2108
|
-
|
|
2225
|
+
const sysInst = body.systemInstruction;
|
|
2226
|
+
if (sysInst && Array.isArray(sysInst.parts)) {
|
|
2109
2227
|
messages.push({
|
|
2110
2228
|
role: "system",
|
|
2111
|
-
content:
|
|
2229
|
+
content: sysInst.parts.map((p) => String(p.text ?? "")).join("\n")
|
|
2112
2230
|
});
|
|
2113
2231
|
}
|
|
2114
|
-
|
|
2232
|
+
const contents = Array.isArray(body.contents) ? body.contents : [];
|
|
2233
|
+
for (const item of contents) {
|
|
2115
2234
|
const role = item.role === "model" ? "assistant" : "user";
|
|
2116
|
-
const
|
|
2235
|
+
const parts = Array.isArray(item.parts) ? item.parts : [];
|
|
2236
|
+
const content = parts.map((p) => String(p.text ?? "")).join("\n");
|
|
2117
2237
|
messages.push({ role, content });
|
|
2118
2238
|
}
|
|
2119
2239
|
const model = body.model || body.modelId || "gemini-2.0-flash";
|
|
2240
|
+
const genConfig = body.generationConfig;
|
|
2120
2241
|
return {
|
|
2121
2242
|
model,
|
|
2122
2243
|
messages,
|
|
2123
2244
|
stream: body.stream === true,
|
|
2124
|
-
maxTokens:
|
|
2125
|
-
temperature:
|
|
2245
|
+
maxTokens: genConfig?.maxOutputTokens,
|
|
2246
|
+
temperature: genConfig?.temperature
|
|
2126
2247
|
};
|
|
2127
2248
|
}
|
|
2128
2249
|
function parseGoogleResponse(body, _streaming) {
|
|
2129
|
-
const
|
|
2250
|
+
const candidates = Array.isArray(body.candidates) ? body.candidates : [];
|
|
2251
|
+
const candidate = candidates[0];
|
|
2252
|
+
const usage = body.usageMetadata;
|
|
2253
|
+
const candidateContent = candidate?.content;
|
|
2254
|
+
const parts = Array.isArray(candidateContent?.parts) ? candidateContent.parts : [];
|
|
2130
2255
|
return {
|
|
2131
2256
|
model: body.modelVersion || body.model || "gemini-2.0-flash",
|
|
2132
|
-
inputTokens:
|
|
2133
|
-
outputTokens:
|
|
2134
|
-
content:
|
|
2257
|
+
inputTokens: usage?.promptTokenCount || 0,
|
|
2258
|
+
outputTokens: usage?.candidatesTokenCount || 0,
|
|
2259
|
+
content: parts.map((p) => String(p.text ?? "")).join("\n"),
|
|
2135
2260
|
finishReason: candidate?.finishReason || "STOP"
|
|
2136
2261
|
};
|
|
2137
2262
|
}
|
|
@@ -2240,8 +2365,8 @@ var init_providers = __esm({
|
|
|
2240
2365
|
});
|
|
2241
2366
|
|
|
2242
2367
|
// src/gateway/interceptor.ts
|
|
2243
|
-
import { readFileSync as
|
|
2244
|
-
import { resolve as
|
|
2368
|
+
import { readFileSync as readFileSync3 } from "fs";
|
|
2369
|
+
import { resolve as resolve10 } from "path";
|
|
2245
2370
|
function estimateTokensFromString(s) {
|
|
2246
2371
|
return Math.ceil(Buffer.byteLength(s, "utf-8") / 4);
|
|
2247
2372
|
}
|
|
@@ -2355,8 +2480,8 @@ async function optimizeContext(messages, analysis, config) {
|
|
|
2355
2480
|
continue;
|
|
2356
2481
|
}
|
|
2357
2482
|
try {
|
|
2358
|
-
const fullPath =
|
|
2359
|
-
const content =
|
|
2483
|
+
const fullPath = resolve10(config.projectPath, f.relativePath);
|
|
2484
|
+
const content = readFileSync3(fullPath, "utf-8");
|
|
2360
2485
|
const fileTokens = estimateTokensFromString(content);
|
|
2361
2486
|
const remainingBudget = contentBudget - usedTokens;
|
|
2362
2487
|
let fileContent;
|
|
@@ -2425,7 +2550,8 @@ async function optimizeContext(messages, analysis, config) {
|
|
|
2425
2550
|
);
|
|
2426
2551
|
return { messages: optimizedMessages, injected: true, optimizeDecisions };
|
|
2427
2552
|
} catch (err) {
|
|
2428
|
-
|
|
2553
|
+
const errMsg = err instanceof Error ? err.message : String(err);
|
|
2554
|
+
optimizeDecisions.push(`Context optimization failed: ${errMsg}`);
|
|
2429
2555
|
return { messages, injected: false, optimizeDecisions };
|
|
2430
2556
|
}
|
|
2431
2557
|
}
|
|
@@ -2438,8 +2564,8 @@ var init_interceptor = __esm({
|
|
|
2438
2564
|
});
|
|
2439
2565
|
|
|
2440
2566
|
// src/gateway/tracker.ts
|
|
2441
|
-
import { mkdirSync as
|
|
2442
|
-
import { join as
|
|
2567
|
+
import { mkdirSync as mkdirSync7, appendFileSync as appendFileSync2, readFileSync as readFileSync4, readdirSync, existsSync as existsSync5 } from "fs";
|
|
2568
|
+
import { join as join14 } from "path";
|
|
2443
2569
|
import { randomUUID } from "crypto";
|
|
2444
2570
|
var UsageTracker;
|
|
2445
2571
|
var init_tracker = __esm({
|
|
@@ -2455,8 +2581,8 @@ var init_tracker = __esm({
|
|
|
2455
2581
|
memRecords = [];
|
|
2456
2582
|
constructor(config) {
|
|
2457
2583
|
this.config = config;
|
|
2458
|
-
this.logDir =
|
|
2459
|
-
|
|
2584
|
+
this.logDir = join14(config.logDir, "usage");
|
|
2585
|
+
mkdirSync7(this.logDir, { recursive: true });
|
|
2460
2586
|
}
|
|
2461
2587
|
// ===== EVENT SYSTEM =====
|
|
2462
2588
|
onEvent(handler) {
|
|
@@ -2478,12 +2604,12 @@ var init_tracker = __esm({
|
|
|
2478
2604
|
...params
|
|
2479
2605
|
};
|
|
2480
2606
|
const monthKey = this.getMonthKey(record.timestamp);
|
|
2481
|
-
const logFile =
|
|
2607
|
+
const logFile = join14(this.logDir, `${monthKey}.jsonl`);
|
|
2482
2608
|
const line = JSON.stringify({
|
|
2483
2609
|
...record,
|
|
2484
2610
|
timestamp: record.timestamp.toISOString()
|
|
2485
2611
|
});
|
|
2486
|
-
|
|
2612
|
+
appendFileSync2(logFile, line + "\n");
|
|
2487
2613
|
this.memRecords.push(record);
|
|
2488
2614
|
this.cache = null;
|
|
2489
2615
|
this.emit({ type: "request", record });
|
|
@@ -2612,9 +2738,9 @@ var init_tracker = __esm({
|
|
|
2612
2738
|
return date.toISOString().slice(0, 7);
|
|
2613
2739
|
}
|
|
2614
2740
|
getMonthRecordsByKey(monthKey) {
|
|
2615
|
-
const filePath =
|
|
2616
|
-
if (!
|
|
2617
|
-
return
|
|
2741
|
+
const filePath = join14(this.logDir, `${monthKey}.jsonl`);
|
|
2742
|
+
if (!existsSync5(filePath)) return [];
|
|
2743
|
+
return readFileSync4(filePath, "utf-8").split("\n").filter((line) => line.trim()).map((line) => {
|
|
2618
2744
|
try {
|
|
2619
2745
|
const parsed = JSON.parse(line);
|
|
2620
2746
|
parsed.timestamp = new Date(parsed.timestamp);
|
|
@@ -2627,9 +2753,9 @@ var init_tracker = __esm({
|
|
|
2627
2753
|
getMonthRecords(date) {
|
|
2628
2754
|
const monthKey = this.getMonthKey(date);
|
|
2629
2755
|
if (this.cache && this.cacheMonth === monthKey) return this.cache;
|
|
2630
|
-
const filePath =
|
|
2631
|
-
if (!
|
|
2632
|
-
const records =
|
|
2756
|
+
const filePath = join14(this.logDir, `${monthKey}.jsonl`);
|
|
2757
|
+
if (!existsSync5(filePath)) return [];
|
|
2758
|
+
const records = readFileSync4(filePath, "utf-8").split("\n").filter((line) => line.trim()).map((line) => {
|
|
2633
2759
|
try {
|
|
2634
2760
|
const parsed = JSON.parse(line);
|
|
2635
2761
|
parsed.timestamp = new Date(parsed.timestamp);
|
|
@@ -2643,11 +2769,11 @@ var init_tracker = __esm({
|
|
|
2643
2769
|
return records;
|
|
2644
2770
|
}
|
|
2645
2771
|
getAllRecords() {
|
|
2646
|
-
if (!
|
|
2772
|
+
if (!existsSync5(this.logDir)) return [];
|
|
2647
2773
|
const files = readdirSync(this.logDir).filter((f) => f.endsWith(".jsonl")).sort();
|
|
2648
2774
|
const allRecords = [];
|
|
2649
2775
|
for (const file of files) {
|
|
2650
|
-
const content =
|
|
2776
|
+
const content = readFileSync4(join14(this.logDir, file), "utf-8");
|
|
2651
2777
|
const records = content.split("\n").filter((line) => line.trim()).map((line) => {
|
|
2652
2778
|
try {
|
|
2653
2779
|
const parsed = JSON.parse(line);
|
|
@@ -2676,7 +2802,7 @@ import { request as httpRequest } from "http";
|
|
|
2676
2802
|
import { URL } from "url";
|
|
2677
2803
|
import { lookup } from "dns/promises";
|
|
2678
2804
|
function readBody(req, maxBytes = 0) {
|
|
2679
|
-
return new Promise((
|
|
2805
|
+
return new Promise((resolve12, reject) => {
|
|
2680
2806
|
const chunks = [];
|
|
2681
2807
|
let totalBytes = 0;
|
|
2682
2808
|
req.on("data", (chunk) => {
|
|
@@ -2688,7 +2814,7 @@ function readBody(req, maxBytes = 0) {
|
|
|
2688
2814
|
}
|
|
2689
2815
|
chunks.push(chunk);
|
|
2690
2816
|
});
|
|
2691
|
-
req.on("end", () =>
|
|
2817
|
+
req.on("end", () => resolve12(Buffer.concat(chunks).toString()));
|
|
2692
2818
|
req.on("error", reject);
|
|
2693
2819
|
});
|
|
2694
2820
|
}
|
|
@@ -2904,18 +3030,18 @@ var init_server = __esm({
|
|
|
2904
3030
|
this.analysisPromise = this.refreshAnalysis();
|
|
2905
3031
|
}
|
|
2906
3032
|
this.server = createServer((req, res) => this.handleRequest(req, res));
|
|
2907
|
-
return new Promise((
|
|
3033
|
+
return new Promise((resolve12) => {
|
|
2908
3034
|
this.server.listen(this.config.port, this.config.host, () => {
|
|
2909
|
-
|
|
3035
|
+
resolve12();
|
|
2910
3036
|
});
|
|
2911
3037
|
});
|
|
2912
3038
|
}
|
|
2913
3039
|
async stop() {
|
|
2914
|
-
return new Promise((
|
|
3040
|
+
return new Promise((resolve12) => {
|
|
2915
3041
|
if (this.server) {
|
|
2916
|
-
this.server.close(() =>
|
|
3042
|
+
this.server.close(() => resolve12());
|
|
2917
3043
|
} else {
|
|
2918
|
-
|
|
3044
|
+
resolve12();
|
|
2919
3045
|
}
|
|
2920
3046
|
});
|
|
2921
3047
|
}
|
|
@@ -2929,7 +3055,8 @@ var init_server = __esm({
|
|
|
2929
3055
|
this.analysis = analysis;
|
|
2930
3056
|
return analysis;
|
|
2931
3057
|
} catch (err) {
|
|
2932
|
-
|
|
3058
|
+
const message = err instanceof Error ? err.message : String(err);
|
|
3059
|
+
this.emit({ type: "error", message: `Analysis failed: ${message}`, error: err instanceof Error ? err : void 0 });
|
|
2933
3060
|
throw err;
|
|
2934
3061
|
}
|
|
2935
3062
|
}
|
|
@@ -2966,7 +3093,8 @@ var init_server = __esm({
|
|
|
2966
3093
|
try {
|
|
2967
3094
|
body = await readBody(req, this.config.maxBodyBytes);
|
|
2968
3095
|
} catch (err) {
|
|
2969
|
-
const
|
|
3096
|
+
const errMsg = err instanceof Error ? err.message : String(err);
|
|
3097
|
+
const status = errMsg === "body-too-large" ? 413 : 400;
|
|
2970
3098
|
res.writeHead(status, { "Content-Type": "application/json" });
|
|
2971
3099
|
res.end(JSON.stringify({ error: status === 413 ? `Request body too large. Max: ${Math.round(this.config.maxBodyBytes / 1024 / 1024)}MB` : "Failed to read request body" }));
|
|
2972
3100
|
return;
|
|
@@ -3077,12 +3205,13 @@ var init_server = __esm({
|
|
|
3077
3205
|
try {
|
|
3078
3206
|
await this.proxyRequest(targetUrl, req, res, modifiedBody, provider, parsed, interceptResult, startTime);
|
|
3079
3207
|
} catch (err) {
|
|
3208
|
+
const errMsg = err instanceof Error ? err.message : String(err);
|
|
3080
3209
|
if (!res.headersSent) {
|
|
3081
|
-
const status =
|
|
3210
|
+
const status = errMsg === "upstream-timeout" ? 504 : 502;
|
|
3082
3211
|
res.writeHead(status, { "Content-Type": "application/json" });
|
|
3083
|
-
res.end(JSON.stringify({ error: status === 504 ? "Upstream provider timeout" : `Proxy error: ${
|
|
3212
|
+
res.end(JSON.stringify({ error: status === 504 ? "Upstream provider timeout" : `Proxy error: ${errMsg}` }));
|
|
3084
3213
|
}
|
|
3085
|
-
this.emit({ type: "error", message: `Proxy error: ${
|
|
3214
|
+
this.emit({ type: "error", message: `Proxy error: ${errMsg}`, error: err instanceof Error ? err : void 0 });
|
|
3086
3215
|
}
|
|
3087
3216
|
}
|
|
3088
3217
|
// ===== PROXY =====
|
|
@@ -3097,7 +3226,7 @@ var init_server = __esm({
|
|
|
3097
3226
|
if (value) forwardHeaders[key] = Array.isArray(value) ? value[0] : value;
|
|
3098
3227
|
}
|
|
3099
3228
|
forwardHeaders["content-length"] = Buffer.byteLength(body).toString();
|
|
3100
|
-
return new Promise((
|
|
3229
|
+
return new Promise((resolve12, reject) => {
|
|
3101
3230
|
const proxyReq = requester(
|
|
3102
3231
|
{
|
|
3103
3232
|
hostname: url.hostname,
|
|
@@ -3118,7 +3247,7 @@ var init_server = __esm({
|
|
|
3118
3247
|
parsed,
|
|
3119
3248
|
interceptResult,
|
|
3120
3249
|
startTime
|
|
3121
|
-
).then(
|
|
3250
|
+
).then(resolve12).catch(reject);
|
|
3122
3251
|
} else {
|
|
3123
3252
|
this.handleBufferedResponse(
|
|
3124
3253
|
proxyRes,
|
|
@@ -3127,7 +3256,7 @@ var init_server = __esm({
|
|
|
3127
3256
|
parsed,
|
|
3128
3257
|
interceptResult,
|
|
3129
3258
|
startTime
|
|
3130
|
-
).then(
|
|
3259
|
+
).then(resolve12).catch(reject);
|
|
3131
3260
|
}
|
|
3132
3261
|
}
|
|
3133
3262
|
);
|
|
@@ -3147,7 +3276,7 @@ var init_server = __esm({
|
|
|
3147
3276
|
let inputTokens = 0;
|
|
3148
3277
|
let outputTokens = 0;
|
|
3149
3278
|
let sseBuffer = "";
|
|
3150
|
-
return new Promise((
|
|
3279
|
+
return new Promise((resolve12) => {
|
|
3151
3280
|
proxyRes.on("data", (chunk) => {
|
|
3152
3281
|
clientRes.write(chunk);
|
|
3153
3282
|
sseBuffer += chunk.toString();
|
|
@@ -3208,17 +3337,17 @@ var init_server = __esm({
|
|
|
3208
3337
|
latencyMs: Date.now() - startTime,
|
|
3209
3338
|
stream: true
|
|
3210
3339
|
});
|
|
3211
|
-
|
|
3340
|
+
resolve12();
|
|
3212
3341
|
});
|
|
3213
3342
|
proxyRes.on("error", () => {
|
|
3214
3343
|
clientRes.end();
|
|
3215
|
-
|
|
3344
|
+
resolve12();
|
|
3216
3345
|
});
|
|
3217
3346
|
});
|
|
3218
3347
|
}
|
|
3219
3348
|
// ===== BUFFERED HANDLER =====
|
|
3220
3349
|
async handleBufferedResponse(proxyRes, clientRes, provider, parsed, interceptResult, startTime) {
|
|
3221
|
-
return new Promise((
|
|
3350
|
+
return new Promise((resolve12) => {
|
|
3222
3351
|
const chunks = [];
|
|
3223
3352
|
proxyRes.on("data", (chunk) => chunks.push(chunk));
|
|
3224
3353
|
proxyRes.on("end", () => {
|
|
@@ -3275,11 +3404,11 @@ var init_server = __esm({
|
|
|
3275
3404
|
error: "response-parse-failed"
|
|
3276
3405
|
});
|
|
3277
3406
|
}
|
|
3278
|
-
|
|
3407
|
+
resolve12();
|
|
3279
3408
|
});
|
|
3280
3409
|
proxyRes.on("error", () => {
|
|
3281
3410
|
clientRes.end();
|
|
3282
|
-
|
|
3411
|
+
resolve12();
|
|
3283
3412
|
});
|
|
3284
3413
|
});
|
|
3285
3414
|
}
|
|
@@ -3297,8 +3426,7 @@ var init_server = __esm({
|
|
|
3297
3426
|
|
|
3298
3427
|
// src/cli/score.ts
|
|
3299
3428
|
init_analyzer();
|
|
3300
|
-
import { resolve as
|
|
3301
|
-
import { mkdirSync as mkdirSync3, writeFileSync as writeFileSync2, readFileSync as readFileSync4, appendFileSync as appendFileSync2 } from "fs";
|
|
3429
|
+
import { resolve as resolve11 } from "path";
|
|
3302
3430
|
|
|
3303
3431
|
// src/engine/score.ts
|
|
3304
3432
|
init_selector();
|
|
@@ -3605,9 +3733,9 @@ function renderBar(pct, width) {
|
|
|
3605
3733
|
return "\u2588".repeat(filled) + "\u2591".repeat(empty);
|
|
3606
3734
|
}
|
|
3607
3735
|
function padCenter(str, width) {
|
|
3608
|
-
const
|
|
3609
|
-
const left = Math.floor(
|
|
3610
|
-
return " ".repeat(left) + str + " ".repeat(
|
|
3736
|
+
const pad3 = Math.max(0, width - str.length);
|
|
3737
|
+
const left = Math.floor(pad3 / 2);
|
|
3738
|
+
return " ".repeat(left) + str + " ".repeat(pad3 - left);
|
|
3611
3739
|
}
|
|
3612
3740
|
function formatNumber(n) {
|
|
3613
3741
|
if (n >= 1e6) return `${(n / 1e6).toFixed(1)}M`;
|
|
@@ -3769,163 +3897,798 @@ function fmt(n) {
|
|
|
3769
3897
|
return n.toString();
|
|
3770
3898
|
}
|
|
3771
3899
|
|
|
3772
|
-
// src/
|
|
3773
|
-
|
|
3774
|
-
|
|
3775
|
-
|
|
3776
|
-
|
|
3777
|
-
|
|
3778
|
-
|
|
3779
|
-
|
|
3780
|
-
|
|
3781
|
-
|
|
3782
|
-
|
|
3783
|
-
|
|
3784
|
-
|
|
3785
|
-
{ file: "pnpm-workspace.yaml", tool: "pnpm-workspaces" },
|
|
3786
|
-
{
|
|
3787
|
-
file: "package.json",
|
|
3788
|
-
tool: "npm-workspaces",
|
|
3789
|
-
validate: (content) => {
|
|
3790
|
-
try {
|
|
3791
|
-
const pkg = JSON.parse(content);
|
|
3792
|
-
return Array.isArray(pkg.workspaces) || typeof pkg.workspaces?.packages !== "undefined";
|
|
3793
|
-
} catch {
|
|
3794
|
-
return false;
|
|
3795
|
-
}
|
|
3796
|
-
}
|
|
3797
|
-
}
|
|
3798
|
-
];
|
|
3799
|
-
for (const check of checks) {
|
|
3800
|
-
const filePath = join5(rootPath, check.file);
|
|
3801
|
-
if (existsSync4(filePath)) {
|
|
3802
|
-
if (!check.validate) return check.tool;
|
|
3803
|
-
try {
|
|
3804
|
-
const content = await readFile5(filePath, "utf-8");
|
|
3805
|
-
if (check.validate(content)) {
|
|
3806
|
-
if (check.tool === "npm-workspaces") {
|
|
3807
|
-
if (existsSync4(join5(rootPath, "yarn.lock"))) return "yarn-workspaces";
|
|
3808
|
-
return "npm-workspaces";
|
|
3809
|
-
}
|
|
3810
|
-
return check.tool;
|
|
3811
|
-
}
|
|
3812
|
-
} catch {
|
|
3813
|
-
}
|
|
3814
|
-
}
|
|
3900
|
+
// src/engine/logger.ts
|
|
3901
|
+
var LEVEL_ORDER = { debug: 0, info: 1, warn: 2, error: 3 };
|
|
3902
|
+
var currentLevel = process.env.CTO_LOG_LEVEL ?? "warn";
|
|
3903
|
+
var jsonOutput = process.env.CTO_LOG_JSON === "1";
|
|
3904
|
+
function shouldLog(level) {
|
|
3905
|
+
return LEVEL_ORDER[level] >= LEVEL_ORDER[currentLevel];
|
|
3906
|
+
}
|
|
3907
|
+
function emit(entry) {
|
|
3908
|
+
if (!shouldLog(entry.level)) return;
|
|
3909
|
+
if (jsonOutput) {
|
|
3910
|
+
const stream = entry.level === "error" ? process.stderr : process.stdout;
|
|
3911
|
+
stream.write(JSON.stringify(entry) + "\n");
|
|
3912
|
+
return;
|
|
3815
3913
|
}
|
|
3816
|
-
|
|
3817
|
-
}
|
|
3818
|
-
|
|
3819
|
-
|
|
3820
|
-
|
|
3821
|
-
|
|
3822
|
-
|
|
3823
|
-
|
|
3824
|
-
|
|
3825
|
-
|
|
3826
|
-
|
|
3827
|
-
|
|
3828
|
-
const pkgJsonPath = join5(searchDir, entry.name, "package.json");
|
|
3829
|
-
if (existsSync4(pkgJsonPath)) {
|
|
3830
|
-
packagePaths.push(join5(searchDir, entry.name));
|
|
3831
|
-
}
|
|
3832
|
-
}
|
|
3833
|
-
} catch {
|
|
3834
|
-
}
|
|
3914
|
+
const prefix = entry.module ? `[${entry.module}]` : "";
|
|
3915
|
+
const extra = Object.entries(entry).filter(([k]) => !["level", "msg", "ts", "module"].includes(k)).map(([k, v]) => `${k}=${typeof v === "object" ? JSON.stringify(v) : v}`).join(" ");
|
|
3916
|
+
const line = `${prefix} ${entry.msg}${extra ? " " + extra : ""}`.trim();
|
|
3917
|
+
if (entry.level === "error") {
|
|
3918
|
+
process.stderr.write(` \u274C ${line}
|
|
3919
|
+
`);
|
|
3920
|
+
} else if (entry.level === "warn") {
|
|
3921
|
+
process.stderr.write(` \u26A0\uFE0F ${line}
|
|
3922
|
+
`);
|
|
3923
|
+
} else {
|
|
3924
|
+
process.stdout.write(` ${line}
|
|
3925
|
+
`);
|
|
3835
3926
|
}
|
|
3836
|
-
return packagePaths;
|
|
3837
3927
|
}
|
|
3838
|
-
|
|
3839
|
-
|
|
3840
|
-
|
|
3841
|
-
|
|
3842
|
-
|
|
3843
|
-
|
|
3844
|
-
|
|
3845
|
-
|
|
3846
|
-
|
|
3847
|
-
|
|
3848
|
-
const packages = [];
|
|
3849
|
-
let inPackages = false;
|
|
3850
|
-
for (const line of content.split("\n")) {
|
|
3851
|
-
const trimmed = line.trim();
|
|
3852
|
-
if (trimmed === "packages:") {
|
|
3853
|
-
inPackages = true;
|
|
3854
|
-
continue;
|
|
3855
|
-
}
|
|
3856
|
-
if (inPackages && trimmed.startsWith("- ")) {
|
|
3857
|
-
packages.push(trimmed.slice(2).replace(/['"]/g, ""));
|
|
3858
|
-
} else if (inPackages && !trimmed.startsWith("-") && trimmed.length > 0) {
|
|
3859
|
-
inPackages = false;
|
|
3860
|
-
}
|
|
3861
|
-
}
|
|
3862
|
-
return resolveWorkspaceGlobs(rootPath, packages);
|
|
3863
|
-
}
|
|
3864
|
-
case "turborepo": {
|
|
3865
|
-
const pkgJson = JSON.parse(await readFile5(join5(rootPath, "package.json"), "utf-8"));
|
|
3866
|
-
const workspaces = Array.isArray(pkgJson.workspaces) ? pkgJson.workspaces : pkgJson.workspaces?.packages || [];
|
|
3867
|
-
if (workspaces.length > 0) return resolveWorkspaceGlobs(rootPath, workspaces);
|
|
3868
|
-
if (existsSync4(join5(rootPath, "pnpm-workspace.yaml"))) {
|
|
3869
|
-
return discoverPackages(rootPath, "pnpm-workspaces");
|
|
3870
|
-
}
|
|
3871
|
-
return [];
|
|
3872
|
-
}
|
|
3873
|
-
case "nx": {
|
|
3874
|
-
const standardDirs = ["packages", "apps", "libs"];
|
|
3875
|
-
const globs = standardDirs.filter((d) => existsSync4(join5(rootPath, d)));
|
|
3876
|
-
if (globs.length > 0) return resolveWorkspaceGlobs(rootPath, globs);
|
|
3877
|
-
try {
|
|
3878
|
-
const pkgJson = JSON.parse(await readFile5(join5(rootPath, "package.json"), "utf-8"));
|
|
3879
|
-
const workspaces = Array.isArray(pkgJson.workspaces) ? pkgJson.workspaces : [];
|
|
3880
|
-
if (workspaces.length > 0) return resolveWorkspaceGlobs(rootPath, workspaces);
|
|
3881
|
-
} catch {
|
|
3882
|
-
}
|
|
3883
|
-
return [];
|
|
3884
|
-
}
|
|
3885
|
-
case "lerna": {
|
|
3886
|
-
const lernaJson = JSON.parse(await readFile5(join5(rootPath, "lerna.json"), "utf-8"));
|
|
3887
|
-
const packages = lernaJson.packages || ["packages/*"];
|
|
3888
|
-
return resolveWorkspaceGlobs(rootPath, packages);
|
|
3889
|
-
}
|
|
3890
|
-
default:
|
|
3891
|
-
return [];
|
|
3892
|
-
}
|
|
3928
|
+
function createLogger(module) {
|
|
3929
|
+
const log5 = (level, msg, data) => {
|
|
3930
|
+
emit({ level, msg, ts: (/* @__PURE__ */ new Date()).toISOString(), module, ...data });
|
|
3931
|
+
};
|
|
3932
|
+
return {
|
|
3933
|
+
debug: (msg, data) => log5("debug", msg, data),
|
|
3934
|
+
info: (msg, data) => log5("info", msg, data),
|
|
3935
|
+
warn: (msg, data) => log5("warn", msg, data),
|
|
3936
|
+
error: (msg, data) => log5("error", msg, data)
|
|
3937
|
+
};
|
|
3893
3938
|
}
|
|
3894
|
-
|
|
3895
|
-
|
|
3896
|
-
|
|
3897
|
-
|
|
3898
|
-
|
|
3899
|
-
|
|
3900
|
-
|
|
3901
|
-
|
|
3939
|
+
|
|
3940
|
+
// src/engine/errors.ts
|
|
3941
|
+
var CtoError = class extends Error {
|
|
3942
|
+
code;
|
|
3943
|
+
module;
|
|
3944
|
+
context;
|
|
3945
|
+
constructor(code, message, module, context) {
|
|
3946
|
+
super(message);
|
|
3947
|
+
this.name = "CtoError";
|
|
3948
|
+
this.code = code;
|
|
3949
|
+
this.module = module;
|
|
3950
|
+
this.context = context;
|
|
3951
|
+
}
|
|
3952
|
+
toJSON() {
|
|
3953
|
+
return {
|
|
3954
|
+
name: this.name,
|
|
3955
|
+
code: this.code,
|
|
3956
|
+
message: this.message,
|
|
3957
|
+
module: this.module,
|
|
3958
|
+
context: this.context
|
|
3959
|
+
};
|
|
3960
|
+
}
|
|
3961
|
+
};
|
|
3962
|
+
|
|
3963
|
+
// src/cli/commands/fix.ts
|
|
3964
|
+
init_selector();
|
|
3965
|
+
import { join as join4 } from "path";
|
|
3966
|
+
import { mkdirSync as mkdirSync2, writeFileSync as writeFileSync2 } from "fs";
|
|
3967
|
+
var log = createLogger("cli:fix");
|
|
3968
|
+
async function runFix(projectPath, analysis, score) {
|
|
3969
|
+
const ctoDir = join4(projectPath, ".cto");
|
|
3970
|
+
mkdirSync2(ctoDir, { recursive: true });
|
|
3971
|
+
const selection = await selectContext({
|
|
3972
|
+
task: "general code review and refactoring",
|
|
3973
|
+
analysis,
|
|
3974
|
+
budget: 5e4
|
|
3975
|
+
});
|
|
3976
|
+
const criticalFiles = selection.files.filter((f) => f.riskScore >= 60).sort((a, b) => b.riskScore - a.riskScore);
|
|
3977
|
+
const typeFiles = analysis.files.filter((f) => f.kind === "type").sort((a, b) => b.riskScore - a.riskScore);
|
|
3978
|
+
const entryFiles = analysis.files.filter((f) => f.kind === "entry").sort((a, b) => b.riskScore - a.riskScore);
|
|
3979
|
+
let contextMd = `# CTO Context \u2014 ${analysis.projectName}
|
|
3980
|
+
`;
|
|
3981
|
+
contextMd += `# Generated by cto-ai-cli \xB7 Score: ${score.overall}/100 (${score.grade})
|
|
3982
|
+
`;
|
|
3983
|
+
contextMd += `# Paste this into your AI prompt for better results.
|
|
3984
|
+
|
|
3985
|
+
`;
|
|
3986
|
+
contextMd += `## Critical files (always include these):
|
|
3987
|
+
`;
|
|
3988
|
+
for (const f of criticalFiles.slice(0, 15)) {
|
|
3989
|
+
contextMd += `- ${f.relativePath} \u2014 risk:${f.riskScore} (${f.reason})
|
|
3990
|
+
`;
|
|
3991
|
+
}
|
|
3992
|
+
if (typeFiles.length > 0) {
|
|
3993
|
+
contextMd += `
|
|
3994
|
+
## Type definitions (AI needs these to generate correct code):
|
|
3995
|
+
`;
|
|
3996
|
+
for (const f of typeFiles.slice(0, 10)) {
|
|
3997
|
+
contextMd += `- ${f.relativePath} (${f.tokens} tokens)
|
|
3998
|
+
`;
|
|
3902
3999
|
}
|
|
3903
4000
|
}
|
|
3904
|
-
|
|
3905
|
-
|
|
3906
|
-
|
|
3907
|
-
|
|
3908
|
-
|
|
3909
|
-
|
|
3910
|
-
|
|
3911
|
-
edgeMap.set(key, { files: /* @__PURE__ */ new Set(), type: "dependency" });
|
|
3912
|
-
}
|
|
3913
|
-
edgeMap.get(key).files.add(edge.from);
|
|
4001
|
+
if (entryFiles.length > 0) {
|
|
4002
|
+
contextMd += `
|
|
4003
|
+
## Entry points:
|
|
4004
|
+
`;
|
|
4005
|
+
for (const f of entryFiles.slice(0, 5)) {
|
|
4006
|
+
contextMd += `- ${f.relativePath}
|
|
4007
|
+
`;
|
|
3914
4008
|
}
|
|
3915
4009
|
}
|
|
3916
|
-
|
|
3917
|
-
|
|
3918
|
-
|
|
3919
|
-
})
|
|
3920
|
-
|
|
3921
|
-
|
|
3922
|
-
|
|
3923
|
-
|
|
3924
|
-
|
|
3925
|
-
|
|
3926
|
-
|
|
3927
|
-
|
|
3928
|
-
|
|
4010
|
+
contextMd += `
|
|
4011
|
+
## Project structure:
|
|
4012
|
+
`;
|
|
4013
|
+
contextMd += `- ${analysis.totalFiles} files, ${Math.round(analysis.totalTokens / 1e3)}K tokens total
|
|
4014
|
+
`;
|
|
4015
|
+
contextMd += `- Stack: ${analysis.stack.join(", ") || "unknown"}
|
|
4016
|
+
`;
|
|
4017
|
+
contextMd += `- Clusters: ${analysis.graph.clusters.length}
|
|
4018
|
+
`;
|
|
4019
|
+
contextMd += `- Hubs: ${analysis.graph.hubs.map((h) => h.relativePath).slice(0, 5).join(", ")}
|
|
4020
|
+
`;
|
|
4021
|
+
contextMd += `
|
|
4022
|
+
## Recommended token budget: ${selection.totalTokens.toLocaleString()} tokens
|
|
4023
|
+
`;
|
|
4024
|
+
contextMd += `## Full project tokens: ${analysis.totalTokens.toLocaleString()} tokens
|
|
4025
|
+
`;
|
|
4026
|
+
contextMd += `## Savings: ${score.comparison.savedPercent}% (${formatTokens(score.comparison.savedTokens)})
|
|
4027
|
+
`;
|
|
4028
|
+
writeFileSync2(join4(ctoDir, "context.md"), contextMd);
|
|
4029
|
+
const config = {
|
|
4030
|
+
version: "5.0",
|
|
4031
|
+
project: analysis.projectName,
|
|
4032
|
+
generatedAt: (/* @__PURE__ */ new Date()).toISOString(),
|
|
4033
|
+
score: score.overall,
|
|
4034
|
+
grade: score.grade,
|
|
4035
|
+
budget: 5e4,
|
|
4036
|
+
criticalFiles: criticalFiles.slice(0, 20).map((f) => f.relativePath),
|
|
4037
|
+
typeFiles: typeFiles.map((f) => f.relativePath),
|
|
4038
|
+
ignorePatterns: [
|
|
4039
|
+
"node_modules",
|
|
4040
|
+
"dist",
|
|
4041
|
+
"build",
|
|
4042
|
+
".git",
|
|
4043
|
+
"*.test.*",
|
|
4044
|
+
"*.spec.*",
|
|
4045
|
+
"__tests__",
|
|
4046
|
+
"*.md",
|
|
4047
|
+
"*.json",
|
|
4048
|
+
"*.lock"
|
|
4049
|
+
],
|
|
4050
|
+
insights: score.insights.slice(0, 5).map((i) => ({
|
|
4051
|
+
type: i.type,
|
|
4052
|
+
title: i.title,
|
|
4053
|
+
impact: i.impact
|
|
4054
|
+
}))
|
|
4055
|
+
};
|
|
4056
|
+
writeFileSync2(join4(ctoDir, "config.json"), JSON.stringify(config, null, 2));
|
|
4057
|
+
const ignoreContent = [
|
|
4058
|
+
"# CTO AI-ignore \u2014 files that add noise to AI context",
|
|
4059
|
+
"# Generated by cto-ai-cli",
|
|
4060
|
+
"",
|
|
4061
|
+
"# Build artifacts",
|
|
4062
|
+
"dist/",
|
|
4063
|
+
"build/",
|
|
4064
|
+
".next/",
|
|
4065
|
+
"coverage/",
|
|
4066
|
+
"",
|
|
4067
|
+
"# Dependencies",
|
|
4068
|
+
"node_modules/",
|
|
4069
|
+
"",
|
|
4070
|
+
"# Large/binary files",
|
|
4071
|
+
"*.lock",
|
|
4072
|
+
"package-lock.json",
|
|
4073
|
+
"yarn.lock",
|
|
4074
|
+
"",
|
|
4075
|
+
"# Low-value for AI",
|
|
4076
|
+
...analysis.graph.orphans.filter((o) => {
|
|
4077
|
+
const f = analysis.files.find((af) => af.relativePath === o);
|
|
4078
|
+
return f && f.riskScore < 20;
|
|
4079
|
+
}).slice(0, 20).map((o) => `${o} # orphan, low-risk`),
|
|
4080
|
+
""
|
|
4081
|
+
].join("\n");
|
|
4082
|
+
writeFileSync2(join4(ctoDir, ".cteignore"), ignoreContent);
|
|
4083
|
+
log.info("Fix complete", { files: 3, dir: ctoDir });
|
|
4084
|
+
console.log("");
|
|
4085
|
+
console.log(" \u2705 Auto-fix complete! Generated:");
|
|
4086
|
+
console.log("");
|
|
4087
|
+
console.log(" \u{1F4CB} .cto/context.md Copy-paste this into Claude/Cursor/ChatGPT");
|
|
4088
|
+
console.log(" \u2699\uFE0F .cto/config.json Optimized CTO configuration");
|
|
4089
|
+
console.log(" \u{1F6AB} .cto/.cteignore Files AI should skip");
|
|
4090
|
+
console.log("");
|
|
4091
|
+
console.log(" \u{1F4A1} Tip: Paste .cto/context.md at the start of your AI conversation");
|
|
4092
|
+
console.log(" for dramatically better code generation.");
|
|
4093
|
+
console.log("");
|
|
4094
|
+
}
|
|
4095
|
+
function formatTokens(n) {
|
|
4096
|
+
if (n >= 1e6) return `${(n / 1e6).toFixed(1)}M`;
|
|
4097
|
+
if (n >= 1e3) return `${(n / 1e3).toFixed(1)}K`;
|
|
4098
|
+
return n.toString();
|
|
4099
|
+
}
|
|
4100
|
+
|
|
4101
|
+
// src/cli/commands/context.ts
|
|
4102
|
+
init_selector();
|
|
4103
|
+
import { join as join5 } from "path";
|
|
4104
|
+
import { mkdirSync as mkdirSync3, writeFileSync as writeFileSync3, readFileSync as readFileSync2 } from "fs";
|
|
4105
|
+
var log2 = createLogger("cli:context");
|
|
4106
|
+
async function runContext(projectPath, analysis, task) {
|
|
4107
|
+
const ctoDir = join5(projectPath, ".cto");
|
|
4108
|
+
mkdirSync3(ctoDir, { recursive: true });
|
|
4109
|
+
const selection = await selectContext({ task, analysis, budget: 5e4 });
|
|
4110
|
+
const typeFiles = analysis.files.filter((f) => f.kind === "type");
|
|
4111
|
+
const selectedPaths = new Set(selection.files.map((f) => f.relativePath));
|
|
4112
|
+
const includedTypes = typeFiles.filter((f) => selectedPaths.has(f.relativePath));
|
|
4113
|
+
let contextMd = `# Context for: ${task}
|
|
4114
|
+
`;
|
|
4115
|
+
contextMd += `# Generated by cto-ai-cli
|
|
4116
|
+
`;
|
|
4117
|
+
contextMd += `# ${selection.files.length} files selected, ${selection.totalTokens.toLocaleString()} tokens
|
|
4118
|
+
|
|
4119
|
+
`;
|
|
4120
|
+
const targets = selection.files.filter((f) => f.reason === "Target file");
|
|
4121
|
+
const critical = selection.files.filter((f) => f.riskScore >= 80 && f.reason !== "Target file");
|
|
4122
|
+
const high = selection.files.filter((f) => f.riskScore >= 60 && f.riskScore < 80 && f.reason !== "Target file");
|
|
4123
|
+
const rest = selection.files.filter((f) => f.riskScore < 60 && f.reason !== "Target file");
|
|
4124
|
+
if (targets.length > 0) {
|
|
4125
|
+
contextMd += `## Target files (directly related to task):
|
|
4126
|
+
`;
|
|
4127
|
+
for (const f of targets) contextMd += `- ${f.relativePath}
|
|
4128
|
+
`;
|
|
4129
|
+
contextMd += "\n";
|
|
4130
|
+
}
|
|
4131
|
+
if (critical.length > 0) {
|
|
4132
|
+
contextMd += `## Critical dependencies:
|
|
4133
|
+
`;
|
|
4134
|
+
for (const f of critical) contextMd += `- ${f.relativePath} \u2014 ${f.reason}
|
|
4135
|
+
`;
|
|
4136
|
+
contextMd += "\n";
|
|
4137
|
+
}
|
|
4138
|
+
if (includedTypes.length > 0) {
|
|
4139
|
+
contextMd += `## Type definitions (needed for correct code generation):
|
|
4140
|
+
`;
|
|
4141
|
+
for (const f of includedTypes) contextMd += `- ${f.relativePath}
|
|
4142
|
+
`;
|
|
4143
|
+
contextMd += "\n";
|
|
4144
|
+
}
|
|
4145
|
+
if (high.length > 0) {
|
|
4146
|
+
contextMd += `## High-relevance files:
|
|
4147
|
+
`;
|
|
4148
|
+
for (const f of high) contextMd += `- ${f.relativePath}
|
|
4149
|
+
`;
|
|
4150
|
+
contextMd += "\n";
|
|
4151
|
+
}
|
|
4152
|
+
if (rest.length > 0) {
|
|
4153
|
+
contextMd += `## Supporting files:
|
|
4154
|
+
`;
|
|
4155
|
+
for (const f of rest.slice(0, 15)) contextMd += `- ${f.relativePath}
|
|
4156
|
+
`;
|
|
4157
|
+
if (rest.length > 15) contextMd += `- ... and ${rest.length - 15} more
|
|
4158
|
+
`;
|
|
4159
|
+
contextMd += "\n";
|
|
4160
|
+
}
|
|
4161
|
+
contextMd += `---
|
|
4162
|
+
|
|
4163
|
+
`;
|
|
4164
|
+
contextMd += `## File contents (top ${Math.min(targets.length + critical.length, 10)} files):
|
|
4165
|
+
|
|
4166
|
+
`;
|
|
4167
|
+
const topFiles = [...targets, ...critical].slice(0, 10);
|
|
4168
|
+
for (const sf of topFiles) {
|
|
4169
|
+
const fullFile = analysis.files.find((f) => f.relativePath === sf.relativePath);
|
|
4170
|
+
if (!fullFile) continue;
|
|
4171
|
+
try {
|
|
4172
|
+
const content = readFileSync2(fullFile.path, "utf-8");
|
|
4173
|
+
const ext = fullFile.extension.replace(".", "");
|
|
4174
|
+
const maxChars = 5e3;
|
|
4175
|
+
const truncated = content.length > maxChars;
|
|
4176
|
+
contextMd += `### ${sf.relativePath}
|
|
4177
|
+
`;
|
|
4178
|
+
contextMd += `\`\`\`${ext}
|
|
4179
|
+
${content.slice(0, maxChars)}${truncated ? "\n// ... [truncated \u2014 " + (content.length - maxChars) + " chars omitted]" : ""}
|
|
4180
|
+
\`\`\`
|
|
4181
|
+
|
|
4182
|
+
`;
|
|
4183
|
+
} catch (err) {
|
|
4184
|
+
log2.debug("Could not read file for context", { file: fullFile.path, error: String(err) });
|
|
4185
|
+
}
|
|
4186
|
+
}
|
|
4187
|
+
const safeName = task.replace(/[^a-zA-Z0-9]/g, "-").toLowerCase().slice(0, 40);
|
|
4188
|
+
const filename = `context-${safeName}.md`;
|
|
4189
|
+
writeFileSync3(join5(ctoDir, filename), contextMd);
|
|
4190
|
+
console.log("");
|
|
4191
|
+
console.log(` \u2705 Task context generated!`);
|
|
4192
|
+
console.log("");
|
|
4193
|
+
console.log(` \u{1F4CB} .cto/${filename}`);
|
|
4194
|
+
console.log(` ${selection.files.length} files \xB7 ${selection.totalTokens.toLocaleString()} tokens`);
|
|
4195
|
+
console.log(` Coverage: ${selection.coverage.score}%`);
|
|
4196
|
+
console.log("");
|
|
4197
|
+
console.log(` \u{1F4A1} Copy-paste this file into Claude/Cursor/ChatGPT for`);
|
|
4198
|
+
console.log(` optimized context on: "${task}"`);
|
|
4199
|
+
console.log("");
|
|
4200
|
+
}
|
|
4201
|
+
|
|
4202
|
+
// src/cli/commands/report.ts
|
|
4203
|
+
import { join as join6 } from "path";
|
|
4204
|
+
import { mkdirSync as mkdirSync4, writeFileSync as writeFileSync4 } from "fs";
|
|
4205
|
+
async function runReport(projectPath, analysis, score) {
|
|
4206
|
+
const gradeEmoji = score.grade.startsWith("A") ? "\u{1F7E2}" : score.grade.startsWith("B") ? "\u{1F535}" : score.grade.startsWith("C") ? "\u{1F7E1}" : "\u{1F534}";
|
|
4207
|
+
const gradeColor = score.overall >= 80 ? "brightgreen" : score.overall >= 60 ? "green" : score.overall >= 40 ? "yellow" : "red";
|
|
4208
|
+
const safeGrade = encodeURIComponent(score.grade);
|
|
4209
|
+
let report = `# CTO Context Score\u2122 Report
|
|
4210
|
+
|
|
4211
|
+
`;
|
|
4212
|
+
report += `
|
|
4213
|
+
`;
|
|
4214
|
+
report += `
|
|
4215
|
+
|
|
4216
|
+
`;
|
|
4217
|
+
report += `> Generated by [cto-ai-cli](https://npmjs.com/package/cto-ai-cli) on ${(/* @__PURE__ */ new Date()).toISOString().split("T")[0]}
|
|
4218
|
+
|
|
4219
|
+
`;
|
|
4220
|
+
report += `## Project: ${analysis.projectName}
|
|
4221
|
+
|
|
4222
|
+
`;
|
|
4223
|
+
report += `| Metric | Value |
|
|
4224
|
+
|--------|-------|
|
|
4225
|
+
`;
|
|
4226
|
+
report += `| **Score** | ${gradeEmoji} ${score.overall}/100 (${score.grade}) |
|
|
4227
|
+
`;
|
|
4228
|
+
report += `| **Files** | ${analysis.totalFiles} |
|
|
4229
|
+
`;
|
|
4230
|
+
report += `| **Total tokens** | ${analysis.totalTokens.toLocaleString()} |
|
|
4231
|
+
`;
|
|
4232
|
+
report += `| **Optimized tokens** | ${score.comparison.optimizedTokens.toLocaleString()} |
|
|
4233
|
+
`;
|
|
4234
|
+
report += `| **Token savings** | ${score.comparison.savedPercent}% (${formatTokens(score.comparison.savedTokens)}) |
|
|
4235
|
+
`;
|
|
4236
|
+
report += `| **Est. monthly savings** | $${score.comparison.monthlySavingsUSD.toFixed(2)} |
|
|
4237
|
+
`;
|
|
4238
|
+
report += `| **Stack** | ${analysis.stack.join(", ") || "unknown"} |
|
|
4239
|
+
|
|
4240
|
+
`;
|
|
4241
|
+
report += `## Dimensions
|
|
4242
|
+
|
|
4243
|
+
`;
|
|
4244
|
+
report += `| Dimension | Score | Weight | Detail |
|
|
4245
|
+
|-----------|-------|--------|--------|
|
|
4246
|
+
`;
|
|
4247
|
+
report += `| Efficiency | ${score.dimensions.efficiency.score}% | 30% | ${score.dimensions.efficiency.detail} |
|
|
4248
|
+
`;
|
|
4249
|
+
report += `| Coverage | ${score.dimensions.coverage.score}% | 25% | ${score.dimensions.coverage.detail} |
|
|
4250
|
+
`;
|
|
4251
|
+
report += `| Risk Control | ${score.dimensions.riskControl.score}% | 20% | ${score.dimensions.riskControl.detail} |
|
|
4252
|
+
`;
|
|
4253
|
+
report += `| Structure | ${score.dimensions.structure.score}% | 15% | ${score.dimensions.structure.detail} |
|
|
4254
|
+
`;
|
|
4255
|
+
report += `| Governance | ${score.dimensions.governance.score}% | 10% | ${score.dimensions.governance.detail} |
|
|
4256
|
+
|
|
4257
|
+
`;
|
|
4258
|
+
if (score.insights.length > 0) {
|
|
4259
|
+
report += `## Insights
|
|
4260
|
+
|
|
4261
|
+
`;
|
|
4262
|
+
for (const insight of score.insights.slice(0, 8)) {
|
|
4263
|
+
const icon = insight.type === "strength" ? "\u2705" : insight.type === "weakness" ? "\u26A0\uFE0F" : "\u{1F4A1}";
|
|
4264
|
+
report += `- ${icon} **${insight.title}** \u2014 ${insight.detail}
|
|
4265
|
+
`;
|
|
4266
|
+
}
|
|
4267
|
+
report += "\n";
|
|
4268
|
+
}
|
|
4269
|
+
report += `## Badge for your README
|
|
4270
|
+
|
|
4271
|
+
`;
|
|
4272
|
+
report += `\`\`\`markdown
|
|
4273
|
+

|
|
4274
|
+
\`\`\`
|
|
4275
|
+
|
|
4276
|
+
`;
|
|
4277
|
+
report += `---
|
|
4278
|
+
|
|
4279
|
+
*Run \`npx cto-ai-cli\` to generate your own report. [Learn more](https://npmjs.com/package/cto-ai-cli)*
|
|
4280
|
+
`;
|
|
4281
|
+
const ctoDir = join6(projectPath, ".cto");
|
|
4282
|
+
mkdirSync4(ctoDir, { recursive: true });
|
|
4283
|
+
writeFileSync4(join6(ctoDir, "report.md"), report);
|
|
4284
|
+
console.log("");
|
|
4285
|
+
console.log(" \u2705 Report generated!");
|
|
4286
|
+
console.log("");
|
|
4287
|
+
console.log(" \u{1F4CA} .cto/report.md Share on Slack, Discord, or GitHub");
|
|
4288
|
+
console.log("");
|
|
4289
|
+
console.log(" \u{1F3F7}\uFE0F Badge for your README:");
|
|
4290
|
+
console.log(` `);
|
|
4291
|
+
console.log("");
|
|
4292
|
+
}
|
|
4293
|
+
|
|
4294
|
+
// src/cli/commands/compare.ts
|
|
4295
|
+
var PROJECT_PROFILES = [
|
|
4296
|
+
// Zod: Small, pure TypeScript, excellent types, focused API
|
|
4297
|
+
{ name: "Zod", files: 441, tokens: "804K", efficiency: 88, coverage: 98, riskControl: 95, structure: 82, governance: 100 },
|
|
4298
|
+
// Prisma: Well-structured, strongly typed, clear module boundaries
|
|
4299
|
+
{ name: "Prisma Client", files: 320, tokens: "650K", efficiency: 82, coverage: 95, riskControl: 90, structure: 78, governance: 100 },
|
|
4300
|
+
// tRPC: TypeScript-first, moderate size, good structure
|
|
4301
|
+
{ name: "tRPC", files: 280, tokens: "420K", efficiency: 80, coverage: 92, riskControl: 85, structure: 72, governance: 100 },
|
|
4302
|
+
// Next.js: Large, complex, many entry points, harder to compress
|
|
4303
|
+
{ name: "Next.js (core)", files: 890, tokens: "2.1M", efficiency: 72, coverage: 85, riskControl: 75, structure: 65, governance: 90 },
|
|
4304
|
+
// Express: Small but flat structure, few types, weak graph
|
|
4305
|
+
{ name: "Express.js", files: 158, tokens: "171K", efficiency: 68, coverage: 80, riskControl: 78, structure: 55, governance: 75 },
|
|
4306
|
+
// Lodash: Many small independent files, no graph, no types
|
|
4307
|
+
{ name: "Lodash", files: 612, tokens: "380K", efficiency: 60, coverage: 70, riskControl: 65, structure: 40, governance: 60 }
|
|
4308
|
+
];
|
|
4309
|
+
function computeProfileScore(p) {
|
|
4310
|
+
const overall = Math.round(
|
|
4311
|
+
p.efficiency / 100 * 30 + p.coverage / 100 * 25 + p.riskControl / 100 * 20 + p.structure / 100 * 15 + p.governance / 100 * 10
|
|
4312
|
+
);
|
|
4313
|
+
const grade = overall >= 95 ? "A+" : overall >= 90 ? "A" : overall >= 85 ? "A-" : overall >= 80 ? "B+" : overall >= 75 ? "B" : overall >= 70 ? "B-" : overall >= 65 ? "C+" : overall >= 60 ? "C" : overall >= 55 ? "C-" : overall >= 40 ? "D" : "F";
|
|
4314
|
+
return { score: overall, grade };
|
|
4315
|
+
}
|
|
4316
|
+
function runCompare(score) {
|
|
4317
|
+
const benchmarks = PROJECT_PROFILES.map((p) => {
|
|
4318
|
+
const computed = computeProfileScore(p);
|
|
4319
|
+
return { name: p.name, score: computed.score, grade: computed.grade, files: p.files, tokens: p.tokens };
|
|
4320
|
+
});
|
|
4321
|
+
console.log("");
|
|
4322
|
+
console.log(" \u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550");
|
|
4323
|
+
console.log(" \u{1F4CA} Your project vs popular open source");
|
|
4324
|
+
console.log(" \u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550");
|
|
4325
|
+
console.log("");
|
|
4326
|
+
const all = [
|
|
4327
|
+
{ name: `\u2192 ${score.meta.projectName} (you)`, score: score.overall, grade: score.grade, isYou: true },
|
|
4328
|
+
...benchmarks.map((b) => ({ ...b, isYou: false }))
|
|
4329
|
+
].sort((a, b) => b.score - a.score);
|
|
4330
|
+
for (const entry of all) {
|
|
4331
|
+
const bar = renderCompareBar(entry.score);
|
|
4332
|
+
const marker = entry.isYou ? " \u25C4" : "";
|
|
4333
|
+
const name = entry.name.padEnd(25);
|
|
4334
|
+
console.log(` ${name} ${entry.score.toString().padStart(3)}/100 (${entry.grade.padEnd(2)}) ${bar}${marker}`);
|
|
4335
|
+
}
|
|
4336
|
+
console.log("");
|
|
4337
|
+
const beaten = benchmarks.filter((b) => score.overall > b.score);
|
|
4338
|
+
const aheadOf = benchmarks.filter((b) => score.overall < b.score);
|
|
4339
|
+
if (beaten.length > 0) console.log(` \u2705 You beat: ${beaten.map((b) => b.name).join(", ")}`);
|
|
4340
|
+
if (aheadOf.length > 0 && aheadOf.length <= 3) {
|
|
4341
|
+
console.log(` \u{1F3AF} To reach ${aheadOf[0].name}'s level: run --fix and address the insights above`);
|
|
4342
|
+
}
|
|
4343
|
+
if (beaten.length === benchmarks.length) console.log(` \u{1F3C6} You outperform ALL benchmarked projects!`);
|
|
4344
|
+
console.log("");
|
|
4345
|
+
console.log(" \u2139 Comparison scores are estimated from project profiles using the");
|
|
4346
|
+
console.log(" same scoring algorithm. Run CTO on each repo for exact scores.");
|
|
4347
|
+
console.log("");
|
|
4348
|
+
}
|
|
4349
|
+
function renderCompareBar(pct) {
|
|
4350
|
+
const width = 25;
|
|
4351
|
+
const filled = Math.round(pct / 100 * width);
|
|
4352
|
+
return "\u2588".repeat(filled) + "\u2591".repeat(width - filled);
|
|
4353
|
+
}
|
|
4354
|
+
|
|
4355
|
+
// src/cli/commands/audit.ts
|
|
4356
|
+
init_secrets();
|
|
4357
|
+
import { join as join7 } from "path";
|
|
4358
|
+
import { mkdirSync as mkdirSync5, writeFileSync as writeFileSync5, appendFileSync } from "fs";
|
|
4359
|
+
var log3 = createLogger("cli:audit");
|
|
4360
|
+
async function runAudit(projectPath, analysis, flags = {}) {
|
|
4361
|
+
if (flags.initHookMode) {
|
|
4362
|
+
const { generatePreCommitHook: generatePreCommitHook2 } = await Promise.resolve().then(() => (init_secrets(), secrets_exports));
|
|
4363
|
+
const hookPath = generatePreCommitHook2(projectPath, "husky");
|
|
4364
|
+
console.log("");
|
|
4365
|
+
console.log(" \u2705 Pre-commit hook generated!");
|
|
4366
|
+
console.log(` \u{1F4CB} ${hookPath}`);
|
|
4367
|
+
console.log("");
|
|
4368
|
+
console.log(" Staged files will be scanned for secrets before every commit.");
|
|
4369
|
+
console.log(" To remove: delete the hook file.");
|
|
4370
|
+
console.log("");
|
|
4371
|
+
return;
|
|
4372
|
+
}
|
|
4373
|
+
console.log("");
|
|
4374
|
+
console.log(" \u{1F50D} Running security audit...");
|
|
4375
|
+
console.log("");
|
|
4376
|
+
const filePaths = analysis.files.map((f) => f.path);
|
|
4377
|
+
const result = await auditProject(projectPath, filePaths, {
|
|
4378
|
+
includePII: true,
|
|
4379
|
+
incrementalScan: !flags.fullScanMode,
|
|
4380
|
+
useAllowlist: !flags.noAllowlistMode
|
|
4381
|
+
});
|
|
4382
|
+
const { summary, findings, recommendations } = result;
|
|
4383
|
+
const statusIcon = summary.bySeverity.critical > 0 ? "\u{1F534}" : summary.bySeverity.high > 0 ? "\u{1F7E0}" : summary.totalFindings > 0 ? "\u{1F7E1}" : "\u{1F7E2}";
|
|
4384
|
+
const statusText = summary.bySeverity.critical > 0 ? "CRITICAL ISSUES FOUND" : summary.bySeverity.high > 0 ? "HIGH-SEVERITY ISSUES FOUND" : summary.totalFindings > 0 ? "MINOR ISSUES FOUND" : "ALL CLEAR";
|
|
4385
|
+
console.log(" \u2554\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2557");
|
|
4386
|
+
console.log(" \u2551 \u2551");
|
|
4387
|
+
console.log(` \u2551 ${statusIcon} Security Audit: ${statusText.padEnd(28)} \u2551`);
|
|
4388
|
+
console.log(" \u2551 \u2551");
|
|
4389
|
+
console.log(` \u2551 Files scanned: ${summary.filesScanned.toString().padEnd(30)} \u2551`);
|
|
4390
|
+
console.log(` \u2551 Files affected: ${summary.filesWithSecrets.toString().padEnd(30)} \u2551`);
|
|
4391
|
+
console.log(` \u2551 Total findings: ${summary.totalFindings.toString().padEnd(30)} \u2551`);
|
|
4392
|
+
console.log(" \u2551 \u2551");
|
|
4393
|
+
console.log(" \u2560\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2563");
|
|
4394
|
+
console.log(" \u2551 \u2551");
|
|
4395
|
+
if (summary.bySeverity.critical > 0) console.log(` \u2551 \u{1F534} Critical: ${summary.bySeverity.critical.toString().padEnd(33)} \u2551`);
|
|
4396
|
+
if (summary.bySeverity.high > 0) console.log(` \u2551 \u{1F7E0} High: ${summary.bySeverity.high.toString().padEnd(33)} \u2551`);
|
|
4397
|
+
if (summary.bySeverity.medium > 0) console.log(` \u2551 \u{1F7E1} Medium: ${summary.bySeverity.medium.toString().padEnd(33)} \u2551`);
|
|
4398
|
+
if (summary.bySeverity.low > 0) console.log(` \u2551 \u{1F535} Low: ${summary.bySeverity.low.toString().padEnd(33)} \u2551`);
|
|
4399
|
+
if (summary.totalFindings === 0) console.log(" \u2551 \u2705 No secrets or PII detected \u2551");
|
|
4400
|
+
console.log(" \u2551 \u2551");
|
|
4401
|
+
if (Object.keys(summary.byType).length > 0) {
|
|
4402
|
+
console.log(" \u2560\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2563");
|
|
4403
|
+
console.log(" \u2551 \u2551");
|
|
4404
|
+
console.log(" \u2551 By type: \u2551");
|
|
4405
|
+
for (const [type, count] of Object.entries(summary.byType)) {
|
|
4406
|
+
const label = type.padEnd(18);
|
|
4407
|
+
console.log(` \u2551 ${label} ${count.toString().padEnd(28)} \u2551`);
|
|
4408
|
+
}
|
|
4409
|
+
console.log(" \u2551 \u2551");
|
|
4410
|
+
}
|
|
4411
|
+
console.log(" \u255A\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u255D");
|
|
4412
|
+
if (findings.length > 0) {
|
|
4413
|
+
console.log("");
|
|
4414
|
+
console.log(" Findings:");
|
|
4415
|
+
console.log("");
|
|
4416
|
+
const shown = findings.slice(0, 15);
|
|
4417
|
+
for (const f of shown) {
|
|
4418
|
+
const icon = f.severity === "critical" ? "\u{1F534}" : f.severity === "high" ? "\u{1F7E0}" : f.severity === "medium" ? "\u{1F7E1}" : "\u{1F535}";
|
|
4419
|
+
const sev = f.severity.toUpperCase().padEnd(8);
|
|
4420
|
+
console.log(` ${icon} ${sev} ${f.file}:${f.line}`);
|
|
4421
|
+
console.log(` ${f.type}: ${f.redacted}`);
|
|
4422
|
+
}
|
|
4423
|
+
if (findings.length > 15) {
|
|
4424
|
+
console.log(` ... and ${findings.length - 15} more (see .cto/audit/ for full report)`);
|
|
4425
|
+
}
|
|
4426
|
+
}
|
|
4427
|
+
if (recommendations.length > 0) {
|
|
4428
|
+
console.log("");
|
|
4429
|
+
console.log(" Recommendations:");
|
|
4430
|
+
console.log("");
|
|
4431
|
+
for (const rec of recommendations) {
|
|
4432
|
+
const icon = rec.startsWith("CRITICAL") ? "\u{1F6A8}" : "\u{1F4A1}";
|
|
4433
|
+
console.log(` ${icon} ${rec}`);
|
|
4434
|
+
}
|
|
4435
|
+
}
|
|
4436
|
+
const ctoDir = join7(projectPath, ".cto");
|
|
4437
|
+
const auditDir = join7(ctoDir, "audit");
|
|
4438
|
+
mkdirSync5(auditDir, { recursive: true });
|
|
4439
|
+
const now = /* @__PURE__ */ new Date();
|
|
4440
|
+
const dateStr = now.toISOString().split("T")[0];
|
|
4441
|
+
const logFile = join7(auditDir, `${dateStr}.jsonl`);
|
|
4442
|
+
const logEntry = {
|
|
4443
|
+
timestamp: now.toISOString(),
|
|
4444
|
+
version: "5.0.0",
|
|
4445
|
+
summary: {
|
|
4446
|
+
filesScanned: summary.filesScanned,
|
|
4447
|
+
filesWithSecrets: summary.filesWithSecrets,
|
|
4448
|
+
totalFindings: summary.totalFindings,
|
|
4449
|
+
bySeverity: summary.bySeverity,
|
|
4450
|
+
byType: summary.byType
|
|
4451
|
+
},
|
|
4452
|
+
findings: findings.map((f) => ({
|
|
4453
|
+
type: f.type,
|
|
4454
|
+
file: f.file,
|
|
4455
|
+
line: f.line,
|
|
4456
|
+
severity: f.severity,
|
|
4457
|
+
redacted: f.redacted
|
|
4458
|
+
}))
|
|
4459
|
+
};
|
|
4460
|
+
appendFileSync(logFile, JSON.stringify(logEntry) + "\n");
|
|
4461
|
+
let report = `# Security Audit Report
|
|
4462
|
+
|
|
4463
|
+
`;
|
|
4464
|
+
report += `> Generated by cto-ai-cli on ${now.toISOString()}
|
|
4465
|
+
|
|
4466
|
+
`;
|
|
4467
|
+
report += `## Summary
|
|
4468
|
+
|
|
4469
|
+
| Metric | Value |
|
|
4470
|
+
|--------|-------|
|
|
4471
|
+
`;
|
|
4472
|
+
report += `| Files scanned | ${summary.filesScanned} |
|
|
4473
|
+
`;
|
|
4474
|
+
report += `| Files with issues | ${summary.filesWithSecrets} |
|
|
4475
|
+
`;
|
|
4476
|
+
report += `| Total findings | ${summary.totalFindings} |
|
|
4477
|
+
`;
|
|
4478
|
+
report += `| Critical | ${summary.bySeverity.critical} |
|
|
4479
|
+
`;
|
|
4480
|
+
report += `| High | ${summary.bySeverity.high} |
|
|
4481
|
+
`;
|
|
4482
|
+
report += `| Medium | ${summary.bySeverity.medium} |
|
|
4483
|
+
|
|
4484
|
+
`;
|
|
4485
|
+
if (findings.length > 0) {
|
|
4486
|
+
report += `## Findings
|
|
4487
|
+
|
|
4488
|
+
| Severity | Type | File | Line | Redacted |
|
|
4489
|
+
|----------|------|------|------|----------|
|
|
4490
|
+
`;
|
|
4491
|
+
for (const f of findings) {
|
|
4492
|
+
report += `| ${f.severity} | ${f.type} | ${f.file} | ${f.line} | \`${f.redacted.slice(0, 30)}\` |
|
|
4493
|
+
`;
|
|
4494
|
+
}
|
|
4495
|
+
report += "\n";
|
|
4496
|
+
}
|
|
4497
|
+
if (recommendations.length > 0) {
|
|
4498
|
+
report += `## Recommendations
|
|
4499
|
+
|
|
4500
|
+
`;
|
|
4501
|
+
for (const rec of recommendations) report += `- ${rec}
|
|
4502
|
+
`;
|
|
4503
|
+
}
|
|
4504
|
+
writeFileSync5(join7(auditDir, "report.md"), report);
|
|
4505
|
+
const envSecrets = findings.filter(
|
|
4506
|
+
(f) => f.type === "env-variable" || f.type === "password" || f.type === "api-key" || f.type === "aws-key" || f.type === "connection-string"
|
|
4507
|
+
);
|
|
4508
|
+
if (envSecrets.length > 0) {
|
|
4509
|
+
const envVarNames = /* @__PURE__ */ new Set();
|
|
4510
|
+
for (const f of envSecrets) {
|
|
4511
|
+
const varMatch = f.match.match(/^([A-Z_][A-Z0-9_]*)\s*[:=]/i);
|
|
4512
|
+
if (varMatch) {
|
|
4513
|
+
envVarNames.add(varMatch[1].toUpperCase());
|
|
4514
|
+
} else {
|
|
4515
|
+
envVarNames.add(f.type.toUpperCase().replace(/-/g, "_"));
|
|
4516
|
+
}
|
|
4517
|
+
}
|
|
4518
|
+
if (envVarNames.size > 0) {
|
|
4519
|
+
let envExample = "# Environment variables \u2014 NEVER commit real values\n";
|
|
4520
|
+
envExample += "# Generated by cto-ai-cli --audit\n\n";
|
|
4521
|
+
for (const name of envVarNames) envExample += `${name}=your_${name.toLowerCase()}_here
|
|
4522
|
+
`;
|
|
4523
|
+
writeFileSync5(join7(ctoDir, ".env.example"), envExample);
|
|
4524
|
+
}
|
|
4525
|
+
}
|
|
4526
|
+
log3.info("Audit complete", { findings: summary.totalFindings, critical: summary.bySeverity.critical });
|
|
4527
|
+
console.log("");
|
|
4528
|
+
console.log(" \u{1F4C1} Audit artifacts:");
|
|
4529
|
+
console.log(` \u{1F4CB} .cto/audit/${dateStr}.jsonl Audit log (append-only)`);
|
|
4530
|
+
console.log(" \u{1F4CA} .cto/audit/report.md Full report");
|
|
4531
|
+
if (envSecrets.length > 0) console.log(" \u{1F4DD} .cto/.env.example Template for environment variables");
|
|
4532
|
+
console.log("");
|
|
4533
|
+
if (process.env.CI && (summary.bySeverity.critical > 0 || summary.bySeverity.high > 0)) {
|
|
4534
|
+
console.log(" \u274C CI mode: Failing due to critical/high severity findings.");
|
|
4535
|
+
process.exit(1);
|
|
4536
|
+
}
|
|
4537
|
+
}
|
|
4538
|
+
|
|
4539
|
+
// src/engine/monorepo.ts
|
|
4540
|
+
import { readFile as readFile5, readdir as readdir2 } from "fs/promises";
|
|
4541
|
+
import { join as join8, relative as relative4, basename as basename3 } from "path";
|
|
4542
|
+
import { existsSync as existsSync3 } from "fs";
|
|
4543
|
+
async function detectMonorepoTool(rootPath) {
|
|
4544
|
+
const checks = [
|
|
4545
|
+
{ file: "nx.json", tool: "nx" },
|
|
4546
|
+
{ file: "turbo.json", tool: "turborepo" },
|
|
4547
|
+
{ file: "lerna.json", tool: "lerna" },
|
|
4548
|
+
{ file: "pnpm-workspace.yaml", tool: "pnpm-workspaces" },
|
|
4549
|
+
{
|
|
4550
|
+
file: "package.json",
|
|
4551
|
+
tool: "npm-workspaces",
|
|
4552
|
+
validate: (content) => {
|
|
4553
|
+
try {
|
|
4554
|
+
const pkg = JSON.parse(content);
|
|
4555
|
+
return Array.isArray(pkg.workspaces) || typeof pkg.workspaces?.packages !== "undefined";
|
|
4556
|
+
} catch {
|
|
4557
|
+
return false;
|
|
4558
|
+
}
|
|
4559
|
+
}
|
|
4560
|
+
}
|
|
4561
|
+
];
|
|
4562
|
+
for (const check of checks) {
|
|
4563
|
+
const filePath = join8(rootPath, check.file);
|
|
4564
|
+
if (existsSync3(filePath)) {
|
|
4565
|
+
if (!check.validate) return check.tool;
|
|
4566
|
+
try {
|
|
4567
|
+
const content = await readFile5(filePath, "utf-8");
|
|
4568
|
+
if (check.validate(content)) {
|
|
4569
|
+
if (check.tool === "npm-workspaces") {
|
|
4570
|
+
if (existsSync3(join8(rootPath, "yarn.lock"))) return "yarn-workspaces";
|
|
4571
|
+
return "npm-workspaces";
|
|
4572
|
+
}
|
|
4573
|
+
return check.tool;
|
|
4574
|
+
}
|
|
4575
|
+
} catch {
|
|
4576
|
+
}
|
|
4577
|
+
}
|
|
4578
|
+
}
|
|
4579
|
+
return "none";
|
|
4580
|
+
}
|
|
4581
|
+
async function resolveWorkspaceGlobs(rootPath, globs) {
|
|
4582
|
+
const packagePaths = [];
|
|
4583
|
+
for (const glob of globs) {
|
|
4584
|
+
const cleanGlob = glob.replace(/\/?\*\*?$/, "");
|
|
4585
|
+
const searchDir = join8(rootPath, cleanGlob);
|
|
4586
|
+
if (!existsSync3(searchDir)) continue;
|
|
4587
|
+
try {
|
|
4588
|
+
const entries = await readdir2(searchDir, { withFileTypes: true });
|
|
4589
|
+
for (const entry of entries) {
|
|
4590
|
+
if (!entry.isDirectory()) continue;
|
|
4591
|
+
const pkgJsonPath = join8(searchDir, entry.name, "package.json");
|
|
4592
|
+
if (existsSync3(pkgJsonPath)) {
|
|
4593
|
+
packagePaths.push(join8(searchDir, entry.name));
|
|
4594
|
+
}
|
|
4595
|
+
}
|
|
4596
|
+
} catch {
|
|
4597
|
+
}
|
|
4598
|
+
}
|
|
4599
|
+
return packagePaths;
|
|
4600
|
+
}
|
|
4601
|
+
async function discoverPackages(rootPath, tool) {
|
|
4602
|
+
switch (tool) {
|
|
4603
|
+
case "npm-workspaces":
|
|
4604
|
+
case "yarn-workspaces": {
|
|
4605
|
+
const pkgJson = JSON.parse(await readFile5(join8(rootPath, "package.json"), "utf-8"));
|
|
4606
|
+
const workspaces = Array.isArray(pkgJson.workspaces) ? pkgJson.workspaces : pkgJson.workspaces?.packages || [];
|
|
4607
|
+
return resolveWorkspaceGlobs(rootPath, workspaces);
|
|
4608
|
+
}
|
|
4609
|
+
case "pnpm-workspaces": {
|
|
4610
|
+
const content = await readFile5(join8(rootPath, "pnpm-workspace.yaml"), "utf-8");
|
|
4611
|
+
const packages = [];
|
|
4612
|
+
let inPackages = false;
|
|
4613
|
+
for (const line of content.split("\n")) {
|
|
4614
|
+
const trimmed = line.trim();
|
|
4615
|
+
if (trimmed === "packages:") {
|
|
4616
|
+
inPackages = true;
|
|
4617
|
+
continue;
|
|
4618
|
+
}
|
|
4619
|
+
if (inPackages && trimmed.startsWith("- ")) {
|
|
4620
|
+
packages.push(trimmed.slice(2).replace(/['"]/g, ""));
|
|
4621
|
+
} else if (inPackages && !trimmed.startsWith("-") && trimmed.length > 0) {
|
|
4622
|
+
inPackages = false;
|
|
4623
|
+
}
|
|
4624
|
+
}
|
|
4625
|
+
return resolveWorkspaceGlobs(rootPath, packages);
|
|
4626
|
+
}
|
|
4627
|
+
case "turborepo": {
|
|
4628
|
+
const pkgJson = JSON.parse(await readFile5(join8(rootPath, "package.json"), "utf-8"));
|
|
4629
|
+
const workspaces = Array.isArray(pkgJson.workspaces) ? pkgJson.workspaces : pkgJson.workspaces?.packages || [];
|
|
4630
|
+
if (workspaces.length > 0) return resolveWorkspaceGlobs(rootPath, workspaces);
|
|
4631
|
+
if (existsSync3(join8(rootPath, "pnpm-workspace.yaml"))) {
|
|
4632
|
+
return discoverPackages(rootPath, "pnpm-workspaces");
|
|
4633
|
+
}
|
|
4634
|
+
return [];
|
|
4635
|
+
}
|
|
4636
|
+
case "nx": {
|
|
4637
|
+
const standardDirs = ["packages", "apps", "libs"];
|
|
4638
|
+
const globs = standardDirs.filter((d) => existsSync3(join8(rootPath, d)));
|
|
4639
|
+
if (globs.length > 0) return resolveWorkspaceGlobs(rootPath, globs);
|
|
4640
|
+
try {
|
|
4641
|
+
const pkgJson = JSON.parse(await readFile5(join8(rootPath, "package.json"), "utf-8"));
|
|
4642
|
+
const workspaces = Array.isArray(pkgJson.workspaces) ? pkgJson.workspaces : [];
|
|
4643
|
+
if (workspaces.length > 0) return resolveWorkspaceGlobs(rootPath, workspaces);
|
|
4644
|
+
} catch {
|
|
4645
|
+
}
|
|
4646
|
+
return [];
|
|
4647
|
+
}
|
|
4648
|
+
case "lerna": {
|
|
4649
|
+
const lernaJson = JSON.parse(await readFile5(join8(rootPath, "lerna.json"), "utf-8"));
|
|
4650
|
+
const packages = lernaJson.packages || ["packages/*"];
|
|
4651
|
+
return resolveWorkspaceGlobs(rootPath, packages);
|
|
4652
|
+
}
|
|
4653
|
+
default:
|
|
4654
|
+
return [];
|
|
4655
|
+
}
|
|
4656
|
+
}
|
|
4657
|
+
function buildCrossPackageEdges(packages, allFiles, graphEdges, rootPath) {
|
|
4658
|
+
const fileToPackage = /* @__PURE__ */ new Map();
|
|
4659
|
+
for (const pkg of packages) {
|
|
4660
|
+
const pkgRel = relative4(rootPath, pkg.path);
|
|
4661
|
+
for (const f of allFiles) {
|
|
4662
|
+
if (f.relativePath.startsWith(pkgRel + "/") || f.relativePath.startsWith(pkgRel + "\\")) {
|
|
4663
|
+
fileToPackage.set(f.relativePath, pkg.name);
|
|
4664
|
+
}
|
|
4665
|
+
}
|
|
4666
|
+
}
|
|
4667
|
+
const edgeMap = /* @__PURE__ */ new Map();
|
|
4668
|
+
for (const edge of graphEdges) {
|
|
4669
|
+
const fromPkg = fileToPackage.get(edge.from);
|
|
4670
|
+
const toPkg = fileToPackage.get(edge.to);
|
|
4671
|
+
if (fromPkg && toPkg && fromPkg !== toPkg) {
|
|
4672
|
+
const key = `${fromPkg}\u2192${toPkg}`;
|
|
4673
|
+
if (!edgeMap.has(key)) {
|
|
4674
|
+
edgeMap.set(key, { files: /* @__PURE__ */ new Set(), type: "dependency" });
|
|
4675
|
+
}
|
|
4676
|
+
edgeMap.get(key).files.add(edge.from);
|
|
4677
|
+
}
|
|
4678
|
+
}
|
|
4679
|
+
return Array.from(edgeMap.entries()).map(([key, val]) => {
|
|
4680
|
+
const [from, to] = key.split("\u2192");
|
|
4681
|
+
return { from, to, files: val.files.size, type: val.type };
|
|
4682
|
+
});
|
|
4683
|
+
}
|
|
4684
|
+
async function analyzeMonorepo(rootPath, analysis) {
|
|
4685
|
+
const tool = await detectMonorepoTool(rootPath);
|
|
4686
|
+
if (tool === "none") {
|
|
4687
|
+
return {
|
|
4688
|
+
detected: false,
|
|
4689
|
+
tool: "none",
|
|
4690
|
+
rootPath,
|
|
4691
|
+
packages: [],
|
|
3929
4692
|
sharedPackages: [],
|
|
3930
4693
|
crossPackageEdges: [],
|
|
3931
4694
|
isolationScore: 100,
|
|
@@ -3937,7 +4700,7 @@ async function analyzeMonorepo(rootPath, analysis) {
|
|
|
3937
4700
|
const packages = [];
|
|
3938
4701
|
const packageTokenMap = {};
|
|
3939
4702
|
for (const pkgPath of packagePaths) {
|
|
3940
|
-
const pkgJsonPath =
|
|
4703
|
+
const pkgJsonPath = join8(pkgPath, "package.json");
|
|
3941
4704
|
let name = basename3(pkgPath);
|
|
3942
4705
|
let pkgDeps = [];
|
|
3943
4706
|
try {
|
|
@@ -3980,7 +4743,7 @@ async function analyzeMonorepo(rootPath, analysis) {
|
|
|
3980
4743
|
}
|
|
3981
4744
|
const pkgNames = new Set(packages.map((p) => p.name));
|
|
3982
4745
|
for (const pkg of packages) {
|
|
3983
|
-
const pkgJsonPath =
|
|
4746
|
+
const pkgJsonPath = join8(pkg.path, "package.json");
|
|
3984
4747
|
try {
|
|
3985
4748
|
const pkgJson = JSON.parse(await readFile5(pkgJsonPath, "utf-8"));
|
|
3986
4749
|
const allDeps = {
|
|
@@ -4127,14 +4890,36 @@ function renderPackageContext(result) {
|
|
|
4127
4890
|
lines.push(` ... and ${result.excludedPackages.length - 10} more`);
|
|
4128
4891
|
}
|
|
4129
4892
|
}
|
|
4130
|
-
lines.push("");
|
|
4131
|
-
return lines.join("\n");
|
|
4893
|
+
lines.push("");
|
|
4894
|
+
return lines.join("\n");
|
|
4895
|
+
}
|
|
4896
|
+
|
|
4897
|
+
// src/cli/commands/monorepo.ts
|
|
4898
|
+
async function runMonorepo(projectPath, analysis, targetPackage, jsonMode) {
|
|
4899
|
+
const mono = await analyzeMonorepo(projectPath, analysis);
|
|
4900
|
+
if (jsonMode) {
|
|
4901
|
+
if (targetPackage) {
|
|
4902
|
+
const pkgCtx = selectPackageContext(mono, targetPackage);
|
|
4903
|
+
console.log(JSON.stringify({ monorepo: mono, packageContext: pkgCtx }, null, 2));
|
|
4904
|
+
} else {
|
|
4905
|
+
console.log(JSON.stringify(mono, null, 2));
|
|
4906
|
+
}
|
|
4907
|
+
return;
|
|
4908
|
+
}
|
|
4909
|
+
console.log(renderMonorepoAnalysis(mono));
|
|
4910
|
+
if (targetPackage && mono.detected) {
|
|
4911
|
+
const pkgCtx = selectPackageContext(mono, targetPackage);
|
|
4912
|
+
console.log(renderPackageContext(pkgCtx));
|
|
4913
|
+
}
|
|
4132
4914
|
}
|
|
4133
4915
|
|
|
4916
|
+
// src/cli/commands/ci.ts
|
|
4917
|
+
init_secrets();
|
|
4918
|
+
|
|
4134
4919
|
// src/engine/quality-gate.ts
|
|
4135
4920
|
import { readFile as readFile6, writeFile as writeFile2, mkdir } from "fs/promises";
|
|
4136
4921
|
import { resolve as resolve5 } from "path";
|
|
4137
|
-
import { existsSync as
|
|
4922
|
+
import { existsSync as existsSync4 } from "fs";
|
|
4138
4923
|
var DEFAULT_GATE_CONFIG = {
|
|
4139
4924
|
threshold: 70,
|
|
4140
4925
|
failOnSecrets: true,
|
|
@@ -4145,7 +4930,7 @@ var DEFAULT_GATE_CONFIG = {
|
|
|
4145
4930
|
};
|
|
4146
4931
|
async function loadBaseline(projectPath, baselinePath) {
|
|
4147
4932
|
const filePath = resolve5(projectPath, baselinePath || ".cto/baseline.json");
|
|
4148
|
-
if (!
|
|
4933
|
+
if (!existsSync4(filePath)) return null;
|
|
4149
4934
|
try {
|
|
4150
4935
|
const content = await readFile6(filePath, "utf-8");
|
|
4151
4936
|
return JSON.parse(content);
|
|
@@ -4155,7 +4940,7 @@ async function loadBaseline(projectPath, baselinePath) {
|
|
|
4155
4940
|
}
|
|
4156
4941
|
async function saveBaseline(projectPath, score, commit, branch, baselinePath) {
|
|
4157
4942
|
const dir = resolve5(projectPath, ".cto");
|
|
4158
|
-
if (!
|
|
4943
|
+
if (!existsSync4(dir)) await mkdir(dir, { recursive: true });
|
|
4159
4944
|
const baseline = {
|
|
4160
4945
|
score: score.overall,
|
|
4161
4946
|
grade: score.grade,
|
|
@@ -4239,912 +5024,1328 @@ function generatePRComment(score, analysis, checks, baseline, delta) {
|
|
|
4239
5024
|
"",
|
|
4240
5025
|
`### ${gradeEmoji} Context Score: ${score.overall}/100 (${score.grade})${deltaStr}`,
|
|
4241
5026
|
"",
|
|
4242
|
-
`> **${analysis.projectName}** \xB7 ${analysis.totalFiles} files \xB7 ${Math.round(analysis.totalTokens / 1e3)}K tokens`,
|
|
4243
|
-
"",
|
|
4244
|
-
"### Checks",
|
|
4245
|
-
"",
|
|
4246
|
-
"| Check | Status | Detail |",
|
|
4247
|
-
"|-------|--------|--------|"
|
|
4248
|
-
];
|
|
4249
|
-
for (const check of checks) {
|
|
4250
|
-
const icon = check.passed ? "\u2705" : check.severity === "warning" ? "\u26A0\uFE0F" : "\u274C";
|
|
4251
|
-
lines.push(`| ${check.name} | ${icon} | ${check.detail} |`);
|
|
4252
|
-
}
|
|
4253
|
-
lines.push("");
|
|
4254
|
-
lines.push("### Dimensions");
|
|
4255
|
-
lines.push("");
|
|
4256
|
-
lines.push("| Dimension | Score | vs Baseline |");
|
|
4257
|
-
lines.push("|-----------|-------|-------------|");
|
|
4258
|
-
for (const [name, dim] of Object.entries(score.dimensions)) {
|
|
4259
|
-
const prev = baseline?.dimensions[name];
|
|
4260
|
-
const diff = prev !== void 0 ? dim.score - prev : null;
|
|
4261
|
-
const diffStr = diff !== null ? `${diff >= 0 ? "+" : ""}${diff}` : "\u2014";
|
|
4262
|
-
const bar = renderBar2(dim.score);
|
|
4263
|
-
lines.push(`| ${name} | ${bar} ${dim.score}% | ${diffStr} |`);
|
|
4264
|
-
}
|
|
4265
|
-
lines.push("");
|
|
4266
|
-
lines.push("### Savings");
|
|
4267
|
-
lines.push("");
|
|
4268
|
-
lines.push(`| Metric | Value |`);
|
|
4269
|
-
lines.push(`|--------|-------|`);
|
|
4270
|
-
lines.push(`| Tokens saved | ${score.comparison.savedTokens.toLocaleString()} (${score.comparison.savedPercent}%) |`);
|
|
4271
|
-
lines.push(`| Monthly savings | $${score.comparison.monthlySavingsUSD.toFixed(2)} |`);
|
|
4272
|
-
if (score.insights.length > 0) {
|
|
4273
|
-
lines.push("");
|
|
4274
|
-
lines.push("### Insights");
|
|
4275
|
-
lines.push("");
|
|
4276
|
-
for (const insight of score.insights.slice(0, 5)) {
|
|
4277
|
-
const icon = insight.type === "strength" ? "\u2705" : insight.type === "weakness" ? "\u26A0\uFE0F" : "\u{1F4A1}";
|
|
4278
|
-
lines.push(`- ${icon} **${insight.title}** \u2014 ${insight.detail}`);
|
|
4279
|
-
}
|
|
4280
|
-
}
|
|
4281
|
-
lines.push("");
|
|
4282
|
-
lines.push("---");
|
|
4283
|
-
lines.push(`<sub>Generated by [CTO Quality Gate](https://npmjs.com/package/cto-ai-cli) \xB7 ${(/* @__PURE__ */ new Date()).toISOString().split("T")[0]}</sub>`);
|
|
4284
|
-
return lines.join("\n");
|
|
4285
|
-
}
|
|
4286
|
-
function renderBar2(score) {
|
|
4287
|
-
const filled = Math.round(score / 10);
|
|
4288
|
-
return "\u2588".repeat(filled) + "\u2591".repeat(10 - filled);
|
|
4289
|
-
}
|
|
4290
|
-
function generateSummary(score, checks, passed) {
|
|
4291
|
-
const status = passed ? "\u2705 PASSED" : "\u274C FAILED";
|
|
4292
|
-
const failedChecks = checks.filter((c) => !c.passed && c.severity === "error");
|
|
4293
|
-
const warnings = checks.filter((c) => !c.passed && c.severity === "warning");
|
|
4294
|
-
let summary = `Quality Gate ${status} \u2014 Score: ${score.overall}/100 (${score.grade})`;
|
|
4295
|
-
if (failedChecks.length > 0) {
|
|
4296
|
-
summary += `
|
|
4297
|
-
Failed: ${failedChecks.map((c) => c.name).join(", ")}`;
|
|
4298
|
-
}
|
|
4299
|
-
if (warnings.length > 0) {
|
|
4300
|
-
summary += `
|
|
4301
|
-
Warnings: ${warnings.map((c) => c.name).join(", ")}`;
|
|
4302
|
-
}
|
|
4303
|
-
return summary;
|
|
4304
|
-
}
|
|
4305
|
-
|
|
4306
|
-
// src/cli/score.ts
|
|
4307
|
-
async function main() {
|
|
4308
|
-
const args = process.argv.slice(2);
|
|
4309
|
-
const jsonMode = args.includes("--json");
|
|
4310
|
-
const benchmarkMode = args.includes("--benchmark");
|
|
4311
|
-
const fixMode = args.includes("--fix");
|
|
4312
|
-
const reportMode = args.includes("--report");
|
|
4313
|
-
const compareMode = args.includes("--compare");
|
|
4314
|
-
const auditMode = args.includes("--audit");
|
|
4315
|
-
const initHookMode = args.includes("--init-hook");
|
|
4316
|
-
const fullScanMode = args.includes("--full-scan");
|
|
4317
|
-
const noAllowlistMode = args.includes("--no-allowlist");
|
|
4318
|
-
const monorepoMode = args.includes("--monorepo");
|
|
4319
|
-
const gatewayMode = args.includes("--gateway");
|
|
4320
|
-
const ciMode = args.includes("--ci");
|
|
4321
|
-
const helpMode = args.includes("--help") || args.includes("-h");
|
|
4322
|
-
const pkgIdx = args.indexOf("--package");
|
|
4323
|
-
const targetPackage = pkgIdx !== -1 && args[pkgIdx + 1] ? args[pkgIdx + 1] : null;
|
|
4324
|
-
const threshIdx = args.indexOf("--threshold");
|
|
4325
|
-
const thresholdArg = threshIdx !== -1 && args[threshIdx + 1] ? parseInt(args[threshIdx + 1], 10) : 70;
|
|
4326
|
-
const contextIdx = args.indexOf("--context");
|
|
4327
|
-
const contextTask = contextIdx !== -1 && args[contextIdx + 1] ? args[contextIdx + 1] : null;
|
|
4328
|
-
const pathArg = args.find((a) => !a.startsWith("--") && !a.startsWith("-") && a !== contextTask && a !== targetPackage);
|
|
4329
|
-
const projectPath = resolve7(pathArg ?? ".");
|
|
4330
|
-
if (helpMode) {
|
|
4331
|
-
console.log(`
|
|
4332
|
-
\u26A1 cto-score \u2014 How AI-ready is your codebase?
|
|
4333
|
-
|
|
4334
|
-
Usage:
|
|
4335
|
-
npx cto-ai-cli Scan current directory
|
|
4336
|
-
npx cto-ai-cli ./path Scan a specific project
|
|
4337
|
-
|
|
4338
|
-
Phase 1 \u2014 Marketing:
|
|
4339
|
-
npx cto-ai-cli --benchmark CTO vs naive vs random comparison
|
|
4340
|
-
npx cto-ai-cli --fix Auto-generate optimized context files
|
|
4341
|
-
npx cto-ai-cli --context "your task" Generate task-specific context
|
|
4342
|
-
npx cto-ai-cli --report Generate shareable markdown report
|
|
4343
|
-
npx cto-ai-cli --compare Compare your score vs popular projects
|
|
4344
|
-
|
|
4345
|
-
Phase 2 \u2014 Security:
|
|
4346
|
-
npx cto-ai-cli --audit Security audit: detect secrets & PII
|
|
4347
|
-
npx cto-ai-cli --audit --init-hook Generate pre-commit hook
|
|
4348
|
-
npx cto-ai-cli --audit --full-scan Skip incremental cache
|
|
4349
|
-
npx cto-ai-cli --audit --no-allowlist Ignore allowlist
|
|
4350
|
-
|
|
4351
|
-
Phase 3 \u2014 Gateway:
|
|
4352
|
-
npx cto-ai-cli --gateway Start Context Gateway (proxy)
|
|
4353
|
-
npx cto-ai-cli --gateway --port 9000 Custom port
|
|
4354
|
-
npx cto-ai-cli --gateway --block-secrets Block requests with secrets
|
|
4355
|
-
npx cto-ai-cli --gateway --budget-daily 10 Daily budget ($10/day)
|
|
4356
|
-
|
|
4357
|
-
Phase 4 \u2014 Monorepo:
|
|
4358
|
-
npx cto-ai-cli --monorepo Analyze monorepo structure
|
|
4359
|
-
npx cto-ai-cli --monorepo --package <name> Context savings for a package
|
|
4360
|
-
|
|
4361
|
-
Phase 5 \u2014 CI/CD Quality Gate:
|
|
4362
|
-
npx cto-ai-cli --ci Run quality gate (exits 1 on failure)
|
|
4363
|
-
npx cto-ai-cli --ci --threshold 80 Set minimum score (default: 70)
|
|
4364
|
-
npx cto-ai-cli --ci --json JSON output for CI pipelines
|
|
4365
|
-
|
|
4366
|
-
Options:
|
|
4367
|
-
npx cto-ai-cli --json Output as JSON (for CI/scripts)
|
|
4368
|
-
|
|
4369
|
-
What it does:
|
|
4370
|
-
Analyzes your project's structure, dependencies, and risk profile.
|
|
4371
|
-
Gives you a single 0-100 score showing how efficiently AI tools
|
|
4372
|
-
can work with your codebase.
|
|
4373
|
-
|
|
4374
|
-
No data leaves your machine. No API keys needed. MIT licensed.
|
|
4375
|
-
Learn more: https://npmjs.com/package/cto-ai-cli
|
|
4376
|
-
`);
|
|
4377
|
-
process.exit(0);
|
|
4378
|
-
}
|
|
4379
|
-
if (gatewayMode) {
|
|
4380
|
-
const { ContextGateway: ContextGateway2 } = await Promise.resolve().then(() => (init_server(), server_exports));
|
|
4381
|
-
const { DEFAULT_GATEWAY_CONFIG: DEFAULT_GATEWAY_CONFIG2 } = await Promise.resolve().then(() => (init_types(), types_exports));
|
|
4382
|
-
const getArg = (flag) => {
|
|
4383
|
-
const idx = args.indexOf(flag);
|
|
4384
|
-
return idx !== -1 && args[idx + 1] ? args[idx + 1] : void 0;
|
|
4385
|
-
};
|
|
4386
|
-
const gwConfig = { projectPath };
|
|
4387
|
-
const port = getArg("--port");
|
|
4388
|
-
if (port) gwConfig.port = parseInt(port, 10);
|
|
4389
|
-
const budgetDaily = getArg("--budget-daily");
|
|
4390
|
-
if (budgetDaily) gwConfig.budgetDaily = parseFloat(budgetDaily);
|
|
4391
|
-
const budgetMonthly = getArg("--budget-monthly");
|
|
4392
|
-
if (budgetMonthly) gwConfig.budgetMonthly = parseFloat(budgetMonthly);
|
|
4393
|
-
const apiKey = getArg("--api-key");
|
|
4394
|
-
if (apiKey) gwConfig.apiKey = apiKey;
|
|
4395
|
-
if (args.includes("--block-secrets")) gwConfig.blockOnSecrets = true;
|
|
4396
|
-
if (args.includes("--no-optimize")) gwConfig.optimize = false;
|
|
4397
|
-
if (args.includes("--no-redact")) gwConfig.redactSecrets = false;
|
|
4398
|
-
if (args.includes("--no-dashboard")) gwConfig.dashboard = false;
|
|
4399
|
-
const finalConfig = { ...DEFAULT_GATEWAY_CONFIG2, ...gwConfig };
|
|
4400
|
-
const gateway = new ContextGateway2(finalConfig);
|
|
4401
|
-
gateway.onEvent((event) => {
|
|
4402
|
-
const ts = (/* @__PURE__ */ new Date()).toLocaleTimeString();
|
|
4403
|
-
switch (event.type) {
|
|
4404
|
-
case "request": {
|
|
4405
|
-
const r = event.record;
|
|
4406
|
-
const saved = r.savedTokens > 0 ? ` (saved ${(r.savedTokens / 1e3).toFixed(1)}K)` : "";
|
|
4407
|
-
const secrets = r.secretsRedacted > 0 ? ` [${r.secretsRedacted} redacted]` : "";
|
|
4408
|
-
console.log(` ${ts} ${r.provider}/${r.model} $${r.costUSD.toFixed(4)}${saved}${secrets} ${r.latencyMs}ms`);
|
|
4409
|
-
break;
|
|
4410
|
-
}
|
|
4411
|
-
case "budget-alert":
|
|
4412
|
-
console.log(` \u26A0\uFE0F ${ts} Budget alert: $${event.current.toFixed(2)}/$${event.limit.toFixed(2)} (${event.period})`);
|
|
4413
|
-
break;
|
|
4414
|
-
case "budget-exceeded":
|
|
4415
|
-
console.log(` \u{1F534} ${ts} Budget EXCEEDED: $${event.current.toFixed(2)}/$${event.limit.toFixed(2)} (${event.period})`);
|
|
4416
|
-
break;
|
|
4417
|
-
case "error":
|
|
4418
|
-
console.log(` \u274C ${ts} ${event.message}`);
|
|
4419
|
-
break;
|
|
4420
|
-
}
|
|
4421
|
-
});
|
|
4422
|
-
console.log("");
|
|
4423
|
-
console.log(" \u26A1 CTO Context Gateway v4.1.0");
|
|
4424
|
-
console.log("");
|
|
4425
|
-
await gateway.start();
|
|
4426
|
-
console.log(` \u{1F310} Proxy: http://${finalConfig.host}:${finalConfig.port}`);
|
|
4427
|
-
if (finalConfig.dashboard) {
|
|
4428
|
-
console.log(` \u{1F4CA} Dashboard: http://${finalConfig.host}:${finalConfig.port}${finalConfig.dashboardPath}`);
|
|
4429
|
-
}
|
|
4430
|
-
console.log(` \u{1F4C1} Project: ${finalConfig.projectPath}`);
|
|
4431
|
-
console.log("");
|
|
4432
|
-
console.log(" Waiting for requests... (Ctrl+C to stop)");
|
|
4433
|
-
console.log("");
|
|
4434
|
-
await new Promise(() => {
|
|
4435
|
-
});
|
|
4436
|
-
return;
|
|
4437
|
-
}
|
|
4438
|
-
console.log("");
|
|
4439
|
-
console.log(" \u26A1 cto-score \u2014 analyzing your project...");
|
|
4440
|
-
console.log("");
|
|
4441
|
-
try {
|
|
4442
|
-
const startTime = Date.now();
|
|
4443
|
-
const analysis = await analyzeProject(projectPath);
|
|
4444
|
-
const score = await computeContextScore(analysis);
|
|
4445
|
-
const elapsed = ((Date.now() - startTime) / 1e3).toFixed(1);
|
|
4446
|
-
if (jsonMode) {
|
|
4447
|
-
console.log(JSON.stringify({
|
|
4448
|
-
project: analysis.projectName,
|
|
4449
|
-
files: analysis.totalFiles,
|
|
4450
|
-
tokens: analysis.totalTokens,
|
|
4451
|
-
score: score.overall,
|
|
4452
|
-
grade: score.grade,
|
|
4453
|
-
dimensions: {
|
|
4454
|
-
efficiency: score.dimensions.efficiency.score,
|
|
4455
|
-
coverage: score.dimensions.coverage.score,
|
|
4456
|
-
riskControl: score.dimensions.riskControl.score,
|
|
4457
|
-
structure: score.dimensions.structure.score,
|
|
4458
|
-
governance: score.dimensions.governance.score
|
|
4459
|
-
},
|
|
4460
|
-
savings: {
|
|
4461
|
-
percent: score.comparison.savedPercent,
|
|
4462
|
-
monthlyUSD: score.comparison.monthlySavingsUSD,
|
|
4463
|
-
tokensOptimized: score.comparison.optimizedTokens,
|
|
4464
|
-
tokensNaive: score.comparison.naiveTokens
|
|
4465
|
-
}
|
|
4466
|
-
}, null, 2));
|
|
4467
|
-
process.exit(0);
|
|
4468
|
-
}
|
|
4469
|
-
console.log(renderContextScore(score));
|
|
4470
|
-
if (benchmarkMode) {
|
|
4471
|
-
const benchmark = await runBenchmark(analysis);
|
|
4472
|
-
console.log(renderBenchmark(benchmark));
|
|
4473
|
-
}
|
|
4474
|
-
if (fixMode) {
|
|
4475
|
-
await runFix(projectPath, analysis, score);
|
|
4476
|
-
}
|
|
4477
|
-
if (contextTask) {
|
|
4478
|
-
await runContext(projectPath, analysis, contextTask);
|
|
4479
|
-
}
|
|
4480
|
-
if (reportMode) {
|
|
4481
|
-
await runReport(projectPath, analysis, score);
|
|
4482
|
-
}
|
|
4483
|
-
if (compareMode) {
|
|
4484
|
-
runCompare(score);
|
|
4485
|
-
}
|
|
4486
|
-
if (auditMode) {
|
|
4487
|
-
await runAudit(projectPath, analysis, { initHookMode, fullScanMode, noAllowlistMode });
|
|
4488
|
-
}
|
|
4489
|
-
if (monorepoMode) {
|
|
4490
|
-
await runMonorepo(projectPath, analysis, targetPackage, jsonMode);
|
|
4491
|
-
}
|
|
4492
|
-
if (ciMode) {
|
|
4493
|
-
await runCIGate(projectPath, analysis, score, thresholdArg, jsonMode);
|
|
4494
|
-
}
|
|
4495
|
-
console.log("");
|
|
4496
|
-
console.log(` Scanned in ${elapsed}s \xB7 ${analysis.totalFiles} files \xB7 ${Math.round(analysis.totalTokens / 1e3)}K tokens`);
|
|
4497
|
-
console.log("");
|
|
4498
|
-
console.log(" What does this mean?");
|
|
4499
|
-
console.log(` Your project scores ${score.overall}/100 (${score.grade}) for AI context efficiency.`);
|
|
4500
|
-
if (score.overall >= 80) {
|
|
4501
|
-
console.log(" \u2705 Great! AI tools can work effectively with your codebase.");
|
|
4502
|
-
} else if (score.overall >= 60) {
|
|
4503
|
-
console.log(" \u{1F7E1} Good, but there's room to improve. Run with --fix to auto-optimize.");
|
|
4504
|
-
} else {
|
|
4505
|
-
console.log(" \u{1F534} AI tools are likely wasting tokens on your project. Run with --fix.");
|
|
4506
|
-
}
|
|
4507
|
-
console.log("");
|
|
4508
|
-
console.log(" Next steps:");
|
|
4509
|
-
console.log(" npx cto-ai-cli --fix Auto-generate optimized context");
|
|
4510
|
-
console.log(' npx cto-ai-cli --context "your task" Task-specific context for Claude/Cursor');
|
|
4511
|
-
console.log(" npx cto-ai-cli --report Shareable report + badge");
|
|
4512
|
-
console.log(" npx cto-ai-cli --compare Compare vs popular open source");
|
|
4513
|
-
console.log(" npm i -g cto-ai-cli Install for full CLI + MCP server");
|
|
4514
|
-
console.log("");
|
|
4515
|
-
} catch (err) {
|
|
4516
|
-
console.error(` \u274C Error: ${err.message}`);
|
|
4517
|
-
console.error("");
|
|
4518
|
-
console.error(" Make sure you're running this in a project directory with source files.");
|
|
4519
|
-
console.error(" Supported: TypeScript, JavaScript, Python, Go, Rust, Java, C/C++");
|
|
4520
|
-
process.exit(1);
|
|
4521
|
-
}
|
|
4522
|
-
}
|
|
4523
|
-
async function runFix(projectPath, analysis, score) {
|
|
4524
|
-
const ctoDir = join8(projectPath, ".cto");
|
|
4525
|
-
mkdirSync3(ctoDir, { recursive: true });
|
|
4526
|
-
const selection = await selectContext({
|
|
4527
|
-
task: "general code review and refactoring",
|
|
4528
|
-
analysis,
|
|
4529
|
-
budget: 5e4
|
|
4530
|
-
});
|
|
4531
|
-
const criticalFiles = selection.files.filter((f) => f.riskScore >= 60).sort((a, b) => b.riskScore - a.riskScore);
|
|
4532
|
-
const typeFiles = analysis.files.filter((f) => f.kind === "type").sort((a, b) => b.riskScore - a.riskScore);
|
|
4533
|
-
const entryFiles = analysis.files.filter((f) => f.kind === "entry").sort((a, b) => b.riskScore - a.riskScore);
|
|
4534
|
-
let contextMd = `# CTO Context \u2014 ${analysis.projectName}
|
|
4535
|
-
`;
|
|
4536
|
-
contextMd += `# Generated by cto-ai-cli \xB7 Score: ${score.overall}/100 (${score.grade})
|
|
4537
|
-
`;
|
|
4538
|
-
contextMd += `# Paste this into your AI prompt for better results.
|
|
4539
|
-
|
|
4540
|
-
`;
|
|
4541
|
-
contextMd += `## Critical files (always include these):
|
|
4542
|
-
`;
|
|
4543
|
-
for (const f of criticalFiles.slice(0, 15)) {
|
|
4544
|
-
contextMd += `- ${f.relativePath} \u2014 risk:${f.riskScore} (${f.reason})
|
|
4545
|
-
`;
|
|
4546
|
-
}
|
|
4547
|
-
if (typeFiles.length > 0) {
|
|
4548
|
-
contextMd += `
|
|
4549
|
-
## Type definitions (AI needs these to generate correct code):
|
|
4550
|
-
`;
|
|
4551
|
-
for (const f of typeFiles.slice(0, 10)) {
|
|
4552
|
-
contextMd += `- ${f.relativePath} (${f.tokens} tokens)
|
|
4553
|
-
`;
|
|
4554
|
-
}
|
|
4555
|
-
}
|
|
4556
|
-
if (entryFiles.length > 0) {
|
|
4557
|
-
contextMd += `
|
|
4558
|
-
## Entry points:
|
|
4559
|
-
`;
|
|
4560
|
-
for (const f of entryFiles.slice(0, 5)) {
|
|
4561
|
-
contextMd += `- ${f.relativePath}
|
|
4562
|
-
`;
|
|
4563
|
-
}
|
|
4564
|
-
}
|
|
4565
|
-
contextMd += `
|
|
4566
|
-
## Project structure:
|
|
4567
|
-
`;
|
|
4568
|
-
contextMd += `- ${analysis.totalFiles} files, ${Math.round(analysis.totalTokens / 1e3)}K tokens total
|
|
4569
|
-
`;
|
|
4570
|
-
contextMd += `- Stack: ${analysis.stack.join(", ") || "unknown"}
|
|
4571
|
-
`;
|
|
4572
|
-
contextMd += `- Clusters: ${analysis.graph.clusters.length}
|
|
4573
|
-
`;
|
|
4574
|
-
contextMd += `- Hubs: ${analysis.graph.hubs.map((h) => h.relativePath).slice(0, 5).join(", ")}
|
|
4575
|
-
`;
|
|
4576
|
-
contextMd += `
|
|
4577
|
-
## Recommended token budget: ${selection.totalTokens.toLocaleString()} tokens
|
|
4578
|
-
`;
|
|
4579
|
-
contextMd += `## Full project tokens: ${analysis.totalTokens.toLocaleString()} tokens
|
|
4580
|
-
`;
|
|
4581
|
-
contextMd += `## Savings: ${score.comparison.savedPercent}% (${formatTokens(score.comparison.savedTokens)})
|
|
4582
|
-
`;
|
|
4583
|
-
writeFileSync2(join8(ctoDir, "context.md"), contextMd);
|
|
4584
|
-
const config = {
|
|
4585
|
-
version: "3.0",
|
|
4586
|
-
project: analysis.projectName,
|
|
4587
|
-
generatedAt: (/* @__PURE__ */ new Date()).toISOString(),
|
|
4588
|
-
score: score.overall,
|
|
4589
|
-
grade: score.grade,
|
|
4590
|
-
budget: 5e4,
|
|
4591
|
-
criticalFiles: criticalFiles.slice(0, 20).map((f) => f.relativePath),
|
|
4592
|
-
typeFiles: typeFiles.map((f) => f.relativePath),
|
|
4593
|
-
ignorePatterns: [
|
|
4594
|
-
"node_modules",
|
|
4595
|
-
"dist",
|
|
4596
|
-
"build",
|
|
4597
|
-
".git",
|
|
4598
|
-
"*.test.*",
|
|
4599
|
-
"*.spec.*",
|
|
4600
|
-
"__tests__",
|
|
4601
|
-
"*.md",
|
|
4602
|
-
"*.json",
|
|
4603
|
-
"*.lock"
|
|
4604
|
-
],
|
|
4605
|
-
insights: score.insights.slice(0, 5).map((i) => ({
|
|
4606
|
-
type: i.type,
|
|
4607
|
-
title: i.title,
|
|
4608
|
-
impact: i.impact
|
|
4609
|
-
}))
|
|
4610
|
-
};
|
|
4611
|
-
writeFileSync2(join8(ctoDir, "config.json"), JSON.stringify(config, null, 2));
|
|
4612
|
-
const ignoreContent = [
|
|
4613
|
-
"# CTO AI-ignore \u2014 files that add noise to AI context",
|
|
4614
|
-
"# Generated by cto-ai-cli",
|
|
4615
|
-
"",
|
|
4616
|
-
"# Build artifacts",
|
|
4617
|
-
"dist/",
|
|
4618
|
-
"build/",
|
|
4619
|
-
".next/",
|
|
4620
|
-
"coverage/",
|
|
4621
|
-
"",
|
|
4622
|
-
"# Dependencies",
|
|
4623
|
-
"node_modules/",
|
|
5027
|
+
`> **${analysis.projectName}** \xB7 ${analysis.totalFiles} files \xB7 ${Math.round(analysis.totalTokens / 1e3)}K tokens`,
|
|
4624
5028
|
"",
|
|
4625
|
-
"
|
|
4626
|
-
"*.lock",
|
|
4627
|
-
"package-lock.json",
|
|
4628
|
-
"yarn.lock",
|
|
5029
|
+
"### Checks",
|
|
4629
5030
|
"",
|
|
4630
|
-
"
|
|
4631
|
-
|
|
4632
|
-
|
|
4633
|
-
|
|
4634
|
-
|
|
4635
|
-
|
|
4636
|
-
].join("\n");
|
|
4637
|
-
writeFileSync2(join8(ctoDir, ".cteignore"), ignoreContent);
|
|
4638
|
-
console.log("");
|
|
4639
|
-
console.log(" \u2705 Auto-fix complete! Generated:");
|
|
4640
|
-
console.log("");
|
|
4641
|
-
console.log(" \u{1F4CB} .cto/context.md Copy-paste this into Claude/Cursor/ChatGPT");
|
|
4642
|
-
console.log(" \u2699\uFE0F .cto/config.json Optimized CTO configuration");
|
|
4643
|
-
console.log(" \u{1F6AB} .cto/.cteignore Files AI should skip");
|
|
4644
|
-
console.log("");
|
|
4645
|
-
console.log(" \u{1F4A1} Tip: Paste .cto/context.md at the start of your AI conversation");
|
|
4646
|
-
console.log(" for dramatically better code generation.");
|
|
4647
|
-
console.log("");
|
|
4648
|
-
}
|
|
4649
|
-
async function runContext(projectPath, analysis, task) {
|
|
4650
|
-
const ctoDir = join8(projectPath, ".cto");
|
|
4651
|
-
mkdirSync3(ctoDir, { recursive: true });
|
|
4652
|
-
const selection = await selectContext({
|
|
4653
|
-
task,
|
|
4654
|
-
analysis,
|
|
4655
|
-
budget: 5e4
|
|
4656
|
-
});
|
|
4657
|
-
const typeFiles = analysis.files.filter((f) => f.kind === "type");
|
|
4658
|
-
const selectedPaths = new Set(selection.files.map((f) => f.relativePath));
|
|
4659
|
-
const includedTypes = typeFiles.filter((f) => selectedPaths.has(f.relativePath));
|
|
4660
|
-
let contextMd = `# Context for: ${task}
|
|
4661
|
-
`;
|
|
4662
|
-
contextMd += `# Generated by cto-ai-cli
|
|
4663
|
-
`;
|
|
4664
|
-
contextMd += `# ${selection.files.length} files selected, ${selection.totalTokens.toLocaleString()} tokens
|
|
4665
|
-
|
|
4666
|
-
`;
|
|
4667
|
-
const targets = selection.files.filter((f) => f.reason === "Target file");
|
|
4668
|
-
const critical = selection.files.filter((f) => f.riskScore >= 80 && f.reason !== "Target file");
|
|
4669
|
-
const high = selection.files.filter((f) => f.riskScore >= 60 && f.riskScore < 80 && f.reason !== "Target file");
|
|
4670
|
-
const rest = selection.files.filter((f) => f.riskScore < 60 && f.reason !== "Target file");
|
|
4671
|
-
if (targets.length > 0) {
|
|
4672
|
-
contextMd += `## Target files (directly related to task):
|
|
4673
|
-
`;
|
|
4674
|
-
for (const f of targets) {
|
|
4675
|
-
contextMd += `- ${f.relativePath}
|
|
4676
|
-
`;
|
|
4677
|
-
}
|
|
4678
|
-
contextMd += "\n";
|
|
5031
|
+
"| Check | Status | Detail |",
|
|
5032
|
+
"|-------|--------|--------|"
|
|
5033
|
+
];
|
|
5034
|
+
for (const check of checks) {
|
|
5035
|
+
const icon = check.passed ? "\u2705" : check.severity === "warning" ? "\u26A0\uFE0F" : "\u274C";
|
|
5036
|
+
lines.push(`| ${check.name} | ${icon} | ${check.detail} |`);
|
|
4679
5037
|
}
|
|
4680
|
-
|
|
4681
|
-
|
|
4682
|
-
|
|
4683
|
-
|
|
4684
|
-
|
|
4685
|
-
|
|
4686
|
-
|
|
4687
|
-
|
|
5038
|
+
lines.push("");
|
|
5039
|
+
lines.push("### Dimensions");
|
|
5040
|
+
lines.push("");
|
|
5041
|
+
lines.push("| Dimension | Score | vs Baseline |");
|
|
5042
|
+
lines.push("|-----------|-------|-------------|");
|
|
5043
|
+
for (const [name, dim] of Object.entries(score.dimensions)) {
|
|
5044
|
+
const prev = baseline?.dimensions[name];
|
|
5045
|
+
const diff = prev !== void 0 ? dim.score - prev : null;
|
|
5046
|
+
const diffStr = diff !== null ? `${diff >= 0 ? "+" : ""}${diff}` : "\u2014";
|
|
5047
|
+
const bar = renderBar2(dim.score);
|
|
5048
|
+
lines.push(`| ${name} | ${bar} ${dim.score}% | ${diffStr} |`);
|
|
4688
5049
|
}
|
|
4689
|
-
|
|
4690
|
-
|
|
4691
|
-
|
|
4692
|
-
|
|
4693
|
-
|
|
4694
|
-
|
|
5050
|
+
lines.push("");
|
|
5051
|
+
lines.push("### Savings");
|
|
5052
|
+
lines.push("");
|
|
5053
|
+
lines.push(`| Metric | Value |`);
|
|
5054
|
+
lines.push(`|--------|-------|`);
|
|
5055
|
+
lines.push(`| Tokens saved | ${score.comparison.savedTokens.toLocaleString()} (${score.comparison.savedPercent}%) |`);
|
|
5056
|
+
lines.push(`| Monthly savings | $${score.comparison.monthlySavingsUSD.toFixed(2)} |`);
|
|
5057
|
+
if (score.insights.length > 0) {
|
|
5058
|
+
lines.push("");
|
|
5059
|
+
lines.push("### Insights");
|
|
5060
|
+
lines.push("");
|
|
5061
|
+
for (const insight of score.insights.slice(0, 5)) {
|
|
5062
|
+
const icon = insight.type === "strength" ? "\u2705" : insight.type === "weakness" ? "\u26A0\uFE0F" : "\u{1F4A1}";
|
|
5063
|
+
lines.push(`- ${icon} **${insight.title}** \u2014 ${insight.detail}`);
|
|
4695
5064
|
}
|
|
4696
|
-
contextMd += "\n";
|
|
4697
5065
|
}
|
|
4698
|
-
|
|
4699
|
-
|
|
4700
|
-
|
|
4701
|
-
|
|
4702
|
-
|
|
4703
|
-
|
|
4704
|
-
|
|
4705
|
-
|
|
5066
|
+
lines.push("");
|
|
5067
|
+
lines.push("---");
|
|
5068
|
+
lines.push(`<sub>Generated by [CTO Quality Gate](https://npmjs.com/package/cto-ai-cli) \xB7 ${(/* @__PURE__ */ new Date()).toISOString().split("T")[0]}</sub>`);
|
|
5069
|
+
return lines.join("\n");
|
|
5070
|
+
}
|
|
5071
|
+
function renderBar2(score) {
|
|
5072
|
+
const filled = Math.round(score / 10);
|
|
5073
|
+
return "\u2588".repeat(filled) + "\u2591".repeat(10 - filled);
|
|
5074
|
+
}
|
|
5075
|
+
function generateSummary(score, checks, passed) {
|
|
5076
|
+
const status = passed ? "\u2705 PASSED" : "\u274C FAILED";
|
|
5077
|
+
const failedChecks = checks.filter((c) => !c.passed && c.severity === "error");
|
|
5078
|
+
const warnings = checks.filter((c) => !c.passed && c.severity === "warning");
|
|
5079
|
+
let summary = `Quality Gate ${status} \u2014 Score: ${score.overall}/100 (${score.grade})`;
|
|
5080
|
+
if (failedChecks.length > 0) {
|
|
5081
|
+
summary += `
|
|
5082
|
+
Failed: ${failedChecks.map((c) => c.name).join(", ")}`;
|
|
4706
5083
|
}
|
|
4707
|
-
if (
|
|
4708
|
-
|
|
4709
|
-
`;
|
|
4710
|
-
for (const f of rest.slice(0, 15)) {
|
|
4711
|
-
contextMd += `- ${f.relativePath}
|
|
4712
|
-
`;
|
|
4713
|
-
}
|
|
4714
|
-
if (rest.length > 15) {
|
|
4715
|
-
contextMd += `- ... and ${rest.length - 15} more
|
|
4716
|
-
`;
|
|
4717
|
-
}
|
|
4718
|
-
contextMd += "\n";
|
|
5084
|
+
if (warnings.length > 0) {
|
|
5085
|
+
summary += `
|
|
5086
|
+
Warnings: ${warnings.map((c) => c.name).join(", ")}`;
|
|
4719
5087
|
}
|
|
4720
|
-
|
|
4721
|
-
|
|
4722
|
-
`;
|
|
4723
|
-
contextMd += `## File contents (top ${Math.min(targets.length + critical.length, 10)} files):
|
|
4724
|
-
|
|
4725
|
-
`;
|
|
4726
|
-
const topFiles = [...targets, ...critical].slice(0, 10);
|
|
4727
|
-
for (const sf of topFiles) {
|
|
4728
|
-
const fullFile = analysis.files.find((f) => f.relativePath === sf.relativePath);
|
|
4729
|
-
if (!fullFile) continue;
|
|
4730
|
-
try {
|
|
4731
|
-
const content = readFileSync4(fullFile.path, "utf-8");
|
|
4732
|
-
const ext = fullFile.extension.replace(".", "");
|
|
4733
|
-
contextMd += `### ${sf.relativePath}
|
|
4734
|
-
`;
|
|
4735
|
-
const maxChars = 5e3;
|
|
4736
|
-
const truncated = content.length > maxChars;
|
|
4737
|
-
contextMd += `\`\`\`${ext}
|
|
4738
|
-
${content.slice(0, maxChars)}${truncated ? "\n// ... [truncated \u2014 " + (content.length - maxChars) + " chars omitted]" : ""}
|
|
4739
|
-
\`\`\`
|
|
5088
|
+
return summary;
|
|
5089
|
+
}
|
|
4740
5090
|
|
|
4741
|
-
|
|
4742
|
-
|
|
4743
|
-
|
|
5091
|
+
// src/cli/commands/ci.ts
|
|
5092
|
+
async function runCIGate(projectPath, analysis, score, threshold, jsonMode) {
|
|
5093
|
+
const filePaths = analysis.files.map((f) => f.path);
|
|
5094
|
+
const auditResult = await auditProject(projectPath, filePaths, { includePII: false });
|
|
5095
|
+
const result = await runQualityGate(score, analysis, auditResult.findings, { threshold });
|
|
5096
|
+
await saveBaseline(projectPath, score);
|
|
5097
|
+
if (jsonMode) {
|
|
5098
|
+
console.log(JSON.stringify(result, null, 2));
|
|
5099
|
+
if (!result.passed) process.exit(1);
|
|
5100
|
+
return;
|
|
4744
5101
|
}
|
|
4745
|
-
const safeName = task.replace(/[^a-zA-Z0-9]/g, "-").toLowerCase().slice(0, 40);
|
|
4746
|
-
const filename = `context-${safeName}.md`;
|
|
4747
|
-
writeFileSync2(join8(ctoDir, filename), contextMd);
|
|
4748
5102
|
console.log("");
|
|
4749
|
-
console.log(
|
|
5103
|
+
console.log(" \u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550");
|
|
5104
|
+
console.log(` \u{1F6A6} Quality Gate: ${result.passed ? "\u2705 PASSED" : "\u274C FAILED"}`);
|
|
5105
|
+
console.log(" \u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550");
|
|
4750
5106
|
console.log("");
|
|
4751
|
-
|
|
4752
|
-
|
|
4753
|
-
|
|
5107
|
+
for (const check of result.checks) {
|
|
5108
|
+
const icon = check.passed ? "\u2705" : check.severity === "warning" ? "\u26A0\uFE0F" : "\u274C";
|
|
5109
|
+
console.log(` ${icon} ${check.name}: ${check.detail}`);
|
|
5110
|
+
}
|
|
5111
|
+
if (result.delta !== null) {
|
|
5112
|
+
const arrow = result.delta >= 0 ? "\u2191" : "\u2193";
|
|
5113
|
+
console.log("");
|
|
5114
|
+
console.log(` \u{1F4CA} Score: ${result.score}/100 (${result.grade}) ${arrow} ${Math.abs(result.delta)} from baseline`);
|
|
5115
|
+
}
|
|
4754
5116
|
console.log("");
|
|
4755
|
-
console.log(
|
|
4756
|
-
console.log(` optimized context on: "${task}"`);
|
|
5117
|
+
console.log(" \u{1F4CB} Baseline saved to .cto/baseline.json");
|
|
4757
5118
|
console.log("");
|
|
5119
|
+
if (!result.passed) {
|
|
5120
|
+
console.log(" \u274C Quality gate failed. Fix the issues above and re-run.");
|
|
5121
|
+
process.exit(1);
|
|
5122
|
+
}
|
|
4758
5123
|
}
|
|
4759
|
-
async function runReport(projectPath, analysis, score) {
|
|
4760
|
-
const gradeEmoji = score.grade.startsWith("A") ? "\u{1F7E2}" : score.grade.startsWith("B") ? "\u{1F535}" : score.grade.startsWith("C") ? "\u{1F7E1}" : "\u{1F534}";
|
|
4761
|
-
const gradeColor = score.overall >= 80 ? "brightgreen" : score.overall >= 60 ? "green" : score.overall >= 40 ? "yellow" : "red";
|
|
4762
|
-
let report = `# CTO Context Score\u2122 Report
|
|
4763
|
-
|
|
4764
|
-
`;
|
|
4765
|
-
const safeGrade = encodeURIComponent(score.grade);
|
|
4766
|
-
report += `
|
|
4767
|
-
`;
|
|
4768
|
-
report += `
|
|
4769
|
-
|
|
4770
|
-
`;
|
|
4771
|
-
report += `> Generated by [cto-ai-cli](https://npmjs.com/package/cto-ai-cli) on ${(/* @__PURE__ */ new Date()).toISOString().split("T")[0]}
|
|
4772
|
-
|
|
4773
|
-
`;
|
|
4774
|
-
report += `## Project: ${analysis.projectName}
|
|
4775
|
-
|
|
4776
|
-
`;
|
|
4777
|
-
report += `| Metric | Value |
|
|
4778
|
-
`;
|
|
4779
|
-
report += `|--------|-------|
|
|
4780
|
-
`;
|
|
4781
|
-
report += `| **Score** | ${gradeEmoji} ${score.overall}/100 (${score.grade}) |
|
|
4782
|
-
`;
|
|
4783
|
-
report += `| **Files** | ${analysis.totalFiles} |
|
|
4784
|
-
`;
|
|
4785
|
-
report += `| **Total tokens** | ${analysis.totalTokens.toLocaleString()} |
|
|
4786
|
-
`;
|
|
4787
|
-
report += `| **Optimized tokens** | ${score.comparison.optimizedTokens.toLocaleString()} |
|
|
4788
|
-
`;
|
|
4789
|
-
report += `| **Token savings** | ${score.comparison.savedPercent}% (${formatTokens(score.comparison.savedTokens)}) |
|
|
4790
|
-
`;
|
|
4791
|
-
report += `| **Est. monthly savings** | $${score.comparison.monthlySavingsUSD.toFixed(2)} |
|
|
4792
|
-
`;
|
|
4793
|
-
report += `| **Stack** | ${analysis.stack.join(", ") || "unknown"} |
|
|
4794
|
-
|
|
4795
|
-
`;
|
|
4796
|
-
report += `## Dimensions
|
|
4797
5124
|
|
|
4798
|
-
|
|
4799
|
-
|
|
4800
|
-
|
|
4801
|
-
report += `|-----------|-------|--------|--------|
|
|
4802
|
-
`;
|
|
4803
|
-
report += `| Efficiency | ${score.dimensions.efficiency.score}% | 30% | ${score.dimensions.efficiency.detail} |
|
|
4804
|
-
`;
|
|
4805
|
-
report += `| Coverage | ${score.dimensions.coverage.score}% | 25% | ${score.dimensions.coverage.detail} |
|
|
4806
|
-
`;
|
|
4807
|
-
report += `| Risk Control | ${score.dimensions.riskControl.score}% | 20% | ${score.dimensions.riskControl.detail} |
|
|
4808
|
-
`;
|
|
4809
|
-
report += `| Structure | ${score.dimensions.structure.score}% | 15% | ${score.dimensions.structure.detail} |
|
|
4810
|
-
`;
|
|
4811
|
-
report += `| Governance | ${score.dimensions.governance.score}% | 10% | ${score.dimensions.governance.detail} |
|
|
5125
|
+
// src/engine/feedback.ts
|
|
5126
|
+
import { resolve as resolve6, join as join10 } from "path";
|
|
5127
|
+
import { readFile as readFile7, writeFile as writeFile3, mkdir as mkdir2 } from "fs/promises";
|
|
4812
5128
|
|
|
4813
|
-
|
|
4814
|
-
|
|
4815
|
-
|
|
5129
|
+
// src/interact/router.ts
|
|
5130
|
+
var TASK_KEYWORDS = {
|
|
5131
|
+
debug: ["debug", "fix", "bug", "error", "issue", "broken", "crash", "failing", "wrong"],
|
|
5132
|
+
review: ["review", "check", "assess", "evaluate", "audit", "inspect", "critique"],
|
|
5133
|
+
refactor: ["refactor", "restructure", "reorganize", "clean up", "simplify", "extract", "move"],
|
|
5134
|
+
test: ["test", "spec", "coverage", "unit test", "integration test", "e2e"],
|
|
5135
|
+
docs: ["document", "docs", "readme", "jsdoc", "comment", "explain"],
|
|
5136
|
+
feature: ["add", "implement", "create", "build", "new", "feature", "endpoint"],
|
|
5137
|
+
architecture: ["architecture", "design", "system", "structure", "migrate", "pattern"],
|
|
5138
|
+
"simple-edit": ["rename", "typo", "update", "change", "modify", "tweak", "adjust"]
|
|
5139
|
+
};
|
|
5140
|
+
function classifyTask(taskDescription) {
|
|
5141
|
+
const lower = taskDescription.toLowerCase();
|
|
5142
|
+
let bestType = "simple-edit";
|
|
5143
|
+
let bestScore = 0;
|
|
5144
|
+
for (const [type, keywords] of Object.entries(TASK_KEYWORDS)) {
|
|
5145
|
+
let score = 0;
|
|
5146
|
+
for (const kw of keywords) {
|
|
5147
|
+
if (lower.includes(kw)) score++;
|
|
5148
|
+
}
|
|
5149
|
+
if (score > bestScore) {
|
|
5150
|
+
bestScore = score;
|
|
5151
|
+
bestType = type;
|
|
5152
|
+
}
|
|
5153
|
+
}
|
|
5154
|
+
return bestType;
|
|
5155
|
+
}
|
|
4816
5156
|
|
|
4817
|
-
|
|
4818
|
-
|
|
4819
|
-
|
|
4820
|
-
|
|
4821
|
-
|
|
5157
|
+
// src/engine/feedback.ts
|
|
5158
|
+
async function getFeedbackPath(projectPath) {
|
|
5159
|
+
const ctoDir = join10(resolve6(projectPath), ".cto");
|
|
5160
|
+
await mkdir2(ctoDir, { recursive: true });
|
|
5161
|
+
return join10(ctoDir, "feedback.json");
|
|
5162
|
+
}
|
|
5163
|
+
async function getModelPath(projectPath) {
|
|
5164
|
+
const ctoDir = join10(resolve6(projectPath), ".cto");
|
|
5165
|
+
await mkdir2(ctoDir, { recursive: true });
|
|
5166
|
+
return join10(ctoDir, "feedback-model.json");
|
|
5167
|
+
}
|
|
5168
|
+
async function loadFeedback(projectPath) {
|
|
5169
|
+
try {
|
|
5170
|
+
const raw = await readFile7(await getFeedbackPath(projectPath), "utf-8");
|
|
5171
|
+
return JSON.parse(raw);
|
|
5172
|
+
} catch {
|
|
5173
|
+
return [];
|
|
5174
|
+
}
|
|
5175
|
+
}
|
|
5176
|
+
async function loadFeedbackModel(projectPath) {
|
|
5177
|
+
try {
|
|
5178
|
+
const raw = await readFile7(await getModelPath(projectPath), "utf-8");
|
|
5179
|
+
return JSON.parse(raw);
|
|
5180
|
+
} catch {
|
|
5181
|
+
return createEmptyModel();
|
|
5182
|
+
}
|
|
5183
|
+
}
|
|
5184
|
+
function createEmptyModel() {
|
|
5185
|
+
return {
|
|
5186
|
+
version: 2,
|
|
5187
|
+
updatedAt: (/* @__PURE__ */ new Date()).toISOString(),
|
|
5188
|
+
totalFeedback: 0,
|
|
5189
|
+
acceptRate: 0,
|
|
5190
|
+
fileAcceptance: {},
|
|
5191
|
+
taskTypeAcceptance: {},
|
|
5192
|
+
pairAcceptance: {},
|
|
5193
|
+
sessions: {},
|
|
5194
|
+
strategyComparison: {},
|
|
5195
|
+
insights: []
|
|
5196
|
+
};
|
|
5197
|
+
}
|
|
5198
|
+
async function exportFeedbackForTeam(projectPath, projectName) {
|
|
5199
|
+
const model = await loadFeedbackModel(projectPath);
|
|
5200
|
+
const entries = await loadFeedback(projectPath);
|
|
5201
|
+
return {
|
|
5202
|
+
version: 2,
|
|
5203
|
+
exportedAt: (/* @__PURE__ */ new Date()).toISOString(),
|
|
5204
|
+
projectName,
|
|
5205
|
+
model,
|
|
5206
|
+
entrySummary: {
|
|
5207
|
+
total: entries.length,
|
|
5208
|
+
accepted: entries.filter((e) => e.outcome.accepted).length,
|
|
5209
|
+
sessions: new Set(entries.map((e) => e.sessionId).filter(Boolean)).size
|
|
5210
|
+
}
|
|
5211
|
+
};
|
|
5212
|
+
}
|
|
5213
|
+
function renderFeedbackReport(model) {
|
|
5214
|
+
const lines = [];
|
|
5215
|
+
lines.push("");
|
|
5216
|
+
lines.push(` \u2554\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2557`);
|
|
5217
|
+
lines.push(` \u2551 \u{1F4CA} Feedback Loop Report \u2551`);
|
|
5218
|
+
lines.push(` \u2560\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2563`);
|
|
5219
|
+
lines.push(` \u2551 \u2551`);
|
|
5220
|
+
lines.push(` \u2551 Total feedback: ${pad2(model.totalFeedback.toString(), 8)} \u2551`);
|
|
5221
|
+
lines.push(` \u2551 Accept rate: ${pad2(Math.round(model.acceptRate * 100) + "%", 8)} \u2551`);
|
|
5222
|
+
lines.push(` \u2551 Tracked files: ${pad2(Object.keys(model.fileAcceptance).length.toString(), 8)} \u2551`);
|
|
5223
|
+
lines.push(` \u2551 Task types: ${pad2(Object.keys(model.taskTypeAcceptance).length.toString(), 8)} \u2551`);
|
|
5224
|
+
lines.push(` \u2551 \u2551`);
|
|
5225
|
+
if (model.insights.length > 0) {
|
|
5226
|
+
lines.push(` \u2551 Insights: \u2551`);
|
|
5227
|
+
for (const insight of model.insights.slice(0, 5)) {
|
|
5228
|
+
const icon = insight.type === "positive" ? "\u2705" : insight.type === "negative" ? "\u26A0\uFE0F" : "\u{1F4A1}";
|
|
5229
|
+
lines.push(` \u2551 ${icon} ${pad2(insight.title, 43)} \u2551`);
|
|
4822
5230
|
}
|
|
4823
|
-
report += "\n";
|
|
4824
5231
|
}
|
|
4825
|
-
|
|
4826
|
-
|
|
4827
|
-
|
|
4828
|
-
|
|
4829
|
-
|
|
4830
|
-
|
|
4831
|
-
|
|
4832
|
-
|
|
5232
|
+
lines.push(` \u2551 \u2551`);
|
|
5233
|
+
lines.push(` \u255A\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u255D`);
|
|
5234
|
+
return lines.join("\n");
|
|
5235
|
+
}
|
|
5236
|
+
function pad2(s, w) {
|
|
5237
|
+
return s.padEnd(w).substring(0, w);
|
|
5238
|
+
}
|
|
5239
|
+
function renderCrossRepoReport(stats) {
|
|
5240
|
+
const lines = [];
|
|
5241
|
+
lines.push("");
|
|
5242
|
+
lines.push(" \u2554\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2557");
|
|
5243
|
+
lines.push(" \u2551 \u{1F310} Cross-Repo Intelligence \u2551");
|
|
5244
|
+
lines.push(" \u2560\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2563");
|
|
5245
|
+
lines.push(" \u2551 \u2551");
|
|
5246
|
+
lines.push(` \u2551 Projects learned: ${pad2(stats.totalProjects.toString(), 8)} \u2551`);
|
|
5247
|
+
lines.push(` \u2551 Total observations: ${pad2(stats.totalObservations.toString(), 8)} \u2551`);
|
|
5248
|
+
lines.push(` \u2551 Archetypes: ${pad2(stats.archetypes.length.toString(), 8)} \u2551`);
|
|
5249
|
+
lines.push(` \u2551 Universal patterns: ${pad2(stats.universalPatterns.toString(), 8)} \u2551`);
|
|
5250
|
+
lines.push(" \u2551 \u2551");
|
|
5251
|
+
if (stats.archetypes.length > 0) {
|
|
5252
|
+
lines.push(" \u2551 Archetypes: \u2551");
|
|
5253
|
+
for (const a of stats.archetypes.slice(0, 5)) {
|
|
5254
|
+
lines.push(` \u2551 ${pad2(a.name, 24)} ${pad2(a.observations + " obs", 12)} \u2551`);
|
|
5255
|
+
}
|
|
5256
|
+
}
|
|
5257
|
+
lines.push(" \u2551 \u2551");
|
|
5258
|
+
lines.push(" \u255A\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u255D");
|
|
5259
|
+
return lines.join("\n");
|
|
5260
|
+
}
|
|
4833
5261
|
|
|
4834
|
-
|
|
4835
|
-
|
|
5262
|
+
// src/engine/predictor.ts
|
|
5263
|
+
import { resolve as resolve7, join as join11 } from "path";
|
|
5264
|
+
import { readFile as readFile8, writeFile as writeFile4, mkdir as mkdir3 } from "fs/promises";
|
|
5265
|
+
init_graph_utils();
|
|
5266
|
+
var DEFAULT_PREDICTOR_CONFIG = {
|
|
5267
|
+
maxCoSelectionPairs: 500,
|
|
5268
|
+
decayFactor: 0.95,
|
|
5269
|
+
// slight decay to favor recent patterns
|
|
5270
|
+
minObservations: 2
|
|
5271
|
+
// need at least 2 observations before predicting
|
|
5272
|
+
};
|
|
5273
|
+
async function getModelPath2(projectPath) {
|
|
5274
|
+
const ctoDir = join11(resolve7(projectPath), ".cto");
|
|
5275
|
+
await mkdir3(ctoDir, { recursive: true });
|
|
5276
|
+
return join11(ctoDir, "predictor.json");
|
|
5277
|
+
}
|
|
5278
|
+
async function loadModel(projectPath) {
|
|
5279
|
+
try {
|
|
5280
|
+
const path = await getModelPath2(projectPath);
|
|
5281
|
+
const raw = await readFile8(path, "utf-8");
|
|
5282
|
+
return JSON.parse(raw);
|
|
5283
|
+
} catch {
|
|
5284
|
+
return createEmptyModel2();
|
|
5285
|
+
}
|
|
5286
|
+
}
|
|
5287
|
+
function createEmptyModel2() {
|
|
5288
|
+
return {
|
|
5289
|
+
version: 1,
|
|
5290
|
+
trainedAt: (/* @__PURE__ */ new Date()).toISOString(),
|
|
5291
|
+
totalObservations: 0,
|
|
5292
|
+
taskTypeFrequency: {},
|
|
5293
|
+
keywordFrequency: {},
|
|
5294
|
+
fileStats: {},
|
|
5295
|
+
coSelection: {}
|
|
5296
|
+
};
|
|
5297
|
+
}
|
|
5298
|
+
async function predictRelevantFiles(projectPath, task, analysis, config = {}) {
|
|
5299
|
+
const cfg = { ...DEFAULT_PREDICTOR_CONFIG, ...config };
|
|
5300
|
+
const model = await loadModel(projectPath);
|
|
5301
|
+
if (model.totalObservations < cfg.minObservations) {
|
|
5302
|
+
return [];
|
|
5303
|
+
}
|
|
5304
|
+
const taskType = classifyTask(task);
|
|
5305
|
+
const keywords = extractKeywords(task);
|
|
5306
|
+
const scores = /* @__PURE__ */ new Map();
|
|
5307
|
+
const boost = (path, amount, reason) => {
|
|
5308
|
+
const existing = scores.get(path) ?? { score: 0, reasons: [] };
|
|
5309
|
+
existing.score += amount;
|
|
5310
|
+
existing.reasons.push(reason);
|
|
5311
|
+
scores.set(path, existing);
|
|
5312
|
+
};
|
|
5313
|
+
const taskFreqs = model.taskTypeFrequency[taskType];
|
|
5314
|
+
if (taskFreqs) {
|
|
5315
|
+
const maxFreq = Math.max(...Object.values(taskFreqs));
|
|
5316
|
+
for (const [path, freq] of Object.entries(taskFreqs)) {
|
|
5317
|
+
const normalized = maxFreq > 0 ? freq / maxFreq : 0;
|
|
5318
|
+
boost(path, normalized * 30, `${taskType} task history (${freq}\xD7 selected)`);
|
|
5319
|
+
}
|
|
5320
|
+
}
|
|
5321
|
+
for (const kw of keywords) {
|
|
5322
|
+
const kwFreqs = model.keywordFrequency[kw];
|
|
5323
|
+
if (kwFreqs) {
|
|
5324
|
+
const maxFreq = Math.max(...Object.values(kwFreqs));
|
|
5325
|
+
for (const [path, freq] of Object.entries(kwFreqs)) {
|
|
5326
|
+
const normalized = maxFreq > 0 ? freq / maxFreq : 0;
|
|
5327
|
+
boost(path, normalized * 20, `keyword "${kw}" (${freq}\xD7 selected)`);
|
|
5328
|
+
}
|
|
5329
|
+
}
|
|
5330
|
+
}
|
|
5331
|
+
for (const [path, stats] of Object.entries(model.fileStats)) {
|
|
5332
|
+
const freqRatio = model.totalObservations > 0 ? stats.totalSelections / model.totalObservations : 0;
|
|
5333
|
+
if (freqRatio > 0.3) {
|
|
5334
|
+
boost(path, freqRatio * 10, `frequently selected (${stats.totalSelections}/${model.totalObservations})`);
|
|
5335
|
+
}
|
|
5336
|
+
}
|
|
5337
|
+
const topPredicted = [...scores.entries()].sort((a, b) => b[1].score - a[1].score).slice(0, 10).map(([path]) => path);
|
|
5338
|
+
for (const topPath of topPredicted) {
|
|
5339
|
+
const coFiles = model.coSelection[topPath];
|
|
5340
|
+
if (coFiles) {
|
|
5341
|
+
for (const [coPath, count] of Object.entries(coFiles)) {
|
|
5342
|
+
if (count >= 2) {
|
|
5343
|
+
boost(coPath, count * 2, `co-selected with ${topPath} (${count}\xD7)`);
|
|
5344
|
+
}
|
|
5345
|
+
}
|
|
5346
|
+
}
|
|
5347
|
+
}
|
|
5348
|
+
const existingPaths = new Set(analysis.files.map((f) => f.relativePath));
|
|
5349
|
+
const results = [];
|
|
5350
|
+
for (const [path, data] of scores) {
|
|
5351
|
+
if (existingPaths.has(path)) {
|
|
5352
|
+
results.push({
|
|
5353
|
+
filePath: path,
|
|
5354
|
+
predictedScore: Math.round(data.score * 10) / 10,
|
|
5355
|
+
reasons: data.reasons.slice(0, 5)
|
|
5356
|
+
});
|
|
5357
|
+
}
|
|
5358
|
+
}
|
|
5359
|
+
return results.sort((a, b) => b.predictedScore - a.predictedScore).slice(0, 50);
|
|
5360
|
+
}
|
|
5361
|
+
function getModelStats(model) {
|
|
5362
|
+
const coSelectionPairs = Object.values(model.coSelection).reduce((s, pairs) => s + Object.keys(pairs).length, 0) / 2;
|
|
5363
|
+
return {
|
|
5364
|
+
observations: model.totalObservations,
|
|
5365
|
+
taskTypes: Object.keys(model.taskTypeFrequency).length,
|
|
5366
|
+
keywords: Object.keys(model.keywordFrequency).length,
|
|
5367
|
+
trackedFiles: Object.keys(model.fileStats).length,
|
|
5368
|
+
coSelectionPairs: Math.round(coSelectionPairs),
|
|
5369
|
+
trainedAt: model.trainedAt
|
|
5370
|
+
};
|
|
5371
|
+
}
|
|
5372
|
+
function extractKeywords(task) {
|
|
5373
|
+
const stopWords = /* @__PURE__ */ new Set([
|
|
5374
|
+
"the",
|
|
5375
|
+
"a",
|
|
5376
|
+
"an",
|
|
5377
|
+
"is",
|
|
5378
|
+
"are",
|
|
5379
|
+
"was",
|
|
5380
|
+
"were",
|
|
5381
|
+
"be",
|
|
5382
|
+
"been",
|
|
5383
|
+
"being",
|
|
5384
|
+
"have",
|
|
5385
|
+
"has",
|
|
5386
|
+
"had",
|
|
5387
|
+
"do",
|
|
5388
|
+
"does",
|
|
5389
|
+
"did",
|
|
5390
|
+
"will",
|
|
5391
|
+
"would",
|
|
5392
|
+
"could",
|
|
5393
|
+
"should",
|
|
5394
|
+
"may",
|
|
5395
|
+
"might",
|
|
5396
|
+
"can",
|
|
5397
|
+
"shall",
|
|
5398
|
+
"to",
|
|
5399
|
+
"of",
|
|
5400
|
+
"in",
|
|
5401
|
+
"for",
|
|
5402
|
+
"on",
|
|
5403
|
+
"with",
|
|
5404
|
+
"at",
|
|
5405
|
+
"by",
|
|
5406
|
+
"from",
|
|
5407
|
+
"as",
|
|
5408
|
+
"into",
|
|
5409
|
+
"through",
|
|
5410
|
+
"and",
|
|
5411
|
+
"but",
|
|
5412
|
+
"or",
|
|
5413
|
+
"not",
|
|
5414
|
+
"this",
|
|
5415
|
+
"that",
|
|
5416
|
+
"it",
|
|
5417
|
+
"its",
|
|
5418
|
+
"fix",
|
|
5419
|
+
"add",
|
|
5420
|
+
"remove",
|
|
5421
|
+
"update",
|
|
5422
|
+
"change",
|
|
5423
|
+
"refactor",
|
|
5424
|
+
"implement",
|
|
5425
|
+
"create",
|
|
5426
|
+
"build",
|
|
5427
|
+
"make",
|
|
5428
|
+
"code",
|
|
5429
|
+
"file",
|
|
5430
|
+
"module",
|
|
5431
|
+
"function"
|
|
5432
|
+
]);
|
|
5433
|
+
return task.toLowerCase().replace(/[^a-z0-9\s]/g, "").split(/\s+/).filter((w) => w.length > 2 && !stopWords.has(w));
|
|
5434
|
+
}
|
|
4836
5435
|
|
|
4837
|
-
|
|
4838
|
-
|
|
4839
|
-
|
|
4840
|
-
|
|
4841
|
-
|
|
4842
|
-
|
|
4843
|
-
|
|
4844
|
-
|
|
4845
|
-
|
|
4846
|
-
|
|
4847
|
-
|
|
4848
|
-
|
|
4849
|
-
|
|
4850
|
-
|
|
4851
|
-
console.log(" Copy-paste this markdown into your README:");
|
|
4852
|
-
console.log(` `);
|
|
4853
|
-
console.log("");
|
|
5436
|
+
// src/engine/cross-repo.ts
|
|
5437
|
+
import { join as join12, basename as basename4 } from "path";
|
|
5438
|
+
import { readFile as readFile9, writeFile as writeFile5, mkdir as mkdir4 } from "fs/promises";
|
|
5439
|
+
function getGlobalModelPath() {
|
|
5440
|
+
const home = process.env.HOME ?? process.env.USERPROFILE ?? "/tmp";
|
|
5441
|
+
return join12(home, ".cto", "global-intelligence.json");
|
|
5442
|
+
}
|
|
5443
|
+
async function loadGlobalModel() {
|
|
5444
|
+
try {
|
|
5445
|
+
const raw = await readFile9(getGlobalModelPath(), "utf-8");
|
|
5446
|
+
return JSON.parse(raw);
|
|
5447
|
+
} catch {
|
|
5448
|
+
return createEmptyModel3();
|
|
5449
|
+
}
|
|
4854
5450
|
}
|
|
4855
|
-
function
|
|
4856
|
-
|
|
4857
|
-
|
|
4858
|
-
|
|
4859
|
-
|
|
4860
|
-
|
|
4861
|
-
|
|
4862
|
-
|
|
4863
|
-
|
|
5451
|
+
function createEmptyModel3() {
|
|
5452
|
+
return {
|
|
5453
|
+
version: 1,
|
|
5454
|
+
updatedAt: (/* @__PURE__ */ new Date()).toISOString(),
|
|
5455
|
+
totalProjects: 0,
|
|
5456
|
+
totalObservations: 0,
|
|
5457
|
+
archetypes: {},
|
|
5458
|
+
universalPatterns: []
|
|
5459
|
+
};
|
|
5460
|
+
}
|
|
5461
|
+
function getCrossRepoStats(model) {
|
|
5462
|
+
return {
|
|
5463
|
+
totalProjects: model.totalProjects,
|
|
5464
|
+
totalObservations: model.totalObservations,
|
|
5465
|
+
archetypes: Object.values(model.archetypes).map((a) => ({
|
|
5466
|
+
name: a.name,
|
|
5467
|
+
projects: a.projectCount,
|
|
5468
|
+
observations: a.observationCount
|
|
5469
|
+
})),
|
|
5470
|
+
universalPatterns: model.universalPatterns.length
|
|
5471
|
+
};
|
|
5472
|
+
}
|
|
5473
|
+
|
|
5474
|
+
// src/cli/commands/learn.ts
|
|
5475
|
+
async function runLearn(projectPath, analysis, jsonMode) {
|
|
5476
|
+
const feedbackModel = await loadFeedbackModel(projectPath);
|
|
5477
|
+
const predictorModel = await loadModel(projectPath);
|
|
5478
|
+
const predictorStats = getModelStats(predictorModel);
|
|
5479
|
+
const globalModel = await loadGlobalModel();
|
|
5480
|
+
const crossRepoStats = getCrossRepoStats(globalModel);
|
|
5481
|
+
if (jsonMode) {
|
|
5482
|
+
const exported = await exportFeedbackForTeam(projectPath, analysis.projectName);
|
|
5483
|
+
console.log(JSON.stringify({
|
|
5484
|
+
feedback: feedbackModel,
|
|
5485
|
+
predictor: predictorStats,
|
|
5486
|
+
crossRepo: crossRepoStats,
|
|
5487
|
+
teamExport: exported
|
|
5488
|
+
}, null, 2));
|
|
5489
|
+
return;
|
|
5490
|
+
}
|
|
5491
|
+
console.log(renderFeedbackReport(feedbackModel));
|
|
4864
5492
|
console.log("");
|
|
4865
5493
|
console.log(" \u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550");
|
|
4866
|
-
console.log(" \u{
|
|
5494
|
+
console.log(" \u{1F9E0} Predictor Model");
|
|
4867
5495
|
console.log(" \u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550");
|
|
4868
5496
|
console.log("");
|
|
4869
|
-
|
|
4870
|
-
|
|
4871
|
-
|
|
4872
|
-
|
|
4873
|
-
|
|
4874
|
-
|
|
4875
|
-
const marker = entry.isYou ? " \u25C4" : "";
|
|
4876
|
-
const name = entry.name.padEnd(25);
|
|
4877
|
-
console.log(` ${name} ${entry.score.toString().padStart(3)}/100 (${entry.grade.padEnd(2)}) ${bar}${marker}`);
|
|
4878
|
-
}
|
|
5497
|
+
console.log(` Observations: ${predictorStats.observations}`);
|
|
5498
|
+
console.log(` Task types: ${predictorStats.taskTypes}`);
|
|
5499
|
+
console.log(` Keywords: ${predictorStats.keywords}`);
|
|
5500
|
+
console.log(` Tracked files: ${predictorStats.trackedFiles}`);
|
|
5501
|
+
console.log(` Co-selection: ${predictorStats.coSelectionPairs} pairs`);
|
|
5502
|
+
console.log(` Last trained: ${predictorStats.trainedAt || "never"}`);
|
|
4879
5503
|
console.log("");
|
|
4880
|
-
|
|
4881
|
-
|
|
4882
|
-
|
|
4883
|
-
console.log(
|
|
5504
|
+
console.log(renderCrossRepoReport(crossRepoStats));
|
|
5505
|
+
if (feedbackModel.insights.length > 0) {
|
|
5506
|
+
console.log("");
|
|
5507
|
+
console.log(" \u{1F4A1} Top Insights:");
|
|
5508
|
+
console.log("");
|
|
5509
|
+
for (const insight of feedbackModel.insights.slice(0, 8)) {
|
|
5510
|
+
const icon = insight.type === "positive" ? "\u2705" : insight.type === "negative" ? "\u26A0\uFE0F" : "\u{1F4A1}";
|
|
5511
|
+
console.log(` ${icon} ${insight.title}`);
|
|
5512
|
+
console.log(` ${insight.detail}`);
|
|
5513
|
+
}
|
|
5514
|
+
console.log("");
|
|
4884
5515
|
}
|
|
4885
|
-
|
|
4886
|
-
|
|
5516
|
+
const strategies = Object.entries(feedbackModel.strategyComparison);
|
|
5517
|
+
if (strategies.length > 1) {
|
|
5518
|
+
console.log("");
|
|
5519
|
+
console.log(" \u{1F52C} A/B Strategy Comparison:");
|
|
5520
|
+
console.log("");
|
|
5521
|
+
for (const [name, sc] of strategies) {
|
|
5522
|
+
console.log(` ${name}: ${Math.round(sc.acceptRate * 100)}% accept (n=${sc.totalCount}), avg ${Math.round(sc.avgTimeToAccept / 1e3)}s`);
|
|
5523
|
+
}
|
|
5524
|
+
console.log("");
|
|
4887
5525
|
}
|
|
4888
|
-
|
|
4889
|
-
|
|
5526
|
+
}
|
|
5527
|
+
async function runPredict(projectPath, analysis, task, jsonMode) {
|
|
5528
|
+
const predictions = await predictRelevantFiles(projectPath, task, analysis);
|
|
5529
|
+
if (jsonMode) {
|
|
5530
|
+
console.log(JSON.stringify({ task, predictions }, null, 2));
|
|
5531
|
+
return;
|
|
4890
5532
|
}
|
|
4891
5533
|
console.log("");
|
|
4892
|
-
|
|
4893
|
-
|
|
4894
|
-
|
|
4895
|
-
|
|
4896
|
-
|
|
4897
|
-
|
|
4898
|
-
}
|
|
4899
|
-
async function runAudit(projectPath, analysis, flags = {}) {
|
|
4900
|
-
if (flags.initHookMode) {
|
|
4901
|
-
const { generatePreCommitHook: generatePreCommitHook2 } = await Promise.resolve().then(() => (init_secrets(), secrets_exports));
|
|
4902
|
-
const hookPath = generatePreCommitHook2(projectPath, "husky");
|
|
4903
|
-
console.log("");
|
|
4904
|
-
console.log(" \u2705 Pre-commit hook generated!");
|
|
4905
|
-
console.log(` \u{1F4CB} ${hookPath}`);
|
|
4906
|
-
console.log("");
|
|
4907
|
-
console.log(" Staged files will be scanned for secrets before every commit.");
|
|
4908
|
-
console.log(" To remove: delete the hook file.");
|
|
5534
|
+
console.log(" \u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550");
|
|
5535
|
+
console.log(` \u{1F52E} Predictions for: "${task}"`);
|
|
5536
|
+
console.log(" \u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550");
|
|
5537
|
+
console.log("");
|
|
5538
|
+
if (predictions.length === 0) {
|
|
5539
|
+
console.log(" Not enough learning data yet. Use the tool more and run --learn to see insights.");
|
|
4909
5540
|
console.log("");
|
|
4910
5541
|
return;
|
|
4911
5542
|
}
|
|
5543
|
+
for (const p of predictions.slice(0, 15)) {
|
|
5544
|
+
const bar = "\u2588".repeat(Math.round(p.predictedScore / 5)) + "\u2591".repeat(Math.max(0, 20 - Math.round(p.predictedScore / 5)));
|
|
5545
|
+
console.log(` ${bar} ${p.predictedScore.toFixed(1).padStart(5)} ${p.filePath}`);
|
|
5546
|
+
if (p.reasons.length > 0) console.log(` ${p.reasons[0]}`);
|
|
5547
|
+
}
|
|
4912
5548
|
console.log("");
|
|
4913
|
-
console.log(
|
|
5549
|
+
console.log(` ${predictions.length} files predicted as relevant.`);
|
|
4914
5550
|
console.log("");
|
|
4915
|
-
|
|
4916
|
-
|
|
4917
|
-
|
|
4918
|
-
|
|
4919
|
-
|
|
4920
|
-
|
|
4921
|
-
|
|
4922
|
-
|
|
4923
|
-
|
|
4924
|
-
|
|
4925
|
-
|
|
4926
|
-
|
|
4927
|
-
|
|
4928
|
-
|
|
4929
|
-
|
|
4930
|
-
|
|
4931
|
-
|
|
4932
|
-
|
|
4933
|
-
|
|
4934
|
-
if (summary.bySeverity.critical > 0) {
|
|
4935
|
-
console.log(` \u2551 \u{1F534} Critical: ${summary.bySeverity.critical.toString().padEnd(33)} \u2551`);
|
|
5551
|
+
}
|
|
5552
|
+
|
|
5553
|
+
// src/cli/commands/review.ts
|
|
5554
|
+
import { join as join13 } from "path";
|
|
5555
|
+
import { mkdirSync as mkdirSync6, writeFileSync as writeFileSync6 } from "fs";
|
|
5556
|
+
|
|
5557
|
+
// src/engine/code-review.ts
|
|
5558
|
+
init_graph_utils();
|
|
5559
|
+
import { resolve as resolve9, basename as basename5, dirname as dirname4 } from "path";
|
|
5560
|
+
import { readFile as readFile10 } from "fs/promises";
|
|
5561
|
+
import { execFile } from "child_process";
|
|
5562
|
+
import { promisify } from "util";
|
|
5563
|
+
var exec = promisify(execFile);
|
|
5564
|
+
async function git(args, cwd) {
|
|
5565
|
+
try {
|
|
5566
|
+
const { stdout } = await exec("git", args, { cwd, maxBuffer: 10 * 1024 * 1024 });
|
|
5567
|
+
return stdout.trim();
|
|
5568
|
+
} catch {
|
|
5569
|
+
return "";
|
|
4936
5570
|
}
|
|
4937
|
-
|
|
4938
|
-
|
|
5571
|
+
}
|
|
5572
|
+
async function analyzeForReview(analysis, options = {}) {
|
|
5573
|
+
const projectPath = resolve9(analysis.projectPath);
|
|
5574
|
+
const baseBranch = options.baseBranch ?? "main";
|
|
5575
|
+
const depth = options.depth ?? 2;
|
|
5576
|
+
const maxPromptFiles = options.maxPromptFiles ?? 20;
|
|
5577
|
+
const isRepo = await git(["rev-parse", "--is-inside-work-tree"], projectPath) === "true";
|
|
5578
|
+
if (!isRepo) {
|
|
5579
|
+
return emptyResult2(baseBranch);
|
|
4939
5580
|
}
|
|
4940
|
-
|
|
4941
|
-
|
|
5581
|
+
const branch = await git(["rev-parse", "--abbrev-ref", "HEAD"], projectPath);
|
|
5582
|
+
const changedFiles = await getChangedFilesWithHunks(projectPath, baseBranch, analysis);
|
|
5583
|
+
if (changedFiles.length === 0) {
|
|
5584
|
+
return {
|
|
5585
|
+
...emptyResult2(baseBranch),
|
|
5586
|
+
branch,
|
|
5587
|
+
isGitRepo: true,
|
|
5588
|
+
renderedSummary: "# Code Review\n\nNo changed files detected."
|
|
5589
|
+
};
|
|
4942
5590
|
}
|
|
4943
|
-
|
|
4944
|
-
|
|
5591
|
+
const totalLinesChanged = changedFiles.reduce((s, f) => s + f.linesAdded + f.linesRemoved, 0);
|
|
5592
|
+
const breakingChanges = detectBreakingChanges(changedFiles, analysis);
|
|
5593
|
+
const missingFiles = findMissingFiles(changedFiles, analysis, depth);
|
|
5594
|
+
const impactRadius = computeImpactRadius(changedFiles, analysis, depth);
|
|
5595
|
+
const reviewQuality = calculateReviewQuality(changedFiles, breakingChanges, missingFiles, impactRadius, totalLinesChanged);
|
|
5596
|
+
const reviewPrompt = await generateReviewPrompt(changedFiles, breakingChanges, missingFiles, analysis, projectPath, maxPromptFiles);
|
|
5597
|
+
const renderedSummary = renderReviewSummary(branch, baseBranch, changedFiles, breakingChanges, missingFiles, impactRadius, reviewQuality);
|
|
5598
|
+
return {
|
|
5599
|
+
branch,
|
|
5600
|
+
baseBranch,
|
|
5601
|
+
isGitRepo: true,
|
|
5602
|
+
changedFiles,
|
|
5603
|
+
totalLinesChanged,
|
|
5604
|
+
breakingChanges,
|
|
5605
|
+
missingFiles,
|
|
5606
|
+
impactRadius,
|
|
5607
|
+
reviewQuality,
|
|
5608
|
+
reviewPrompt,
|
|
5609
|
+
renderedSummary
|
|
5610
|
+
};
|
|
5611
|
+
}
|
|
5612
|
+
async function getChangedFilesWithHunks(projectPath, baseBranch, analysis) {
|
|
5613
|
+
const files = [];
|
|
5614
|
+
const fileMap = new Map(analysis.files.map((f) => [f.relativePath, f]));
|
|
5615
|
+
const numstat = await git(["diff", "--numstat", "HEAD"], projectPath);
|
|
5616
|
+
const branchNumstat = await git(["diff", "--numstat", `${baseBranch}...HEAD`], projectPath);
|
|
5617
|
+
const nameStatus = await git(["diff", "--name-status", `${baseBranch}...HEAD`], projectPath);
|
|
5618
|
+
const changeTypes = /* @__PURE__ */ new Map();
|
|
5619
|
+
for (const line of nameStatus.split("\n")) {
|
|
5620
|
+
const parts = line.trim().split(" ");
|
|
5621
|
+
if (parts.length < 2) continue;
|
|
5622
|
+
const status = parts[0];
|
|
5623
|
+
const filePath = parts[parts.length - 1];
|
|
5624
|
+
if (status === "A") changeTypes.set(filePath, "added");
|
|
5625
|
+
else if (status === "D") changeTypes.set(filePath, "deleted");
|
|
5626
|
+
else if (status.startsWith("R")) changeTypes.set(filePath, "renamed");
|
|
5627
|
+
else changeTypes.set(filePath, "modified");
|
|
5628
|
+
}
|
|
5629
|
+
const allNumstat = (numstat + "\n" + branchNumstat).split("\n");
|
|
5630
|
+
const seen = /* @__PURE__ */ new Set();
|
|
5631
|
+
for (const line of allNumstat) {
|
|
5632
|
+
const parts = line.trim().split(" ");
|
|
5633
|
+
if (parts.length < 3) continue;
|
|
5634
|
+
const added = parts[0] === "-" ? 0 : parseInt(parts[0], 10) || 0;
|
|
5635
|
+
const removed = parts[1] === "-" ? 0 : parseInt(parts[1], 10) || 0;
|
|
5636
|
+
const filePath = parts[2];
|
|
5637
|
+
if (!filePath || seen.has(filePath)) continue;
|
|
5638
|
+
seen.add(filePath);
|
|
5639
|
+
const af = fileMap.get(filePath);
|
|
5640
|
+
const changeType = changeTypes.get(filePath) ?? "modified";
|
|
5641
|
+
const hunks = await parseDiffHunks(projectPath, baseBranch, filePath);
|
|
5642
|
+
const hasExportChanges = hunks.some(
|
|
5643
|
+
(h) => h.additions.some((l) => /^\s*export\s/.test(l)) || h.deletions.some((l) => /^\s*export\s/.test(l))
|
|
5644
|
+
);
|
|
5645
|
+
const hasTypeChanges = hunks.some(
|
|
5646
|
+
(h) => h.additions.some((l) => /^\s*(interface|type|enum)\s/.test(l)) || h.deletions.some((l) => /^\s*(interface|type|enum)\s/.test(l))
|
|
5647
|
+
);
|
|
5648
|
+
files.push({
|
|
5649
|
+
relativePath: filePath,
|
|
5650
|
+
changeType,
|
|
5651
|
+
linesAdded: added,
|
|
5652
|
+
linesRemoved: removed,
|
|
5653
|
+
riskScore: af?.riskScore ?? 0,
|
|
5654
|
+
kind: af?.kind ?? "unknown",
|
|
5655
|
+
hunks,
|
|
5656
|
+
hasExportChanges,
|
|
5657
|
+
hasTypeChanges
|
|
5658
|
+
});
|
|
4945
5659
|
}
|
|
4946
|
-
|
|
4947
|
-
|
|
5660
|
+
const workingNumstat = await git(["diff", "--numstat"], projectPath);
|
|
5661
|
+
for (const line of workingNumstat.split("\n")) {
|
|
5662
|
+
const parts = line.trim().split(" ");
|
|
5663
|
+
if (parts.length < 3) continue;
|
|
5664
|
+
const filePath = parts[2];
|
|
5665
|
+
if (!filePath || seen.has(filePath)) continue;
|
|
5666
|
+
seen.add(filePath);
|
|
5667
|
+
const af = fileMap.get(filePath);
|
|
5668
|
+
const added = parts[0] === "-" ? 0 : parseInt(parts[0], 10) || 0;
|
|
5669
|
+
const removed = parts[1] === "-" ? 0 : parseInt(parts[1], 10) || 0;
|
|
5670
|
+
files.push({
|
|
5671
|
+
relativePath: filePath,
|
|
5672
|
+
changeType: "modified",
|
|
5673
|
+
linesAdded: added,
|
|
5674
|
+
linesRemoved: removed,
|
|
5675
|
+
riskScore: af?.riskScore ?? 0,
|
|
5676
|
+
kind: af?.kind ?? "unknown",
|
|
5677
|
+
hunks: [],
|
|
5678
|
+
hasExportChanges: false,
|
|
5679
|
+
hasTypeChanges: false
|
|
5680
|
+
});
|
|
4948
5681
|
}
|
|
4949
|
-
|
|
4950
|
-
|
|
4951
|
-
|
|
4952
|
-
|
|
4953
|
-
|
|
4954
|
-
|
|
4955
|
-
|
|
4956
|
-
|
|
5682
|
+
return files.sort((a, b) => b.riskScore - a.riskScore);
|
|
5683
|
+
}
|
|
5684
|
+
async function parseDiffHunks(projectPath, baseBranch, filePath) {
|
|
5685
|
+
const diff = await git(["diff", "-U3", `${baseBranch}...HEAD`, "--", filePath], projectPath);
|
|
5686
|
+
if (!diff) return [];
|
|
5687
|
+
const hunks = [];
|
|
5688
|
+
const lines = diff.split("\n");
|
|
5689
|
+
let currentHunk = null;
|
|
5690
|
+
for (const line of lines) {
|
|
5691
|
+
const hunkMatch = line.match(/^@@\s+-(\d+)(?:,\d+)?\s+\+(\d+)(?:,\d+)?\s+@@\s*(.*)/);
|
|
5692
|
+
if (hunkMatch) {
|
|
5693
|
+
if (currentHunk) hunks.push(currentHunk);
|
|
5694
|
+
currentHunk = {
|
|
5695
|
+
startLine: parseInt(hunkMatch[2], 10),
|
|
5696
|
+
endLine: parseInt(hunkMatch[2], 10),
|
|
5697
|
+
header: hunkMatch[3] || "",
|
|
5698
|
+
additions: [],
|
|
5699
|
+
deletions: []
|
|
5700
|
+
};
|
|
5701
|
+
continue;
|
|
5702
|
+
}
|
|
5703
|
+
if (!currentHunk) continue;
|
|
5704
|
+
if (line.startsWith("+") && !line.startsWith("+++")) {
|
|
5705
|
+
currentHunk.additions.push(line.substring(1));
|
|
5706
|
+
currentHunk.endLine++;
|
|
5707
|
+
} else if (line.startsWith("-") && !line.startsWith("---")) {
|
|
5708
|
+
currentHunk.deletions.push(line.substring(1));
|
|
5709
|
+
} else if (!line.startsWith("\\")) {
|
|
5710
|
+
if (currentHunk) currentHunk.endLine++;
|
|
4957
5711
|
}
|
|
4958
|
-
console.log(" \u2551 \u2551");
|
|
4959
5712
|
}
|
|
4960
|
-
|
|
4961
|
-
|
|
4962
|
-
|
|
4963
|
-
|
|
4964
|
-
|
|
4965
|
-
|
|
4966
|
-
|
|
4967
|
-
|
|
4968
|
-
const
|
|
4969
|
-
|
|
4970
|
-
|
|
5713
|
+
if (currentHunk) hunks.push(currentHunk);
|
|
5714
|
+
return hunks;
|
|
5715
|
+
}
|
|
5716
|
+
function detectBreakingChanges(changedFiles, analysis) {
|
|
5717
|
+
const breaks = [];
|
|
5718
|
+
const adj = buildAdjacencyList(analysis.graph.edges);
|
|
5719
|
+
for (const file of changedFiles) {
|
|
5720
|
+
if (file.changeType === "deleted") {
|
|
5721
|
+
const dependents = findDependents(file.relativePath, analysis);
|
|
5722
|
+
if (dependents.length > 0) {
|
|
5723
|
+
breaks.push({
|
|
5724
|
+
file: file.relativePath,
|
|
5725
|
+
type: "export-removed",
|
|
5726
|
+
severity: "critical",
|
|
5727
|
+
description: `File deleted but ${dependents.length} files depend on it`,
|
|
5728
|
+
affectedFiles: dependents
|
|
5729
|
+
});
|
|
5730
|
+
}
|
|
5731
|
+
continue;
|
|
4971
5732
|
}
|
|
4972
|
-
|
|
4973
|
-
|
|
5733
|
+
for (const hunk of file.hunks) {
|
|
5734
|
+
for (const del of hunk.deletions) {
|
|
5735
|
+
const exportMatch = del.match(/^\s*export\s+(function|const|class|type|interface|enum)\s+(\w+)/);
|
|
5736
|
+
if (exportMatch) {
|
|
5737
|
+
const [, kind, name] = exportMatch;
|
|
5738
|
+
const wasReAdded = hunk.additions.some(
|
|
5739
|
+
(a) => new RegExp(`export\\s+${kind}\\s+${name}\\b`).test(a)
|
|
5740
|
+
);
|
|
5741
|
+
if (!wasReAdded) {
|
|
5742
|
+
const dependents = findDependents(file.relativePath, analysis);
|
|
5743
|
+
breaks.push({
|
|
5744
|
+
file: file.relativePath,
|
|
5745
|
+
type: kind === "type" || kind === "interface" ? "type-changed" : "export-removed",
|
|
5746
|
+
severity: dependents.length > 3 ? "critical" : dependents.length > 0 ? "high" : "medium",
|
|
5747
|
+
description: `Removed export ${kind} ${name}`,
|
|
5748
|
+
affectedFiles: dependents
|
|
5749
|
+
});
|
|
5750
|
+
}
|
|
5751
|
+
}
|
|
5752
|
+
const propMatch = del.match(/^\s+(\w+)\s*[?:].*[;,]?\s*$/);
|
|
5753
|
+
if (propMatch && file.hasTypeChanges) {
|
|
5754
|
+
const propName = propMatch[1];
|
|
5755
|
+
const wasReAdded = hunk.additions.some(
|
|
5756
|
+
(a) => new RegExp(`\\b${propName}\\s*[?:]`).test(a)
|
|
5757
|
+
);
|
|
5758
|
+
if (!wasReAdded) {
|
|
5759
|
+
breaks.push({
|
|
5760
|
+
file: file.relativePath,
|
|
5761
|
+
type: "interface-changed",
|
|
5762
|
+
severity: "high",
|
|
5763
|
+
description: `Removed property "${propName}" from type/interface`,
|
|
5764
|
+
affectedFiles: findDependents(file.relativePath, analysis)
|
|
5765
|
+
});
|
|
5766
|
+
}
|
|
5767
|
+
}
|
|
5768
|
+
}
|
|
5769
|
+
for (const add of hunk.additions) {
|
|
5770
|
+
const fnMatch = add.match(/^\s*export\s+(async\s+)?function\s+(\w+)\s*\(([^)]*)\)/);
|
|
5771
|
+
if (fnMatch) {
|
|
5772
|
+
const [, , fnName, newParams] = fnMatch;
|
|
5773
|
+
for (const del of hunk.deletions) {
|
|
5774
|
+
const origMatch = del.match(new RegExp(`export\\s+(async\\s+)?function\\s+${fnName}\\s*\\(([^)]*)\\)`));
|
|
5775
|
+
if (origMatch) {
|
|
5776
|
+
const oldParams = origMatch[2];
|
|
5777
|
+
if (oldParams !== newParams) {
|
|
5778
|
+
const oldCount = oldParams.split(",").filter((p) => p.trim()).length;
|
|
5779
|
+
const newCount = newParams.split(",").filter((p) => p.trim()).length;
|
|
5780
|
+
if (oldCount !== newCount || !paramsCompatible(oldParams, newParams)) {
|
|
5781
|
+
breaks.push({
|
|
5782
|
+
file: file.relativePath,
|
|
5783
|
+
type: "function-signature",
|
|
5784
|
+
severity: "high",
|
|
5785
|
+
description: `Function "${fnName}" signature changed: (${oldParams.trim()}) \u2192 (${newParams.trim()})`,
|
|
5786
|
+
affectedFiles: findDependents(file.relativePath, analysis)
|
|
5787
|
+
});
|
|
5788
|
+
}
|
|
5789
|
+
}
|
|
5790
|
+
}
|
|
5791
|
+
}
|
|
5792
|
+
}
|
|
5793
|
+
}
|
|
5794
|
+
}
|
|
5795
|
+
}
|
|
5796
|
+
return breaks.sort((a, b) => {
|
|
5797
|
+
const sev = { critical: 0, high: 1, medium: 2 };
|
|
5798
|
+
return sev[a.severity] - sev[b.severity];
|
|
5799
|
+
});
|
|
5800
|
+
}
|
|
5801
|
+
function paramsCompatible(oldParams, newParams) {
|
|
5802
|
+
const oldParts = oldParams.split(",").map((p) => p.trim().split(":")[0].trim().replace("?", ""));
|
|
5803
|
+
const newParts = newParams.split(",").map((p) => p.trim().split(":")[0].trim().replace("?", ""));
|
|
5804
|
+
let j = 0;
|
|
5805
|
+
for (let i = 0; i < oldParts.length && j < newParts.length; i++) {
|
|
5806
|
+
if (oldParts[i] === newParts[j]) j++;
|
|
5807
|
+
}
|
|
5808
|
+
return j >= oldParts.length;
|
|
5809
|
+
}
|
|
5810
|
+
function findDependents(filePath, analysis) {
|
|
5811
|
+
return analysis.files.filter((f) => f.imports.includes(filePath) || f.imports.some((imp) => imp.endsWith(filePath))).map((f) => f.relativePath);
|
|
5812
|
+
}
|
|
5813
|
+
function findMissingFiles(changedFiles, analysis, depth) {
|
|
5814
|
+
const missing = [];
|
|
5815
|
+
const changedPaths = new Set(changedFiles.map((f) => f.relativePath));
|
|
5816
|
+
const fileMap = new Map(analysis.files.map((f) => [f.relativePath, f]));
|
|
5817
|
+
for (const changed of changedFiles) {
|
|
5818
|
+
if (changed.changeType === "deleted") continue;
|
|
5819
|
+
const af = fileMap.get(changed.relativePath);
|
|
5820
|
+
if (!af) continue;
|
|
5821
|
+
if (changed.hasTypeChanges || changed.hasExportChanges) {
|
|
5822
|
+
const dir2 = dirname4(changed.relativePath);
|
|
5823
|
+
const base = basename5(changed.relativePath).replace(/\.[^.]+$/, "");
|
|
5824
|
+
const typeVariants = [
|
|
5825
|
+
`${dir2}/${base}.types.ts`,
|
|
5826
|
+
`${dir2}/${base}.types.tsx`,
|
|
5827
|
+
`${dir2}/types.ts`,
|
|
5828
|
+
`${dir2}/types/${base}.ts`,
|
|
5829
|
+
`${dir2}/index.d.ts`
|
|
5830
|
+
];
|
|
5831
|
+
for (const variant of typeVariants) {
|
|
5832
|
+
if (fileMap.has(variant) && !changedPaths.has(variant)) {
|
|
5833
|
+
missing.push({
|
|
5834
|
+
file: variant,
|
|
5835
|
+
reason: `Type file for ${changed.relativePath} \u2014 may need updates`,
|
|
5836
|
+
severity: changed.hasExportChanges ? "high" : "medium",
|
|
5837
|
+
relatedChangedFile: changed.relativePath,
|
|
5838
|
+
relationship: "sibling-type"
|
|
5839
|
+
});
|
|
5840
|
+
}
|
|
5841
|
+
}
|
|
5842
|
+
}
|
|
5843
|
+
const testVariants = [
|
|
5844
|
+
changed.relativePath.replace(/\.([^.]+)$/, ".test.$1"),
|
|
5845
|
+
changed.relativePath.replace(/\.([^.]+)$/, ".spec.$1"),
|
|
5846
|
+
changed.relativePath.replace(/^src\//, "tests/").replace(/\.([^.]+)$/, ".test.$1"),
|
|
5847
|
+
changed.relativePath.replace(/^src\//, "__tests__/").replace(/\.([^.]+)$/, ".test.$1")
|
|
5848
|
+
];
|
|
5849
|
+
for (const testPath of testVariants) {
|
|
5850
|
+
if (fileMap.has(testPath) && !changedPaths.has(testPath)) {
|
|
5851
|
+
missing.push({
|
|
5852
|
+
file: testPath,
|
|
5853
|
+
reason: `Test file for ${changed.relativePath} \u2014 should be updated`,
|
|
5854
|
+
severity: "medium",
|
|
5855
|
+
relatedChangedFile: changed.relativePath,
|
|
5856
|
+
relationship: "test"
|
|
5857
|
+
});
|
|
5858
|
+
break;
|
|
5859
|
+
}
|
|
5860
|
+
}
|
|
5861
|
+
if (changed.hasExportChanges) {
|
|
5862
|
+
const importers = analysis.files.filter(
|
|
5863
|
+
(f) => f.imports.includes(af.relativePath) && !changedPaths.has(f.relativePath)
|
|
5864
|
+
);
|
|
5865
|
+
for (const importer of importers.slice(0, 5)) {
|
|
5866
|
+
missing.push({
|
|
5867
|
+
file: importer.relativePath,
|
|
5868
|
+
reason: `Imports ${changed.relativePath} which has export changes`,
|
|
5869
|
+
severity: "high",
|
|
5870
|
+
relatedChangedFile: changed.relativePath,
|
|
5871
|
+
relationship: "imported-by"
|
|
5872
|
+
});
|
|
5873
|
+
}
|
|
5874
|
+
}
|
|
5875
|
+
const dir = dirname4(changed.relativePath);
|
|
5876
|
+
const indexFile = `${dir}/index.ts`;
|
|
5877
|
+
if (fileMap.has(indexFile) && !changedPaths.has(indexFile) && changed.hasExportChanges) {
|
|
5878
|
+
missing.push({
|
|
5879
|
+
file: indexFile,
|
|
5880
|
+
reason: `Barrel export may need updating after changes to ${changed.relativePath}`,
|
|
5881
|
+
severity: "medium",
|
|
5882
|
+
relatedChangedFile: changed.relativePath,
|
|
5883
|
+
relationship: "co-located"
|
|
5884
|
+
});
|
|
5885
|
+
}
|
|
5886
|
+
}
|
|
5887
|
+
const seen = /* @__PURE__ */ new Set();
|
|
5888
|
+
return missing.filter((m) => {
|
|
5889
|
+
if (seen.has(m.file)) return false;
|
|
5890
|
+
seen.add(m.file);
|
|
5891
|
+
return true;
|
|
5892
|
+
}).sort((a, b) => {
|
|
5893
|
+
const sev = { high: 0, medium: 1, low: 2 };
|
|
5894
|
+
return sev[a.severity] - sev[b.severity];
|
|
5895
|
+
});
|
|
5896
|
+
}
|
|
5897
|
+
function computeImpactRadius(changedFiles, analysis, depth) {
|
|
5898
|
+
const changedPaths = changedFiles.map((f) => f.relativePath);
|
|
5899
|
+
const adj = buildAdjacencyList(analysis.graph.edges);
|
|
5900
|
+
const direct = /* @__PURE__ */ new Set();
|
|
5901
|
+
for (const path of changedPaths) {
|
|
5902
|
+
const importers = adj.reverse.get(path) ?? [];
|
|
5903
|
+
const imports = adj.forward.get(path) ?? [];
|
|
5904
|
+
for (const n of [...importers, ...imports]) {
|
|
5905
|
+
if (!changedPaths.includes(n)) direct.add(n);
|
|
4974
5906
|
}
|
|
4975
5907
|
}
|
|
4976
|
-
|
|
4977
|
-
|
|
4978
|
-
|
|
4979
|
-
|
|
4980
|
-
|
|
4981
|
-
const icon = rec.startsWith("CRITICAL") ? "\u{1F6A8}" : "\u{1F4A1}";
|
|
4982
|
-
console.log(` ${icon} ${rec}`);
|
|
5908
|
+
const allAffected = bfsBidirectional(changedPaths, adj, depth);
|
|
5909
|
+
const transitive = /* @__PURE__ */ new Set();
|
|
5910
|
+
for (const path of allAffected) {
|
|
5911
|
+
if (!changedPaths.includes(path) && !direct.has(path)) {
|
|
5912
|
+
transitive.add(path);
|
|
4983
5913
|
}
|
|
4984
5914
|
}
|
|
4985
|
-
const
|
|
4986
|
-
|
|
4987
|
-
|
|
4988
|
-
|
|
4989
|
-
const
|
|
4990
|
-
|
|
4991
|
-
|
|
4992
|
-
|
|
4993
|
-
|
|
4994
|
-
|
|
4995
|
-
|
|
4996
|
-
|
|
4997
|
-
|
|
4998
|
-
|
|
4999
|
-
|
|
5000
|
-
|
|
5001
|
-
|
|
5002
|
-
|
|
5003
|
-
|
|
5004
|
-
|
|
5005
|
-
|
|
5006
|
-
|
|
5007
|
-
}))
|
|
5915
|
+
const affectedTests = [...allAffected].filter((p) => {
|
|
5916
|
+
const f = analysis.files.find((af) => af.relativePath === p);
|
|
5917
|
+
return f?.kind === "test";
|
|
5918
|
+
}).length;
|
|
5919
|
+
const hotspots = changedFiles.map((f) => ({
|
|
5920
|
+
file: f.relativePath,
|
|
5921
|
+
dependents: adj.reverse.get(f.relativePath)?.length ?? 0,
|
|
5922
|
+
riskScore: f.riskScore
|
|
5923
|
+
})).sort((a, b) => b.dependents * b.riskScore - a.dependents * a.riskScore).slice(0, 5);
|
|
5924
|
+
const totalAffected = direct.size + transitive.size;
|
|
5925
|
+
const maxRisk = Math.max(...changedFiles.map((f) => f.riskScore), 0);
|
|
5926
|
+
const avgRisk = changedFiles.length > 0 ? changedFiles.reduce((s, f) => s + f.riskScore, 0) / changedFiles.length : 0;
|
|
5927
|
+
const riskScore = Math.min(100, Math.round(
|
|
5928
|
+
avgRisk * 0.3 + maxRisk * 0.2 + Math.min(100, totalAffected * 3) * 0.3 + Math.min(100, changedFiles.length * 5) * 0.2
|
|
5929
|
+
));
|
|
5930
|
+
return {
|
|
5931
|
+
directlyAffected: direct.size,
|
|
5932
|
+
transitivelyAffected: transitive.size,
|
|
5933
|
+
totalAffected,
|
|
5934
|
+
affectedTests,
|
|
5935
|
+
riskScore,
|
|
5936
|
+
hotspots
|
|
5008
5937
|
};
|
|
5009
|
-
|
|
5010
|
-
|
|
5011
|
-
|
|
5012
|
-
|
|
5013
|
-
|
|
5014
|
-
|
|
5015
|
-
|
|
5016
|
-
|
|
5017
|
-
|
|
5018
|
-
|
|
5019
|
-
|
|
5020
|
-
|
|
5021
|
-
|
|
5022
|
-
|
|
5023
|
-
|
|
5024
|
-
|
|
5025
|
-
|
|
5026
|
-
|
|
5027
|
-
|
|
5028
|
-
|
|
5029
|
-
|
|
5030
|
-
|
|
5031
|
-
|
|
5032
|
-
|
|
5033
|
-
|
|
5034
|
-
|
|
5035
|
-
|
|
5036
|
-
|
|
5037
|
-
|
|
5038
|
-
|
|
5039
|
-
|
|
5040
|
-
|
|
5041
|
-
|
|
5042
|
-
|
|
5043
|
-
|
|
5044
|
-
|
|
5045
|
-
|
|
5046
|
-
|
|
5938
|
+
}
|
|
5939
|
+
function calculateReviewQuality(changedFiles, breakingChanges, missingFiles, impactRadius, totalLinesChanged) {
|
|
5940
|
+
const factors = [];
|
|
5941
|
+
const sizeScore = totalLinesChanged <= 50 ? 100 : totalLinesChanged <= 200 ? 85 : totalLinesChanged <= 500 ? 65 : totalLinesChanged <= 1e3 ? 40 : 20;
|
|
5942
|
+
factors.push({
|
|
5943
|
+
name: "PR Size",
|
|
5944
|
+
score: sizeScore,
|
|
5945
|
+
weight: 0.25,
|
|
5946
|
+
detail: `${totalLinesChanged} lines changed \u2014 ${sizeScore >= 80 ? "easy" : sizeScore >= 50 ? "manageable" : "too large"} to review`
|
|
5947
|
+
});
|
|
5948
|
+
const focusScore = changedFiles.length <= 3 ? 100 : changedFiles.length <= 8 ? 80 : changedFiles.length <= 15 ? 55 : 25;
|
|
5949
|
+
factors.push({
|
|
5950
|
+
name: "Focus",
|
|
5951
|
+
score: focusScore,
|
|
5952
|
+
weight: 0.2,
|
|
5953
|
+
detail: `${changedFiles.length} files \u2014 ${focusScore >= 80 ? "focused" : focusScore >= 50 ? "moderate scope" : "unfocused"}`
|
|
5954
|
+
});
|
|
5955
|
+
const criticalBreaks = breakingChanges.filter((b) => b.severity === "critical").length;
|
|
5956
|
+
const highBreaks = breakingChanges.filter((b) => b.severity === "high").length;
|
|
5957
|
+
const breakScore = criticalBreaks > 0 ? 10 : highBreaks > 2 ? 30 : highBreaks > 0 ? 60 : breakingChanges.length > 0 ? 80 : 100;
|
|
5958
|
+
factors.push({
|
|
5959
|
+
name: "Breaking Changes",
|
|
5960
|
+
score: breakScore,
|
|
5961
|
+
weight: 0.25,
|
|
5962
|
+
detail: `${breakingChanges.length} breaking changes (${criticalBreaks} critical, ${highBreaks} high)`
|
|
5963
|
+
});
|
|
5964
|
+
const highMissing = missingFiles.filter((m) => m.severity === "high").length;
|
|
5965
|
+
const completenessScore = highMissing > 3 ? 20 : highMissing > 0 ? 50 : missingFiles.length > 3 ? 65 : missingFiles.length > 0 ? 80 : 100;
|
|
5966
|
+
factors.push({
|
|
5967
|
+
name: "Completeness",
|
|
5968
|
+
score: completenessScore,
|
|
5969
|
+
weight: 0.15,
|
|
5970
|
+
detail: `${missingFiles.length} potentially missing files (${highMissing} high priority)`
|
|
5971
|
+
});
|
|
5972
|
+
const radiusScore = impactRadius.totalAffected <= 5 ? 100 : impactRadius.totalAffected <= 15 ? 75 : impactRadius.totalAffected <= 30 ? 50 : 25;
|
|
5973
|
+
factors.push({
|
|
5974
|
+
name: "Blast Radius",
|
|
5975
|
+
score: radiusScore,
|
|
5976
|
+
weight: 0.15,
|
|
5977
|
+
detail: `${impactRadius.totalAffected} files affected (${impactRadius.directlyAffected} direct, ${impactRadius.transitivelyAffected} transitive)`
|
|
5978
|
+
});
|
|
5979
|
+
const overall = Math.round(factors.reduce((s, f) => s + f.score * f.weight, 0));
|
|
5980
|
+
const grade = overall >= 95 ? "A+" : overall >= 90 ? "A" : overall >= 85 ? "A-" : overall >= 80 ? "B+" : overall >= 75 ? "B" : overall >= 70 ? "B-" : overall >= 65 ? "C+" : overall >= 60 ? "C" : overall >= 55 ? "C-" : overall >= 50 ? "D+" : overall >= 45 ? "D" : overall >= 40 ? "D-" : "F";
|
|
5981
|
+
return { score: overall, grade, factors };
|
|
5982
|
+
}
|
|
5983
|
+
async function generateReviewPrompt(changedFiles, breakingChanges, missingFiles, analysis, projectPath, maxFiles) {
|
|
5984
|
+
const lines = [];
|
|
5985
|
+
lines.push("# Code Review Context");
|
|
5986
|
+
lines.push("");
|
|
5987
|
+
lines.push("## Project: " + analysis.projectName);
|
|
5988
|
+
lines.push("## Stack: " + analysis.stack.join(", "));
|
|
5989
|
+
lines.push("");
|
|
5990
|
+
if (breakingChanges.length > 0) {
|
|
5991
|
+
lines.push("## \u26A0\uFE0F BREAKING CHANGES DETECTED");
|
|
5992
|
+
lines.push("");
|
|
5993
|
+
for (const bc of breakingChanges) {
|
|
5994
|
+
lines.push("- **" + bc.severity.toUpperCase() + "** " + bc.file + ": " + bc.description);
|
|
5995
|
+
if (bc.affectedFiles.length > 0) {
|
|
5996
|
+
lines.push(" Affected: " + bc.affectedFiles.slice(0, 5).join(", "));
|
|
5997
|
+
}
|
|
5047
5998
|
}
|
|
5048
|
-
|
|
5999
|
+
lines.push("");
|
|
5049
6000
|
}
|
|
5050
|
-
if (
|
|
5051
|
-
|
|
5052
|
-
|
|
5053
|
-
|
|
5054
|
-
|
|
5055
|
-
report += `- ${rec}
|
|
5056
|
-
`;
|
|
6001
|
+
if (missingFiles.length > 0) {
|
|
6002
|
+
lines.push("## \u{1F4CB} Potentially Missing Files");
|
|
6003
|
+
lines.push("");
|
|
6004
|
+
for (const mf of missingFiles.slice(0, 10)) {
|
|
6005
|
+
lines.push("- " + mf.file + " \u2014 " + mf.reason);
|
|
5057
6006
|
}
|
|
6007
|
+
lines.push("");
|
|
5058
6008
|
}
|
|
5059
|
-
|
|
5060
|
-
|
|
5061
|
-
|
|
5062
|
-
)
|
|
5063
|
-
|
|
5064
|
-
|
|
5065
|
-
|
|
5066
|
-
|
|
5067
|
-
|
|
5068
|
-
|
|
5069
|
-
|
|
5070
|
-
const
|
|
5071
|
-
|
|
6009
|
+
lines.push("## Changed Files");
|
|
6010
|
+
lines.push("");
|
|
6011
|
+
const topFiles = changedFiles.slice(0, maxFiles);
|
|
6012
|
+
for (const file of topFiles) {
|
|
6013
|
+
const icon = file.changeType === "added" ? "\u{1F195}" : file.changeType === "deleted" ? "\u{1F5D1}\uFE0F" : "\u{1F4DD}";
|
|
6014
|
+
lines.push("### " + icon + " " + file.relativePath + " (risk: " + file.riskScore + ", " + file.kind + ")");
|
|
6015
|
+
lines.push("+" + file.linesAdded + "/-" + file.linesRemoved + " lines");
|
|
6016
|
+
lines.push("");
|
|
6017
|
+
if (file.riskScore >= 40 && file.changeType !== "deleted") {
|
|
6018
|
+
try {
|
|
6019
|
+
const content = await readFile10(resolve9(projectPath, file.relativePath), "utf-8");
|
|
6020
|
+
const ext = file.relativePath.split(".").pop() ?? "";
|
|
6021
|
+
const maxChars = 4e3;
|
|
6022
|
+
const truncated = content.length > maxChars;
|
|
6023
|
+
lines.push("```" + ext);
|
|
6024
|
+
lines.push(content.slice(0, maxChars));
|
|
6025
|
+
if (truncated) lines.push("// ... [truncated]");
|
|
6026
|
+
lines.push("```");
|
|
6027
|
+
lines.push("");
|
|
6028
|
+
} catch {
|
|
5072
6029
|
}
|
|
5073
6030
|
}
|
|
5074
|
-
|
|
5075
|
-
|
|
5076
|
-
|
|
5077
|
-
|
|
5078
|
-
|
|
5079
|
-
|
|
5080
|
-
|
|
5081
|
-
|
|
6031
|
+
}
|
|
6032
|
+
lines.push("## Review Instructions");
|
|
6033
|
+
lines.push("");
|
|
6034
|
+
lines.push("1. Check breaking changes above for correctness");
|
|
6035
|
+
lines.push("2. Verify all affected files have been updated");
|
|
6036
|
+
lines.push("3. Review changed files for bugs, security issues, and code quality");
|
|
6037
|
+
lines.push("4. Ensure tests cover the changes");
|
|
6038
|
+
if (missingFiles.length > 0) {
|
|
6039
|
+
lines.push('5. Consider whether the "potentially missing files" need updates');
|
|
6040
|
+
}
|
|
6041
|
+
return lines.join("\n");
|
|
6042
|
+
}
|
|
6043
|
+
function renderReviewSummary(branch, baseBranch, changedFiles, breakingChanges, missingFiles, impactRadius, reviewQuality) {
|
|
6044
|
+
const lines = [];
|
|
6045
|
+
const qIcon = reviewQuality.score >= 80 ? "\u{1F7E2}" : reviewQuality.score >= 60 ? "\u{1F7E1}" : "\u{1F534}";
|
|
6046
|
+
lines.push("");
|
|
6047
|
+
lines.push(" \u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550");
|
|
6048
|
+
lines.push(" " + qIcon + " Code Review: " + reviewQuality.score + "/100 (" + reviewQuality.grade + ")");
|
|
6049
|
+
lines.push(" \u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550");
|
|
6050
|
+
lines.push("");
|
|
6051
|
+
lines.push(" Branch: " + branch + " \u2190 " + baseBranch);
|
|
6052
|
+
lines.push(" Files: " + changedFiles.length + " changed");
|
|
6053
|
+
lines.push(" Lines: +" + changedFiles.reduce((s, f) => s + f.linesAdded, 0) + "/-" + changedFiles.reduce((s, f) => s + f.linesRemoved, 0));
|
|
6054
|
+
lines.push("");
|
|
6055
|
+
for (const f of reviewQuality.factors) {
|
|
6056
|
+
const icon = f.score >= 80 ? "\u2705" : f.score >= 50 ? "\u26A0\uFE0F" : "\u274C";
|
|
6057
|
+
lines.push(" " + icon + " " + f.name + ": " + f.score + "/100 \u2014 " + f.detail);
|
|
6058
|
+
}
|
|
6059
|
+
if (breakingChanges.length > 0) {
|
|
6060
|
+
lines.push("");
|
|
6061
|
+
lines.push(" \u26A0\uFE0F BREAKING CHANGES (" + breakingChanges.length + "):");
|
|
6062
|
+
for (const bc of breakingChanges.slice(0, 5)) {
|
|
6063
|
+
const icon = bc.severity === "critical" ? "\u{1F534}" : bc.severity === "high" ? "\u{1F7E0}" : "\u{1F7E1}";
|
|
6064
|
+
lines.push(" " + icon + " " + bc.file + ": " + bc.description);
|
|
5082
6065
|
}
|
|
5083
6066
|
}
|
|
5084
|
-
|
|
5085
|
-
|
|
5086
|
-
|
|
5087
|
-
|
|
5088
|
-
|
|
5089
|
-
|
|
6067
|
+
if (missingFiles.length > 0) {
|
|
6068
|
+
lines.push("");
|
|
6069
|
+
lines.push(" \u{1F4CB} Potentially missing (" + missingFiles.length + "):");
|
|
6070
|
+
for (const mf of missingFiles.slice(0, 5)) {
|
|
6071
|
+
lines.push(" \u2192 " + mf.file + " (" + mf.reason + ")");
|
|
6072
|
+
}
|
|
5090
6073
|
}
|
|
5091
|
-
|
|
5092
|
-
|
|
5093
|
-
|
|
5094
|
-
|
|
6074
|
+
lines.push("");
|
|
6075
|
+
lines.push(" \u{1F4A5} Impact: " + impactRadius.totalAffected + " files affected (" + impactRadius.directlyAffected + " direct, " + impactRadius.transitivelyAffected + " transitive)");
|
|
6076
|
+
if (impactRadius.affectedTests > 0) {
|
|
6077
|
+
lines.push(" \u{1F9EA} Tests: " + impactRadius.affectedTests + " test files in blast radius");
|
|
6078
|
+
}
|
|
6079
|
+
if (impactRadius.hotspots.length > 0) {
|
|
6080
|
+
lines.push("");
|
|
6081
|
+
lines.push(" \u{1F525} Hotspots:");
|
|
6082
|
+
for (const h of impactRadius.hotspots.slice(0, 3)) {
|
|
6083
|
+
lines.push(" " + h.file + " (risk: " + h.riskScore + ", " + h.dependents + " dependents)");
|
|
6084
|
+
}
|
|
5095
6085
|
}
|
|
6086
|
+
lines.push("");
|
|
6087
|
+
return lines.join("\n");
|
|
5096
6088
|
}
|
|
5097
|
-
function
|
|
5098
|
-
|
|
5099
|
-
|
|
5100
|
-
|
|
6089
|
+
function emptyResult2(baseBranch) {
|
|
6090
|
+
return {
|
|
6091
|
+
branch: "",
|
|
6092
|
+
baseBranch,
|
|
6093
|
+
isGitRepo: false,
|
|
6094
|
+
changedFiles: [],
|
|
6095
|
+
totalLinesChanged: 0,
|
|
6096
|
+
breakingChanges: [],
|
|
6097
|
+
missingFiles: [],
|
|
6098
|
+
impactRadius: {
|
|
6099
|
+
directlyAffected: 0,
|
|
6100
|
+
transitivelyAffected: 0,
|
|
6101
|
+
totalAffected: 0,
|
|
6102
|
+
affectedTests: 0,
|
|
6103
|
+
riskScore: 0,
|
|
6104
|
+
hotspots: []
|
|
6105
|
+
},
|
|
6106
|
+
reviewQuality: {
|
|
6107
|
+
score: 0,
|
|
6108
|
+
grade: "F",
|
|
6109
|
+
factors: []
|
|
6110
|
+
},
|
|
6111
|
+
reviewPrompt: "",
|
|
6112
|
+
renderedSummary: "# Code Review\n\nNot a git repository."
|
|
6113
|
+
};
|
|
5101
6114
|
}
|
|
5102
|
-
|
|
5103
|
-
|
|
6115
|
+
|
|
6116
|
+
// src/cli/commands/review.ts
|
|
6117
|
+
async function runReview(projectPath, analysis, jsonMode) {
|
|
6118
|
+
const result = await analyzeForReview(analysis);
|
|
5104
6119
|
if (jsonMode) {
|
|
5105
|
-
|
|
5106
|
-
|
|
5107
|
-
|
|
5108
|
-
|
|
5109
|
-
|
|
5110
|
-
|
|
6120
|
+
console.log(JSON.stringify({
|
|
6121
|
+
branch: result.branch,
|
|
6122
|
+
baseBranch: result.baseBranch,
|
|
6123
|
+
changedFiles: result.changedFiles.length,
|
|
6124
|
+
totalLinesChanged: result.totalLinesChanged,
|
|
6125
|
+
breakingChanges: result.breakingChanges,
|
|
6126
|
+
missingFiles: result.missingFiles,
|
|
6127
|
+
impactRadius: result.impactRadius,
|
|
6128
|
+
reviewQuality: result.reviewQuality
|
|
6129
|
+
}, null, 2));
|
|
5111
6130
|
return;
|
|
5112
6131
|
}
|
|
5113
|
-
console.log(
|
|
5114
|
-
|
|
5115
|
-
|
|
5116
|
-
|
|
5117
|
-
}
|
|
6132
|
+
console.log(result.renderedSummary);
|
|
6133
|
+
const ctoDir = join13(projectPath, ".cto");
|
|
6134
|
+
mkdirSync6(ctoDir, { recursive: true });
|
|
6135
|
+
writeFileSync6(join13(ctoDir, "review-prompt.md"), result.reviewPrompt);
|
|
6136
|
+
console.log(" \u{1F4CB} Review prompt saved: .cto/review-prompt.md");
|
|
6137
|
+
console.log(" Paste it into Claude/Cursor for an AI-powered code review.");
|
|
6138
|
+
console.log("");
|
|
5118
6139
|
}
|
|
5119
|
-
|
|
5120
|
-
|
|
5121
|
-
|
|
5122
|
-
const
|
|
5123
|
-
await
|
|
5124
|
-
|
|
5125
|
-
|
|
5126
|
-
|
|
5127
|
-
|
|
6140
|
+
|
|
6141
|
+
// src/cli/commands/gateway.ts
|
|
6142
|
+
async function runGateway(args, projectPath) {
|
|
6143
|
+
const { ContextGateway: ContextGateway2 } = await Promise.resolve().then(() => (init_server(), server_exports));
|
|
6144
|
+
const { DEFAULT_GATEWAY_CONFIG: DEFAULT_GATEWAY_CONFIG2 } = await Promise.resolve().then(() => (init_types(), types_exports));
|
|
6145
|
+
const getArg = (flag) => {
|
|
6146
|
+
const idx = args.indexOf(flag);
|
|
6147
|
+
return idx !== -1 && args[idx + 1] ? args[idx + 1] : void 0;
|
|
6148
|
+
};
|
|
6149
|
+
const gwConfig = { projectPath };
|
|
6150
|
+
const port = getArg("--port");
|
|
6151
|
+
if (port) gwConfig.port = parseInt(port, 10);
|
|
6152
|
+
const budgetDaily = getArg("--budget-daily");
|
|
6153
|
+
if (budgetDaily) gwConfig.budgetDaily = parseFloat(budgetDaily);
|
|
6154
|
+
const budgetMonthly = getArg("--budget-monthly");
|
|
6155
|
+
if (budgetMonthly) gwConfig.budgetMonthly = parseFloat(budgetMonthly);
|
|
6156
|
+
const apiKey = getArg("--api-key");
|
|
6157
|
+
if (apiKey) gwConfig.apiKey = apiKey;
|
|
6158
|
+
if (args.includes("--block-secrets")) gwConfig.blockOnSecrets = true;
|
|
6159
|
+
if (args.includes("--no-optimize")) gwConfig.optimize = false;
|
|
6160
|
+
if (args.includes("--no-redact")) gwConfig.redactSecrets = false;
|
|
6161
|
+
if (args.includes("--no-dashboard")) gwConfig.dashboard = false;
|
|
6162
|
+
const finalConfig = { ...DEFAULT_GATEWAY_CONFIG2, ...gwConfig };
|
|
6163
|
+
const gateway = new ContextGateway2(finalConfig);
|
|
6164
|
+
gateway.onEvent((event) => {
|
|
6165
|
+
const ts = (/* @__PURE__ */ new Date()).toLocaleTimeString();
|
|
6166
|
+
switch (event.type) {
|
|
6167
|
+
case "request": {
|
|
6168
|
+
const r = event.record;
|
|
6169
|
+
const saved = r.savedTokens > 0 ? ` (saved ${(r.savedTokens / 1e3).toFixed(1)}K)` : "";
|
|
6170
|
+
const secrets = r.secretsRedacted > 0 ? ` [${r.secretsRedacted} redacted]` : "";
|
|
6171
|
+
console.log(` ${ts} ${r.provider}/${r.model} $${r.costUSD.toFixed(4)}${saved}${secrets} ${r.latencyMs}ms`);
|
|
6172
|
+
break;
|
|
6173
|
+
}
|
|
6174
|
+
case "budget-alert":
|
|
6175
|
+
console.log(` \u26A0\uFE0F ${ts} Budget alert: $${event.current.toFixed(2)}/$${event.limit.toFixed(2)} (${event.period})`);
|
|
6176
|
+
break;
|
|
6177
|
+
case "budget-exceeded":
|
|
6178
|
+
console.log(` \u{1F534} ${ts} Budget EXCEEDED: $${event.current.toFixed(2)}/$${event.limit.toFixed(2)} (${event.period})`);
|
|
6179
|
+
break;
|
|
6180
|
+
case "error":
|
|
6181
|
+
console.log(` \u274C ${ts} ${event.message}`);
|
|
6182
|
+
break;
|
|
6183
|
+
}
|
|
6184
|
+
});
|
|
6185
|
+
console.log("");
|
|
6186
|
+
console.log(" \u26A1 CTO Context Gateway v5.0.0");
|
|
6187
|
+
console.log("");
|
|
6188
|
+
await gateway.start();
|
|
6189
|
+
console.log(` \u{1F310} Proxy: http://${finalConfig.host}:${finalConfig.port}`);
|
|
6190
|
+
if (finalConfig.dashboard) {
|
|
6191
|
+
console.log(` \u{1F4CA} Dashboard: http://${finalConfig.host}:${finalConfig.port}${finalConfig.dashboardPath}`);
|
|
5128
6192
|
}
|
|
6193
|
+
console.log(` \u{1F4C1} Project: ${finalConfig.projectPath}`);
|
|
5129
6194
|
console.log("");
|
|
5130
|
-
console.log("
|
|
5131
|
-
console.log(` \u{1F6A6} Quality Gate: ${result.passed ? "\u2705 PASSED" : "\u274C FAILED"}`);
|
|
5132
|
-
console.log(" \u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550");
|
|
6195
|
+
console.log(" Waiting for requests... (Ctrl+C to stop)");
|
|
5133
6196
|
console.log("");
|
|
5134
|
-
|
|
5135
|
-
|
|
5136
|
-
|
|
6197
|
+
await new Promise(() => {
|
|
6198
|
+
});
|
|
6199
|
+
}
|
|
6200
|
+
|
|
6201
|
+
// src/cli/score.ts
|
|
6202
|
+
var log4 = createLogger("cli");
|
|
6203
|
+
function parseArgs(argv) {
|
|
6204
|
+
const args = argv.slice(2);
|
|
6205
|
+
const has = (flag) => args.includes(flag);
|
|
6206
|
+
const getVal = (flag, fallback) => {
|
|
6207
|
+
const idx = args.indexOf(flag);
|
|
6208
|
+
return idx !== -1 && args[idx + 1] ? args[idx + 1] : fallback;
|
|
6209
|
+
};
|
|
6210
|
+
const getInt = (flag, fallback) => {
|
|
6211
|
+
const v = getVal(flag);
|
|
6212
|
+
return v ? parseInt(v, 10) : fallback;
|
|
6213
|
+
};
|
|
6214
|
+
const contextTask = getVal("--context") ?? null;
|
|
6215
|
+
const targetPackage = getVal("--package") ?? null;
|
|
6216
|
+
const pathArg = args.find((a) => !a.startsWith("--") && !a.startsWith("-") && a !== contextTask && a !== targetPackage);
|
|
6217
|
+
return {
|
|
6218
|
+
args,
|
|
6219
|
+
projectPath: resolve11(pathArg ?? "."),
|
|
6220
|
+
jsonMode: has("--json"),
|
|
6221
|
+
benchmarkMode: has("--benchmark"),
|
|
6222
|
+
fixMode: has("--fix"),
|
|
6223
|
+
reportMode: has("--report"),
|
|
6224
|
+
compareMode: has("--compare"),
|
|
6225
|
+
auditMode: has("--audit"),
|
|
6226
|
+
initHookMode: has("--init-hook"),
|
|
6227
|
+
fullScanMode: has("--full-scan"),
|
|
6228
|
+
noAllowlistMode: has("--no-allowlist"),
|
|
6229
|
+
monorepoMode: has("--monorepo"),
|
|
6230
|
+
gatewayMode: has("--gateway"),
|
|
6231
|
+
ciMode: has("--ci"),
|
|
6232
|
+
learnMode: has("--learn") || has("--feedback"),
|
|
6233
|
+
predictMode: has("--predict"),
|
|
6234
|
+
reviewMode: has("--review"),
|
|
6235
|
+
helpMode: has("--help") || has("-h"),
|
|
6236
|
+
threshold: getInt("--threshold", 70),
|
|
6237
|
+
contextTask,
|
|
6238
|
+
targetPackage
|
|
6239
|
+
};
|
|
6240
|
+
}
|
|
6241
|
+
function showHelp() {
|
|
6242
|
+
console.log(`
|
|
6243
|
+
\u26A1 cto-ai-cli \u2014 How AI-ready is your codebase?
|
|
6244
|
+
|
|
6245
|
+
Usage:
|
|
6246
|
+
npx cto-ai-cli [path] Score a project
|
|
6247
|
+
npx cto-ai-cli --fix Auto-generate optimized context
|
|
6248
|
+
npx cto-ai-cli --context "task" Task-specific context
|
|
6249
|
+
npx cto-ai-cli --audit Security audit
|
|
6250
|
+
npx cto-ai-cli --review PR review analysis
|
|
6251
|
+
npx cto-ai-cli --ci --threshold 80 CI quality gate
|
|
6252
|
+
npx cto-ai-cli --monorepo Monorepo analysis
|
|
6253
|
+
npx cto-ai-cli --gateway Start AI proxy
|
|
6254
|
+
npx cto-ai-cli --learn Learning mode
|
|
6255
|
+
npx cto-ai-cli --predict File predictions
|
|
6256
|
+
npx cto-ai-cli --benchmark CTO vs naive comparison
|
|
6257
|
+
npx cto-ai-cli --report Markdown report + badge
|
|
6258
|
+
npx cto-ai-cli --compare Compare vs popular projects
|
|
6259
|
+
npx cto-ai-cli --json JSON output
|
|
6260
|
+
|
|
6261
|
+
No data leaves your machine. No API keys. MIT licensed.
|
|
6262
|
+
https://npmjs.com/package/cto-ai-cli
|
|
6263
|
+
`);
|
|
6264
|
+
}
|
|
6265
|
+
async function main() {
|
|
6266
|
+
const flags = parseArgs(process.argv);
|
|
6267
|
+
if (flags.helpMode) {
|
|
6268
|
+
showHelp();
|
|
6269
|
+
process.exit(0);
|
|
5137
6270
|
}
|
|
5138
|
-
if (
|
|
5139
|
-
|
|
5140
|
-
|
|
5141
|
-
console.log(` \u{1F4CA} Score: ${result.score}/100 (${result.grade}) ${arrow} ${Math.abs(result.delta)} from baseline`);
|
|
6271
|
+
if (flags.gatewayMode) {
|
|
6272
|
+
await runGateway(flags.args, flags.projectPath);
|
|
6273
|
+
return;
|
|
5142
6274
|
}
|
|
5143
6275
|
console.log("");
|
|
5144
|
-
console.log(" \
|
|
6276
|
+
console.log(" \u26A1 cto-score \u2014 analyzing your project...");
|
|
5145
6277
|
console.log("");
|
|
5146
|
-
|
|
5147
|
-
|
|
6278
|
+
try {
|
|
6279
|
+
const startTime = Date.now();
|
|
6280
|
+
const analysis = await analyzeProject(flags.projectPath);
|
|
6281
|
+
const score = await computeContextScore(analysis);
|
|
6282
|
+
const elapsed = ((Date.now() - startTime) / 1e3).toFixed(1);
|
|
6283
|
+
log4.debug("Analysis complete", { files: analysis.totalFiles, elapsed });
|
|
6284
|
+
const hasModeFlag = flags.learnMode || flags.predictMode || flags.reviewMode || flags.ciMode;
|
|
6285
|
+
if (flags.jsonMode && !hasModeFlag) {
|
|
6286
|
+
console.log(JSON.stringify({
|
|
6287
|
+
project: analysis.projectName,
|
|
6288
|
+
files: analysis.totalFiles,
|
|
6289
|
+
tokens: analysis.totalTokens,
|
|
6290
|
+
score: score.overall,
|
|
6291
|
+
grade: score.grade,
|
|
6292
|
+
dimensions: {
|
|
6293
|
+
efficiency: score.dimensions.efficiency.score,
|
|
6294
|
+
coverage: score.dimensions.coverage.score,
|
|
6295
|
+
riskControl: score.dimensions.riskControl.score,
|
|
6296
|
+
structure: score.dimensions.structure.score,
|
|
6297
|
+
governance: score.dimensions.governance.score
|
|
6298
|
+
},
|
|
6299
|
+
savings: {
|
|
6300
|
+
percent: score.comparison.savedPercent,
|
|
6301
|
+
monthlyUSD: score.comparison.monthlySavingsUSD,
|
|
6302
|
+
tokensOptimized: score.comparison.optimizedTokens,
|
|
6303
|
+
tokensNaive: score.comparison.naiveTokens
|
|
6304
|
+
}
|
|
6305
|
+
}, null, 2));
|
|
6306
|
+
process.exit(0);
|
|
6307
|
+
}
|
|
6308
|
+
if (!(flags.jsonMode && hasModeFlag)) {
|
|
6309
|
+
console.log(renderContextScore(score));
|
|
6310
|
+
}
|
|
6311
|
+
if (flags.benchmarkMode) {
|
|
6312
|
+
const b = await runBenchmark(analysis);
|
|
6313
|
+
console.log(renderBenchmark(b));
|
|
6314
|
+
}
|
|
6315
|
+
if (flags.fixMode) await runFix(flags.projectPath, analysis, score);
|
|
6316
|
+
if (flags.contextTask) await runContext(flags.projectPath, analysis, flags.contextTask);
|
|
6317
|
+
if (flags.reportMode) await runReport(flags.projectPath, analysis, score);
|
|
6318
|
+
if (flags.compareMode) runCompare(score);
|
|
6319
|
+
if (flags.auditMode) await runAudit(flags.projectPath, analysis, { initHookMode: flags.initHookMode, fullScanMode: flags.fullScanMode, noAllowlistMode: flags.noAllowlistMode });
|
|
6320
|
+
if (flags.monorepoMode) await runMonorepo(flags.projectPath, analysis, flags.targetPackage, flags.jsonMode);
|
|
6321
|
+
if (flags.ciMode) await runCIGate(flags.projectPath, analysis, score, flags.threshold, flags.jsonMode);
|
|
6322
|
+
if (flags.learnMode) await runLearn(flags.projectPath, analysis, flags.jsonMode);
|
|
6323
|
+
if (flags.predictMode) await runPredict(flags.projectPath, analysis, flags.contextTask ?? "general code review", flags.jsonMode);
|
|
6324
|
+
if (flags.reviewMode) await runReview(flags.projectPath, analysis, flags.jsonMode);
|
|
6325
|
+
if (flags.jsonMode && hasModeFlag) {
|
|
6326
|
+
process.exit(0);
|
|
6327
|
+
}
|
|
6328
|
+
console.log("");
|
|
6329
|
+
console.log(` Scanned in ${elapsed}s \xB7 ${analysis.totalFiles} files \xB7 ${Math.round(analysis.totalTokens / 1e3)}K tokens`);
|
|
6330
|
+
console.log("");
|
|
6331
|
+
console.log(" What does this mean?");
|
|
6332
|
+
console.log(` Your project scores ${score.overall}/100 (${score.grade}) for AI context efficiency.`);
|
|
6333
|
+
if (score.overall >= 80) console.log(" \u2705 Great! AI tools can work effectively with your codebase.");
|
|
6334
|
+
else if (score.overall >= 60) console.log(" \u{1F7E1} Good, but there's room to improve. Run with --fix to auto-optimize.");
|
|
6335
|
+
else console.log(" \u{1F534} AI tools are likely wasting tokens on your project. Run with --fix.");
|
|
6336
|
+
console.log("");
|
|
6337
|
+
console.log(" Next steps:");
|
|
6338
|
+
console.log(" npx cto-ai-cli --fix Auto-generate optimized context");
|
|
6339
|
+
console.log(' npx cto-ai-cli --context "your task" Task-specific context for Claude/Cursor');
|
|
6340
|
+
console.log(" npx cto-ai-cli --report Shareable report + badge");
|
|
6341
|
+
console.log("");
|
|
6342
|
+
} catch (err) {
|
|
6343
|
+
const message = err instanceof CtoError ? `[${err.code}] ${err.message}` : err instanceof Error ? err.message : String(err);
|
|
6344
|
+
log4.error("CLI failed", { error: message });
|
|
6345
|
+
console.error(` \u274C Error: ${message}`);
|
|
6346
|
+
console.error("");
|
|
6347
|
+
console.error(" Make sure you're running this in a project directory with source files.");
|
|
6348
|
+
console.error(" Supported: TypeScript, JavaScript, Python, Go, Rust, Java, C/C++");
|
|
5148
6349
|
process.exit(1);
|
|
5149
6350
|
}
|
|
5150
6351
|
}
|