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.
@@ -1,8 +1,8 @@
1
1
  #!/usr/bin/env node
2
2
 
3
3
  // src/api/dashboard.ts
4
- import { resolve as resolve5, join as join5 } from "path";
5
- import { writeFile, mkdir, readFile as readFile5 } from "fs/promises";
4
+ import { resolve as resolve5, join as join6 } from "path";
5
+ import { writeFile as writeFile2, mkdir, readFile as readFile5 } from "fs/promises";
6
6
 
7
7
  // src/engine/analyzer.ts
8
8
  import { readFile as readFile2, readdir, stat as stat2 } from "fs/promises";
@@ -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,
@@ -1868,7 +1952,7 @@ function computeStrategyScore(strategy, budget) {
1868
1952
  // src/api/dashboard.ts
1869
1953
  async function loadHistory(ctoDir) {
1870
1954
  try {
1871
- const raw = await readFile5(join5(ctoDir, "history.json"), "utf-8");
1955
+ const raw = await readFile5(join6(ctoDir, "history.json"), "utf-8");
1872
1956
  return JSON.parse(raw);
1873
1957
  } catch {
1874
1958
  return [];
@@ -1876,11 +1960,11 @@ async function loadHistory(ctoDir) {
1876
1960
  }
1877
1961
  async function saveHistory(ctoDir, history) {
1878
1962
  await mkdir(ctoDir, { recursive: true });
1879
- await writeFile(join5(ctoDir, "history.json"), JSON.stringify(history, null, 2));
1963
+ await writeFile2(join6(ctoDir, "history.json"), JSON.stringify(history, null, 2));
1880
1964
  }
1881
1965
  async function generateDashboard(projectPath, task = "general code review and refactoring", budget = 5e4) {
1882
1966
  const absPath = resolve5(projectPath);
1883
- const ctoDir = join5(absPath, ".cto");
1967
+ const ctoDir = join6(absPath, ".cto");
1884
1968
  const analysis = await getCachedAnalysis(absPath);
1885
1969
  const score = await computeContextScore(analysis, task, budget);
1886
1970
  const benchmark = await runBenchmark(analysis, task, budget);
@@ -1910,9 +1994,9 @@ async function generateDashboard(projectPath, task = "general code review and re
1910
1994
  generatedAt: /* @__PURE__ */ new Date()
1911
1995
  };
1912
1996
  const html = renderDashboardHTML(data);
1913
- const htmlPath = join5(ctoDir, "dashboard.html");
1997
+ const htmlPath = join6(ctoDir, "dashboard.html");
1914
1998
  await mkdir(ctoDir, { recursive: true });
1915
- await writeFile(htmlPath, html, "utf-8");
1999
+ await writeFile2(htmlPath, html, "utf-8");
1916
2000
  return { htmlPath, data };
1917
2001
  }
1918
2002
  function generateBadgeURL(score) {