cto-ai-cli 4.0.0 → 5.1.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/DOCS.md +201 -2
- package/README.md +217 -312
- package/dist/action/index.js +281 -162
- package/dist/api/dashboard.js +281 -162
- package/dist/api/dashboard.js.map +1 -1
- package/dist/api/server.js +362 -184
- package/dist/api/server.js.map +1 -1
- package/dist/cli/gateway.js +358 -229
- package/dist/cli/score.js +2426 -1225
- package/dist/cli/v2/index.js +290 -175
- package/dist/cli/v2/index.js.map +1 -1
- package/dist/engine/index.d.ts +150 -1
- package/dist/engine/index.js +1130 -219
- package/dist/engine/index.js.map +1 -1
- package/dist/fsevents-X6WP4TKM.node +0 -0
- package/dist/gateway/index.d.ts +2 -2
- package/dist/gateway/index.js +358 -229
- package/dist/gateway/index.js.map +1 -1
- package/dist/interact/index.js +263 -148
- package/dist/interact/index.js.map +1 -1
- package/dist/mcp/v2.js +297 -178
- package/dist/mcp/v2.js.map +1 -1
- package/package.json +8 -22
- package/dist/core/index.d.ts +0 -717
- package/dist/core/index.js +0 -4446
- package/dist/core/index.js.map +0 -1
package/dist/api/server.js
CHANGED
|
@@ -592,8 +592,8 @@ async function analyzeProject(projectPath, config) {
|
|
|
592
592
|
maxDepth: mergedConfig.analysis.maxDepth
|
|
593
593
|
});
|
|
594
594
|
const tokenMethod = mergedConfig.tokens.method;
|
|
595
|
-
const
|
|
596
|
-
|
|
595
|
+
const BATCH_SIZE = 50;
|
|
596
|
+
async function estimateFileTokens(entry) {
|
|
597
597
|
let tokens;
|
|
598
598
|
if (tokenMethod === "tiktoken") {
|
|
599
599
|
try {
|
|
@@ -605,7 +605,7 @@ async function analyzeProject(projectPath, config) {
|
|
|
605
605
|
} else {
|
|
606
606
|
tokens = countTokensChars4(entry.size);
|
|
607
607
|
}
|
|
608
|
-
|
|
608
|
+
return {
|
|
609
609
|
path: entry.path,
|
|
610
610
|
relativePath: entry.relativePath,
|
|
611
611
|
extension: entry.extension,
|
|
@@ -614,16 +614,20 @@ async function analyzeProject(projectPath, config) {
|
|
|
614
614
|
lines: entry.lines,
|
|
615
615
|
lastModified: entry.lastModified,
|
|
616
616
|
kind: classifyFileKind(entry.relativePath),
|
|
617
|
-
// Graph data — populated by graph analysis
|
|
618
617
|
imports: [],
|
|
619
618
|
importedBy: [],
|
|
620
619
|
isHub: false,
|
|
621
620
|
complexity: 0,
|
|
622
|
-
// Risk data — populated by risk analysis
|
|
623
621
|
riskScore: 0,
|
|
624
622
|
riskFactors: [],
|
|
625
623
|
exclusionImpact: "none"
|
|
626
|
-
}
|
|
624
|
+
};
|
|
625
|
+
}
|
|
626
|
+
const files = [];
|
|
627
|
+
for (let i = 0; i < walkEntries.length; i += BATCH_SIZE) {
|
|
628
|
+
const batch = walkEntries.slice(i, i + BATCH_SIZE);
|
|
629
|
+
const results = await Promise.all(batch.map(estimateFileTokens));
|
|
630
|
+
files.push(...results);
|
|
627
631
|
}
|
|
628
632
|
const graph = buildProjectGraph(absPath, files);
|
|
629
633
|
for (const file of files) {
|
|
@@ -1005,10 +1009,7 @@ function deduplicateFindings(findings) {
|
|
|
1005
1009
|
}
|
|
1006
1010
|
|
|
1007
1011
|
// src/engine/pruner.ts
|
|
1008
|
-
import { Project as Project2, SyntaxKind as SyntaxKind2 } from "ts-morph";
|
|
1009
1012
|
import { readFile as readFile4 } from "fs/promises";
|
|
1010
|
-
import { existsSync as existsSync3 } from "fs";
|
|
1011
|
-
import { join as join5 } from "path";
|
|
1012
1013
|
var TS_EXTENSIONS2 = /* @__PURE__ */ new Set(["ts", "tsx", "js", "jsx", "mts", "mjs"]);
|
|
1013
1014
|
async function pruneFile(file, level) {
|
|
1014
1015
|
if (level === "excluded") {
|
|
@@ -1031,23 +1032,7 @@ async function pruneTypeScript(file, level) {
|
|
|
1031
1032
|
} catch {
|
|
1032
1033
|
return emptyResult(file, level);
|
|
1033
1034
|
}
|
|
1034
|
-
|
|
1035
|
-
try {
|
|
1036
|
-
const tsConfigPath = findTsConfig(file.path);
|
|
1037
|
-
project = new Project2({
|
|
1038
|
-
tsConfigFilePath: tsConfigPath,
|
|
1039
|
-
skipAddingFilesFromTsConfig: true,
|
|
1040
|
-
compilerOptions: tsConfigPath ? void 0 : { allowJs: true, esModuleInterop: true }
|
|
1041
|
-
});
|
|
1042
|
-
project.createSourceFile(file.path, content, { overwrite: true });
|
|
1043
|
-
} catch {
|
|
1044
|
-
return pruneGenericFromContent(file, content, level);
|
|
1045
|
-
}
|
|
1046
|
-
const sourceFile = project.getSourceFiles()[0];
|
|
1047
|
-
if (!sourceFile) {
|
|
1048
|
-
return pruneGenericFromContent(file, content, level);
|
|
1049
|
-
}
|
|
1050
|
-
const prunedContent = level === "signatures" ? extractSignaturesAST(sourceFile) : extractSkeletonAST(sourceFile);
|
|
1035
|
+
const prunedContent = level === "signatures" ? extractSignaturesRegex(content) : extractSkeletonRegex(content);
|
|
1051
1036
|
const prunedTokens = countTokensChars4(Buffer.byteLength(prunedContent, "utf-8"));
|
|
1052
1037
|
const savingsPercent = file.tokens > 0 ? (file.tokens - prunedTokens) / file.tokens * 100 : 0;
|
|
1053
1038
|
return {
|
|
@@ -1059,131 +1044,281 @@ async function pruneTypeScript(file, level) {
|
|
|
1059
1044
|
savingsPercent: Math.max(0, savingsPercent)
|
|
1060
1045
|
};
|
|
1061
1046
|
}
|
|
1062
|
-
function
|
|
1047
|
+
function extractSignaturesRegex(content) {
|
|
1048
|
+
const lines = content.split("\n");
|
|
1063
1049
|
const parts = [];
|
|
1064
|
-
|
|
1065
|
-
|
|
1066
|
-
|
|
1067
|
-
|
|
1068
|
-
|
|
1069
|
-
|
|
1070
|
-
|
|
1071
|
-
|
|
1072
|
-
|
|
1073
|
-
|
|
1074
|
-
|
|
1075
|
-
|
|
1076
|
-
|
|
1077
|
-
|
|
1078
|
-
|
|
1079
|
-
|
|
1080
|
-
|
|
1081
|
-
|
|
1082
|
-
|
|
1083
|
-
|
|
1084
|
-
|
|
1085
|
-
|
|
1086
|
-
|
|
1087
|
-
|
|
1088
|
-
|
|
1089
|
-
|
|
1090
|
-
|
|
1091
|
-
|
|
1092
|
-
|
|
1093
|
-
|
|
1094
|
-
|
|
1095
|
-
|
|
1096
|
-
|
|
1097
|
-
|
|
1098
|
-
|
|
1099
|
-
|
|
1100
|
-
|
|
1101
|
-
|
|
1102
|
-
|
|
1103
|
-
|
|
1104
|
-
|
|
1105
|
-
|
|
1106
|
-
|
|
1050
|
+
let i = 0;
|
|
1051
|
+
while (i < lines.length) {
|
|
1052
|
+
const line = lines[i];
|
|
1053
|
+
const trimmed = line.trim();
|
|
1054
|
+
if (trimmed === "") {
|
|
1055
|
+
i++;
|
|
1056
|
+
continue;
|
|
1057
|
+
}
|
|
1058
|
+
if (trimmed.startsWith("/**")) {
|
|
1059
|
+
const docLines = [];
|
|
1060
|
+
while (i < lines.length) {
|
|
1061
|
+
docLines.push(lines[i]);
|
|
1062
|
+
if (lines[i].includes("*/")) {
|
|
1063
|
+
i++;
|
|
1064
|
+
break;
|
|
1065
|
+
}
|
|
1066
|
+
i++;
|
|
1067
|
+
}
|
|
1068
|
+
parts.push(docLines.join("\n"));
|
|
1069
|
+
continue;
|
|
1070
|
+
}
|
|
1071
|
+
if (trimmed.startsWith("//")) {
|
|
1072
|
+
parts.push(line);
|
|
1073
|
+
i++;
|
|
1074
|
+
continue;
|
|
1075
|
+
}
|
|
1076
|
+
if (/^\s*(import|export)\s/.test(line) && (trimmed.includes(" from ") || trimmed.startsWith("import "))) {
|
|
1077
|
+
const block = collectBracedLine(lines, i);
|
|
1078
|
+
parts.push(block.text);
|
|
1079
|
+
i = block.nextIndex;
|
|
1080
|
+
continue;
|
|
1081
|
+
}
|
|
1082
|
+
if (/^\s*export\s*(\{|\*)/.test(trimmed)) {
|
|
1083
|
+
const block = collectBracedLine(lines, i);
|
|
1084
|
+
parts.push(block.text);
|
|
1085
|
+
i = block.nextIndex;
|
|
1086
|
+
continue;
|
|
1087
|
+
}
|
|
1088
|
+
if (/^\s*(export\s+)?type\s+\w/.test(trimmed) && !trimmed.startsWith("typeof")) {
|
|
1089
|
+
const block = collectBalanced(lines, i);
|
|
1090
|
+
parts.push(block.text);
|
|
1091
|
+
i = block.nextIndex;
|
|
1092
|
+
continue;
|
|
1093
|
+
}
|
|
1094
|
+
if (/^\s*(export\s+)?interface\s+\w/.test(trimmed)) {
|
|
1095
|
+
const block = collectBalanced(lines, i);
|
|
1096
|
+
parts.push(block.text);
|
|
1097
|
+
i = block.nextIndex;
|
|
1098
|
+
continue;
|
|
1099
|
+
}
|
|
1100
|
+
if (/^\s*(export\s+)?(const\s+)?enum\s+\w/.test(trimmed)) {
|
|
1101
|
+
const block = collectBalanced(lines, i);
|
|
1102
|
+
parts.push(block.text);
|
|
1103
|
+
i = block.nextIndex;
|
|
1104
|
+
continue;
|
|
1105
|
+
}
|
|
1106
|
+
const fnMatch = trimmed.match(/^(export\s+)?(async\s+)?function\s+(\w+)/);
|
|
1107
|
+
if (fnMatch) {
|
|
1108
|
+
const sig = extractFnSignature(lines, i);
|
|
1109
|
+
parts.push(`${sig} { /* ... */ }`);
|
|
1110
|
+
i = skipBlock(lines, i);
|
|
1111
|
+
continue;
|
|
1112
|
+
}
|
|
1113
|
+
const arrowMatch = trimmed.match(/^(export\s+)?(const|let|var)\s+(\w+)/);
|
|
1114
|
+
if (arrowMatch && looksLikeFunctionDecl(lines, i)) {
|
|
1115
|
+
const prefix = trimmed.match(/^((?:export\s+)?(?:const|let|var)\s+\w+[^=]*=)/)?.[1];
|
|
1116
|
+
if (prefix) {
|
|
1117
|
+
parts.push(`${prefix} /* ... */;`);
|
|
1107
1118
|
}
|
|
1119
|
+
i = skipBlock(lines, i);
|
|
1120
|
+
continue;
|
|
1108
1121
|
}
|
|
1109
|
-
|
|
1110
|
-
|
|
1111
|
-
|
|
1112
|
-
|
|
1113
|
-
|
|
1114
|
-
|
|
1115
|
-
|
|
1116
|
-
|
|
1117
|
-
|
|
1118
|
-
|
|
1119
|
-
|
|
1120
|
-
|
|
1121
|
-
|
|
1122
|
-
for (const prop of cls.getProperties()) {
|
|
1123
|
-
parts.push(` ${prop.getText()}`);
|
|
1124
|
-
}
|
|
1125
|
-
const ctor = cls.getConstructors()[0];
|
|
1126
|
-
if (ctor) {
|
|
1127
|
-
const ctorParams = ctor.getParameters().map((p) => p.getText()).join(", ");
|
|
1128
|
-
parts.push(` constructor(${ctorParams}) { /* ... */ }`);
|
|
1129
|
-
}
|
|
1130
|
-
for (const method of cls.getMethods()) {
|
|
1131
|
-
const isStatic = method.isStatic();
|
|
1132
|
-
const isAsync = method.isAsync();
|
|
1133
|
-
const methodName = method.getName();
|
|
1134
|
-
const methodParams = method.getParameters().map((p) => p.getText()).join(", ");
|
|
1135
|
-
const returnType = method.getReturnTypeNode()?.getText();
|
|
1136
|
-
const returnStr = returnType ? `: ${returnType}` : "";
|
|
1137
|
-
const staticStr = isStatic ? "static " : "";
|
|
1138
|
-
const asyncStr = isAsync ? "async " : "";
|
|
1139
|
-
parts.push(` ${staticStr}${asyncStr}${methodName}(${methodParams})${returnStr} { /* ... */ }`);
|
|
1140
|
-
}
|
|
1141
|
-
parts.push("}");
|
|
1142
|
-
}
|
|
1143
|
-
for (const exp of sf.getExportDeclarations()) {
|
|
1144
|
-
parts.push(exp.getText());
|
|
1145
|
-
}
|
|
1146
|
-
for (const exp of sf.getExportAssignments()) {
|
|
1147
|
-
parts.push(exp.getText());
|
|
1122
|
+
if (arrowMatch) {
|
|
1123
|
+
const block = collectStatement(lines, i);
|
|
1124
|
+
parts.push(block.text);
|
|
1125
|
+
i = block.nextIndex;
|
|
1126
|
+
continue;
|
|
1127
|
+
}
|
|
1128
|
+
if (/^\s*(export\s+)?(abstract\s+)?class\s+\w/.test(trimmed)) {
|
|
1129
|
+
const classOutline = extractClassOutline(lines, i);
|
|
1130
|
+
parts.push(classOutline.text);
|
|
1131
|
+
i = classOutline.nextIndex;
|
|
1132
|
+
continue;
|
|
1133
|
+
}
|
|
1134
|
+
i++;
|
|
1148
1135
|
}
|
|
1149
1136
|
return parts.join("\n");
|
|
1150
1137
|
}
|
|
1151
|
-
function
|
|
1138
|
+
function extractSkeletonRegex(content) {
|
|
1139
|
+
const lines = content.split("\n");
|
|
1152
1140
|
const parts = [];
|
|
1153
|
-
|
|
1154
|
-
|
|
1155
|
-
|
|
1156
|
-
|
|
1157
|
-
|
|
1158
|
-
|
|
1159
|
-
|
|
1160
|
-
|
|
1161
|
-
|
|
1162
|
-
|
|
1163
|
-
|
|
1164
|
-
|
|
1165
|
-
|
|
1166
|
-
|
|
1167
|
-
|
|
1168
|
-
|
|
1169
|
-
|
|
1170
|
-
|
|
1171
|
-
|
|
1172
|
-
|
|
1173
|
-
|
|
1174
|
-
|
|
1175
|
-
|
|
1176
|
-
|
|
1177
|
-
|
|
1178
|
-
|
|
1179
|
-
|
|
1180
|
-
|
|
1181
|
-
|
|
1182
|
-
|
|
1183
|
-
|
|
1141
|
+
let i = 0;
|
|
1142
|
+
while (i < lines.length) {
|
|
1143
|
+
const trimmed = lines[i].trim();
|
|
1144
|
+
if (/^import\s/.test(trimmed)) {
|
|
1145
|
+
const block = collectBracedLine(lines, i);
|
|
1146
|
+
parts.push(block.text);
|
|
1147
|
+
i = block.nextIndex;
|
|
1148
|
+
continue;
|
|
1149
|
+
}
|
|
1150
|
+
if (/^export\s+(type|interface)\s+\w/.test(trimmed)) {
|
|
1151
|
+
const block = collectBalanced(lines, i);
|
|
1152
|
+
parts.push(block.text);
|
|
1153
|
+
i = block.nextIndex;
|
|
1154
|
+
continue;
|
|
1155
|
+
}
|
|
1156
|
+
if (/^export\s+(const\s+)?enum\s+\w/.test(trimmed)) {
|
|
1157
|
+
const block = collectBalanced(lines, i);
|
|
1158
|
+
parts.push(block.text);
|
|
1159
|
+
i = block.nextIndex;
|
|
1160
|
+
continue;
|
|
1161
|
+
}
|
|
1162
|
+
if (/^export\s+(async\s+)?function\s+\w/.test(trimmed)) {
|
|
1163
|
+
const sig = extractFnSignature(lines, i);
|
|
1164
|
+
parts.push(`${sig};`);
|
|
1165
|
+
i = skipBlock(lines, i);
|
|
1166
|
+
continue;
|
|
1167
|
+
}
|
|
1168
|
+
if (/^export\s+(abstract\s+)?class\s+/.test(trimmed)) {
|
|
1169
|
+
const nameMatch = trimmed.match(/class\s+(\w+)/);
|
|
1170
|
+
const name = nameMatch?.[1] ?? "Unknown";
|
|
1171
|
+
const end = skipBlock(lines, i);
|
|
1172
|
+
const methods = [];
|
|
1173
|
+
for (let j = i + 1; j < end; j++) {
|
|
1174
|
+
const mt = lines[j].trim();
|
|
1175
|
+
const mm = mt.match(/^(?:static\s+)?(?:async\s+)?(\w+)\s*\(/);
|
|
1176
|
+
if (mm && mm[1] !== "constructor") methods.push(mm[1]);
|
|
1177
|
+
}
|
|
1178
|
+
parts.push(`export class ${name} { /* methods: ${methods.join(", ")} */ }`);
|
|
1179
|
+
i = end;
|
|
1180
|
+
continue;
|
|
1181
|
+
}
|
|
1182
|
+
if (/^export\s*(\{|\*)/.test(trimmed)) {
|
|
1183
|
+
const block = collectBracedLine(lines, i);
|
|
1184
|
+
parts.push(block.text);
|
|
1185
|
+
i = block.nextIndex;
|
|
1186
|
+
continue;
|
|
1187
|
+
}
|
|
1188
|
+
i++;
|
|
1184
1189
|
}
|
|
1185
1190
|
return parts.join("\n");
|
|
1186
1191
|
}
|
|
1192
|
+
function collectBracedLine(lines, start) {
|
|
1193
|
+
let text = lines[start];
|
|
1194
|
+
let i = start + 1;
|
|
1195
|
+
while (i < lines.length && !text.includes(";") && !text.trimEnd().endsWith("'") && !text.trimEnd().endsWith('"')) {
|
|
1196
|
+
text += "\n" + lines[i];
|
|
1197
|
+
i++;
|
|
1198
|
+
}
|
|
1199
|
+
return { text, nextIndex: i };
|
|
1200
|
+
}
|
|
1201
|
+
function collectBalanced(lines, start) {
|
|
1202
|
+
let depth = 0;
|
|
1203
|
+
let text = "";
|
|
1204
|
+
let i = start;
|
|
1205
|
+
let started = false;
|
|
1206
|
+
while (i < lines.length) {
|
|
1207
|
+
const line = lines[i];
|
|
1208
|
+
text += (text ? "\n" : "") + line;
|
|
1209
|
+
for (const ch of line) {
|
|
1210
|
+
if (ch === "{" || ch === "(") {
|
|
1211
|
+
depth++;
|
|
1212
|
+
started = true;
|
|
1213
|
+
}
|
|
1214
|
+
if (ch === "}" || ch === ")") depth--;
|
|
1215
|
+
}
|
|
1216
|
+
i++;
|
|
1217
|
+
if (started && depth <= 0) break;
|
|
1218
|
+
if (!started && line.includes(";")) break;
|
|
1219
|
+
}
|
|
1220
|
+
return { text, nextIndex: i };
|
|
1221
|
+
}
|
|
1222
|
+
function collectStatement(lines, start) {
|
|
1223
|
+
let text = lines[start];
|
|
1224
|
+
let i = start + 1;
|
|
1225
|
+
if (text.includes(";")) return { text, nextIndex: i };
|
|
1226
|
+
let depth = 0;
|
|
1227
|
+
for (const ch of text) {
|
|
1228
|
+
if (ch === "{" || ch === "(" || ch === "[") depth++;
|
|
1229
|
+
if (ch === "}" || ch === ")" || ch === "]") depth--;
|
|
1230
|
+
}
|
|
1231
|
+
while (i < lines.length && depth > 0) {
|
|
1232
|
+
text += "\n" + lines[i];
|
|
1233
|
+
for (const ch of lines[i]) {
|
|
1234
|
+
if (ch === "{" || ch === "(" || ch === "[") depth++;
|
|
1235
|
+
if (ch === "}" || ch === ")" || ch === "]") depth--;
|
|
1236
|
+
}
|
|
1237
|
+
i++;
|
|
1238
|
+
}
|
|
1239
|
+
return { text, nextIndex: i };
|
|
1240
|
+
}
|
|
1241
|
+
function extractFnSignature(lines, start) {
|
|
1242
|
+
let sig = "";
|
|
1243
|
+
let i = start;
|
|
1244
|
+
while (i < lines.length) {
|
|
1245
|
+
const line = lines[i].trim();
|
|
1246
|
+
sig += (sig ? " " : "") + line;
|
|
1247
|
+
if (line.includes("{")) {
|
|
1248
|
+
sig = sig.replace(/\s*\{[^]*$/, "").trim();
|
|
1249
|
+
break;
|
|
1250
|
+
}
|
|
1251
|
+
i++;
|
|
1252
|
+
}
|
|
1253
|
+
return sig;
|
|
1254
|
+
}
|
|
1255
|
+
function skipBlock(lines, start) {
|
|
1256
|
+
let depth = 0;
|
|
1257
|
+
let i = start;
|
|
1258
|
+
let foundBrace = false;
|
|
1259
|
+
while (i < lines.length) {
|
|
1260
|
+
for (const ch of lines[i]) {
|
|
1261
|
+
if (ch === "{") {
|
|
1262
|
+
depth++;
|
|
1263
|
+
foundBrace = true;
|
|
1264
|
+
}
|
|
1265
|
+
if (ch === "}") depth--;
|
|
1266
|
+
}
|
|
1267
|
+
i++;
|
|
1268
|
+
if (foundBrace && depth <= 0) break;
|
|
1269
|
+
if (!foundBrace && lines[i - 1].includes(";")) break;
|
|
1270
|
+
}
|
|
1271
|
+
return i;
|
|
1272
|
+
}
|
|
1273
|
+
function looksLikeFunctionDecl(lines, start) {
|
|
1274
|
+
const chunk = lines.slice(start, Math.min(start + 5, lines.length)).join(" ");
|
|
1275
|
+
return /=>/.test(chunk) || /=\s*function/.test(chunk);
|
|
1276
|
+
}
|
|
1277
|
+
function extractClassOutline(lines, start) {
|
|
1278
|
+
const header = lines[start].trim();
|
|
1279
|
+
let headerText = header;
|
|
1280
|
+
let i = start + 1;
|
|
1281
|
+
if (!header.includes("{")) {
|
|
1282
|
+
while (i < lines.length) {
|
|
1283
|
+
headerText += " " + lines[i].trim();
|
|
1284
|
+
if (lines[i].includes("{")) {
|
|
1285
|
+
i++;
|
|
1286
|
+
break;
|
|
1287
|
+
}
|
|
1288
|
+
i++;
|
|
1289
|
+
}
|
|
1290
|
+
} else {
|
|
1291
|
+
i = start + 1;
|
|
1292
|
+
}
|
|
1293
|
+
const bodyParts = [headerText.replace(/\{[^]*$/, "{").trim()];
|
|
1294
|
+
let depth = 1;
|
|
1295
|
+
while (i < lines.length && depth > 0) {
|
|
1296
|
+
const line = lines[i];
|
|
1297
|
+
const trimmed = line.trim();
|
|
1298
|
+
for (const ch of line) {
|
|
1299
|
+
if (ch === "{") depth++;
|
|
1300
|
+
if (ch === "}") depth--;
|
|
1301
|
+
}
|
|
1302
|
+
if (depth <= 0) {
|
|
1303
|
+
i++;
|
|
1304
|
+
break;
|
|
1305
|
+
}
|
|
1306
|
+
if (depth === 1) {
|
|
1307
|
+
if (/^(private|protected|public|readonly|static|#)/.test(trimmed) && !trimmed.includes("(")) {
|
|
1308
|
+
bodyParts.push(` ${trimmed}`);
|
|
1309
|
+
} else if (/^constructor\s*\(/.test(trimmed)) {
|
|
1310
|
+
const sig = extractFnSignature(lines, i);
|
|
1311
|
+
bodyParts.push(` ${sig} { /* ... */ }`);
|
|
1312
|
+
} else if (/^(?:static\s+)?(?:async\s+)?(?:get\s+|set\s+)?\w+\s*[(<]/.test(trimmed) && !trimmed.startsWith("//")) {
|
|
1313
|
+
const sig = extractFnSignature(lines, i);
|
|
1314
|
+
bodyParts.push(` ${sig} { /* ... */ }`);
|
|
1315
|
+
}
|
|
1316
|
+
}
|
|
1317
|
+
i++;
|
|
1318
|
+
}
|
|
1319
|
+
bodyParts.push("}");
|
|
1320
|
+
return { text: bodyParts.join("\n"), nextIndex: i };
|
|
1321
|
+
}
|
|
1187
1322
|
async function pruneGeneric(file, level) {
|
|
1188
1323
|
let content;
|
|
1189
1324
|
try {
|
|
@@ -1244,22 +1379,6 @@ function emptyResult(file, level) {
|
|
|
1244
1379
|
savingsPercent: 100
|
|
1245
1380
|
};
|
|
1246
1381
|
}
|
|
1247
|
-
function addJSDoc(node, parts) {
|
|
1248
|
-
if (!node.getJsDocs) return;
|
|
1249
|
-
const docs = node.getJsDocs();
|
|
1250
|
-
if (docs.length > 0) {
|
|
1251
|
-
parts.push(docs[0].getText());
|
|
1252
|
-
}
|
|
1253
|
-
}
|
|
1254
|
-
function findTsConfig(filePath) {
|
|
1255
|
-
let dir = filePath;
|
|
1256
|
-
for (let i = 0; i < 10; i++) {
|
|
1257
|
-
dir = join5(dir, "..");
|
|
1258
|
-
const candidate = join5(dir, "tsconfig.json");
|
|
1259
|
-
if (existsSync3(candidate)) return candidate;
|
|
1260
|
-
}
|
|
1261
|
-
return void 0;
|
|
1262
|
-
}
|
|
1263
1382
|
|
|
1264
1383
|
// src/engine/graph-utils.ts
|
|
1265
1384
|
function buildAdjacencyList(edges) {
|
|
@@ -2922,18 +3041,18 @@ function formatCost(cost) {
|
|
|
2922
3041
|
// src/govern/audit.ts
|
|
2923
3042
|
import { randomUUID, createHash as createHash5 } from "crypto";
|
|
2924
3043
|
import { readdir as readdir3, chmod } from "fs/promises";
|
|
2925
|
-
import { join as
|
|
3044
|
+
import { join as join5 } from "path";
|
|
2926
3045
|
import { userInfo } from "os";
|
|
2927
3046
|
import { homedir } from "os";
|
|
2928
3047
|
var CTO_DIR = ".cto-ai";
|
|
2929
3048
|
var AUDIT_DIR = "audit";
|
|
2930
3049
|
var MAX_ENTRIES_PER_FILE = 500;
|
|
2931
3050
|
function getAuditDir() {
|
|
2932
|
-
return
|
|
3051
|
+
return join5(homedir(), CTO_DIR, AUDIT_DIR);
|
|
2933
3052
|
}
|
|
2934
3053
|
function getCurrentAuditFile() {
|
|
2935
3054
|
const date = (/* @__PURE__ */ new Date()).toISOString().split("T")[0].replace(/-/g, "");
|
|
2936
|
-
return
|
|
3055
|
+
return join5(getAuditDir(), `audit_${date}.json`);
|
|
2937
3056
|
}
|
|
2938
3057
|
function computeIntegrityHash(entry) {
|
|
2939
3058
|
const payload = JSON.stringify({
|
|
@@ -2961,7 +3080,7 @@ async function readJSON(filePath) {
|
|
|
2961
3080
|
}
|
|
2962
3081
|
async function writeJSON(filePath, data) {
|
|
2963
3082
|
const { writeFile: writeFile2 } = await import("fs/promises");
|
|
2964
|
-
await ensureDir(
|
|
3083
|
+
await ensureDir(join5(filePath, ".."));
|
|
2965
3084
|
await writeFile2(filePath, JSON.stringify(data, null, 2), "utf-8");
|
|
2966
3085
|
}
|
|
2967
3086
|
async function logAudit(action, projectPath, details = {}) {
|
|
@@ -3111,6 +3230,52 @@ async function planInteraction(input) {
|
|
|
3111
3230
|
// src/api/server.ts
|
|
3112
3231
|
var API_VERSION = "1.0.0";
|
|
3113
3232
|
var DEFAULT_PORT = 3141;
|
|
3233
|
+
var FORBIDDEN_PREFIXES = [
|
|
3234
|
+
"/etc",
|
|
3235
|
+
"/usr",
|
|
3236
|
+
"/var",
|
|
3237
|
+
"/sys",
|
|
3238
|
+
"/proc",
|
|
3239
|
+
"/dev",
|
|
3240
|
+
"/boot",
|
|
3241
|
+
"/sbin",
|
|
3242
|
+
"/bin",
|
|
3243
|
+
"/tmp",
|
|
3244
|
+
"/root",
|
|
3245
|
+
"/lib",
|
|
3246
|
+
"/opt"
|
|
3247
|
+
];
|
|
3248
|
+
var FORBIDDEN_PATTERNS = [".ssh", ".gnupg", ".aws", ".env", "passwd", "shadow"];
|
|
3249
|
+
function getAllowedRoots() {
|
|
3250
|
+
const raw = process.env.CTO_ALLOWED_ROOTS;
|
|
3251
|
+
if (!raw) return null;
|
|
3252
|
+
return raw.split(",").map((r) => resolve7(r.trim()));
|
|
3253
|
+
}
|
|
3254
|
+
function validateProjectPath(projectPath) {
|
|
3255
|
+
if (typeof projectPath !== "string" || !projectPath.trim()) {
|
|
3256
|
+
return { valid: false, reason: "Missing or invalid path field" };
|
|
3257
|
+
}
|
|
3258
|
+
const absPath = resolve7(projectPath);
|
|
3259
|
+
const lower = absPath.toLowerCase();
|
|
3260
|
+
for (const prefix of FORBIDDEN_PREFIXES) {
|
|
3261
|
+
if (lower === prefix || lower.startsWith(prefix + "/")) {
|
|
3262
|
+
return { valid: false, reason: `Access denied: system path ${prefix}` };
|
|
3263
|
+
}
|
|
3264
|
+
}
|
|
3265
|
+
for (const pattern of FORBIDDEN_PATTERNS) {
|
|
3266
|
+
if (lower.includes(pattern)) {
|
|
3267
|
+
return { valid: false, reason: `Access denied: path contains forbidden segment '${pattern}'` };
|
|
3268
|
+
}
|
|
3269
|
+
}
|
|
3270
|
+
const roots = getAllowedRoots();
|
|
3271
|
+
if (roots && roots.length > 0) {
|
|
3272
|
+
const allowed = roots.some((root) => absPath === root || absPath.startsWith(root + "/"));
|
|
3273
|
+
if (!allowed) {
|
|
3274
|
+
return { valid: false, reason: `Access denied: path not under allowed roots. Set CTO_ALLOWED_ROOTS to allow.` };
|
|
3275
|
+
}
|
|
3276
|
+
}
|
|
3277
|
+
return { valid: true, absPath };
|
|
3278
|
+
}
|
|
3114
3279
|
function validateApiKey(req) {
|
|
3115
3280
|
const apiKey = process.env.CTO_API_KEY;
|
|
3116
3281
|
if (!apiKey) return true;
|
|
@@ -3134,6 +3299,12 @@ function checkRateLimit(ip) {
|
|
|
3134
3299
|
entry.count++;
|
|
3135
3300
|
return true;
|
|
3136
3301
|
}
|
|
3302
|
+
setInterval(() => {
|
|
3303
|
+
const now = Date.now();
|
|
3304
|
+
for (const [ip, entry] of requestCounts) {
|
|
3305
|
+
if (now > entry.reset) requestCounts.delete(ip);
|
|
3306
|
+
}
|
|
3307
|
+
}, 5 * 6e4).unref();
|
|
3137
3308
|
function getIP(req) {
|
|
3138
3309
|
return req.headers["x-forwarded-for"]?.split(",")[0]?.trim() ?? req.socket.remoteAddress ?? "unknown";
|
|
3139
3310
|
}
|
|
@@ -3166,9 +3337,9 @@ function error(res, status, message) {
|
|
|
3166
3337
|
json(res, status, { error: message, status });
|
|
3167
3338
|
}
|
|
3168
3339
|
async function handleAnalyze(body, res) {
|
|
3169
|
-
const
|
|
3170
|
-
if (!
|
|
3171
|
-
const absPath =
|
|
3340
|
+
const check = validateProjectPath(body.path);
|
|
3341
|
+
if (!check.valid) return error(res, 403, check.reason);
|
|
3342
|
+
const absPath = check.absPath;
|
|
3172
3343
|
const analysis = await getCachedAnalysis(absPath);
|
|
3173
3344
|
json(res, 200, {
|
|
3174
3345
|
project: analysis.projectName,
|
|
@@ -3186,10 +3357,11 @@ async function handleAnalyze(body, res) {
|
|
|
3186
3357
|
});
|
|
3187
3358
|
}
|
|
3188
3359
|
async function handleSelect(body, res) {
|
|
3189
|
-
const {
|
|
3190
|
-
|
|
3360
|
+
const { task, budget } = body;
|
|
3361
|
+
const check = validateProjectPath(body.path);
|
|
3362
|
+
if (!check.valid) return error(res, 403, check.reason);
|
|
3191
3363
|
if (!task) return error(res, 400, "Missing required field: task");
|
|
3192
|
-
const absPath =
|
|
3364
|
+
const absPath = check.absPath;
|
|
3193
3365
|
const analysis = await getCachedAnalysis(absPath);
|
|
3194
3366
|
const selection = await selectContext({
|
|
3195
3367
|
task,
|
|
@@ -3213,9 +3385,10 @@ async function handleSelect(body, res) {
|
|
|
3213
3385
|
});
|
|
3214
3386
|
}
|
|
3215
3387
|
async function handleScore(body, res) {
|
|
3216
|
-
const {
|
|
3217
|
-
|
|
3218
|
-
|
|
3388
|
+
const { task, budget } = body;
|
|
3389
|
+
const check = validateProjectPath(body.path);
|
|
3390
|
+
if (!check.valid) return error(res, 403, check.reason);
|
|
3391
|
+
const absPath = check.absPath;
|
|
3219
3392
|
const analysis = await getCachedAnalysis(absPath);
|
|
3220
3393
|
const score = await computeContextScore(
|
|
3221
3394
|
analysis,
|
|
@@ -3238,9 +3411,10 @@ async function handleScore(body, res) {
|
|
|
3238
3411
|
});
|
|
3239
3412
|
}
|
|
3240
3413
|
async function handleBenchmark(body, res) {
|
|
3241
|
-
const {
|
|
3242
|
-
|
|
3243
|
-
|
|
3414
|
+
const { task, budget } = body;
|
|
3415
|
+
const check = validateProjectPath(body.path);
|
|
3416
|
+
if (!check.valid) return error(res, 403, check.reason);
|
|
3417
|
+
const absPath = check.absPath;
|
|
3244
3418
|
const analysis = await getCachedAnalysis(absPath);
|
|
3245
3419
|
const result = await runBenchmark(
|
|
3246
3420
|
analysis,
|
|
@@ -3250,10 +3424,11 @@ async function handleBenchmark(body, res) {
|
|
|
3250
3424
|
json(res, 200, result);
|
|
3251
3425
|
}
|
|
3252
3426
|
async function handleQuality(body, res) {
|
|
3253
|
-
const {
|
|
3254
|
-
|
|
3427
|
+
const { task, budget } = body;
|
|
3428
|
+
const check = validateProjectPath(body.path);
|
|
3429
|
+
if (!check.valid) return error(res, 403, check.reason);
|
|
3255
3430
|
if (!task) return error(res, 400, "Missing required field: task");
|
|
3256
|
-
const absPath =
|
|
3431
|
+
const absPath = check.absPath;
|
|
3257
3432
|
const analysis = await getCachedAnalysis(absPath);
|
|
3258
3433
|
const result = await runQualityBenchmark(analysis, task, budget ?? 5e4);
|
|
3259
3434
|
json(res, 200, {
|
|
@@ -3267,9 +3442,10 @@ async function handleQuality(body, res) {
|
|
|
3267
3442
|
});
|
|
3268
3443
|
}
|
|
3269
3444
|
async function handlePRContext(body, res) {
|
|
3270
|
-
const {
|
|
3271
|
-
|
|
3272
|
-
|
|
3445
|
+
const { baseBranch, depth, includeTests } = body;
|
|
3446
|
+
const check = validateProjectPath(body.path);
|
|
3447
|
+
if (!check.valid) return error(res, 403, check.reason);
|
|
3448
|
+
const absPath = check.absPath;
|
|
3273
3449
|
const analysis = await getCachedAnalysis(absPath);
|
|
3274
3450
|
const pr = await generatePRContext(analysis, {
|
|
3275
3451
|
baseBranch: baseBranch ?? "main",
|
|
@@ -3288,10 +3464,11 @@ async function handlePRContext(body, res) {
|
|
|
3288
3464
|
});
|
|
3289
3465
|
}
|
|
3290
3466
|
async function handleInteract(body, res) {
|
|
3291
|
-
const {
|
|
3292
|
-
|
|
3467
|
+
const { task, budget, model } = body;
|
|
3468
|
+
const check = validateProjectPath(body.path);
|
|
3469
|
+
if (!check.valid) return error(res, 403, check.reason);
|
|
3293
3470
|
if (!task) return error(res, 400, "Missing required field: task");
|
|
3294
|
-
const absPath =
|
|
3471
|
+
const absPath = check.absPath;
|
|
3295
3472
|
const analysis = await getCachedAnalysis(absPath);
|
|
3296
3473
|
const plan = await planInteraction({
|
|
3297
3474
|
task,
|
|
@@ -3447,8 +3624,9 @@ function createAPIServer() {
|
|
|
3447
3624
|
const body = req.method === "GET" ? {} : await readBody(req);
|
|
3448
3625
|
await handler(body, res);
|
|
3449
3626
|
} catch (err) {
|
|
3450
|
-
|
|
3451
|
-
error(
|
|
3627
|
+
const message = err instanceof Error ? err.message : String(err);
|
|
3628
|
+
console.error(`[CTO API] Error on ${routeKey}:`, message);
|
|
3629
|
+
error(res, 500, message || "Internal server error");
|
|
3452
3630
|
}
|
|
3453
3631
|
});
|
|
3454
3632
|
return server2;
|