cto-ai-cli 3.2.0 → 5.0.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.
@@ -800,11 +800,13 @@ async function getCachedAnalysis(projectPath, config) {
800
800
  }
801
801
 
802
802
  // src/engine/selector.ts
803
- import { createHash as createHash3 } from "crypto";
803
+ import { createHash as createHash4 } from "crypto";
804
804
 
805
805
  // src/govern/secrets.ts
806
806
  import { readFile as readFile3 } from "fs/promises";
807
- import { resolve as resolve4, relative as relative4 } from "path";
807
+ import { readFileSync, existsSync as existsSync2, mkdirSync, writeFileSync } from "fs";
808
+ import { resolve as resolve4, relative as relative4, join as join4, dirname as dirname2 } from "path";
809
+ import { createHash as createHash3 } from "crypto";
808
810
  var BUILTIN_PATTERNS = [
809
811
  // API Keys
810
812
  { type: "api-key", source: `(?:api[_-]?key|apikey)\\s*[:=]\\s*['"]?([a-zA-Z0-9_\\-]{20,})['"]?`, flags: "gi", severity: "critical", description: "API Key" },
@@ -849,17 +851,46 @@ var BUILTIN_PATTERNS = [
849
851
  { type: "api-key", source: "SG\\.[a-zA-Z0-9_-]{22}\\.[a-zA-Z0-9_-]{43}", flags: "g", severity: "critical", description: "SendGrid API Key" },
850
852
  // JWT
851
853
  { type: "token", source: "eyJ[a-zA-Z0-9_-]{10,}\\.eyJ[a-zA-Z0-9_-]{10,}\\.[a-zA-Z0-9_-]{10,}", flags: "g", severity: "high", description: "JSON Web Token" },
854
+ // Datadog
855
+ { type: "api-key", source: `(?:DD_API_KEY|DATADOG_API_KEY)\\s*[:=]\\s*['"]?([a-f0-9]{32})['"]?`, flags: "gi", severity: "critical", description: "Datadog API Key" },
856
+ { type: "api-key", source: `(?:DD_APP_KEY|DATADOG_APP_KEY)\\s*[:=]\\s*['"]?([a-f0-9]{40})['"]?`, flags: "gi", severity: "critical", description: "Datadog App Key" },
857
+ // Sentry
858
+ { type: "connection-string", source: "https://[a-f0-9]{32}@[a-z0-9]+\\.ingest\\.sentry\\.io/[0-9]+", flags: "g", severity: "high", description: "Sentry DSN" },
859
+ // Firebase
860
+ { type: "api-key", source: `(?:FIREBASE_API_KEY|FIREBASE_KEY)\\s*[:=]\\s*['"]?([a-zA-Z0-9_\\-]{30,})['"]?`, flags: "gi", severity: "high", description: "Firebase API Key" },
861
+ { type: "connection-string", source: `firebase[a-z]*:\\/\\/[^\\s'"]+`, flags: "gi", severity: "high", description: "Firebase URL" },
862
+ // Supabase
863
+ { type: "api-key", source: "sbp_[a-f0-9]{40}", flags: "g", severity: "critical", description: "Supabase Service Key" },
864
+ { type: "token", source: "eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9\\.[a-zA-Z0-9_-]{20,}\\.[a-zA-Z0-9_-]{20,}", flags: "g", severity: "high", description: "Supabase Anon/Service JWT" },
865
+ // Vercel
866
+ { type: "token", source: `(?:VERCEL_TOKEN|VERCEL_API_TOKEN)\\s*[:=]\\s*['"]?([a-zA-Z0-9]{24,})['"]?`, flags: "gi", severity: "critical", description: "Vercel Token" },
867
+ // Heroku
868
+ { type: "api-key", source: `(?:HEROKU_API_KEY|HEROKU_TOKEN)\\s*[:=]\\s*['"]?([a-f0-9\\-]{36,})['"]?`, flags: "gi", severity: "critical", description: "Heroku API Key" },
869
+ // DigitalOcean
870
+ { type: "token", source: "dop_v1_[a-f0-9]{64}", flags: "g", severity: "critical", description: "DigitalOcean Personal Access Token" },
871
+ { type: "token", source: "doo_v1_[a-f0-9]{64}", flags: "g", severity: "critical", description: "DigitalOcean OAuth Token" },
872
+ // Mailgun
873
+ { type: "api-key", source: "key-[a-zA-Z0-9]{32}", flags: "g", severity: "high", description: "Mailgun API Key" },
852
874
  // PII
853
875
  { type: "pii", source: "\\b[A-Za-z0-9._%+-]+@[A-Za-z0-9.-]+\\.[A-Z|a-z]{2,}\\b", flags: "g", severity: "medium", description: "Email Address (PII)" },
854
- { type: "pii", source: "\\b\\d{3}[-.]?\\d{2}[-.]?\\d{4}\\b", flags: "g", severity: "high", description: "Possible SSN (PII)" }
876
+ { type: "pii", source: "\\b(?!000|666|9\\d{2})(\\d{3})[-.]?(?!00)(\\d{2})[-.]?(?!0000)(\\d{4})\\b", flags: "g", severity: "high", description: "Possible SSN (PII)" }
855
877
  ];
878
+ var _cachedBuiltinPatterns = null;
879
+ function getBuiltinPatterns() {
880
+ if (!_cachedBuiltinPatterns) {
881
+ _cachedBuiltinPatterns = BUILTIN_PATTERNS.map((def) => ({
882
+ type: def.type,
883
+ pattern: new RegExp(def.source, def.flags),
884
+ severity: def.severity,
885
+ description: def.description
886
+ }));
887
+ }
888
+ return _cachedBuiltinPatterns;
889
+ }
856
890
  function buildPatterns(customPatterns = []) {
857
- const patterns = BUILTIN_PATTERNS.map((def) => ({
858
- type: def.type,
859
- pattern: new RegExp(def.source, def.flags),
860
- severity: def.severity,
861
- description: def.description
862
- }));
891
+ const builtins = getBuiltinPatterns();
892
+ if (customPatterns.length === 0) return builtins;
893
+ const patterns = [...builtins];
863
894
  for (const custom of customPatterns) {
864
895
  try {
865
896
  patterns.push({
@@ -873,7 +904,7 @@ function buildPatterns(customPatterns = []) {
873
904
  }
874
905
  return patterns;
875
906
  }
876
- function scanContentForSecrets(content, filePath, customPatterns = []) {
907
+ function scanContentForSecrets(content, filePath, customPatterns = [], extraPiiSafeDomains) {
877
908
  const findings = [];
878
909
  const lines = content.split("\n");
879
910
  const allPatterns = buildPatterns(customPatterns);
@@ -885,6 +916,7 @@ function scanContentForSecrets(content, filePath, customPatterns = []) {
885
916
  while ((match = secretPattern.pattern.exec(line)) !== null) {
886
917
  const matchText = match[0];
887
918
  if (isTemplateOrPlaceholder(matchText)) continue;
919
+ if (secretPattern.type === "pii" && isSafeEmail(matchText, extraPiiSafeDomains)) continue;
888
920
  findings.push({
889
921
  type: secretPattern.type,
890
922
  file: filePath,
@@ -932,6 +964,36 @@ function isTemplateOrPlaceholder(value) {
932
964
  ];
933
965
  return placeholders.some((p) => p.test(value));
934
966
  }
967
+ var PII_SAFE_EMAIL_DOMAINS = /* @__PURE__ */ new Set([
968
+ "example.com",
969
+ "example.org",
970
+ "example.net",
971
+ "test.com",
972
+ "test.org",
973
+ "test.net",
974
+ "localhost",
975
+ "localhost.localdomain",
976
+ "email.com",
977
+ "mail.com",
978
+ "foo.com",
979
+ "bar.com",
980
+ "baz.com",
981
+ "acme.com",
982
+ "company.com",
983
+ "corp.com",
984
+ "noreply.com",
985
+ "no-reply.com",
986
+ "users.noreply.github.com",
987
+ "placeholder.com"
988
+ ]);
989
+ function isSafeEmail(value, extraDomains) {
990
+ const match = value.match(/@([a-zA-Z0-9.-]+)$/);
991
+ if (!match) return false;
992
+ const domain = match[1].toLowerCase();
993
+ if (PII_SAFE_EMAIL_DOMAINS.has(domain)) return true;
994
+ if (extraDomains && extraDomains.has(domain)) return true;
995
+ return false;
996
+ }
935
997
  function deduplicateFindings(findings) {
936
998
  const seen = /* @__PURE__ */ new Set();
937
999
  return findings.filter((f) => {
@@ -943,10 +1005,7 @@ function deduplicateFindings(findings) {
943
1005
  }
944
1006
 
945
1007
  // src/engine/pruner.ts
946
- import { Project as Project2, SyntaxKind as SyntaxKind2 } from "ts-morph";
947
1008
  import { readFile as readFile4 } from "fs/promises";
948
- import { existsSync as existsSync2 } from "fs";
949
- import { join as join4 } from "path";
950
1009
  var TS_EXTENSIONS2 = /* @__PURE__ */ new Set(["ts", "tsx", "js", "jsx", "mts", "mjs"]);
951
1010
  async function pruneFile(file, level) {
952
1011
  if (level === "excluded") {
@@ -969,23 +1028,7 @@ async function pruneTypeScript(file, level) {
969
1028
  } catch {
970
1029
  return emptyResult(file, level);
971
1030
  }
972
- let project;
973
- try {
974
- const tsConfigPath = findTsConfig(file.path);
975
- project = new Project2({
976
- tsConfigFilePath: tsConfigPath,
977
- skipAddingFilesFromTsConfig: true,
978
- compilerOptions: tsConfigPath ? void 0 : { allowJs: true, esModuleInterop: true }
979
- });
980
- project.createSourceFile(file.path, content, { overwrite: true });
981
- } catch {
982
- return pruneGenericFromContent(file, content, level);
983
- }
984
- const sourceFile = project.getSourceFiles()[0];
985
- if (!sourceFile) {
986
- return pruneGenericFromContent(file, content, level);
987
- }
988
- const prunedContent = level === "signatures" ? extractSignaturesAST(sourceFile) : extractSkeletonAST(sourceFile);
1031
+ const prunedContent = level === "signatures" ? extractSignaturesRegex(content) : extractSkeletonRegex(content);
989
1032
  const prunedTokens = countTokensChars4(Buffer.byteLength(prunedContent, "utf-8"));
990
1033
  const savingsPercent = file.tokens > 0 ? (file.tokens - prunedTokens) / file.tokens * 100 : 0;
991
1034
  return {
@@ -997,131 +1040,281 @@ async function pruneTypeScript(file, level) {
997
1040
  savingsPercent: Math.max(0, savingsPercent)
998
1041
  };
999
1042
  }
1000
- function extractSignaturesAST(sf) {
1043
+ function extractSignaturesRegex(content) {
1044
+ const lines = content.split("\n");
1001
1045
  const parts = [];
1002
- for (const imp of sf.getImportDeclarations()) {
1003
- parts.push(imp.getText());
1004
- }
1005
- if (parts.length > 0) parts.push("");
1006
- for (const ta of sf.getTypeAliases()) {
1007
- addJSDoc(ta, parts);
1008
- parts.push(ta.getText());
1009
- }
1010
- for (const iface of sf.getInterfaces()) {
1011
- addJSDoc(iface, parts);
1012
- parts.push(iface.getText());
1013
- }
1014
- for (const en of sf.getEnums()) {
1015
- addJSDoc(en, parts);
1016
- parts.push(en.getText());
1017
- }
1018
- for (const fn of sf.getFunctions()) {
1019
- addJSDoc(fn, parts);
1020
- const isExported = fn.isExported();
1021
- const isAsync = fn.isAsync();
1022
- const name = fn.getName() ?? "<anonymous>";
1023
- const params = fn.getParameters().map((p) => p.getText()).join(", ");
1024
- const returnType = fn.getReturnTypeNode()?.getText();
1025
- const returnStr = returnType ? `: ${returnType}` : "";
1026
- const prefix = isExported ? "export " : "";
1027
- const asyncStr = isAsync ? "async " : "";
1028
- parts.push(`${prefix}${asyncStr}function ${name}(${params})${returnStr} { /* ... */ }`);
1029
- }
1030
- for (const stmt of sf.getVariableStatements()) {
1031
- for (const decl of stmt.getDeclarations()) {
1032
- const init = decl.getInitializer();
1033
- if (init && (init.getKind() === SyntaxKind2.ArrowFunction || init.getKind() === SyntaxKind2.FunctionExpression)) {
1034
- addJSDoc(stmt, parts);
1035
- const isExported = stmt.isExported();
1036
- const prefix = isExported ? "export " : "";
1037
- const kind = stmt.getDeclarationKind();
1038
- const name = decl.getName();
1039
- const typeNode = decl.getTypeNode()?.getText();
1040
- const typeStr = typeNode ? `: ${typeNode}` : "";
1041
- parts.push(`${prefix}${kind} ${name}${typeStr} = /* ... */;`);
1042
- } else {
1043
- addJSDoc(stmt, parts);
1044
- parts.push(stmt.getText());
1046
+ let i = 0;
1047
+ while (i < lines.length) {
1048
+ const line = lines[i];
1049
+ const trimmed = line.trim();
1050
+ if (trimmed === "") {
1051
+ i++;
1052
+ continue;
1053
+ }
1054
+ if (trimmed.startsWith("/**")) {
1055
+ const docLines = [];
1056
+ while (i < lines.length) {
1057
+ docLines.push(lines[i]);
1058
+ if (lines[i].includes("*/")) {
1059
+ i++;
1060
+ break;
1061
+ }
1062
+ i++;
1063
+ }
1064
+ parts.push(docLines.join("\n"));
1065
+ continue;
1066
+ }
1067
+ if (trimmed.startsWith("//")) {
1068
+ parts.push(line);
1069
+ i++;
1070
+ continue;
1071
+ }
1072
+ if (/^\s*(import|export)\s/.test(line) && (trimmed.includes(" from ") || trimmed.startsWith("import "))) {
1073
+ const block = collectBracedLine(lines, i);
1074
+ parts.push(block.text);
1075
+ i = block.nextIndex;
1076
+ continue;
1077
+ }
1078
+ if (/^\s*export\s*(\{|\*)/.test(trimmed)) {
1079
+ const block = collectBracedLine(lines, i);
1080
+ parts.push(block.text);
1081
+ i = block.nextIndex;
1082
+ continue;
1083
+ }
1084
+ if (/^\s*(export\s+)?type\s+\w/.test(trimmed) && !trimmed.startsWith("typeof")) {
1085
+ const block = collectBalanced(lines, i);
1086
+ parts.push(block.text);
1087
+ i = block.nextIndex;
1088
+ continue;
1089
+ }
1090
+ if (/^\s*(export\s+)?interface\s+\w/.test(trimmed)) {
1091
+ const block = collectBalanced(lines, i);
1092
+ parts.push(block.text);
1093
+ i = block.nextIndex;
1094
+ continue;
1095
+ }
1096
+ if (/^\s*(export\s+)?(const\s+)?enum\s+\w/.test(trimmed)) {
1097
+ const block = collectBalanced(lines, i);
1098
+ parts.push(block.text);
1099
+ i = block.nextIndex;
1100
+ continue;
1101
+ }
1102
+ const fnMatch = trimmed.match(/^(export\s+)?(async\s+)?function\s+(\w+)/);
1103
+ if (fnMatch) {
1104
+ const sig = extractFnSignature(lines, i);
1105
+ parts.push(`${sig} { /* ... */ }`);
1106
+ i = skipBlock(lines, i);
1107
+ continue;
1108
+ }
1109
+ const arrowMatch = trimmed.match(/^(export\s+)?(const|let|var)\s+(\w+)/);
1110
+ if (arrowMatch && looksLikeFunctionDecl(lines, i)) {
1111
+ const prefix = trimmed.match(/^((?:export\s+)?(?:const|let|var)\s+\w+[^=]*=)/)?.[1];
1112
+ if (prefix) {
1113
+ parts.push(`${prefix} /* ... */;`);
1045
1114
  }
1115
+ i = skipBlock(lines, i);
1116
+ continue;
1046
1117
  }
1047
- }
1048
- for (const cls of sf.getClasses()) {
1049
- addJSDoc(cls, parts);
1050
- const isExported = cls.isExported();
1051
- const prefix = isExported ? "export " : "";
1052
- const name = cls.getName() ?? "<anonymous>";
1053
- const ext = cls.getExtends()?.getText();
1054
- const impl = cls.getImplements().map((i) => i.getText()).join(", ");
1055
- let header = `${prefix}class ${name}`;
1056
- if (ext) header += ` extends ${ext}`;
1057
- if (impl) header += ` implements ${impl}`;
1058
- header += " {";
1059
- parts.push(header);
1060
- for (const prop of cls.getProperties()) {
1061
- parts.push(` ${prop.getText()}`);
1062
- }
1063
- const ctor = cls.getConstructors()[0];
1064
- if (ctor) {
1065
- const ctorParams = ctor.getParameters().map((p) => p.getText()).join(", ");
1066
- parts.push(` constructor(${ctorParams}) { /* ... */ }`);
1067
- }
1068
- for (const method of cls.getMethods()) {
1069
- const isStatic = method.isStatic();
1070
- const isAsync = method.isAsync();
1071
- const methodName = method.getName();
1072
- const methodParams = method.getParameters().map((p) => p.getText()).join(", ");
1073
- const returnType = method.getReturnTypeNode()?.getText();
1074
- const returnStr = returnType ? `: ${returnType}` : "";
1075
- const staticStr = isStatic ? "static " : "";
1076
- const asyncStr = isAsync ? "async " : "";
1077
- parts.push(` ${staticStr}${asyncStr}${methodName}(${methodParams})${returnStr} { /* ... */ }`);
1078
- }
1079
- parts.push("}");
1080
- }
1081
- for (const exp of sf.getExportDeclarations()) {
1082
- parts.push(exp.getText());
1083
- }
1084
- for (const exp of sf.getExportAssignments()) {
1085
- parts.push(exp.getText());
1118
+ if (arrowMatch) {
1119
+ const block = collectStatement(lines, i);
1120
+ parts.push(block.text);
1121
+ i = block.nextIndex;
1122
+ continue;
1123
+ }
1124
+ if (/^\s*(export\s+)?(abstract\s+)?class\s+\w/.test(trimmed)) {
1125
+ const classOutline = extractClassOutline(lines, i);
1126
+ parts.push(classOutline.text);
1127
+ i = classOutline.nextIndex;
1128
+ continue;
1129
+ }
1130
+ i++;
1086
1131
  }
1087
1132
  return parts.join("\n");
1088
1133
  }
1089
- function extractSkeletonAST(sf) {
1134
+ function extractSkeletonRegex(content) {
1135
+ const lines = content.split("\n");
1090
1136
  const parts = [];
1091
- for (const imp of sf.getImportDeclarations()) {
1092
- parts.push(imp.getText());
1093
- }
1094
- if (parts.length > 0) parts.push("");
1095
- for (const ta of sf.getTypeAliases()) {
1096
- if (ta.isExported()) parts.push(ta.getText());
1097
- }
1098
- for (const iface of sf.getInterfaces()) {
1099
- if (!iface.isExported()) continue;
1100
- const ext = iface.getExtends().map((e) => e.getText());
1101
- const extStr = ext.length > 0 ? ` extends ${ext.join(", ")}` : "";
1102
- parts.push(`export interface ${iface.getName()}${extStr} { /* ${iface.getProperties().length} props */ }`);
1103
- }
1104
- for (const en of sf.getEnums()) {
1105
- if (!en.isExported()) continue;
1106
- const members = en.getMembers().map((m) => m.getName());
1107
- parts.push(`export enum ${en.getName()} { ${members.join(", ")} }`);
1108
- }
1109
- for (const fn of sf.getFunctions()) {
1110
- if (!fn.isExported()) continue;
1111
- const name = fn.getName() ?? "<anonymous>";
1112
- const params = fn.getParameters().map((p) => p.getText()).join(", ");
1113
- parts.push(`export function ${name}(${params});`);
1114
- }
1115
- for (const cls of sf.getClasses()) {
1116
- if (!cls.isExported()) continue;
1117
- const methods = cls.getMethods().map((m) => m.getName());
1118
- parts.push(`export class ${cls.getName()} { /* methods: ${methods.join(", ")} */ }`);
1119
- }
1120
- for (const exp of sf.getExportDeclarations()) {
1121
- parts.push(exp.getText());
1137
+ let i = 0;
1138
+ while (i < lines.length) {
1139
+ const trimmed = lines[i].trim();
1140
+ if (/^import\s/.test(trimmed)) {
1141
+ const block = collectBracedLine(lines, i);
1142
+ parts.push(block.text);
1143
+ i = block.nextIndex;
1144
+ continue;
1145
+ }
1146
+ if (/^export\s+(type|interface)\s+\w/.test(trimmed)) {
1147
+ const block = collectBalanced(lines, i);
1148
+ parts.push(block.text);
1149
+ i = block.nextIndex;
1150
+ continue;
1151
+ }
1152
+ if (/^export\s+(const\s+)?enum\s+\w/.test(trimmed)) {
1153
+ const block = collectBalanced(lines, i);
1154
+ parts.push(block.text);
1155
+ i = block.nextIndex;
1156
+ continue;
1157
+ }
1158
+ if (/^export\s+(async\s+)?function\s+\w/.test(trimmed)) {
1159
+ const sig = extractFnSignature(lines, i);
1160
+ parts.push(`${sig};`);
1161
+ i = skipBlock(lines, i);
1162
+ continue;
1163
+ }
1164
+ if (/^export\s+(abstract\s+)?class\s+/.test(trimmed)) {
1165
+ const nameMatch = trimmed.match(/class\s+(\w+)/);
1166
+ const name = nameMatch?.[1] ?? "Unknown";
1167
+ const end = skipBlock(lines, i);
1168
+ const methods = [];
1169
+ for (let j = i + 1; j < end; j++) {
1170
+ const mt = lines[j].trim();
1171
+ const mm = mt.match(/^(?:static\s+)?(?:async\s+)?(\w+)\s*\(/);
1172
+ if (mm && mm[1] !== "constructor") methods.push(mm[1]);
1173
+ }
1174
+ parts.push(`export class ${name} { /* methods: ${methods.join(", ")} */ }`);
1175
+ i = end;
1176
+ continue;
1177
+ }
1178
+ if (/^export\s*(\{|\*)/.test(trimmed)) {
1179
+ const block = collectBracedLine(lines, i);
1180
+ parts.push(block.text);
1181
+ i = block.nextIndex;
1182
+ continue;
1183
+ }
1184
+ i++;
1122
1185
  }
1123
1186
  return parts.join("\n");
1124
1187
  }
1188
+ function collectBracedLine(lines, start) {
1189
+ let text = lines[start];
1190
+ let i = start + 1;
1191
+ while (i < lines.length && !text.includes(";") && !text.trimEnd().endsWith("'") && !text.trimEnd().endsWith('"')) {
1192
+ text += "\n" + lines[i];
1193
+ i++;
1194
+ }
1195
+ return { text, nextIndex: i };
1196
+ }
1197
+ function collectBalanced(lines, start) {
1198
+ let depth = 0;
1199
+ let text = "";
1200
+ let i = start;
1201
+ let started = false;
1202
+ while (i < lines.length) {
1203
+ const line = lines[i];
1204
+ text += (text ? "\n" : "") + line;
1205
+ for (const ch of line) {
1206
+ if (ch === "{" || ch === "(") {
1207
+ depth++;
1208
+ started = true;
1209
+ }
1210
+ if (ch === "}" || ch === ")") depth--;
1211
+ }
1212
+ i++;
1213
+ if (started && depth <= 0) break;
1214
+ if (!started && line.includes(";")) break;
1215
+ }
1216
+ return { text, nextIndex: i };
1217
+ }
1218
+ function collectStatement(lines, start) {
1219
+ let text = lines[start];
1220
+ let i = start + 1;
1221
+ if (text.includes(";")) return { text, nextIndex: i };
1222
+ let depth = 0;
1223
+ for (const ch of text) {
1224
+ if (ch === "{" || ch === "(" || ch === "[") depth++;
1225
+ if (ch === "}" || ch === ")" || ch === "]") depth--;
1226
+ }
1227
+ while (i < lines.length && depth > 0) {
1228
+ text += "\n" + lines[i];
1229
+ for (const ch of lines[i]) {
1230
+ if (ch === "{" || ch === "(" || ch === "[") depth++;
1231
+ if (ch === "}" || ch === ")" || ch === "]") depth--;
1232
+ }
1233
+ i++;
1234
+ }
1235
+ return { text, nextIndex: i };
1236
+ }
1237
+ function extractFnSignature(lines, start) {
1238
+ let sig = "";
1239
+ let i = start;
1240
+ while (i < lines.length) {
1241
+ const line = lines[i].trim();
1242
+ sig += (sig ? " " : "") + line;
1243
+ if (line.includes("{")) {
1244
+ sig = sig.replace(/\s*\{[^]*$/, "").trim();
1245
+ break;
1246
+ }
1247
+ i++;
1248
+ }
1249
+ return sig;
1250
+ }
1251
+ function skipBlock(lines, start) {
1252
+ let depth = 0;
1253
+ let i = start;
1254
+ let foundBrace = false;
1255
+ while (i < lines.length) {
1256
+ for (const ch of lines[i]) {
1257
+ if (ch === "{") {
1258
+ depth++;
1259
+ foundBrace = true;
1260
+ }
1261
+ if (ch === "}") depth--;
1262
+ }
1263
+ i++;
1264
+ if (foundBrace && depth <= 0) break;
1265
+ if (!foundBrace && lines[i - 1].includes(";")) break;
1266
+ }
1267
+ return i;
1268
+ }
1269
+ function looksLikeFunctionDecl(lines, start) {
1270
+ const chunk = lines.slice(start, Math.min(start + 5, lines.length)).join(" ");
1271
+ return /=>/.test(chunk) || /=\s*function/.test(chunk);
1272
+ }
1273
+ function extractClassOutline(lines, start) {
1274
+ const header = lines[start].trim();
1275
+ let headerText = header;
1276
+ let i = start + 1;
1277
+ if (!header.includes("{")) {
1278
+ while (i < lines.length) {
1279
+ headerText += " " + lines[i].trim();
1280
+ if (lines[i].includes("{")) {
1281
+ i++;
1282
+ break;
1283
+ }
1284
+ i++;
1285
+ }
1286
+ } else {
1287
+ i = start + 1;
1288
+ }
1289
+ const bodyParts = [headerText.replace(/\{[^]*$/, "{").trim()];
1290
+ let depth = 1;
1291
+ while (i < lines.length && depth > 0) {
1292
+ const line = lines[i];
1293
+ const trimmed = line.trim();
1294
+ for (const ch of line) {
1295
+ if (ch === "{") depth++;
1296
+ if (ch === "}") depth--;
1297
+ }
1298
+ if (depth <= 0) {
1299
+ i++;
1300
+ break;
1301
+ }
1302
+ if (depth === 1) {
1303
+ if (/^(private|protected|public|readonly|static|#)/.test(trimmed) && !trimmed.includes("(")) {
1304
+ bodyParts.push(` ${trimmed}`);
1305
+ } else if (/^constructor\s*\(/.test(trimmed)) {
1306
+ const sig = extractFnSignature(lines, i);
1307
+ bodyParts.push(` ${sig} { /* ... */ }`);
1308
+ } else if (/^(?:static\s+)?(?:async\s+)?(?:get\s+|set\s+)?\w+\s*[(<]/.test(trimmed) && !trimmed.startsWith("//")) {
1309
+ const sig = extractFnSignature(lines, i);
1310
+ bodyParts.push(` ${sig} { /* ... */ }`);
1311
+ }
1312
+ }
1313
+ i++;
1314
+ }
1315
+ bodyParts.push("}");
1316
+ return { text: bodyParts.join("\n"), nextIndex: i };
1317
+ }
1125
1318
  async function pruneGeneric(file, level) {
1126
1319
  let content;
1127
1320
  try {
@@ -1182,22 +1375,6 @@ function emptyResult(file, level) {
1182
1375
  savingsPercent: 100
1183
1376
  };
1184
1377
  }
1185
- function addJSDoc(node, parts) {
1186
- if (!node.getJsDocs) return;
1187
- const docs = node.getJsDocs();
1188
- if (docs.length > 0) {
1189
- parts.push(docs[0].getText());
1190
- }
1191
- }
1192
- function findTsConfig(filePath) {
1193
- let dir = filePath;
1194
- for (let i = 0; i < 10; i++) {
1195
- dir = join4(dir, "..");
1196
- const candidate = join4(dir, "tsconfig.json");
1197
- if (existsSync2(candidate)) return candidate;
1198
- }
1199
- return void 0;
1200
- }
1201
1378
 
1202
1379
  // src/engine/graph-utils.ts
1203
1380
  function buildAdjacencyList(edges) {
@@ -1461,7 +1638,7 @@ async function selectContext(input) {
1461
1638
  );
1462
1639
  const excludedRisk = excludedFiles.length > 0 ? Math.round(excludedFiles.reduce((s, f) => s + f.riskScore, 0) / excludedFiles.length) : 0;
1463
1640
  const hashInput = selectedFiles.map((f) => `${f.relativePath}:${f.pruneLevel}`).sort().join("|") + `|budget:${budget}`;
1464
- const hash = createHash3("sha256").update(hashInput).digest("hex").substring(0, 16);
1641
+ const hash = createHash4("sha256").update(hashInput).digest("hex").substring(0, 16);
1465
1642
  return {
1466
1643
  files: selectedFiles,
1467
1644
  totalTokens: usedTokens,
@@ -2858,7 +3035,7 @@ function formatCost(cost) {
2858
3035
  }
2859
3036
 
2860
3037
  // src/govern/audit.ts
2861
- import { randomUUID, createHash as createHash4 } from "crypto";
3038
+ import { randomUUID, createHash as createHash5 } from "crypto";
2862
3039
  import { readdir as readdir3, chmod } from "fs/promises";
2863
3040
  import { join as join5 } from "path";
2864
3041
  import { userInfo } from "os";
@@ -2882,7 +3059,7 @@ function computeIntegrityHash(entry) {
2882
3059
  projectPath: entry.projectPath,
2883
3060
  details: entry.details
2884
3061
  });
2885
- return createHash4("sha256").update(payload).digest("hex");
3062
+ return createHash5("sha256").update(payload).digest("hex");
2886
3063
  }
2887
3064
  async function ensureDir(dirPath) {
2888
3065
  const { mkdir } = await import("fs/promises");
@@ -2898,9 +3075,9 @@ async function readJSON(filePath) {
2898
3075
  }
2899
3076
  }
2900
3077
  async function writeJSON(filePath, data) {
2901
- const { writeFile } = await import("fs/promises");
3078
+ const { writeFile: writeFile2 } = await import("fs/promises");
2902
3079
  await ensureDir(join5(filePath, ".."));
2903
- await writeFile(filePath, JSON.stringify(data, null, 2), "utf-8");
3080
+ await writeFile2(filePath, JSON.stringify(data, null, 2), "utf-8");
2904
3081
  }
2905
3082
  async function logAudit(action, projectPath, details = {}) {
2906
3083
  const auditDir = getAuditDir();
@@ -3072,6 +3249,12 @@ function checkRateLimit(ip) {
3072
3249
  entry.count++;
3073
3250
  return true;
3074
3251
  }
3252
+ setInterval(() => {
3253
+ const now = Date.now();
3254
+ for (const [ip, entry] of requestCounts) {
3255
+ if (now > entry.reset) requestCounts.delete(ip);
3256
+ }
3257
+ }, 5 * 6e4).unref();
3075
3258
  function getIP(req) {
3076
3259
  return req.headers["x-forwarded-for"]?.split(",")[0]?.trim() ?? req.socket.remoteAddress ?? "unknown";
3077
3260
  }