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/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 files = [];
625
- for (const entry of walkEntries) {
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
- files.push({
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
- let project;
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 extractSignaturesAST(sf) {
1385
+ function extractSignaturesRegex(content) {
1386
+ const lines = content.split("\n");
1401
1387
  const parts = [];
1402
- for (const imp of sf.getImportDeclarations()) {
1403
- parts.push(imp.getText());
1404
- }
1405
- if (parts.length > 0) parts.push("");
1406
- for (const ta of sf.getTypeAliases()) {
1407
- addJSDoc(ta, parts);
1408
- parts.push(ta.getText());
1409
- }
1410
- for (const iface of sf.getInterfaces()) {
1411
- addJSDoc(iface, parts);
1412
- parts.push(iface.getText());
1413
- }
1414
- for (const en of sf.getEnums()) {
1415
- addJSDoc(en, parts);
1416
- parts.push(en.getText());
1417
- }
1418
- for (const fn of sf.getFunctions()) {
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
- for (const cls of sf.getClasses()) {
1449
- addJSDoc(cls, parts);
1450
- const isExported = cls.isExported();
1451
- const prefix = isExported ? "export " : "";
1452
- const name = cls.getName() ?? "<anonymous>";
1453
- const ext = cls.getExtends()?.getText();
1454
- const impl = cls.getImplements().map((i) => i.getText()).join(", ");
1455
- let header = `${prefix}class ${name}`;
1456
- if (ext) header += ` extends ${ext}`;
1457
- if (impl) header += ` implements ${impl}`;
1458
- header += " {";
1459
- parts.push(header);
1460
- for (const prop of cls.getProperties()) {
1461
- parts.push(` ${prop.getText()}`);
1462
- }
1463
- const ctor = cls.getConstructors()[0];
1464
- if (ctor) {
1465
- const ctorParams = ctor.getParameters().map((p) => p.getText()).join(", ");
1466
- parts.push(` constructor(${ctorParams}) { /* ... */ }`);
1467
- }
1468
- for (const method of cls.getMethods()) {
1469
- const isStatic = method.isStatic();
1470
- const isAsync = method.isAsync();
1471
- const methodName = method.getName();
1472
- const methodParams = method.getParameters().map((p) => p.getText()).join(", ");
1473
- const returnType = method.getReturnTypeNode()?.getText();
1474
- const returnStr = returnType ? `: ${returnType}` : "";
1475
- const staticStr = isStatic ? "static " : "";
1476
- const asyncStr = isAsync ? "async " : "";
1477
- parts.push(` ${staticStr}${asyncStr}${methodName}(${methodParams})${returnStr} { /* ... */ }`);
1478
- }
1479
- parts.push("}");
1480
- }
1481
- for (const exp of sf.getExportDeclarations()) {
1482
- parts.push(exp.getText());
1483
- }
1484
- for (const exp of sf.getExportAssignments()) {
1485
- parts.push(exp.getText());
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 extractSkeletonAST(sf) {
1476
+ function extractSkeletonRegex(content) {
1477
+ const lines = content.split("\n");
1490
1478
  const parts = [];
1491
- for (const imp of sf.getImportDeclarations()) {
1492
- parts.push(imp.getText());
1493
- }
1494
- if (parts.length > 0) parts.push("");
1495
- for (const ta of sf.getTypeAliases()) {
1496
- if (ta.isExported()) parts.push(ta.getText());
1497
- }
1498
- for (const iface of sf.getInterfaces()) {
1499
- if (!iface.isExported()) continue;
1500
- const ext = iface.getExtends().map((e) => e.getText());
1501
- const extStr = ext.length > 0 ? ` extends ${ext.join(", ")}` : "";
1502
- parts.push(`export interface ${iface.getName()}${extStr} { /* ${iface.getProperties().length} props */ }`);
1503
- }
1504
- for (const en of sf.getEnums()) {
1505
- if (!en.isExported()) continue;
1506
- const members = en.getMembers().map((m) => m.getName());
1507
- parts.push(`export enum ${en.getName()} { ${members.join(", ")} }`);
1508
- }
1509
- for (const fn of sf.getFunctions()) {
1510
- if (!fn.isExported()) continue;
1511
- const name = fn.getName() ?? "<anonymous>";
1512
- const params = fn.getParameters().map((p) => p.getText()).join(", ");
1513
- parts.push(`export function ${name}(${params});`);
1514
- }
1515
- for (const cls of sf.getClasses()) {
1516
- if (!cls.isExported()) continue;
1517
- const methods = cls.getMethods().map((m) => m.getName());
1518
- parts.push(`export class ${cls.getName()} { /* methods: ${methods.join(", ")} */ }`);
1519
- }
1520
- for (const exp of sf.getExportDeclarations()) {
1521
- parts.push(exp.getText());
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 messages = (body.messages || []).map((m) => ({
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, streaming) {
2061
- if (streaming) {
2062
- return {
2063
- model: body.model || "unknown",
2064
- inputTokens: body.usage?.prompt_tokens || 0,
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: body.usage?.prompt_tokens || 0,
2073
- outputTokens: body.usage?.completion_tokens || 0,
2074
- content: body.choices?.[0]?.message?.content || "",
2075
- finishReason: body.choices?.[0]?.finish_reason || "stop"
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
- for (const m of body.messages || []) {
2084
- messages.push({
2085
- role: m.role || "user",
2086
- content: typeof m.content === "string" ? m.content : m.content?.map((b) => b.text || "").join("\n") || ""
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: body.usage?.input_tokens || 0,
2101
- outputTokens: body.usage?.output_tokens || 0,
2102
- content: body.content?.map((b) => b.text || "").join("\n") || "",
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
- if (body.systemInstruction?.parts) {
2225
+ const sysInst = body.systemInstruction;
2226
+ if (sysInst && Array.isArray(sysInst.parts)) {
2109
2227
  messages.push({
2110
2228
  role: "system",
2111
- content: body.systemInstruction.parts.map((p) => p.text || "").join("\n")
2229
+ content: sysInst.parts.map((p) => String(p.text ?? "")).join("\n")
2112
2230
  });
2113
2231
  }
2114
- for (const item of body.contents || []) {
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 content = item.parts?.map((p) => p.text || "").join("\n") || "";
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: body.generationConfig?.maxOutputTokens,
2125
- temperature: body.generationConfig?.temperature
2245
+ maxTokens: genConfig?.maxOutputTokens,
2246
+ temperature: genConfig?.temperature
2126
2247
  };
2127
2248
  }
2128
2249
  function parseGoogleResponse(body, _streaming) {
2129
- const candidate = body.candidates?.[0];
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: body.usageMetadata?.promptTokenCount || 0,
2133
- outputTokens: body.usageMetadata?.candidatesTokenCount || 0,
2134
- content: candidate?.content?.parts?.map((p) => p.text || "").join("\n") || "",
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 readFileSync2 } from "fs";
2244
- import { resolve as resolve6 } from "path";
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 = resolve6(config.projectPath, f.relativePath);
2359
- const content = readFileSync2(fullPath, "utf-8");
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
- optimizeDecisions.push(`Context optimization failed: ${err.message}`);
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 mkdirSync2, appendFileSync, readFileSync as readFileSync3, readdirSync, existsSync as existsSync6 } from "fs";
2442
- import { join as join7 } from "path";
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 = join7(config.logDir, "usage");
2459
- mkdirSync2(this.logDir, { recursive: true });
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 = join7(this.logDir, `${monthKey}.jsonl`);
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
- appendFileSync(logFile, line + "\n");
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 = join7(this.logDir, `${monthKey}.jsonl`);
2616
- if (!existsSync6(filePath)) return [];
2617
- return readFileSync3(filePath, "utf-8").split("\n").filter((line) => line.trim()).map((line) => {
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 = join7(this.logDir, `${monthKey}.jsonl`);
2631
- if (!existsSync6(filePath)) return [];
2632
- const records = readFileSync3(filePath, "utf-8").split("\n").filter((line) => line.trim()).map((line) => {
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 (!existsSync6(this.logDir)) return [];
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 = readFileSync3(join7(this.logDir, file), "utf-8");
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((resolve8, reject) => {
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", () => resolve8(Buffer.concat(chunks).toString()));
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((resolve8) => {
3033
+ return new Promise((resolve12) => {
2908
3034
  this.server.listen(this.config.port, this.config.host, () => {
2909
- resolve8();
3035
+ resolve12();
2910
3036
  });
2911
3037
  });
2912
3038
  }
2913
3039
  async stop() {
2914
- return new Promise((resolve8) => {
3040
+ return new Promise((resolve12) => {
2915
3041
  if (this.server) {
2916
- this.server.close(() => resolve8());
3042
+ this.server.close(() => resolve12());
2917
3043
  } else {
2918
- resolve8();
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
- this.emit({ type: "error", message: `Analysis failed: ${err.message}`, error: err });
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 status = err.message === "body-too-large" ? 413 : 400;
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 = err.message === "upstream-timeout" ? 504 : 502;
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: ${err.message}` }));
3212
+ res.end(JSON.stringify({ error: status === 504 ? "Upstream provider timeout" : `Proxy error: ${errMsg}` }));
3084
3213
  }
3085
- this.emit({ type: "error", message: `Proxy error: ${err.message}`, error: err });
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((resolve8, reject) => {
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(resolve8).catch(reject);
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(resolve8).catch(reject);
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((resolve8) => {
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
- resolve8();
3340
+ resolve12();
3212
3341
  });
3213
3342
  proxyRes.on("error", () => {
3214
3343
  clientRes.end();
3215
- resolve8();
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((resolve8) => {
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
- resolve8();
3407
+ resolve12();
3279
3408
  });
3280
3409
  proxyRes.on("error", () => {
3281
3410
  clientRes.end();
3282
- resolve8();
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 resolve7, join as join8 } from "path";
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 pad2 = Math.max(0, width - str.length);
3609
- const left = Math.floor(pad2 / 2);
3610
- return " ".repeat(left) + str + " ".repeat(pad2 - left);
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/cli/score.ts
3773
- init_selector();
3774
- init_secrets();
3775
-
3776
- // src/engine/monorepo.ts
3777
- import { readFile as readFile5, readdir as readdir2 } from "fs/promises";
3778
- import { join as join5, relative as relative4, basename as basename3 } from "path";
3779
- import { existsSync as existsSync4 } from "fs";
3780
- async function detectMonorepoTool(rootPath) {
3781
- const checks = [
3782
- { file: "nx.json", tool: "nx" },
3783
- { file: "turbo.json", tool: "turborepo" },
3784
- { file: "lerna.json", tool: "lerna" },
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
- return "none";
3817
- }
3818
- async function resolveWorkspaceGlobs(rootPath, globs) {
3819
- const packagePaths = [];
3820
- for (const glob of globs) {
3821
- const cleanGlob = glob.replace(/\/?\*\*?$/, "");
3822
- const searchDir = join5(rootPath, cleanGlob);
3823
- if (!existsSync4(searchDir)) continue;
3824
- try {
3825
- const entries = await readdir2(searchDir, { withFileTypes: true });
3826
- for (const entry of entries) {
3827
- if (!entry.isDirectory()) continue;
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
- async function discoverPackages(rootPath, tool) {
3839
- switch (tool) {
3840
- case "npm-workspaces":
3841
- case "yarn-workspaces": {
3842
- const pkgJson = JSON.parse(await readFile5(join5(rootPath, "package.json"), "utf-8"));
3843
- const workspaces = Array.isArray(pkgJson.workspaces) ? pkgJson.workspaces : pkgJson.workspaces?.packages || [];
3844
- return resolveWorkspaceGlobs(rootPath, workspaces);
3845
- }
3846
- case "pnpm-workspaces": {
3847
- const content = await readFile5(join5(rootPath, "pnpm-workspace.yaml"), "utf-8");
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
- function buildCrossPackageEdges(packages, allFiles, graphEdges, rootPath) {
3895
- const fileToPackage = /* @__PURE__ */ new Map();
3896
- for (const pkg of packages) {
3897
- const pkgRel = relative4(rootPath, pkg.path);
3898
- for (const f of allFiles) {
3899
- if (f.relativePath.startsWith(pkgRel + "/") || f.relativePath.startsWith(pkgRel + "\\")) {
3900
- fileToPackage.set(f.relativePath, pkg.name);
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
- const edgeMap = /* @__PURE__ */ new Map();
3905
- for (const edge of graphEdges) {
3906
- const fromPkg = fileToPackage.get(edge.from);
3907
- const toPkg = fileToPackage.get(edge.to);
3908
- if (fromPkg && toPkg && fromPkg !== toPkg) {
3909
- const key = `${fromPkg}\u2192${toPkg}`;
3910
- if (!edgeMap.has(key)) {
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
- return Array.from(edgeMap.entries()).map(([key, val]) => {
3917
- const [from, to] = key.split("\u2192");
3918
- return { from, to, files: val.files.size, type: val.type };
3919
- });
3920
- }
3921
- async function analyzeMonorepo(rootPath, analysis) {
3922
- const tool = await detectMonorepoTool(rootPath);
3923
- if (tool === "none") {
3924
- return {
3925
- detected: false,
3926
- tool: "none",
3927
- rootPath,
3928
- packages: [],
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 += `![CTO Score](https://img.shields.io/badge/CTO_Score-${score.overall}%2F100-${gradeColor}?style=for-the-badge)
4213
+ `;
4214
+ report += `![Grade](https://img.shields.io/badge/Grade-${safeGrade}-${gradeColor}?style=for-the-badge)
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
+ ![CTO Score](https://img.shields.io/badge/CTO_Score-${score.overall}%2F100-${gradeColor})
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(` ![CTO Score](https://img.shields.io/badge/CTO_Score-${score.overall}%2F100-${gradeColor})`);
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 = join5(pkgPath, "package.json");
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 = join5(pkg.path, "package.json");
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 existsSync5 } from "fs";
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 (!existsSync5(filePath)) return null;
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 (!existsSync5(dir)) await mkdir(dir, { recursive: true });
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
- "# Large/binary files",
4626
- "*.lock",
4627
- "package-lock.json",
4628
- "yarn.lock",
5029
+ "### Checks",
4629
5030
  "",
4630
- "# Low-value for AI",
4631
- ...analysis.graph.orphans.filter((o) => {
4632
- const f = analysis.files.find((af) => af.relativePath === o);
4633
- return f && f.riskScore < 20;
4634
- }).slice(0, 20).map((o) => `${o} # orphan, low-risk`),
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
- if (critical.length > 0) {
4681
- contextMd += `## Critical dependencies:
4682
- `;
4683
- for (const f of critical) {
4684
- contextMd += `- ${f.relativePath} \u2014 ${f.reason}
4685
- `;
4686
- }
4687
- contextMd += "\n";
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
- if (includedTypes.length > 0) {
4690
- contextMd += `## Type definitions (needed for correct code generation):
4691
- `;
4692
- for (const f of includedTypes) {
4693
- contextMd += `- ${f.relativePath}
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
- if (high.length > 0) {
4699
- contextMd += `## High-relevance files:
4700
- `;
4701
- for (const f of high) {
4702
- contextMd += `- ${f.relativePath}
4703
- `;
4704
- }
4705
- contextMd += "\n";
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 (rest.length > 0) {
4708
- contextMd += `## Supporting files:
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
- contextMd += `---
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
- } catch {
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(` \u2705 Task context generated!`);
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
- console.log(` \u{1F4CB} .cto/${filename}`);
4752
- console.log(` ${selection.files.length} files \xB7 ${selection.totalTokens.toLocaleString()} tokens`);
4753
- console.log(` Coverage: ${selection.coverage.score}%`);
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(` \u{1F4A1} Copy-paste this file into Claude/Cursor/ChatGPT for`);
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 += `![CTO Score](https://img.shields.io/badge/CTO_Score-${score.overall}%2F100-${gradeColor}?style=for-the-badge)
4767
- `;
4768
- report += `![Grade](https://img.shields.io/badge/Grade-${safeGrade}-${gradeColor}?style=for-the-badge)
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
- report += `| Dimension | Score | Weight | Detail |
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
- if (score.insights.length > 0) {
4815
- report += `## Insights
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
- for (const insight of score.insights.slice(0, 8)) {
4819
- const icon = insight.type === "strength" ? "\u2705" : insight.type === "weakness" ? "\u26A0\uFE0F" : "\u{1F4A1}";
4820
- report += `- ${icon} **${insight.title}** \u2014 ${insight.detail}
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
- report += `## Badge for your README
4826
-
4827
- `;
4828
- report += `\`\`\`markdown
4829
- `;
4830
- report += `![CTO Score](https://img.shields.io/badge/CTO_Score-${score.overall}%2F100-${gradeColor})
4831
- `;
4832
- report += `\`\`\`
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
- report += `---
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
- report += `*Run \`npx cto-ai-cli\` to generate your own report. [Learn more](https://npmjs.com/package/cto-ai-cli)*
4839
- `;
4840
- const ctoDir = join8(projectPath, ".cto");
4841
- mkdirSync3(ctoDir, { recursive: true });
4842
- writeFileSync2(join8(ctoDir, "report.md"), report);
4843
- console.log("");
4844
- console.log(" \u2705 Report generated!");
4845
- console.log("");
4846
- console.log(" \u{1F4CA} .cto/report.md Share on Slack, Discord, or GitHub");
4847
- console.log("");
4848
- console.log(" \u{1F3F7}\uFE0F Badge for your README:");
4849
- console.log(` ![CTO Score](https://img.shields.io/badge/CTO_Score-${score.overall}%2F100-${gradeColor})`);
4850
- console.log("");
4851
- console.log(" Copy-paste this markdown into your README:");
4852
- console.log(` ![CTO Score](https://img.shields.io/badge/CTO_Score-${score.overall}%2F100-${gradeColor})`);
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 runCompare(score) {
4856
- const benchmarks = [
4857
- { name: "Zod", score: 92, grade: "A", files: 441, tokens: "804K" },
4858
- { name: "Prisma Client", score: 88, grade: "A", files: 320, tokens: "650K" },
4859
- { name: "tRPC", score: 85, grade: "A-", files: 280, tokens: "420K" },
4860
- { name: "Next.js (core)", score: 78, grade: "B+", files: 890, tokens: "2.1M" },
4861
- { name: "Express.js", score: 74, grade: "B-", files: 158, tokens: "171K" },
4862
- { name: "Lodash", score: 65, grade: "C+", files: 612, tokens: "380K" }
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{1F4CA} Your project vs popular open source");
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
- const all = [
4870
- { name: `\u2192 ${score.meta.projectName} (you)`, score: score.overall, grade: score.grade, isYou: true },
4871
- ...benchmarks.map((b) => ({ ...b, isYou: false }))
4872
- ].sort((a, b) => b.score - a.score);
4873
- for (const entry of all) {
4874
- const bar = renderCompareBar(entry.score);
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
- const beaten = benchmarks.filter((b) => score.overall > b.score);
4881
- const aheadOf = benchmarks.filter((b) => score.overall < b.score);
4882
- if (beaten.length > 0) {
4883
- console.log(` \u2705 You beat: ${beaten.map((b) => b.name).join(", ")}`);
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
- if (aheadOf.length > 0 && aheadOf.length <= 3) {
4886
- console.log(` \u{1F3AF} To reach ${aheadOf[0].name}'s level: run --fix and address the insights above`);
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
- if (beaten.length === benchmarks.length) {
4889
- console.log(" \u{1F3C6} You outperform ALL benchmarked projects! \u{1F389}");
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
- function renderCompareBar(pct) {
4894
- const width = 25;
4895
- const filled = Math.round(pct / 100 * width);
4896
- const empty = width - filled;
4897
- return "\u2588".repeat(filled) + "\u2591".repeat(empty);
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(" \u{1F50D} Running security audit...");
5549
+ console.log(` ${predictions.length} files predicted as relevant.`);
4914
5550
  console.log("");
4915
- const filePaths = analysis.files.map((f) => f.path);
4916
- const result = await auditProject(projectPath, filePaths, {
4917
- includePII: true,
4918
- incrementalScan: !flags.fullScanMode,
4919
- useAllowlist: !flags.noAllowlistMode
4920
- });
4921
- const { summary, findings, recommendations } = result;
4922
- const statusIcon = summary.bySeverity.critical > 0 ? "\u{1F534}" : summary.bySeverity.high > 0 ? "\u{1F7E0}" : summary.totalFindings > 0 ? "\u{1F7E1}" : "\u{1F7E2}";
4923
- 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";
4924
- 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");
4925
- console.log(" \u2551 \u2551");
4926
- console.log(` \u2551 ${statusIcon} Security Audit: ${statusText.padEnd(28)} \u2551`);
4927
- console.log(" \u2551 \u2551");
4928
- console.log(` \u2551 Files scanned: ${summary.filesScanned.toString().padEnd(30)} \u2551`);
4929
- console.log(` \u2551 Files affected: ${summary.filesWithSecrets.toString().padEnd(30)} \u2551`);
4930
- console.log(` \u2551 Total findings: ${summary.totalFindings.toString().padEnd(30)} \u2551`);
4931
- console.log(" \u2551 \u2551");
4932
- 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");
4933
- console.log(" \u2551 \u2551");
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
- if (summary.bySeverity.high > 0) {
4938
- console.log(` \u2551 \u{1F7E0} High: ${summary.bySeverity.high.toString().padEnd(33)} \u2551`);
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
- if (summary.bySeverity.medium > 0) {
4941
- console.log(` \u2551 \u{1F7E1} Medium: ${summary.bySeverity.medium.toString().padEnd(33)} \u2551`);
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
- if (summary.bySeverity.low > 0) {
4944
- console.log(` \u2551 \u{1F535} Low: ${summary.bySeverity.low.toString().padEnd(33)} \u2551`);
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
- if (summary.totalFindings === 0) {
4947
- console.log(" \u2551 \u2705 No secrets or PII detected \u2551");
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
- console.log(" \u2551 \u2551");
4950
- if (Object.keys(summary.byType).length > 0) {
4951
- 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");
4952
- console.log(" \u2551 \u2551");
4953
- console.log(" \u2551 By type: \u2551");
4954
- for (const [type, count] of Object.entries(summary.byType)) {
4955
- const label = type.padEnd(18);
4956
- console.log(` \u2551 ${label} ${count.toString().padEnd(28)} \u2551`);
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
- 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");
4961
- if (findings.length > 0) {
4962
- console.log("");
4963
- console.log(" Findings:");
4964
- console.log("");
4965
- const shown = findings.slice(0, 15);
4966
- for (const f of shown) {
4967
- const icon = f.severity === "critical" ? "\u{1F534}" : f.severity === "high" ? "\u{1F7E0}" : f.severity === "medium" ? "\u{1F7E1}" : "\u{1F535}";
4968
- const sev = f.severity.toUpperCase().padEnd(8);
4969
- console.log(` ${icon} ${sev} ${f.file}:${f.line}`);
4970
- console.log(` ${f.type}: ${f.redacted}`);
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
- if (findings.length > 15) {
4973
- console.log(` ... and ${findings.length - 15} more (see .cto/audit/ for full report)`);
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
- if (recommendations.length > 0) {
4977
- console.log("");
4978
- console.log(" Recommendations:");
4979
- console.log("");
4980
- for (const rec of recommendations) {
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 ctoDir = join8(projectPath, ".cto");
4986
- const auditDir = join8(ctoDir, "audit");
4987
- mkdirSync3(auditDir, { recursive: true });
4988
- const now = /* @__PURE__ */ new Date();
4989
- const dateStr = now.toISOString().split("T")[0];
4990
- const logFile = join8(auditDir, `${dateStr}.jsonl`);
4991
- const logEntry = {
4992
- timestamp: now.toISOString(),
4993
- version: "3.2.0",
4994
- summary: {
4995
- filesScanned: summary.filesScanned,
4996
- filesWithSecrets: summary.filesWithSecrets,
4997
- totalFindings: summary.totalFindings,
4998
- bySeverity: summary.bySeverity,
4999
- byType: summary.byType
5000
- },
5001
- findings: findings.map((f) => ({
5002
- type: f.type,
5003
- file: f.file,
5004
- line: f.line,
5005
- severity: f.severity,
5006
- redacted: f.redacted
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
- appendFileSync2(logFile, JSON.stringify(logEntry) + "\n");
5010
- let report = `# Security Audit Report
5011
-
5012
- `;
5013
- report += `> Generated by cto-ai-cli on ${now.toISOString()}
5014
-
5015
- `;
5016
- report += `## Summary
5017
-
5018
- `;
5019
- report += `| Metric | Value |
5020
- `;
5021
- report += `|--------|-------|
5022
- `;
5023
- report += `| Files scanned | ${summary.filesScanned} |
5024
- `;
5025
- report += `| Files with issues | ${summary.filesWithSecrets} |
5026
- `;
5027
- report += `| Total findings | ${summary.totalFindings} |
5028
- `;
5029
- report += `| Critical | ${summary.bySeverity.critical} |
5030
- `;
5031
- report += `| High | ${summary.bySeverity.high} |
5032
- `;
5033
- report += `| Medium | ${summary.bySeverity.medium} |
5034
-
5035
- `;
5036
- if (findings.length > 0) {
5037
- report += `## Findings
5038
-
5039
- `;
5040
- report += `| Severity | Type | File | Line | Redacted |
5041
- `;
5042
- report += `|----------|------|------|------|----------|
5043
- `;
5044
- for (const f of findings) {
5045
- report += `| ${f.severity} | ${f.type} | ${f.file} | ${f.line} | \`${f.redacted.slice(0, 30)}\` |
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
- report += "\n";
5999
+ lines.push("");
5049
6000
  }
5050
- if (recommendations.length > 0) {
5051
- report += `## Recommendations
5052
-
5053
- `;
5054
- for (const rec of recommendations) {
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
- writeFileSync2(join8(auditDir, "report.md"), report);
5060
- const envSecrets = findings.filter(
5061
- (f) => f.type === "env-variable" || f.type === "password" || f.type === "api-key" || f.type === "aws-key" || f.type === "connection-string"
5062
- );
5063
- if (envSecrets.length > 0) {
5064
- const envVarNames = /* @__PURE__ */ new Set();
5065
- for (const f of envSecrets) {
5066
- const varMatch = f.match.match(/^([A-Z_][A-Z0-9_]*)\s*[:=]/i);
5067
- if (varMatch) {
5068
- envVarNames.add(varMatch[1].toUpperCase());
5069
- } else {
5070
- const name = f.type.toUpperCase().replace(/-/g, "_");
5071
- envVarNames.add(name);
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
- if (envVarNames.size > 0) {
5075
- let envExample = "# Environment variables \u2014 NEVER commit real values\n";
5076
- envExample += "# Generated by cto-ai-cli --audit\n\n";
5077
- for (const name of envVarNames) {
5078
- envExample += `${name}=your_${name.toLowerCase()}_here
5079
- `;
5080
- }
5081
- writeFileSync2(join8(ctoDir, ".env.example"), envExample);
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
- console.log("");
5085
- console.log(" \u{1F4C1} Audit artifacts:");
5086
- console.log(` \u{1F4CB} .cto/audit/${dateStr}.jsonl Audit log (append-only)`);
5087
- console.log(" \u{1F4CA} .cto/audit/report.md Full report");
5088
- if (envSecrets.length > 0) {
5089
- console.log(" \u{1F4DD} .cto/.env.example Template for environment variables");
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
- console.log("");
5092
- if (process.env.CI && (summary.bySeverity.critical > 0 || summary.bySeverity.high > 0)) {
5093
- console.log(" \u274C CI mode: Failing due to critical/high severity findings.");
5094
- process.exit(1);
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 formatTokens(n) {
5098
- if (n >= 1e6) return `${(n / 1e6).toFixed(1)}M`;
5099
- if (n >= 1e3) return `${(n / 1e3).toFixed(1)}K`;
5100
- return n.toString();
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
- async function runMonorepo(projectPath, analysis, targetPackage, jsonMode) {
5103
- const mono = await analyzeMonorepo(projectPath, analysis);
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
- if (targetPackage) {
5106
- const pkgCtx = selectPackageContext(mono, targetPackage);
5107
- console.log(JSON.stringify({ monorepo: mono, packageContext: pkgCtx }, null, 2));
5108
- } else {
5109
- console.log(JSON.stringify(mono, null, 2));
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(renderMonorepoAnalysis(mono));
5114
- if (targetPackage && mono.detected) {
5115
- const pkgCtx = selectPackageContext(mono, targetPackage);
5116
- console.log(renderPackageContext(pkgCtx));
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
- async function runCIGate(projectPath, analysis, score, threshold, jsonMode) {
5120
- const filePaths = analysis.files.map((f) => f.path);
5121
- const auditResult = await auditProject(projectPath, filePaths, { includePII: false });
5122
- const result = await runQualityGate(score, analysis, auditResult.findings, { threshold });
5123
- await saveBaseline(projectPath, score);
5124
- if (jsonMode) {
5125
- console.log(JSON.stringify(result, null, 2));
5126
- if (!result.passed) process.exit(1);
5127
- return;
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(" \u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550");
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
- for (const check of result.checks) {
5135
- const icon = check.passed ? "\u2705" : check.severity === "warning" ? "\u26A0\uFE0F" : "\u274C";
5136
- console.log(` ${icon} ${check.name}: ${check.detail}`);
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 (result.delta !== null) {
5139
- const arrow = result.delta >= 0 ? "\u2191" : "\u2193";
5140
- console.log("");
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(" \u{1F4CB} Baseline saved to .cto/baseline.json");
6276
+ console.log(" \u26A1 cto-score \u2014 analyzing your project...");
5145
6277
  console.log("");
5146
- if (!result.passed) {
5147
- console.log(" \u274C Quality gate failed. Fix the issues above and re-run.");
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
  }