cto-ai-cli 3.1.0 → 4.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" },
@@ -829,15 +831,66 @@ var BUILTIN_PATTERNS = [
829
831
  { type: "connection-string", source: `(?:mongodb(?:\\+srv)?|postgres(?:ql)?|mysql|redis|amqp):\\/\\/[^\\s'"]+:[^\\s'"]+@[^\\s'"]+`, flags: "gi", severity: "critical", description: "Database Connection String" },
830
832
  { type: "connection-string", source: `(?:DATABASE_URL|REDIS_URL|MONGODB_URI)\\s*[:=]\\s*['"]?([^\\s'"]{10,})['"]?`, flags: "gi", severity: "high", description: "Database URL" },
831
833
  // Environment variables with secrets
832
- { type: "env-variable", source: `(?:SECRET|PRIVATE|ENCRYPTION)[_-]?(?:KEY|TOKEN|PASS)\\s*[:=]\\s*['"]?([^\\s'"]{8,})['"]?`, flags: "gi", severity: "high", description: "Secret Environment Variable" }
834
+ { type: "env-variable", source: `(?:SECRET|PRIVATE|ENCRYPTION)[_-]?(?:KEY|TOKEN|PASS)\\s*[:=]\\s*['"]?([^\\s'"]{8,})['"]?`, flags: "gi", severity: "high", description: "Secret Environment Variable" },
835
+ // Stripe
836
+ { type: "api-key", source: "sk_live_[a-zA-Z0-9]{24,}", flags: "g", severity: "critical", description: "Stripe Live Secret Key" },
837
+ { type: "api-key", source: "pk_live_[a-zA-Z0-9]{24,}", flags: "g", severity: "high", description: "Stripe Live Publishable Key" },
838
+ { type: "api-key", source: "rk_live_[a-zA-Z0-9]{24,}", flags: "g", severity: "critical", description: "Stripe Restricted Key" },
839
+ // Slack
840
+ { type: "token", source: "xoxb-[0-9]{10,}-[0-9]{10,}-[a-zA-Z0-9]{24,}", flags: "g", severity: "critical", description: "Slack Bot Token" },
841
+ { type: "token", source: "xoxp-[0-9]{10,}-[0-9]{10,}-[a-zA-Z0-9]{24,}", flags: "g", severity: "critical", description: "Slack User Token" },
842
+ { type: "api-key", source: "https://hooks\\.slack\\.com/services/T[a-zA-Z0-9_]+/B[a-zA-Z0-9_]+/[a-zA-Z0-9_]+", flags: "g", severity: "high", description: "Slack Webhook URL" },
843
+ // Google
844
+ { type: "api-key", source: "AIza[0-9A-Za-z_-]{35}", flags: "g", severity: "high", description: "Google API Key" },
845
+ { type: "token", source: "ya29\\.[0-9A-Za-z_-]+", flags: "g", severity: "high", description: "Google OAuth Token" },
846
+ // Azure
847
+ { type: "api-key", source: "(?:AccountKey|SharedAccessKey)\\s*=\\s*[a-zA-Z0-9+/=]{40,}", flags: "g", severity: "critical", description: "Azure Storage Key" },
848
+ // Twilio
849
+ { type: "api-key", source: "AC[a-f0-9]{32}", flags: "g", severity: "high", description: "Twilio Account SID" },
850
+ // SendGrid
851
+ { type: "api-key", source: "SG\\.[a-zA-Z0-9_-]{22}\\.[a-zA-Z0-9_-]{43}", flags: "g", severity: "critical", description: "SendGrid API Key" },
852
+ // JWT
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" },
874
+ // PII
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)" },
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)" }
833
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
+ }
834
890
  function buildPatterns(customPatterns = []) {
835
- const patterns = BUILTIN_PATTERNS.map((def) => ({
836
- type: def.type,
837
- pattern: new RegExp(def.source, def.flags),
838
- severity: def.severity,
839
- description: def.description
840
- }));
891
+ const builtins = getBuiltinPatterns();
892
+ if (customPatterns.length === 0) return builtins;
893
+ const patterns = [...builtins];
841
894
  for (const custom of customPatterns) {
842
895
  try {
843
896
  patterns.push({
@@ -851,7 +904,7 @@ function buildPatterns(customPatterns = []) {
851
904
  }
852
905
  return patterns;
853
906
  }
854
- function scanContentForSecrets(content, filePath, customPatterns = []) {
907
+ function scanContentForSecrets(content, filePath, customPatterns = [], extraPiiSafeDomains) {
855
908
  const findings = [];
856
909
  const lines = content.split("\n");
857
910
  const allPatterns = buildPatterns(customPatterns);
@@ -863,6 +916,7 @@ function scanContentForSecrets(content, filePath, customPatterns = []) {
863
916
  while ((match = secretPattern.pattern.exec(line)) !== null) {
864
917
  const matchText = match[0];
865
918
  if (isTemplateOrPlaceholder(matchText)) continue;
919
+ if (secretPattern.type === "pii" && isSafeEmail(matchText, extraPiiSafeDomains)) continue;
866
920
  findings.push({
867
921
  type: secretPattern.type,
868
922
  file: filePath,
@@ -910,6 +964,36 @@ function isTemplateOrPlaceholder(value) {
910
964
  ];
911
965
  return placeholders.some((p) => p.test(value));
912
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
+ }
913
997
  function deduplicateFindings(findings) {
914
998
  const seen = /* @__PURE__ */ new Set();
915
999
  return findings.filter((f) => {
@@ -923,8 +1007,8 @@ function deduplicateFindings(findings) {
923
1007
  // src/engine/pruner.ts
924
1008
  import { Project as Project2, SyntaxKind as SyntaxKind2 } from "ts-morph";
925
1009
  import { readFile as readFile4 } from "fs/promises";
926
- import { existsSync as existsSync2 } from "fs";
927
- import { join as join4 } from "path";
1010
+ import { existsSync as existsSync3 } from "fs";
1011
+ import { join as join5 } from "path";
928
1012
  var TS_EXTENSIONS2 = /* @__PURE__ */ new Set(["ts", "tsx", "js", "jsx", "mts", "mjs"]);
929
1013
  async function pruneFile(file, level) {
930
1014
  if (level === "excluded") {
@@ -1170,9 +1254,9 @@ function addJSDoc(node, parts) {
1170
1254
  function findTsConfig(filePath) {
1171
1255
  let dir = filePath;
1172
1256
  for (let i = 0; i < 10; i++) {
1173
- dir = join4(dir, "..");
1174
- const candidate = join4(dir, "tsconfig.json");
1175
- if (existsSync2(candidate)) return candidate;
1257
+ dir = join5(dir, "..");
1258
+ const candidate = join5(dir, "tsconfig.json");
1259
+ if (existsSync3(candidate)) return candidate;
1176
1260
  }
1177
1261
  return void 0;
1178
1262
  }
@@ -1439,7 +1523,7 @@ async function selectContext(input) {
1439
1523
  );
1440
1524
  const excludedRisk = excludedFiles.length > 0 ? Math.round(excludedFiles.reduce((s, f) => s + f.riskScore, 0) / excludedFiles.length) : 0;
1441
1525
  const hashInput = selectedFiles.map((f) => `${f.relativePath}:${f.pruneLevel}`).sort().join("|") + `|budget:${budget}`;
1442
- const hash = createHash3("sha256").update(hashInput).digest("hex").substring(0, 16);
1526
+ const hash = createHash4("sha256").update(hashInput).digest("hex").substring(0, 16);
1443
1527
  return {
1444
1528
  files: selectedFiles,
1445
1529
  totalTokens: usedTokens,
@@ -2836,20 +2920,20 @@ function formatCost(cost) {
2836
2920
  }
2837
2921
 
2838
2922
  // src/govern/audit.ts
2839
- import { randomUUID, createHash as createHash4 } from "crypto";
2923
+ import { randomUUID, createHash as createHash5 } from "crypto";
2840
2924
  import { readdir as readdir3, chmod } from "fs/promises";
2841
- import { join as join5 } from "path";
2925
+ import { join as join6 } from "path";
2842
2926
  import { userInfo } from "os";
2843
2927
  import { homedir } from "os";
2844
2928
  var CTO_DIR = ".cto-ai";
2845
2929
  var AUDIT_DIR = "audit";
2846
2930
  var MAX_ENTRIES_PER_FILE = 500;
2847
2931
  function getAuditDir() {
2848
- return join5(homedir(), CTO_DIR, AUDIT_DIR);
2932
+ return join6(homedir(), CTO_DIR, AUDIT_DIR);
2849
2933
  }
2850
2934
  function getCurrentAuditFile() {
2851
2935
  const date = (/* @__PURE__ */ new Date()).toISOString().split("T")[0].replace(/-/g, "");
2852
- return join5(getAuditDir(), `audit_${date}.json`);
2936
+ return join6(getAuditDir(), `audit_${date}.json`);
2853
2937
  }
2854
2938
  function computeIntegrityHash(entry) {
2855
2939
  const payload = JSON.stringify({
@@ -2860,7 +2944,7 @@ function computeIntegrityHash(entry) {
2860
2944
  projectPath: entry.projectPath,
2861
2945
  details: entry.details
2862
2946
  });
2863
- return createHash4("sha256").update(payload).digest("hex");
2947
+ return createHash5("sha256").update(payload).digest("hex");
2864
2948
  }
2865
2949
  async function ensureDir(dirPath) {
2866
2950
  const { mkdir } = await import("fs/promises");
@@ -2876,9 +2960,9 @@ async function readJSON(filePath) {
2876
2960
  }
2877
2961
  }
2878
2962
  async function writeJSON(filePath, data) {
2879
- const { writeFile } = await import("fs/promises");
2880
- await ensureDir(join5(filePath, ".."));
2881
- await writeFile(filePath, JSON.stringify(data, null, 2), "utf-8");
2963
+ const { writeFile: writeFile2 } = await import("fs/promises");
2964
+ await ensureDir(join6(filePath, ".."));
2965
+ await writeFile2(filePath, JSON.stringify(data, null, 2), "utf-8");
2882
2966
  }
2883
2967
  async function logAudit(action, projectPath, details = {}) {
2884
2968
  const auditDir = getAuditDir();